@wrongstack/cli 0.295.1 → 0.296.3
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.
- package/data/providers.json +0 -24
- package/dist/acp-A5OSIU2P.js +824 -0
- package/dist/acp-A5OSIU2P.js.map +7 -0
- package/dist/acp-server-agent.d.ts.map +1 -1
- package/dist/arg-parser.d.ts.map +1 -1
- package/dist/audit-2ZSFGNIF.js +102 -0
- package/dist/audit-2ZSFGNIF.js.map +7 -0
- package/dist/auth-CJWQJVRH.js +504 -0
- package/dist/auth-CJWQJVRH.js.map +7 -0
- package/dist/auth-menu/auth-menu-audit.d.ts +0 -4
- package/dist/auth-menu/auth-menu-audit.d.ts.map +1 -1
- package/dist/bench-7ZDWSNEF.js +278 -0
- package/dist/bench-7ZDWSNEF.js.map +7 -0
- package/dist/boot/brain-decision-log.d.ts +7 -5
- package/dist/boot/brain-decision-log.d.ts.map +1 -1
- package/dist/boot/dispatch-webui.d.ts +5 -8
- package/dist/boot/dispatch-webui.d.ts.map +1 -1
- package/dist/boot/system-prompt-builder.d.ts +3 -0
- package/dist/boot/system-prompt-builder.d.ts.map +1 -1
- package/dist/boot/tui-coordinator-callbacks.d.ts +45 -0
- package/dist/boot/tui-coordinator-callbacks.d.ts.map +1 -0
- package/dist/boot/tui-project-switch.d.ts +2 -3
- package/dist/boot/tui-project-switch.d.ts.map +1 -1
- package/dist/boot/tui-runtime-state.d.ts +5 -6
- package/dist/boot/tui-runtime-state.d.ts.map +1 -1
- package/dist/boot/tui-session-resume.d.ts.map +1 -1
- package/dist/boot/tui-settings-adapter.d.ts +2 -3
- package/dist/boot/tui-settings-adapter.d.ts.map +1 -1
- package/dist/boot.d.ts.map +1 -1
- package/dist/brain-menu/panel-service.d.ts.map +1 -1
- package/dist/chimera-review-task.d.ts +6 -0
- package/dist/chimera-review-task.d.ts.map +1 -0
- package/dist/chimera-reviewer-policy.d.ts +34 -0
- package/dist/chimera-reviewer-policy.d.ts.map +1 -0
- package/dist/chronicle-QQEQFSSE.js +256 -0
- package/dist/chronicle-QQEQFSSE.js.map +7 -0
- package/dist/chunk-5N34EWJR.js +418 -0
- package/dist/chunk-5N34EWJR.js.map +7 -0
- package/dist/chunk-66T43Q4Y.js +90 -0
- package/dist/chunk-66T43Q4Y.js.map +7 -0
- package/dist/chunk-7OCVIDC7.js +11 -0
- package/dist/chunk-7OCVIDC7.js.map +7 -0
- package/dist/chunk-ABXK4VVY.js +84 -0
- package/dist/chunk-ABXK4VVY.js.map +7 -0
- package/dist/chunk-AXVZMEWG.js +306 -0
- package/dist/chunk-AXVZMEWG.js.map +7 -0
- package/dist/chunk-AZESNNIC.js +216 -0
- package/dist/chunk-AZESNNIC.js.map +7 -0
- package/dist/chunk-BGFM3PD7.js +41 -0
- package/dist/chunk-BGFM3PD7.js.map +7 -0
- package/dist/chunk-BQCAJ7FI.js +425 -0
- package/dist/chunk-BQCAJ7FI.js.map +7 -0
- package/dist/chunk-DPJZQCVP.js +13 -0
- package/dist/chunk-DPJZQCVP.js.map +7 -0
- package/dist/chunk-F4PEGAJZ.js +176 -0
- package/dist/chunk-F4PEGAJZ.js.map +7 -0
- package/dist/chunk-IHBYBYGA.js +443 -0
- package/dist/chunk-IHBYBYGA.js.map +7 -0
- package/dist/chunk-JBSQ7ONN.js +1023 -0
- package/dist/chunk-JBSQ7ONN.js.map +7 -0
- package/dist/chunk-JBTFWWKV.js +1325 -0
- package/dist/chunk-JBTFWWKV.js.map +7 -0
- package/dist/chunk-JUHRI6OV.js +100 -0
- package/dist/chunk-JUHRI6OV.js.map +7 -0
- package/dist/chunk-OI7GWJDS.js +3303 -0
- package/dist/chunk-OI7GWJDS.js.map +7 -0
- package/dist/chunk-OP47B3IX.js +142 -0
- package/dist/chunk-OP47B3IX.js.map +7 -0
- package/dist/chunk-PJHET45A.js +158 -0
- package/dist/chunk-PJHET45A.js.map +7 -0
- package/dist/chunk-Q5GTM25S.js +17 -0
- package/dist/chunk-Q5GTM25S.js.map +7 -0
- package/dist/chunk-QD544J2B.js +19 -0
- package/dist/chunk-QD544J2B.js.map +7 -0
- package/dist/chunk-QUNHS6O3.js +258 -0
- package/dist/chunk-QUNHS6O3.js.map +7 -0
- package/dist/chunk-TADVX644.js +480 -0
- package/dist/chunk-TADVX644.js.map +7 -0
- package/dist/chunk-TGDHN4LM.js +81 -0
- package/dist/chunk-TGDHN4LM.js.map +7 -0
- package/dist/chunk-TYO2OVAD.js +50 -0
- package/dist/chunk-TYO2OVAD.js.map +7 -0
- package/dist/chunk-VCYZ3WZV.js +14 -0
- package/dist/chunk-VCYZ3WZV.js.map +7 -0
- package/dist/chunk-VJUEZZYB.js +29 -0
- package/dist/chunk-VJUEZZYB.js.map +7 -0
- package/dist/chunk-WHOIR577.js +1 -0
- package/dist/chunk-WHOIR577.js.map +7 -0
- package/dist/chunk-WY7KKOUK.js +460 -0
- package/dist/chunk-WY7KKOUK.js.map +7 -0
- package/dist/chunk-XJXDOF63.js +28 -0
- package/dist/chunk-XJXDOF63.js.map +7 -0
- package/dist/chunk-XWEGWYDU.js +560 -0
- package/dist/chunk-XWEGWYDU.js.map +7 -0
- package/dist/chunk-YWUPHRGB.js +346 -0
- package/dist/chunk-YWUPHRGB.js.map +7 -0
- package/dist/chunk-Z7YGASQP.js +313 -0
- package/dist/chunk-Z7YGASQP.js.map +7 -0
- package/dist/cli-entry-main.d.ts +2 -0
- package/dist/cli-entry-main.d.ts.map +1 -0
- package/dist/cli-entry-point.d.ts +3 -3
- package/dist/cli-entry-point.d.ts.map +1 -1
- package/dist/cli-main-RTY35OPD.js +29268 -0
- package/dist/cli-main-RTY35OPD.js.map +7 -0
- package/dist/cli-main-helpers.d.ts +21 -0
- package/dist/cli-main-helpers.d.ts.map +1 -0
- package/dist/cli-main.d.ts +25 -1
- package/dist/cli-main.d.ts.map +1 -1
- package/dist/context-limit.d.ts +48 -0
- package/dist/context-limit.d.ts.map +1 -1
- package/dist/diag-doctor-OQ3AA63O.js +170 -0
- package/dist/diag-doctor-OQ3AA63O.js.map +7 -0
- package/dist/execute-deps.d.ts +11 -12
- package/dist/execute-deps.d.ts.map +1 -1
- package/dist/execution-O7I5DYE6.js +4682 -0
- package/dist/execution-O7I5DYE6.js.map +7 -0
- package/dist/execution-chimera-ask.d.ts +10 -0
- package/dist/execution-chimera-ask.d.ts.map +1 -0
- package/dist/execution-chimera-cascade.d.ts +15 -0
- package/dist/execution-chimera-cascade.d.ts.map +1 -0
- package/dist/execution-cleanup.d.ts +23 -0
- package/dist/execution-cleanup.d.ts.map +1 -0
- package/dist/execution-kanban-dispatch.d.ts +10 -0
- package/dist/execution-kanban-dispatch.d.ts.map +1 -0
- package/dist/execution-repl-fleet-callbacks.d.ts +11 -0
- package/dist/execution-repl-fleet-callbacks.d.ts.map +1 -0
- package/dist/execution-storage-observability.d.ts +3 -0
- package/dist/execution-storage-observability.d.ts.map +1 -0
- package/dist/execution-tui-next-step-callbacks.d.ts +20 -0
- package/dist/execution-tui-next-step-callbacks.d.ts.map +1 -0
- package/dist/execution.d.ts +2 -139
- package/dist/execution.d.ts.map +1 -1
- package/dist/export-TPORYVRZ.js +64 -0
- package/dist/export-TPORYVRZ.js.map +7 -0
- package/dist/fleet/host-acp-runner-cache.d.ts +9 -0
- package/dist/fleet/host-acp-runner-cache.d.ts.map +1 -0
- package/dist/fleet/host-acp.d.ts +3 -0
- package/dist/fleet/host-acp.d.ts.map +1 -0
- package/dist/fleet/host-concurrency.d.ts +2 -0
- package/dist/fleet/host-concurrency.d.ts.map +1 -0
- package/dist/fleet/host-context.d.ts +5 -0
- package/dist/fleet/host-context.d.ts.map +1 -0
- package/dist/fleet/host-director-builder.d.ts +24 -0
- package/dist/fleet/host-director-builder.d.ts.map +1 -0
- package/dist/fleet/host-director-event-bridges.d.ts +37 -0
- package/dist/fleet/host-director-event-bridges.d.ts.map +1 -0
- package/dist/fleet/host-director-services.d.ts +20 -0
- package/dist/fleet/host-director-services.d.ts.map +1 -0
- package/dist/fleet/host-event-bridge.d.ts +17 -0
- package/dist/fleet/host-event-bridge.d.ts.map +1 -0
- package/dist/fleet/host-helpers.d.ts +22 -0
- package/dist/fleet/host-helpers.d.ts.map +1 -0
- package/dist/fleet/host-learning-tracker.d.ts +11 -0
- package/dist/fleet/host-learning-tracker.d.ts.map +1 -0
- package/dist/fleet/host-learning.d.ts +4 -0
- package/dist/fleet/host-learning.d.ts.map +1 -0
- package/dist/fleet/host-lifecycle-events.d.ts +4 -0
- package/dist/fleet/host-lifecycle-events.d.ts.map +1 -0
- package/dist/fleet/host-paths.d.ts +3 -0
- package/dist/fleet/host-paths.d.ts.map +1 -0
- package/dist/fleet/host-provider.d.ts +22 -0
- package/dist/fleet/host-provider.d.ts.map +1 -0
- package/dist/fleet/host-session-writer.d.ts +3 -0
- package/dist/fleet/host-session-writer.d.ts.map +1 -0
- package/dist/fleet/host-shadow-pass.d.ts +21 -0
- package/dist/fleet/host-shadow-pass.d.ts.map +1 -0
- package/dist/fleet/host-spawn-types.d.ts +25 -0
- package/dist/fleet/host-spawn-types.d.ts.map +1 -0
- package/dist/fleet/host-status.d.ts +48 -0
- package/dist/fleet/host-status.d.ts.map +1 -0
- package/dist/fleet/host-subagent-factory.d.ts +16 -0
- package/dist/fleet/host-subagent-factory.d.ts.map +1 -0
- package/dist/fleet/host-supervisor.d.ts +16 -0
- package/dist/fleet/host-supervisor.d.ts.map +1 -0
- package/dist/fleet/host-task-result-report.d.ts +11 -0
- package/dist/fleet/host-task-result-report.d.ts.map +1 -0
- package/dist/fleet/host-types.d.ts +89 -0
- package/dist/fleet/host-types.d.ts.map +1 -0
- package/dist/fleet/host.d.ts +10 -357
- package/dist/fleet/host.d.ts.map +1 -1
- package/dist/hq-JUYKUNJX.js +547 -0
- package/dist/hq-JUYKUNJX.js.map +7 -0
- package/dist/hq-command-controller.d.ts +2 -2
- package/dist/hq-command-controller.d.ts.map +1 -1
- package/dist/hq-server/auth.d.ts +15 -4
- package/dist/hq-server/auth.d.ts.map +1 -1
- package/dist/hq-server/project-root.d.ts +5 -0
- package/dist/hq-server/project-root.d.ts.map +1 -0
- package/dist/hq-server/routes/auth-handlers.d.ts +25 -0
- package/dist/hq-server/routes/auth-handlers.d.ts.map +1 -0
- package/dist/hq-server/routes/session-handlers.d.ts +8 -0
- package/dist/hq-server/routes/session-handlers.d.ts.map +1 -0
- package/dist/hq-server/routes.d.ts +8 -6
- package/dist/hq-server/routes.d.ts.map +1 -1
- package/dist/hq-server/snapshot.d.ts +44 -0
- package/dist/hq-server/snapshot.d.ts.map +1 -1
- package/dist/hq-server/types.d.ts +22 -4
- package/dist/hq-server/types.d.ts.map +1 -1
- package/dist/hq-server/utils.d.ts +8 -0
- package/dist/hq-server/utils.d.ts.map +1 -1
- package/dist/hq-server/ws.d.ts +1 -1
- package/dist/hq-server/ws.d.ts.map +1 -1
- package/dist/hq-server-6AQ3BXZA.js +39 -0
- package/dist/hq-server-6AQ3BXZA.js.map +7 -0
- package/dist/hq-server.d.ts +3 -3
- package/dist/hq-server.d.ts.map +1 -1
- package/dist/hq-ttl-U2FAYFAT.js +8 -0
- package/dist/hq-ttl-U2FAYFAT.js.map +7 -0
- package/dist/hq-tunnel-RFKGC4HI.js +114 -0
- package/dist/hq-tunnel-RFKGC4HI.js.map +7 -0
- package/dist/hq-tunnel.d.ts +1 -1
- package/dist/hq-tunnel.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2503 -53026
- package/dist/index.js.map +4 -4
- package/dist/init-E2NDDOHI.js +27 -0
- package/dist/init-E2NDDOHI.js.map +7 -0
- package/dist/kanban-dispatch-route.d.ts +14 -0
- package/dist/kanban-dispatch-route.d.ts.map +1 -0
- package/dist/kanban-hq-sync.d.ts +2 -0
- package/dist/kanban-hq-sync.d.ts.map +1 -1
- package/dist/kanban-run-mirror-EKVNGXWW.js +12 -0
- package/dist/kanban-run-mirror-EKVNGXWW.js.map +7 -0
- package/dist/live-settings-input.d.ts +69 -0
- package/dist/live-settings-input.d.ts.map +1 -0
- package/dist/mailbox-serve-2UWHEN3M.js +302 -0
- package/dist/mailbox-serve-2UWHEN3M.js.map +7 -0
- package/dist/mcp-CYLPUEHC.js +413 -0
- package/dist/mcp-CYLPUEHC.js.map +7 -0
- package/dist/modeldiag-HENYJ4N7.js +1290 -0
- package/dist/modeldiag-HENYJ4N7.js.map +7 -0
- package/dist/permissions-FNYLZIL4.js +115 -0
- package/dist/permissions-FNYLZIL4.js.map +7 -0
- package/dist/picker-key-state.d.ts +7 -0
- package/dist/picker-key-state.d.ts.map +1 -0
- package/dist/picker-model-picker.d.ts +6 -0
- package/dist/picker-model-picker.d.ts.map +1 -0
- package/dist/picker-ui.d.ts +17 -0
- package/dist/picker-ui.d.ts.map +1 -0
- package/dist/picker.d.ts +4 -37
- package/dist/picker.d.ts.map +1 -1
- package/dist/plugin-usage-OU6X6LM3.js +40 -0
- package/dist/plugin-usage-OU6X6LM3.js.map +7 -0
- package/dist/plugins-4LJ5IUBI.js +24 -0
- package/dist/plugins-4LJ5IUBI.js.map +7 -0
- package/dist/pre-launch.d.ts +0 -1
- package/dist/pre-launch.d.ts.map +1 -1
- package/dist/project-BLQ3ALVL.js +76 -0
- package/dist/project-BLQ3ALVL.js.map +7 -0
- package/dist/project-manifest-GE25PW4S.js +18 -0
- package/dist/project-manifest-GE25PW4S.js.map +7 -0
- package/dist/project-picker-BOCWHBXP.js +17 -0
- package/dist/project-picker-BOCWHBXP.js.map +7 -0
- package/dist/projects-XE76NFNZ.js +38 -0
- package/dist/projects-XE76NFNZ.js.map +7 -0
- package/dist/provider-helpers.d.ts +22 -6
- package/dist/provider-helpers.d.ts.map +1 -1
- package/dist/providers-models-GDELBHZK.js +589 -0
- package/dist/providers-models-GDELBHZK.js.map +7 -0
- package/dist/quick-VIO2BEHS.js +10 -0
- package/dist/quick-VIO2BEHS.js.map +7 -0
- package/dist/repl-auto-proceed.d.ts +19 -0
- package/dist/repl-auto-proceed.d.ts.map +1 -0
- package/dist/repl-client-registration.d.ts +7 -0
- package/dist/repl-client-registration.d.ts.map +1 -0
- package/dist/repl-input.d.ts +11 -0
- package/dist/repl-input.d.ts.map +1 -0
- package/dist/repl-options.d.ts +54 -0
- package/dist/repl-options.d.ts.map +1 -0
- package/dist/repl-rendering.d.ts +4 -0
- package/dist/repl-rendering.d.ts.map +1 -0
- package/dist/repl-suggestions.d.ts +21 -0
- package/dist/repl-suggestions.d.ts.map +1 -0
- package/dist/repl.d.ts +3 -162
- package/dist/repl.d.ts.map +1 -1
- package/dist/replay-KCTXNMZQ.js +75 -0
- package/dist/replay-KCTXNMZQ.js.map +7 -0
- package/dist/rewind-EJMVFVLS.js +185 -0
- package/dist/rewind-EJMVFVLS.js.map +7 -0
- package/dist/sdd-runtime-JG4EEKL6.js +54 -0
- package/dist/sdd-runtime-JG4EEKL6.js.map +7 -0
- package/dist/services/sdd/project-context.d.ts +1 -1
- package/dist/services/sdd/project-context.d.ts.map +1 -1
- package/dist/services/sdd/task-manager.d.ts.map +1 -1
- package/dist/services/statusline-config.d.ts +1 -1
- package/dist/services/statusline-config.d.ts.map +1 -1
- package/dist/session-event-wiring.d.ts +2 -2
- package/dist/session-event-wiring.d.ts.map +1 -1
- package/dist/sessions-config-NLCMK3BY.js +409 -0
- package/dist/sessions-config-NLCMK3BY.js.map +7 -0
- package/dist/settings-menu.d.ts +3 -18
- package/dist/settings-menu.d.ts.map +1 -1
- package/dist/simpleui-dist-JIFGUJO7.js +93 -0
- package/dist/simpleui-dist-JIFGUJO7.js.map +7 -0
- package/dist/slash-commands/brain.d.ts.map +1 -1
- package/dist/slash-commands/clear.d.ts.map +1 -1
- package/dist/slash-commands/fallback.d.ts.map +1 -1
- package/dist/slash-commands/kanban-agent-helpers.d.ts +18 -0
- package/dist/slash-commands/kanban-agent-helpers.d.ts.map +1 -0
- package/dist/slash-commands/kanban-format.d.ts +16 -0
- package/dist/slash-commands/kanban-format.d.ts.map +1 -0
- package/dist/slash-commands/kanban-help.d.ts +2 -0
- package/dist/slash-commands/kanban-help.d.ts.map +1 -0
- package/dist/slash-commands/kanban.d.ts.map +1 -1
- package/dist/slash-commands/mailbox-demo.d.ts.map +1 -1
- package/dist/slash-commands/mailbox.d.ts +2 -1
- package/dist/slash-commands/mailbox.d.ts.map +1 -1
- package/dist/slash-commands/memory-compact.d.ts +5 -0
- package/dist/slash-commands/memory-compact.d.ts.map +1 -0
- package/dist/slash-commands/memory-flags.d.ts +36 -0
- package/dist/slash-commands/memory-flags.d.ts.map +1 -0
- package/dist/slash-commands/memory-formatters.d.ts +16 -0
- package/dist/slash-commands/memory-formatters.d.ts.map +1 -0
- package/dist/slash-commands/memory.d.ts.map +1 -1
- package/dist/slash-commands/mouse.d.ts +2 -2
- package/dist/slash-commands/review.d.ts.map +1 -1
- package/dist/slash-commands/sdd-command-helpers.d.ts +23 -0
- package/dist/slash-commands/sdd-command-helpers.d.ts.map +1 -0
- package/dist/slash-commands/sdd-format.d.ts +32 -0
- package/dist/slash-commands/sdd-format.d.ts.map +1 -0
- package/dist/slash-commands/sdd.d.ts.map +1 -1
- package/dist/slash-commands/settings.d.ts.map +1 -1
- package/dist/subcommands/handlers/chronicle.d.ts.map +1 -1
- package/dist/subcommands/handlers/mailbox-serve.d.ts.map +1 -1
- package/dist/subcommands/handlers/model-smoke-test.d.ts +51 -0
- package/dist/subcommands/handlers/model-smoke-test.d.ts.map +1 -0
- package/dist/subcommands/handlers/modeldiag-profiles.d.ts +64 -0
- package/dist/subcommands/handlers/modeldiag-profiles.d.ts.map +1 -0
- package/dist/subcommands/handlers/modeldiag.d.ts.map +1 -1
- package/dist/subcommands/handlers/per-subcommand-deep-help-table.d.ts +3 -0
- package/dist/subcommands/handlers/per-subcommand-deep-help-table.d.ts.map +1 -0
- package/dist/subcommands/handlers/per-subcommand-help-data.d.ts +3 -0
- package/dist/subcommands/handlers/per-subcommand-help-data.d.ts.map +1 -0
- package/dist/subcommands/handlers/per-subcommand-help-table.d.ts +36 -0
- package/dist/subcommands/handlers/per-subcommand-help-table.d.ts.map +1 -0
- package/dist/subcommands/handlers/per-subcommand-help-types.d.ts +18 -0
- package/dist/subcommands/handlers/per-subcommand-help-types.d.ts.map +1 -0
- package/dist/subcommands/handlers/per-subcommand-help.d.ts +2 -80
- package/dist/subcommands/handlers/per-subcommand-help.d.ts.map +1 -1
- package/dist/subcommands/handlers/rewind.d.ts.map +1 -1
- package/dist/subcommands/index.d.ts +7 -0
- package/dist/subcommands/index.d.ts.map +1 -1
- package/dist/tools-skills-UNKLOB7M.js +29 -0
- package/dist/tools-skills-UNKLOB7M.js.map +7 -0
- package/dist/update-YC4TYW2Y.js +16 -0
- package/dist/update-YC4TYW2Y.js.map +7 -0
- package/dist/update-check-UMYTOH7Z.js +18 -0
- package/dist/update-check-UMYTOH7Z.js.map +7 -0
- package/dist/version-help-DROHZRW6.js +11 -0
- package/dist/version-help-DROHZRW6.js.map +7 -0
- package/dist/webui-server/connection-handler.d.ts.map +1 -1
- package/dist/webui-server/kanban-host-adapter.d.ts +5 -1
- package/dist/webui-server/kanban-host-adapter.d.ts.map +1 -1
- package/dist/webui-server/listen-helpers.d.ts +18 -0
- package/dist/webui-server/listen-helpers.d.ts.map +1 -0
- package/dist/webui-server/node-pty-loader.d.ts +6 -0
- package/dist/webui-server/node-pty-loader.d.ts.map +1 -0
- package/dist/webui-server-ZF5KVGC5.js +1460 -0
- package/dist/webui-server-ZF5KVGC5.js.map +7 -0
- package/dist/webui-server-options.d.ts +191 -0
- package/dist/webui-server-options.d.ts.map +1 -0
- package/dist/webui-server.d.ts +2 -178
- package/dist/webui-server.d.ts.map +1 -1
- package/dist/wiring/brain-and-orchestration.d.ts.map +1 -1
- package/dist/wiring/codebase-index.d.ts.map +1 -1
- package/dist/wiring/command-host-state.d.ts +4 -4
- package/dist/wiring/dep-watcher-bridge.d.ts +23 -0
- package/dist/wiring/dep-watcher-bridge.d.ts.map +1 -0
- package/dist/wiring/dep-watcher.d.ts.map +1 -1
- package/dist/wiring/hq-telemetry.d.ts +2 -2
- package/dist/wiring/lifecycle-plugins.d.ts +4 -3
- package/dist/wiring/lifecycle-plugins.d.ts.map +1 -1
- package/dist/wiring/mailbox-bridge-bootstrap.d.ts +1 -1
- package/dist/wiring/management-tools.d.ts +15 -0
- package/dist/wiring/management-tools.d.ts.map +1 -0
- package/dist/wiring/pipeline.d.ts +2 -1
- package/dist/wiring/pipeline.d.ts.map +1 -1
- package/dist/wiring/plugins.d.ts +1 -1
- package/dist/wiring/plugins.d.ts.map +1 -1
- package/dist/wiring/provider-runtime-setup.d.ts +14 -1
- package/dist/wiring/provider-runtime-setup.d.ts.map +1 -1
- package/dist/wiring/provider.d.ts.map +1 -1
- package/dist/wiring/runtime-controller-deps.d.ts +1 -1
- package/dist/wiring/runtime-controller-deps.d.ts.map +1 -1
- package/dist/wiring/sage.d.ts.map +1 -1
- package/dist/wiring/session-registry.d.ts +38 -0
- package/dist/wiring/session-registry.d.ts.map +1 -0
- package/dist/wiring/session.d.ts +10 -6
- package/dist/wiring/session.d.ts.map +1 -1
- package/dist/wiring/tools.d.ts.map +1 -1
- package/package.json +22 -22
- package/dist/cli-recovery-prompt.d.ts +0 -19
- package/dist/cli-recovery-prompt.d.ts.map +0 -1
- package/dist/first-run-auth-gate.d.ts +0 -20
- package/dist/first-run-auth-gate.d.ts.map +0 -1
- package/dist/pre-launch/indexing-question.d.ts +0 -28
- package/dist/pre-launch/indexing-question.d.ts.map +0 -1
|
@@ -0,0 +1,3303 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveAuditActor
|
|
3
|
+
} from "./chunk-Q5GTM25S.js";
|
|
4
|
+
|
|
5
|
+
// src/hq-server.ts
|
|
6
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
7
|
+
import * as fs4 from "node:fs/promises";
|
|
8
|
+
import * as http from "node:http";
|
|
9
|
+
import * as path6 from "node:path";
|
|
10
|
+
import {
|
|
11
|
+
createMailboxHttpRouter,
|
|
12
|
+
createProjectMailbox,
|
|
13
|
+
getSharedProjectMailbox,
|
|
14
|
+
MAILBOX_HTTP_DEFAULT_MAX_AGE_MS,
|
|
15
|
+
MailboxEventEmitter,
|
|
16
|
+
MailboxHttpRateLimiter
|
|
17
|
+
} from "@wrongstack/core/coordination";
|
|
18
|
+
import {
|
|
19
|
+
createHqPersistence,
|
|
20
|
+
HqAlertEngine,
|
|
21
|
+
HqCommandAuditLog,
|
|
22
|
+
HqBootstrapCodeStore,
|
|
23
|
+
mintHqCookieSecret as mintHqCookieSecret2,
|
|
24
|
+
mutateHqAuthFile as mutateHqAuthFile2,
|
|
25
|
+
toAlertMessage,
|
|
26
|
+
watchHqAuthFile
|
|
27
|
+
} from "@wrongstack/core/hq";
|
|
28
|
+
import { createCompatibilityTrustBoundary } from "@wrongstack/core/security";
|
|
29
|
+
import { WebSocket as WebSocket3, WebSocketServer } from "ws";
|
|
30
|
+
|
|
31
|
+
// src/hq-recovery-html.ts
|
|
32
|
+
var HQ_RECOVERY_HTML = `<!doctype html>
|
|
33
|
+
<html lang="en">
|
|
34
|
+
<head>
|
|
35
|
+
<meta charset="utf-8">
|
|
36
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
37
|
+
<title>WrongStack HQ \u2014 assets unavailable</title>
|
|
38
|
+
<style>
|
|
39
|
+
:root{color-scheme:dark;background:#0c111b;color:#e7edf7;font:16px/1.5 system-ui,sans-serif}
|
|
40
|
+
body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px;box-sizing:border-box}
|
|
41
|
+
main{max-width:680px;border:1px solid #334155;background:#111827;padding:28px}
|
|
42
|
+
h1{margin-top:0;font-size:1.5rem}code{color:#93c5fd}p{color:#cbd5e1}
|
|
43
|
+
</style>
|
|
44
|
+
</head>
|
|
45
|
+
<body>
|
|
46
|
+
<main data-hq-recovery-shell>
|
|
47
|
+
<h1>HQ frontend assets are unavailable</h1>
|
|
48
|
+
<p>The HQ backend is running, but the packaged React application could not be resolved.</p>
|
|
49
|
+
<p>Rebuild or reinstall <code>@wrongstack/webui-hq</code>, then restart HQ. API routes remain available for diagnostics.</p>
|
|
50
|
+
</main>
|
|
51
|
+
</body>
|
|
52
|
+
</html>`;
|
|
53
|
+
var HQ_HTML = HQ_RECOVERY_HTML;
|
|
54
|
+
|
|
55
|
+
// src/hq-server/auth.ts
|
|
56
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
57
|
+
import { isIP } from "node:net";
|
|
58
|
+
var HQ_SESSION_COOKIE = "hq.session";
|
|
59
|
+
var HQ_SESSION_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
60
|
+
function setHqSecurityHeaders(res) {
|
|
61
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
62
|
+
res.setHeader("X-Frame-Options", "DENY");
|
|
63
|
+
res.setHeader("Referrer-Policy", "no-referrer");
|
|
64
|
+
res.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
|
|
65
|
+
res.setHeader(
|
|
66
|
+
"Content-Security-Policy",
|
|
67
|
+
[
|
|
68
|
+
"default-src 'self'",
|
|
69
|
+
"base-uri 'none'",
|
|
70
|
+
"object-src 'none'",
|
|
71
|
+
"frame-ancestors 'none'",
|
|
72
|
+
"form-action 'self'",
|
|
73
|
+
"script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'",
|
|
74
|
+
"style-src 'self' 'unsafe-inline'",
|
|
75
|
+
"font-src 'self' data:",
|
|
76
|
+
"img-src 'self' data:",
|
|
77
|
+
"connect-src 'self'"
|
|
78
|
+
].join("; ")
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
function hasTrustedRequestAuthority(req, boundHost, boundPort, trustedPublicOrigins) {
|
|
82
|
+
const rawHost = req.headers.host?.trim();
|
|
83
|
+
if (!rawHost) return false;
|
|
84
|
+
try {
|
|
85
|
+
const requestUrl = new URL(`http://${rawHost}`);
|
|
86
|
+
if (requestUrl.username || requestUrl.password || requestUrl.pathname !== "/" || requestUrl.search || requestUrl.hash) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
for (const origin of trustedPublicOrigins) {
|
|
90
|
+
const trusted = new URL(origin);
|
|
91
|
+
const normalizedRequest = new URL(`${trusted.protocol}//${rawHost}`);
|
|
92
|
+
if (trusted.host.toLowerCase() === normalizedRequest.host.toLowerCase()) return true;
|
|
93
|
+
}
|
|
94
|
+
const hostname = requestUrl.hostname.toLowerCase().replace(/^\[(.*)\]$/, "$1");
|
|
95
|
+
const requestPort = requestUrl.port || "80";
|
|
96
|
+
const portMatches = boundPort === void 0 || boundPort === 0 || requestPort === String(boundPort);
|
|
97
|
+
const isLoopback = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
|
98
|
+
if (isLoopback) return portMatches;
|
|
99
|
+
if (isIP(hostname) !== 0) {
|
|
100
|
+
const normalizedBoundHost = boundHost?.toLowerCase().replace(/^\[(.*)\]$/, "$1");
|
|
101
|
+
const permitsIpHost = normalizedBoundHost === void 0 || normalizedBoundHost === "0.0.0.0" || normalizedBoundHost === "::" || normalizedBoundHost === hostname;
|
|
102
|
+
return permitsIpHost && portMatches;
|
|
103
|
+
}
|
|
104
|
+
return boundHost?.toLowerCase() === hostname && portMatches;
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function hasTrustedBrowserOrigin(req, boundHost, boundPort, trustedPublicOrigins = /* @__PURE__ */ new Set()) {
|
|
110
|
+
if (!hasTrustedRequestAuthority(req, boundHost, boundPort, trustedPublicOrigins)) return false;
|
|
111
|
+
const origin = req.headers.origin;
|
|
112
|
+
if (origin === void 0) return true;
|
|
113
|
+
try {
|
|
114
|
+
const parsed = new URL(origin);
|
|
115
|
+
if (parsed.protocol === "file:") return true;
|
|
116
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
|
|
117
|
+
const rawRequestHost = req.headers.host?.trim();
|
|
118
|
+
if (!rawRequestHost) return false;
|
|
119
|
+
const requestHost = new URL(`${parsed.protocol}//${rawRequestHost}`).host.toLowerCase();
|
|
120
|
+
if (parsed.host.toLowerCase() !== requestHost) return false;
|
|
121
|
+
const registeredPublicOrigin = [...trustedPublicOrigins].some(
|
|
122
|
+
(trustedOrigin) => new URL(trustedOrigin).host.toLowerCase() === requestHost
|
|
123
|
+
);
|
|
124
|
+
return !registeredPublicOrigin || trustedPublicOrigins.has(parsed.origin.toLowerCase());
|
|
125
|
+
} catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function signHqSession(sessionId, secret) {
|
|
130
|
+
return createHmac("sha256", secret).update(sessionId).digest("hex");
|
|
131
|
+
}
|
|
132
|
+
function serializeHqSessionCookie(sessionId, secret) {
|
|
133
|
+
return `${sessionId}.${signHqSession(sessionId, secret)}`;
|
|
134
|
+
}
|
|
135
|
+
function parseHqSessionCookie(value, secret) {
|
|
136
|
+
const dot = value.indexOf(".");
|
|
137
|
+
if (dot === -1) return void 0;
|
|
138
|
+
const sessionId = value.slice(0, dot);
|
|
139
|
+
const sig = value.slice(dot + 1);
|
|
140
|
+
if (!sessionId || !sig) return void 0;
|
|
141
|
+
const expected = signHqSession(sessionId, secret);
|
|
142
|
+
try {
|
|
143
|
+
if (!timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return void 0;
|
|
144
|
+
} catch {
|
|
145
|
+
return void 0;
|
|
146
|
+
}
|
|
147
|
+
return sessionId;
|
|
148
|
+
}
|
|
149
|
+
function parseCookieHeader(cookieHeader) {
|
|
150
|
+
const out = {};
|
|
151
|
+
if (typeof cookieHeader !== "string") return out;
|
|
152
|
+
for (const part of cookieHeader.split(";")) {
|
|
153
|
+
const idx = part.indexOf("=");
|
|
154
|
+
if (idx === -1) continue;
|
|
155
|
+
const key = part.slice(0, idx).trim();
|
|
156
|
+
const value = part.slice(idx + 1).trim();
|
|
157
|
+
if (key) {
|
|
158
|
+
try {
|
|
159
|
+
out[key] = decodeURIComponent(value);
|
|
160
|
+
} catch {
|
|
161
|
+
out[key] = value;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
function setHqSessionCookie(res, value, secure) {
|
|
168
|
+
const parts = [
|
|
169
|
+
`${HQ_SESSION_COOKIE}=${encodeURIComponent(value)}`,
|
|
170
|
+
"Path=/",
|
|
171
|
+
"HttpOnly",
|
|
172
|
+
"SameSite=Lax",
|
|
173
|
+
`Max-Age=${Math.floor(HQ_SESSION_MAX_AGE_MS / 1e3)}`
|
|
174
|
+
];
|
|
175
|
+
if (secure) parts.push("Secure");
|
|
176
|
+
res.setHeader("Set-Cookie", parts.join("; "));
|
|
177
|
+
}
|
|
178
|
+
function clearHqSessionCookie(res, secure) {
|
|
179
|
+
const parts = [`${HQ_SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`];
|
|
180
|
+
if (secure) parts.push("Secure");
|
|
181
|
+
res.setHeader("Set-Cookie", parts.join("; "));
|
|
182
|
+
}
|
|
183
|
+
function isTokenAuth(auth) {
|
|
184
|
+
return auth !== void 0 && auth.kind === "token";
|
|
185
|
+
}
|
|
186
|
+
function isCookieAuth(auth) {
|
|
187
|
+
return auth !== void 0 && auth.kind === "cookie";
|
|
188
|
+
}
|
|
189
|
+
function extractBrowserToken(req, url) {
|
|
190
|
+
const queryToken = url.searchParams.get("token");
|
|
191
|
+
if (queryToken) {
|
|
192
|
+
console.warn(
|
|
193
|
+
JSON.stringify({
|
|
194
|
+
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.",
|
|
197
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
198
|
+
})
|
|
199
|
+
);
|
|
200
|
+
return queryToken;
|
|
201
|
+
}
|
|
202
|
+
const auth = req.headers.authorization;
|
|
203
|
+
if (typeof auth === "string" && auth.toLowerCase().startsWith("bearer ")) {
|
|
204
|
+
return auth.slice(7).trim();
|
|
205
|
+
}
|
|
206
|
+
return void 0;
|
|
207
|
+
}
|
|
208
|
+
function timingSafeTokenMatch(tokens, supplied) {
|
|
209
|
+
if (!supplied) return void 0;
|
|
210
|
+
const b = Buffer.from(supplied);
|
|
211
|
+
for (const candidate of tokens) {
|
|
212
|
+
const a = Buffer.from(candidate);
|
|
213
|
+
if (a.length === b.length && timingSafeEqual(a, b)) return candidate;
|
|
214
|
+
}
|
|
215
|
+
return void 0;
|
|
216
|
+
}
|
|
217
|
+
function authenticateBrowserRequest(req, url, mutableAuth, sessions) {
|
|
218
|
+
const token = extractBrowserToken(req, url);
|
|
219
|
+
if (token) {
|
|
220
|
+
const matchedToken = timingSafeTokenMatch(mutableAuth.browserTokens, token);
|
|
221
|
+
if (matchedToken) {
|
|
222
|
+
const obj = mutableAuth.browserTokenObjs.get(matchedToken);
|
|
223
|
+
const ctx = {
|
|
224
|
+
kind: "token",
|
|
225
|
+
token: matchedToken,
|
|
226
|
+
id: obj?.id ?? "unknown"
|
|
227
|
+
};
|
|
228
|
+
if (obj?.capabilities !== void 0) ctx.capabilities = obj.capabilities;
|
|
229
|
+
return ctx;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (mutableAuth.cookieSecret) {
|
|
233
|
+
const cookies = parseCookieHeader(req.headers.cookie);
|
|
234
|
+
const raw = cookies[HQ_SESSION_COOKIE];
|
|
235
|
+
if (raw) {
|
|
236
|
+
const sessionId = parseHqSessionCookie(raw, mutableAuth.cookieSecret);
|
|
237
|
+
if (sessionId) {
|
|
238
|
+
const session = sessions.get(sessionId);
|
|
239
|
+
if (session && Date.now() - session.createdAt < HQ_SESSION_MAX_AGE_MS) {
|
|
240
|
+
if (session.kind === "token" && session.tokenId !== void 0) {
|
|
241
|
+
const liveToken = [...mutableAuth.browserTokenObjs.values()].find(
|
|
242
|
+
(obj) => obj.id === session.tokenId
|
|
243
|
+
);
|
|
244
|
+
if (!liveToken) {
|
|
245
|
+
sessions.delete(sessionId);
|
|
246
|
+
return void 0;
|
|
247
|
+
}
|
|
248
|
+
const ctx = { kind: "cookie", tokenId: session.tokenId };
|
|
249
|
+
if (liveToken.capabilities !== void 0) ctx.capabilities = liveToken.capabilities;
|
|
250
|
+
return ctx;
|
|
251
|
+
}
|
|
252
|
+
return { kind: "cookie" };
|
|
253
|
+
}
|
|
254
|
+
if (session) sessions.delete(sessionId);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return void 0;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/hq-server/auth-state.ts
|
|
262
|
+
import { DEFAULT_HQ_REDACTION_POLICY, hqAuthContentHash, isTokenExpired, logHqAuthAudit } from "@wrongstack/core/hq";
|
|
263
|
+
var TOKEN_EXPIRY_WARNING_MS = 24 * 60 * 60 * 1e3;
|
|
264
|
+
function liveTokens(list) {
|
|
265
|
+
return (list ?? []).filter((token) => !isTokenExpired(token));
|
|
266
|
+
}
|
|
267
|
+
function createHqAuthState(authFile, dataDir) {
|
|
268
|
+
let rawBrowserTokens = authFile.browserTokens ?? [];
|
|
269
|
+
let rawClientTokens = authFile.clientTokens ?? [];
|
|
270
|
+
const mutableAuth = {
|
|
271
|
+
operatorPolicy: { ...DEFAULT_HQ_REDACTION_POLICY, ...authFile.redactionPolicy ?? {} },
|
|
272
|
+
operatorPolicyOverride: authFile.redactionPolicy,
|
|
273
|
+
browserTokens: new Set(liveTokens(authFile.browserTokens).map(({ token }) => token)),
|
|
274
|
+
clientTokens: new Set(liveTokens(authFile.clientTokens).map(({ token }) => token)),
|
|
275
|
+
browserTokenObjs: browserTokenMap(authFile.browserTokens),
|
|
276
|
+
clientTokenObjs: new Map(liveTokens(authFile.clientTokens).map((token) => [token.token, token])),
|
|
277
|
+
passwordHash: authFile.passwordHash,
|
|
278
|
+
cookieSecret: authFile.cookieSecret,
|
|
279
|
+
alertRules: authFile.alertRules
|
|
280
|
+
};
|
|
281
|
+
return {
|
|
282
|
+
mutableAuth,
|
|
283
|
+
tokenStats: () => {
|
|
284
|
+
const now = Date.now();
|
|
285
|
+
const all = [...rawBrowserTokens, ...rawClientTokens];
|
|
286
|
+
const live = all.filter((token) => !isTokenExpired(token, now));
|
|
287
|
+
return {
|
|
288
|
+
browserTotal: mutableAuth.browserTokens.size,
|
|
289
|
+
clientTotal: mutableAuth.clientTokens.size,
|
|
290
|
+
expired: all.length - live.length,
|
|
291
|
+
expiringSoon: live.filter((token) => {
|
|
292
|
+
if (token.expiresAt === void 0) return false;
|
|
293
|
+
const expiry = Date.parse(token.expiresAt);
|
|
294
|
+
return Number.isFinite(expiry) && expiry - now <= TOKEN_EXPIRY_WARNING_MS;
|
|
295
|
+
}).length
|
|
296
|
+
};
|
|
297
|
+
},
|
|
298
|
+
apply: (next) => {
|
|
299
|
+
const newBrowser = next.browserTokens ?? [];
|
|
300
|
+
const newClient = next.clientTokens ?? [];
|
|
301
|
+
auditPrunedTokens("browser", rawBrowserTokens, newBrowser, next, dataDir);
|
|
302
|
+
auditPrunedTokens("client", rawClientTokens, newClient, next, dataDir);
|
|
303
|
+
rawBrowserTokens = newBrowser;
|
|
304
|
+
rawClientTokens = newClient;
|
|
305
|
+
mutableAuth.operatorPolicy = {
|
|
306
|
+
...DEFAULT_HQ_REDACTION_POLICY,
|
|
307
|
+
...next.redactionPolicy ?? {}
|
|
308
|
+
};
|
|
309
|
+
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));
|
|
312
|
+
mutableAuth.browserTokenObjs = browserTokenMap(next.browserTokens);
|
|
313
|
+
mutableAuth.clientTokenObjs = new Map(
|
|
314
|
+
liveTokens(next.clientTokens).map((token) => [token.token, token])
|
|
315
|
+
);
|
|
316
|
+
mutableAuth.passwordHash = next.passwordHash;
|
|
317
|
+
mutableAuth.cookieSecret = next.cookieSecret;
|
|
318
|
+
mutableAuth.alertRules = next.alertRules;
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function browserTokenMap(tokens) {
|
|
323
|
+
return new Map(
|
|
324
|
+
liveTokens(tokens).map((token) => [
|
|
325
|
+
token.token,
|
|
326
|
+
{
|
|
327
|
+
id: token.id,
|
|
328
|
+
...token.capabilities !== void 0 ? { capabilities: token.capabilities } : {},
|
|
329
|
+
...token.expiresAt !== void 0 ? { expiresAt: token.expiresAt } : {}
|
|
330
|
+
}
|
|
331
|
+
])
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
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;
|
|
337
|
+
if (prunedCount === 0) return;
|
|
338
|
+
const contentHash = hqAuthContentHash(authFile);
|
|
339
|
+
logHqAuthAudit(dataDir, {
|
|
340
|
+
kind: "expired-prune",
|
|
341
|
+
scope,
|
|
342
|
+
tokenId: "(aggregate)",
|
|
343
|
+
prunedCount,
|
|
344
|
+
...contentHash !== void 0 ? { contentHash } : {}
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/hq-server/preflight.ts
|
|
349
|
+
import { assessHqExposure, ensureHqFirstRunAuthFile, resolveHqDataDir } from "@wrongstack/core/hq";
|
|
350
|
+
import { HqInsecureExposureError } from "@wrongstack/core/hq";
|
|
351
|
+
async function prepareHqServerStart(options, defaults) {
|
|
352
|
+
const host = options.host ?? defaults.host;
|
|
353
|
+
const port = options.port ?? defaults.port;
|
|
354
|
+
const dataDir = resolveHqDataDir(options.dataDir);
|
|
355
|
+
const firstRunAuth = await ensureHqFirstRunAuthFile(dataDir, {
|
|
356
|
+
warn: (message) => console.warn(
|
|
357
|
+
JSON.stringify({
|
|
358
|
+
level: "warn",
|
|
359
|
+
event: "hq.auth_load_failed",
|
|
360
|
+
message,
|
|
361
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
362
|
+
})
|
|
363
|
+
),
|
|
364
|
+
actor: resolveAuditActor(),
|
|
365
|
+
...options.password !== void 0 ? { password: options.password } : {},
|
|
366
|
+
...options.tokenTtlMs !== void 0 ? { tokenTtlMs: options.tokenTtlMs } : {}
|
|
367
|
+
});
|
|
368
|
+
const { authFile } = firstRunAuth;
|
|
369
|
+
if (options.requireBrowserAuth && (authFile.browserTokens ?? []).length === 0 && authFile.passwordHash === void 0) {
|
|
370
|
+
throw new HqInsecureExposureError(
|
|
371
|
+
"HQ public relay requires browser authentication. Set --password or create a browser token first."
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
const exposure = assessHqExposure({
|
|
375
|
+
host,
|
|
376
|
+
hasBrowserTokens: (authFile.browserTokens ?? []).length > 0,
|
|
377
|
+
hasPassword: authFile.passwordHash !== void 0,
|
|
378
|
+
allowInsecure: options.allowInsecureOpen
|
|
379
|
+
});
|
|
380
|
+
if (exposure.kind === "refuse") throw new HqInsecureExposureError(exposure.message);
|
|
381
|
+
if (exposure.kind === "warn") {
|
|
382
|
+
console.warn(
|
|
383
|
+
JSON.stringify({
|
|
384
|
+
level: "warn",
|
|
385
|
+
event: "hq.insecure_exposure",
|
|
386
|
+
message: exposure.message,
|
|
387
|
+
host,
|
|
388
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
389
|
+
})
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
return { host, port, dataDir, firstRunAuth };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/hq-server/routes.ts
|
|
396
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
397
|
+
import * as path4 from "node:path";
|
|
398
|
+
import { tokenHasCapability, validateHqCommand } from "@wrongstack/core/hq";
|
|
399
|
+
import {
|
|
400
|
+
resolveProjectDir
|
|
401
|
+
} from "@wrongstack/core/coordination";
|
|
402
|
+
|
|
403
|
+
// src/hq-static-serve.ts
|
|
404
|
+
import * as fs from "node:fs";
|
|
405
|
+
import { createRequire } from "node:module";
|
|
406
|
+
import * as path from "node:path";
|
|
407
|
+
var requireFromHere = createRequire(import.meta.url);
|
|
408
|
+
var MIME = {
|
|
409
|
+
".html": "text/html; charset=utf-8",
|
|
410
|
+
".js": "application/javascript; charset=utf-8",
|
|
411
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
412
|
+
".css": "text/css; charset=utf-8",
|
|
413
|
+
".json": "application/json; charset=utf-8",
|
|
414
|
+
".svg": "image/svg+xml",
|
|
415
|
+
".png": "image/png",
|
|
416
|
+
".jpg": "image/jpeg",
|
|
417
|
+
".woff": "font/woff",
|
|
418
|
+
".woff2": "font/woff2",
|
|
419
|
+
".ico": "image/x-icon"
|
|
420
|
+
};
|
|
421
|
+
var cachedDistDir;
|
|
422
|
+
function resolveHqDistDir() {
|
|
423
|
+
if (cachedDistDir !== void 0) return cachedDistDir;
|
|
424
|
+
try {
|
|
425
|
+
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"))) {
|
|
428
|
+
cachedDistDir = dist;
|
|
429
|
+
return dist;
|
|
430
|
+
}
|
|
431
|
+
} catch {
|
|
432
|
+
}
|
|
433
|
+
try {
|
|
434
|
+
let dir = path.dirname(new URL(import.meta.url).pathname.replace(/^\/(\w:)/, "$1"));
|
|
435
|
+
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"))) {
|
|
438
|
+
cachedDistDir = candidate;
|
|
439
|
+
return candidate;
|
|
440
|
+
}
|
|
441
|
+
const parent = path.dirname(dir);
|
|
442
|
+
if (parent === dir) break;
|
|
443
|
+
dir = parent;
|
|
444
|
+
}
|
|
445
|
+
} catch {
|
|
446
|
+
}
|
|
447
|
+
cachedDistDir = null;
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
function isInsideDist(filePath, distDir) {
|
|
451
|
+
const rel = path.relative(distDir, filePath);
|
|
452
|
+
return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
453
|
+
}
|
|
454
|
+
function isMissingPath(error) {
|
|
455
|
+
const code = error.code;
|
|
456
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
457
|
+
}
|
|
458
|
+
async function readRegularFile(filePath) {
|
|
459
|
+
try {
|
|
460
|
+
const info = await fs.promises.stat(filePath);
|
|
461
|
+
if (!info.isFile()) return null;
|
|
462
|
+
return await fs.promises.readFile(filePath);
|
|
463
|
+
} catch (error) {
|
|
464
|
+
if (isMissingPath(error)) return null;
|
|
465
|
+
throw error;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
async function serveHqStatic(_req, res, urlPath, distDir) {
|
|
469
|
+
const relPath = urlPath === "/" || urlPath === "" ? "/index.html" : urlPath;
|
|
470
|
+
let decodedPath;
|
|
471
|
+
try {
|
|
472
|
+
decodedPath = decodeURIComponent(relPath);
|
|
473
|
+
} catch {
|
|
474
|
+
return { handled: false };
|
|
475
|
+
}
|
|
476
|
+
const filePath = path.join(distDir, decodedPath);
|
|
477
|
+
if (!isInsideDist(filePath, distDir)) {
|
|
478
|
+
return { handled: false };
|
|
479
|
+
}
|
|
480
|
+
const data = await readRegularFile(filePath);
|
|
481
|
+
if (data === null) {
|
|
482
|
+
const indexPath = path.join(distDir, "index.html");
|
|
483
|
+
const html = await readRegularFile(indexPath);
|
|
484
|
+
if (html !== null) {
|
|
485
|
+
res.writeHead(200, {
|
|
486
|
+
"Content-Type": MIME[".html"],
|
|
487
|
+
"Cache-Control": "no-cache",
|
|
488
|
+
"Content-Length": html.byteLength
|
|
489
|
+
});
|
|
490
|
+
res.end(html);
|
|
491
|
+
return { handled: true };
|
|
492
|
+
}
|
|
493
|
+
return { handled: false };
|
|
494
|
+
}
|
|
495
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
496
|
+
const mime = MIME[ext] ?? "application/octet-stream";
|
|
497
|
+
const cacheControl = decodedPath.startsWith("/assets/") ? "public, max-age=31536000, immutable" : ext === ".html" ? "no-cache" : "public, max-age=3600";
|
|
498
|
+
res.writeHead(200, {
|
|
499
|
+
"Content-Type": mime,
|
|
500
|
+
"Cache-Control": cacheControl,
|
|
501
|
+
"Content-Length": data.byteLength
|
|
502
|
+
});
|
|
503
|
+
res.end(data);
|
|
504
|
+
return { handled: true };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/hq-server/snapshot.ts
|
|
508
|
+
import { WebSocket } from "ws";
|
|
509
|
+
|
|
510
|
+
// src/hq-server/utils.ts
|
|
511
|
+
import * as fs2 from "node:fs/promises";
|
|
512
|
+
import * as os from "node:os";
|
|
513
|
+
import * as path2 from "node:path";
|
|
514
|
+
import { resolveHqDataDir as resolveHqDataDir2 } from "@wrongstack/core/hq";
|
|
515
|
+
var TRANSCRIPT_RING_MAX = 4e3;
|
|
516
|
+
var MAX_TRANSCRIPT_SESSIONS = 400;
|
|
517
|
+
var MAX_AGENT_RINGS = 800;
|
|
518
|
+
function evictOldest(map, max) {
|
|
519
|
+
while (map.size > max) {
|
|
520
|
+
const oldest = map.keys().next().value;
|
|
521
|
+
if (oldest === void 0) break;
|
|
522
|
+
map.delete(oldest);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function agentRingKey(sessionId, subId) {
|
|
526
|
+
return sessionId ? `${sessionId}::${subId}` : subId;
|
|
527
|
+
}
|
|
528
|
+
function agentMessageToEntry(p) {
|
|
529
|
+
const kind = typeof p["kind"] === "string" ? p["kind"] : "text";
|
|
530
|
+
const role = kind === "thinking" ? "thinking" : kind === "tool_use" || kind === "tool_result" ? "tool" : kind === "error" ? "error" : kind === "status" || kind === "system" ? "system" : "assistant";
|
|
531
|
+
return {
|
|
532
|
+
ts: typeof p["ts"] === "string" ? p["ts"] : (/* @__PURE__ */ new Date()).toISOString(),
|
|
533
|
+
role,
|
|
534
|
+
text: typeof p["content"] === "string" ? p["content"] : "",
|
|
535
|
+
...typeof p["toolName"] === "string" ? { tool: p["toolName"] } : {},
|
|
536
|
+
...kind === "error" ? { isError: true } : {}
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
async function readLocalSubagentTranscript(sessionId, subagentId) {
|
|
540
|
+
try {
|
|
541
|
+
const { SessionRegistry } = await import("@wrongstack/core/storage");
|
|
542
|
+
const { resolveWstackPaths, sessionScopedPath } = await import("@wrongstack/core/utils");
|
|
543
|
+
const globalRoot = path2.dirname(resolveHqDataDir2());
|
|
544
|
+
const registry = new SessionRegistry(globalRoot);
|
|
545
|
+
const entry = await registry.get(sessionId).catch(() => null);
|
|
546
|
+
if (!entry) return null;
|
|
547
|
+
const paths = resolveWstackPaths({ projectRoot: entry.projectRoot, globalRoot });
|
|
548
|
+
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);
|
|
551
|
+
if (raw === null) return null;
|
|
552
|
+
const out = [];
|
|
553
|
+
for (const line of raw.split("\n")) {
|
|
554
|
+
const trimmed = line.trim();
|
|
555
|
+
if (trimmed.length === 0) continue;
|
|
556
|
+
try {
|
|
557
|
+
out.push(agentMessageToEntry(JSON.parse(trimmed)));
|
|
558
|
+
} catch {
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return out;
|
|
562
|
+
} catch {
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
function decodePathSegment(value) {
|
|
567
|
+
try {
|
|
568
|
+
return decodeURIComponent(value);
|
|
569
|
+
} catch {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
function displayHost(host) {
|
|
574
|
+
return host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
575
|
+
}
|
|
576
|
+
var RequestBodyTooLargeError = class extends Error {
|
|
577
|
+
};
|
|
578
|
+
function readRequestBody(req, maxBytes = 1e6) {
|
|
579
|
+
return new Promise((resolve, reject) => {
|
|
580
|
+
const chunks = [];
|
|
581
|
+
let size = 0;
|
|
582
|
+
let settled = false;
|
|
583
|
+
req.on("data", (chunk) => {
|
|
584
|
+
if (settled) return;
|
|
585
|
+
size += chunk.length;
|
|
586
|
+
if (size > maxBytes) {
|
|
587
|
+
settled = true;
|
|
588
|
+
chunks.length = 0;
|
|
589
|
+
reject(new RequestBodyTooLargeError("request body too large"));
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
chunks.push(chunk);
|
|
593
|
+
});
|
|
594
|
+
req.on("end", () => {
|
|
595
|
+
if (!settled) resolve(Buffer.concat(chunks).toString("utf8"));
|
|
596
|
+
});
|
|
597
|
+
req.on("error", (error) => {
|
|
598
|
+
if (!settled) reject(error);
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
function writeInvalidBody(res, error) {
|
|
603
|
+
const tooLarge = error instanceof RequestBodyTooLargeError;
|
|
604
|
+
res.writeHead(tooLarge ? 413 : 400, { "Content-Type": "application/json" });
|
|
605
|
+
res.end(JSON.stringify({ error: tooLarge ? "request body too large" : "invalid json body" }));
|
|
606
|
+
}
|
|
607
|
+
function buildHttpUrl(host, port, token) {
|
|
608
|
+
const url = new URL(`http://${displayHost(host)}:${port}/`);
|
|
609
|
+
if (token) url.searchParams.set("token", token);
|
|
610
|
+
return url.toString();
|
|
611
|
+
}
|
|
612
|
+
function buildBootstrapHttpUrl(host, port, bootstrapCode) {
|
|
613
|
+
const url = new URL(`http://${displayHost(host)}:${port}/`);
|
|
614
|
+
url.hash = `bootstrap=${bootstrapCode}`;
|
|
615
|
+
return url.toString();
|
|
616
|
+
}
|
|
617
|
+
function buildClientWsUrl(host, port, token) {
|
|
618
|
+
const url = new URL(`ws://${displayHost(host)}:${port}/ws/client`);
|
|
619
|
+
if (token) url.searchParams.set("token", token);
|
|
620
|
+
return url.toString();
|
|
621
|
+
}
|
|
622
|
+
function hqRuntimeMarkerPath(dataDir) {
|
|
623
|
+
return path2.join(dataDir, "runtime.json");
|
|
624
|
+
}
|
|
625
|
+
function lanIPv4Addresses() {
|
|
626
|
+
const out = [];
|
|
627
|
+
try {
|
|
628
|
+
const ifaces = os.networkInterfaces();
|
|
629
|
+
for (const name of Object.keys(ifaces)) {
|
|
630
|
+
for (const ni of ifaces[name] ?? []) {
|
|
631
|
+
if (ni.family === "IPv4" && !ni.internal) out.push(ni.address);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
} catch {
|
|
635
|
+
}
|
|
636
|
+
return out;
|
|
637
|
+
}
|
|
638
|
+
function truncateHqSummary(value, maxLength) {
|
|
639
|
+
if (typeof value !== "string" || value.length === 0) return void 0;
|
|
640
|
+
if (value.length <= maxLength) return value;
|
|
641
|
+
return `${value.slice(0, maxLength)}\u2026[truncated:${value.length - maxLength}]`;
|
|
642
|
+
}
|
|
643
|
+
function hqMachineKey(hostname, machineId) {
|
|
644
|
+
const hn = hostname?.trim();
|
|
645
|
+
return hn ? `host:${hn.toLowerCase()}` : `mid:${machineId || "local"}`;
|
|
646
|
+
}
|
|
647
|
+
function recordTimeseriesSignal(persistence, event) {
|
|
648
|
+
try {
|
|
649
|
+
if (event.type === "session.usage") {
|
|
650
|
+
const p = event.payload;
|
|
651
|
+
persistence.timeseries.record({
|
|
652
|
+
ts: Date.parse(event.timestamp) || Date.now(),
|
|
653
|
+
...typeof p.costUsd === "number" ? { costUsd: p.costUsd } : {},
|
|
654
|
+
...typeof p.inputTokens === "number" ? { inputTokens: p.inputTokens } : {},
|
|
655
|
+
...typeof p.outputTokens === "number" ? { outputTokens: p.outputTokens } : {},
|
|
656
|
+
...typeof p.model === "string" && p.model.length > 0 ? { model: p.model } : {},
|
|
657
|
+
...typeof p.provider === "string" && p.provider.length > 0 ? { provider: p.provider } : {},
|
|
658
|
+
...typeof p.cacheRead === "number" ? { cacheRead: p.cacheRead } : {},
|
|
659
|
+
...typeof p.cacheWrite === "number" ? { cacheWrite: p.cacheWrite } : {}
|
|
660
|
+
});
|
|
661
|
+
} else if (event.type === "tool.completed") {
|
|
662
|
+
persistence.timeseries.record({
|
|
663
|
+
ts: Date.parse(event.timestamp) || Date.now(),
|
|
664
|
+
toolCalls: 1
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
} catch {
|
|
668
|
+
}
|
|
669
|
+
}
|
|
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
|
+
|
|
685
|
+
// src/hq-server/snapshot.ts
|
|
686
|
+
var HQ_SNAPSHOT_BROADCAST_DEBOUNCE_MS = 100;
|
|
687
|
+
var HQ_SNAPSHOT_PERSIST_MIN_INTERVAL_MS = 3e3;
|
|
688
|
+
var HQ_BROWSER_MAX_BUFFERED_BYTES = 32 * 1024 * 1024;
|
|
689
|
+
function sendGuarded(ws, data) {
|
|
690
|
+
if (ws.readyState !== WebSocket.OPEN) return false;
|
|
691
|
+
const buffered = Number.isFinite(ws.bufferedAmount) ? ws.bufferedAmount : 0;
|
|
692
|
+
if (buffered + Buffer.byteLength(data, "utf8") > HQ_BROWSER_MAX_BUFFERED_BYTES) {
|
|
693
|
+
try {
|
|
694
|
+
ws.terminate();
|
|
695
|
+
} catch {
|
|
696
|
+
try {
|
|
697
|
+
ws.close(1013, "client cannot keep up");
|
|
698
|
+
} catch {
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
703
|
+
try {
|
|
704
|
+
ws.send(data);
|
|
705
|
+
return true;
|
|
706
|
+
} catch {
|
|
707
|
+
return false;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
var HQ_STALE_SNAPSHOT_MS = 5 * 6e4;
|
|
711
|
+
function reapStaleClientState(clients, nowMs = Date.now()) {
|
|
712
|
+
for (const client of clients.values()) {
|
|
713
|
+
for (const [sessionId, tracked] of client.sessions) {
|
|
714
|
+
if (nowMs - tracked.receivedAt > HQ_STALE_SNAPSHOT_MS) client.sessions.delete(sessionId);
|
|
715
|
+
}
|
|
716
|
+
for (const [runId, fleet] of client.fleets) {
|
|
717
|
+
if (nowMs - fleet.receivedAt > HQ_STALE_SNAPSHOT_MS) client.fleets.delete(runId);
|
|
718
|
+
}
|
|
719
|
+
for (const [sessionId, tracked] of client.mcpSnapshots) {
|
|
720
|
+
if (nowMs - tracked.receivedAt > HQ_STALE_SNAPSHOT_MS) client.mcpSnapshots.delete(sessionId);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
function buildSnapshot(clients, options) {
|
|
725
|
+
reapStaleClientState(clients);
|
|
726
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
727
|
+
const clientRecordById = /* @__PURE__ */ new Map();
|
|
728
|
+
const projectMap = /* @__PURE__ */ new Map();
|
|
729
|
+
const mailboxSummaries = [];
|
|
730
|
+
const sessionById = /* @__PURE__ */ new Map();
|
|
731
|
+
const fleetByRunId = /* @__PURE__ */ new Map();
|
|
732
|
+
for (const client of clients.values()) {
|
|
733
|
+
const machineId = client.machineId || client.project.machineId || "";
|
|
734
|
+
if (!clientRecordById.has(client.clientId)) {
|
|
735
|
+
clientRecordById.set(client.clientId, {
|
|
736
|
+
clientId: client.clientId,
|
|
737
|
+
kind: client.kind,
|
|
738
|
+
machineId,
|
|
739
|
+
...client.hostname ? { hostname: client.hostname } : {},
|
|
740
|
+
...client.pid ? { pid: client.pid } : {},
|
|
741
|
+
...client.version ? { version: client.version } : {},
|
|
742
|
+
connected: true,
|
|
743
|
+
connectedAt: client.connectedAt,
|
|
744
|
+
lastSeenAt: client.lastSeenAt,
|
|
745
|
+
projectId: client.projectId,
|
|
746
|
+
capabilities: client.capabilities
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
let project = projectMap.get(client.projectId);
|
|
750
|
+
if (!project) {
|
|
751
|
+
project = {
|
|
752
|
+
projectId: client.projectId,
|
|
753
|
+
projectName: client.project.projectName || client.projectId,
|
|
754
|
+
projectRootDisplay: client.project.projectRoot,
|
|
755
|
+
machineIds: [machineId],
|
|
756
|
+
...client.project.gitBranch ? { gitBranch: client.project.gitBranch } : {},
|
|
757
|
+
activeClients: 0,
|
|
758
|
+
activeSessions: 0,
|
|
759
|
+
activeSubagents: 0,
|
|
760
|
+
totalCostUsd: 0,
|
|
761
|
+
lastActivityAt: now,
|
|
762
|
+
status: "active"
|
|
763
|
+
};
|
|
764
|
+
projectMap.set(client.projectId, project);
|
|
765
|
+
} else if (machineId && !project.machineIds.includes(machineId)) {
|
|
766
|
+
project.machineIds = [...project.machineIds, machineId];
|
|
767
|
+
}
|
|
768
|
+
for (const tracked of client.sessions.values()) {
|
|
769
|
+
const cutoff = Date.now() - 5 * 6e4;
|
|
770
|
+
const agents = tracked.payload.agents.filter((agent) => {
|
|
771
|
+
if (agent.id === "leader") return true;
|
|
772
|
+
const lastActivityAt = Date.parse(agent.lastActivityAt);
|
|
773
|
+
return Number.isFinite(lastActivityAt) && lastActivityAt >= cutoff;
|
|
774
|
+
});
|
|
775
|
+
sessionById.set(tracked.payload.sessionId, {
|
|
776
|
+
...tracked.payload,
|
|
777
|
+
clientId: client.clientId,
|
|
778
|
+
agents,
|
|
779
|
+
agentCount: agents.length
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
for (const snapshot of client.mailboxes.values()) {
|
|
783
|
+
mailboxSummaries.push({
|
|
784
|
+
mailboxId: snapshot.mailboxId,
|
|
785
|
+
projectId: client.projectId,
|
|
786
|
+
scope: snapshot.scope,
|
|
787
|
+
messageCount: snapshot.totals.messages,
|
|
788
|
+
unreadCount: snapshot.totals.unread,
|
|
789
|
+
incompleteCount: snapshot.totals.incomplete,
|
|
790
|
+
highPriorityCount: snapshot.totals.highPriority,
|
|
791
|
+
onlineAgentCount: snapshot.totals.onlineAgents,
|
|
792
|
+
lastActivityAt: now
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
for (const fleet of client.fleets.values()) {
|
|
796
|
+
fleetByRunId.set(fleet.payload.runId, {
|
|
797
|
+
payload: fleet.payload,
|
|
798
|
+
clientId: client.clientId,
|
|
799
|
+
projectId: client.projectId,
|
|
800
|
+
lastActivityAt: new Date(fleet.receivedAt).toISOString()
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
const processKeyOf = (rec) => rec.pid !== void 0 ? `${rec.machineId}:${rec.pid}` : rec.clientId;
|
|
805
|
+
const countedProjectProcesses = /* @__PURE__ */ new Set();
|
|
806
|
+
for (const rec of clientRecordById.values()) {
|
|
807
|
+
const key = `${rec.projectId}|${processKeyOf(rec)}`;
|
|
808
|
+
if (countedProjectProcesses.has(key)) continue;
|
|
809
|
+
countedProjectProcesses.add(key);
|
|
810
|
+
const project = projectMap.get(rec.projectId);
|
|
811
|
+
if (project) project.activeClients++;
|
|
812
|
+
}
|
|
813
|
+
const liveSessions = Array.from(sessionById.values());
|
|
814
|
+
const machineMap = /* @__PURE__ */ new Map();
|
|
815
|
+
let totalAgents = 0;
|
|
816
|
+
let totalSubagents = 0;
|
|
817
|
+
let totalCostUsd = 0;
|
|
818
|
+
for (const session of liveSessions) {
|
|
819
|
+
let project = projectMap.get(session.projectId);
|
|
820
|
+
if (!project) {
|
|
821
|
+
project = {
|
|
822
|
+
projectId: session.projectId,
|
|
823
|
+
projectName: session.projectName || session.projectId,
|
|
824
|
+
projectRootDisplay: session.projectRoot,
|
|
825
|
+
machineIds: [session.machineId],
|
|
826
|
+
...session.gitBranch ? { gitBranch: session.gitBranch } : {},
|
|
827
|
+
activeClients: 0,
|
|
828
|
+
activeSessions: 0,
|
|
829
|
+
activeSubagents: 0,
|
|
830
|
+
totalCostUsd: 0,
|
|
831
|
+
lastActivityAt: session.lastActivityAt,
|
|
832
|
+
status: "active"
|
|
833
|
+
};
|
|
834
|
+
projectMap.set(session.projectId, project);
|
|
835
|
+
} else if (session.machineId && !project.machineIds.includes(session.machineId)) {
|
|
836
|
+
project.machineIds = [...project.machineIds, session.machineId];
|
|
837
|
+
}
|
|
838
|
+
project.activeSessions++;
|
|
839
|
+
let sessionCost = 0;
|
|
840
|
+
for (const agent of session.agents) {
|
|
841
|
+
totalAgents++;
|
|
842
|
+
if (agent.id !== "leader") totalSubagents++;
|
|
843
|
+
if (typeof agent.costUsd === "number") {
|
|
844
|
+
sessionCost += agent.costUsd;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
project.activeSubagents += session.agents.filter((a) => a.id !== "leader").length;
|
|
848
|
+
project.totalCostUsd += sessionCost;
|
|
849
|
+
totalCostUsd += sessionCost;
|
|
850
|
+
const mKey = hqMachineKey(session.hostname, session.machineId);
|
|
851
|
+
let machine = machineMap.get(mKey);
|
|
852
|
+
if (!machine) {
|
|
853
|
+
machine = {
|
|
854
|
+
record: {
|
|
855
|
+
machineId: session.machineId,
|
|
856
|
+
...session.hostname ? { hostname: session.hostname } : {},
|
|
857
|
+
clientCount: 0,
|
|
858
|
+
sessionCount: 0,
|
|
859
|
+
agentCount: 0,
|
|
860
|
+
projectIds: [],
|
|
861
|
+
lastActivityAt: session.lastActivityAt
|
|
862
|
+
},
|
|
863
|
+
projects: /* @__PURE__ */ new Set()
|
|
864
|
+
};
|
|
865
|
+
machineMap.set(mKey, machine);
|
|
866
|
+
}
|
|
867
|
+
machine.record.sessionCount++;
|
|
868
|
+
machine.record.agentCount += session.agents.length;
|
|
869
|
+
machine.projects.add(session.projectId);
|
|
870
|
+
if (session.lastActivityAt > machine.record.lastActivityAt) {
|
|
871
|
+
machine.record.lastActivityAt = session.lastActivityAt;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
const countedMachineProcesses = /* @__PURE__ */ new Set();
|
|
875
|
+
for (const rec of clientRecordById.values()) {
|
|
876
|
+
if (!rec.machineId && !rec.hostname) continue;
|
|
877
|
+
const rKey = hqMachineKey(rec.hostname, rec.machineId);
|
|
878
|
+
let machine = machineMap.get(rKey);
|
|
879
|
+
if (!machine) {
|
|
880
|
+
machine = {
|
|
881
|
+
record: {
|
|
882
|
+
machineId: rec.machineId,
|
|
883
|
+
...rec.hostname ? { hostname: rec.hostname } : {},
|
|
884
|
+
clientCount: 0,
|
|
885
|
+
sessionCount: 0,
|
|
886
|
+
agentCount: 0,
|
|
887
|
+
projectIds: [],
|
|
888
|
+
lastActivityAt: rec.lastSeenAt
|
|
889
|
+
},
|
|
890
|
+
projects: /* @__PURE__ */ new Set()
|
|
891
|
+
};
|
|
892
|
+
machineMap.set(rKey, machine);
|
|
893
|
+
}
|
|
894
|
+
const processKey = `${rKey}|${processKeyOf(rec)}`;
|
|
895
|
+
if (!countedMachineProcesses.has(processKey)) {
|
|
896
|
+
countedMachineProcesses.add(processKey);
|
|
897
|
+
machine.record.clientCount++;
|
|
898
|
+
}
|
|
899
|
+
machine.projects.add(rec.projectId);
|
|
900
|
+
if (rec.hostname && !machine.record.hostname) machine.record.hostname = rec.hostname;
|
|
901
|
+
}
|
|
902
|
+
const machines = Array.from(machineMap.values()).map((m) => ({
|
|
903
|
+
...m.record,
|
|
904
|
+
projectIds: Array.from(m.projects)
|
|
905
|
+
}));
|
|
906
|
+
const clientRecords = Array.from(clientRecordById.values());
|
|
907
|
+
const projects = Array.from(projectMap.values());
|
|
908
|
+
let unread = 0;
|
|
909
|
+
let incomplete = 0;
|
|
910
|
+
for (const m of mailboxSummaries) {
|
|
911
|
+
unread += m.unreadCount;
|
|
912
|
+
incomplete += m.incompleteCount;
|
|
913
|
+
}
|
|
914
|
+
const sessions = liveSessions.map((s) => {
|
|
915
|
+
let sessionCost = 0;
|
|
916
|
+
for (const agent of s.agents) {
|
|
917
|
+
if (typeof agent.costUsd === "number") sessionCost += agent.costUsd;
|
|
918
|
+
}
|
|
919
|
+
const provider = s.agents.find((a) => a.model !== void 0)?.model;
|
|
920
|
+
return {
|
|
921
|
+
sessionId: s.sessionId,
|
|
922
|
+
projectId: s.projectId,
|
|
923
|
+
clientId: s.clientId ?? `${s.machineId}:${s.clientKind}`,
|
|
924
|
+
status: s.status === "active" ? "running" : "idle",
|
|
925
|
+
...provider !== void 0 ? { model: provider } : {},
|
|
926
|
+
startedAt: s.startedAt,
|
|
927
|
+
lastActivityAt: s.lastActivityAt,
|
|
928
|
+
...sessionCost > 0 ? { costUsd: sessionCost } : {}
|
|
929
|
+
};
|
|
930
|
+
});
|
|
931
|
+
const fleets = Array.from(fleetByRunId.values()).map((f) => ({
|
|
932
|
+
runId: f.payload.runId,
|
|
933
|
+
projectId: f.projectId,
|
|
934
|
+
clientId: f.clientId,
|
|
935
|
+
activeSubagents: f.payload.activeSubagents,
|
|
936
|
+
queuedTasks: f.payload.queuedTasks,
|
|
937
|
+
completedTasks: f.payload.completedTasks,
|
|
938
|
+
failedTasks: f.payload.failedTasks,
|
|
939
|
+
...f.payload.totalCostUsd !== void 0 ? { totalCostUsd: f.payload.totalCostUsd } : {},
|
|
940
|
+
lastActivityAt: f.lastActivityAt
|
|
941
|
+
}));
|
|
942
|
+
const mcpServers = [];
|
|
943
|
+
const seenMcp = /* @__PURE__ */ new Set();
|
|
944
|
+
for (const client of clients.values()) {
|
|
945
|
+
for (const [sessionId, tracked] of client.mcpSnapshots.entries()) {
|
|
946
|
+
for (const server of tracked.servers) {
|
|
947
|
+
const key = `${client.clientId}:${sessionId}:${server.name}`;
|
|
948
|
+
if (seenMcp.has(key)) continue;
|
|
949
|
+
seenMcp.add(key);
|
|
950
|
+
mcpServers.push(server);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
return {
|
|
955
|
+
generatedAt: now,
|
|
956
|
+
clients: clientRecords,
|
|
957
|
+
projects,
|
|
958
|
+
sessions,
|
|
959
|
+
fleets,
|
|
960
|
+
mailboxes: mailboxSummaries,
|
|
961
|
+
machines,
|
|
962
|
+
liveSessions,
|
|
963
|
+
mcpServers,
|
|
964
|
+
totals: {
|
|
965
|
+
activeProjects: projects.length,
|
|
966
|
+
activeClients: new Set(clientRecords.map(processKeyOf)).size,
|
|
967
|
+
activeSessions: liveSessions.length,
|
|
968
|
+
activeSubagents: totalSubagents,
|
|
969
|
+
unreadMailboxMessages: unread,
|
|
970
|
+
incompleteMailboxMessages: incomplete,
|
|
971
|
+
totalCostUsd,
|
|
972
|
+
activeMachines: machines.length,
|
|
973
|
+
activeAgents: totalAgents,
|
|
974
|
+
...options?.tokenStats !== void 0 ? { tokenStats: options.tokenStats } : {}
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
function createSnapshotBroadcaster(clients, browsers, persistence) {
|
|
979
|
+
let cached = "";
|
|
980
|
+
let dirty = true;
|
|
981
|
+
let timer = null;
|
|
982
|
+
let closed = false;
|
|
983
|
+
let lastPersistAt = 0;
|
|
984
|
+
const persistCheckpoint = (snapshot) => {
|
|
985
|
+
if (persistence === void 0) return;
|
|
986
|
+
const now = Date.now();
|
|
987
|
+
if (now - lastPersistAt < HQ_SNAPSHOT_PERSIST_MIN_INTERVAL_MS) return;
|
|
988
|
+
lastPersistAt = now;
|
|
989
|
+
persistence.snapshotStore.save(snapshot);
|
|
990
|
+
};
|
|
991
|
+
const serialize = () => {
|
|
992
|
+
if (!dirty && cached.length > 0) return cached;
|
|
993
|
+
const snapshot = buildSnapshot(clients);
|
|
994
|
+
const msg = { type: "hq.snapshot", snapshot };
|
|
995
|
+
cached = JSON.stringify(msg);
|
|
996
|
+
dirty = false;
|
|
997
|
+
persistCheckpoint(snapshot);
|
|
998
|
+
return cached;
|
|
999
|
+
};
|
|
1000
|
+
const flush = () => {
|
|
1001
|
+
timer = null;
|
|
1002
|
+
if (browsers.size === 0) {
|
|
1003
|
+
if (persistence !== void 0 && Date.now() - lastPersistAt >= HQ_SNAPSHOT_PERSIST_MIN_INTERVAL_MS) {
|
|
1004
|
+
persistCheckpoint(buildSnapshot(clients));
|
|
1005
|
+
}
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
const data = serialize();
|
|
1009
|
+
for (const ws of browsers) sendGuarded(ws, data);
|
|
1010
|
+
};
|
|
1011
|
+
return {
|
|
1012
|
+
currentSerialized: serialize,
|
|
1013
|
+
broadcast: () => {
|
|
1014
|
+
if (closed) return;
|
|
1015
|
+
dirty = true;
|
|
1016
|
+
if (timer !== null) return;
|
|
1017
|
+
timer = setTimeout(flush, HQ_SNAPSHOT_BROADCAST_DEBOUNCE_MS);
|
|
1018
|
+
timer.unref?.();
|
|
1019
|
+
},
|
|
1020
|
+
close: () => {
|
|
1021
|
+
closed = true;
|
|
1022
|
+
if (timer !== null) {
|
|
1023
|
+
clearTimeout(timer);
|
|
1024
|
+
timer = null;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
function buildProjectDetail(clients, projectId) {
|
|
1030
|
+
const projectClients = [];
|
|
1031
|
+
for (const c of clients.values()) {
|
|
1032
|
+
if (c.projectId === projectId) projectClients.push(c);
|
|
1033
|
+
}
|
|
1034
|
+
if (projectClients.length === 0) return null;
|
|
1035
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1036
|
+
const clientRecords = projectClients.map((c) => ({
|
|
1037
|
+
clientId: c.clientId,
|
|
1038
|
+
kind: c.kind,
|
|
1039
|
+
machineId: "",
|
|
1040
|
+
...c.hostname ? { hostname: c.hostname } : {},
|
|
1041
|
+
...c.pid ? { pid: c.pid } : {},
|
|
1042
|
+
...c.version ? { version: c.version } : {},
|
|
1043
|
+
connected: true,
|
|
1044
|
+
connectedAt: c.connectedAt,
|
|
1045
|
+
lastSeenAt: c.lastSeenAt,
|
|
1046
|
+
projectId: c.projectId,
|
|
1047
|
+
capabilities: c.capabilities
|
|
1048
|
+
}));
|
|
1049
|
+
const mailboxPayloads = [];
|
|
1050
|
+
let latestActivity = now;
|
|
1051
|
+
for (const c of projectClients) {
|
|
1052
|
+
for (const snap of c.mailboxes.values()) {
|
|
1053
|
+
mailboxPayloads.push(snap);
|
|
1054
|
+
if (snap.totals.messages > 0) latestActivity = now;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
const primaryProject = projectClients[0].project;
|
|
1058
|
+
const machineIds = Array.from(new Set(projectClients.map((client) => client.project.machineId)));
|
|
1059
|
+
const project = {
|
|
1060
|
+
projectId,
|
|
1061
|
+
projectName: primaryProject.projectName || projectId,
|
|
1062
|
+
projectRootDisplay: primaryProject.projectRoot,
|
|
1063
|
+
machineIds,
|
|
1064
|
+
...primaryProject.gitBranch ? { gitBranch: primaryProject.gitBranch } : {},
|
|
1065
|
+
activeClients: projectClients.length,
|
|
1066
|
+
activeSessions: 0,
|
|
1067
|
+
activeSubagents: 0,
|
|
1068
|
+
totalCostUsd: 0,
|
|
1069
|
+
lastActivityAt: latestActivity,
|
|
1070
|
+
status: "active"
|
|
1071
|
+
};
|
|
1072
|
+
return {
|
|
1073
|
+
generatedAt: now,
|
|
1074
|
+
project,
|
|
1075
|
+
clients: clientRecords,
|
|
1076
|
+
mailboxes: mailboxPayloads
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
function broadcastEvent(event, browsers) {
|
|
1080
|
+
const msg = { type: "hq.event", event };
|
|
1081
|
+
const data = JSON.stringify(msg);
|
|
1082
|
+
for (const ws of browsers) sendGuarded(ws, data);
|
|
1083
|
+
}
|
|
1084
|
+
function broadcastCommandStatus(command, browsers) {
|
|
1085
|
+
const data = JSON.stringify({ type: "hq.command_status", command });
|
|
1086
|
+
for (const ws of browsers) sendGuarded(ws, data);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// src/hq-server/trust-boundary.ts
|
|
1090
|
+
import { isTrustDecisionAllowed } from "@wrongstack/core/security";
|
|
1091
|
+
async function authorizeHqCommand(input) {
|
|
1092
|
+
const { boundary, command, commandId, enqueuedBy, authMethod, target } = input;
|
|
1093
|
+
const subject = command.type === "run-command" ? {
|
|
1094
|
+
kind: "command",
|
|
1095
|
+
id: command.command,
|
|
1096
|
+
attributes: { targetClientId: target.clientId }
|
|
1097
|
+
} : {
|
|
1098
|
+
kind: "custom",
|
|
1099
|
+
id: command.type,
|
|
1100
|
+
attributes: { targetClientId: target.clientId }
|
|
1101
|
+
};
|
|
1102
|
+
const request = {
|
|
1103
|
+
version: 1,
|
|
1104
|
+
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 },
|
|
1110
|
+
surface: "hq",
|
|
1111
|
+
capability: command.type === "run-command" ? "hq.control.execute" : "hq.control.enqueue",
|
|
1112
|
+
subject,
|
|
1113
|
+
risk: command.type === "run-command" ? "critical" : "high",
|
|
1114
|
+
scope: { projectId: target.projectId },
|
|
1115
|
+
authContext: { method: authMethod, principalId: enqueuedBy },
|
|
1116
|
+
metadata: { commandType: command.type, targetKind: target.kind }
|
|
1117
|
+
};
|
|
1118
|
+
const decision = await boundary.evaluate(request);
|
|
1119
|
+
console.warn(
|
|
1120
|
+
JSON.stringify({
|
|
1121
|
+
level: "info",
|
|
1122
|
+
event: "hq.trust_boundary.decision",
|
|
1123
|
+
requestId: commandId,
|
|
1124
|
+
capability: request.capability,
|
|
1125
|
+
decision: decision.kind,
|
|
1126
|
+
policyId: decision.policyId,
|
|
1127
|
+
actor: enqueuedBy,
|
|
1128
|
+
clientId: target.clientId,
|
|
1129
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1130
|
+
})
|
|
1131
|
+
);
|
|
1132
|
+
return { allowed: isTrustDecisionAllowed(decision), reason: decision.reason };
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// src/hq-server/routes/auth-handlers.ts
|
|
1136
|
+
import { randomUUID } from "node:crypto";
|
|
1137
|
+
import {
|
|
1138
|
+
DEFAULT_HQ_REDACTION_POLICY as DEFAULT_HQ_REDACTION_POLICY2,
|
|
1139
|
+
hashHqPassword,
|
|
1140
|
+
isLoopbackHost,
|
|
1141
|
+
mintHqCookieSecret,
|
|
1142
|
+
mutateHqAuthFile,
|
|
1143
|
+
verifyHqPassword
|
|
1144
|
+
} from "@wrongstack/core/hq";
|
|
1145
|
+
function isLoopbackRequest(req) {
|
|
1146
|
+
const address = req.socket.remoteAddress?.replace(/^::ffff:/, "");
|
|
1147
|
+
return address !== void 0 && isLoopbackHost(address);
|
|
1148
|
+
}
|
|
1149
|
+
async function handleApiAuthStatus(req, res, url, mutableAuth, sessions, requireBrowserAuth, trustedPublicOrigins, secureCookies) {
|
|
1150
|
+
const auth = authenticateBrowserRequest(req, url, mutableAuth, sessions);
|
|
1151
|
+
const openMode = mutableAuth.browserTokens.size === 0 && mutableAuth.passwordHash === void 0;
|
|
1152
|
+
const localOpenMode = openMode && !requireBrowserAuth && isLoopbackRequest(req);
|
|
1153
|
+
const publicOrigin = trustedPublicOrigins.values().next().value;
|
|
1154
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1155
|
+
res.end(
|
|
1156
|
+
JSON.stringify({
|
|
1157
|
+
tokenMode: mutableAuth.browserTokens.size > 0,
|
|
1158
|
+
passwordMode: mutableAuth.passwordHash !== void 0,
|
|
1159
|
+
publicRelay: requireBrowserAuth === true || publicOrigin !== void 0,
|
|
1160
|
+
...publicOrigin !== void 0 ? { publicOrigin } : {},
|
|
1161
|
+
secureCookies: secureCookies === true,
|
|
1162
|
+
loggedIn: auth !== void 0 || localOpenMode,
|
|
1163
|
+
authKind: isCookieAuth(auth) ? "password" : isTokenAuth(auth) ? "token" : localOpenMode ? "open" : void 0
|
|
1164
|
+
})
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
async function handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, secureCookies) {
|
|
1168
|
+
if (!mutableAuth.passwordHash) {
|
|
1169
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1170
|
+
res.end(
|
|
1171
|
+
JSON.stringify({
|
|
1172
|
+
error: {
|
|
1173
|
+
code: "PASSWORD_NOT_CONFIGURED",
|
|
1174
|
+
message: "Password login is not enabled on this HQ server."
|
|
1175
|
+
}
|
|
1176
|
+
})
|
|
1177
|
+
);
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
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);
|
|
1184
|
+
res.writeHead(429, {
|
|
1185
|
+
"Content-Type": "application/json",
|
|
1186
|
+
"Retry-After": String(retryAfter)
|
|
1187
|
+
});
|
|
1188
|
+
res.end(
|
|
1189
|
+
JSON.stringify({
|
|
1190
|
+
error: {
|
|
1191
|
+
code: "RATE_LIMITED",
|
|
1192
|
+
message: `Too many failed login attempts. Retry after ${retryAfter} seconds.`
|
|
1193
|
+
}
|
|
1194
|
+
})
|
|
1195
|
+
);
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
let body;
|
|
1199
|
+
try {
|
|
1200
|
+
body = JSON.parse(await readRequestBody(req));
|
|
1201
|
+
} catch (error) {
|
|
1202
|
+
writeInvalidBody(res, error);
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
if (typeof body.password !== "string" || body.password.length === 0) {
|
|
1206
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1207
|
+
res.end(
|
|
1208
|
+
JSON.stringify({ error: { code: "BAD_REQUEST", message: "password is required" } })
|
|
1209
|
+
);
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
const ok = await verifyHqPassword(body.password, mutableAuth.passwordHash);
|
|
1213
|
+
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
|
+
});
|
|
1222
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1223
|
+
res.end(
|
|
1224
|
+
JSON.stringify({ error: { code: "INVALID_PASSWORD", message: "Invalid password." } })
|
|
1225
|
+
);
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
loginAttempts.delete(clientIp);
|
|
1229
|
+
const sessionId = randomUUID();
|
|
1230
|
+
sessions.set(sessionId, { createdAt: Date.now(), kind: "password" });
|
|
1231
|
+
setHqSessionCookie(
|
|
1232
|
+
res,
|
|
1233
|
+
serializeHqSessionCookie(sessionId, mutableAuth.cookieSecret),
|
|
1234
|
+
secureCookies
|
|
1235
|
+
);
|
|
1236
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1237
|
+
res.end(JSON.stringify({ loggedIn: true }));
|
|
1238
|
+
}
|
|
1239
|
+
async function handleApiLogout(req, res, mutableAuth, sessions, secureCookies) {
|
|
1240
|
+
const auth = authenticateBrowserRequest(
|
|
1241
|
+
req,
|
|
1242
|
+
new URL(req.url ?? "/", "http://localhost"),
|
|
1243
|
+
mutableAuth,
|
|
1244
|
+
sessions
|
|
1245
|
+
);
|
|
1246
|
+
if (isCookieAuth(auth)) {
|
|
1247
|
+
const cookies = parseCookieHeader(req.headers.cookie);
|
|
1248
|
+
const raw = cookies[HQ_SESSION_COOKIE];
|
|
1249
|
+
if (raw) {
|
|
1250
|
+
const sessionId = parseHqSessionCookie(raw, mutableAuth.cookieSecret ?? "");
|
|
1251
|
+
if (sessionId) sessions.delete(sessionId);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
clearHqSessionCookie(res, secureCookies);
|
|
1255
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1256
|
+
res.end(JSON.stringify({ loggedIn: false }));
|
|
1257
|
+
}
|
|
1258
|
+
async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secureCookies, requireBrowserAuth) {
|
|
1259
|
+
const auth = authenticateBrowserRequest(
|
|
1260
|
+
req,
|
|
1261
|
+
new URL(req.url ?? "/", "http://localhost"),
|
|
1262
|
+
mutableAuth,
|
|
1263
|
+
sessions
|
|
1264
|
+
);
|
|
1265
|
+
const localOpenBootstrap = auth === void 0 && req.method === "POST" && mutableAuth.browserTokens.size === 0 && mutableAuth.passwordHash === void 0 && isLoopbackRequest(req);
|
|
1266
|
+
if (auth === void 0 && !localOpenBootstrap) {
|
|
1267
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1268
|
+
res.end(
|
|
1269
|
+
JSON.stringify({
|
|
1270
|
+
error: {
|
|
1271
|
+
code: "AUTH_REQUIRED",
|
|
1272
|
+
message: "A browser token or password session is required to manage the password."
|
|
1273
|
+
}
|
|
1274
|
+
})
|
|
1275
|
+
);
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
let body = {};
|
|
1279
|
+
try {
|
|
1280
|
+
body = JSON.parse(await readRequestBody(req));
|
|
1281
|
+
} catch {
|
|
1282
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1283
|
+
res.end(
|
|
1284
|
+
JSON.stringify({ error: { code: "BAD_REQUEST", message: "Invalid JSON body." } })
|
|
1285
|
+
);
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
if (isCookieAuth(auth) && mutableAuth.passwordHash !== void 0) {
|
|
1289
|
+
const currentPassword = typeof body.currentPassword === "string" ? body.currentPassword : "";
|
|
1290
|
+
if (!currentPassword || !await verifyHqPassword(currentPassword, mutableAuth.passwordHash)) {
|
|
1291
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1292
|
+
res.end(
|
|
1293
|
+
JSON.stringify({
|
|
1294
|
+
error: { code: "INVALID_CURRENT_PASSWORD", message: "Current password is invalid." }
|
|
1295
|
+
})
|
|
1296
|
+
);
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
if (req.method === "DELETE") {
|
|
1301
|
+
if (requireBrowserAuth && mutableAuth.browserTokens.size === 0) {
|
|
1302
|
+
res.writeHead(409, { "Content-Type": "application/json" });
|
|
1303
|
+
res.end(
|
|
1304
|
+
JSON.stringify({
|
|
1305
|
+
error: {
|
|
1306
|
+
code: "BROWSER_AUTH_REQUIRED",
|
|
1307
|
+
message: "Cannot remove the only browser authentication method while a public relay is active."
|
|
1308
|
+
}
|
|
1309
|
+
})
|
|
1310
|
+
);
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
const next2 = await mutateHqAuthFile(dataDir, (current) => {
|
|
1314
|
+
const updated = { ...current };
|
|
1315
|
+
delete updated.passwordHash;
|
|
1316
|
+
delete updated.cookieSecret;
|
|
1317
|
+
return updated;
|
|
1318
|
+
});
|
|
1319
|
+
applyAuthFile(mutableAuth, next2);
|
|
1320
|
+
sessions.clear();
|
|
1321
|
+
clearHqSessionCookie(res, secureCookies);
|
|
1322
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1323
|
+
res.end(
|
|
1324
|
+
JSON.stringify({
|
|
1325
|
+
passwordMode: false,
|
|
1326
|
+
tokenMode: mutableAuth.browserTokens.size > 0,
|
|
1327
|
+
loggedIn: !isCookieAuth(auth) && isTokenAuth(auth)
|
|
1328
|
+
})
|
|
1329
|
+
);
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
const newPassword = typeof body.newPassword === "string" ? body.newPassword : "";
|
|
1333
|
+
if (newPassword.length < 8 || newPassword.length > 1024) {
|
|
1334
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1335
|
+
res.end(
|
|
1336
|
+
JSON.stringify({
|
|
1337
|
+
error: { code: "INVALID_PASSWORD", message: "New password must be between 8 and 1024 characters." }
|
|
1338
|
+
})
|
|
1339
|
+
);
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
const passwordHash = await hashHqPassword(newPassword);
|
|
1343
|
+
const cookieSecret = mintHqCookieSecret();
|
|
1344
|
+
const next = await mutateHqAuthFile(dataDir, (current) => ({
|
|
1345
|
+
...current,
|
|
1346
|
+
passwordHash,
|
|
1347
|
+
cookieSecret
|
|
1348
|
+
}));
|
|
1349
|
+
applyAuthFile(mutableAuth, next);
|
|
1350
|
+
sessions.clear();
|
|
1351
|
+
if (isCookieAuth(auth) || localOpenBootstrap) {
|
|
1352
|
+
const sessionId = randomUUID();
|
|
1353
|
+
sessions.set(sessionId, { createdAt: Date.now(), kind: "password" });
|
|
1354
|
+
setHqSessionCookie(
|
|
1355
|
+
res,
|
|
1356
|
+
serializeHqSessionCookie(sessionId, cookieSecret),
|
|
1357
|
+
secureCookies
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1361
|
+
res.end(
|
|
1362
|
+
JSON.stringify({
|
|
1363
|
+
passwordMode: true,
|
|
1364
|
+
tokenMode: mutableAuth.browserTokens.size > 0,
|
|
1365
|
+
loggedIn: true
|
|
1366
|
+
})
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
function applyAuthFile(mutableAuth, next) {
|
|
1370
|
+
mutableAuth.operatorPolicy = {
|
|
1371
|
+
...DEFAULT_HQ_REDACTION_POLICY2,
|
|
1372
|
+
...next.redactionPolicy ?? {}
|
|
1373
|
+
};
|
|
1374
|
+
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));
|
|
1377
|
+
mutableAuth.browserTokenObjs = new Map(
|
|
1378
|
+
(next.browserTokens ?? []).map((t) => [
|
|
1379
|
+
t.token,
|
|
1380
|
+
{ id: t.id, ...t.capabilities !== void 0 ? { capabilities: t.capabilities } : {} }
|
|
1381
|
+
])
|
|
1382
|
+
);
|
|
1383
|
+
mutableAuth.clientTokenObjs = new Map(
|
|
1384
|
+
(next.clientTokens ?? []).map((token) => [token.token, token])
|
|
1385
|
+
);
|
|
1386
|
+
mutableAuth.passwordHash = next.passwordHash;
|
|
1387
|
+
mutableAuth.cookieSecret = next.cookieSecret;
|
|
1388
|
+
mutableAuth.alertRules = next.alertRules;
|
|
1389
|
+
}
|
|
1390
|
+
async function handleApiBootstrap(req, res, mutableAuth, sessions, secureCookies, bootstrapStore) {
|
|
1391
|
+
if (!mutableAuth.cookieSecret) {
|
|
1392
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1393
|
+
res.end(
|
|
1394
|
+
JSON.stringify({
|
|
1395
|
+
error: {
|
|
1396
|
+
code: "BOOTSTRAP_UNAVAILABLE",
|
|
1397
|
+
message: "Cookie signing is not configured on this HQ server."
|
|
1398
|
+
}
|
|
1399
|
+
})
|
|
1400
|
+
);
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
let body;
|
|
1404
|
+
try {
|
|
1405
|
+
body = JSON.parse(await readRequestBody(req));
|
|
1406
|
+
} catch (error) {
|
|
1407
|
+
writeInvalidBody(res, error);
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
if (typeof body.code !== "string" || body.code.length === 0) {
|
|
1411
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1412
|
+
res.end(
|
|
1413
|
+
JSON.stringify({ error: { code: "BAD_REQUEST", message: "code is required" } })
|
|
1414
|
+
);
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
const entry = bootstrapStore.consume(body.code);
|
|
1418
|
+
if (!entry) {
|
|
1419
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1420
|
+
res.end(
|
|
1421
|
+
JSON.stringify({
|
|
1422
|
+
error: {
|
|
1423
|
+
code: "INVALID_OR_EXPIRED_CODE",
|
|
1424
|
+
message: "The bootstrap code is invalid, already used, or expired."
|
|
1425
|
+
}
|
|
1426
|
+
})
|
|
1427
|
+
);
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
const tokenLive = [...mutableAuth.browserTokenObjs.values()].some(
|
|
1431
|
+
(obj) => obj.id === entry.tokenId
|
|
1432
|
+
);
|
|
1433
|
+
if (!tokenLive) {
|
|
1434
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1435
|
+
res.end(
|
|
1436
|
+
JSON.stringify({
|
|
1437
|
+
error: {
|
|
1438
|
+
code: "TOKEN_REVOKED",
|
|
1439
|
+
message: "The token that issued this bootstrap code has been revoked."
|
|
1440
|
+
}
|
|
1441
|
+
})
|
|
1442
|
+
);
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
const sessionId = randomUUID();
|
|
1446
|
+
sessions.set(sessionId, {
|
|
1447
|
+
createdAt: Date.now(),
|
|
1448
|
+
kind: "token",
|
|
1449
|
+
tokenId: entry.tokenId,
|
|
1450
|
+
...entry.capabilities !== void 0 ? { capabilities: entry.capabilities } : {}
|
|
1451
|
+
});
|
|
1452
|
+
setHqSessionCookie(
|
|
1453
|
+
res,
|
|
1454
|
+
serializeHqSessionCookie(sessionId, mutableAuth.cookieSecret),
|
|
1455
|
+
secureCookies
|
|
1456
|
+
);
|
|
1457
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1458
|
+
res.end(JSON.stringify({ loggedIn: true }));
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// src/hq-server/routes/system-handlers.ts
|
|
1462
|
+
async function handleApiSystemUpdate(res) {
|
|
1463
|
+
const [{ checkForUpdate }, { detectUpdatePackageName }] = await Promise.all([
|
|
1464
|
+
import("./update-check-UMYTOH7Z.js"),
|
|
1465
|
+
import("./update-YC4TYW2Y.js")
|
|
1466
|
+
]);
|
|
1467
|
+
const packageName = detectUpdatePackageName();
|
|
1468
|
+
const info = await checkForUpdate({ packageName });
|
|
1469
|
+
res.writeHead(200, {
|
|
1470
|
+
"Content-Type": "application/json",
|
|
1471
|
+
"Cache-Control": "no-store"
|
|
1472
|
+
});
|
|
1473
|
+
res.end(
|
|
1474
|
+
JSON.stringify({
|
|
1475
|
+
...info,
|
|
1476
|
+
packageName,
|
|
1477
|
+
command: "wstack update"
|
|
1478
|
+
})
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
async function handleApiSystemHealth(res, clients, persistence, eventLog) {
|
|
1482
|
+
const now = Date.now();
|
|
1483
|
+
const eventCount = eventLog.length;
|
|
1484
|
+
let timeseriesStoreStatus = "unknown";
|
|
1485
|
+
let kanbanStoreStatus = "unknown";
|
|
1486
|
+
try {
|
|
1487
|
+
const tsOk = await persistence.timeseries.read(now - 3e5);
|
|
1488
|
+
timeseriesStoreStatus = Array.isArray(tsOk) ? "ok" : "degraded";
|
|
1489
|
+
} catch {
|
|
1490
|
+
timeseriesStoreStatus = "degraded";
|
|
1491
|
+
}
|
|
1492
|
+
try {
|
|
1493
|
+
await persistence.kanban.load("__hq_health_probe__");
|
|
1494
|
+
kanbanStoreStatus = "ok";
|
|
1495
|
+
} catch {
|
|
1496
|
+
kanbanStoreStatus = "degraded";
|
|
1497
|
+
}
|
|
1498
|
+
const inMemoryEventLogActive = true;
|
|
1499
|
+
const allOk = inMemoryEventLogActive && timeseriesStoreStatus === "ok" && kanbanStoreStatus === "ok";
|
|
1500
|
+
const clientCount = clients.size;
|
|
1501
|
+
const connectedClients = [...clients.values()].filter(
|
|
1502
|
+
(c) => Date.now() - new Date(c.lastSeenAt).getTime() < 6e4
|
|
1503
|
+
).length;
|
|
1504
|
+
res.writeHead(200, {
|
|
1505
|
+
"Content-Type": "application/json",
|
|
1506
|
+
"Cache-Control": "no-store"
|
|
1507
|
+
});
|
|
1508
|
+
res.end(
|
|
1509
|
+
JSON.stringify({
|
|
1510
|
+
status: allOk ? "healthy" : "degraded",
|
|
1511
|
+
uptime: {
|
|
1512
|
+
serverTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1513
|
+
eventLogSize: eventCount
|
|
1514
|
+
},
|
|
1515
|
+
stores: {
|
|
1516
|
+
events: "ok",
|
|
1517
|
+
timeseries: timeseriesStoreStatus,
|
|
1518
|
+
kanban: kanbanStoreStatus
|
|
1519
|
+
},
|
|
1520
|
+
connections: {
|
|
1521
|
+
total: clientCount,
|
|
1522
|
+
active: connectedClients,
|
|
1523
|
+
stale: clientCount - connectedClients
|
|
1524
|
+
}
|
|
1525
|
+
})
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
async function handleApiCommandsAudit(req, res, auditLog) {
|
|
1529
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
1530
|
+
const rawLimit = Number.parseInt(url.searchParams.get("limit") ?? "200", 10);
|
|
1531
|
+
const limit = Math.min(1e3, Math.max(1, Number.isFinite(rawLimit) ? rawLimit : 200));
|
|
1532
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1533
|
+
res.end(JSON.stringify({ commands: auditLog.recent(limit) }));
|
|
1534
|
+
}
|
|
1535
|
+
async function handleApiAlerts(req, res, alertEngine) {
|
|
1536
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
1537
|
+
const rawLimit = Number.parseInt(url.searchParams.get("limit") ?? "100", 10);
|
|
1538
|
+
const limit = Math.min(500, Math.max(1, Number.isFinite(rawLimit) ? rawLimit : 100));
|
|
1539
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1540
|
+
res.end(
|
|
1541
|
+
JSON.stringify({
|
|
1542
|
+
active: alertEngine.activeAlerts(),
|
|
1543
|
+
history: alertEngine.recentAlerts(limit)
|
|
1544
|
+
})
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// src/hq-server/routes/session-handlers.ts
|
|
1549
|
+
import * as path3 from "node:path";
|
|
1550
|
+
import {
|
|
1551
|
+
buildTranscriptFromEvents,
|
|
1552
|
+
resolveHqDataDir as resolveHqDataDir3
|
|
1553
|
+
} from "@wrongstack/core/hq";
|
|
1554
|
+
async function handleApiSessions(res) {
|
|
1555
|
+
const { SessionRegistry } = await import("@wrongstack/core/storage");
|
|
1556
|
+
const globalRoot = path3.dirname(resolveHqDataDir3());
|
|
1557
|
+
try {
|
|
1558
|
+
const registry = new SessionRegistry(globalRoot);
|
|
1559
|
+
const sessions = await registry.list();
|
|
1560
|
+
const result = sessions.filter((s) => s.status !== "stale").map((s) => ({
|
|
1561
|
+
sessionId: s.sessionId,
|
|
1562
|
+
projectSlug: s.projectSlug,
|
|
1563
|
+
projectName: s.projectName,
|
|
1564
|
+
projectRoot: s.projectRoot,
|
|
1565
|
+
workingDir: s.workingDir,
|
|
1566
|
+
status: s.status,
|
|
1567
|
+
pid: s.pid,
|
|
1568
|
+
startedAt: s.startedAt,
|
|
1569
|
+
lastHeartbeatAt: s.lastHeartbeatAt,
|
|
1570
|
+
agentCount: s.agentCount,
|
|
1571
|
+
agents: s.agents.map((a) => ({
|
|
1572
|
+
id: a.id,
|
|
1573
|
+
name: a.name,
|
|
1574
|
+
status: a.status,
|
|
1575
|
+
currentTool: a.currentTool,
|
|
1576
|
+
iterations: a.iterations,
|
|
1577
|
+
toolCalls: a.toolCalls,
|
|
1578
|
+
lastActivityAt: a.lastActivityAt
|
|
1579
|
+
}))
|
|
1580
|
+
}));
|
|
1581
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1582
|
+
res.end(JSON.stringify(result));
|
|
1583
|
+
} catch (err) {
|
|
1584
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1585
|
+
res.end(JSON.stringify({ error: sanitizeApiError(err) }));
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
async function handleApiSessionEvents(req, res, match, transcripts) {
|
|
1589
|
+
const { SessionRegistry, DefaultSessionStore } = await import("@wrongstack/core/storage");
|
|
1590
|
+
const { resolveWstackPaths } = await import("@wrongstack/core/utils");
|
|
1591
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
1592
|
+
const full = url.searchParams.get("full") === "1";
|
|
1593
|
+
const rawLimit = Number.parseInt(url.searchParams.get("limit") ?? "200", 10);
|
|
1594
|
+
const limit = Math.min(5e3, Math.max(1, Number.isFinite(rawLimit) ? rawLimit : 200));
|
|
1595
|
+
const sessionId = decodePathSegment(match[1]);
|
|
1596
|
+
if (sessionId === null) {
|
|
1597
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1598
|
+
res.end(JSON.stringify({ error: "invalid sessionId encoding" }));
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
const globalRoot = path3.dirname(resolveHqDataDir3());
|
|
1602
|
+
try {
|
|
1603
|
+
const registry = new SessionRegistry(globalRoot);
|
|
1604
|
+
const entry = await registry.get(sessionId).catch(() => null);
|
|
1605
|
+
let entries = [];
|
|
1606
|
+
let source = "stream";
|
|
1607
|
+
let status;
|
|
1608
|
+
let clientType;
|
|
1609
|
+
let projectName;
|
|
1610
|
+
if (entry && "projectRoot" in entry) {
|
|
1611
|
+
const paths = resolveWstackPaths({
|
|
1612
|
+
projectRoot: entry.projectRoot,
|
|
1613
|
+
globalRoot
|
|
1614
|
+
});
|
|
1615
|
+
const store = new DefaultSessionStore({ dir: paths.projectSessions });
|
|
1616
|
+
const data = await store.load(sessionId).catch(() => null);
|
|
1617
|
+
if (data) {
|
|
1618
|
+
entries = buildTranscriptFromEvents(
|
|
1619
|
+
data.events.map((e) => e)
|
|
1620
|
+
);
|
|
1621
|
+
source = "disk";
|
|
1622
|
+
status = entry.status;
|
|
1623
|
+
clientType = entry.clientType;
|
|
1624
|
+
projectName = entry.projectName;
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
if (entries.length === 0) {
|
|
1628
|
+
const ring = transcripts.get(sessionId);
|
|
1629
|
+
if (!ring && !entry) {
|
|
1630
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1631
|
+
res.end(JSON.stringify({ error: "Session not found" }));
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
entries = ring ? ring.entries : [];
|
|
1635
|
+
source = "stream";
|
|
1636
|
+
if (entry) {
|
|
1637
|
+
status = entry.status;
|
|
1638
|
+
clientType = entry.clientType;
|
|
1639
|
+
projectName = entry.projectName;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
const total = entries.length;
|
|
1643
|
+
const tail = full ? entries : entries.slice(-limit);
|
|
1644
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1645
|
+
res.end(
|
|
1646
|
+
JSON.stringify({
|
|
1647
|
+
sessionId,
|
|
1648
|
+
source,
|
|
1649
|
+
...status !== void 0 ? { status } : {},
|
|
1650
|
+
...clientType !== void 0 ? { clientType } : {},
|
|
1651
|
+
...projectName !== void 0 ? { projectName } : {},
|
|
1652
|
+
total,
|
|
1653
|
+
entries: tail
|
|
1654
|
+
})
|
|
1655
|
+
);
|
|
1656
|
+
} catch (err) {
|
|
1657
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1658
|
+
res.end(JSON.stringify({ error: sanitizeApiError(err) }));
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
async function handleApiSessionAgentMessages(req, res, match, agentMessages) {
|
|
1662
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
1663
|
+
const sid = decodePathSegment(match[1]);
|
|
1664
|
+
const aid = decodePathSegment(match[2]);
|
|
1665
|
+
if (sid === null || aid === null) {
|
|
1666
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1667
|
+
res.end(JSON.stringify({ error: "invalid session or agent id encoding" }));
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
const full = url.searchParams.get("full") === "1";
|
|
1671
|
+
const disk = await readLocalSubagentTranscript(sid, aid);
|
|
1672
|
+
const source = disk !== null ? "disk" : "stream";
|
|
1673
|
+
const all = disk !== null ? disk : agentMessages.get(agentRingKey(sid, aid)) ?? agentMessages.get(aid) ?? [];
|
|
1674
|
+
const entries = full ? all : all.slice(-200);
|
|
1675
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1676
|
+
res.end(JSON.stringify({ subagentId: aid, sessionId: sid, source, total: all.length, entries }));
|
|
1677
|
+
}
|
|
1678
|
+
async function handleApiAgentMessages(req, res, match, agentMessages) {
|
|
1679
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
1680
|
+
const id = decodePathSegment(match[1]);
|
|
1681
|
+
if (id === null) {
|
|
1682
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1683
|
+
res.end(JSON.stringify({ error: "invalid agent id encoding" }));
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
const full = url.searchParams.get("full") === "1";
|
|
1687
|
+
const merged = [];
|
|
1688
|
+
for (const [key, ring] of agentMessages) {
|
|
1689
|
+
if (key === id || key.endsWith(`::${id}`)) merged.push(...ring);
|
|
1690
|
+
}
|
|
1691
|
+
merged.sort((a, b) => a.ts.localeCompare(b.ts));
|
|
1692
|
+
const entries = full ? merged : merged.slice(-200);
|
|
1693
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1694
|
+
res.end(JSON.stringify({ subagentId: id, total: merged.length, entries }));
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
// src/hq-server/project-root.ts
|
|
1698
|
+
import { deriveHqProjectId } from "@wrongstack/core/hq";
|
|
1699
|
+
function resolveHqProjectRoot(globalRoot, ids) {
|
|
1700
|
+
const fn = async () => {
|
|
1701
|
+
const { SessionRegistry } = await import("@wrongstack/core/storage");
|
|
1702
|
+
try {
|
|
1703
|
+
const registry = new SessionRegistry(globalRoot);
|
|
1704
|
+
if (typeof ids.sessionId === "string") {
|
|
1705
|
+
const entry = await registry.get(ids.sessionId).catch(() => null);
|
|
1706
|
+
if (entry?.projectRoot) return entry.projectRoot;
|
|
1707
|
+
}
|
|
1708
|
+
if (typeof ids.projectId === "string") {
|
|
1709
|
+
const { createHash } = await import("node:crypto");
|
|
1710
|
+
const all = await registry.list().catch(() => []);
|
|
1711
|
+
const projectIds = /* @__PURE__ */ new Map();
|
|
1712
|
+
const projectIdForRoot = (projectRoot) => {
|
|
1713
|
+
const cached = projectIds.get(projectRoot);
|
|
1714
|
+
if (cached !== void 0) return cached;
|
|
1715
|
+
const derived = deriveHqProjectId(projectRoot);
|
|
1716
|
+
projectIds.set(projectRoot, derived);
|
|
1717
|
+
return derived;
|
|
1718
|
+
};
|
|
1719
|
+
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
|
|
1721
|
+
);
|
|
1722
|
+
if (match) return match.projectRoot;
|
|
1723
|
+
}
|
|
1724
|
+
} catch {
|
|
1725
|
+
}
|
|
1726
|
+
return void 0;
|
|
1727
|
+
};
|
|
1728
|
+
return fn();
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// src/hq-server/routes.ts
|
|
1732
|
+
var setHqSecurityHeaders2 = setHqSecurityHeaders;
|
|
1733
|
+
var hasTrustedBrowserOrigin2 = hasTrustedBrowserOrigin;
|
|
1734
|
+
var authenticateBrowserRequest2 = authenticateBrowserRequest;
|
|
1735
|
+
var isTokenAuth2 = isTokenAuth;
|
|
1736
|
+
var isCookieAuth2 = isCookieAuth;
|
|
1737
|
+
var parseHqSessionCookie2 = parseHqSessionCookie;
|
|
1738
|
+
var parseCookieHeader2 = parseCookieHeader;
|
|
1739
|
+
var HQ_SESSION_COOKIE2 = HQ_SESSION_COOKIE;
|
|
1740
|
+
var decodePathSegment2 = decodePathSegment;
|
|
1741
|
+
var readRequestBody2 = readRequestBody;
|
|
1742
|
+
var writeInvalidBody2 = writeInvalidBody;
|
|
1743
|
+
var sanitizeApiError2 = sanitizeApiError;
|
|
1744
|
+
var buildHttpUrl2 = buildHttpUrl;
|
|
1745
|
+
var buildBootstrapHttpUrl2 = buildBootstrapHttpUrl;
|
|
1746
|
+
var buildClientWsUrl2 = buildClientWsUrl;
|
|
1747
|
+
var agentRingKey2 = agentRingKey;
|
|
1748
|
+
var agentMessageToEntry2 = agentMessageToEntry;
|
|
1749
|
+
var readLocalSubagentTranscript2 = readLocalSubagentTranscript;
|
|
1750
|
+
function createHqRouter(deps) {
|
|
1751
|
+
const {
|
|
1752
|
+
host,
|
|
1753
|
+
listeningPort,
|
|
1754
|
+
trustedPublicOrigins,
|
|
1755
|
+
mutableAuth,
|
|
1756
|
+
sessions,
|
|
1757
|
+
loginAttempts,
|
|
1758
|
+
clients,
|
|
1759
|
+
browsers,
|
|
1760
|
+
eventLog,
|
|
1761
|
+
transcripts,
|
|
1762
|
+
agentMessages,
|
|
1763
|
+
alertEngine,
|
|
1764
|
+
auditLog,
|
|
1765
|
+
persistence,
|
|
1766
|
+
dataDir,
|
|
1767
|
+
hqSessionTag,
|
|
1768
|
+
requireBrowserAuth,
|
|
1769
|
+
secureCookies,
|
|
1770
|
+
authorizeMailboxGateway,
|
|
1771
|
+
getMailboxGateway,
|
|
1772
|
+
getTokenStats,
|
|
1773
|
+
trustBoundary,
|
|
1774
|
+
bootstrapStore
|
|
1775
|
+
} = deps;
|
|
1776
|
+
return async (req, res) => {
|
|
1777
|
+
try {
|
|
1778
|
+
const url = new URL(req.url ?? "/", `http://${host}:${listeningPort}`);
|
|
1779
|
+
setHqSecurityHeaders2(res);
|
|
1780
|
+
if (!hasTrustedBrowserOrigin2(req, host, listeningPort, trustedPublicOrigins)) {
|
|
1781
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1782
|
+
res.end(JSON.stringify({ error: "forbidden: untrusted request origin" }));
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
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)) {
|
|
1786
|
+
const auth = authenticateBrowserRequest2(req, url, mutableAuth, sessions);
|
|
1787
|
+
if (!auth) {
|
|
1788
|
+
const tokenOnly = mutableAuth.browserTokens.size > 0 && !mutableAuth.passwordHash;
|
|
1789
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1790
|
+
res.end(
|
|
1791
|
+
JSON.stringify({
|
|
1792
|
+
error: {
|
|
1793
|
+
code: tokenOnly ? "INVALID_TOKEN" : "UNAUTHORIZED",
|
|
1794
|
+
message: tokenOnly ? "A valid ?token= or Authorization: Bearer is required for HTTP access in browser token mode." : "A valid browser token or password session is required."
|
|
1795
|
+
}
|
|
1796
|
+
})
|
|
1797
|
+
);
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
const hqDistDir = resolveHqDistDir();
|
|
1802
|
+
const isApiOrWsPath = url.pathname.startsWith("/api/") || url.pathname.startsWith("/ws/");
|
|
1803
|
+
if (hqDistDir !== null && !isApiOrWsPath) {
|
|
1804
|
+
const served = await serveHqStatic(req, res, url.pathname, hqDistDir);
|
|
1805
|
+
if (served.handled) return;
|
|
1806
|
+
}
|
|
1807
|
+
if (url.pathname === "/" || url.pathname === "/index.html") {
|
|
1808
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1809
|
+
res.end(HQ_HTML);
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
if (url.pathname === "/api/auth/status" && req.method === "GET") {
|
|
1813
|
+
await handleApiAuthStatus(req, res, url, mutableAuth, sessions, requireBrowserAuth, trustedPublicOrigins, secureCookies);
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
if (url.pathname === "/api/login" && req.method === "POST") {
|
|
1817
|
+
await handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, secureCookies);
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
if (url.pathname === "/api/auth/bootstrap" && req.method === "POST") {
|
|
1821
|
+
if (bootstrapStore) {
|
|
1822
|
+
await handleApiBootstrap(req, res, mutableAuth, sessions, secureCookies, bootstrapStore);
|
|
1823
|
+
} else {
|
|
1824
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1825
|
+
res.end(
|
|
1826
|
+
JSON.stringify({
|
|
1827
|
+
error: {
|
|
1828
|
+
code: "BOOTSTRAP_UNAVAILABLE",
|
|
1829
|
+
message: "Bootstrap exchange is not enabled on this HQ server."
|
|
1830
|
+
}
|
|
1831
|
+
})
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
if (url.pathname === "/api/logout" && req.method === "POST") {
|
|
1837
|
+
await handleApiLogout(req, res, mutableAuth, sessions, secureCookies);
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
if (url.pathname === "/api/auth/password" && (req.method === "POST" || req.method === "DELETE")) {
|
|
1841
|
+
await handleApiPassword(req, res, mutableAuth, sessions, dataDir, secureCookies, requireBrowserAuth);
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
if (url.pathname === "/api/snapshot" && req.method === "GET") {
|
|
1845
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1846
|
+
res.end(JSON.stringify(buildSnapshot(clients, { tokenStats: getTokenStats?.() })));
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
if (url.pathname === "/api/system/update" && req.method === "GET") {
|
|
1850
|
+
await handleApiSystemUpdate(res);
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
if (url.pathname === "/api/system/health" && req.method === "GET") {
|
|
1854
|
+
await handleApiSystemHealth(res, clients, persistence, eventLog);
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1857
|
+
const mailboxGatewayMatch = url.pathname.match(
|
|
1858
|
+
/^\/api\/projects\/([^/]+)\/mailbox(?:\/(.*))?$/
|
|
1859
|
+
);
|
|
1860
|
+
if (mailboxGatewayMatch) {
|
|
1861
|
+
await handleMailboxGateway(req, res, mailboxGatewayMatch, authorizeMailboxGateway, getMailboxGateway, dataDir);
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
const projectKanbanMatch = url.pathname.match(/^\/api\/projects\/([^/]+)\/kanban$/);
|
|
1865
|
+
if (projectKanbanMatch && req.method === "GET") {
|
|
1866
|
+
const projectId = decodePathSegment2(projectKanbanMatch[1]);
|
|
1867
|
+
if (projectId === null) {
|
|
1868
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1869
|
+
res.end(JSON.stringify({ error: { code: "VALIDATION_ERROR", message: "Invalid project id." } }));
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
const snapshot = await persistence.kanban.load(projectId);
|
|
1873
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1874
|
+
res.end(JSON.stringify(snapshot));
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
if (url.pathname.startsWith("/api/projects/") && req.method === "GET") {
|
|
1878
|
+
await handleApiProjectDetail(req, res, url, clients);
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
if (url.pathname === "/api/fleet" && req.method === "GET") {
|
|
1882
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1883
|
+
res.end(JSON.stringify(buildSnapshot(clients, { tokenStats: getTokenStats?.() })));
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
if (url.pathname === "/api/events" && req.method === "GET") {
|
|
1887
|
+
await handleApiEvents(req, res, persistence);
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
if (url.pathname === "/api/trends/cost" && req.method === "GET") {
|
|
1891
|
+
await handleApiTrendsCost(req, res, persistence);
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
if (url.pathname === "/api/command" && req.method === "POST") {
|
|
1895
|
+
await handleApiCommand(
|
|
1896
|
+
req,
|
|
1897
|
+
res,
|
|
1898
|
+
url,
|
|
1899
|
+
mutableAuth,
|
|
1900
|
+
sessions,
|
|
1901
|
+
clients,
|
|
1902
|
+
browsers,
|
|
1903
|
+
auditLog,
|
|
1904
|
+
trustBoundary
|
|
1905
|
+
);
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
if (url.pathname === "/api/mailbox-send" && req.method === "POST") {
|
|
1909
|
+
await handleApiMailboxSend(req, res, mutableAuth, sessions, dataDir, getMailboxGateway, hqSessionTag);
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
const mailboxActionMatch = url.pathname.match(
|
|
1913
|
+
/^\/api\/mailbox\/messages\/([^/]+)\/action$/
|
|
1914
|
+
);
|
|
1915
|
+
if (mailboxActionMatch && req.method === "POST") {
|
|
1916
|
+
await handleMailboxAction(req, res, mailboxActionMatch, mutableAuth, sessions, dataDir, getMailboxGateway);
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
if (url.pathname === "/api/commands" && req.method === "GET") {
|
|
1920
|
+
await handleApiCommandsAudit(req, res, auditLog);
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
if (url.pathname === "/api/alerts" && req.method === "GET") {
|
|
1924
|
+
await handleApiAlerts(req, res, alertEngine);
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
if (url.pathname === "/api/sessions" && req.method === "GET") {
|
|
1928
|
+
await handleApiSessions(res);
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1931
|
+
const eventsMatch = url.pathname.match(/^\/api\/sessions\/([^/]+)\/events$/);
|
|
1932
|
+
if (eventsMatch && req.method === "GET") {
|
|
1933
|
+
await handleApiSessionEvents(req, res, eventsMatch, transcripts);
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
const sessionAgentMsgMatch = url.pathname.match(
|
|
1937
|
+
/^\/api\/sessions\/([^/]+)\/agents\/([^/]+)\/messages$/
|
|
1938
|
+
);
|
|
1939
|
+
if (sessionAgentMsgMatch && req.method === "GET") {
|
|
1940
|
+
await handleApiSessionAgentMessages(req, res, sessionAgentMsgMatch, agentMessages);
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
const agentMsgMatch = url.pathname.match(/^\/api\/agents\/([^/]+)\/messages$/);
|
|
1944
|
+
if (agentMsgMatch && req.method === "GET") {
|
|
1945
|
+
await handleApiAgentMessages(req, res, agentMsgMatch, agentMessages);
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
1949
|
+
res.end("Not found");
|
|
1950
|
+
} catch (err) {
|
|
1951
|
+
console.error(
|
|
1952
|
+
JSON.stringify({
|
|
1953
|
+
level: "error",
|
|
1954
|
+
event: "hq.http_handler_error",
|
|
1955
|
+
message: String(err),
|
|
1956
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1957
|
+
})
|
|
1958
|
+
);
|
|
1959
|
+
if (!res.headersSent) {
|
|
1960
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1961
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
};
|
|
1965
|
+
}
|
|
1966
|
+
async function handleMailboxGateway(_req, res, match, authorizeMailboxGateway, getMailboxGateway, dataDir) {
|
|
1967
|
+
const projectId = decodePathSegment2(match[1]);
|
|
1968
|
+
if (projectId === null || projectId.length === 0) {
|
|
1969
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1970
|
+
res.end(
|
|
1971
|
+
JSON.stringify({ error: { code: "BAD_REQUEST", message: "invalid projectId encoding" } })
|
|
1972
|
+
);
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
const gatewayGlobalRoot = path4.dirname(dataDir);
|
|
1976
|
+
const preliminaryAccess = authorizeMailboxGateway(_req, `project:${projectId}`);
|
|
1977
|
+
if (!preliminaryAccess.allowed) {
|
|
1978
|
+
res.writeHead(preliminaryAccess.status ?? 401, {
|
|
1979
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
1980
|
+
"Cache-Control": "no-store"
|
|
1981
|
+
});
|
|
1982
|
+
res.end(JSON.stringify(preliminaryAccess.body));
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
const projectRoot = await resolveHqProjectRoot(gatewayGlobalRoot, { projectId });
|
|
1986
|
+
if (!projectRoot) {
|
|
1987
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1988
|
+
res.end(
|
|
1989
|
+
JSON.stringify({ error: { code: "NOT_FOUND", message: `Unknown project: ${projectId}` } })
|
|
1990
|
+
);
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
const suffix = match[2];
|
|
1994
|
+
const rawUrl = _req.url ?? "";
|
|
1995
|
+
const queryIndex = rawUrl.indexOf("?");
|
|
1996
|
+
const querySuffix = queryIndex === -1 ? "" : rawUrl.slice(queryIndex);
|
|
1997
|
+
const canonicalPath = suffix ? `/mailbox/${suffix}${querySuffix}` : `/mailbox${querySuffix}`;
|
|
1998
|
+
const projectDir = resolveProjectDir(projectRoot, gatewayGlobalRoot);
|
|
1999
|
+
await getMailboxGateway(projectDir).router.handle(_req, res, canonicalPath);
|
|
2000
|
+
}
|
|
2001
|
+
async function handleApiProjectDetail(_req, res, url, clients) {
|
|
2002
|
+
const projectId = decodePathSegment2(url.pathname.slice("/api/projects/".length));
|
|
2003
|
+
if (projectId === null) {
|
|
2004
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2005
|
+
res.end(
|
|
2006
|
+
JSON.stringify({ error: { code: "BAD_REQUEST", message: "invalid projectId encoding" } })
|
|
2007
|
+
);
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
if (!projectId) {
|
|
2011
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2012
|
+
res.end(
|
|
2013
|
+
JSON.stringify({ error: { code: "BAD_REQUEST", message: "projectId is required" } })
|
|
2014
|
+
);
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
const detail = buildProjectDetail(clients, projectId);
|
|
2018
|
+
if (!detail) {
|
|
2019
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2020
|
+
res.end(
|
|
2021
|
+
JSON.stringify({ error: { code: "NOT_FOUND", message: `Unknown project: ${projectId}` } })
|
|
2022
|
+
);
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
2025
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2026
|
+
res.end(JSON.stringify(detail));
|
|
2027
|
+
}
|
|
2028
|
+
async function handleApiEvents(req, res, persistence) {
|
|
2029
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
2030
|
+
const rawLimit = Number.parseInt(url.searchParams.get("limit") ?? "200", 10);
|
|
2031
|
+
const limit = Math.min(5e3, Math.max(1, Number.isFinite(rawLimit) ? rawLimit : 200));
|
|
2032
|
+
const typeFilter = url.searchParams.get("type") ?? void 0;
|
|
2033
|
+
const events = await persistence.eventLog.recent(limit, typeFilter);
|
|
2034
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2035
|
+
res.end(JSON.stringify({ events, total: events.length }));
|
|
2036
|
+
}
|
|
2037
|
+
async function handleApiTrendsCost(req, res, persistence) {
|
|
2038
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
2039
|
+
const rawSince = Number.parseInt(url.searchParams.get("since") ?? "0", 10);
|
|
2040
|
+
const since = Number.isFinite(rawSince) ? rawSince : 0;
|
|
2041
|
+
const samples = await persistence.timeseries.read(since);
|
|
2042
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2043
|
+
res.end(JSON.stringify({ samples }));
|
|
2044
|
+
}
|
|
2045
|
+
async function handleApiCommand(req, res, url, mutableAuth, sessions, clients, browsers, auditLog, trustBoundary) {
|
|
2046
|
+
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) {
|
|
2050
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
2051
|
+
res.end(JSON.stringify({ error: "unauthorized" }));
|
|
2052
|
+
return;
|
|
2053
|
+
}
|
|
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;
|
|
2055
|
+
if (!canEnqueue) {
|
|
2056
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
2057
|
+
res.end(JSON.stringify({ error: "forbidden: token lacks control.enqueue capability" }));
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
2060
|
+
let body;
|
|
2061
|
+
try {
|
|
2062
|
+
body = JSON.parse(await readRequestBody2(req));
|
|
2063
|
+
} catch (error) {
|
|
2064
|
+
writeInvalidBody2(res, error);
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2067
|
+
if (typeof body.clientId !== "string" || typeof body.type !== "string") {
|
|
2068
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2069
|
+
res.end(JSON.stringify({ error: "missing clientId or type" }));
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
let target;
|
|
2073
|
+
for (const c of clients.values()) {
|
|
2074
|
+
if (c.clientId === body.clientId) {
|
|
2075
|
+
target = c;
|
|
2076
|
+
break;
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
if (target === void 0) {
|
|
2080
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2081
|
+
res.end(JSON.stringify({ error: "client not connected", clientId: body.clientId }));
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2084
|
+
if (!target.capabilities.includes("control.receive")) {
|
|
2085
|
+
res.writeHead(409, { "Content-Type": "application/json" });
|
|
2086
|
+
res.end(
|
|
2087
|
+
JSON.stringify({
|
|
2088
|
+
error: "client does not accept control commands",
|
|
2089
|
+
clientId: body.clientId
|
|
2090
|
+
})
|
|
2091
|
+
);
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
const commandId = randomUUID2();
|
|
2095
|
+
const queued = {
|
|
2096
|
+
commandId,
|
|
2097
|
+
type: body.type,
|
|
2098
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2099
|
+
payload: body.payload ?? {},
|
|
2100
|
+
requiresAck: true
|
|
2101
|
+
};
|
|
2102
|
+
const validated = validateHqCommand(queued);
|
|
2103
|
+
if (validated === null) {
|
|
2104
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2105
|
+
res.end(
|
|
2106
|
+
JSON.stringify({ error: "unrecognized or malformed command", type: body.type })
|
|
2107
|
+
);
|
|
2108
|
+
return;
|
|
2109
|
+
}
|
|
2110
|
+
if (validated.type === "run-command" && !tokenHasCapability(target.authToken, "control.execute")) {
|
|
2111
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
2112
|
+
res.end(
|
|
2113
|
+
JSON.stringify({
|
|
2114
|
+
error: "forbidden: target client token lacks control.execute capability"
|
|
2115
|
+
})
|
|
2116
|
+
);
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
const enqueuedBy = isCookieAuth2(auth) ? auth.tokenId ?? "password-session" : isTokenAuth2(auth) ? auth.id : "open-mode";
|
|
2120
|
+
const authorization = await authorizeHqCommand({
|
|
2121
|
+
boundary: trustBoundary,
|
|
2122
|
+
command: validated,
|
|
2123
|
+
commandId,
|
|
2124
|
+
enqueuedBy,
|
|
2125
|
+
authMethod: isCookieAuth2(auth) ? "session" : "bearer-token",
|
|
2126
|
+
target
|
|
2127
|
+
});
|
|
2128
|
+
if (!authorization.allowed) {
|
|
2129
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
2130
|
+
res.end(JSON.stringify({ error: `command denied: ${authorization.reason}` }));
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
target.commandQueue.push(queued);
|
|
2134
|
+
if (target.commandQueue.length > 200)
|
|
2135
|
+
target.commandQueue.splice(0, target.commandQueue.length - 200);
|
|
2136
|
+
const auditEntry = {
|
|
2137
|
+
commandId,
|
|
2138
|
+
type: validated.type,
|
|
2139
|
+
clientId: target.clientId,
|
|
2140
|
+
enqueuedBy,
|
|
2141
|
+
enqueuedAt: queued.createdAt,
|
|
2142
|
+
status: "queued"
|
|
2143
|
+
};
|
|
2144
|
+
auditLog.record(auditEntry);
|
|
2145
|
+
broadcastCommandStatus(auditEntry, browsers);
|
|
2146
|
+
res.writeHead(202, { "Content-Type": "application/json" });
|
|
2147
|
+
res.end(JSON.stringify({ commandId, queued: true, clientId: target.clientId }));
|
|
2148
|
+
}
|
|
2149
|
+
async function handleApiMailboxSend(_req, res, mutableAuth, sessions, dataDir, getMailboxGateway, hqSessionTag) {
|
|
2150
|
+
const auth = authenticateBrowserRequest2(
|
|
2151
|
+
_req,
|
|
2152
|
+
new URL(_req.url ?? "/", "http://localhost"),
|
|
2153
|
+
mutableAuth,
|
|
2154
|
+
sessions
|
|
2155
|
+
);
|
|
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;
|
|
2157
|
+
if (!canEnqueue) {
|
|
2158
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
2159
|
+
res.end(JSON.stringify({ error: "forbidden: token lacks control.enqueue capability" }));
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
let mbody;
|
|
2163
|
+
try {
|
|
2164
|
+
mbody = JSON.parse(await readRequestBody2(_req));
|
|
2165
|
+
} catch (error) {
|
|
2166
|
+
writeInvalidBody2(res, error);
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
if (typeof mbody.type !== "string" || typeof mbody.body !== "string") {
|
|
2170
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2171
|
+
res.end(JSON.stringify({ error: "missing type or body" }));
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
if (typeof mbody.sessionId !== "string" && typeof mbody.projectId !== "string") {
|
|
2175
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2176
|
+
res.end(JSON.stringify({ error: "missing sessionId or projectId" }));
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
if (mbody.audience !== void 0 && mbody.audience !== "all" && mbody.audience !== "leaders") {
|
|
2180
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2181
|
+
res.end(JSON.stringify({ error: "audience must be all or leaders" }));
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
const mbGlobalRoot = path4.dirname(dataDir);
|
|
2185
|
+
const projectRoot = await resolveHqProjectRoot(mbGlobalRoot, {
|
|
2186
|
+
sessionId: mbody.sessionId,
|
|
2187
|
+
projectId: mbody.projectId
|
|
2188
|
+
});
|
|
2189
|
+
if (typeof projectRoot !== "string" || projectRoot.length === 0) {
|
|
2190
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2191
|
+
res.end(JSON.stringify({ error: "could not resolve target project mailbox" }));
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
const to = typeof mbody.to === "string" ? mbody.to : "leader";
|
|
2195
|
+
const subject = typeof mbody.subject === "string" ? mbody.subject : "HQ prompt";
|
|
2196
|
+
const priority = mbody.priority === "high" ? "high" : mbody.priority === "low" ? "low" : "normal";
|
|
2197
|
+
const audience = mbody.audience === "leaders" ? "leaders" : "all";
|
|
2198
|
+
const mailboxType = mbody.type === "queue" ? "note" : ["note", "ask", "assign", "steer", "btw", "broadcast", "status", "result", "review"].includes(
|
|
2199
|
+
mbody.type
|
|
2200
|
+
) ? mbody.type : void 0;
|
|
2201
|
+
if (mailboxType === void 0) {
|
|
2202
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2203
|
+
res.end(
|
|
2204
|
+
JSON.stringify({
|
|
2205
|
+
error: "unrecognized or malformed mailbox message",
|
|
2206
|
+
type: mbody.type
|
|
2207
|
+
})
|
|
2208
|
+
);
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
if ((mailboxType === "assign" || mailboxType === "steer") && (to === "*" || to.trim().toLowerCase() === "all")) {
|
|
2212
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2213
|
+
res.end(JSON.stringify({ error: `${mailboxType} requires a specific recipient` }));
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
try {
|
|
2217
|
+
const projectDir = resolveProjectDir(projectRoot, mbGlobalRoot);
|
|
2218
|
+
const mailbox = getMailboxGateway(projectDir).mailbox;
|
|
2219
|
+
const from = `hq@${hqSessionTag}`;
|
|
2220
|
+
const deliveryTo = mailboxType === "broadcast" ? "all" : to;
|
|
2221
|
+
const sent = await mailbox.send({
|
|
2222
|
+
from,
|
|
2223
|
+
to: deliveryTo,
|
|
2224
|
+
type: mailboxType,
|
|
2225
|
+
subject,
|
|
2226
|
+
body: mbody.body,
|
|
2227
|
+
priority,
|
|
2228
|
+
audience
|
|
2229
|
+
});
|
|
2230
|
+
res.writeHead(202, { "Content-Type": "application/json" });
|
|
2231
|
+
res.end(
|
|
2232
|
+
JSON.stringify({
|
|
2233
|
+
delivered: true,
|
|
2234
|
+
messageId: sent?.id,
|
|
2235
|
+
to: sent?.to ?? deliveryTo,
|
|
2236
|
+
type: mailboxType,
|
|
2237
|
+
audience
|
|
2238
|
+
})
|
|
2239
|
+
);
|
|
2240
|
+
} catch (err) {
|
|
2241
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2242
|
+
res.end(JSON.stringify({ error: "mailbox write failed", detail: String(err) }));
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
async function handleMailboxAction(_req, res, match, _mutableAuth, _sessions, dataDir, getMailboxGateway) {
|
|
2246
|
+
const MAILBOX_ACTIONS = [
|
|
2247
|
+
"mark-read",
|
|
2248
|
+
"acknowledge",
|
|
2249
|
+
"reopen",
|
|
2250
|
+
"soft-delete",
|
|
2251
|
+
"restore"
|
|
2252
|
+
];
|
|
2253
|
+
let abody;
|
|
2254
|
+
try {
|
|
2255
|
+
abody = JSON.parse(await readRequestBody2(_req));
|
|
2256
|
+
} catch (error) {
|
|
2257
|
+
writeInvalidBody2(res, error);
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
const mailId = decodePathSegment2(match[1]);
|
|
2261
|
+
if (mailId === null) {
|
|
2262
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2263
|
+
res.end(JSON.stringify({ error: "invalid mailId encoding" }));
|
|
2264
|
+
return;
|
|
2265
|
+
}
|
|
2266
|
+
const action = MAILBOX_ACTIONS.find((a) => a === abody.action);
|
|
2267
|
+
if (action === void 0) {
|
|
2268
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2269
|
+
res.end(JSON.stringify({ error: "unrecognized action", action: abody.action }));
|
|
2270
|
+
return;
|
|
2271
|
+
}
|
|
2272
|
+
if (typeof abody.readerId !== "string" || abody.readerId.length === 0) {
|
|
2273
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2274
|
+
res.end(JSON.stringify({ error: "missing readerId" }));
|
|
2275
|
+
return;
|
|
2276
|
+
}
|
|
2277
|
+
if (typeof abody.sessionId !== "string" && typeof abody.projectId !== "string") {
|
|
2278
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2279
|
+
res.end(JSON.stringify({ error: "missing sessionId or projectId" }));
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
const actGlobalRoot = path4.dirname(dataDir);
|
|
2283
|
+
const projectRoot = await resolveHqProjectRoot(actGlobalRoot, {
|
|
2284
|
+
sessionId: abody.sessionId,
|
|
2285
|
+
projectId: abody.projectId
|
|
2286
|
+
});
|
|
2287
|
+
if (typeof projectRoot !== "string" || projectRoot.length === 0) {
|
|
2288
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2289
|
+
res.end(JSON.stringify({ error: "could not resolve target project mailbox" }));
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2292
|
+
try {
|
|
2293
|
+
const { actionToAckInput } = await import("@wrongstack/core/coordination");
|
|
2294
|
+
const projectDir = resolveProjectDir(projectRoot, actGlobalRoot);
|
|
2295
|
+
const mailbox = getMailboxGateway(projectDir).mailbox;
|
|
2296
|
+
const readerId = abody.readerId;
|
|
2297
|
+
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
|
+
if (message === null) {
|
|
2299
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2300
|
+
res.end(JSON.stringify({ error: "message not found", mailId }));
|
|
2301
|
+
return;
|
|
2302
|
+
}
|
|
2303
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2304
|
+
res.end(JSON.stringify({ action, mailId, message: null, changed: true }));
|
|
2305
|
+
} catch (err) {
|
|
2306
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2307
|
+
res.end(
|
|
2308
|
+
JSON.stringify({
|
|
2309
|
+
error: "mailbox action failed",
|
|
2310
|
+
detail: sanitizeApiError2(err)
|
|
2311
|
+
})
|
|
2312
|
+
);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
// src/hq-server/startup.ts
|
|
2317
|
+
import * as fs3 from "node:fs/promises";
|
|
2318
|
+
import * as path5 from "node:path";
|
|
2319
|
+
var hqRuntimeMarkerPath2 = hqRuntimeMarkerPath;
|
|
2320
|
+
async function writeHqRuntimeMarker(dataDir, url) {
|
|
2321
|
+
const file = hqRuntimeMarkerPath2(dataDir);
|
|
2322
|
+
const payload = JSON.stringify(
|
|
2323
|
+
{ url, pid: process.pid, updatedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
2324
|
+
null,
|
|
2325
|
+
2
|
|
2326
|
+
);
|
|
2327
|
+
await fs3.mkdir(path5.dirname(file), { recursive: true });
|
|
2328
|
+
await fs3.writeFile(file, `${payload}
|
|
2329
|
+
`, { encoding: "utf8", mode: 384 });
|
|
2330
|
+
}
|
|
2331
|
+
async function clearHqRuntimeMarker(dataDir, url) {
|
|
2332
|
+
const file = hqRuntimeMarkerPath2(dataDir);
|
|
2333
|
+
try {
|
|
2334
|
+
const raw = await fs3.readFile(file, "utf8");
|
|
2335
|
+
const parsed = JSON.parse(raw);
|
|
2336
|
+
if (parsed.url === url && parsed.pid === process.pid) {
|
|
2337
|
+
await fs3.rm(file, { force: true });
|
|
2338
|
+
}
|
|
2339
|
+
} catch {
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
function writeHqStartupInfo(write, handle) {
|
|
2343
|
+
const startup = handle.firstRunSetup;
|
|
2344
|
+
write(`WrongStack HQ listening on http://${handle.host}:${handle.port}
|
|
2345
|
+
`);
|
|
2346
|
+
if (!startup) {
|
|
2347
|
+
write(`Browser endpoint: ${buildHttpUrl(handle.host, handle.port)}
|
|
2348
|
+
`);
|
|
2349
|
+
write(`Client endpoint: ${buildClientWsUrl(handle.host, handle.port)}
|
|
2350
|
+
`);
|
|
2351
|
+
writeHqLanEndpoints(write, handle, void 0);
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
write(`Browser endpoint: ${startup.browserUrl}
|
|
2355
|
+
`);
|
|
2356
|
+
write(`Client endpoint: ${startup.clientUrl}
|
|
2357
|
+
`);
|
|
2358
|
+
if (startup.createdAuth) {
|
|
2359
|
+
write(`
|
|
2360
|
+
First-run HQ auth created in ${startup.dataDir}
|
|
2361
|
+
`);
|
|
2362
|
+
} else {
|
|
2363
|
+
write(`
|
|
2364
|
+
HQ auth loaded from ${startup.dataDir}
|
|
2365
|
+
`);
|
|
2366
|
+
}
|
|
2367
|
+
write("Start clients with:\n");
|
|
2368
|
+
write(` WRONGSTACK_HQ_URL=${startup.clientEnv.WRONGSTACK_HQ_URL}
|
|
2369
|
+
`);
|
|
2370
|
+
if (startup.clientEnv.WRONGSTACK_HQ_TOKEN) {
|
|
2371
|
+
write(` WRONGSTACK_HQ_TOKEN=${startup.clientEnv.WRONGSTACK_HQ_TOKEN}
|
|
2372
|
+
`);
|
|
2373
|
+
write(`Credentials are stored in ${path5.join(startup.dataDir, "auth.json")}
|
|
2374
|
+
`);
|
|
2375
|
+
}
|
|
2376
|
+
writeHqLanEndpoints(write, handle, void 0);
|
|
2377
|
+
}
|
|
2378
|
+
function writeHqLanEndpoints(write, handle, browserToken) {
|
|
2379
|
+
if (handle.host !== "0.0.0.0" && handle.host !== "::") return;
|
|
2380
|
+
const ips = lanIPv4Addresses();
|
|
2381
|
+
if (ips.length === 0) return;
|
|
2382
|
+
write("\nReachable from other machines on your network:\n");
|
|
2383
|
+
for (const ip of ips) {
|
|
2384
|
+
write(` ${buildHttpUrl(ip, handle.port, browserToken)}
|
|
2385
|
+
`);
|
|
2386
|
+
}
|
|
2387
|
+
write(` On another machine, set WRONGSTACK_HQ_URL=http://${ips[0]}:${handle.port}
|
|
2388
|
+
`);
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
// src/hq-server/ws.ts
|
|
2392
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
2393
|
+
import {
|
|
2394
|
+
HQ_PROTOCOL_VERSION,
|
|
2395
|
+
HQ_TRANSCRIPT_TEXT_CAP,
|
|
2396
|
+
parseHqEventPayload,
|
|
2397
|
+
parseHqFrame,
|
|
2398
|
+
redactHqEvent,
|
|
2399
|
+
resolveHqRedactionPolicy,
|
|
2400
|
+
tightenHqRedactionPolicy,
|
|
2401
|
+
tokenHasCapability as tokenHasCapability2
|
|
2402
|
+
} from "@wrongstack/core/hq";
|
|
2403
|
+
import { WebSocket as WebSocket2 } from "ws";
|
|
2404
|
+
var MAX_EVENT_LOG = 5e3;
|
|
2405
|
+
function handleBrowser(ws, snapshotBroadcaster, browsers) {
|
|
2406
|
+
browsers.add(ws);
|
|
2407
|
+
ws.on("error", (err) => {
|
|
2408
|
+
console.warn(
|
|
2409
|
+
JSON.stringify({
|
|
2410
|
+
level: "warn",
|
|
2411
|
+
event: "hq.browser_socket_error",
|
|
2412
|
+
message: err instanceof Error ? err.message : String(err),
|
|
2413
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2414
|
+
})
|
|
2415
|
+
);
|
|
2416
|
+
});
|
|
2417
|
+
sendGuarded(ws, snapshotBroadcaster.currentSerialized());
|
|
2418
|
+
ws.on("close", () => {
|
|
2419
|
+
browsers.delete(ws);
|
|
2420
|
+
});
|
|
2421
|
+
}
|
|
2422
|
+
function handleClient(ws, clients, browsers, eventLog, auth, snapshotBroadcaster, transcripts, agentMessages, persistence, auditLog) {
|
|
2423
|
+
let registered = false;
|
|
2424
|
+
function persistEvent(event) {
|
|
2425
|
+
eventLog.push(event);
|
|
2426
|
+
if (eventLog.length > MAX_EVENT_LOG) eventLog.splice(0, eventLog.length - MAX_EVENT_LOG);
|
|
2427
|
+
if (persistence !== void 0) {
|
|
2428
|
+
persistence.eventLog.append(event);
|
|
2429
|
+
recordTimeseriesSignal(persistence, event);
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
ws.on("error", (err) => {
|
|
2433
|
+
console.warn(
|
|
2434
|
+
JSON.stringify({
|
|
2435
|
+
level: "warn",
|
|
2436
|
+
event: "hq.client_socket_error",
|
|
2437
|
+
message: err instanceof Error ? err.message : String(err),
|
|
2438
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2439
|
+
})
|
|
2440
|
+
);
|
|
2441
|
+
});
|
|
2442
|
+
ws.on("message", (data) => {
|
|
2443
|
+
const raw = typeof data === "string" ? data : Buffer.isBuffer(data) ? data : new TextDecoder().decode(data);
|
|
2444
|
+
const parsed = parseHqFrame(raw);
|
|
2445
|
+
if (!parsed.ok) {
|
|
2446
|
+
const code = parsed.reason === "invalid-json" ? 1003 : 1008;
|
|
2447
|
+
ws.close(code, `invalid frame: ${parsed.reason}`);
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
const frame = parsed.frame;
|
|
2451
|
+
if (frame.type === "client.hello") {
|
|
2452
|
+
if (registered) {
|
|
2453
|
+
ws.close(1008, "duplicate client.hello");
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2456
|
+
const payload = frame.payload;
|
|
2457
|
+
if (payload.protocolVersion !== HQ_PROTOCOL_VERSION) {
|
|
2458
|
+
ws.close(1008, "protocol version mismatch");
|
|
2459
|
+
return;
|
|
2460
|
+
}
|
|
2461
|
+
const canPublishTelemetry = auth.token === void 0 || tokenHasCapability2(auth.token, "telemetry.publish");
|
|
2462
|
+
const acceptedCapabilities = payload.capabilities.filter(
|
|
2463
|
+
(capability) => capability === "control.receive" || canPublishTelemetry
|
|
2464
|
+
);
|
|
2465
|
+
const declaredRedactionPolicy = resolveHqRedactionPolicy(payload.redactionPolicy);
|
|
2466
|
+
for (const [otherWs, otherClient] of clients) {
|
|
2467
|
+
const samePublisher = otherClient.projectId === payload.project.projectId && otherClient.kind === payload.client.kind && otherClient.pid !== void 0 && payload.client.pid !== void 0 && otherClient.pid === payload.client.pid && (otherClient.machineId || otherClient.project.machineId) === (payload.client.machineId || payload.project.machineId);
|
|
2468
|
+
if (otherWs !== ws && (otherClient.clientId === payload.client.clientId || samePublisher && otherClient.sessions.size === 0)) {
|
|
2469
|
+
clients.delete(otherWs);
|
|
2470
|
+
otherWs.close(4001, "superseded by a newer HQ connection");
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
const client = {
|
|
2474
|
+
ws,
|
|
2475
|
+
clientId: payload.client.clientId,
|
|
2476
|
+
projectId: payload.project.projectId,
|
|
2477
|
+
project: payload.project,
|
|
2478
|
+
kind: payload.client.kind,
|
|
2479
|
+
connectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2480
|
+
lastSeenAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2481
|
+
...payload.client.hostname ? { hostname: payload.client.hostname } : {},
|
|
2482
|
+
...payload.client.pid ? { pid: payload.client.pid } : {},
|
|
2483
|
+
...payload.client.version ? { version: payload.client.version } : {},
|
|
2484
|
+
capabilities: acceptedCapabilities,
|
|
2485
|
+
...auth.token !== void 0 ? { authToken: auth.token } : {},
|
|
2486
|
+
declaredRedactionPolicy,
|
|
2487
|
+
lastEventSeq: 0,
|
|
2488
|
+
mailboxes: /* @__PURE__ */ new Map(),
|
|
2489
|
+
machineId: payload.client.machineId || payload.project.machineId,
|
|
2490
|
+
sessions: /* @__PURE__ */ new Map(),
|
|
2491
|
+
fleets: /* @__PURE__ */ new Map(),
|
|
2492
|
+
mcpSnapshots: /* @__PURE__ */ new Map(),
|
|
2493
|
+
commandQueue: []
|
|
2494
|
+
};
|
|
2495
|
+
clients.set(ws, client);
|
|
2496
|
+
registered = true;
|
|
2497
|
+
const welcome = {
|
|
2498
|
+
type: "hq.welcome",
|
|
2499
|
+
protocolVersion: HQ_PROTOCOL_VERSION,
|
|
2500
|
+
serverTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2501
|
+
acceptedCapabilities,
|
|
2502
|
+
// The operator-configured override (from <dataDir>/auth.json) wins
|
|
2503
|
+
// over the default. The client learns the *effective* policy.
|
|
2504
|
+
redactionPolicy: tightenHqRedactionPolicy(
|
|
2505
|
+
declaredRedactionPolicy,
|
|
2506
|
+
auth.getOperatorPolicy()
|
|
2507
|
+
)
|
|
2508
|
+
};
|
|
2509
|
+
ws.send(JSON.stringify(welcome));
|
|
2510
|
+
if (persistence !== void 0) {
|
|
2511
|
+
void persistence.kanban.load(client.projectId).then((payload2) => {
|
|
2512
|
+
sendGuarded(ws, JSON.stringify({ type: "hq.kanban_snapshot", payload: payload2 }));
|
|
2513
|
+
}).catch(() => void 0);
|
|
2514
|
+
}
|
|
2515
|
+
const event = {
|
|
2516
|
+
id: randomUUID3(),
|
|
2517
|
+
type: "client.hello",
|
|
2518
|
+
schemaVersion: HQ_PROTOCOL_VERSION,
|
|
2519
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2520
|
+
clientId: payload.client.clientId,
|
|
2521
|
+
projectId: payload.project.projectId,
|
|
2522
|
+
seq: 0,
|
|
2523
|
+
payload: { client: payload.client, project: payload.project }
|
|
2524
|
+
};
|
|
2525
|
+
persistEvent(event);
|
|
2526
|
+
snapshotBroadcaster.broadcast();
|
|
2527
|
+
broadcastEvent(event, browsers);
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
if (!registered) return;
|
|
2531
|
+
if (frame.type === "client.command_poll") {
|
|
2532
|
+
const client = clients.get(ws);
|
|
2533
|
+
if (client) {
|
|
2534
|
+
if (frame.clientId !== client.clientId || frame.projectId !== client.projectId) {
|
|
2535
|
+
ws.close(1008, "command poll identity mismatch");
|
|
2536
|
+
return;
|
|
2537
|
+
}
|
|
2538
|
+
client.lastSeenAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2539
|
+
const afterId = frame.afterCommandId;
|
|
2540
|
+
const limit = frame.limit ?? 25;
|
|
2541
|
+
let toSend;
|
|
2542
|
+
if (afterId === void 0) {
|
|
2543
|
+
toSend = client.commandQueue.slice(0, limit);
|
|
2544
|
+
} else {
|
|
2545
|
+
const idx = client.commandQueue.findIndex((c) => c.commandId === afterId);
|
|
2546
|
+
toSend = (idx >= 0 ? client.commandQueue.slice(idx + 1) : client.commandQueue).slice(
|
|
2547
|
+
0,
|
|
2548
|
+
limit
|
|
2549
|
+
);
|
|
2550
|
+
}
|
|
2551
|
+
if (toSend.length > 0) {
|
|
2552
|
+
const batch = JSON.stringify({ type: "hq.command_batch", commands: toSend });
|
|
2553
|
+
if (ws.readyState === WebSocket2.OPEN) ws.send(batch);
|
|
2554
|
+
for (const cmd of toSend) {
|
|
2555
|
+
auditLog?.update(cmd.commandId, { status: "delivered" });
|
|
2556
|
+
const updated = auditLog?.get(cmd.commandId);
|
|
2557
|
+
if (updated !== void 0) broadcastCommandStatus(updated, browsers);
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
if (frame.type === "client.command_ack") {
|
|
2564
|
+
const client = clients.get(ws);
|
|
2565
|
+
if (client) {
|
|
2566
|
+
if (frame.clientId !== client.clientId || frame.projectId !== client.projectId) {
|
|
2567
|
+
ws.close(1008, "command ack identity mismatch");
|
|
2568
|
+
return;
|
|
2569
|
+
}
|
|
2570
|
+
client.lastSeenAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2571
|
+
}
|
|
2572
|
+
if (client) {
|
|
2573
|
+
const updated = auditLog?.updateForClient(frame.commandId, client.clientId, {
|
|
2574
|
+
status: "acked",
|
|
2575
|
+
ackStatus: frame.status,
|
|
2576
|
+
...frame.message !== void 0 ? { ackMessage: frame.message } : {},
|
|
2577
|
+
ackedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2578
|
+
});
|
|
2579
|
+
if (updated === true) {
|
|
2580
|
+
const entry = auditLog?.get(frame.commandId);
|
|
2581
|
+
if (entry !== void 0) broadcastCommandStatus(entry, browsers);
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
return;
|
|
2585
|
+
}
|
|
2586
|
+
if (frame.type === "client.event") {
|
|
2587
|
+
const client = clients.get(ws);
|
|
2588
|
+
if (!client) return;
|
|
2589
|
+
const incomingEvent = frame.event;
|
|
2590
|
+
if (incomingEvent.clientId !== client.clientId || incomingEvent.projectId !== client.projectId) {
|
|
2591
|
+
ws.close(1008, "event identity mismatch");
|
|
2592
|
+
return;
|
|
2593
|
+
}
|
|
2594
|
+
if (!tokenHasCapability2(client.authToken, "telemetry.publish") && auth.token !== void 0) {
|
|
2595
|
+
ws.close(1008, "client token lacks telemetry.publish capability");
|
|
2596
|
+
return;
|
|
2597
|
+
}
|
|
2598
|
+
if (incomingEvent.seq <= client.lastEventSeq) {
|
|
2599
|
+
ws.close(1008, "event sequence must increase monotonically");
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
const parsedPayload = parseHqEventPayload(incomingEvent.type, incomingEvent.payload);
|
|
2603
|
+
if (!parsedPayload.ok) {
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
client.lastEventSeq = incomingEvent.seq;
|
|
2607
|
+
client.lastSeenAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2608
|
+
const event = redactHqEvent(
|
|
2609
|
+
{ ...incomingEvent, payload: parsedPayload.payload },
|
|
2610
|
+
{
|
|
2611
|
+
policy: tightenHqRedactionPolicy(
|
|
2612
|
+
client.declaredRedactionPolicy,
|
|
2613
|
+
auth.getOperatorPolicy()
|
|
2614
|
+
),
|
|
2615
|
+
projectRoot: client.project.projectRoot,
|
|
2616
|
+
// Chat-transcript events carry full turns — the generic 500-char
|
|
2617
|
+
// summary cap would truncate them a second time after the
|
|
2618
|
+
// publisher already applied the transcript cap.
|
|
2619
|
+
...incomingEvent.type === "session.transcript" || incomingEvent.type === "agent.message" ? { maxSummaryLength: HQ_TRANSCRIPT_TEXT_CAP } : {}
|
|
2620
|
+
}
|
|
2621
|
+
).value;
|
|
2622
|
+
if (event.type === "client.heartbeat") {
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2625
|
+
if (event.type === "kanban.snapshot") {
|
|
2626
|
+
const payload = event.payload;
|
|
2627
|
+
if (payload.projectId !== client.projectId || persistence === void 0) return;
|
|
2628
|
+
void persistence.kanban.merge(payload).then((merged) => {
|
|
2629
|
+
const touched = /* @__PURE__ */ new Set();
|
|
2630
|
+
for (const record of payload.boards) touched.add(record.boardId);
|
|
2631
|
+
for (const record of payload.tombstones) touched.add(record.boardId);
|
|
2632
|
+
const delta = {
|
|
2633
|
+
projectId: merged.projectId,
|
|
2634
|
+
generatedAt: merged.generatedAt,
|
|
2635
|
+
boards: merged.boards.filter((record) => touched.has(record.boardId)),
|
|
2636
|
+
tombstones: merged.tombstones.filter((record) => touched.has(record.boardId))
|
|
2637
|
+
};
|
|
2638
|
+
const message = JSON.stringify({ type: "hq.kanban_snapshot", payload: delta });
|
|
2639
|
+
for (const peer of clients.values()) {
|
|
2640
|
+
if (peer.projectId === client.projectId) sendGuarded(peer.ws, message);
|
|
2641
|
+
}
|
|
2642
|
+
}).catch((error) => {
|
|
2643
|
+
console.warn(
|
|
2644
|
+
JSON.stringify({
|
|
2645
|
+
level: "warn",
|
|
2646
|
+
event: "hq.kanban_merge_failed",
|
|
2647
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2648
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2649
|
+
})
|
|
2650
|
+
);
|
|
2651
|
+
});
|
|
2652
|
+
persistEvent(event);
|
|
2653
|
+
broadcastEvent(event, browsers);
|
|
2654
|
+
return;
|
|
2655
|
+
}
|
|
2656
|
+
if (event.type === "mailbox.snapshot" && client !== void 0) {
|
|
2657
|
+
const payloadResult = parseHqEventPayload(event.type, event.payload);
|
|
2658
|
+
if (payloadResult.ok) {
|
|
2659
|
+
const payload = payloadResult.payload;
|
|
2660
|
+
client.mailboxes.set(client.projectId + ":" + payload.mailboxId, payload);
|
|
2661
|
+
persistEvent(event);
|
|
2662
|
+
snapshotBroadcaster.broadcast();
|
|
2663
|
+
broadcastEvent(event, browsers);
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2666
|
+
return;
|
|
2667
|
+
}
|
|
2668
|
+
if (event.type === "mailbox.event") {
|
|
2669
|
+
const payloadResult = parseHqEventPayload(event.type, event.payload);
|
|
2670
|
+
if (!payloadResult.ok) {
|
|
2671
|
+
return;
|
|
2672
|
+
}
|
|
2673
|
+
const payload = payloadResult.payload;
|
|
2674
|
+
const sanitizedSummary = truncateHqSummary(payload.summary, 280);
|
|
2675
|
+
const sanitizedEvent = sanitizedSummary === void 0 ? event : { ...event, payload: { ...payload, summary: sanitizedSummary } };
|
|
2676
|
+
persistEvent(sanitizedEvent);
|
|
2677
|
+
broadcastEvent(sanitizedEvent, browsers);
|
|
2678
|
+
return;
|
|
2679
|
+
}
|
|
2680
|
+
if (event.type === "session.snapshot" && client !== void 0) {
|
|
2681
|
+
const result = parseHqEventPayload(event.type, event.payload);
|
|
2682
|
+
if (result.ok) {
|
|
2683
|
+
const payload = result.payload;
|
|
2684
|
+
client.sessions.set(payload.sessionId, { payload, receivedAt: Date.now() });
|
|
2685
|
+
snapshotBroadcaster.broadcast();
|
|
2686
|
+
}
|
|
2687
|
+
return;
|
|
2688
|
+
}
|
|
2689
|
+
if (event.type === "session.ended" && client !== void 0) {
|
|
2690
|
+
const result = parseHqEventPayload(event.type, event.payload);
|
|
2691
|
+
if (result.ok) {
|
|
2692
|
+
const payload = result.payload;
|
|
2693
|
+
client.sessions.delete(payload.sessionId);
|
|
2694
|
+
client.mcpSnapshots.delete(payload.sessionId);
|
|
2695
|
+
for (const [runId, fleet] of client.fleets) {
|
|
2696
|
+
if (fleet.sessionId === payload.sessionId) client.fleets.delete(runId);
|
|
2697
|
+
}
|
|
2698
|
+
snapshotBroadcaster.broadcast();
|
|
2699
|
+
}
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2702
|
+
if (event.type === "fleet.snapshot" && client !== void 0) {
|
|
2703
|
+
const result = parseHqEventPayload(event.type, event.payload);
|
|
2704
|
+
if (result.ok) {
|
|
2705
|
+
const payload = result.payload;
|
|
2706
|
+
client.fleets.set(payload.runId, {
|
|
2707
|
+
payload,
|
|
2708
|
+
...event.sessionId !== void 0 ? { sessionId: event.sessionId } : {},
|
|
2709
|
+
receivedAt: Date.now()
|
|
2710
|
+
});
|
|
2711
|
+
snapshotBroadcaster.broadcast();
|
|
2712
|
+
}
|
|
2713
|
+
return;
|
|
2714
|
+
}
|
|
2715
|
+
if (event.type === "mcp.health.snapshot" && client !== void 0) {
|
|
2716
|
+
const result = parseHqEventPayload(event.type, event.payload);
|
|
2717
|
+
if (result.ok) {
|
|
2718
|
+
const payload = result.payload;
|
|
2719
|
+
const sessionId = event.sessionId || "unknown";
|
|
2720
|
+
const stamped = payload.servers.map((s) => ({
|
|
2721
|
+
...s,
|
|
2722
|
+
projectId: client.projectId,
|
|
2723
|
+
clientId: client.clientId
|
|
2724
|
+
}));
|
|
2725
|
+
client.mcpSnapshots.set(sessionId, { servers: stamped, receivedAt: Date.now() });
|
|
2726
|
+
snapshotBroadcaster.broadcast();
|
|
2727
|
+
}
|
|
2728
|
+
return;
|
|
2729
|
+
}
|
|
2730
|
+
if (event.type === "session.transcript" && client !== void 0) {
|
|
2731
|
+
const result = parseHqEventPayload(event.type, event.payload);
|
|
2732
|
+
if (result.ok) {
|
|
2733
|
+
const payload = result.payload;
|
|
2734
|
+
let ring = transcripts.get(payload.sessionId);
|
|
2735
|
+
if (!ring) {
|
|
2736
|
+
ring = { entries: [], ...client.machineId ? { machineId: client.machineId } : {} };
|
|
2737
|
+
}
|
|
2738
|
+
for (const entry of payload.entries) ring.entries.push(entry);
|
|
2739
|
+
if (ring.entries.length > TRANSCRIPT_RING_MAX) {
|
|
2740
|
+
ring.entries.splice(0, ring.entries.length - TRANSCRIPT_RING_MAX);
|
|
2741
|
+
}
|
|
2742
|
+
transcripts.delete(payload.sessionId);
|
|
2743
|
+
transcripts.set(payload.sessionId, ring);
|
|
2744
|
+
evictOldest(transcripts, MAX_TRANSCRIPT_SESSIONS);
|
|
2745
|
+
broadcastEvent(event, browsers);
|
|
2746
|
+
}
|
|
2747
|
+
return;
|
|
2748
|
+
}
|
|
2749
|
+
if (event.type === "agent.message") {
|
|
2750
|
+
const p = event.payload;
|
|
2751
|
+
const subId = p && typeof p["subagentId"] === "string" ? p["subagentId"] : void 0;
|
|
2752
|
+
if (subId) {
|
|
2753
|
+
const key = agentRingKey(event.sessionId, subId);
|
|
2754
|
+
let ring = agentMessages.get(key);
|
|
2755
|
+
if (!ring) ring = [];
|
|
2756
|
+
ring.push(agentMessageToEntry(p));
|
|
2757
|
+
if (ring.length > TRANSCRIPT_RING_MAX) ring.splice(0, ring.length - TRANSCRIPT_RING_MAX);
|
|
2758
|
+
agentMessages.delete(key);
|
|
2759
|
+
agentMessages.set(key, ring);
|
|
2760
|
+
evictOldest(agentMessages, MAX_AGENT_RINGS);
|
|
2761
|
+
}
|
|
2762
|
+
persistEvent(event);
|
|
2763
|
+
broadcastEvent(event, browsers);
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2766
|
+
persistEvent(event);
|
|
2767
|
+
broadcastEvent(event, browsers);
|
|
2768
|
+
}
|
|
2769
|
+
});
|
|
2770
|
+
ws.on("close", () => {
|
|
2771
|
+
clients.delete(ws);
|
|
2772
|
+
snapshotBroadcaster.broadcast();
|
|
2773
|
+
});
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
// src/hq-server.ts
|
|
2777
|
+
import { HqInsecureExposureError as HqInsecureExposureError2 } from "@wrongstack/core/hq";
|
|
2778
|
+
var DEFAULT_HOST = "127.0.0.1";
|
|
2779
|
+
var DEFAULT_PORT = 3499;
|
|
2780
|
+
var MAX_EVENT_LOG2 = 5e3;
|
|
2781
|
+
var MAX_NON_STRICT_PORT_SCAN = 50;
|
|
2782
|
+
var CLIENT_TTL_MS = 6e4;
|
|
2783
|
+
var CLIENT_CLEANUP_INTERVAL_MS = 3e4;
|
|
2784
|
+
var BROWSER_HEARTBEAT_INTERVAL_MS = 15e3;
|
|
2785
|
+
var SESSION_SNAPSHOT_TTL_MS = 3e4;
|
|
2786
|
+
async function startHqServer(options = {}) {
|
|
2787
|
+
const { host, port, dataDir, firstRunAuth } = await prepareHqServerStart(options, {
|
|
2788
|
+
host: DEFAULT_HOST,
|
|
2789
|
+
port: DEFAULT_PORT
|
|
2790
|
+
});
|
|
2791
|
+
return startHqServerWithAuth(options, host, port, dataDir, firstRunAuth);
|
|
2792
|
+
}
|
|
2793
|
+
async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth) {
|
|
2794
|
+
const trustBoundary = options.trustBoundary ?? createCompatibilityTrustBoundary({ policyId: "hq-trusted-host-compat-v1" });
|
|
2795
|
+
const authFile = firstRunAuth.authFile;
|
|
2796
|
+
const authState = createHqAuthState(authFile, dataDir);
|
|
2797
|
+
const { mutableAuth } = authState;
|
|
2798
|
+
if (!mutableAuth.cookieSecret && mutableAuth.browserTokens.size > 0) {
|
|
2799
|
+
const secret = mintHqCookieSecret2();
|
|
2800
|
+
const next = await mutateHqAuthFile2(dataDir, (current) => ({
|
|
2801
|
+
...current,
|
|
2802
|
+
cookieSecret: secret
|
|
2803
|
+
}));
|
|
2804
|
+
mutableAuth.cookieSecret = next.cookieSecret;
|
|
2805
|
+
console.warn(
|
|
2806
|
+
JSON.stringify({
|
|
2807
|
+
level: "info",
|
|
2808
|
+
event: "hq.cookie_secret_provisioned",
|
|
2809
|
+
message: "Cookie signing secret provisioned for token-mode bootstrap exchange.",
|
|
2810
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2811
|
+
})
|
|
2812
|
+
);
|
|
2813
|
+
}
|
|
2814
|
+
const bootstrapStore = new HqBootstrapCodeStore();
|
|
2815
|
+
console.warn(
|
|
2816
|
+
JSON.stringify({
|
|
2817
|
+
level: "info",
|
|
2818
|
+
event: "hq.startup",
|
|
2819
|
+
message: "WrongStack HQ starting",
|
|
2820
|
+
dataDir,
|
|
2821
|
+
host,
|
|
2822
|
+
port,
|
|
2823
|
+
operatorPolicyActive: authFile.redactionPolicy !== void 0,
|
|
2824
|
+
browserTokenMode: mutableAuth.browserTokens.size > 0,
|
|
2825
|
+
clientTokenMode: mutableAuth.clientTokens.size > 0,
|
|
2826
|
+
passwordMode: mutableAuth.passwordHash !== void 0,
|
|
2827
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2828
|
+
})
|
|
2829
|
+
);
|
|
2830
|
+
return new Promise((resolve, reject) => {
|
|
2831
|
+
const trustedPublicOrigins = /* @__PURE__ */ new Set();
|
|
2832
|
+
let listeningPort = port;
|
|
2833
|
+
const clients = /* @__PURE__ */ new Map();
|
|
2834
|
+
const browsers = /* @__PURE__ */ new Set();
|
|
2835
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
2836
|
+
const eventLog = [];
|
|
2837
|
+
const transcripts = /* @__PURE__ */ new Map();
|
|
2838
|
+
const agentMessages = /* @__PURE__ */ new Map();
|
|
2839
|
+
const mailboxGateways = /* @__PURE__ */ new Map();
|
|
2840
|
+
const mailboxGatewayRateLimiter = new MailboxHttpRateLimiter();
|
|
2841
|
+
const mailboxGatewayRateLimitCleanup = setInterval(
|
|
2842
|
+
() => mailboxGatewayRateLimiter.cleanup(),
|
|
2843
|
+
12e4
|
|
2844
|
+
);
|
|
2845
|
+
mailboxGatewayRateLimitCleanup.unref?.();
|
|
2846
|
+
const loginAttempts = /* @__PURE__ */ new Map();
|
|
2847
|
+
const LOGIN_ATTEMPT_RETENTION_MS = 15 * 6e4;
|
|
2848
|
+
const HQ_SESSION_MAX_AGE_MS2 = 7 * 24 * 60 * 60 * 1e3;
|
|
2849
|
+
const SESSION_CLEANUP_INTERVAL_MS = 6e4;
|
|
2850
|
+
const sessionCleanupTimer = setInterval(() => {
|
|
2851
|
+
const cutoff = Date.now() - HQ_SESSION_MAX_AGE_MS2;
|
|
2852
|
+
for (const [id, session] of sessions) {
|
|
2853
|
+
if (session.createdAt < cutoff) sessions.delete(id);
|
|
2854
|
+
}
|
|
2855
|
+
const now = Date.now();
|
|
2856
|
+
for (const [ip, entry] of loginAttempts) {
|
|
2857
|
+
if (now - entry.lastAttempt > LOGIN_ATTEMPT_RETENTION_MS) loginAttempts.delete(ip);
|
|
2858
|
+
}
|
|
2859
|
+
}, SESSION_CLEANUP_INTERVAL_MS);
|
|
2860
|
+
sessionCleanupTimer.unref?.();
|
|
2861
|
+
const persistence = createHqPersistence(dataDir);
|
|
2862
|
+
const auditLog = new HqCommandAuditLog(1e3, (entry) => persistence.commandLog.append(entry));
|
|
2863
|
+
void (async () => {
|
|
2864
|
+
const projectsDir = path6.join(path6.dirname(dataDir), "projects");
|
|
2865
|
+
const entries = await fs4.readdir(projectsDir, { withFileTypes: true }).catch(() => []);
|
|
2866
|
+
await Promise.all(
|
|
2867
|
+
entries.filter((entry) => entry.isDirectory()).map(async (entry) => {
|
|
2868
|
+
const projectDir = path6.join(projectsDir, entry.name);
|
|
2869
|
+
const metadataPath = path6.join(projectDir, ".mailbox-server.json");
|
|
2870
|
+
const ownerPid = await fs4.readFile(metadataPath, "utf8").then((raw) => JSON.parse(raw).pid).catch(() => void 0);
|
|
2871
|
+
if (ownerPid === void 0) return;
|
|
2872
|
+
let ownerAlive = false;
|
|
2873
|
+
try {
|
|
2874
|
+
process.kill(ownerPid, 0);
|
|
2875
|
+
ownerAlive = true;
|
|
2876
|
+
} catch {
|
|
2877
|
+
ownerAlive = false;
|
|
2878
|
+
}
|
|
2879
|
+
if (!ownerAlive) {
|
|
2880
|
+
await fs4.rm(metadataPath, { force: true }).catch(() => {
|
|
2881
|
+
});
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
const mailbox = getSharedProjectMailbox(projectDir);
|
|
2885
|
+
try {
|
|
2886
|
+
await Promise.all([mailbox.getAgentStatuses(), mailbox.purgeClients()]);
|
|
2887
|
+
} finally {
|
|
2888
|
+
await mailbox.close().catch(() => {
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
})
|
|
2892
|
+
);
|
|
2893
|
+
})().catch(() => {
|
|
2894
|
+
});
|
|
2895
|
+
const hqSessionTag = randomUUID4().slice(0, 8);
|
|
2896
|
+
const authorizeMailboxGateway = (req, projectDir) => {
|
|
2897
|
+
const requestUrl = new URL(req.url ?? "/", `http://${host}:${port}`);
|
|
2898
|
+
const auth = authenticateBrowserRequest2(req, requestUrl, mutableAuth, sessions);
|
|
2899
|
+
const tokenMode = mutableAuth.browserTokens.size > 0;
|
|
2900
|
+
const passwordMode = mutableAuth.passwordHash !== void 0;
|
|
2901
|
+
if ((tokenMode || passwordMode) && auth === void 0) {
|
|
2902
|
+
return {
|
|
2903
|
+
allowed: false,
|
|
2904
|
+
status: 401,
|
|
2905
|
+
body: { error: { code: "UNAUTHORIZED", message: "unauthorized" } }
|
|
2906
|
+
};
|
|
2907
|
+
}
|
|
2908
|
+
const token = isTokenAuth2(auth) ? mutableAuth.browserTokenObjs.get(auth.token) : void 0;
|
|
2909
|
+
const canUseMailbox = isCookieAuth2(auth) ? auth.capabilities === void 0 || auth.capabilities.includes("control.enqueue") : isTokenAuth2(auth) ? token?.capabilities === void 0 || !!token?.capabilities.includes("control.enqueue") : !tokenMode;
|
|
2910
|
+
if (!canUseMailbox) {
|
|
2911
|
+
return {
|
|
2912
|
+
allowed: false,
|
|
2913
|
+
status: 403,
|
|
2914
|
+
body: {
|
|
2915
|
+
error: {
|
|
2916
|
+
code: "FORBIDDEN",
|
|
2917
|
+
message: "forbidden: token lacks control.enqueue capability"
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
};
|
|
2921
|
+
}
|
|
2922
|
+
const identity = isTokenAuth2(auth) ? auth.id : isCookieAuth2(auth) ? "cookie" : "open";
|
|
2923
|
+
return { allowed: true, rateLimitKey: `hq:${identity}:${projectDir}` };
|
|
2924
|
+
};
|
|
2925
|
+
const getMailboxGateway = (projectDir) => {
|
|
2926
|
+
const existing = mailboxGateways.get(projectDir);
|
|
2927
|
+
if (existing) return existing;
|
|
2928
|
+
const eventEmitter = new MailboxEventEmitter();
|
|
2929
|
+
const mailbox = createProjectMailbox({ projectDir, eventEmitter });
|
|
2930
|
+
const router = createMailboxHttpRouter({
|
|
2931
|
+
mailbox,
|
|
2932
|
+
eventEmitter,
|
|
2933
|
+
rateLimiter: mailboxGatewayRateLimiter,
|
|
2934
|
+
authorize: (request) => authorizeMailboxGateway(request, projectDir),
|
|
2935
|
+
// Wire the 1h look-back that the router's docs promise at
|
|
2936
|
+
// mailbox-http-router.ts:25-32 / :47-62. The router itself is
|
|
2937
|
+
// opt-in (L146-152): without this option, every retained
|
|
2938
|
+
// message would be returned. Per-request opt-in to the full
|
|
2939
|
+
// history remains available via `?sinceMs=0`.
|
|
2940
|
+
defaultMaxAgeMs: MAILBOX_HTTP_DEFAULT_MAX_AGE_MS
|
|
2941
|
+
});
|
|
2942
|
+
const gateway = { mailbox, router };
|
|
2943
|
+
mailboxGateways.set(projectDir, gateway);
|
|
2944
|
+
return gateway;
|
|
2945
|
+
};
|
|
2946
|
+
const alertEngine = new HqAlertEngine({
|
|
2947
|
+
onAlert: (alert) => {
|
|
2948
|
+
const msg = toAlertMessage(alert);
|
|
2949
|
+
const data = JSON.stringify(msg);
|
|
2950
|
+
for (const ws of browsers) {
|
|
2951
|
+
if (ws.readyState === WebSocket3.OPEN) ws.send(data);
|
|
2952
|
+
}
|
|
2953
|
+
},
|
|
2954
|
+
onPersist: (alert) => persistence.alertLog.append(alert)
|
|
2955
|
+
});
|
|
2956
|
+
void persistence.eventLog.hydrate().catch(() => void 0);
|
|
2957
|
+
void persistence.timeseries.load().catch(() => void 0);
|
|
2958
|
+
persistence.eventLog.recent(MAX_EVENT_LOG2).then((prior) => {
|
|
2959
|
+
for (let i = prior.length - 1; i >= 0; i--) eventLog.push(prior[i]);
|
|
2960
|
+
}).catch(() => {
|
|
2961
|
+
});
|
|
2962
|
+
persistence.alertLog.readAll().then((prior) => {
|
|
2963
|
+
alertEngine.seed(prior);
|
|
2964
|
+
}).catch(() => {
|
|
2965
|
+
});
|
|
2966
|
+
persistence.commandLog.readAll().then((prior) => {
|
|
2967
|
+
auditLog.seed(prior);
|
|
2968
|
+
}).catch(() => {
|
|
2969
|
+
});
|
|
2970
|
+
const timeseriesFlushTimer = setInterval(() => {
|
|
2971
|
+
persistence.timeseries.flush();
|
|
2972
|
+
}, 6e4);
|
|
2973
|
+
timeseriesFlushTimer.unref?.();
|
|
2974
|
+
const snapshotBroadcaster = createSnapshotBroadcaster(
|
|
2975
|
+
clients,
|
|
2976
|
+
browsers,
|
|
2977
|
+
persistence
|
|
2978
|
+
);
|
|
2979
|
+
snapshotBroadcaster.currentSerialized();
|
|
2980
|
+
const stopAlertEngine = alertEngine.startPeriodic(
|
|
2981
|
+
() => buildSnapshot(clients, { tokenStats: authState.tokenStats() }),
|
|
2982
|
+
() => mutableAuth.alertRules
|
|
2983
|
+
);
|
|
2984
|
+
const clientTtlMs = options.clientTtlMs ?? CLIENT_TTL_MS;
|
|
2985
|
+
const sessionSnapshotTtlMs = options.sessionSnapshotTtlMs ?? SESSION_SNAPSHOT_TTL_MS;
|
|
2986
|
+
const cleanupTimer = setInterval(() => {
|
|
2987
|
+
const cutoff = Date.now() - clientTtlMs;
|
|
2988
|
+
const sessionCutoff = Date.now() - sessionSnapshotTtlMs;
|
|
2989
|
+
let changed = false;
|
|
2990
|
+
for (const [ws, client] of clients.entries()) {
|
|
2991
|
+
if (new Date(client.lastSeenAt).getTime() < cutoff) {
|
|
2992
|
+
ws.terminate();
|
|
2993
|
+
clients.delete(ws);
|
|
2994
|
+
changed = true;
|
|
2995
|
+
continue;
|
|
2996
|
+
}
|
|
2997
|
+
const expiredSessions = /* @__PURE__ */ new Set();
|
|
2998
|
+
for (const [sessionId, tracked] of client.sessions.entries()) {
|
|
2999
|
+
if (tracked.receivedAt < sessionCutoff) {
|
|
3000
|
+
client.sessions.delete(sessionId);
|
|
3001
|
+
client.mcpSnapshots.delete(sessionId);
|
|
3002
|
+
expiredSessions.add(sessionId);
|
|
3003
|
+
changed = true;
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
for (const [runId, fleet] of client.fleets) {
|
|
3007
|
+
if (fleet.sessionId !== void 0 && expiredSessions.has(fleet.sessionId)) {
|
|
3008
|
+
client.fleets.delete(runId);
|
|
3009
|
+
changed = true;
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
if (client.capabilities.includes("session.summary") && client.sessions.size === 0 && Date.now() - Date.parse(client.connectedAt) > sessionSnapshotTtlMs) {
|
|
3013
|
+
ws.terminate();
|
|
3014
|
+
clients.delete(ws);
|
|
3015
|
+
changed = true;
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
if (changed) snapshotBroadcaster.broadcast();
|
|
3019
|
+
}, options.clientCleanupIntervalMs ?? CLIENT_CLEANUP_INTERVAL_MS);
|
|
3020
|
+
cleanupTimer.unref?.();
|
|
3021
|
+
const routerDeps = {
|
|
3022
|
+
trustBoundary,
|
|
3023
|
+
host,
|
|
3024
|
+
listeningPort,
|
|
3025
|
+
trustedPublicOrigins,
|
|
3026
|
+
mutableAuth,
|
|
3027
|
+
sessions,
|
|
3028
|
+
loginAttempts,
|
|
3029
|
+
clients,
|
|
3030
|
+
browsers,
|
|
3031
|
+
eventLog,
|
|
3032
|
+
transcripts,
|
|
3033
|
+
agentMessages,
|
|
3034
|
+
mailboxGateways,
|
|
3035
|
+
mailboxGatewayRateLimiter,
|
|
3036
|
+
alertEngine,
|
|
3037
|
+
auditLog,
|
|
3038
|
+
persistence,
|
|
3039
|
+
dataDir,
|
|
3040
|
+
hqSessionTag,
|
|
3041
|
+
requireBrowserAuth: options.requireBrowserAuth,
|
|
3042
|
+
secureCookies: options.secureCookies,
|
|
3043
|
+
authorizeMailboxGateway,
|
|
3044
|
+
getMailboxGateway,
|
|
3045
|
+
getTokenStats: () => authState.tokenStats(),
|
|
3046
|
+
bootstrapStore
|
|
3047
|
+
};
|
|
3048
|
+
const handleRequest = createHqRouter(routerDeps);
|
|
3049
|
+
const httpServer = http.createServer((req, res) => {
|
|
3050
|
+
void handleRequest(req, res);
|
|
3051
|
+
});
|
|
3052
|
+
const wss = new WebSocketServer({ noServer: true, maxPayload: 1 * 1024 * 1024 });
|
|
3053
|
+
const browserHeartbeatTimer = setInterval(() => {
|
|
3054
|
+
const heartbeat = JSON.stringify({
|
|
3055
|
+
type: "hq.heartbeat",
|
|
3056
|
+
serverTime: (/* @__PURE__ */ new Date()).toISOString()
|
|
3057
|
+
});
|
|
3058
|
+
for (const browser of browsers) {
|
|
3059
|
+
if (browser.readyState === WebSocket3.OPEN) browser.send(heartbeat);
|
|
3060
|
+
}
|
|
3061
|
+
}, options.browserHeartbeatIntervalMs ?? BROWSER_HEARTBEAT_INTERVAL_MS);
|
|
3062
|
+
browserHeartbeatTimer.unref?.();
|
|
3063
|
+
httpServer.on("upgrade", (req, socket, head) => {
|
|
3064
|
+
const url = new URL(req.url ?? "/", `http://${host}:${port}`);
|
|
3065
|
+
const pathname = url.pathname;
|
|
3066
|
+
if (pathname !== "/ws/client" && pathname !== "/ws/browser") {
|
|
3067
|
+
socket.destroy();
|
|
3068
|
+
return;
|
|
3069
|
+
}
|
|
3070
|
+
if (!hasTrustedBrowserOrigin2(req, host, listeningPort, trustedPublicOrigins)) {
|
|
3071
|
+
socket.write(
|
|
3072
|
+
"HTTP/1.1 403 Forbidden\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n" + JSON.stringify({
|
|
3073
|
+
error: {
|
|
3074
|
+
code: "INVALID_ORIGIN",
|
|
3075
|
+
message: "Cross-origin WebSocket upgrade rejected."
|
|
3076
|
+
}
|
|
3077
|
+
})
|
|
3078
|
+
);
|
|
3079
|
+
socket.destroy();
|
|
3080
|
+
return;
|
|
3081
|
+
}
|
|
3082
|
+
const tokenSet = pathname === "/ws/browser" ? mutableAuth.browserTokens : mutableAuth.clientTokens;
|
|
3083
|
+
const needsAuth = pathname === "/ws/browser" ? options.requireBrowserAuth || tokenSet.size > 0 || mutableAuth.passwordHash !== void 0 : tokenSet.size > 0;
|
|
3084
|
+
if (needsAuth) {
|
|
3085
|
+
const supplied = url.searchParams.get("token") ?? "";
|
|
3086
|
+
const tokenValid = timingSafeTokenMatch(tokenSet, supplied) !== void 0;
|
|
3087
|
+
const cookieValid = pathname === "/ws/browser" && mutableAuth.cookieSecret !== void 0 && (() => {
|
|
3088
|
+
const cookies = parseCookieHeader2(req.headers.cookie);
|
|
3089
|
+
const raw = cookies[HQ_SESSION_COOKIE2];
|
|
3090
|
+
if (!raw) return false;
|
|
3091
|
+
const sessionId = parseHqSessionCookie2(raw, mutableAuth.cookieSecret);
|
|
3092
|
+
if (sessionId === void 0) return false;
|
|
3093
|
+
const session = sessions.get(sessionId);
|
|
3094
|
+
if (!session || Date.now() - session.createdAt >= HQ_SESSION_MAX_AGE_MS)
|
|
3095
|
+
return false;
|
|
3096
|
+
if (session.kind === "token" && session.tokenId !== void 0) {
|
|
3097
|
+
return [...mutableAuth.browserTokenObjs.values()].some(
|
|
3098
|
+
(obj) => obj.id === session.tokenId
|
|
3099
|
+
);
|
|
3100
|
+
}
|
|
3101
|
+
return true;
|
|
3102
|
+
})();
|
|
3103
|
+
if (!tokenValid && !cookieValid) {
|
|
3104
|
+
socket.write(
|
|
3105
|
+
"HTTP/1.1 401 Unauthorized\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n" + JSON.stringify({
|
|
3106
|
+
error: {
|
|
3107
|
+
code: "UNAUTHORIZED",
|
|
3108
|
+
message: pathname === "/ws/browser" ? "A valid ?token= or password session is required for browser connections." : "A valid ?token= is required for client connections in token mode."
|
|
3109
|
+
}
|
|
3110
|
+
})
|
|
3111
|
+
);
|
|
3112
|
+
socket.destroy();
|
|
3113
|
+
return;
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
3117
|
+
wss.emit("connection", ws, req, pathname);
|
|
3118
|
+
});
|
|
3119
|
+
});
|
|
3120
|
+
wss.on("connection", (ws, req, pathname) => {
|
|
3121
|
+
if (pathname === "/ws/browser") {
|
|
3122
|
+
handleBrowser(ws, snapshotBroadcaster, browsers);
|
|
3123
|
+
} else {
|
|
3124
|
+
const token = new URL(req.url ?? "/", `http://${host}:${port}`).searchParams.get("token");
|
|
3125
|
+
handleClient(
|
|
3126
|
+
ws,
|
|
3127
|
+
clients,
|
|
3128
|
+
browsers,
|
|
3129
|
+
eventLog,
|
|
3130
|
+
{
|
|
3131
|
+
...token ? { token: mutableAuth.clientTokenObjs.get(token) } : {},
|
|
3132
|
+
getOperatorPolicy: () => mutableAuth.operatorPolicyOverride
|
|
3133
|
+
},
|
|
3134
|
+
snapshotBroadcaster,
|
|
3135
|
+
transcripts,
|
|
3136
|
+
agentMessages,
|
|
3137
|
+
persistence,
|
|
3138
|
+
auditLog
|
|
3139
|
+
);
|
|
3140
|
+
}
|
|
3141
|
+
});
|
|
3142
|
+
const authWatcher = watchHqAuthFile(
|
|
3143
|
+
dataDir,
|
|
3144
|
+
(next) => {
|
|
3145
|
+
authState.apply(next);
|
|
3146
|
+
if (options.requireBrowserAuth && mutableAuth.browserTokens.size === 0 && mutableAuth.passwordHash === void 0) {
|
|
3147
|
+
sessions.clear();
|
|
3148
|
+
for (const browser of browsers) browser.close(1008, "Browser authentication removed");
|
|
3149
|
+
}
|
|
3150
|
+
console.warn(
|
|
3151
|
+
JSON.stringify({
|
|
3152
|
+
level: "info",
|
|
3153
|
+
event: "hq.auth.reloaded",
|
|
3154
|
+
message: "HQ auth.json reloaded",
|
|
3155
|
+
browserTokenCount: mutableAuth.browserTokens.size,
|
|
3156
|
+
clientTokenCount: mutableAuth.clientTokens.size,
|
|
3157
|
+
passwordMode: mutableAuth.passwordHash !== void 0,
|
|
3158
|
+
alertRulesActive: mutableAuth.alertRules !== void 0,
|
|
3159
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3160
|
+
})
|
|
3161
|
+
);
|
|
3162
|
+
},
|
|
3163
|
+
{
|
|
3164
|
+
warn: (msg) => console.warn(
|
|
3165
|
+
JSON.stringify({
|
|
3166
|
+
level: "warn",
|
|
3167
|
+
event: "hq.auth.reload_failed",
|
|
3168
|
+
message: msg,
|
|
3169
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3170
|
+
})
|
|
3171
|
+
)
|
|
3172
|
+
}
|
|
3173
|
+
);
|
|
3174
|
+
let bindAttempts = 0;
|
|
3175
|
+
const listen = (nextPort) => {
|
|
3176
|
+
httpServer.listen(nextPort, host);
|
|
3177
|
+
};
|
|
3178
|
+
const onError = (err) => {
|
|
3179
|
+
if (err.code === "EADDRINUSE" && !options.strictPort && !options.exactPort && bindAttempts < MAX_NON_STRICT_PORT_SCAN) {
|
|
3180
|
+
bindAttempts += 1;
|
|
3181
|
+
listen(port + bindAttempts);
|
|
3182
|
+
} else {
|
|
3183
|
+
authWatcher.close();
|
|
3184
|
+
snapshotBroadcaster.close();
|
|
3185
|
+
wss.close();
|
|
3186
|
+
reject(err);
|
|
3187
|
+
}
|
|
3188
|
+
};
|
|
3189
|
+
httpServer.on("error", onError);
|
|
3190
|
+
listen(port);
|
|
3191
|
+
httpServer.once("listening", () => {
|
|
3192
|
+
void (async () => {
|
|
3193
|
+
httpServer.removeListener("error", onError);
|
|
3194
|
+
const addr = httpServer.address();
|
|
3195
|
+
const actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
3196
|
+
listeningPort = actualPort;
|
|
3197
|
+
const browserToken = firstRunAuth.browserToken?.token ?? authFile.browserTokens?.find((t) => t.token.trim().length > 0)?.token;
|
|
3198
|
+
const clientToken = firstRunAuth.clientToken?.token ?? authFile.clientTokens?.find((t) => t.token.trim().length > 0)?.token;
|
|
3199
|
+
const hqUrl = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${actualPort}`;
|
|
3200
|
+
await writeHqRuntimeMarker(dataDir, hqUrl).catch(() => {
|
|
3201
|
+
});
|
|
3202
|
+
const browserTokenObj = mutableAuth.browserTokenObjs.get(browserToken ?? "");
|
|
3203
|
+
let browserUrl;
|
|
3204
|
+
if (browserToken && browserTokenObj) {
|
|
3205
|
+
const code = bootstrapStore.issue({
|
|
3206
|
+
tokenId: browserTokenObj.id,
|
|
3207
|
+
...browserTokenObj.capabilities !== void 0 ? { capabilities: browserTokenObj.capabilities } : {}
|
|
3208
|
+
});
|
|
3209
|
+
browserUrl = buildBootstrapHttpUrl2(host, actualPort, code);
|
|
3210
|
+
} else {
|
|
3211
|
+
browserUrl = buildHttpUrl2(host, actualPort);
|
|
3212
|
+
}
|
|
3213
|
+
const startupInfo = {
|
|
3214
|
+
dataDir,
|
|
3215
|
+
browserUrl,
|
|
3216
|
+
clientUrl: buildClientWsUrl2(host, actualPort, clientToken),
|
|
3217
|
+
clientEnv: {
|
|
3218
|
+
WRONGSTACK_HQ_URL: hqUrl,
|
|
3219
|
+
...clientToken ? { WRONGSTACK_HQ_TOKEN: clientToken } : {}
|
|
3220
|
+
},
|
|
3221
|
+
createdAuth: firstRunAuth.created,
|
|
3222
|
+
browserTokenMode: mutableAuth.browserTokens.size > 0,
|
|
3223
|
+
passwordMode: mutableAuth.passwordHash !== void 0
|
|
3224
|
+
};
|
|
3225
|
+
let closed = false;
|
|
3226
|
+
const handle = {
|
|
3227
|
+
host,
|
|
3228
|
+
port: actualPort,
|
|
3229
|
+
firstRunSetup: startupInfo,
|
|
3230
|
+
trustPublicOrigin: (origin) => {
|
|
3231
|
+
const parsed = new URL(origin);
|
|
3232
|
+
if (parsed.protocol !== "https:" || parsed.origin !== origin) {
|
|
3233
|
+
throw new TypeError(
|
|
3234
|
+
"Trusted HQ public origin must be an exact HTTPS origin without a path."
|
|
3235
|
+
);
|
|
3236
|
+
}
|
|
3237
|
+
trustedPublicOrigins.add(parsed.origin.toLowerCase());
|
|
3238
|
+
},
|
|
3239
|
+
close: () => new Promise((res) => {
|
|
3240
|
+
if (closed) {
|
|
3241
|
+
res();
|
|
3242
|
+
return;
|
|
3243
|
+
}
|
|
3244
|
+
closed = true;
|
|
3245
|
+
clearInterval(cleanupTimer);
|
|
3246
|
+
clearInterval(browserHeartbeatTimer);
|
|
3247
|
+
clearInterval(timeseriesFlushTimer);
|
|
3248
|
+
clearInterval(mailboxGatewayRateLimitCleanup);
|
|
3249
|
+
clearInterval(sessionCleanupTimer);
|
|
3250
|
+
stopAlertEngine();
|
|
3251
|
+
authWatcher.close();
|
|
3252
|
+
snapshotBroadcaster.close();
|
|
3253
|
+
bootstrapStore.clear();
|
|
3254
|
+
persistence.timeseries.flush();
|
|
3255
|
+
for (const ws of browsers) ws.close(1001, "HQ shutting down");
|
|
3256
|
+
for (const ws of clients.keys()) ws.close(1001, "HQ shutting down");
|
|
3257
|
+
wss.close();
|
|
3258
|
+
for (const { router } of mailboxGateways.values()) router.close();
|
|
3259
|
+
httpServer.close(() => {
|
|
3260
|
+
const mailboxCloses = [...mailboxGateways.values()].map(
|
|
3261
|
+
({ mailbox }) => mailbox.close().catch(() => void 0)
|
|
3262
|
+
);
|
|
3263
|
+
mailboxGateways.clear();
|
|
3264
|
+
void Promise.all([
|
|
3265
|
+
...mailboxCloses,
|
|
3266
|
+
persistence.eventLog.drain(),
|
|
3267
|
+
persistence.snapshotStore.drain(),
|
|
3268
|
+
persistence.timeseries.drain(),
|
|
3269
|
+
persistence.kanban.drain()
|
|
3270
|
+
]).then(() => clearHqRuntimeMarker(dataDir, hqUrl)).catch(() => void 0).finally(() => res());
|
|
3271
|
+
});
|
|
3272
|
+
})
|
|
3273
|
+
};
|
|
3274
|
+
writeHqStartupInfo((line) => console.log(line.trimEnd()), {
|
|
3275
|
+
host,
|
|
3276
|
+
port: actualPort,
|
|
3277
|
+
firstRunSetup: startupInfo
|
|
3278
|
+
});
|
|
3279
|
+
resolve(handle);
|
|
3280
|
+
})();
|
|
3281
|
+
});
|
|
3282
|
+
});
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
export {
|
|
3286
|
+
HQ_HTML,
|
|
3287
|
+
decodePathSegment2 as decodePathSegment,
|
|
3288
|
+
sanitizeApiError2 as sanitizeApiError,
|
|
3289
|
+
agentRingKey2 as agentRingKey,
|
|
3290
|
+
agentMessageToEntry2 as agentMessageToEntry,
|
|
3291
|
+
readLocalSubagentTranscript2 as readLocalSubagentTranscript,
|
|
3292
|
+
DEFAULT_HOST,
|
|
3293
|
+
DEFAULT_PORT,
|
|
3294
|
+
MAX_EVENT_LOG2 as MAX_EVENT_LOG,
|
|
3295
|
+
MAX_NON_STRICT_PORT_SCAN,
|
|
3296
|
+
CLIENT_TTL_MS,
|
|
3297
|
+
CLIENT_CLEANUP_INTERVAL_MS,
|
|
3298
|
+
BROWSER_HEARTBEAT_INTERVAL_MS,
|
|
3299
|
+
SESSION_SNAPSHOT_TTL_MS,
|
|
3300
|
+
startHqServer,
|
|
3301
|
+
HqInsecureExposureError2 as HqInsecureExposureError
|
|
3302
|
+
};
|
|
3303
|
+
//# sourceMappingURL=chunk-OI7GWJDS.js.map
|