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