@wrongstack/cli 0.302.0 → 0.302.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/providers.json +41 -155
- package/dist/{acp-DATHHPNT.js → acp-XCE4JJF3.js} +4 -4
- package/dist/{audit-QRCLEHHW.js → audit-BBR22QH3.js} +2 -2
- package/dist/{auth-PJD3JRH2.js → auth-K7CYVVKH.js} +121 -78
- package/dist/auth-menu/index.d.ts +5 -5
- package/dist/auth-menu/local.d.ts +4 -5
- package/dist/auth-menu/openai-codex-oauth.d.ts +1 -1
- package/dist/{auto-discover-providers-DRW4OZHG.js → auto-discover-providers-MHFLDSD3.js} +2 -2
- package/dist/{bench-7ZDWSNEF.js → bench-XNDUJOSZ.js} +10 -11
- package/dist/boot/auto-discover-providers.d.ts +1 -1
- package/dist/boot/container-wiring.d.ts +3 -3
- package/dist/boot/dispatch-webui.d.ts +1 -1
- package/dist/boot/system-prompt.d.ts +1 -2
- package/dist/boot/tool-registry.d.ts +1 -1
- package/dist/boot/tui-coordinator-setup.d.ts +1 -1
- package/dist/boot/tui-project-picker-callback.d.ts +1 -1
- package/dist/boot-config.d.ts +2 -2
- package/dist/boot.d.ts +1 -1
- package/dist/{chunk-ETOEGL3V.js → chunk-3IC4IEZC.js} +7 -2
- package/dist/{chunk-PFVVUH5B.js → chunk-3JPTNADJ.js} +3 -2
- package/dist/{chunk-4GVH7V3S.js → chunk-52C7PD2H.js} +1 -1
- package/dist/{chunk-OP47B3IX.js → chunk-7YLN7YUA.js} +7 -2
- package/dist/{chunk-263G3FMB.js → chunk-B4JLSXZB.js} +11 -14
- package/dist/{chunk-I5MEUFDI.js → chunk-BXQG3H2Y.js} +8 -3
- package/dist/{chunk-MT6FGXO3.js → chunk-C5NXM2SI.js} +19 -6
- package/dist/{chunk-XTIXVJXP.js → chunk-DGXNL7OT.js} +1 -1
- package/dist/{chunk-3MOUBRKZ.js → chunk-FJVCVZIV.js} +974 -935
- package/dist/{chunk-MRLYDHQ7.js → chunk-JQURRYU4.js} +4 -6
- package/dist/{chunk-ZECLGUTF.js → chunk-KV6DNQT6.js} +2 -2
- package/dist/{chunk-F4PEGAJZ.js → chunk-NUIHRGPY.js} +1 -1
- package/dist/{chunk-GUHQQG63.js → chunk-RK7E3IXT.js} +2 -4
- package/dist/{chunk-PYTFS4MC.js → chunk-SSSJQVUY.js} +27 -16
- package/dist/{chunk-PJHET45A.js → chunk-SZ42FYPT.js} +3 -3
- package/dist/{chunk-2KDOFTTM.js → chunk-WOXUE2CN.js} +1067 -900
- package/dist/{chunk-A6Q5KGLV.js → chunk-YKBRFSGL.js} +5 -5
- package/dist/cli-context.d.ts +2 -2
- package/dist/cli-eternal-flag.d.ts +1 -2
- package/dist/{cli-main-EOJ74CI4.js → cli-main-PJMDZHDK.js} +2680 -2133
- package/dist/context-limit.d.ts +11 -5
- package/dist/{diag-doctor-OQ3AA63O.js → diag-doctor-ZF5BF2MK.js} +2 -3
- package/dist/execute-deps.d.ts +1 -1
- package/dist/{execution-CQ772VPS.js → execution-LZRYRGDD.js} +234 -143
- package/dist/{export-DOBGPY23.js → export-CCUXRJOU.js} +3 -11
- package/dist/fleet/host-supervisor.d.ts +1 -1
- package/dist/fleet/host.d.ts +1 -1
- package/dist/fleet/routing.d.ts +1 -2
- package/dist/goal-host.d.ts +2 -2
- package/dist/hooks-wiring.d.ts +1 -1
- package/dist/{hq-LN5CJHCM.js → hq-AHPAE7EN.js} +104 -55
- package/dist/hq-command-controller.d.ts +1 -1
- package/dist/hq-server/routes/system-handlers.d.ts +1 -2
- package/dist/hq-server/routes.d.ts +7 -7
- package/dist/{hq-server-S7HZG3PA.js → hq-server-KU2Z54QU.js} +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2635 -2642
- package/dist/mailbox-bridge-bootstrap.d.ts +2 -2
- package/dist/{mcp-OHPJH73U.js → mcp-E5EBGOHF.js} +6 -6
- package/dist/mcp-serve.d.ts +1 -1
- package/dist/{modeldiag-3DOK4BWW.js → modeldiag-SIVUOX2Y.js} +18 -13
- package/dist/{permissions-FNYLZIL4.js → permissions-2PWBL4YU.js} +5 -3
- package/dist/picker.d.ts +1 -1
- package/dist/plugin-management.d.ts +1 -1
- package/dist/{plugin-usage-YEVLOR33.js → plugin-usage-PTEETL3J.js} +2 -2
- package/dist/{plugins-K3BAUP4D.js → plugins-XRGWC3NJ.js} +3 -3
- package/dist/pre-launch.d.ts +2 -2
- package/dist/{providers-models-WEZ4EGLL.js → providers-models-VV74TRQ2.js} +86 -37
- package/dist/repl.d.ts +1 -1
- package/dist/{replay-FCBNHW3K.js → replay-WBRMEWQ5.js} +2 -2
- package/dist/{rewind-AOD3JIYX.js → rewind-7RE6ONIC.js} +17 -17
- package/dist/{sdd-runtime-AIJCJGWC.js → sdd-runtime-GFZ3NJKS.js} +2 -2
- package/dist/{sessions-config-XXN7267N.js → sessions-config-YX2ZPAFM.js} +36 -32
- package/dist/{simpleui-dist-JIFGUJO7.js → simpleui-dist-33VO3QHF.js} +9 -2
- package/dist/slash-commands/agents.d.ts +14 -0
- package/dist/slash-commands/command-context.d.ts +2 -3
- package/dist/slash-commands/helpers.d.ts +1 -1
- package/dist/slash-commands/index.d.ts +1 -1
- package/dist/slash-commands/intake.d.ts +10 -0
- package/dist/slash-commands/memory-triage.d.ts +1 -1
- package/dist/slash-commands/provider-status.d.ts +1 -1
- package/dist/subcommands/handlers/hq.d.ts +20 -0
- package/dist/subcommands/handlers/per-subcommand-help-data.d.ts +1 -1
- package/dist/{update-MV6TUACD.js → update-LTDJGAFU.js} +3 -3
- package/dist/{update-check-UMYTOH7Z.js → update-check-WARHSVZA.js} +2 -2
- package/dist/webui-server/connection-handler.d.ts +1 -1
- package/dist/webui-server/context-breakdown.d.ts +1 -1
- package/dist/webui-server/cost-helpers.d.ts +1 -1
- package/dist/webui-server/lifecycle.d.ts +1 -1
- package/dist/webui-server/prefs-seeding.d.ts +1 -1
- package/dist/webui-server/setup-events.d.ts +1 -1
- package/dist/webui-server/static-serve.d.ts +1 -1
- package/dist/{webui-server-NFNRGNNT.js → webui-server-E3OPEKU5.js} +18 -10
- package/dist/webui-server-options.d.ts +1 -1
- package/dist/wiring/command-host-state.d.ts +2 -2
- package/dist/wiring/controllers.d.ts +1 -2
- package/dist/wiring/dep-watcher.d.ts +1 -1
- package/dist/wiring/director-setup.d.ts +3 -3
- package/dist/wiring/eternal-command-handlers.d.ts +2 -2
- package/dist/wiring/management-tools.d.ts +2 -2
- package/dist/wiring/provider-runtime.d.ts +1 -1
- package/dist/wiring/provider-status.d.ts +1 -1
- package/dist/wiring/provider-utility-tools.d.ts +3 -3
- package/dist/wiring/provider.d.ts +1 -1
- package/dist/wiring/replay.d.ts +1 -1
- package/dist/wiring/runtime-controller-deps.d.ts +1 -1
- package/dist/wiring/runtime-dispatch-state.d.ts +1 -2
- package/dist/wiring/runtime-lifecycle-deps.d.ts +2 -2
- package/dist/wiring/runtime-picker-deps.d.ts +1 -2
- package/dist/wiring/sage.d.ts +1 -1
- package/dist/wiring/session-command-handlers.d.ts +1 -2
- package/dist/wiring/session.d.ts +6 -1
- package/dist/wiring/slash-commands.d.ts +2 -3
- 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/
|
|
758
|
-
import {
|
|
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 {
|
|
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 =
|
|
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/
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
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
|
-
|
|
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
|
-
|
|
971
|
-
|
|
972
|
-
return
|
|
973
|
-
} catch {
|
|
974
|
-
return false;
|
|
1085
|
+
if (!callerCanAdministerAuth(auth)) {
|
|
1086
|
+
writeAuthAdminRequired(res);
|
|
1087
|
+
return void 0;
|
|
975
1088
|
}
|
|
1089
|
+
return auth;
|
|
976
1090
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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: {
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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, {
|
|
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(
|
|
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(
|
|
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(
|
|
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, {
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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(
|
|
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(
|
|
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({
|
|
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-
|
|
2231
|
-
import("./update-
|
|
2000
|
+
import("./update-check-WARHSVZA.js"),
|
|
2001
|
+
import("./update-LTDJGAFU.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
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
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
|
-
|
|
2260
|
-
|
|
2104
|
+
ws.send(data);
|
|
2105
|
+
return true;
|
|
2261
2106
|
} catch {
|
|
2262
|
-
|
|
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
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
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
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
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
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
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
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
const
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
}
|
|
2367
|
-
const
|
|
2368
|
-
|
|
2369
|
-
const
|
|
2370
|
-
const
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
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
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
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
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
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
|
-
|
|
2428
|
-
const
|
|
2429
|
-
const
|
|
2430
|
-
const
|
|
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
|
-
|
|
2445
|
-
const
|
|
2446
|
-
const
|
|
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/
|
|
2464
|
-
import {
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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" : [
|
|
3023
|
-
|
|
3024
|
-
|
|
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
|
-
...
|
|
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-
|
|
4776
|
+
//# sourceMappingURL=chunk-WOXUE2CN.js.map
|