@wrongstack/cli 0.302.0 → 0.303.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/data/providers.json +41 -155
  2. package/dist/{acp-DATHHPNT.js → acp-AQMGKW6O.js} +6 -5
  3. package/dist/{audit-QRCLEHHW.js → audit-BBR22QH3.js} +2 -2
  4. package/dist/{auth-PJD3JRH2.js → auth-K7CYVVKH.js} +121 -78
  5. package/dist/auth-menu/index.d.ts +5 -5
  6. package/dist/auth-menu/local.d.ts +4 -5
  7. package/dist/auth-menu/openai-codex-oauth.d.ts +1 -1
  8. package/dist/{auto-discover-providers-DRW4OZHG.js → auto-discover-providers-MHFLDSD3.js} +2 -2
  9. package/dist/{bench-7ZDWSNEF.js → bench-XNDUJOSZ.js} +10 -11
  10. package/dist/boot/auto-discover-providers.d.ts +1 -1
  11. package/dist/boot/container-wiring.d.ts +3 -3
  12. package/dist/boot/dispatch-webui.d.ts +1 -1
  13. package/dist/boot/system-prompt.d.ts +1 -2
  14. package/dist/boot/tool-registry.d.ts +1 -1
  15. package/dist/boot/tui-coordinator-setup.d.ts +1 -1
  16. package/dist/boot/tui-project-picker-callback.d.ts +1 -1
  17. package/dist/boot/tui-theme-adapter.d.ts +33 -0
  18. package/dist/boot-config.d.ts +2 -2
  19. package/dist/boot.d.ts +1 -1
  20. package/dist/{chunk-ETOEGL3V.js → chunk-3IC4IEZC.js} +7 -2
  21. package/dist/{chunk-PFVVUH5B.js → chunk-3JPTNADJ.js} +3 -2
  22. package/dist/{chunk-4GVH7V3S.js → chunk-52C7PD2H.js} +1 -1
  23. package/dist/{chunk-I5MEUFDI.js → chunk-53STH6CX.js} +94 -15
  24. package/dist/{chunk-OP47B3IX.js → chunk-7YLN7YUA.js} +7 -2
  25. package/dist/{chunk-263G3FMB.js → chunk-B4JLSXZB.js} +11 -14
  26. package/dist/{chunk-MT6FGXO3.js → chunk-C5NXM2SI.js} +19 -6
  27. package/dist/{chunk-XTIXVJXP.js → chunk-DGXNL7OT.js} +1 -1
  28. package/dist/{chunk-3MOUBRKZ.js → chunk-FJVCVZIV.js} +974 -935
  29. package/dist/{chunk-MRLYDHQ7.js → chunk-JQURRYU4.js} +4 -6
  30. package/dist/{chunk-ZECLGUTF.js → chunk-KV6DNQT6.js} +2 -2
  31. package/dist/{chunk-2KDOFTTM.js → chunk-LUAFQIXT.js} +1067 -900
  32. package/dist/{chunk-F4PEGAJZ.js → chunk-NUIHRGPY.js} +1 -1
  33. package/dist/{chunk-GUHQQG63.js → chunk-RK7E3IXT.js} +2 -4
  34. package/dist/{chunk-PYTFS4MC.js → chunk-SSSJQVUY.js} +27 -16
  35. package/dist/{chunk-PJHET45A.js → chunk-SZ42FYPT.js} +3 -3
  36. package/dist/{chunk-A6Q5KGLV.js → chunk-YKBRFSGL.js} +5 -5
  37. package/dist/cli-context.d.ts +2 -2
  38. package/dist/cli-eternal-flag.d.ts +1 -2
  39. package/dist/{cli-main-EOJ74CI4.js → cli-main-37XO26GK.js} +3269 -2206
  40. package/dist/context-limit.d.ts +11 -5
  41. package/dist/{diag-doctor-OQ3AA63O.js → diag-doctor-ZF5BF2MK.js} +2 -3
  42. package/dist/execute-deps.d.ts +1 -1
  43. package/dist/{execution-CQ772VPS.js → execution-MVOH6PAQ.js} +302 -149
  44. package/dist/{export-DOBGPY23.js → export-CCUXRJOU.js} +3 -11
  45. package/dist/fleet/host-context.d.ts +9 -0
  46. package/dist/fleet/host-learning-tracker.d.ts +2 -2
  47. package/dist/fleet/host-learning.d.ts +12 -1
  48. package/dist/fleet/host-subagent-factory.d.ts +1 -1
  49. package/dist/fleet/host-supervisor.d.ts +1 -1
  50. package/dist/fleet/host.d.ts +23 -1
  51. package/dist/fleet/routing.d.ts +1 -2
  52. package/dist/goal-host.d.ts +2 -2
  53. package/dist/hooks-wiring.d.ts +1 -1
  54. package/dist/{hq-LN5CJHCM.js → hq-CD77GVSS.js} +104 -55
  55. package/dist/hq-command-controller.d.ts +1 -1
  56. package/dist/hq-server/routes/system-handlers.d.ts +1 -2
  57. package/dist/hq-server/routes.d.ts +7 -7
  58. package/dist/{hq-server-S7HZG3PA.js → hq-server-DME6EWIO.js} +2 -2
  59. package/dist/index.d.ts +1 -1
  60. package/dist/index.js +2635 -2642
  61. package/dist/mailbox-bridge-bootstrap.d.ts +2 -2
  62. package/dist/{mcp-OHPJH73U.js → mcp-AX6ZAFJR.js} +8 -7
  63. package/dist/mcp-serve.d.ts +1 -1
  64. package/dist/{modeldiag-3DOK4BWW.js → modeldiag-SIVUOX2Y.js} +18 -13
  65. package/dist/{permissions-FNYLZIL4.js → permissions-2PWBL4YU.js} +5 -3
  66. package/dist/picker.d.ts +1 -1
  67. package/dist/plugin-management.d.ts +1 -1
  68. package/dist/{plugin-usage-YEVLOR33.js → plugin-usage-PTEETL3J.js} +2 -2
  69. package/dist/{plugins-K3BAUP4D.js → plugins-XRGWC3NJ.js} +3 -3
  70. package/dist/pre-launch.d.ts +2 -2
  71. package/dist/{providers-models-WEZ4EGLL.js → providers-models-VV74TRQ2.js} +86 -37
  72. package/dist/repl.d.ts +1 -1
  73. package/dist/{replay-FCBNHW3K.js → replay-WBRMEWQ5.js} +2 -2
  74. package/dist/{rewind-AOD3JIYX.js → rewind-7RE6ONIC.js} +17 -17
  75. package/dist/{sdd-runtime-AIJCJGWC.js → sdd-runtime-GFZ3NJKS.js} +2 -2
  76. package/dist/{sessions-config-XXN7267N.js → sessions-config-YX2ZPAFM.js} +36 -32
  77. package/dist/{simpleui-dist-JIFGUJO7.js → simpleui-dist-33VO3QHF.js} +9 -2
  78. package/dist/slash-commands/agent-improve.d.ts +4 -1
  79. package/dist/slash-commands/agents.d.ts +14 -0
  80. package/dist/slash-commands/command-context.d.ts +2 -3
  81. package/dist/slash-commands/helpers.d.ts +1 -1
  82. package/dist/slash-commands/index.d.ts +1 -1
  83. package/dist/slash-commands/intake.d.ts +10 -0
  84. package/dist/slash-commands/memory-triage.d.ts +1 -1
  85. package/dist/slash-commands/provider-status.d.ts +1 -1
  86. package/dist/slash-commands/theme.d.ts +6 -0
  87. package/dist/subcommands/handlers/hq.d.ts +20 -0
  88. package/dist/subcommands/handlers/per-subcommand-help-data.d.ts +1 -1
  89. package/dist/subcommands/handlers/update.d.ts +2 -0
  90. package/dist/{update-MV6TUACD.js → update-VZSOZGYC.js} +3 -3
  91. package/dist/{update-check-UMYTOH7Z.js → update-check-WARHSVZA.js} +2 -2
  92. package/dist/webui-server/connection-handler.d.ts +1 -1
  93. package/dist/webui-server/context-breakdown.d.ts +1 -1
  94. package/dist/webui-server/cost-helpers.d.ts +1 -1
  95. package/dist/webui-server/lifecycle.d.ts +1 -1
  96. package/dist/webui-server/prefs-seeding.d.ts +1 -1
  97. package/dist/webui-server/setup-events.d.ts +1 -1
  98. package/dist/webui-server/static-serve.d.ts +1 -1
  99. package/dist/{webui-server-NFNRGNNT.js → webui-server-E3OPEKU5.js} +18 -10
  100. package/dist/webui-server-options.d.ts +1 -1
  101. package/dist/wiring/command-host-state.d.ts +2 -2
  102. package/dist/wiring/controllers.d.ts +1 -2
  103. package/dist/wiring/dep-watcher.d.ts +1 -1
  104. package/dist/wiring/director-setup.d.ts +3 -3
  105. package/dist/wiring/eternal-command-handlers.d.ts +2 -2
  106. package/dist/wiring/management-tools.d.ts +2 -2
  107. package/dist/wiring/provider-runtime.d.ts +1 -1
  108. package/dist/wiring/provider-status.d.ts +1 -1
  109. package/dist/wiring/provider-utility-tools.d.ts +3 -3
  110. package/dist/wiring/provider.d.ts +1 -1
  111. package/dist/wiring/replay.d.ts +1 -1
  112. package/dist/wiring/runtime-controller-deps.d.ts +1 -1
  113. package/dist/wiring/runtime-dispatch-state.d.ts +1 -2
  114. package/dist/wiring/runtime-lifecycle-deps.d.ts +2 -2
  115. package/dist/wiring/runtime-picker-deps.d.ts +1 -2
  116. package/dist/wiring/sage.d.ts +1 -1
  117. package/dist/wiring/session-command-handlers.d.ts +1 -2
  118. package/dist/wiring/session.d.ts +6 -1
  119. package/dist/wiring/slash-commands.d.ts +2 -3
  120. package/package.json +23 -23
@@ -27,13 +27,14 @@ import {
27
27
  HqCommandAuditLog,
28
28
  hqTokenKey as hqTokenKey2,
29
29
  hqTokenVerifier as hqTokenVerifier2,
30
+ isLoopbackHost as isLoopbackHost3,
31
+ isTokenExpired as isTokenExpired4,
30
32
  mintHqCookieSecret as mintHqCookieSecret2,
31
33
  mutateHqAuthFile as mutateHqAuthFile2,
32
34
  toAlertMessage,
33
35
  watchHqAuthFile
34
36
  } from "@wrongstack/core/hq";
35
37
  import { createCompatibilityTrustBoundary } from "@wrongstack/core/security";
36
- import { isLoopbackHost as isLoopbackHost3 } from "@wrongstack/core/hq";
37
38
  import { WebSocket as WebSocket3, WebSocketServer } from "ws";
38
39
 
39
40
  // src/hq-recovery-html.ts
@@ -414,8 +415,8 @@ function auditPrunedTokens(scope, previous, next, authFile, dataDir) {
414
415
  import { createHash } from "node:crypto";
415
416
  import * as fs from "node:fs/promises";
416
417
  import * as path from "node:path";
417
- import { atomicWrite } from "@wrongstack/core/utils";
418
418
  import { restrictFilePermissions } from "@wrongstack/core/security";
419
+ import { atomicWrite } from "@wrongstack/core/utils";
419
420
  var LoginAttemptStore = class _LoginAttemptStore {
420
421
  store = /* @__PURE__ */ new Map();
421
422
  writeTimer;
@@ -586,10 +587,10 @@ var LoginAttemptStore = class _LoginAttemptStore {
586
587
  import {
587
588
  assessHqExposure,
588
589
  ensureHqFirstRunAuthFile,
590
+ HqInsecureExposureError,
589
591
  isTokenExpired as isTokenExpired3,
590
592
  resolveHqDataDir
591
593
  } from "@wrongstack/core/hq";
592
- import { HqInsecureExposureError } from "@wrongstack/core/hq";
593
594
  async function prepareHqServerStart(options, defaults) {
594
595
  const host = options.host ?? defaults.host;
595
596
  const port = options.port ?? defaults.port;
@@ -645,10 +646,13 @@ async function prepareHqServerStart(options, defaults) {
645
646
  // src/hq-server/routes.ts
646
647
  import { randomUUID as randomUUID2 } from "node:crypto";
647
648
  import * as path5 from "node:path";
648
- import { tokenHasCapability, validateHqCommand } from "@wrongstack/core/hq";
649
649
  import {
650
650
  resolveProjectDir
651
651
  } from "@wrongstack/core/coordination";
652
+ import {
653
+ tokenHasCapability,
654
+ validateHqCommand
655
+ } from "@wrongstack/core/hq";
652
656
 
653
657
  // src/hq-static-serve.ts
654
658
  import * as fs2 from "node:fs";
@@ -754,8 +758,99 @@ async function serveHqStatic(_req, res, urlPath, distDir) {
754
758
  return { handled: true };
755
759
  }
756
760
 
757
- // src/hq-server/snapshot.ts
758
- import { WebSocket } from "ws";
761
+ // src/hq-server/project-root.ts
762
+ import { deriveHqProjectId } from "@wrongstack/core/hq";
763
+ function resolveHqProjectRoot(globalRoot, ids) {
764
+ const fn = async () => {
765
+ const { getSessionRegistry } = await import("@wrongstack/core/storage");
766
+ try {
767
+ const registry = getSessionRegistry(globalRoot);
768
+ if (typeof ids.sessionId === "string") {
769
+ const entry = await registry.get(ids.sessionId).catch(() => null);
770
+ if (entry?.projectRoot) return entry.projectRoot;
771
+ }
772
+ if (typeof ids.projectId === "string") {
773
+ const { createHash: createHash2 } = await import("node:crypto");
774
+ const all = await registry.list().catch(() => []);
775
+ const projectIds = /* @__PURE__ */ new Map();
776
+ const projectIdForRoot = (projectRoot) => {
777
+ const cached = projectIds.get(projectRoot);
778
+ if (cached !== void 0) return cached;
779
+ const derived = deriveHqProjectId(projectRoot);
780
+ projectIds.set(projectRoot, derived);
781
+ return derived;
782
+ };
783
+ const match = all.find(
784
+ (e) => e.projectSlug === ids.projectId || projectIdForRoot(e.projectRoot) === ids.projectId || createHash2("sha256").update(e.projectRoot).digest("hex").slice(0, 12) === ids.projectId
785
+ );
786
+ if (match) return match.projectRoot;
787
+ }
788
+ } catch {
789
+ }
790
+ return void 0;
791
+ };
792
+ return fn();
793
+ }
794
+
795
+ // src/hq-server/routes/auth-handlers.ts
796
+ import { randomUUID } from "node:crypto";
797
+ import {
798
+ hashHqPassword,
799
+ isLoopbackHost as isLoopbackHost2,
800
+ mintHqCookieSecret,
801
+ mutateHqAuthFile,
802
+ verifyHqPassword
803
+ } from "@wrongstack/core/hq";
804
+ import {
805
+ buildOtpAuthUri,
806
+ generateRecoveryCodes,
807
+ generateTotpSecret,
808
+ hashRecoveryCode,
809
+ verifyRecoveryCode,
810
+ verifyTotp,
811
+ verifyTotpCounter
812
+ } from "@wrongstack/core/security";
813
+
814
+ // src/hq-server/client-address.ts
815
+ import { isIP as isIP2 } from "node:net";
816
+ function normalizeAddress(raw) {
817
+ const trimmed = raw.trim().replace(/^\[(.*)\]$/, "$1");
818
+ return trimmed.replace(/^::ffff:/i, "");
819
+ }
820
+ function parseForwardedEntry(entry) {
821
+ const value = entry.trim();
822
+ if (!value) return void 0;
823
+ const bracketed = /^\[([^\]]+)\](?::\d+)?$/.exec(value);
824
+ if (bracketed?.[1] !== void 0) {
825
+ const inner = normalizeAddress(bracketed[1]);
826
+ return isIP2(inner) !== 0 ? inner : void 0;
827
+ }
828
+ const normalized = normalizeAddress(value);
829
+ if (isIP2(normalized) !== 0) return normalized;
830
+ const lastColon = normalized.lastIndexOf(":");
831
+ if (lastColon > 0 && normalized.indexOf(":") === lastColon) {
832
+ const host = normalized.slice(0, lastColon);
833
+ if (isIP2(host) === 4) return host;
834
+ }
835
+ return void 0;
836
+ }
837
+ function forwardedChain(req) {
838
+ const header = req.headers["x-forwarded-for"];
839
+ if (header === void 0) return [];
840
+ const raw = Array.isArray(header) ? header.join(",") : header;
841
+ return raw.split(",").map((entry) => parseForwardedEntry(entry)).filter((entry) => entry !== void 0);
842
+ }
843
+ function resolveClientAddress(req, trustedProxyHops = 0) {
844
+ const socketAddress = req.socket.remoteAddress ? normalizeAddress(req.socket.remoteAddress) : void 0;
845
+ const hops = Number.isFinite(trustedProxyHops) ? Math.max(0, Math.trunc(trustedProxyHops)) : 0;
846
+ if (hops === 0) return socketAddress ?? "unknown";
847
+ const chain = [
848
+ ...socketAddress !== void 0 ? [socketAddress] : [],
849
+ ...forwardedChain(req).reverse()
850
+ ];
851
+ if (hops >= chain.length) return socketAddress ?? "unknown";
852
+ return chain[hops] ?? socketAddress ?? "unknown";
853
+ }
759
854
 
760
855
  // src/hq-server/utils.ts
761
856
  import * as fs3 from "node:fs/promises";
@@ -789,10 +884,10 @@ function agentMessageToEntry(p) {
789
884
  }
790
885
  async function readLocalSubagentTranscript(sessionId, subagentId) {
791
886
  try {
792
- const { SessionRegistry } = await import("@wrongstack/core/storage");
887
+ const { getSessionRegistry } = await import("@wrongstack/core/storage");
793
888
  const { resolveWstackPaths, sessionScopedPath } = await import("@wrongstack/core/utils");
794
889
  const globalRoot = path3.dirname(resolveHqDataDir2());
795
- const registry = new SessionRegistry(globalRoot);
890
+ const registry = getSessionRegistry(globalRoot);
796
891
  const entry = await registry.get(sessionId).catch(() => null);
797
892
  if (!entry) return null;
798
893
  const paths = resolveWstackPaths({ projectRoot: entry.projectRoot, globalRoot });
@@ -949,610 +1044,69 @@ function recordTimeseriesSignal(persistence, event) {
949
1044
  }
950
1045
  }
951
1046
 
952
- // src/hq-server/snapshot.ts
953
- var HQ_SNAPSHOT_BROADCAST_DEBOUNCE_MS = 100;
954
- var HQ_SNAPSHOT_PERSIST_MIN_INTERVAL_MS = 3e3;
955
- var HQ_BROWSER_MAX_BUFFERED_BYTES = 32 * 1024 * 1024;
956
- function sendGuarded(ws, data) {
957
- if (ws.readyState !== WebSocket.OPEN) return false;
958
- const buffered = Number.isFinite(ws.bufferedAmount) ? ws.bufferedAmount : 0;
959
- if (buffered + Buffer.byteLength(data, "utf8") > HQ_BROWSER_MAX_BUFFERED_BYTES) {
960
- try {
961
- ws.terminate();
962
- } catch {
963
- try {
964
- ws.close(1013, "client cannot keep up");
965
- } catch {
1047
+ // src/hq-server/routes/auth-handlers.ts
1048
+ import { readHqAuthAuditTail } from "@wrongstack/core/hq";
1049
+ function isLoopbackRequest(req) {
1050
+ const address = req.socket.remoteAddress?.replace(/^::ffff:/, "");
1051
+ return address !== void 0 && isLoopbackHost2(address);
1052
+ }
1053
+ var HQ_AUTH_ADMIN_CAPABILITY = "auth.admin";
1054
+ function callerCanAdministerAuth(auth) {
1055
+ if (auth === void 0) return false;
1056
+ if (auth.kind === "cookie" && auth.tokenId === void 0) return true;
1057
+ if (auth.capabilities === void 0) return true;
1058
+ return auth.capabilities.includes(HQ_AUTH_ADMIN_CAPABILITY);
1059
+ }
1060
+ function writeAuthAdminRequired(res) {
1061
+ res.writeHead(403, { "Content-Type": "application/json" });
1062
+ res.end(
1063
+ JSON.stringify({
1064
+ error: {
1065
+ code: "AUTH_ADMIN_REQUIRED",
1066
+ message: `This endpoint requires the '${HQ_AUTH_ADMIN_CAPABILITY}' capability.`
966
1067
  }
967
- }
968
- return false;
1068
+ })
1069
+ );
1070
+ }
1071
+ function authorizeAuthAdmin(req, res, mutableAuth, sessions) {
1072
+ const auth = authenticateBrowserRequest(
1073
+ req,
1074
+ new URL(req.url ?? "/", "http://localhost"),
1075
+ mutableAuth,
1076
+ sessions
1077
+ );
1078
+ if (!auth) {
1079
+ res.writeHead(401, { "Content-Type": "application/json" });
1080
+ res.end(
1081
+ JSON.stringify({ error: { code: "UNAUTHORIZED", message: "Authentication required." } })
1082
+ );
1083
+ return void 0;
969
1084
  }
970
- try {
971
- ws.send(data);
972
- return true;
973
- } catch {
974
- return false;
1085
+ if (!callerCanAdministerAuth(auth)) {
1086
+ writeAuthAdminRequired(res);
1087
+ return void 0;
975
1088
  }
1089
+ return auth;
976
1090
  }
977
- var HQ_STALE_SNAPSHOT_MS = 5 * 6e4;
978
- function reapStaleClientState(clients, nowMs = Date.now()) {
979
- for (const client of clients.values()) {
980
- for (const [sessionId, tracked] of client.sessions) {
981
- if (nowMs - tracked.receivedAt > HQ_STALE_SNAPSHOT_MS) client.sessions.delete(sessionId);
982
- }
983
- for (const [runId, fleet] of client.fleets) {
984
- if (nowMs - fleet.receivedAt > HQ_STALE_SNAPSHOT_MS) client.fleets.delete(runId);
985
- }
986
- for (const [sessionId, tracked] of client.mcpSnapshots) {
987
- if (nowMs - tracked.receivedAt > HQ_STALE_SNAPSHOT_MS) client.mcpSnapshots.delete(sessionId);
988
- }
989
- if (client.governanceSnapshot !== void 0 && nowMs - client.governanceSnapshot.receivedAt > HQ_STALE_SNAPSHOT_MS) {
990
- delete client.governanceSnapshot;
991
- }
992
- }
993
- }
994
- function buildSnapshot(clients, options) {
995
- reapStaleClientState(clients);
996
- const now = (/* @__PURE__ */ new Date()).toISOString();
997
- const clientRecordById = /* @__PURE__ */ new Map();
998
- const projectMap = /* @__PURE__ */ new Map();
999
- const mailboxSummaries = [];
1000
- const sessionById = /* @__PURE__ */ new Map();
1001
- const fleetByRunId = /* @__PURE__ */ new Map();
1002
- for (const client of clients.values()) {
1003
- const machineId = client.machineId || client.project.machineId || "";
1004
- if (!clientRecordById.has(client.clientId)) {
1005
- clientRecordById.set(client.clientId, {
1006
- clientId: client.clientId,
1007
- kind: client.kind,
1008
- machineId,
1009
- ...client.hostname ? { hostname: client.hostname } : {},
1010
- ...client.pid ? { pid: client.pid } : {},
1011
- ...client.version ? { version: client.version } : {},
1012
- connected: true,
1013
- connectedAt: client.connectedAt,
1014
- lastSeenAt: client.lastSeenAt,
1015
- projectId: client.projectId,
1016
- capabilities: client.capabilities
1017
- });
1018
- }
1019
- let project = projectMap.get(client.projectId);
1020
- if (!project) {
1021
- project = {
1022
- projectId: client.projectId,
1023
- projectName: client.project.projectName || client.projectId,
1024
- projectRootDisplay: client.project.projectRoot,
1025
- machineIds: [machineId],
1026
- ...client.project.gitBranch ? { gitBranch: client.project.gitBranch } : {},
1027
- activeClients: 0,
1028
- activeSessions: 0,
1029
- activeSubagents: 0,
1030
- totalCostUsd: 0,
1031
- lastActivityAt: now,
1032
- status: "active"
1033
- };
1034
- projectMap.set(client.projectId, project);
1035
- } else if (machineId && !project.machineIds.includes(machineId)) {
1036
- project.machineIds = [...project.machineIds, machineId];
1037
- }
1038
- const governance = client.governanceSnapshot?.payload;
1039
- if (governance !== void 0 && (project.governance === void 0 || Date.parse(governance.observedAt) >= Date.parse(project.governance.observedAt))) {
1040
- project.governance = governance;
1041
- }
1042
- for (const tracked of client.sessions.values()) {
1043
- const cutoff = Date.now() - 5 * 6e4;
1044
- const agents = tracked.payload.agents.filter((agent) => {
1045
- if (agent.id === "leader") return true;
1046
- const lastActivityAt = Date.parse(agent.lastActivityAt);
1047
- return Number.isFinite(lastActivityAt) && lastActivityAt >= cutoff;
1048
- });
1049
- sessionById.set(tracked.payload.sessionId, {
1050
- ...tracked.payload,
1051
- clientId: client.clientId,
1052
- agents,
1053
- agentCount: agents.length
1054
- });
1055
- }
1056
- for (const snapshot of client.mailboxes.values()) {
1057
- mailboxSummaries.push({
1058
- mailboxId: snapshot.mailboxId,
1059
- projectId: client.projectId,
1060
- scope: snapshot.scope,
1061
- messageCount: snapshot.totals.messages,
1062
- unreadCount: snapshot.totals.unread,
1063
- incompleteCount: snapshot.totals.incomplete,
1064
- highPriorityCount: snapshot.totals.highPriority,
1065
- onlineAgentCount: snapshot.totals.onlineAgents,
1066
- lastActivityAt: now
1067
- });
1068
- }
1069
- for (const fleet of client.fleets.values()) {
1070
- fleetByRunId.set(fleet.payload.runId, {
1071
- payload: fleet.payload,
1072
- clientId: client.clientId,
1073
- projectId: client.projectId,
1074
- lastActivityAt: new Date(fleet.receivedAt).toISOString()
1075
- });
1076
- }
1077
- }
1078
- const processKeyOf = (rec) => rec.pid !== void 0 ? `${rec.machineId}:${rec.pid}` : rec.clientId;
1079
- const countedProjectProcesses = /* @__PURE__ */ new Set();
1080
- for (const rec of clientRecordById.values()) {
1081
- const key = `${rec.projectId}|${processKeyOf(rec)}`;
1082
- if (countedProjectProcesses.has(key)) continue;
1083
- countedProjectProcesses.add(key);
1084
- const project = projectMap.get(rec.projectId);
1085
- if (project) project.activeClients++;
1086
- }
1087
- const liveSessions = Array.from(sessionById.values());
1088
- const machineMap = /* @__PURE__ */ new Map();
1089
- let totalAgents = 0;
1090
- let totalSubagents = 0;
1091
- let totalCostUsd = 0;
1092
- for (const session of liveSessions) {
1093
- let project = projectMap.get(session.projectId);
1094
- if (!project) {
1095
- project = {
1096
- projectId: session.projectId,
1097
- projectName: session.projectName || session.projectId,
1098
- projectRootDisplay: session.projectRoot,
1099
- machineIds: [session.machineId],
1100
- ...session.gitBranch ? { gitBranch: session.gitBranch } : {},
1101
- activeClients: 0,
1102
- activeSessions: 0,
1103
- activeSubagents: 0,
1104
- totalCostUsd: 0,
1105
- lastActivityAt: session.lastActivityAt,
1106
- status: "active"
1107
- };
1108
- projectMap.set(session.projectId, project);
1109
- } else if (session.machineId && !project.machineIds.includes(session.machineId)) {
1110
- project.machineIds = [...project.machineIds, session.machineId];
1111
- }
1112
- project.activeSessions++;
1113
- let sessionCost = 0;
1114
- for (const agent of session.agents) {
1115
- totalAgents++;
1116
- if (agent.id !== "leader") totalSubagents++;
1117
- if (typeof agent.costUsd === "number") {
1118
- sessionCost += agent.costUsd;
1119
- }
1120
- }
1121
- project.activeSubagents += session.agents.filter((a) => a.id !== "leader").length;
1122
- project.totalCostUsd += sessionCost;
1123
- totalCostUsd += sessionCost;
1124
- const mKey = hqMachineKey(session.hostname, session.machineId);
1125
- let machine = machineMap.get(mKey);
1126
- if (!machine) {
1127
- machine = {
1128
- record: {
1129
- machineId: session.machineId,
1130
- ...session.hostname ? { hostname: session.hostname } : {},
1131
- clientCount: 0,
1132
- sessionCount: 0,
1133
- agentCount: 0,
1134
- projectIds: [],
1135
- lastActivityAt: session.lastActivityAt
1136
- },
1137
- projects: /* @__PURE__ */ new Set()
1138
- };
1139
- machineMap.set(mKey, machine);
1140
- }
1141
- machine.record.sessionCount++;
1142
- machine.record.agentCount += session.agents.length;
1143
- machine.projects.add(session.projectId);
1144
- if (session.lastActivityAt > machine.record.lastActivityAt) {
1145
- machine.record.lastActivityAt = session.lastActivityAt;
1146
- }
1147
- }
1148
- const countedMachineProcesses = /* @__PURE__ */ new Set();
1149
- for (const rec of clientRecordById.values()) {
1150
- if (!rec.machineId && !rec.hostname) continue;
1151
- const rKey = hqMachineKey(rec.hostname, rec.machineId);
1152
- let machine = machineMap.get(rKey);
1153
- if (!machine) {
1154
- machine = {
1155
- record: {
1156
- machineId: rec.machineId,
1157
- ...rec.hostname ? { hostname: rec.hostname } : {},
1158
- clientCount: 0,
1159
- sessionCount: 0,
1160
- agentCount: 0,
1161
- projectIds: [],
1162
- lastActivityAt: rec.lastSeenAt
1163
- },
1164
- projects: /* @__PURE__ */ new Set()
1165
- };
1166
- machineMap.set(rKey, machine);
1167
- }
1168
- const processKey = `${rKey}|${processKeyOf(rec)}`;
1169
- if (!countedMachineProcesses.has(processKey)) {
1170
- countedMachineProcesses.add(processKey);
1171
- machine.record.clientCount++;
1172
- }
1173
- machine.projects.add(rec.projectId);
1174
- if (rec.hostname && !machine.record.hostname) machine.record.hostname = rec.hostname;
1175
- }
1176
- const machines = Array.from(machineMap.values()).map((m) => ({
1177
- ...m.record,
1178
- projectIds: Array.from(m.projects)
1179
- }));
1180
- const clientRecords = Array.from(clientRecordById.values());
1181
- const projects = Array.from(projectMap.values());
1182
- let unread = 0;
1183
- let incomplete = 0;
1184
- for (const m of mailboxSummaries) {
1185
- unread += m.unreadCount;
1186
- incomplete += m.incompleteCount;
1187
- }
1188
- const sessions = liveSessions.map((s) => {
1189
- let sessionCost = 0;
1190
- for (const agent of s.agents) {
1191
- if (typeof agent.costUsd === "number") sessionCost += agent.costUsd;
1192
- }
1193
- const provider = s.agents.find((a) => a.model !== void 0)?.model;
1194
- return {
1195
- sessionId: s.sessionId,
1196
- projectId: s.projectId,
1197
- clientId: s.clientId ?? `${s.machineId}:${s.clientKind}`,
1198
- status: s.status === "active" ? "running" : "idle",
1199
- ...provider !== void 0 ? { model: provider } : {},
1200
- startedAt: s.startedAt,
1201
- lastActivityAt: s.lastActivityAt,
1202
- ...sessionCost > 0 ? { costUsd: sessionCost } : {}
1203
- };
1204
- });
1205
- const fleets = Array.from(fleetByRunId.values()).map((f) => ({
1206
- runId: f.payload.runId,
1207
- projectId: f.projectId,
1208
- clientId: f.clientId,
1209
- activeSubagents: f.payload.activeSubagents,
1210
- queuedTasks: f.payload.queuedTasks,
1211
- completedTasks: f.payload.completedTasks,
1212
- failedTasks: f.payload.failedTasks,
1213
- ...f.payload.totalCostUsd !== void 0 ? { totalCostUsd: f.payload.totalCostUsd } : {},
1214
- lastActivityAt: f.lastActivityAt,
1215
- ...f.payload.maxConcurrent !== void 0 ? { maxConcurrent: f.payload.maxConcurrent } : {},
1216
- ...f.payload.maxSpawns !== void 0 ? { maxSpawns: f.payload.maxSpawns } : {},
1217
- ...f.payload.usedSpawns !== void 0 ? { usedSpawns: f.payload.usedSpawns } : {},
1218
- ...f.payload.remainingSpawns !== void 0 ? { remainingSpawns: f.payload.remainingSpawns } : {},
1219
- ...f.payload.effectiveSource !== void 0 ? { effectiveSource: f.payload.effectiveSource } : {},
1220
- ...f.payload.checkpointMaxSpawns !== void 0 ? { checkpointMaxSpawns: f.payload.checkpointMaxSpawns } : {},
1221
- ...f.payload.ceilingMismatch ? { ceilingMismatch: true } : {}
1222
- }));
1223
- const mcpServers = [];
1224
- const seenMcp = /* @__PURE__ */ new Set();
1225
- for (const client of clients.values()) {
1226
- for (const [sessionId, tracked] of client.mcpSnapshots.entries()) {
1227
- for (const server of tracked.servers) {
1228
- const key = `${client.clientId}:${sessionId}:${server.name}`;
1229
- if (seenMcp.has(key)) continue;
1230
- seenMcp.add(key);
1231
- mcpServers.push(server);
1232
- }
1233
- }
1234
- }
1235
- return {
1236
- generatedAt: now,
1237
- clients: clientRecords,
1238
- projects,
1239
- sessions,
1240
- fleets,
1241
- mailboxes: mailboxSummaries,
1242
- machines,
1243
- liveSessions,
1244
- mcpServers,
1245
- totals: {
1246
- activeProjects: projects.length,
1247
- activeClients: new Set(clientRecords.map(processKeyOf)).size,
1248
- activeSessions: liveSessions.length,
1249
- activeSubagents: totalSubagents,
1250
- unreadMailboxMessages: unread,
1251
- incompleteMailboxMessages: incomplete,
1252
- totalCostUsd,
1253
- activeMachines: machines.length,
1254
- activeAgents: totalAgents,
1255
- ...options?.tokenStats !== void 0 ? { tokenStats: options.tokenStats } : {}
1256
- }
1257
- };
1258
- }
1259
- function createSnapshotBroadcaster(clients, browsers, persistence) {
1260
- let cached = "";
1261
- let dirty = true;
1262
- let timer = null;
1263
- let closed = false;
1264
- let lastPersistAt = 0;
1265
- const persistCheckpoint = (snapshot) => {
1266
- if (persistence === void 0) return;
1267
- const now = Date.now();
1268
- if (now - lastPersistAt < HQ_SNAPSHOT_PERSIST_MIN_INTERVAL_MS) return;
1269
- lastPersistAt = now;
1270
- persistence.snapshotStore.save(snapshot);
1271
- };
1272
- const serialize = () => {
1273
- if (!dirty && cached.length > 0) return cached;
1274
- const snapshot = buildSnapshot(clients);
1275
- const msg = { type: "hq.snapshot", snapshot };
1276
- cached = JSON.stringify(msg);
1277
- dirty = false;
1278
- persistCheckpoint(snapshot);
1279
- return cached;
1280
- };
1281
- const flush = () => {
1282
- timer = null;
1283
- if (browsers.size === 0) {
1284
- if (persistence !== void 0 && Date.now() - lastPersistAt >= HQ_SNAPSHOT_PERSIST_MIN_INTERVAL_MS) {
1285
- persistCheckpoint(buildSnapshot(clients));
1286
- }
1287
- return;
1288
- }
1289
- const data = serialize();
1290
- for (const ws of browsers) sendGuarded(ws, data);
1291
- };
1292
- return {
1293
- currentSerialized: serialize,
1294
- broadcast: () => {
1295
- if (closed) return;
1296
- dirty = true;
1297
- if (timer !== null) return;
1298
- timer = setTimeout(flush, HQ_SNAPSHOT_BROADCAST_DEBOUNCE_MS);
1299
- timer.unref?.();
1300
- },
1301
- close: () => {
1302
- closed = true;
1303
- if (timer !== null) {
1304
- clearTimeout(timer);
1305
- timer = null;
1306
- }
1307
- }
1308
- };
1309
- }
1310
- function buildProjectDetail(clients, projectId) {
1311
- const projectClients = [];
1312
- for (const c of clients.values()) {
1313
- if (c.projectId === projectId) projectClients.push(c);
1314
- }
1315
- if (projectClients.length === 0) return null;
1316
- const now = (/* @__PURE__ */ new Date()).toISOString();
1317
- const clientRecords = projectClients.map((c) => ({
1318
- clientId: c.clientId,
1319
- kind: c.kind,
1320
- machineId: "",
1321
- ...c.hostname ? { hostname: c.hostname } : {},
1322
- ...c.pid ? { pid: c.pid } : {},
1323
- ...c.version ? { version: c.version } : {},
1324
- connected: true,
1325
- connectedAt: c.connectedAt,
1326
- lastSeenAt: c.lastSeenAt,
1327
- projectId: c.projectId,
1328
- capabilities: c.capabilities
1329
- }));
1330
- const mailboxPayloads = [];
1331
- let latestActivity = now;
1332
- for (const c of projectClients) {
1333
- for (const snap of c.mailboxes.values()) {
1334
- mailboxPayloads.push(snap);
1335
- if (snap.totals.messages > 0) latestActivity = now;
1336
- }
1337
- }
1338
- const primaryProject = projectClients[0].project;
1339
- const machineIds = Array.from(new Set(projectClients.map((client) => client.project.machineId)));
1340
- const project = {
1341
- projectId,
1342
- projectName: primaryProject.projectName || projectId,
1343
- projectRootDisplay: primaryProject.projectRoot,
1344
- machineIds,
1345
- ...primaryProject.gitBranch ? { gitBranch: primaryProject.gitBranch } : {},
1346
- activeClients: projectClients.length,
1347
- activeSessions: 0,
1348
- activeSubagents: 0,
1349
- totalCostUsd: 0,
1350
- lastActivityAt: latestActivity,
1351
- status: "active"
1352
- };
1353
- return {
1354
- generatedAt: now,
1355
- project,
1356
- clients: clientRecords,
1357
- mailboxes: mailboxPayloads
1358
- };
1359
- }
1360
- function broadcastEvent(event, browsers) {
1361
- const msg = { type: "hq.event", event };
1362
- const data = JSON.stringify(msg);
1363
- for (const ws of browsers) sendGuarded(ws, data);
1364
- }
1365
- function broadcastCommandStatus(command, browsers) {
1366
- const data = JSON.stringify({ type: "hq.command_status", command });
1367
- for (const ws of browsers) sendGuarded(ws, data);
1368
- }
1369
-
1370
- // src/hq-server/trust-boundary.ts
1371
- import {
1372
- isTrustDecisionAllowed
1373
- } from "@wrongstack/core/security";
1374
- async function authorizeHqCommand(input) {
1375
- const { boundary, command, commandId, enqueuedBy, authMethod, target } = input;
1376
- const subject = command.type === "run-command" ? {
1377
- kind: "command",
1378
- id: command.command,
1379
- attributes: { targetClientId: target.clientId }
1380
- } : {
1381
- kind: "custom",
1382
- id: command.type,
1383
- attributes: { targetClientId: target.clientId }
1384
- };
1385
- const request = {
1386
- version: 1,
1387
- requestId: commandId,
1388
- // WS-050: this was hardcoded to `kind: 'user'`, which made the
1389
- // compatibility policy's ONLY deny rule — `remote-client` + `critical` —
1390
- // unreachable from HQ. A boundary whose single rule can never fire is not
1391
- // defence in depth, it is decoration.
1392
- //
1393
- // The provenance is derived from how the caller authenticated, because
1394
- // that is the honest signal available here:
1395
- //
1396
- // session cookie → `user`. The dashboard's primary flow is a bootstrap
1397
- // code exchanged for an HttpOnly cookie, so this is a human at the
1398
- // browser. NOTE: bootstrap sessions DO carry a `tokenId`, so a
1399
- // "cookie without tokenId" test would have misclassified every normal
1400
- // dashboard user as remote and denied them run-command. Checked before
1401
- // writing this, not assumed.
1402
- // raw bearer token → `remote-client`. The legacy `?token=` / manual-entry
1403
- // path and any script calling the HTTP API directly. A credential that
1404
- // can be copied into a script is not a person at a console.
1405
- //
1406
- // Consequence, deliberate: a bearer-token caller can no longer trigger
1407
- // `run-command` (the only `critical` capability here). `enqueue` is `high`
1408
- // and unaffected. The migration for automation that needs execute is the
1409
- // bootstrap exchange, which is the same thing the dashboard does.
1410
- actor: { kind: authMethod === "session" ? "user" : "remote-client", id: enqueuedBy },
1411
- surface: "hq",
1412
- capability: command.type === "run-command" ? "hq.control.execute" : "hq.control.enqueue",
1413
- subject,
1414
- risk: command.type === "run-command" ? "critical" : "high",
1415
- scope: { projectId: target.projectId },
1416
- authContext: { method: authMethod, principalId: enqueuedBy },
1417
- metadata: { commandType: command.type, targetKind: target.kind }
1418
- };
1419
- const decision = await boundary.evaluate(request);
1420
- console.warn(
1421
- JSON.stringify({
1422
- level: "info",
1423
- event: "hq.trust_boundary.decision",
1424
- requestId: commandId,
1425
- capability: request.capability,
1426
- decision: decision.kind,
1427
- policyId: decision.policyId,
1428
- actor: enqueuedBy,
1429
- clientId: target.clientId,
1430
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1431
- })
1432
- );
1433
- return { allowed: isTrustDecisionAllowed(decision), reason: decision.reason };
1434
- }
1435
-
1436
- // src/hq-server/routes/auth-handlers.ts
1437
- import { randomUUID } from "node:crypto";
1438
- import {
1439
- hashHqPassword,
1440
- isLoopbackHost as isLoopbackHost2,
1441
- mintHqCookieSecret,
1442
- mutateHqAuthFile,
1443
- verifyHqPassword
1444
- } from "@wrongstack/core/hq";
1445
- import {
1446
- buildOtpAuthUri,
1447
- generateRecoveryCodes,
1448
- generateTotpSecret,
1449
- hashRecoveryCode,
1450
- verifyRecoveryCode,
1451
- verifyTotp,
1452
- verifyTotpCounter
1453
- } from "@wrongstack/core/security";
1454
-
1455
- // src/hq-server/client-address.ts
1456
- import { isIP as isIP2 } from "node:net";
1457
- function normalizeAddress(raw) {
1458
- const trimmed = raw.trim().replace(/^\[(.*)\]$/, "$1");
1459
- return trimmed.replace(/^::ffff:/i, "");
1460
- }
1461
- function parseForwardedEntry(entry) {
1462
- const value = entry.trim();
1463
- if (!value) return void 0;
1464
- const bracketed = /^\[([^\]]+)\](?::\d+)?$/.exec(value);
1465
- if (bracketed?.[1] !== void 0) {
1466
- const inner = normalizeAddress(bracketed[1]);
1467
- return isIP2(inner) !== 0 ? inner : void 0;
1468
- }
1469
- const normalized = normalizeAddress(value);
1470
- if (isIP2(normalized) !== 0) return normalized;
1471
- const lastColon = normalized.lastIndexOf(":");
1472
- if (lastColon > 0 && normalized.indexOf(":") === lastColon) {
1473
- const host = normalized.slice(0, lastColon);
1474
- if (isIP2(host) === 4) return host;
1475
- }
1476
- return void 0;
1477
- }
1478
- function forwardedChain(req) {
1479
- const header = req.headers["x-forwarded-for"];
1480
- if (header === void 0) return [];
1481
- const raw = Array.isArray(header) ? header.join(",") : header;
1482
- return raw.split(",").map((entry) => parseForwardedEntry(entry)).filter((entry) => entry !== void 0);
1483
- }
1484
- function resolveClientAddress(req, trustedProxyHops = 0) {
1485
- const socketAddress = req.socket.remoteAddress ? normalizeAddress(req.socket.remoteAddress) : void 0;
1486
- const hops = Number.isFinite(trustedProxyHops) ? Math.max(0, Math.trunc(trustedProxyHops)) : 0;
1487
- if (hops === 0) return socketAddress ?? "unknown";
1488
- const chain = [...socketAddress !== void 0 ? [socketAddress] : [], ...forwardedChain(req).reverse()];
1489
- if (hops >= chain.length) return socketAddress ?? "unknown";
1490
- return chain[hops] ?? socketAddress ?? "unknown";
1491
- }
1492
-
1493
- // src/hq-server/routes/auth-handlers.ts
1494
- import { readHqAuthAuditTail } from "@wrongstack/core/hq";
1495
- function isLoopbackRequest(req) {
1496
- const address = req.socket.remoteAddress?.replace(/^::ffff:/, "");
1497
- return address !== void 0 && isLoopbackHost2(address);
1498
- }
1499
- var HQ_AUTH_ADMIN_CAPABILITY = "auth.admin";
1500
- function callerCanAdministerAuth(auth) {
1501
- if (auth === void 0) return false;
1502
- if (auth.kind === "cookie" && auth.tokenId === void 0) return true;
1503
- if (auth.capabilities === void 0) return true;
1504
- return auth.capabilities.includes(HQ_AUTH_ADMIN_CAPABILITY);
1505
- }
1506
- function writeAuthAdminRequired(res) {
1507
- res.writeHead(403, { "Content-Type": "application/json" });
1508
- res.end(
1509
- JSON.stringify({
1510
- error: {
1511
- code: "AUTH_ADMIN_REQUIRED",
1512
- message: `This endpoint requires the '${HQ_AUTH_ADMIN_CAPABILITY}' capability.`
1513
- }
1514
- })
1515
- );
1516
- }
1517
- function authorizeAuthAdmin(req, res, mutableAuth, sessions) {
1518
- const auth = authenticateBrowserRequest(
1519
- req,
1520
- new URL(req.url ?? "/", "http://localhost"),
1521
- mutableAuth,
1522
- sessions
1523
- );
1524
- if (!auth) {
1525
- res.writeHead(401, { "Content-Type": "application/json" });
1526
- res.end(
1527
- JSON.stringify({ error: { code: "UNAUTHORIZED", message: "Authentication required." } })
1528
- );
1529
- return void 0;
1530
- }
1531
- if (!callerCanAdministerAuth(auth)) {
1532
- writeAuthAdminRequired(res);
1533
- return void 0;
1534
- }
1535
- return auth;
1536
- }
1537
- async function handleApiAuthStatus(req, res, url, mutableAuth, sessions, requireBrowserAuth, trustedPublicOrigins, secureCookies) {
1538
- const auth = authenticateBrowserRequest(req, url, mutableAuth, sessions);
1539
- const openMode = mutableAuth.browserTokens.size === 0 && mutableAuth.passwordHash === void 0 && mutableAuth.requireAuthFloor !== true;
1540
- const localOpenMode = openMode && !requireBrowserAuth && isLoopbackRequest(req);
1541
- const publicOrigin = trustedPublicOrigins.values().next().value;
1542
- res.writeHead(200, { "Content-Type": "application/json" });
1543
- res.end(
1544
- JSON.stringify({
1545
- tokenMode: mutableAuth.browserTokens.size > 0,
1546
- passwordMode: mutableAuth.passwordHash !== void 0,
1547
- totpEnabled: mutableAuth.totpSecret !== void 0,
1548
- recoveryCodesRemaining: mutableAuth.totpRecoveryCodes?.length ?? 0,
1549
- publicRelay: requireBrowserAuth === true || publicOrigin !== void 0,
1550
- ...publicOrigin !== void 0 ? { publicOrigin } : {},
1551
- secureCookies: secureCookies === true,
1552
- loggedIn: auth !== void 0 || localOpenMode,
1553
- authKind: isCookieAuth(auth) ? "password" : isTokenAuth(auth) ? "token" : localOpenMode ? "open" : void 0
1554
- })
1555
- );
1091
+ async function handleApiAuthStatus(req, res, url, mutableAuth, sessions, requireBrowserAuth, trustedPublicOrigins, secureCookies) {
1092
+ const auth = authenticateBrowserRequest(req, url, mutableAuth, sessions);
1093
+ const openMode = mutableAuth.browserTokens.size === 0 && mutableAuth.passwordHash === void 0 && mutableAuth.requireAuthFloor !== true;
1094
+ const localOpenMode = openMode && !requireBrowserAuth && isLoopbackRequest(req);
1095
+ const publicOrigin = trustedPublicOrigins.values().next().value;
1096
+ res.writeHead(200, { "Content-Type": "application/json" });
1097
+ res.end(
1098
+ JSON.stringify({
1099
+ tokenMode: mutableAuth.browserTokens.size > 0,
1100
+ passwordMode: mutableAuth.passwordHash !== void 0,
1101
+ totpEnabled: mutableAuth.totpSecret !== void 0,
1102
+ recoveryCodesRemaining: mutableAuth.totpRecoveryCodes?.length ?? 0,
1103
+ publicRelay: requireBrowserAuth === true || publicOrigin !== void 0,
1104
+ ...publicOrigin !== void 0 ? { publicOrigin } : {},
1105
+ secureCookies: secureCookies === true,
1106
+ loggedIn: auth !== void 0 || localOpenMode,
1107
+ authKind: isCookieAuth(auth) ? "password" : isTokenAuth(auth) ? "token" : localOpenMode ? "open" : void 0
1108
+ })
1109
+ );
1556
1110
  }
1557
1111
  async function handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, secureCookies, trustedProxyHops) {
1558
1112
  if (!mutableAuth.passwordHash) {
@@ -1704,7 +1258,10 @@ async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secur
1704
1258
  res.writeHead(403, { "Content-Type": "application/json" });
1705
1259
  res.end(
1706
1260
  JSON.stringify({
1707
- error: { code: "INVALID_CURRENT_PASSWORD", message: "Current password is required to change or remove it." }
1261
+ error: {
1262
+ code: "INVALID_CURRENT_PASSWORD",
1263
+ message: "Current password is required to change or remove it."
1264
+ }
1708
1265
  })
1709
1266
  );
1710
1267
  return;
@@ -1926,26 +1483,40 @@ async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, login
1926
1483
  const { blocked, retryAfter } = loginAttempts.checkBlocked(clientIp);
1927
1484
  if (blocked) {
1928
1485
  res.writeHead(429, { "Content-Type": "application/json", "Retry-After": String(retryAfter) });
1929
- res.end(JSON.stringify({ error: { code: "RATE_LIMITED", message: "Too many attempts. Try again later." } }));
1486
+ res.end(
1487
+ JSON.stringify({
1488
+ error: { code: "RATE_LIMITED", message: "Too many attempts. Try again later." }
1489
+ })
1490
+ );
1930
1491
  return;
1931
1492
  }
1932
1493
  const raw = readHqSessionCookie(req.headers.cookie);
1933
1494
  const sessionId = raw ? parseHqSessionCookie(raw, mutableAuth.cookieSecret) : void 0;
1934
1495
  if (!sessionId) {
1935
1496
  res.writeHead(401, { "Content-Type": "application/json" });
1936
- res.end(JSON.stringify({ error: { code: "NO_PENDING_SESSION", message: "No pending 2FA session." } }));
1497
+ res.end(
1498
+ JSON.stringify({ error: { code: "NO_PENDING_SESSION", message: "No pending 2FA session." } })
1499
+ );
1937
1500
  return;
1938
1501
  }
1939
1502
  const session = sessions.get(sessionId);
1940
1503
  if (!session?.pending2fa) {
1941
1504
  res.writeHead(401, { "Content-Type": "application/json" });
1942
- res.end(JSON.stringify({ error: { code: "NO_PENDING_SESSION", message: "Session is not pending 2FA." } }));
1505
+ res.end(
1506
+ JSON.stringify({
1507
+ error: { code: "NO_PENDING_SESSION", message: "Session is not pending 2FA." }
1508
+ })
1509
+ );
1943
1510
  return;
1944
1511
  }
1945
1512
  if (Date.now() - session.createdAt > PENDING_2FA_TTL_MS) {
1946
1513
  sessions.delete(sessionId);
1947
1514
  res.writeHead(401, { "Content-Type": "application/json" });
1948
- res.end(JSON.stringify({ error: { code: "TOTP_EXPIRED", message: "2FA session expired. Please log in again." } }));
1515
+ res.end(
1516
+ JSON.stringify({
1517
+ error: { code: "TOTP_EXPIRED", message: "2FA session expired. Please log in again." }
1518
+ })
1519
+ );
1949
1520
  return;
1950
1521
  }
1951
1522
  let body;
@@ -1959,7 +1530,9 @@ async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, login
1959
1530
  if (typeof body.code === "string" && body.code.length > 0) {
1960
1531
  if (!mutableAuth.totpSecret) {
1961
1532
  res.writeHead(400, { "Content-Type": "application/json" });
1962
- res.end(JSON.stringify({ error: { code: "TOTP_NOT_ENABLED", message: "2FA is not configured." } }));
1533
+ res.end(
1534
+ JSON.stringify({ error: { code: "TOTP_NOT_ENABLED", message: "2FA is not configured." } })
1535
+ );
1963
1536
  return;
1964
1537
  }
1965
1538
  const matchedCounter = verifyTotpCounter(body.code, mutableAuth.totpSecret);
@@ -1983,7 +1556,11 @@ async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, login
1983
1556
  loginAttempts.clearOnSuccess(clientIp);
1984
1557
  sessions.delete(sessionId);
1985
1558
  const fullSessionId = randomUUID();
1986
- sessions.set(fullSessionId, { createdAt: Date.now(), kind: "password", lastSeenAt: Date.now() });
1559
+ sessions.set(fullSessionId, {
1560
+ createdAt: Date.now(),
1561
+ kind: "password",
1562
+ lastSeenAt: Date.now()
1563
+ });
1987
1564
  setHqSessionCookie(
1988
1565
  res,
1989
1566
  serializeHqSessionCookie(fullSessionId, mutableAuth.cookieSecret),
@@ -1998,7 +1575,11 @@ async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, login
1998
1575
  const storedHashes = mutableAuth.totpRecoveryCodes ?? [];
1999
1576
  if (storedHashes.length === 0) {
2000
1577
  res.writeHead(400, { "Content-Type": "application/json" });
2001
- res.end(JSON.stringify({ error: { code: "NO_RECOVERY_CODES", message: "No recovery codes available." } }));
1578
+ res.end(
1579
+ JSON.stringify({
1580
+ error: { code: "NO_RECOVERY_CODES", message: "No recovery codes available." }
1581
+ })
1582
+ );
2002
1583
  return;
2003
1584
  }
2004
1585
  if (!verifyRecoveryCode(body.recoveryCode, storedHashes)) {
@@ -2006,7 +1587,11 @@ async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, login
2006
1587
  sessions.delete(sessionId);
2007
1588
  }
2008
1589
  res.writeHead(401, { "Content-Type": "application/json" });
2009
- res.end(JSON.stringify({ error: { code: "INVALID_RECOVERY_CODE", message: "Invalid recovery code." } }));
1590
+ res.end(
1591
+ JSON.stringify({
1592
+ error: { code: "INVALID_RECOVERY_CODE", message: "Invalid recovery code." }
1593
+ })
1594
+ );
2010
1595
  return;
2011
1596
  }
2012
1597
  let consumed = false;
@@ -2033,7 +1618,14 @@ async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, login
2033
1618
  });
2034
1619
  if (!consumed) {
2035
1620
  res.writeHead(409, { "Content-Type": "application/json" });
2036
- res.end(JSON.stringify({ error: { code: "RECOVERY_CODE_ALREADY_USED", message: "This recovery code has already been used." } }));
1621
+ res.end(
1622
+ JSON.stringify({
1623
+ error: {
1624
+ code: "RECOVERY_CODE_ALREADY_USED",
1625
+ message: "This recovery code has already been used."
1626
+ }
1627
+ })
1628
+ );
2037
1629
  return;
2038
1630
  }
2039
1631
  applyAuthFile(next);
@@ -2043,7 +1635,11 @@ async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, login
2043
1635
  loginAttempts.clearOnSuccess(clientIp);
2044
1636
  sessions.delete(sessionId);
2045
1637
  const fullSessionId = randomUUID();
2046
- sessions.set(fullSessionId, { createdAt: Date.now(), kind: "password", lastSeenAt: Date.now() });
1638
+ sessions.set(fullSessionId, {
1639
+ createdAt: Date.now(),
1640
+ kind: "password",
1641
+ lastSeenAt: Date.now()
1642
+ });
2047
1643
  setHqSessionCookie(
2048
1644
  res,
2049
1645
  serializeHqSessionCookie(fullSessionId, mutableAuth.cookieSecret),
@@ -2055,7 +1651,9 @@ async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, login
2055
1651
  }
2056
1652
  res.writeHead(400, { "Content-Type": "application/json" });
2057
1653
  res.end(
2058
- JSON.stringify({ error: { code: "BAD_REQUEST", message: "Provide a `code` or `recoveryCode`." } })
1654
+ JSON.stringify({
1655
+ error: { code: "BAD_REQUEST", message: "Provide a `code` or `recoveryCode`." }
1656
+ })
2059
1657
  );
2060
1658
  }
2061
1659
  async function handleApiTotpSetup(req, res, mutableAuth, sessions, dataDir, applyAuthFile) {
@@ -2063,7 +1661,12 @@ async function handleApiTotpSetup(req, res, mutableAuth, sessions, dataDir, appl
2063
1661
  if (mutableAuth.totpSecret) {
2064
1662
  res.writeHead(409, { "Content-Type": "application/json" });
2065
1663
  res.end(
2066
- JSON.stringify({ error: { code: "TOTP_ALREADY_ENABLED", message: "2FA is already active. Disable it first to re-enroll." } })
1664
+ JSON.stringify({
1665
+ error: {
1666
+ code: "TOTP_ALREADY_ENABLED",
1667
+ message: "2FA is already active. Disable it first to re-enroll."
1668
+ }
1669
+ })
2067
1670
  );
2068
1671
  return;
2069
1672
  }
@@ -2082,7 +1685,9 @@ async function handleApiTotpEnable(req, res, mutableAuth, sessions, dataDir, app
2082
1685
  if (mutableAuth.totpSecret) {
2083
1686
  res.writeHead(409, { "Content-Type": "application/json" });
2084
1687
  res.end(
2085
- JSON.stringify({ error: { code: "TOTP_ALREADY_ENABLED", message: "2FA is already active." } })
1688
+ JSON.stringify({
1689
+ error: { code: "TOTP_ALREADY_ENABLED", message: "2FA is already active." }
1690
+ })
2086
1691
  );
2087
1692
  return;
2088
1693
  }
@@ -2090,7 +1695,9 @@ async function handleApiTotpEnable(req, res, mutableAuth, sessions, dataDir, app
2090
1695
  if (!pendingSecret) {
2091
1696
  res.writeHead(400, { "Content-Type": "application/json" });
2092
1697
  res.end(
2093
- JSON.stringify({ error: { code: "TOTP_NOT_SETUP", message: "Call /api/auth/totp/setup first." } })
1698
+ JSON.stringify({
1699
+ error: { code: "TOTP_NOT_SETUP", message: "Call /api/auth/totp/setup first." }
1700
+ })
2094
1701
  );
2095
1702
  return;
2096
1703
  }
@@ -2109,7 +1716,9 @@ async function handleApiTotpEnable(req, res, mutableAuth, sessions, dataDir, app
2109
1716
  }
2110
1717
  if (!verifyTotp(body.code, pendingSecret)) {
2111
1718
  res.writeHead(401, { "Content-Type": "application/json" });
2112
- res.end(JSON.stringify({ error: { code: "INVALID_TOTP", message: "Invalid authenticator code." } }));
1719
+ res.end(
1720
+ JSON.stringify({ error: { code: "INVALID_TOTP", message: "Invalid authenticator code." } })
1721
+ );
2113
1722
  return;
2114
1723
  }
2115
1724
  const recoveryCodes = generateRecoveryCodes();
@@ -2132,7 +1741,11 @@ async function handleApiTotpDisable(req, res, mutableAuth, sessions, loginAttemp
2132
1741
  const { blocked, retryAfter } = loginAttempts.checkBlocked(clientIp);
2133
1742
  if (blocked) {
2134
1743
  res.writeHead(429, { "Content-Type": "application/json", "Retry-After": String(retryAfter) });
2135
- res.end(JSON.stringify({ error: { code: "RATE_LIMITED", message: "Too many attempts. Try again later." } }));
1744
+ res.end(
1745
+ JSON.stringify({
1746
+ error: { code: "RATE_LIMITED", message: "Too many attempts. Try again later." }
1747
+ })
1748
+ );
2136
1749
  return;
2137
1750
  }
2138
1751
  let body;
@@ -2154,7 +1767,12 @@ async function handleApiTotpDisable(req, res, mutableAuth, sessions, loginAttemp
2154
1767
  recordVerifyFailure(loginAttempts, clientIp);
2155
1768
  res.writeHead(403, { "Content-Type": "application/json" });
2156
1769
  res.end(
2157
- JSON.stringify({ error: { code: "CONFIRMATION_REQUIRED", message: "Provide the current password or a TOTP code." } })
1770
+ JSON.stringify({
1771
+ error: {
1772
+ code: "CONFIRMATION_REQUIRED",
1773
+ message: "Provide the current password or a TOTP code."
1774
+ }
1775
+ })
2158
1776
  );
2159
1777
  return;
2160
1778
  }
@@ -2224,11 +1842,163 @@ function handleApiAuthAudit(_req, res, dataDir) {
2224
1842
  res.end(JSON.stringify({ entries }));
2225
1843
  }
2226
1844
 
1845
+ // src/hq-server/routes/session-handlers.ts
1846
+ import * as path4 from "node:path";
1847
+ import {
1848
+ buildTranscriptFromEvents,
1849
+ resolveHqDataDir as resolveHqDataDir3
1850
+ } from "@wrongstack/core/hq";
1851
+ async function handleApiSessions(res) {
1852
+ const { getSessionRegistry } = await import("@wrongstack/core/storage");
1853
+ const globalRoot = path4.dirname(resolveHqDataDir3());
1854
+ try {
1855
+ const registry = getSessionRegistry(globalRoot);
1856
+ const sessions = await registry.list();
1857
+ const result = sessions.filter((s) => s.status !== "stale").map((s) => ({
1858
+ sessionId: s.sessionId,
1859
+ projectSlug: s.projectSlug,
1860
+ projectName: s.projectName,
1861
+ projectRoot: s.projectRoot,
1862
+ workingDir: s.workingDir,
1863
+ status: s.status,
1864
+ pid: s.pid,
1865
+ startedAt: s.startedAt,
1866
+ lastHeartbeatAt: s.lastHeartbeatAt,
1867
+ agentCount: s.agentCount,
1868
+ agents: s.agents.map((a) => ({
1869
+ id: a.id,
1870
+ name: a.name,
1871
+ status: a.status,
1872
+ currentTool: a.currentTool,
1873
+ iterations: a.iterations,
1874
+ toolCalls: a.toolCalls,
1875
+ lastActivityAt: a.lastActivityAt
1876
+ }))
1877
+ }));
1878
+ res.writeHead(200, { "Content-Type": "application/json" });
1879
+ res.end(JSON.stringify(result));
1880
+ } catch (err) {
1881
+ res.writeHead(500, { "Content-Type": "application/json" });
1882
+ res.end(JSON.stringify({ error: sanitizeApiError(err) }));
1883
+ }
1884
+ }
1885
+ async function handleApiSessionEvents(req, res, match, transcripts) {
1886
+ const { getSessionRegistry, DefaultSessionStore } = await import("@wrongstack/core/storage");
1887
+ const { resolveWstackPaths } = await import("@wrongstack/core/utils");
1888
+ const url = new URL(req.url ?? "/", "http://localhost");
1889
+ const full = url.searchParams.get("full") === "1";
1890
+ const rawLimit = Number.parseInt(url.searchParams.get("limit") ?? "200", 10);
1891
+ const limit = Math.min(5e3, Math.max(1, Number.isFinite(rawLimit) ? rawLimit : 200));
1892
+ const sessionId = decodeSessionId(match[1]);
1893
+ if (sessionId === null) {
1894
+ res.writeHead(400, { "Content-Type": "application/json" });
1895
+ res.end(JSON.stringify({ error: "invalid sessionId encoding" }));
1896
+ return;
1897
+ }
1898
+ const globalRoot = path4.dirname(resolveHqDataDir3());
1899
+ try {
1900
+ const registry = getSessionRegistry(globalRoot);
1901
+ const entry = await registry.get(sessionId).catch(() => null);
1902
+ let entries = [];
1903
+ let source = "stream";
1904
+ let status;
1905
+ let clientType;
1906
+ let projectName;
1907
+ if (entry && "projectRoot" in entry) {
1908
+ const paths = resolveWstackPaths({
1909
+ projectRoot: entry.projectRoot,
1910
+ globalRoot
1911
+ });
1912
+ const store = new DefaultSessionStore({
1913
+ dir: paths.projectSessions,
1914
+ projectRoot: entry.projectRoot
1915
+ });
1916
+ const data = await store.load(sessionId).catch(() => null);
1917
+ if (data) {
1918
+ entries = buildTranscriptFromEvents(
1919
+ data.events.map((e) => e)
1920
+ );
1921
+ source = "disk";
1922
+ status = entry.status;
1923
+ clientType = entry.clientType;
1924
+ projectName = entry.projectName;
1925
+ }
1926
+ }
1927
+ if (entries.length === 0) {
1928
+ const ring = transcripts.get(sessionId);
1929
+ if (!ring && !entry) {
1930
+ res.writeHead(404, { "Content-Type": "application/json" });
1931
+ res.end(JSON.stringify({ error: "Session not found" }));
1932
+ return;
1933
+ }
1934
+ entries = ring ? ring.entries : [];
1935
+ source = "stream";
1936
+ if (entry) {
1937
+ status = entry.status;
1938
+ clientType = entry.clientType;
1939
+ projectName = entry.projectName;
1940
+ }
1941
+ }
1942
+ const total = entries.length;
1943
+ const tail = full ? entries : entries.slice(-limit);
1944
+ res.writeHead(200, { "Content-Type": "application/json" });
1945
+ res.end(
1946
+ JSON.stringify({
1947
+ sessionId,
1948
+ source,
1949
+ ...status !== void 0 ? { status } : {},
1950
+ ...clientType !== void 0 ? { clientType } : {},
1951
+ ...projectName !== void 0 ? { projectName } : {},
1952
+ total,
1953
+ entries: tail
1954
+ })
1955
+ );
1956
+ } catch (err) {
1957
+ res.writeHead(500, { "Content-Type": "application/json" });
1958
+ res.end(JSON.stringify({ error: sanitizeApiError(err) }));
1959
+ }
1960
+ }
1961
+ async function handleApiSessionAgentMessages(req, res, match, agentMessages) {
1962
+ const url = new URL(req.url ?? "/", "http://localhost");
1963
+ const sid = decodeSessionId(match[1]);
1964
+ const aid = decodePathSegment(match[2]);
1965
+ if (sid === null || aid === null) {
1966
+ res.writeHead(400, { "Content-Type": "application/json" });
1967
+ res.end(JSON.stringify({ error: "invalid session or agent id encoding" }));
1968
+ return;
1969
+ }
1970
+ const full = url.searchParams.get("full") === "1";
1971
+ const disk = await readLocalSubagentTranscript(sid, aid);
1972
+ const source = disk !== null ? "disk" : "stream";
1973
+ const all = disk !== null ? disk : agentMessages.get(agentRingKey(sid, aid)) ?? agentMessages.get(aid) ?? [];
1974
+ const entries = full ? all : all.slice(-200);
1975
+ res.writeHead(200, { "Content-Type": "application/json" });
1976
+ res.end(JSON.stringify({ subagentId: aid, sessionId: sid, source, total: all.length, entries }));
1977
+ }
1978
+ async function handleApiAgentMessages(req, res, match, agentMessages) {
1979
+ const url = new URL(req.url ?? "/", "http://localhost");
1980
+ const id = decodePathSegment(match[1]);
1981
+ if (id === null) {
1982
+ res.writeHead(400, { "Content-Type": "application/json" });
1983
+ res.end(JSON.stringify({ error: "invalid agent id encoding" }));
1984
+ return;
1985
+ }
1986
+ const full = url.searchParams.get("full") === "1";
1987
+ const merged = [];
1988
+ for (const [key, ring] of agentMessages) {
1989
+ if (key === id || key.endsWith(`::${id}`)) merged.push(...ring);
1990
+ }
1991
+ merged.sort((a, b) => a.ts.localeCompare(b.ts));
1992
+ const entries = full ? merged : merged.slice(-200);
1993
+ res.writeHead(200, { "Content-Type": "application/json" });
1994
+ res.end(JSON.stringify({ subagentId: id, total: merged.length, entries }));
1995
+ }
1996
+
2227
1997
  // src/hq-server/routes/system-handlers.ts
2228
1998
  async function handleApiSystemUpdate(res) {
2229
1999
  const [{ checkForUpdate }, { detectUpdatePackageName }] = await Promise.all([
2230
- import("./update-check-UMYTOH7Z.js"),
2231
- import("./update-MV6TUACD.js")
2000
+ import("./update-check-WARHSVZA.js"),
2001
+ import("./update-VZSOZGYC.js")
2232
2002
  ]);
2233
2003
  const packageName = detectUpdatePackageName();
2234
2004
  const info = await checkForUpdate({ packageName });
@@ -2238,260 +2008,562 @@ async function handleApiSystemUpdate(res) {
2238
2008
  });
2239
2009
  res.end(
2240
2010
  JSON.stringify({
2241
- ...info,
2242
- packageName,
2243
- command: "wstack update"
2011
+ ...info,
2012
+ packageName,
2013
+ command: "wstack update"
2014
+ })
2015
+ );
2016
+ }
2017
+ async function handleApiSystemHealth(res, clients, persistence, eventLog) {
2018
+ const now = Date.now();
2019
+ const eventCount = eventLog.length;
2020
+ let timeseriesStoreStatus = "unknown";
2021
+ let kanbanStoreStatus = "unknown";
2022
+ try {
2023
+ const tsOk = await persistence.timeseries.read(now - 3e5);
2024
+ timeseriesStoreStatus = Array.isArray(tsOk) ? "ok" : "degraded";
2025
+ } catch {
2026
+ timeseriesStoreStatus = "degraded";
2027
+ }
2028
+ try {
2029
+ await persistence.kanban.load("__hq_health_probe__");
2030
+ kanbanStoreStatus = "ok";
2031
+ } catch {
2032
+ kanbanStoreStatus = "degraded";
2033
+ }
2034
+ const inMemoryEventLogActive = true;
2035
+ const allOk = inMemoryEventLogActive && timeseriesStoreStatus === "ok" && kanbanStoreStatus === "ok";
2036
+ const clientCount = clients.size;
2037
+ const connectedClients = [...clients.values()].filter(
2038
+ (c) => Date.now() - new Date(c.lastSeenAt).getTime() < 6e4
2039
+ ).length;
2040
+ res.writeHead(200, {
2041
+ "Content-Type": "application/json",
2042
+ "Cache-Control": "no-store"
2043
+ });
2044
+ res.end(
2045
+ JSON.stringify({
2046
+ status: allOk ? "healthy" : "degraded",
2047
+ uptime: {
2048
+ serverTime: (/* @__PURE__ */ new Date()).toISOString(),
2049
+ eventLogSize: eventCount
2050
+ },
2051
+ stores: {
2052
+ events: "ok",
2053
+ timeseries: timeseriesStoreStatus,
2054
+ kanban: kanbanStoreStatus
2055
+ },
2056
+ connections: {
2057
+ total: clientCount,
2058
+ active: connectedClients,
2059
+ stale: clientCount - connectedClients
2060
+ }
2061
+ })
2062
+ );
2063
+ }
2064
+ async function handleApiCommandsAudit(req, res, auditLog) {
2065
+ const url = new URL(req.url ?? "/", "http://localhost");
2066
+ const rawLimit = Number.parseInt(url.searchParams.get("limit") ?? "200", 10);
2067
+ const limit = Math.min(1e3, Math.max(1, Number.isFinite(rawLimit) ? rawLimit : 200));
2068
+ res.writeHead(200, { "Content-Type": "application/json" });
2069
+ res.end(JSON.stringify({ commands: auditLog.recent(limit) }));
2070
+ }
2071
+ async function handleApiAlerts(req, res, alertEngine) {
2072
+ const url = new URL(req.url ?? "/", "http://localhost");
2073
+ const rawLimit = Number.parseInt(url.searchParams.get("limit") ?? "100", 10);
2074
+ const limit = Math.min(500, Math.max(1, Number.isFinite(rawLimit) ? rawLimit : 100));
2075
+ res.writeHead(200, { "Content-Type": "application/json" });
2076
+ res.end(
2077
+ JSON.stringify({
2078
+ active: alertEngine.activeAlerts(),
2079
+ history: alertEngine.recentAlerts(limit)
2244
2080
  })
2245
2081
  );
2246
2082
  }
2247
- async function handleApiSystemHealth(res, clients, persistence, eventLog) {
2248
- const now = Date.now();
2249
- const eventCount = eventLog.length;
2250
- let timeseriesStoreStatus = "unknown";
2251
- let kanbanStoreStatus = "unknown";
2252
- try {
2253
- const tsOk = await persistence.timeseries.read(now - 3e5);
2254
- timeseriesStoreStatus = Array.isArray(tsOk) ? "ok" : "degraded";
2255
- } catch {
2256
- timeseriesStoreStatus = "degraded";
2083
+
2084
+ // src/hq-server/snapshot.ts
2085
+ import { WebSocket } from "ws";
2086
+ var HQ_SNAPSHOT_BROADCAST_DEBOUNCE_MS = 100;
2087
+ var HQ_SNAPSHOT_PERSIST_MIN_INTERVAL_MS = 3e3;
2088
+ var HQ_BROWSER_MAX_BUFFERED_BYTES = 32 * 1024 * 1024;
2089
+ function sendGuarded(ws, data) {
2090
+ if (ws.readyState !== WebSocket.OPEN) return false;
2091
+ const buffered = Number.isFinite(ws.bufferedAmount) ? ws.bufferedAmount : 0;
2092
+ if (buffered + Buffer.byteLength(data, "utf8") > HQ_BROWSER_MAX_BUFFERED_BYTES) {
2093
+ try {
2094
+ ws.terminate();
2095
+ } catch {
2096
+ try {
2097
+ ws.close(1013, "client cannot keep up");
2098
+ } catch {
2099
+ }
2100
+ }
2101
+ return false;
2257
2102
  }
2258
2103
  try {
2259
- await persistence.kanban.load("__hq_health_probe__");
2260
- kanbanStoreStatus = "ok";
2104
+ ws.send(data);
2105
+ return true;
2261
2106
  } catch {
2262
- kanbanStoreStatus = "degraded";
2107
+ return false;
2108
+ }
2109
+ }
2110
+ var HQ_STALE_SNAPSHOT_MS = 5 * 6e4;
2111
+ function reapStaleClientState(clients, nowMs = Date.now()) {
2112
+ for (const client of clients.values()) {
2113
+ for (const [sessionId, tracked] of client.sessions) {
2114
+ if (nowMs - tracked.receivedAt > HQ_STALE_SNAPSHOT_MS) client.sessions.delete(sessionId);
2115
+ }
2116
+ for (const [runId, fleet] of client.fleets) {
2117
+ if (nowMs - fleet.receivedAt > HQ_STALE_SNAPSHOT_MS) client.fleets.delete(runId);
2118
+ }
2119
+ for (const [sessionId, tracked] of client.mcpSnapshots) {
2120
+ if (nowMs - tracked.receivedAt > HQ_STALE_SNAPSHOT_MS) client.mcpSnapshots.delete(sessionId);
2121
+ }
2122
+ if (client.governanceSnapshot !== void 0 && nowMs - client.governanceSnapshot.receivedAt > HQ_STALE_SNAPSHOT_MS) {
2123
+ delete client.governanceSnapshot;
2124
+ }
2125
+ }
2126
+ }
2127
+ function buildSnapshot(clients, options) {
2128
+ reapStaleClientState(clients);
2129
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2130
+ const clientRecordById = /* @__PURE__ */ new Map();
2131
+ const projectMap = /* @__PURE__ */ new Map();
2132
+ const mailboxSummaries = [];
2133
+ const sessionById = /* @__PURE__ */ new Map();
2134
+ const fleetByRunId = /* @__PURE__ */ new Map();
2135
+ for (const client of clients.values()) {
2136
+ const machineId = client.machineId || client.project.machineId || "";
2137
+ if (!clientRecordById.has(client.clientId)) {
2138
+ clientRecordById.set(client.clientId, {
2139
+ clientId: client.clientId,
2140
+ kind: client.kind,
2141
+ machineId,
2142
+ ...client.hostname ? { hostname: client.hostname } : {},
2143
+ ...client.pid ? { pid: client.pid } : {},
2144
+ ...client.version ? { version: client.version } : {},
2145
+ connected: true,
2146
+ connectedAt: client.connectedAt,
2147
+ lastSeenAt: client.lastSeenAt,
2148
+ projectId: client.projectId,
2149
+ capabilities: client.capabilities
2150
+ });
2151
+ }
2152
+ let project = projectMap.get(client.projectId);
2153
+ if (!project) {
2154
+ project = {
2155
+ projectId: client.projectId,
2156
+ projectName: client.project.projectName || client.projectId,
2157
+ projectRootDisplay: client.project.projectRoot,
2158
+ machineIds: [machineId],
2159
+ ...client.project.gitBranch ? { gitBranch: client.project.gitBranch } : {},
2160
+ activeClients: 0,
2161
+ activeSessions: 0,
2162
+ activeSubagents: 0,
2163
+ totalCostUsd: 0,
2164
+ lastActivityAt: now,
2165
+ status: "active"
2166
+ };
2167
+ projectMap.set(client.projectId, project);
2168
+ } else if (machineId && !project.machineIds.includes(machineId)) {
2169
+ project.machineIds = [...project.machineIds, machineId];
2170
+ }
2171
+ const governance = client.governanceSnapshot?.payload;
2172
+ if (governance !== void 0 && (project.governance === void 0 || Date.parse(governance.observedAt) >= Date.parse(project.governance.observedAt))) {
2173
+ project.governance = governance;
2174
+ }
2175
+ for (const tracked of client.sessions.values()) {
2176
+ const cutoff = Date.now() - 5 * 6e4;
2177
+ const agents = tracked.payload.agents.filter((agent) => {
2178
+ if (agent.id === "leader") return true;
2179
+ const lastActivityAt = Date.parse(agent.lastActivityAt);
2180
+ return Number.isFinite(lastActivityAt) && lastActivityAt >= cutoff;
2181
+ });
2182
+ sessionById.set(tracked.payload.sessionId, {
2183
+ ...tracked.payload,
2184
+ clientId: client.clientId,
2185
+ agents,
2186
+ agentCount: agents.length
2187
+ });
2188
+ }
2189
+ for (const snapshot of client.mailboxes.values()) {
2190
+ mailboxSummaries.push({
2191
+ mailboxId: snapshot.mailboxId,
2192
+ projectId: client.projectId,
2193
+ scope: snapshot.scope,
2194
+ messageCount: snapshot.totals.messages,
2195
+ unreadCount: snapshot.totals.unread,
2196
+ incompleteCount: snapshot.totals.incomplete,
2197
+ highPriorityCount: snapshot.totals.highPriority,
2198
+ onlineAgentCount: snapshot.totals.onlineAgents,
2199
+ lastActivityAt: now
2200
+ });
2201
+ }
2202
+ for (const fleet of client.fleets.values()) {
2203
+ fleetByRunId.set(fleet.payload.runId, {
2204
+ payload: fleet.payload,
2205
+ clientId: client.clientId,
2206
+ projectId: client.projectId,
2207
+ lastActivityAt: new Date(fleet.receivedAt).toISOString()
2208
+ });
2209
+ }
2210
+ }
2211
+ const processKeyOf = (rec) => rec.pid !== void 0 ? `${rec.machineId}:${rec.pid}` : rec.clientId;
2212
+ const countedProjectProcesses = /* @__PURE__ */ new Set();
2213
+ for (const rec of clientRecordById.values()) {
2214
+ const key = `${rec.projectId}|${processKeyOf(rec)}`;
2215
+ if (countedProjectProcesses.has(key)) continue;
2216
+ countedProjectProcesses.add(key);
2217
+ const project = projectMap.get(rec.projectId);
2218
+ if (project) project.activeClients++;
2219
+ }
2220
+ const liveSessions = Array.from(sessionById.values());
2221
+ const machineMap = /* @__PURE__ */ new Map();
2222
+ let totalAgents = 0;
2223
+ let totalSubagents = 0;
2224
+ let totalCostUsd = 0;
2225
+ for (const session of liveSessions) {
2226
+ let project = projectMap.get(session.projectId);
2227
+ if (!project) {
2228
+ project = {
2229
+ projectId: session.projectId,
2230
+ projectName: session.projectName || session.projectId,
2231
+ projectRootDisplay: session.projectRoot,
2232
+ machineIds: [session.machineId],
2233
+ ...session.gitBranch ? { gitBranch: session.gitBranch } : {},
2234
+ activeClients: 0,
2235
+ activeSessions: 0,
2236
+ activeSubagents: 0,
2237
+ totalCostUsd: 0,
2238
+ lastActivityAt: session.lastActivityAt,
2239
+ status: "active"
2240
+ };
2241
+ projectMap.set(session.projectId, project);
2242
+ } else if (session.machineId && !project.machineIds.includes(session.machineId)) {
2243
+ project.machineIds = [...project.machineIds, session.machineId];
2244
+ }
2245
+ project.activeSessions++;
2246
+ let sessionCost = 0;
2247
+ for (const agent of session.agents) {
2248
+ totalAgents++;
2249
+ if (agent.id !== "leader") totalSubagents++;
2250
+ if (typeof agent.costUsd === "number") {
2251
+ sessionCost += agent.costUsd;
2252
+ }
2253
+ }
2254
+ project.activeSubagents += session.agents.filter((a) => a.id !== "leader").length;
2255
+ project.totalCostUsd += sessionCost;
2256
+ totalCostUsd += sessionCost;
2257
+ const mKey = hqMachineKey(session.hostname, session.machineId);
2258
+ let machine = machineMap.get(mKey);
2259
+ if (!machine) {
2260
+ machine = {
2261
+ record: {
2262
+ machineId: session.machineId,
2263
+ ...session.hostname ? { hostname: session.hostname } : {},
2264
+ clientCount: 0,
2265
+ sessionCount: 0,
2266
+ agentCount: 0,
2267
+ projectIds: [],
2268
+ lastActivityAt: session.lastActivityAt
2269
+ },
2270
+ projects: /* @__PURE__ */ new Set()
2271
+ };
2272
+ machineMap.set(mKey, machine);
2273
+ }
2274
+ machine.record.sessionCount++;
2275
+ machine.record.agentCount += session.agents.length;
2276
+ machine.projects.add(session.projectId);
2277
+ if (session.lastActivityAt > machine.record.lastActivityAt) {
2278
+ machine.record.lastActivityAt = session.lastActivityAt;
2279
+ }
2280
+ }
2281
+ const countedMachineProcesses = /* @__PURE__ */ new Set();
2282
+ for (const rec of clientRecordById.values()) {
2283
+ if (!rec.machineId && !rec.hostname) continue;
2284
+ const rKey = hqMachineKey(rec.hostname, rec.machineId);
2285
+ let machine = machineMap.get(rKey);
2286
+ if (!machine) {
2287
+ machine = {
2288
+ record: {
2289
+ machineId: rec.machineId,
2290
+ ...rec.hostname ? { hostname: rec.hostname } : {},
2291
+ clientCount: 0,
2292
+ sessionCount: 0,
2293
+ agentCount: 0,
2294
+ projectIds: [],
2295
+ lastActivityAt: rec.lastSeenAt
2296
+ },
2297
+ projects: /* @__PURE__ */ new Set()
2298
+ };
2299
+ machineMap.set(rKey, machine);
2300
+ }
2301
+ const processKey = `${rKey}|${processKeyOf(rec)}`;
2302
+ if (!countedMachineProcesses.has(processKey)) {
2303
+ countedMachineProcesses.add(processKey);
2304
+ machine.record.clientCount++;
2305
+ }
2306
+ machine.projects.add(rec.projectId);
2307
+ if (rec.hostname && !machine.record.hostname) machine.record.hostname = rec.hostname;
2308
+ }
2309
+ const machines = Array.from(machineMap.values()).map((m) => ({
2310
+ ...m.record,
2311
+ projectIds: Array.from(m.projects)
2312
+ }));
2313
+ const clientRecords = Array.from(clientRecordById.values());
2314
+ const projects = Array.from(projectMap.values());
2315
+ let unread = 0;
2316
+ let incomplete = 0;
2317
+ for (const m of mailboxSummaries) {
2318
+ unread += m.unreadCount;
2319
+ incomplete += m.incompleteCount;
2263
2320
  }
2264
- const inMemoryEventLogActive = true;
2265
- const allOk = inMemoryEventLogActive && timeseriesStoreStatus === "ok" && kanbanStoreStatus === "ok";
2266
- const clientCount = clients.size;
2267
- const connectedClients = [...clients.values()].filter(
2268
- (c) => Date.now() - new Date(c.lastSeenAt).getTime() < 6e4
2269
- ).length;
2270
- res.writeHead(200, {
2271
- "Content-Type": "application/json",
2272
- "Cache-Control": "no-store"
2273
- });
2274
- res.end(
2275
- JSON.stringify({
2276
- status: allOk ? "healthy" : "degraded",
2277
- uptime: {
2278
- serverTime: (/* @__PURE__ */ new Date()).toISOString(),
2279
- eventLogSize: eventCount
2280
- },
2281
- stores: {
2282
- events: "ok",
2283
- timeseries: timeseriesStoreStatus,
2284
- kanban: kanbanStoreStatus
2285
- },
2286
- connections: {
2287
- total: clientCount,
2288
- active: connectedClients,
2289
- stale: clientCount - connectedClients
2290
- }
2291
- })
2292
- );
2293
- }
2294
- async function handleApiCommandsAudit(req, res, auditLog) {
2295
- const url = new URL(req.url ?? "/", "http://localhost");
2296
- const rawLimit = Number.parseInt(url.searchParams.get("limit") ?? "200", 10);
2297
- const limit = Math.min(1e3, Math.max(1, Number.isFinite(rawLimit) ? rawLimit : 200));
2298
- res.writeHead(200, { "Content-Type": "application/json" });
2299
- res.end(JSON.stringify({ commands: auditLog.recent(limit) }));
2300
- }
2301
- async function handleApiAlerts(req, res, alertEngine) {
2302
- const url = new URL(req.url ?? "/", "http://localhost");
2303
- const rawLimit = Number.parseInt(url.searchParams.get("limit") ?? "100", 10);
2304
- const limit = Math.min(500, Math.max(1, Number.isFinite(rawLimit) ? rawLimit : 100));
2305
- res.writeHead(200, { "Content-Type": "application/json" });
2306
- res.end(
2307
- JSON.stringify({
2308
- active: alertEngine.activeAlerts(),
2309
- history: alertEngine.recentAlerts(limit)
2310
- })
2311
- );
2312
- }
2313
-
2314
- // src/hq-server/routes/session-handlers.ts
2315
- import * as path4 from "node:path";
2316
- import {
2317
- buildTranscriptFromEvents,
2318
- resolveHqDataDir as resolveHqDataDir3
2319
- } from "@wrongstack/core/hq";
2320
- async function handleApiSessions(res) {
2321
- const { SessionRegistry } = await import("@wrongstack/core/storage");
2322
- const globalRoot = path4.dirname(resolveHqDataDir3());
2323
- try {
2324
- const registry = new SessionRegistry(globalRoot);
2325
- const sessions = await registry.list();
2326
- const result = sessions.filter((s) => s.status !== "stale").map((s) => ({
2321
+ const sessions = liveSessions.map((s) => {
2322
+ let sessionCost = 0;
2323
+ for (const agent of s.agents) {
2324
+ if (typeof agent.costUsd === "number") sessionCost += agent.costUsd;
2325
+ }
2326
+ const provider = s.agents.find((a) => a.model !== void 0)?.model;
2327
+ return {
2327
2328
  sessionId: s.sessionId,
2328
- projectSlug: s.projectSlug,
2329
- projectName: s.projectName,
2330
- projectRoot: s.projectRoot,
2331
- workingDir: s.workingDir,
2332
- status: s.status,
2333
- pid: s.pid,
2329
+ projectId: s.projectId,
2330
+ clientId: s.clientId ?? `${s.machineId}:${s.clientKind}`,
2331
+ status: s.status === "active" ? "running" : "idle",
2332
+ ...provider !== void 0 ? { model: provider } : {},
2334
2333
  startedAt: s.startedAt,
2335
- lastHeartbeatAt: s.lastHeartbeatAt,
2336
- agentCount: s.agentCount,
2337
- agents: s.agents.map((a) => ({
2338
- id: a.id,
2339
- name: a.name,
2340
- status: a.status,
2341
- currentTool: a.currentTool,
2342
- iterations: a.iterations,
2343
- toolCalls: a.toolCalls,
2344
- lastActivityAt: a.lastActivityAt
2345
- }))
2346
- }));
2347
- res.writeHead(200, { "Content-Type": "application/json" });
2348
- res.end(JSON.stringify(result));
2349
- } catch (err) {
2350
- res.writeHead(500, { "Content-Type": "application/json" });
2351
- res.end(JSON.stringify({ error: sanitizeApiError(err) }));
2334
+ lastActivityAt: s.lastActivityAt,
2335
+ ...sessionCost > 0 ? { costUsd: sessionCost } : {}
2336
+ };
2337
+ });
2338
+ const fleets = Array.from(fleetByRunId.values()).map((f) => ({
2339
+ runId: f.payload.runId,
2340
+ projectId: f.projectId,
2341
+ clientId: f.clientId,
2342
+ activeSubagents: f.payload.activeSubagents,
2343
+ queuedTasks: f.payload.queuedTasks,
2344
+ completedTasks: f.payload.completedTasks,
2345
+ failedTasks: f.payload.failedTasks,
2346
+ ...f.payload.totalCostUsd !== void 0 ? { totalCostUsd: f.payload.totalCostUsd } : {},
2347
+ lastActivityAt: f.lastActivityAt,
2348
+ ...f.payload.maxConcurrent !== void 0 ? { maxConcurrent: f.payload.maxConcurrent } : {},
2349
+ ...f.payload.maxSpawns !== void 0 ? { maxSpawns: f.payload.maxSpawns } : {},
2350
+ ...f.payload.usedSpawns !== void 0 ? { usedSpawns: f.payload.usedSpawns } : {},
2351
+ ...f.payload.remainingSpawns !== void 0 ? { remainingSpawns: f.payload.remainingSpawns } : {},
2352
+ ...f.payload.effectiveSource !== void 0 ? { effectiveSource: f.payload.effectiveSource } : {},
2353
+ ...f.payload.checkpointMaxSpawns !== void 0 ? { checkpointMaxSpawns: f.payload.checkpointMaxSpawns } : {},
2354
+ ...f.payload.ceilingMismatch ? { ceilingMismatch: true } : {}
2355
+ }));
2356
+ const mcpServers = [];
2357
+ const seenMcp = /* @__PURE__ */ new Set();
2358
+ for (const client of clients.values()) {
2359
+ for (const [sessionId, tracked] of client.mcpSnapshots.entries()) {
2360
+ for (const server of tracked.servers) {
2361
+ const key = `${client.clientId}:${sessionId}:${server.name}`;
2362
+ if (seenMcp.has(key)) continue;
2363
+ seenMcp.add(key);
2364
+ mcpServers.push(server);
2365
+ }
2366
+ }
2352
2367
  }
2368
+ return {
2369
+ generatedAt: now,
2370
+ clients: clientRecords,
2371
+ projects,
2372
+ sessions,
2373
+ fleets,
2374
+ mailboxes: mailboxSummaries,
2375
+ machines,
2376
+ liveSessions,
2377
+ mcpServers,
2378
+ totals: {
2379
+ activeProjects: projects.length,
2380
+ activeClients: new Set(clientRecords.map(processKeyOf)).size,
2381
+ activeSessions: liveSessions.length,
2382
+ activeSubagents: totalSubagents,
2383
+ unreadMailboxMessages: unread,
2384
+ incompleteMailboxMessages: incomplete,
2385
+ totalCostUsd,
2386
+ activeMachines: machines.length,
2387
+ activeAgents: totalAgents,
2388
+ ...options?.tokenStats !== void 0 ? { tokenStats: options.tokenStats } : {}
2389
+ }
2390
+ };
2353
2391
  }
2354
- async function handleApiSessionEvents(req, res, match, transcripts) {
2355
- const { SessionRegistry, DefaultSessionStore } = await import("@wrongstack/core/storage");
2356
- const { resolveWstackPaths } = await import("@wrongstack/core/utils");
2357
- const url = new URL(req.url ?? "/", "http://localhost");
2358
- const full = url.searchParams.get("full") === "1";
2359
- const rawLimit = Number.parseInt(url.searchParams.get("limit") ?? "200", 10);
2360
- const limit = Math.min(5e3, Math.max(1, Number.isFinite(rawLimit) ? rawLimit : 200));
2361
- const sessionId = decodeSessionId(match[1]);
2362
- if (sessionId === null) {
2363
- res.writeHead(400, { "Content-Type": "application/json" });
2364
- res.end(JSON.stringify({ error: "invalid sessionId encoding" }));
2365
- return;
2366
- }
2367
- const globalRoot = path4.dirname(resolveHqDataDir3());
2368
- try {
2369
- const registry = new SessionRegistry(globalRoot);
2370
- const entry = await registry.get(sessionId).catch(() => null);
2371
- let entries = [];
2372
- let source = "stream";
2373
- let status;
2374
- let clientType;
2375
- let projectName;
2376
- if (entry && "projectRoot" in entry) {
2377
- const paths = resolveWstackPaths({
2378
- projectRoot: entry.projectRoot,
2379
- globalRoot
2380
- });
2381
- const store = new DefaultSessionStore({ dir: paths.projectSessions });
2382
- const data = await store.load(sessionId).catch(() => null);
2383
- if (data) {
2384
- entries = buildTranscriptFromEvents(
2385
- data.events.map((e) => e)
2386
- );
2387
- source = "disk";
2388
- status = entry.status;
2389
- clientType = entry.clientType;
2390
- projectName = entry.projectName;
2392
+ function createSnapshotBroadcaster(clients, browsers, persistence) {
2393
+ let cached = "";
2394
+ let dirty = true;
2395
+ let timer = null;
2396
+ let closed = false;
2397
+ let lastPersistAt = 0;
2398
+ const persistCheckpoint = (snapshot) => {
2399
+ if (persistence === void 0) return;
2400
+ const now = Date.now();
2401
+ if (now - lastPersistAt < HQ_SNAPSHOT_PERSIST_MIN_INTERVAL_MS) return;
2402
+ lastPersistAt = now;
2403
+ persistence.snapshotStore.save(snapshot);
2404
+ };
2405
+ const serialize = () => {
2406
+ if (!dirty && cached.length > 0) return cached;
2407
+ const snapshot = buildSnapshot(clients);
2408
+ const msg = { type: "hq.snapshot", snapshot };
2409
+ cached = JSON.stringify(msg);
2410
+ dirty = false;
2411
+ persistCheckpoint(snapshot);
2412
+ return cached;
2413
+ };
2414
+ const flush = () => {
2415
+ timer = null;
2416
+ if (browsers.size === 0) {
2417
+ if (persistence !== void 0 && Date.now() - lastPersistAt >= HQ_SNAPSHOT_PERSIST_MIN_INTERVAL_MS) {
2418
+ persistCheckpoint(buildSnapshot(clients));
2391
2419
  }
2420
+ return;
2392
2421
  }
2393
- if (entries.length === 0) {
2394
- const ring = transcripts.get(sessionId);
2395
- if (!ring && !entry) {
2396
- res.writeHead(404, { "Content-Type": "application/json" });
2397
- res.end(JSON.stringify({ error: "Session not found" }));
2398
- return;
2399
- }
2400
- entries = ring ? ring.entries : [];
2401
- source = "stream";
2402
- if (entry) {
2403
- status = entry.status;
2404
- clientType = entry.clientType;
2405
- projectName = entry.projectName;
2422
+ const data = serialize();
2423
+ for (const ws of browsers) sendGuarded(ws, data);
2424
+ };
2425
+ return {
2426
+ currentSerialized: serialize,
2427
+ broadcast: () => {
2428
+ if (closed) return;
2429
+ dirty = true;
2430
+ if (timer !== null) return;
2431
+ timer = setTimeout(flush, HQ_SNAPSHOT_BROADCAST_DEBOUNCE_MS);
2432
+ timer.unref?.();
2433
+ },
2434
+ close: () => {
2435
+ closed = true;
2436
+ if (timer !== null) {
2437
+ clearTimeout(timer);
2438
+ timer = null;
2406
2439
  }
2407
2440
  }
2408
- const total = entries.length;
2409
- const tail = full ? entries : entries.slice(-limit);
2410
- res.writeHead(200, { "Content-Type": "application/json" });
2411
- res.end(
2412
- JSON.stringify({
2413
- sessionId,
2414
- source,
2415
- ...status !== void 0 ? { status } : {},
2416
- ...clientType !== void 0 ? { clientType } : {},
2417
- ...projectName !== void 0 ? { projectName } : {},
2418
- total,
2419
- entries: tail
2420
- })
2421
- );
2422
- } catch (err) {
2423
- res.writeHead(500, { "Content-Type": "application/json" });
2424
- res.end(JSON.stringify({ error: sanitizeApiError(err) }));
2441
+ };
2442
+ }
2443
+ function buildProjectDetail(clients, projectId) {
2444
+ const projectClients = [];
2445
+ for (const c of clients.values()) {
2446
+ if (c.projectId === projectId) projectClients.push(c);
2447
+ }
2448
+ if (projectClients.length === 0) return null;
2449
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2450
+ const clientRecords = projectClients.map((c) => ({
2451
+ clientId: c.clientId,
2452
+ kind: c.kind,
2453
+ machineId: "",
2454
+ ...c.hostname ? { hostname: c.hostname } : {},
2455
+ ...c.pid ? { pid: c.pid } : {},
2456
+ ...c.version ? { version: c.version } : {},
2457
+ connected: true,
2458
+ connectedAt: c.connectedAt,
2459
+ lastSeenAt: c.lastSeenAt,
2460
+ projectId: c.projectId,
2461
+ capabilities: c.capabilities
2462
+ }));
2463
+ const mailboxPayloads = [];
2464
+ let latestActivity = now;
2465
+ for (const c of projectClients) {
2466
+ for (const snap of c.mailboxes.values()) {
2467
+ mailboxPayloads.push(snap);
2468
+ if (snap.totals.messages > 0) latestActivity = now;
2469
+ }
2425
2470
  }
2471
+ const primaryProject = projectClients[0].project;
2472
+ const machineIds = Array.from(new Set(projectClients.map((client) => client.project.machineId)));
2473
+ const project = {
2474
+ projectId,
2475
+ projectName: primaryProject.projectName || projectId,
2476
+ projectRootDisplay: primaryProject.projectRoot,
2477
+ machineIds,
2478
+ ...primaryProject.gitBranch ? { gitBranch: primaryProject.gitBranch } : {},
2479
+ activeClients: projectClients.length,
2480
+ activeSessions: 0,
2481
+ activeSubagents: 0,
2482
+ totalCostUsd: 0,
2483
+ lastActivityAt: latestActivity,
2484
+ status: "active"
2485
+ };
2486
+ return {
2487
+ generatedAt: now,
2488
+ project,
2489
+ clients: clientRecords,
2490
+ mailboxes: mailboxPayloads
2491
+ };
2426
2492
  }
2427
- async function handleApiSessionAgentMessages(req, res, match, agentMessages) {
2428
- const url = new URL(req.url ?? "/", "http://localhost");
2429
- const sid = decodeSessionId(match[1]);
2430
- const aid = decodePathSegment(match[2]);
2431
- if (sid === null || aid === null) {
2432
- res.writeHead(400, { "Content-Type": "application/json" });
2433
- res.end(JSON.stringify({ error: "invalid session or agent id encoding" }));
2434
- return;
2435
- }
2436
- const full = url.searchParams.get("full") === "1";
2437
- const disk = await readLocalSubagentTranscript(sid, aid);
2438
- const source = disk !== null ? "disk" : "stream";
2439
- const all = disk !== null ? disk : agentMessages.get(agentRingKey(sid, aid)) ?? agentMessages.get(aid) ?? [];
2440
- const entries = full ? all : all.slice(-200);
2441
- res.writeHead(200, { "Content-Type": "application/json" });
2442
- res.end(JSON.stringify({ subagentId: aid, sessionId: sid, source, total: all.length, entries }));
2493
+ function broadcastEvent(event, browsers) {
2494
+ const msg = { type: "hq.event", event };
2495
+ const data = JSON.stringify(msg);
2496
+ for (const ws of browsers) sendGuarded(ws, data);
2443
2497
  }
2444
- async function handleApiAgentMessages(req, res, match, agentMessages) {
2445
- const url = new URL(req.url ?? "/", "http://localhost");
2446
- const id = decodePathSegment(match[1]);
2447
- if (id === null) {
2448
- res.writeHead(400, { "Content-Type": "application/json" });
2449
- res.end(JSON.stringify({ error: "invalid agent id encoding" }));
2450
- return;
2451
- }
2452
- const full = url.searchParams.get("full") === "1";
2453
- const merged = [];
2454
- for (const [key, ring] of agentMessages) {
2455
- if (key === id || key.endsWith(`::${id}`)) merged.push(...ring);
2456
- }
2457
- merged.sort((a, b) => a.ts.localeCompare(b.ts));
2458
- const entries = full ? merged : merged.slice(-200);
2459
- res.writeHead(200, { "Content-Type": "application/json" });
2460
- res.end(JSON.stringify({ subagentId: id, total: merged.length, entries }));
2498
+ function broadcastCommandStatus(command, browsers) {
2499
+ const data = JSON.stringify({ type: "hq.command_status", command });
2500
+ for (const ws of browsers) sendGuarded(ws, data);
2461
2501
  }
2462
2502
 
2463
- // src/hq-server/project-root.ts
2464
- import { deriveHqProjectId } from "@wrongstack/core/hq";
2465
- function resolveHqProjectRoot(globalRoot, ids) {
2466
- const fn = async () => {
2467
- const { SessionRegistry } = await import("@wrongstack/core/storage");
2468
- try {
2469
- const registry = new SessionRegistry(globalRoot);
2470
- if (typeof ids.sessionId === "string") {
2471
- const entry = await registry.get(ids.sessionId).catch(() => null);
2472
- if (entry?.projectRoot) return entry.projectRoot;
2473
- }
2474
- if (typeof ids.projectId === "string") {
2475
- const { createHash: createHash2 } = await import("node:crypto");
2476
- const all = await registry.list().catch(() => []);
2477
- const projectIds = /* @__PURE__ */ new Map();
2478
- const projectIdForRoot = (projectRoot) => {
2479
- const cached = projectIds.get(projectRoot);
2480
- if (cached !== void 0) return cached;
2481
- const derived = deriveHqProjectId(projectRoot);
2482
- projectIds.set(projectRoot, derived);
2483
- return derived;
2484
- };
2485
- const match = all.find(
2486
- (e) => e.projectSlug === ids.projectId || projectIdForRoot(e.projectRoot) === ids.projectId || createHash2("sha256").update(e.projectRoot).digest("hex").slice(0, 12) === ids.projectId
2487
- );
2488
- if (match) return match.projectRoot;
2489
- }
2490
- } catch {
2491
- }
2492
- return void 0;
2503
+ // src/hq-server/trust-boundary.ts
2504
+ import {
2505
+ isTrustDecisionAllowed
2506
+ } from "@wrongstack/core/security";
2507
+ async function authorizeHqCommand(input) {
2508
+ const { boundary, command, commandId, enqueuedBy, authMethod, target } = input;
2509
+ const subject = command.type === "run-command" ? {
2510
+ kind: "command",
2511
+ id: command.command,
2512
+ attributes: { targetClientId: target.clientId }
2513
+ } : {
2514
+ kind: "custom",
2515
+ id: command.type,
2516
+ attributes: { targetClientId: target.clientId }
2493
2517
  };
2494
- return fn();
2518
+ const request = {
2519
+ version: 1,
2520
+ requestId: commandId,
2521
+ // WS-050: this was hardcoded to `kind: 'user'`, which made the
2522
+ // compatibility policy's ONLY deny rule — `remote-client` + `critical` —
2523
+ // unreachable from HQ. A boundary whose single rule can never fire is not
2524
+ // defence in depth, it is decoration.
2525
+ //
2526
+ // The provenance is derived from how the caller authenticated, because
2527
+ // that is the honest signal available here:
2528
+ //
2529
+ // session cookie → `user`. The dashboard's primary flow is a bootstrap
2530
+ // code exchanged for an HttpOnly cookie, so this is a human at the
2531
+ // browser. NOTE: bootstrap sessions DO carry a `tokenId`, so a
2532
+ // "cookie without tokenId" test would have misclassified every normal
2533
+ // dashboard user as remote and denied them run-command. Checked before
2534
+ // writing this, not assumed.
2535
+ // raw bearer token → `remote-client`. The legacy `?token=` / manual-entry
2536
+ // path and any script calling the HTTP API directly. A credential that
2537
+ // can be copied into a script is not a person at a console.
2538
+ //
2539
+ // Consequence, deliberate: a bearer-token caller can no longer trigger
2540
+ // `run-command` (the only `critical` capability here). `enqueue` is `high`
2541
+ // and unaffected. The migration for automation that needs execute is the
2542
+ // bootstrap exchange, which is the same thing the dashboard does.
2543
+ actor: { kind: authMethod === "session" ? "user" : "remote-client", id: enqueuedBy },
2544
+ surface: "hq",
2545
+ capability: command.type === "run-command" ? "hq.control.execute" : "hq.control.enqueue",
2546
+ subject,
2547
+ risk: command.type === "run-command" ? "critical" : "high",
2548
+ scope: { projectId: target.projectId },
2549
+ authContext: { method: authMethod, principalId: enqueuedBy },
2550
+ metadata: { commandType: command.type, targetKind: target.kind }
2551
+ };
2552
+ const decision = await boundary.evaluate(request);
2553
+ console.warn(
2554
+ JSON.stringify({
2555
+ level: "info",
2556
+ event: "hq.trust_boundary.decision",
2557
+ requestId: commandId,
2558
+ capability: request.capability,
2559
+ decision: decision.kind,
2560
+ policyId: decision.policyId,
2561
+ actor: enqueuedBy,
2562
+ clientId: target.clientId,
2563
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2564
+ })
2565
+ );
2566
+ return { allowed: isTrustDecisionAllowed(decision), reason: decision.reason };
2495
2567
  }
2496
2568
 
2497
2569
  // src/hq-server/routes.ts
@@ -2545,13 +2617,7 @@ function createHqRouter(deps) {
2545
2617
  try {
2546
2618
  const url = new URL(req.url ?? "/", `http://${host}:${listeningPort()}`);
2547
2619
  setHqSecurityHeaders2(res);
2548
- if (!hasTrustedBrowserOrigin2(
2549
- req,
2550
- host,
2551
- listeningPort(),
2552
- trustedPublicOrigins,
2553
- allowFileOrigin
2554
- )) {
2620
+ if (!hasTrustedBrowserOrigin2(req, host, listeningPort(), trustedPublicOrigins, allowFileOrigin)) {
2555
2621
  res.writeHead(403, { "Content-Type": "application/json" });
2556
2622
  res.end(JSON.stringify({ error: "forbidden: untrusted request origin" }));
2557
2623
  return;
@@ -2584,7 +2650,16 @@ function createHqRouter(deps) {
2584
2650
  return;
2585
2651
  }
2586
2652
  if (url.pathname === "/api/auth/status" && req.method === "GET") {
2587
- await handleApiAuthStatus(req, res, url, mutableAuth, sessions, requireBrowserAuth, trustedPublicOrigins, secureCookies);
2653
+ await handleApiAuthStatus(
2654
+ req,
2655
+ res,
2656
+ url,
2657
+ mutableAuth,
2658
+ sessions,
2659
+ requireBrowserAuth,
2660
+ trustedPublicOrigins,
2661
+ secureCookies
2662
+ );
2588
2663
  return;
2589
2664
  }
2590
2665
  if (url.pathname === "/api/auth/audit" && req.method === "GET" || url.pathname.startsWith("/api/auth/sessions") && (req.method === "GET" || req.method === "DELETE") || url.pathname.startsWith("/api/auth/totp/") && req.method === "POST") {
@@ -2615,7 +2690,15 @@ function createHqRouter(deps) {
2615
2690
  return;
2616
2691
  }
2617
2692
  if (url.pathname === "/api/login" && req.method === "POST") {
2618
- await handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, secureCookies, trustedProxyHops);
2693
+ await handleApiLogin(
2694
+ req,
2695
+ res,
2696
+ mutableAuth,
2697
+ sessions,
2698
+ loginAttempts,
2699
+ secureCookies,
2700
+ trustedProxyHops
2701
+ );
2619
2702
  return;
2620
2703
  }
2621
2704
  if (url.pathname === "/api/auth/bootstrap" && req.method === "POST") {
@@ -2643,7 +2726,18 @@ function createHqRouter(deps) {
2643
2726
  return;
2644
2727
  }
2645
2728
  if (url.pathname === "/api/login/verify" && req.method === "POST") {
2646
- await handleApiLoginVerify(req, res, url, mutableAuth, sessions, loginAttempts, dataDir, secureCookies, applyAuthFile, trustedProxyHops);
2729
+ await handleApiLoginVerify(
2730
+ req,
2731
+ res,
2732
+ url,
2733
+ mutableAuth,
2734
+ sessions,
2735
+ loginAttempts,
2736
+ dataDir,
2737
+ secureCookies,
2738
+ applyAuthFile,
2739
+ trustedProxyHops
2740
+ );
2647
2741
  return;
2648
2742
  }
2649
2743
  if (url.pathname === "/api/auth/totp/setup" && req.method === "POST") {
@@ -2655,16 +2749,38 @@ function createHqRouter(deps) {
2655
2749
  return;
2656
2750
  }
2657
2751
  if (url.pathname === "/api/auth/totp/disable" && req.method === "POST") {
2658
- await handleApiTotpDisable(req, res, mutableAuth, sessions, loginAttempts, dataDir, applyAuthFile, trustedProxyHops);
2752
+ await handleApiTotpDisable(
2753
+ req,
2754
+ res,
2755
+ mutableAuth,
2756
+ sessions,
2757
+ loginAttempts,
2758
+ dataDir,
2759
+ applyAuthFile,
2760
+ trustedProxyHops
2761
+ );
2659
2762
  return;
2660
2763
  }
2661
2764
  if (url.pathname === "/api/auth/password" && (req.method === "POST" || req.method === "DELETE")) {
2662
- await handleApiPassword(req, res, mutableAuth, sessions, dataDir, secureCookies, requireBrowserAuth, applyAuthFile);
2765
+ await handleApiPassword(
2766
+ req,
2767
+ res,
2768
+ mutableAuth,
2769
+ sessions,
2770
+ dataDir,
2771
+ secureCookies,
2772
+ requireBrowserAuth,
2773
+ applyAuthFile
2774
+ );
2663
2775
  return;
2664
2776
  }
2665
2777
  if (url.pathname === "/api/snapshot" && req.method === "GET") {
2666
2778
  res.writeHead(200, { "Content-Type": "application/json" });
2667
- res.end(JSON.stringify(buildSnapshot(clients, { tokenStats: getTokenStats?.() })));
2779
+ res.end(
2780
+ JSON.stringify(
2781
+ buildSnapshot(clients, { tokenStats: getTokenStats?.() })
2782
+ )
2783
+ );
2668
2784
  return;
2669
2785
  }
2670
2786
  if (url.pathname === "/api/system/update" && req.method === "GET") {
@@ -2679,7 +2795,14 @@ function createHqRouter(deps) {
2679
2795
  /^\/api\/projects\/([^/]+)\/mailbox(?:\/(.*))?$/
2680
2796
  );
2681
2797
  if (mailboxGatewayMatch) {
2682
- await handleMailboxGateway(req, res, mailboxGatewayMatch, authorizeMailboxGateway, getMailboxGateway, dataDir);
2798
+ await handleMailboxGateway(
2799
+ req,
2800
+ res,
2801
+ mailboxGatewayMatch,
2802
+ authorizeMailboxGateway,
2803
+ getMailboxGateway,
2804
+ dataDir
2805
+ );
2683
2806
  return;
2684
2807
  }
2685
2808
  const projectKanbanMatch = url.pathname.match(/^\/api\/projects\/([^/]+)\/kanban$/);
@@ -2687,7 +2810,9 @@ function createHqRouter(deps) {
2687
2810
  const projectId = decodePathSegment2(projectKanbanMatch[1]);
2688
2811
  if (projectId === null) {
2689
2812
  res.writeHead(400, { "Content-Type": "application/json" });
2690
- res.end(JSON.stringify({ error: { code: "VALIDATION_ERROR", message: "Invalid project id." } }));
2813
+ res.end(
2814
+ JSON.stringify({ error: { code: "VALIDATION_ERROR", message: "Invalid project id." } })
2815
+ );
2691
2816
  return;
2692
2817
  }
2693
2818
  const snapshot = await persistence.kanban.load(projectId);
@@ -2701,7 +2826,11 @@ function createHqRouter(deps) {
2701
2826
  }
2702
2827
  if (url.pathname === "/api/fleet" && req.method === "GET") {
2703
2828
  res.writeHead(200, { "Content-Type": "application/json" });
2704
- res.end(JSON.stringify(buildSnapshot(clients, { tokenStats: getTokenStats?.() })));
2829
+ res.end(
2830
+ JSON.stringify(
2831
+ buildSnapshot(clients, { tokenStats: getTokenStats?.() })
2832
+ )
2833
+ );
2705
2834
  return;
2706
2835
  }
2707
2836
  if (url.pathname === "/api/events" && req.method === "GET") {
@@ -2727,14 +2856,28 @@ function createHqRouter(deps) {
2727
2856
  return;
2728
2857
  }
2729
2858
  if (url.pathname === "/api/mailbox-send" && req.method === "POST") {
2730
- await handleApiMailboxSend(req, res, mutableAuth, sessions, dataDir, getMailboxGateway, hqSessionTag);
2859
+ await handleApiMailboxSend(
2860
+ req,
2861
+ res,
2862
+ mutableAuth,
2863
+ sessions,
2864
+ dataDir,
2865
+ getMailboxGateway,
2866
+ hqSessionTag
2867
+ );
2731
2868
  return;
2732
2869
  }
2733
- const mailboxActionMatch = url.pathname.match(
2734
- /^\/api\/mailbox\/messages\/([^/]+)\/action$/
2735
- );
2870
+ const mailboxActionMatch = url.pathname.match(/^\/api\/mailbox\/messages\/([^/]+)\/action$/);
2736
2871
  if (mailboxActionMatch && req.method === "POST") {
2737
- await handleMailboxAction(req, res, mailboxActionMatch, mutableAuth, sessions, dataDir, getMailboxGateway);
2872
+ await handleMailboxAction(
2873
+ req,
2874
+ res,
2875
+ mailboxActionMatch,
2876
+ mutableAuth,
2877
+ sessions,
2878
+ dataDir,
2879
+ getMailboxGateway
2880
+ );
2738
2881
  return;
2739
2882
  }
2740
2883
  if (url.pathname === "/api/commands" && req.method === "GET") {
@@ -2830,9 +2973,7 @@ async function handleApiProjectDetail(_req, res, url, clients) {
2830
2973
  }
2831
2974
  if (!projectId) {
2832
2975
  res.writeHead(400, { "Content-Type": "application/json" });
2833
- res.end(
2834
- JSON.stringify({ error: { code: "BAD_REQUEST", message: "projectId is required" } })
2835
- );
2976
+ res.end(JSON.stringify({ error: { code: "BAD_REQUEST", message: "projectId is required" } }));
2836
2977
  return;
2837
2978
  }
2838
2979
  const detail = buildProjectDetail(clients, projectId);
@@ -2921,9 +3062,7 @@ async function handleApiCommand(req, res, url, mutableAuth, sessions, clients, b
2921
3062
  const validated = validateHqCommand(queued);
2922
3063
  if (validated === null) {
2923
3064
  res.writeHead(400, { "Content-Type": "application/json" });
2924
- res.end(
2925
- JSON.stringify({ error: "unrecognized or malformed command", type: body.type })
2926
- );
3065
+ res.end(JSON.stringify({ error: "unrecognized or malformed command", type: body.type }));
2927
3066
  return;
2928
3067
  }
2929
3068
  if (validated.type === "run-command" && !tokenHasCapability(target.authToken, "control.execute")) {
@@ -3019,9 +3158,17 @@ async function handleApiMailboxSend(_req, res, mutableAuth, sessions, dataDir, g
3019
3158
  const subject = typeof mbody.subject === "string" ? mbody.subject : "HQ prompt";
3020
3159
  const priority = mbody.priority === "high" ? "high" : mbody.priority === "low" ? "low" : "normal";
3021
3160
  const audience = mbody.audience === "leaders" ? "leaders" : "all";
3022
- const mailboxType = mbody.type === "queue" ? "note" : ["note", "ask", "assign", "steer", "btw", "broadcast", "status", "result", "review"].includes(
3023
- mbody.type
3024
- ) ? mbody.type : void 0;
3161
+ const mailboxType = mbody.type === "queue" ? "note" : [
3162
+ "note",
3163
+ "ask",
3164
+ "assign",
3165
+ "steer",
3166
+ "btw",
3167
+ "broadcast",
3168
+ "status",
3169
+ "result",
3170
+ "review"
3171
+ ].includes(mbody.type) ? mbody.type : void 0;
3025
3172
  if (mailboxType === void 0) {
3026
3173
  res.writeHead(400, { "Content-Type": "application/json" });
3027
3174
  res.end(
@@ -3092,13 +3239,7 @@ async function handleMailboxAction(_req, res, match, mutableAuth, sessions, data
3092
3239
  res.end(JSON.stringify({ error: "forbidden: token lacks control.enqueue capability" }));
3093
3240
  return;
3094
3241
  }
3095
- const MAILBOX_ACTIONS = [
3096
- "mark-read",
3097
- "acknowledge",
3098
- "reopen",
3099
- "soft-delete",
3100
- "restore"
3101
- ];
3242
+ const MAILBOX_ACTIONS = ["mark-read", "acknowledge", "reopen", "soft-delete", "restore"];
3102
3243
  let abody;
3103
3244
  try {
3104
3245
  abody = JSON.parse(await readRequestBody2(_req));
@@ -4046,6 +4187,7 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
4046
4187
  const trustedPublicOrigins = /* @__PURE__ */ new Set();
4047
4188
  let listeningPort = port;
4048
4189
  const clients = /* @__PURE__ */ new Map();
4190
+ const clientSocketTokens = /* @__PURE__ */ new Map();
4049
4191
  const browsers = /* @__PURE__ */ new Set();
4050
4192
  const sessions = /* @__PURE__ */ new Map();
4051
4193
  const eventLog = [];
@@ -4417,6 +4559,9 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
4417
4559
  handleBrowser(ws, snapshotBroadcaster, browsers, eventLog);
4418
4560
  } else {
4419
4561
  const token = new URL(req.url ?? "/", `http://${host}:${port}`).searchParams.get("token");
4562
+ const authToken = token ? mutableAuth.clientTokenObjs.get(hqTokenVerifier2(token)) : void 0;
4563
+ clientSocketTokens.set(ws, authToken);
4564
+ ws.once("close", () => clientSocketTokens.delete(ws));
4420
4565
  handleClient(
4421
4566
  ws,
4422
4567
  clients,
@@ -4427,7 +4572,7 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
4427
4572
  // so the presented secret is hashed before lookup. Indexing with the
4428
4573
  // raw value silently returned undefined, which downgraded a scoped
4429
4574
  // token to "no capabilities record" at the capability gate.
4430
- ...token ? { token: mutableAuth.clientTokenObjs.get(hqTokenVerifier2(token)) } : {},
4575
+ ...authToken ? { token: authToken } : {},
4431
4576
  getOperatorPolicy: () => mutableAuth.operatorPolicyOverride
4432
4577
  },
4433
4578
  snapshotBroadcaster,
@@ -4441,7 +4586,29 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
4441
4586
  const authWatcher = watchHqAuthFile(
4442
4587
  dataDir,
4443
4588
  (next) => {
4589
+ const previousBrowserTokens = mutableAuth.browserTokens;
4590
+ const previousPasswordHash = mutableAuth.passwordHash;
4591
+ const previousCookieSecret = mutableAuth.cookieSecret;
4444
4592
  authState.apply(next);
4593
+ const browserTokensChanged = previousBrowserTokens.size !== mutableAuth.browserTokens.size || [...previousBrowserTokens].some((token) => !mutableAuth.browserTokens.has(token));
4594
+ const passwordChanged = previousPasswordHash !== mutableAuth.passwordHash;
4595
+ const cookieSecretChanged = previousCookieSecret !== mutableAuth.cookieSecret;
4596
+ if (browserTokensChanged || passwordChanged || cookieSecretChanged) {
4597
+ const liveBrowserTokenIds = new Set(
4598
+ [...mutableAuth.browserTokenObjs.values()].map((token) => token.id)
4599
+ );
4600
+ for (const [sessionId, session] of sessions) {
4601
+ if (passwordChanged || cookieSecretChanged || session.kind === "token" && (session.tokenId === void 0 || !liveBrowserTokenIds.has(session.tokenId))) {
4602
+ sessions.delete(sessionId);
4603
+ }
4604
+ }
4605
+ for (const browser of browsers) browser.close(1008, "Browser authentication changed");
4606
+ }
4607
+ const clientAuthRequired = hqClientAuthRequired(mutableAuth);
4608
+ for (const [clientSocket, authToken] of clientSocketTokens) {
4609
+ const stillAuthorized = authToken === void 0 ? !clientAuthRequired : !isTokenExpired4(authToken) && mutableAuth.clientTokenObjs.has(hqTokenKey2(authToken));
4610
+ if (!stillAuthorized) clientSocket.close(1008, "Client authentication revoked");
4611
+ }
4445
4612
  if ((options.requireBrowserAuth || mutableAuth.requireAuthFloor) && mutableAuth.browserTokens.size === 0 && mutableAuth.passwordHash === void 0) {
4446
4613
  sessions.clear();
4447
4614
  for (const browser of browsers) browser.close(1008, "Browser authentication removed");
@@ -4606,4 +4773,4 @@ export {
4606
4773
  startHqServer,
4607
4774
  HqInsecureExposureError2 as HqInsecureExposureError
4608
4775
  };
4609
- //# sourceMappingURL=chunk-2KDOFTTM.js.map
4776
+ //# sourceMappingURL=chunk-LUAFQIXT.js.map