@wrongstack/cli 0.297.0 → 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 (505) 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/governance-evidence-candidate.d.ts +3 -0
  4. package/dist/boot/governance-shadow-bridge.d.ts +19 -0
  5. package/dist/boot/governance-trace-context.d.ts +64 -0
  6. package/dist/boot/tui-session-resume.d.ts +10 -1
  7. package/dist/chimera-leader-wake.d.ts +57 -0
  8. package/dist/chimera-reviewer-policy.d.ts +89 -1
  9. package/dist/chimera-subagent-ladder.d.ts +72 -0
  10. package/dist/{chunk-AZESNNIC.js → chunk-4HH3YGG4.js} +3 -3
  11. package/dist/{chunk-IHBYBYGA.js → chunk-A6Q5KGLV.js} +51 -18
  12. package/dist/{chunk-ABXK4VVY.js → chunk-ARWGQUB3.js} +2 -1
  13. package/dist/{chunk-7JABKXET.js → chunk-C5GH4YBL.js} +1018 -175
  14. package/dist/{chunk-TADVX644.js → chunk-GO3TJICK.js} +11 -2
  15. package/dist/{chunk-JBTFWWKV.js → chunk-I2BM6FK6.js} +9 -4
  16. package/dist/{chunk-AXVZMEWG.js → chunk-I5MEUFDI.js} +2 -2
  17. package/dist/chunk-U3SS4NRH.js +10 -0
  18. package/dist/{chunk-QZKWRNIP.js → chunk-V3XH6XBJ.js} +12 -9
  19. package/dist/{cli-main-GOY5YGVL.js → cli-main-TJEU4OUY.js} +954 -268
  20. package/dist/cli-main-helpers.d.ts +42 -2
  21. package/dist/{execution-A4PGK4XV.js → execution-INOV5P7N.js} +1236 -652
  22. package/dist/execution-chimera-cascade.d.ts +7 -1
  23. package/dist/execution-chimera-review.d.ts +35 -0
  24. package/dist/fleet/host-types.d.ts +6 -1
  25. package/dist/fleet-statusline.d.ts +1 -0
  26. package/dist/governance-V2Z42RBE.js +65 -0
  27. package/dist/governance-hq-telemetry.d.ts +17 -0
  28. package/dist/{hq-GBPGXIR5.js → hq-WO6CLUSI.js} +2 -2
  29. package/dist/hq-server/auth.d.ts +33 -2
  30. package/dist/hq-server/login-attempt-store.d.ts +73 -0
  31. package/dist/hq-server/routes/auth-handlers.d.ts +73 -5
  32. package/dist/hq-server/routes.d.ts +5 -5
  33. package/dist/hq-server/types.d.ts +36 -2
  34. package/dist/hq-server/utils.d.ts +51 -6
  35. package/dist/{hq-server-YQUUMTPU.js → hq-server-6TSAORAC.js} +2 -2
  36. package/dist/hq-server.d.ts +10 -0
  37. package/dist/index.js +49 -21
  38. package/dist/live-settings-input.d.ts +1 -1
  39. package/dist/settings-menu.d.ts +1 -1
  40. package/dist/slash-commands/fallback.d.ts +2 -0
  41. package/dist/slash-commands/goal-refiner.d.ts +1 -1
  42. package/dist/subcommands/handlers/acp-connection-gate.d.ts +20 -0
  43. package/dist/subcommands/handlers/governance.d.ts +8 -0
  44. package/dist/{update-YC4TYW2Y.js → update-MV6TUACD.js} +3 -3
  45. package/dist/utils/win32-cmd.d.ts +9 -6
  46. package/dist/{version-help-DROHZRW6.js → version-help-ASKQYXDJ.js} +2 -2
  47. package/dist/{webui-server-XKFUOKMJ.js → webui-server-PCLSXX6L.js} +10 -3
  48. package/dist/wiring/brain-and-orchestration.d.ts +2 -0
  49. package/dist/wiring/provider-runtime-setup.d.ts +2 -2
  50. package/package.json +23 -22
  51. package/dist/acp-A5OSIU2P.js.map +0 -7
  52. package/dist/acp-registry-cache.d.ts.map +0 -1
  53. package/dist/acp-server-agent.d.ts.map +0 -1
  54. package/dist/arg-parser.d.ts.map +0 -1
  55. package/dist/audit-2ZSFGNIF.js.map +0 -7
  56. package/dist/auth-CJWQJVRH.js.map +0 -7
  57. package/dist/auth-menu/add-provider.d.ts.map +0 -1
  58. package/dist/auth-menu/anthropic-oauth.d.ts.map +0 -1
  59. package/dist/auth-menu/auth-menu-audit.d.ts.map +0 -1
  60. package/dist/auth-menu/direct.d.ts.map +0 -1
  61. package/dist/auth-menu/github-copilot-oauth.d.ts.map +0 -1
  62. package/dist/auth-menu/helpers.d.ts.map +0 -1
  63. package/dist/auth-menu/index.d.ts.map +0 -1
  64. package/dist/auth-menu/local-presets.d.ts.map +0 -1
  65. package/dist/auth-menu/local.d.ts.map +0 -1
  66. package/dist/auth-menu/loopback-server.d.ts.map +0 -1
  67. package/dist/auth-menu/oauth-menu.d.ts.map +0 -1
  68. package/dist/auth-menu/openai-codex-oauth.d.ts.map +0 -1
  69. package/dist/auth-menu/panel-service.d.ts.map +0 -1
  70. package/dist/auth-menu/provider-menu.d.ts.map +0 -1
  71. package/dist/auth-menu/shared.d.ts.map +0 -1
  72. package/dist/auth-menu/top-menu.d.ts.map +0 -1
  73. package/dist/auth-menu/types.d.ts.map +0 -1
  74. package/dist/auth-menu.d.ts.map +0 -1
  75. package/dist/bench-7ZDWSNEF.js.map +0 -7
  76. package/dist/boot/auto-discover-providers.d.ts.map +0 -1
  77. package/dist/boot/brain-decision-log.d.ts.map +0 -1
  78. package/dist/boot/config-backup-recovery.d.ts.map +0 -1
  79. package/dist/boot/container-wiring.d.ts.map +0 -1
  80. package/dist/boot/dispatch-singleshot.d.ts.map +0 -1
  81. package/dist/boot/dispatch-tui.d.ts.map +0 -1
  82. package/dist/boot/dispatch-webui.d.ts.map +0 -1
  83. package/dist/boot/event-wiring.d.ts.map +0 -1
  84. package/dist/boot/execution-mode.d.ts.map +0 -1
  85. package/dist/boot/launch-banner.d.ts.map +0 -1
  86. package/dist/boot/launch-menu.d.ts.map +0 -1
  87. package/dist/boot/short-circuit-desktop.d.ts.map +0 -1
  88. package/dist/boot/short-circuit-flags.d.ts.map +0 -1
  89. package/dist/boot/short-circuit-hq.d.ts.map +0 -1
  90. package/dist/boot/simpleui-full-auto.d.ts.map +0 -1
  91. package/dist/boot/system-prompt-builder.d.ts.map +0 -1
  92. package/dist/boot/system-prompt.d.ts.map +0 -1
  93. package/dist/boot/tool-registry.d.ts.map +0 -1
  94. package/dist/boot/tui-coordinator-callbacks.d.ts.map +0 -1
  95. package/dist/boot/tui-coordinator-setup.d.ts.map +0 -1
  96. package/dist/boot/tui-debug-stream.d.ts.map +0 -1
  97. package/dist/boot/tui-goal-wiring.d.ts.map +0 -1
  98. package/dist/boot/tui-live-sessions.d.ts.map +0 -1
  99. package/dist/boot/tui-project-picker-callback.d.ts.map +0 -1
  100. package/dist/boot/tui-project-spawn.d.ts.map +0 -1
  101. package/dist/boot/tui-project-switch.d.ts.map +0 -1
  102. package/dist/boot/tui-runtime-state.d.ts.map +0 -1
  103. package/dist/boot/tui-sdd-callback.d.ts.map +0 -1
  104. package/dist/boot/tui-session-resume.d.ts.map +0 -1
  105. package/dist/boot/tui-settings-adapter.d.ts.map +0 -1
  106. package/dist/boot-config.d.ts.map +0 -1
  107. package/dist/boot.d.ts.map +0 -1
  108. package/dist/brain-menu/panel-service.d.ts.map +0 -1
  109. package/dist/chimera-review-task.d.ts.map +0 -1
  110. package/dist/chimera-reviewer-policy.d.ts.map +0 -1
  111. package/dist/chronicle-QQEQFSSE.js.map +0 -7
  112. package/dist/chunk-5N34EWJR.js.map +0 -7
  113. package/dist/chunk-66T43Q4Y.js.map +0 -7
  114. package/dist/chunk-7JABKXET.js.map +0 -7
  115. package/dist/chunk-7OCVIDC7.js.map +0 -7
  116. package/dist/chunk-ABXK4VVY.js.map +0 -7
  117. package/dist/chunk-AXVZMEWG.js.map +0 -7
  118. package/dist/chunk-AZESNNIC.js.map +0 -7
  119. package/dist/chunk-BGFM3PD7.js.map +0 -7
  120. package/dist/chunk-DPJZQCVP.js.map +0 -7
  121. package/dist/chunk-F4PEGAJZ.js.map +0 -7
  122. package/dist/chunk-IHBYBYGA.js.map +0 -7
  123. package/dist/chunk-JBSQ7ONN.js.map +0 -7
  124. package/dist/chunk-JBTFWWKV.js.map +0 -7
  125. package/dist/chunk-JUHRI6OV.js.map +0 -7
  126. package/dist/chunk-JW75HY4F.js.map +0 -7
  127. package/dist/chunk-OP47B3IX.js.map +0 -7
  128. package/dist/chunk-PJHET45A.js.map +0 -7
  129. package/dist/chunk-Q5GTM25S.js.map +0 -7
  130. package/dist/chunk-QD544J2B.js.map +0 -7
  131. package/dist/chunk-QUNHS6O3.js.map +0 -7
  132. package/dist/chunk-QZKWRNIP.js.map +0 -7
  133. package/dist/chunk-TADVX644.js.map +0 -7
  134. package/dist/chunk-TGDHN4LM.js.map +0 -7
  135. package/dist/chunk-TYO2OVAD.js.map +0 -7
  136. package/dist/chunk-VCYZ3WZV.js.map +0 -7
  137. package/dist/chunk-VJUEZZYB.js +0 -29
  138. package/dist/chunk-VJUEZZYB.js.map +0 -7
  139. package/dist/chunk-WHOIR577.js.map +0 -7
  140. package/dist/chunk-XJXDOF63.js.map +0 -7
  141. package/dist/chunk-XTIXVJXP.js.map +0 -7
  142. package/dist/chunk-XWEGWYDU.js.map +0 -7
  143. package/dist/chunk-YWUPHRGB.js.map +0 -7
  144. package/dist/cli-bundled-prompts.d.ts.map +0 -1
  145. package/dist/cli-bundled-skills.d.ts.map +0 -1
  146. package/dist/cli-context.d.ts.map +0 -1
  147. package/dist/cli-entry-main.d.ts.map +0 -1
  148. package/dist/cli-entry-point.d.ts.map +0 -1
  149. package/dist/cli-eternal-flag.d.ts.map +0 -1
  150. package/dist/cli-main-GOY5YGVL.js.map +0 -7
  151. package/dist/cli-main-helpers.d.ts.map +0 -1
  152. package/dist/cli-main.d.ts.map +0 -1
  153. package/dist/cli-update-notice.d.ts.map +0 -1
  154. package/dist/config-doctor.d.ts.map +0 -1
  155. package/dist/config-history.d.ts.map +0 -1
  156. package/dist/context-limit.d.ts.map +0 -1
  157. package/dist/context-overflow-diagnostic.d.ts.map +0 -1
  158. package/dist/diag-doctor-OQ3AA63O.js.map +0 -7
  159. package/dist/diff-renderer.d.ts.map +0 -1
  160. package/dist/execute-deps.d.ts.map +0 -1
  161. package/dist/execution-A4PGK4XV.js.map +0 -7
  162. package/dist/execution-chimera-ask.d.ts.map +0 -1
  163. package/dist/execution-chimera-cascade.d.ts.map +0 -1
  164. package/dist/execution-cleanup.d.ts.map +0 -1
  165. package/dist/execution-kanban-dispatch.d.ts.map +0 -1
  166. package/dist/execution-repl-fleet-callbacks.d.ts.map +0 -1
  167. package/dist/execution-storage-observability.d.ts.map +0 -1
  168. package/dist/execution-tui-next-step-callbacks.d.ts.map +0 -1
  169. package/dist/execution.d.ts.map +0 -1
  170. package/dist/export-TPORYVRZ.js.map +0 -7
  171. package/dist/fleet/host-acp-runner-cache.d.ts.map +0 -1
  172. package/dist/fleet/host-acp.d.ts.map +0 -1
  173. package/dist/fleet/host-concurrency.d.ts.map +0 -1
  174. package/dist/fleet/host-context.d.ts.map +0 -1
  175. package/dist/fleet/host-director-builder.d.ts.map +0 -1
  176. package/dist/fleet/host-director-event-bridges.d.ts.map +0 -1
  177. package/dist/fleet/host-director-services.d.ts.map +0 -1
  178. package/dist/fleet/host-event-bridge.d.ts.map +0 -1
  179. package/dist/fleet/host-helpers.d.ts.map +0 -1
  180. package/dist/fleet/host-learning-tracker.d.ts.map +0 -1
  181. package/dist/fleet/host-learning.d.ts.map +0 -1
  182. package/dist/fleet/host-lifecycle-events.d.ts.map +0 -1
  183. package/dist/fleet/host-paths.d.ts.map +0 -1
  184. package/dist/fleet/host-provider.d.ts.map +0 -1
  185. package/dist/fleet/host-session-writer.d.ts.map +0 -1
  186. package/dist/fleet/host-shadow-pass.d.ts.map +0 -1
  187. package/dist/fleet/host-spawn-types.d.ts.map +0 -1
  188. package/dist/fleet/host-status.d.ts.map +0 -1
  189. package/dist/fleet/host-subagent-factory.d.ts.map +0 -1
  190. package/dist/fleet/host-supervisor.d.ts.map +0 -1
  191. package/dist/fleet/host-task-result-report.d.ts.map +0 -1
  192. package/dist/fleet/host-types.d.ts.map +0 -1
  193. package/dist/fleet/host.d.ts.map +0 -1
  194. package/dist/fleet/routing.d.ts.map +0 -1
  195. package/dist/fleet/status-broadcast.d.ts.map +0 -1
  196. package/dist/fleet/supervisor-registry.d.ts.map +0 -1
  197. package/dist/fleet-statusline.d.ts.map +0 -1
  198. package/dist/goal-host.d.ts.map +0 -1
  199. package/dist/hooks-wiring.d.ts.map +0 -1
  200. package/dist/hq-GBPGXIR5.js.map +0 -7
  201. package/dist/hq-command-controller.d.ts.map +0 -1
  202. package/dist/hq-publisher.d.ts.map +0 -1
  203. package/dist/hq-recovery-html.d.ts.map +0 -1
  204. package/dist/hq-server/audit-actor.d.ts.map +0 -1
  205. package/dist/hq-server/auth-state.d.ts.map +0 -1
  206. package/dist/hq-server/auth.d.ts.map +0 -1
  207. package/dist/hq-server/preflight.d.ts.map +0 -1
  208. package/dist/hq-server/project-root.d.ts.map +0 -1
  209. package/dist/hq-server/routes/auth-handlers.d.ts.map +0 -1
  210. package/dist/hq-server/routes/session-handlers.d.ts.map +0 -1
  211. package/dist/hq-server/routes/system-handlers.d.ts.map +0 -1
  212. package/dist/hq-server/routes.d.ts.map +0 -1
  213. package/dist/hq-server/snapshot.d.ts.map +0 -1
  214. package/dist/hq-server/startup.d.ts.map +0 -1
  215. package/dist/hq-server/trust-boundary.d.ts.map +0 -1
  216. package/dist/hq-server/types.d.ts.map +0 -1
  217. package/dist/hq-server/utils.d.ts.map +0 -1
  218. package/dist/hq-server/ws.d.ts.map +0 -1
  219. package/dist/hq-server-YQUUMTPU.js.map +0 -7
  220. package/dist/hq-server.d.ts.map +0 -1
  221. package/dist/hq-static-serve.d.ts.map +0 -1
  222. package/dist/hq-ttl-U2FAYFAT.js.map +0 -7
  223. package/dist/hq-tunnel-RFKGC4HI.js.map +0 -7
  224. package/dist/hq-tunnel.d.ts.map +0 -1
  225. package/dist/index.d.ts.map +0 -1
  226. package/dist/index.js.map +0 -7
  227. package/dist/init-E2NDDOHI.js.map +0 -7
  228. package/dist/input-reader.d.ts.map +0 -1
  229. package/dist/kanban-dispatch-route.d.ts.map +0 -1
  230. package/dist/kanban-hq-sync.d.ts.map +0 -1
  231. package/dist/kanban-run-mirror-EKVNGXWW.js.map +0 -7
  232. package/dist/launch-hints.d.ts.map +0 -1
  233. package/dist/live-settings-input.d.ts.map +0 -1
  234. package/dist/mailbox-bridge-bootstrap.d.ts.map +0 -1
  235. package/dist/mailbox-serve-DBOOOOWZ.js.map +0 -7
  236. package/dist/mcp-CYLPUEHC.js.map +0 -7
  237. package/dist/mcp-serve.d.ts.map +0 -1
  238. package/dist/modeldiag-HENYJ4N7.js.map +0 -7
  239. package/dist/multi-agent.d.ts.map +0 -1
  240. package/dist/next-task-predictor.d.ts.map +0 -1
  241. package/dist/permission-prompt.d.ts.map +0 -1
  242. package/dist/permissions-FNYLZIL4.js.map +0 -7
  243. package/dist/picker-key-state.d.ts.map +0 -1
  244. package/dist/picker-model-picker.d.ts.map +0 -1
  245. package/dist/picker-ui.d.ts.map +0 -1
  246. package/dist/picker.d.ts.map +0 -1
  247. package/dist/plugin-api-factory.d.ts.map +0 -1
  248. package/dist/plugin-management.d.ts.map +0 -1
  249. package/dist/plugin-usage-OU6X6LM3.js.map +0 -7
  250. package/dist/plugins-4LJ5IUBI.js.map +0 -7
  251. package/dist/pre-launch/launch-prompts.d.ts.map +0 -1
  252. package/dist/pre-launch/project-check.d.ts.map +0 -1
  253. package/dist/pre-launch.d.ts.map +0 -1
  254. package/dist/preflight.d.ts.map +0 -1
  255. package/dist/profile-config-path.d.ts.map +0 -1
  256. package/dist/project-BLQ3ALVL.js.map +0 -7
  257. package/dist/project-manifest-GE25PW4S.js.map +0 -7
  258. package/dist/project-picker-BOCWHBXP.js.map +0 -7
  259. package/dist/project-picker.d.ts.map +0 -1
  260. package/dist/projects-XE76NFNZ.js.map +0 -7
  261. package/dist/provider-config-utils.d.ts.map +0 -1
  262. package/dist/provider-helpers.d.ts.map +0 -1
  263. package/dist/provider-id.d.ts.map +0 -1
  264. package/dist/providers-models-GDELBHZK.js.map +0 -7
  265. package/dist/quick-VIO2BEHS.js.map +0 -7
  266. package/dist/renderer.d.ts.map +0 -1
  267. package/dist/repl-auto-proceed.d.ts.map +0 -1
  268. package/dist/repl-client-registration.d.ts.map +0 -1
  269. package/dist/repl-input.d.ts.map +0 -1
  270. package/dist/repl-options.d.ts.map +0 -1
  271. package/dist/repl-rendering.d.ts.map +0 -1
  272. package/dist/repl-suggestions.d.ts.map +0 -1
  273. package/dist/repl.d.ts.map +0 -1
  274. package/dist/replay-KCTXNMZQ.js.map +0 -7
  275. package/dist/rewind-EJMVFVLS.js.map +0 -7
  276. package/dist/sdd-runtime-AIJCJGWC.js.map +0 -7
  277. package/dist/services/autonomy-mode.d.ts.map +0 -1
  278. package/dist/services/commit-message.d.ts.map +0 -1
  279. package/dist/services/dispatch-classifier.d.ts.map +0 -1
  280. package/dist/services/mcp-management.d.ts.map +0 -1
  281. package/dist/services/project-facts.d.ts.map +0 -1
  282. package/dist/services/project-manifest.d.ts.map +0 -1
  283. package/dist/services/sdd/project-context.d.ts.map +0 -1
  284. package/dist/services/sdd/spec-detection.d.ts.map +0 -1
  285. package/dist/services/sdd/state.d.ts.map +0 -1
  286. package/dist/services/sdd/task-manager.d.ts.map +0 -1
  287. package/dist/services/sdd-runtime.d.ts.map +0 -1
  288. package/dist/services/statusline-config.d.ts.map +0 -1
  289. package/dist/services/suggestion-store.d.ts.map +0 -1
  290. package/dist/session-event-wiring.d.ts.map +0 -1
  291. package/dist/session-stats.d.ts.map +0 -1
  292. package/dist/sessions-config-NLCMK3BY.js.map +0 -7
  293. package/dist/settings-menu.d.ts.map +0 -1
  294. package/dist/shutdown-cleanup.d.ts.map +0 -1
  295. package/dist/simpleui-dist-JIFGUJO7.js.map +0 -7
  296. package/dist/simpleui-dist.d.ts.map +0 -1
  297. package/dist/slash-commands/acp.d.ts.map +0 -1
  298. package/dist/slash-commands/agent-improve.d.ts.map +0 -1
  299. package/dist/slash-commands/agents.d.ts.map +0 -1
  300. package/dist/slash-commands/audit.d.ts.map +0 -1
  301. package/dist/slash-commands/auth.d.ts.map +0 -1
  302. package/dist/slash-commands/autonomy.d.ts.map +0 -1
  303. package/dist/slash-commands/brain.d.ts.map +0 -1
  304. package/dist/slash-commands/btw.d.ts.map +0 -1
  305. package/dist/slash-commands/clear.d.ts.map +0 -1
  306. package/dist/slash-commands/codebase-reindex.d.ts.map +0 -1
  307. package/dist/slash-commands/collab.d.ts.map +0 -1
  308. package/dist/slash-commands/command-context.d.ts.map +0 -1
  309. package/dist/slash-commands/commit-llm.d.ts.map +0 -1
  310. package/dist/slash-commands/compact.d.ts.map +0 -1
  311. package/dist/slash-commands/context.d.ts.map +0 -1
  312. package/dist/slash-commands/coordinator.d.ts.map +0 -1
  313. package/dist/slash-commands/delegate.d.ts.map +0 -1
  314. package/dist/slash-commands/design.d.ts.map +0 -1
  315. package/dist/slash-commands/dev.d.ts.map +0 -1
  316. package/dist/slash-commands/diag-stats.d.ts.map +0 -1
  317. package/dist/slash-commands/dispatch-llm.d.ts.map +0 -1
  318. package/dist/slash-commands/doctor.d.ts.map +0 -1
  319. package/dist/slash-commands/enhance.d.ts.map +0 -1
  320. package/dist/slash-commands/ensemble.d.ts.map +0 -1
  321. package/dist/slash-commands/f-keys.d.ts.map +0 -1
  322. package/dist/slash-commands/fallback.d.ts.map +0 -1
  323. package/dist/slash-commands/fix-classifier.d.ts.map +0 -1
  324. package/dist/slash-commands/fix.d.ts.map +0 -1
  325. package/dist/slash-commands/fleet.d.ts.map +0 -1
  326. package/dist/slash-commands/git.d.ts.map +0 -1
  327. package/dist/slash-commands/gitid.d.ts.map +0 -1
  328. package/dist/slash-commands/goal-refiner.d.ts.map +0 -1
  329. package/dist/slash-commands/goal.d.ts.map +0 -1
  330. package/dist/slash-commands/health.d.ts.map +0 -1
  331. package/dist/slash-commands/help.d.ts.map +0 -1
  332. package/dist/slash-commands/helpers.d.ts.map +0 -1
  333. package/dist/slash-commands/hq.d.ts.map +0 -1
  334. package/dist/slash-commands/index.d.ts.map +0 -1
  335. package/dist/slash-commands/init.d.ts.map +0 -1
  336. package/dist/slash-commands/interrupt.d.ts.map +0 -1
  337. package/dist/slash-commands/kanban-agent-helpers.d.ts.map +0 -1
  338. package/dist/slash-commands/kanban-format.d.ts.map +0 -1
  339. package/dist/slash-commands/kanban-help.d.ts.map +0 -1
  340. package/dist/slash-commands/kanban.d.ts.map +0 -1
  341. package/dist/slash-commands/mailbox-demo.d.ts.map +0 -1
  342. package/dist/slash-commands/mailbox-serve.d.ts.map +0 -1
  343. package/dist/slash-commands/mailbox.d.ts.map +0 -1
  344. package/dist/slash-commands/mcp-utils.d.ts.map +0 -1
  345. package/dist/slash-commands/mcp.d.ts.map +0 -1
  346. package/dist/slash-commands/memory-compact.d.ts.map +0 -1
  347. package/dist/slash-commands/memory-flags.d.ts.map +0 -1
  348. package/dist/slash-commands/memory-formatters.d.ts.map +0 -1
  349. package/dist/slash-commands/memory.d.ts.map +0 -1
  350. package/dist/slash-commands/metrics.d.ts.map +0 -1
  351. package/dist/slash-commands/mode.d.ts.map +0 -1
  352. package/dist/slash-commands/modelcaps.d.ts.map +0 -1
  353. package/dist/slash-commands/models.d.ts.map +0 -1
  354. package/dist/slash-commands/mouse.d.ts.map +0 -1
  355. package/dist/slash-commands/next.d.ts.map +0 -1
  356. package/dist/slash-commands/plan.d.ts.map +0 -1
  357. package/dist/slash-commands/plugin.d.ts.map +0 -1
  358. package/dist/slash-commands/profile.d.ts.map +0 -1
  359. package/dist/slash-commands/project-utils.d.ts.map +0 -1
  360. package/dist/slash-commands/project.d.ts.map +0 -1
  361. package/dist/slash-commands/provider-status.d.ts.map +0 -1
  362. package/dist/slash-commands/prune.d.ts.map +0 -1
  363. package/dist/slash-commands/refiner.d.ts.map +0 -1
  364. package/dist/slash-commands/review.d.ts.map +0 -1
  365. package/dist/slash-commands/sdd/rendering.d.ts.map +0 -1
  366. package/dist/slash-commands/sdd-command-helpers.d.ts.map +0 -1
  367. package/dist/slash-commands/sdd-format.d.ts.map +0 -1
  368. package/dist/slash-commands/sdd.d.ts.map +0 -1
  369. package/dist/slash-commands/security.d.ts.map +0 -1
  370. package/dist/slash-commands/session.d.ts.map +0 -1
  371. package/dist/slash-commands/setmodel.d.ts.map +0 -1
  372. package/dist/slash-commands/settings.d.ts.map +0 -1
  373. package/dist/slash-commands/shadow.d.ts.map +0 -1
  374. package/dist/slash-commands/slash-deep-help.d.ts.map +0 -1
  375. package/dist/slash-commands/spawn-agents.d.ts.map +0 -1
  376. package/dist/slash-commands/statusline.d.ts.map +0 -1
  377. package/dist/slash-commands/suggest.d.ts.map +0 -1
  378. package/dist/slash-commands/suggestion-store.d.ts.map +0 -1
  379. package/dist/slash-commands/supervisor.d.ts.map +0 -1
  380. package/dist/slash-commands/surfaces.d.ts.map +0 -1
  381. package/dist/slash-commands/tasks.d.ts.map +0 -1
  382. package/dist/slash-commands/techstack.d.ts.map +0 -1
  383. package/dist/slash-commands/telegram-pairing.d.ts.map +0 -1
  384. package/dist/slash-commands/telegram-settings.d.ts.map +0 -1
  385. package/dist/slash-commands/telegram-setup.d.ts.map +0 -1
  386. package/dist/slash-commands/todos.d.ts.map +0 -1
  387. package/dist/slash-commands/tool.d.ts.map +0 -1
  388. package/dist/slash-commands/tools.d.ts.map +0 -1
  389. package/dist/slash-commands/tuneup.d.ts.map +0 -1
  390. package/dist/slash-commands/working-dir.d.ts.map +0 -1
  391. package/dist/slash-commands/worktree.d.ts.map +0 -1
  392. package/dist/slash-commands/yolo.d.ts.map +0 -1
  393. package/dist/spinner.d.ts.map +0 -1
  394. package/dist/subcommands/contracts.d.ts.map +0 -1
  395. package/dist/subcommands/handlers/acp.d.ts.map +0 -1
  396. package/dist/subcommands/handlers/audit.d.ts.map +0 -1
  397. package/dist/subcommands/handlers/auth-local-help.d.ts.map +0 -1
  398. package/dist/subcommands/handlers/auth.d.ts.map +0 -1
  399. package/dist/subcommands/handlers/bench-run-help.d.ts.map +0 -1
  400. package/dist/subcommands/handlers/bench.d.ts.map +0 -1
  401. package/dist/subcommands/handlers/chronicle.d.ts.map +0 -1
  402. package/dist/subcommands/handlers/config-history.d.ts.map +0 -1
  403. package/dist/subcommands/handlers/diag-doctor.d.ts.map +0 -1
  404. package/dist/subcommands/handlers/export.d.ts.map +0 -1
  405. package/dist/subcommands/handlers/helpers.d.ts.map +0 -1
  406. package/dist/subcommands/handlers/hq.d.ts.map +0 -1
  407. package/dist/subcommands/handlers/init.d.ts.map +0 -1
  408. package/dist/subcommands/handlers/mailbox-serve.d.ts.map +0 -1
  409. package/dist/subcommands/handlers/mcp.d.ts.map +0 -1
  410. package/dist/subcommands/handlers/model-smoke-test.d.ts.map +0 -1
  411. package/dist/subcommands/handlers/modeldiag-profiles.d.ts.map +0 -1
  412. package/dist/subcommands/handlers/modeldiag.d.ts.map +0 -1
  413. package/dist/subcommands/handlers/models-add-help.d.ts.map +0 -1
  414. package/dist/subcommands/handlers/per-subcommand-deep-help-table.d.ts.map +0 -1
  415. package/dist/subcommands/handlers/per-subcommand-help-data.d.ts.map +0 -1
  416. package/dist/subcommands/handlers/per-subcommand-help-table.d.ts.map +0 -1
  417. package/dist/subcommands/handlers/per-subcommand-help-types.d.ts.map +0 -1
  418. package/dist/subcommands/handlers/per-subcommand-help.d.ts.map +0 -1
  419. package/dist/subcommands/handlers/permissions.d.ts.map +0 -1
  420. package/dist/subcommands/handlers/plugin-usage.d.ts.map +0 -1
  421. package/dist/subcommands/handlers/project.d.ts.map +0 -1
  422. package/dist/subcommands/handlers/projects.d.ts.map +0 -1
  423. package/dist/subcommands/handlers/providers-models.d.ts.map +0 -1
  424. package/dist/subcommands/handlers/quick.d.ts.map +0 -1
  425. package/dist/subcommands/handlers/replay.d.ts.map +0 -1
  426. package/dist/subcommands/handlers/rewind.d.ts.map +0 -1
  427. package/dist/subcommands/handlers/sessions-config.d.ts.map +0 -1
  428. package/dist/subcommands/handlers/sessions-fleet.d.ts.map +0 -1
  429. package/dist/subcommands/handlers/tools-skills.d.ts.map +0 -1
  430. package/dist/subcommands/handlers/update.d.ts.map +0 -1
  431. package/dist/subcommands/handlers/version-help.d.ts.map +0 -1
  432. package/dist/subcommands/index.d.ts.map +0 -1
  433. package/dist/theme.d.ts.map +0 -1
  434. package/dist/tools-skills-UNKLOB7M.js.map +0 -7
  435. package/dist/tui-thinking-word.d.ts.map +0 -1
  436. package/dist/tuneup.d.ts.map +0 -1
  437. package/dist/update-YC4TYW2Y.js.map +0 -7
  438. package/dist/update-check-UMYTOH7Z.js.map +0 -7
  439. package/dist/update-check.d.ts.map +0 -1
  440. package/dist/utils/delay-format.d.ts.map +0 -1
  441. package/dist/utils/hq-ttl.d.ts.map +0 -1
  442. package/dist/utils/win32-cmd.d.ts.map +0 -1
  443. package/dist/utils.d.ts.map +0 -1
  444. package/dist/version-help-DROHZRW6.js.map +0 -7
  445. package/dist/version.d.ts.map +0 -1
  446. package/dist/webui-server/client-registration.d.ts.map +0 -1
  447. package/dist/webui-server/connection-handler.d.ts.map +0 -1
  448. package/dist/webui-server/context-breakdown.d.ts.map +0 -1
  449. package/dist/webui-server/contracts.d.ts.map +0 -1
  450. package/dist/webui-server/cost-helpers.d.ts.map +0 -1
  451. package/dist/webui-server/kanban-host-adapter.d.ts.map +0 -1
  452. package/dist/webui-server/kanban-run-mirror.d.ts.map +0 -1
  453. package/dist/webui-server/kanban-supervisor.d.ts.map +0 -1
  454. package/dist/webui-server/lifecycle.d.ts.map +0 -1
  455. package/dist/webui-server/listen-helpers.d.ts.map +0 -1
  456. package/dist/webui-server/logger-shim.d.ts.map +0 -1
  457. package/dist/webui-server/node-pty-loader.d.ts.map +0 -1
  458. package/dist/webui-server/prefs-seeding.d.ts.map +0 -1
  459. package/dist/webui-server/privileged-actions.d.ts.map +0 -1
  460. package/dist/webui-server/provider-config.d.ts.map +0 -1
  461. package/dist/webui-server/session-start-payload.d.ts.map +0 -1
  462. package/dist/webui-server/setup-events.d.ts.map +0 -1
  463. package/dist/webui-server/static-serve.d.ts.map +0 -1
  464. package/dist/webui-server/stream-coalescer.d.ts.map +0 -1
  465. package/dist/webui-server/terminal-log-view.d.ts.map +0 -1
  466. package/dist/webui-server-XKFUOKMJ.js.map +0 -7
  467. package/dist/webui-server-options.d.ts.map +0 -1
  468. package/dist/webui-server.d.ts.map +0 -1
  469. package/dist/wiring/brain-and-orchestration.d.ts.map +0 -1
  470. package/dist/wiring/codebase-index.d.ts.map +0 -1
  471. package/dist/wiring/command-host-adapters.d.ts.map +0 -1
  472. package/dist/wiring/command-host-state.d.ts.map +0 -1
  473. package/dist/wiring/controllers.d.ts.map +0 -1
  474. package/dist/wiring/dep-watcher-bridge.d.ts.map +0 -1
  475. package/dist/wiring/dep-watcher.d.ts.map +0 -1
  476. package/dist/wiring/design-studio.d.ts.map +0 -1
  477. package/dist/wiring/director-setup.d.ts.map +0 -1
  478. package/dist/wiring/eternal-command-handlers.d.ts.map +0 -1
  479. package/dist/wiring/fleet-command-handlers.d.ts.map +0 -1
  480. package/dist/wiring/hq-telemetry.d.ts.map +0 -1
  481. package/dist/wiring/lifecycle-plugins.d.ts.map +0 -1
  482. package/dist/wiring/mailbox-bridge-bootstrap.d.ts.map +0 -1
  483. package/dist/wiring/management-tools.d.ts.map +0 -1
  484. package/dist/wiring/metrics.d.ts.map +0 -1
  485. package/dist/wiring/pipeline.d.ts.map +0 -1
  486. package/dist/wiring/plugins.d.ts.map +0 -1
  487. package/dist/wiring/provider-runtime-setup.d.ts.map +0 -1
  488. package/dist/wiring/provider-runtime.d.ts.map +0 -1
  489. package/dist/wiring/provider-status.d.ts.map +0 -1
  490. package/dist/wiring/provider-utility-tools.d.ts.map +0 -1
  491. package/dist/wiring/provider.d.ts.map +0 -1
  492. package/dist/wiring/replay.d.ts.map +0 -1
  493. package/dist/wiring/runtime-controller-deps.d.ts.map +0 -1
  494. package/dist/wiring/runtime-dispatch-state.d.ts.map +0 -1
  495. package/dist/wiring/runtime-lifecycle-deps.d.ts.map +0 -1
  496. package/dist/wiring/runtime-picker-deps.d.ts.map +0 -1
  497. package/dist/wiring/sage.d.ts.map +0 -1
  498. package/dist/wiring/sdd-handlers.d.ts.map +0 -1
  499. package/dist/wiring/security-command.d.ts.map +0 -1
  500. package/dist/wiring/session-command-handlers.d.ts.map +0 -1
  501. package/dist/wiring/session-registry.d.ts.map +0 -1
  502. package/dist/wiring/session.d.ts.map +0 -1
  503. package/dist/wiring/slash-commands.d.ts.map +0 -1
  504. package/dist/wiring/to-execute-deps.d.ts.map +0 -1
  505. 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
22
  HqBootstrapCodeStore,
22
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) }));
2243
2981
  }
2244
2982
  }
2245
- async function handleMailboxAction(_req, res, match, _mutableAuth, _sessions, dataDir, getMailboxGateway) {
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");
2989
+ }
2990
+ return !hqAuthRequired2(mutableAuth);
2991
+ }
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);
@@ -3050,6 +3815,17 @@ function handleClient(ws, clients, browsers, eventLog, auth, snapshotBroadcaster
3050
3815
  }
3051
3816
  return;
3052
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
+ }
3053
3829
  if (event.type === "session.transcript" && client !== void 0) {
3054
3830
  const result = parseHqEventPayload(event.type, event.payload);
3055
3831
  if (result.ok) {
@@ -3154,7 +3930,7 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
3154
3930
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
3155
3931
  })
3156
3932
  );
3157
- return new Promise((resolve, reject) => {
3933
+ return new Promise((resolve2, reject) => {
3158
3934
  const trustedPublicOrigins = /* @__PURE__ */ new Set();
3159
3935
  let listeningPort = port;
3160
3936
  const clients = /* @__PURE__ */ new Map();
@@ -3173,31 +3949,45 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
3173
3949
  12e4
3174
3950
  );
3175
3951
  mailboxGatewayRateLimitCleanup.unref?.();
3176
- const loginAttempts = /* @__PURE__ */ new Map();
3177
- const LOGIN_ATTEMPT_RETENTION_MS = 15 * 6e4;
3952
+ const loginAttempts = new LoginAttemptStore(dataDir);
3953
+ void loginAttempts.load();
3178
3954
  const HQ_SESSION_MAX_AGE_MS2 = 7 * 24 * 60 * 60 * 1e3;
3955
+ const HQ_SESSION_IDLE_TIMEOUT_MS2 = 30 * 6e4;
3179
3956
  const SESSION_CLEANUP_INTERVAL_MS = 6e4;
3957
+ const PENDING_2FA_TTL_MS2 = 5 * 6e4;
3180
3958
  const sessionCleanupTimer = setInterval(() => {
3181
- 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;
3182
3963
  for (const [id, session] of sessions) {
3183
- 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
+ }
3184
3975
  }
3185
- const now = Date.now();
3186
- for (const [ip, entry] of loginAttempts) {
3187
- 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);
3188
3978
  }
3189
3979
  }, SESSION_CLEANUP_INTERVAL_MS);
3190
3980
  sessionCleanupTimer.unref?.();
3191
3981
  const persistence = createHqPersistence(dataDir);
3192
3982
  const auditLog = new HqCommandAuditLog(1e3, (entry) => persistence.commandLog.append(entry));
3193
3983
  void (async () => {
3194
- const projectsDir = path6.join(path6.dirname(dataDir), "projects");
3195
- 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(() => []);
3196
3986
  await Promise.all(
3197
3987
  entries.filter((entry) => entry.isDirectory()).map(async (entry) => {
3198
- const projectDir = path6.join(projectsDir, entry.name);
3199
- const metadataPath = path6.join(projectDir, ".mailbox-server.json");
3200
- 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);
3201
3991
  if (ownerPid === void 0) return;
3202
3992
  let ownerAlive = false;
3203
3993
  try {
@@ -3207,7 +3997,7 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
3207
3997
  ownerAlive = false;
3208
3998
  }
3209
3999
  if (!ownerAlive) {
3210
- await fs4.rm(metadataPath, { force: true }).catch(() => {
4000
+ await fs5.rm(metadataPath, { force: true }).catch(() => {
3211
4001
  });
3212
4002
  return;
3213
4003
  }
@@ -3227,8 +4017,7 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
3227
4017
  const requestUrl = new URL(req.url ?? "/", `http://${host}:${port}`);
3228
4018
  const auth = authenticateBrowserRequest2(req, requestUrl, mutableAuth, sessions);
3229
4019
  const tokenMode = mutableAuth.browserTokens.size > 0;
3230
- const passwordMode = mutableAuth.passwordHash !== void 0;
3231
- if ((tokenMode || passwordMode) && auth === void 0) {
4020
+ if (hqAuthRequired(mutableAuth) && auth === void 0) {
3232
4021
  return {
3233
4022
  allowed: false,
3234
4023
  status: 401,
@@ -3383,6 +4172,7 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
3383
4172
  host,
3384
4173
  listeningPort,
3385
4174
  trustedPublicOrigins,
4175
+ allowFileOrigin: options.allowFileOrigin,
3386
4176
  mutableAuth,
3387
4177
  sessions,
3388
4178
  loginAttempts,
@@ -3427,7 +4217,13 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
3427
4217
  socket.destroy();
3428
4218
  return;
3429
4219
  }
3430
- if (!hasTrustedBrowserOrigin2(req, host, listeningPort, trustedPublicOrigins)) {
4220
+ if (!hasTrustedBrowserOrigin2(
4221
+ req,
4222
+ host,
4223
+ listeningPort,
4224
+ trustedPublicOrigins,
4225
+ options.allowFileOrigin
4226
+ )) {
3431
4227
  socket.write(
3432
4228
  "HTTP/1.1 403 Forbidden\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n" + JSON.stringify({
3433
4229
  error: {
@@ -3440,19 +4236,43 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
3440
4236
  return;
3441
4237
  }
3442
4238
  const tokenSet = pathname === "/ws/browser" ? mutableAuth.browserTokens : mutableAuth.clientTokens;
3443
- 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);
3444
4240
  if (needsAuth) {
3445
- 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
+ }
3446
4261
  const tokenValid = timingSafeTokenMatch(tokenSet, supplied) !== void 0;
3447
4262
  const cookieValid = pathname === "/ws/browser" && mutableAuth.cookieSecret !== void 0 && (() => {
3448
4263
  const cookies = parseCookieHeader2(req.headers.cookie);
3449
- const raw = cookies[HQ_SESSION_COOKIE2];
4264
+ const raw = cookies[HQ_SESSION_COOKIE2] ?? cookies["__Host-hq.session"];
3450
4265
  if (!raw) return false;
3451
4266
  const sessionId = parseHqSessionCookie2(raw, mutableAuth.cookieSecret);
3452
4267
  if (sessionId === void 0) return false;
3453
4268
  const session = sessions.get(sessionId);
3454
4269
  if (!session || Date.now() - session.createdAt >= HQ_SESSION_MAX_AGE_MS)
3455
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;
3456
4276
  if (session.kind === "token" && session.tokenId !== void 0) {
3457
4277
  return [...mutableAuth.browserTokenObjs.values()].some(
3458
4278
  (obj) => obj.id === session.tokenId
@@ -3488,7 +4308,11 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
3488
4308
  browsers,
3489
4309
  eventLog,
3490
4310
  {
3491
- ...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)) } : {},
3492
4316
  getOperatorPolicy: () => mutableAuth.operatorPolicyOverride
3493
4317
  },
3494
4318
  snapshotBroadcaster,
@@ -3503,7 +4327,26 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
3503
4327
  dataDir,
3504
4328
  (next) => {
3505
4329
  authState.apply(next);
3506
- 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) {
3507
4350
  sessions.clear();
3508
4351
  for (const browser of browsers) browser.close(1008, "Browser authentication removed");
3509
4352
  }
@@ -3554,14 +4397,13 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
3554
4397
  const addr = httpServer.address();
3555
4398
  const actualPort = typeof addr === "object" && addr ? addr.port : port;
3556
4399
  listeningPort = actualPort;
3557
- const browserToken = firstRunAuth.browserToken?.token ?? authFile.browserTokens?.find((t) => t.token.trim().length > 0)?.token;
3558
4400
  const clientToken = firstRunAuth.clientToken?.token ?? authFile.clientTokens?.find((t) => t.token.trim().length > 0)?.token;
3559
4401
  const hqUrl = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${actualPort}`;
3560
4402
  await writeHqRuntimeMarker(dataDir, hqUrl).catch(() => {
3561
4403
  });
3562
- 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;
3563
4405
  let browserUrl;
3564
- if (browserToken && browserTokenObj) {
4406
+ if (browserTokenObj) {
3565
4407
  const code = bootstrapStore.issue({
3566
4408
  tokenId: browserTokenObj.id,
3567
4409
  ...browserTokenObj.capabilities !== void 0 ? { capabilities: browserTokenObj.capabilities } : {}
@@ -3608,6 +4450,7 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
3608
4450
  clearInterval(mailboxGatewayRateLimitCleanup);
3609
4451
  clearInterval(mailboxGatewaySweepTimer);
3610
4452
  clearInterval(sessionCleanupTimer);
4453
+ void loginAttempts.flush();
3611
4454
  stopAlertEngine();
3612
4455
  authWatcher.close();
3613
4456
  snapshotBroadcaster.close();
@@ -3638,7 +4481,7 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
3638
4481
  port: actualPort,
3639
4482
  firstRunSetup: startupInfo
3640
4483
  });
3641
- resolve(handle);
4484
+ resolve2(handle);
3642
4485
  })();
3643
4486
  });
3644
4487
  });
@@ -3662,4 +4505,4 @@ export {
3662
4505
  startHqServer,
3663
4506
  HqInsecureExposureError2 as HqInsecureExposureError
3664
4507
  };
3665
- //# sourceMappingURL=chunk-7JABKXET.js.map
4508
+ //# sourceMappingURL=chunk-C5GH4YBL.js.map