@wrongstack/cli 1.0.3 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{acp-JK4ZQO3R.js → acp-GOQURNC2.js} +128 -43
- package/dist/acp-agent-list.d.ts +14 -0
- package/dist/acp-hq-telemetry.d.ts +9 -2
- package/dist/acp-mcp-servers.d.ts +49 -0
- package/dist/acp-server-agent.d.ts +18 -6
- package/dist/{auth-XD3GN5RJ.js → auth-WBXXIQUO.js} +3 -3
- package/dist/boot/short-circuit-desktop.d.ts +1 -0
- package/dist/boot/short-circuit-hq.d.ts +2 -0
- package/dist/boot/tool-registry.d.ts +18 -1
- package/dist/{chunk-JHPXJRRI.js → chunk-4XCBEUBX.js} +13 -3
- package/dist/{chunk-ONOT5OJ2.js → chunk-5A5GCSJC.js} +1 -1
- package/dist/chunk-5EA6OTLJ.js +239 -0
- package/dist/{chunk-O4A3HHKD.js → chunk-672UXYL3.js} +2 -2
- package/dist/{chunk-FPIWTJ2G.js → chunk-6WRKAACA.js} +4 -4
- package/dist/chunk-AUQDGU5D.js +32 -0
- package/dist/{chunk-VOTTENR5.js → chunk-CDY4D5QK.js} +2 -2
- package/dist/{chunk-E6RFXHWD.js → chunk-EK2P53GL.js} +3 -2
- package/dist/{chunk-MXEJEYT7.js → chunk-JX6KL7OF.js} +15 -5
- package/dist/{chunk-GMY45LO6.js → chunk-KEPABQYT.js} +101 -2
- package/dist/{chunk-IMWXXF4J.js → chunk-KNMIBKLV.js} +79 -1
- package/dist/{chunk-4NHD5OXJ.js → chunk-NR7BD73C.js} +5 -5
- package/dist/{chunk-C2E24IXZ.js → chunk-PQZPHGGD.js} +272 -299
- package/dist/chunk-Q7E3BPDU.js +62 -0
- package/dist/{chunk-IUEKNHPT.js → chunk-QDQPF4PB.js} +11 -11
- package/dist/{chunk-VFCJOWJB.js → chunk-VPS2SJW4.js} +4 -5
- package/dist/{chunk-U6KCWRMC.js → chunk-XJ7K67PP.js} +5 -1
- package/dist/{cli-context-Q6QP534K.js → cli-context-BYKJDAFF.js} +129 -25
- package/dist/{cli-main-JQXCDZEA.js → cli-main-5WZBH3WA.js} +1345 -800
- package/dist/{diag-doctor-7LYUYU35.js → diag-doctor-PCER6KHH.js} +2 -2
- package/dist/{execution-KH7WJNAX.js → execution-Z6A26EG6.js} +132 -40
- package/dist/execution-specialist-trigger.d.ts +28 -0
- package/dist/fleet/acp-live-progress.d.ts +19 -0
- package/dist/fleet/host-acp-runner-cache.d.ts +3 -1
- package/dist/fleet/host-acp.d.ts +14 -2
- package/dist/fleet/host.d.ts +1 -0
- package/dist/{hq-IG3YRGCO.js → hq-V4PHEETW.js} +127 -8
- package/dist/hq-command-controller.d.ts +31 -2
- package/dist/hq-server/auth-state.d.ts +6 -0
- package/dist/hq-server/auth.d.ts +10 -1
- package/dist/hq-server/client-address.d.ts +2 -0
- package/dist/hq-server/ip-allowlist.d.ts +16 -0
- package/dist/hq-server/preflight.d.ts +1 -0
- package/dist/hq-server/routes/auth/common.d.ts +34 -0
- package/dist/hq-server/routes/auth/password-routes.d.ts +4 -2
- package/dist/hq-server/routes.d.ts +5 -4
- package/dist/hq-server/secret-input.d.ts +7 -0
- package/dist/hq-server/types.d.ts +21 -2
- package/dist/hq-server/upgrade-handler.d.ts +2 -0
- package/dist/hq-server/utils.d.ts +6 -0
- package/dist/{hq-server-OILU3NPI.js → hq-server-MZ7DV6X7.js} +4 -3
- package/dist/hq-server.d.ts +4 -0
- package/dist/hq-service-L5INQFM4.js +507 -0
- package/dist/hq-service.d.ts +22 -0
- package/dist/index.js +9 -6
- package/dist/ip-allowlist-NHXO6AKJ.js +9 -0
- package/dist/live-settings-input.d.ts +2 -0
- package/dist/{mcp-HOMLC4WE.js → mcp-FX7TKPTV.js} +10 -8
- package/dist/mcp-serve.d.ts +31 -0
- package/dist/{per-subcommand-help-BWBHYVJO.js → per-subcommand-help-DHTYU524.js} +2 -2
- package/dist/{plugins-U7KGQAUG.js → plugins-WJLW6ULV.js} +2 -2
- package/dist/projects-LMNAGGEU.js +36 -0
- package/dist/{proxy-wiring-MGRSKZZB.js → proxy-wiring-K2DORFFL.js} +3 -3
- package/dist/short-circuit-flags-KBTYSJTW.js +10 -0
- package/dist/slash-commands/acp.d.ts +3 -0
- package/dist/slash-commands/command-context.d.ts +22 -0
- package/dist/slash-commands/ensemble.d.ts +1 -1
- package/dist/slash-commands/profile.d.ts +7 -0
- package/dist/subcommands/handlers/hq.d.ts +1 -0
- package/dist/subcommands/handlers/projects.d.ts +14 -0
- package/dist/{subcommands-CUTGBDKP.js → subcommands-DEYSYHIM.js} +2 -2
- package/dist/{version-help-MYKF724O.js → version-help-WXI42PZ3.js} +2 -2
- package/dist/{webui-server-QWBUT7BO.js → webui-server-OBV5JYJV.js} +9 -3
- package/dist/wiring/cli-slash-commands-setup.d.ts +7 -0
- package/dist/wiring/codebase-embeddings.d.ts +2 -2
- package/dist/wiring/command-host-state.d.ts +7 -0
- package/dist/wiring/pipeline.d.ts +2 -0
- package/dist/wiring/provider-runtime-setup.d.ts +1 -1
- package/dist/wiring/session-command-handlers.d.ts +2 -1
- package/dist/wiring/slash-commands.d.ts +1 -0
- package/package.json +28 -28
- package/dist/chunk-Q5GTM25S.js +0 -17
- package/dist/projects-L6C6XLDY.js +0 -37
- package/dist/short-circuit-flags-ZNZX44MC.js +0 -10
|
@@ -3,14 +3,37 @@ import {
|
|
|
3
3
|
terminalText
|
|
4
4
|
} from "./chunk-HXJYHAR4.js";
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
MAX_AGENT_RINGS,
|
|
7
|
+
MAX_TRANSCRIPT_SESSIONS,
|
|
8
|
+
TRANSCRIPT_RING_MAX,
|
|
9
|
+
agentMessageToEntry,
|
|
10
|
+
agentRingKey,
|
|
11
|
+
buildBootstrapHttpUrl,
|
|
12
|
+
buildClientWsUrl,
|
|
13
|
+
buildHttpUrl,
|
|
14
|
+
decodePathSegment,
|
|
15
|
+
decodeSessionId,
|
|
16
|
+
evictOldest,
|
|
17
|
+
hqMachineKey,
|
|
18
|
+
hqRuntimeMarkerPath,
|
|
19
|
+
lanIPv4Addresses,
|
|
20
|
+
readLocalSubagentTranscript,
|
|
21
|
+
readRequestBody,
|
|
22
|
+
recordTimeseriesSignal,
|
|
23
|
+
resolveAuditActor,
|
|
24
|
+
sanitizeApiError,
|
|
25
|
+
truncateHqSummary,
|
|
26
|
+
writeInvalidBody
|
|
27
|
+
} from "./chunk-5EA6OTLJ.js";
|
|
28
|
+
import {
|
|
29
|
+
createHqIpAllowlist
|
|
30
|
+
} from "./chunk-Q7E3BPDU.js";
|
|
8
31
|
|
|
9
32
|
// src/hq-server.ts
|
|
10
33
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
11
|
-
import * as
|
|
34
|
+
import * as fs4 from "node:fs/promises";
|
|
12
35
|
import * as http from "node:http";
|
|
13
|
-
import * as
|
|
36
|
+
import * as path6 from "node:path";
|
|
14
37
|
import { getSharedProjectMailbox } from "@wrongstack/core/coordination";
|
|
15
38
|
import {
|
|
16
39
|
assessHqExposure as assessHqExposure2,
|
|
@@ -21,7 +44,7 @@ import {
|
|
|
21
44
|
hqTokenKey as hqTokenKey2,
|
|
22
45
|
isTokenExpired as isTokenExpired4,
|
|
23
46
|
mintHqCookieSecret as mintHqCookieSecret2,
|
|
24
|
-
mutateHqAuthFile as
|
|
47
|
+
mutateHqAuthFile as mutateHqAuthFile4,
|
|
25
48
|
toAlertMessage,
|
|
26
49
|
watchHqAuthFile
|
|
27
50
|
} from "@wrongstack/core/hq";
|
|
@@ -112,10 +135,15 @@ function hasTrustedRequestAuthority(req, boundHost, boundPort, trustedPublicOrig
|
|
|
112
135
|
return false;
|
|
113
136
|
}
|
|
114
137
|
}
|
|
115
|
-
function hasTrustedBrowserOrigin(req, boundHost, boundPort, trustedPublicOrigins = /* @__PURE__ */ new Set(), allowFileOrigin = false) {
|
|
138
|
+
function hasTrustedBrowserOrigin(req, boundHost, boundPort, trustedPublicOrigins = /* @__PURE__ */ new Set(), allowFileOrigin = false, credentialedSurface = false) {
|
|
116
139
|
if (!hasTrustedRequestAuthority(req, boundHost, boundPort, trustedPublicOrigins)) return false;
|
|
117
140
|
const origin = req.headers.origin;
|
|
118
|
-
if (origin === void 0)
|
|
141
|
+
if (origin === void 0) {
|
|
142
|
+
if (!credentialedSurface) {
|
|
143
|
+
return boundHost !== void 0 && isLoopbackHost(boundHost);
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
119
147
|
try {
|
|
120
148
|
const parsed = new URL(origin);
|
|
121
149
|
if (parsed.protocol === "file:") return allowFileOrigin;
|
|
@@ -199,7 +227,7 @@ function isCookieAuth(auth) {
|
|
|
199
227
|
return auth !== void 0 && auth.kind === "cookie";
|
|
200
228
|
}
|
|
201
229
|
function hqAuthRequired(mutableAuth, requireBrowserAuth) {
|
|
202
|
-
return requireBrowserAuth === true || mutableAuth.requireAuthFloor === true || mutableAuth.browserTokens.size > 0 || mutableAuth.passwordHash !== void 0;
|
|
230
|
+
return (requireBrowserAuth ?? mutableAuth.requireBrowserAuth) === true || mutableAuth.requireAuthFloor === true || mutableAuth.browserTokens.size > 0 || mutableAuth.passwordHash !== void 0;
|
|
203
231
|
}
|
|
204
232
|
function hqClientAuthRequired(mutableAuth) {
|
|
205
233
|
return mutableAuth.requireAuthFloor === true || mutableAuth.clientTokens.size > 0;
|
|
@@ -290,7 +318,7 @@ function authenticateBrowserRequest(req, url, mutableAuth, sessions) {
|
|
|
290
318
|
if (liveToken.capabilities !== void 0) ctx.capabilities = liveToken.capabilities;
|
|
291
319
|
return ctx;
|
|
292
320
|
}
|
|
293
|
-
return { kind: "cookie" };
|
|
321
|
+
return session.capabilities === void 0 ? { kind: "cookie" } : { kind: "cookie", capabilities: session.capabilities };
|
|
294
322
|
}
|
|
295
323
|
if (session) sessions.delete(sessionId);
|
|
296
324
|
}
|
|
@@ -341,7 +369,8 @@ function createHqAuthState(authFile, dataDir, opts = {}) {
|
|
|
341
369
|
clientTokens: /* @__PURE__ */ new Set(),
|
|
342
370
|
browserTokenObjs: /* @__PURE__ */ new Map(),
|
|
343
371
|
clientTokenObjs: /* @__PURE__ */ new Map(),
|
|
344
|
-
alertRules: void 0
|
|
372
|
+
alertRules: void 0,
|
|
373
|
+
requireBrowserAuth: opts.requireBrowserAuth
|
|
345
374
|
};
|
|
346
375
|
projectAuthFile(mutableAuth, authFile);
|
|
347
376
|
return {
|
|
@@ -609,6 +638,7 @@ var MIME = {
|
|
|
609
638
|
".mjs": "application/javascript; charset=utf-8",
|
|
610
639
|
".css": "text/css; charset=utf-8",
|
|
611
640
|
".json": "application/json; charset=utf-8",
|
|
641
|
+
".webmanifest": "application/manifest+json; charset=utf-8",
|
|
612
642
|
".svg": "image/svg+xml",
|
|
613
643
|
".png": "image/png",
|
|
614
644
|
".jpg": "image/jpeg",
|
|
@@ -703,7 +733,32 @@ async function serveHqStatic(_req, res, urlPath, distDir) {
|
|
|
703
733
|
}
|
|
704
734
|
|
|
705
735
|
// src/hq-server/routes/auth/common.ts
|
|
706
|
-
import { isLoopbackHost as isLoopbackHost2 } from "@wrongstack/core/hq";
|
|
736
|
+
import { isLoopbackHost as isLoopbackHost2, mutateHqAuthFile } from "@wrongstack/core/hq";
|
|
737
|
+
import { verifyTotpCounter } from "@wrongstack/core/security";
|
|
738
|
+
function recordVerifyFailure(loginAttempts, clientIp) {
|
|
739
|
+
const prev = loginAttempts.get(clientIp);
|
|
740
|
+
const count = (prev?.count ?? 0) + 1;
|
|
741
|
+
loginAttempts.recordFailure(clientIp);
|
|
742
|
+
return count;
|
|
743
|
+
}
|
|
744
|
+
async function consumeTotpCode(code, mutableAuth, dataDir, applyAuthFile) {
|
|
745
|
+
if (!mutableAuth.totpSecret) return "invalid";
|
|
746
|
+
const matchedCounter = verifyTotpCounter(code, mutableAuth.totpSecret);
|
|
747
|
+
if (matchedCounter === void 0) return "invalid";
|
|
748
|
+
if (mutableAuth.totpLastUsedCounter !== void 0 && matchedCounter <= mutableAuth.totpLastUsedCounter) {
|
|
749
|
+
return "replayed";
|
|
750
|
+
}
|
|
751
|
+
mutableAuth.totpLastUsedCounter = matchedCounter;
|
|
752
|
+
try {
|
|
753
|
+
const next = await mutateHqAuthFile(dataDir, (current) => ({
|
|
754
|
+
...current,
|
|
755
|
+
totpLastUsedCounter: matchedCounter
|
|
756
|
+
}));
|
|
757
|
+
applyAuthFile(next);
|
|
758
|
+
} catch {
|
|
759
|
+
}
|
|
760
|
+
return "ok";
|
|
761
|
+
}
|
|
707
762
|
function isLoopbackRequest(req) {
|
|
708
763
|
const address = req.socket.remoteAddress?.replace(/^::ffff:/, "");
|
|
709
764
|
return address !== void 0 && isLoopbackHost2(address);
|
|
@@ -711,7 +766,6 @@ function isLoopbackRequest(req) {
|
|
|
711
766
|
var HQ_AUTH_ADMIN_CAPABILITY = "auth.admin";
|
|
712
767
|
function callerCanAdministerAuth(auth) {
|
|
713
768
|
if (auth === void 0) return false;
|
|
714
|
-
if (auth.kind === "cookie" && auth.tokenId === void 0) return true;
|
|
715
769
|
if (auth.capabilities === void 0) return true;
|
|
716
770
|
return auth.capabilities.includes(HQ_AUTH_ADMIN_CAPABILITY);
|
|
717
771
|
}
|
|
@@ -752,7 +806,8 @@ import { randomUUID } from "node:crypto";
|
|
|
752
806
|
import {
|
|
753
807
|
hashHqPassword,
|
|
754
808
|
mintHqCookieSecret,
|
|
755
|
-
mutateHqAuthFile,
|
|
809
|
+
mutateHqAuthFile as mutateHqAuthFile2,
|
|
810
|
+
hqPasswordNeedsUpgrade,
|
|
756
811
|
verifyHqPassword
|
|
757
812
|
} from "@wrongstack/core/hq";
|
|
758
813
|
|
|
@@ -762,6 +817,9 @@ function normalizeAddress(raw) {
|
|
|
762
817
|
const trimmed = raw.trim().replace(/^\[(.*)\]$/, "$1");
|
|
763
818
|
return trimmed.replace(/^::ffff:/i, "");
|
|
764
819
|
}
|
|
820
|
+
function resolveSocketAddress(req) {
|
|
821
|
+
return req.socket.remoteAddress ? normalizeAddress(req.socket.remoteAddress) : void 0;
|
|
822
|
+
}
|
|
765
823
|
function parseForwardedEntry(entry) {
|
|
766
824
|
const value = entry.trim();
|
|
767
825
|
if (!value) return void 0;
|
|
@@ -786,7 +844,7 @@ function forwardedChain(req) {
|
|
|
786
844
|
return raw.split(",").map((entry) => parseForwardedEntry(entry)).filter((entry) => entry !== void 0);
|
|
787
845
|
}
|
|
788
846
|
function resolveClientAddress(req, trustedProxyHops = 0) {
|
|
789
|
-
const socketAddress =
|
|
847
|
+
const socketAddress = resolveSocketAddress(req);
|
|
790
848
|
const hops = Number.isFinite(trustedProxyHops) ? Math.max(0, Math.trunc(trustedProxyHops)) : 0;
|
|
791
849
|
if (hops === 0) return socketAddress ?? "unknown";
|
|
792
850
|
const chain = [
|
|
@@ -797,201 +855,6 @@ function resolveClientAddress(req, trustedProxyHops = 0) {
|
|
|
797
855
|
return chain[hops] ?? socketAddress ?? "unknown";
|
|
798
856
|
}
|
|
799
857
|
|
|
800
|
-
// src/hq-server/utils.ts
|
|
801
|
-
import * as fs3 from "node:fs/promises";
|
|
802
|
-
import * as os from "node:os";
|
|
803
|
-
import * as path3 from "node:path";
|
|
804
|
-
import { resolveHqDataDir } from "@wrongstack/core/hq";
|
|
805
|
-
import { sanitizeApiError, scrubErrorDetail } from "@wrongstack/core/security";
|
|
806
|
-
var TRANSCRIPT_RING_MAX = 4e3;
|
|
807
|
-
var MAX_TRANSCRIPT_SESSIONS = 400;
|
|
808
|
-
var MAX_AGENT_RINGS = 800;
|
|
809
|
-
function evictOldest(map, max) {
|
|
810
|
-
while (map.size > max) {
|
|
811
|
-
const oldest = map.keys().next().value;
|
|
812
|
-
if (oldest === void 0) break;
|
|
813
|
-
map.delete(oldest);
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
function agentRingKey(sessionId, subId) {
|
|
817
|
-
return sessionId ? `${sessionId}::${subId}` : subId;
|
|
818
|
-
}
|
|
819
|
-
function agentMessageToEntry(p) {
|
|
820
|
-
const kind = typeof p["kind"] === "string" ? p["kind"] : "text";
|
|
821
|
-
const role = kind === "thinking" ? "thinking" : kind === "tool_use" || kind === "tool_result" ? "tool" : kind === "error" ? "error" : kind === "status" || kind === "system" ? "system" : "assistant";
|
|
822
|
-
return {
|
|
823
|
-
ts: typeof p["ts"] === "string" ? p["ts"] : (/* @__PURE__ */ new Date()).toISOString(),
|
|
824
|
-
role,
|
|
825
|
-
text: typeof p["content"] === "string" ? p["content"] : "",
|
|
826
|
-
...typeof p["toolName"] === "string" ? { tool: p["toolName"] } : {},
|
|
827
|
-
...kind === "error" ? { isError: true } : {}
|
|
828
|
-
};
|
|
829
|
-
}
|
|
830
|
-
async function readLocalSubagentTranscript(sessionId, subagentId) {
|
|
831
|
-
try {
|
|
832
|
-
const { getSessionRegistry } = await import("@wrongstack/core/storage");
|
|
833
|
-
const { resolveWstackPaths, sessionScopedPath } = await import("@wrongstack/core/utils");
|
|
834
|
-
const globalRoot = path3.dirname(resolveHqDataDir());
|
|
835
|
-
const registry = getSessionRegistry(globalRoot);
|
|
836
|
-
const entry = await registry.get(sessionId).catch(() => null);
|
|
837
|
-
if (!entry) return null;
|
|
838
|
-
const paths = resolveWstackPaths({ projectRoot: entry.projectRoot, globalRoot });
|
|
839
|
-
const sessionDir = sessionScopedPath(paths.projectSessions, sessionId, "");
|
|
840
|
-
if (!isSafePathSegment(subagentId)) return null;
|
|
841
|
-
const transcriptsRoot = path3.resolve(sessionDir, "subagents", "transcripts");
|
|
842
|
-
const file = path3.resolve(transcriptsRoot, subagentId, "transcript.jsonl");
|
|
843
|
-
if (!file.startsWith(transcriptsRoot + path3.sep)) return null;
|
|
844
|
-
const raw = await fs3.readFile(file, "utf8").catch(() => null);
|
|
845
|
-
if (raw === null) return null;
|
|
846
|
-
const out = [];
|
|
847
|
-
for (const line of raw.split("\n")) {
|
|
848
|
-
const trimmed = line.trim();
|
|
849
|
-
if (trimmed.length === 0) continue;
|
|
850
|
-
try {
|
|
851
|
-
out.push(agentMessageToEntry(JSON.parse(trimmed)));
|
|
852
|
-
} catch {
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
return out;
|
|
856
|
-
} catch {
|
|
857
|
-
return null;
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
function decodePathSegment(value) {
|
|
861
|
-
let decoded;
|
|
862
|
-
try {
|
|
863
|
-
decoded = decodeURIComponent(value);
|
|
864
|
-
} catch {
|
|
865
|
-
return null;
|
|
866
|
-
}
|
|
867
|
-
return isSafePathSegment(decoded) ? decoded : null;
|
|
868
|
-
}
|
|
869
|
-
function decodeSessionId(value) {
|
|
870
|
-
let decoded;
|
|
871
|
-
try {
|
|
872
|
-
decoded = decodeURIComponent(value);
|
|
873
|
-
} catch {
|
|
874
|
-
return null;
|
|
875
|
-
}
|
|
876
|
-
return isSafeSessionId(decoded) ? decoded : null;
|
|
877
|
-
}
|
|
878
|
-
var MAX_PATH_SEGMENT_LENGTH = 256;
|
|
879
|
-
function isSafePathSegment(value) {
|
|
880
|
-
if (value.length === 0 || value.length > MAX_PATH_SEGMENT_LENGTH) return false;
|
|
881
|
-
if (value === "." || value === "..") return false;
|
|
882
|
-
if (value.includes("/") || value.includes("\\")) return false;
|
|
883
|
-
if (value.includes("\0")) return false;
|
|
884
|
-
if (value.includes(":")) return false;
|
|
885
|
-
return true;
|
|
886
|
-
}
|
|
887
|
-
function isSafeSessionId(value) {
|
|
888
|
-
if (value.length === 0 || value.length > MAX_PATH_SEGMENT_LENGTH) return false;
|
|
889
|
-
if (value.includes("\\") || value.includes("\0") || value.includes(":")) return false;
|
|
890
|
-
const parts = value.split("/");
|
|
891
|
-
if (parts.length > 2) return false;
|
|
892
|
-
return parts.every((p) => p.length > 0 && p !== "." && p !== "..");
|
|
893
|
-
}
|
|
894
|
-
function displayHost(host) {
|
|
895
|
-
return host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
896
|
-
}
|
|
897
|
-
var RequestBodyTooLargeError = class extends Error {
|
|
898
|
-
};
|
|
899
|
-
function readRequestBody(req, maxBytes = 1e6) {
|
|
900
|
-
return new Promise((resolve2, reject) => {
|
|
901
|
-
const chunks = [];
|
|
902
|
-
let size = 0;
|
|
903
|
-
let settled = false;
|
|
904
|
-
req.on("data", (chunk) => {
|
|
905
|
-
if (settled) return;
|
|
906
|
-
size += chunk.length;
|
|
907
|
-
if (size > maxBytes) {
|
|
908
|
-
settled = true;
|
|
909
|
-
chunks.length = 0;
|
|
910
|
-
reject(new RequestBodyTooLargeError("request body too large"));
|
|
911
|
-
return;
|
|
912
|
-
}
|
|
913
|
-
chunks.push(chunk);
|
|
914
|
-
});
|
|
915
|
-
req.on("end", () => {
|
|
916
|
-
if (!settled) resolve2(Buffer.concat(chunks).toString("utf8"));
|
|
917
|
-
});
|
|
918
|
-
req.on("error", (error) => {
|
|
919
|
-
if (!settled) reject(error);
|
|
920
|
-
});
|
|
921
|
-
});
|
|
922
|
-
}
|
|
923
|
-
function writeInvalidBody(res, error) {
|
|
924
|
-
const tooLarge = error instanceof RequestBodyTooLargeError;
|
|
925
|
-
res.writeHead(tooLarge ? 413 : 400, { "Content-Type": "application/json" });
|
|
926
|
-
res.end(JSON.stringify({ error: tooLarge ? "request body too large" : "invalid json body" }));
|
|
927
|
-
}
|
|
928
|
-
function buildHttpUrl(host, port, token) {
|
|
929
|
-
const url = new URL(`http://${displayHost(host)}:${port}/`);
|
|
930
|
-
if (token) url.searchParams.set("token", token);
|
|
931
|
-
return url.toString();
|
|
932
|
-
}
|
|
933
|
-
function buildBootstrapHttpUrl(host, port, bootstrapCode) {
|
|
934
|
-
const url = new URL(`http://${displayHost(host)}:${port}/`);
|
|
935
|
-
url.hash = `bootstrap=${bootstrapCode}`;
|
|
936
|
-
return url.toString();
|
|
937
|
-
}
|
|
938
|
-
function buildClientWsUrl(host, port, token) {
|
|
939
|
-
const url = new URL(`ws://${displayHost(host)}:${port}/ws/client`);
|
|
940
|
-
if (token) url.searchParams.set("token", token);
|
|
941
|
-
return url.toString();
|
|
942
|
-
}
|
|
943
|
-
function hqRuntimeMarkerPath(dataDir) {
|
|
944
|
-
return path3.join(dataDir, "runtime.json");
|
|
945
|
-
}
|
|
946
|
-
function lanIPv4Addresses() {
|
|
947
|
-
const out = [];
|
|
948
|
-
try {
|
|
949
|
-
const ifaces = os.networkInterfaces();
|
|
950
|
-
for (const name of Object.keys(ifaces)) {
|
|
951
|
-
for (const ni of ifaces[name] ?? []) {
|
|
952
|
-
if (ni.family === "IPv4" && !ni.internal) out.push(ni.address);
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
} catch {
|
|
956
|
-
}
|
|
957
|
-
return out;
|
|
958
|
-
}
|
|
959
|
-
function truncateHqSummary(value, maxLength) {
|
|
960
|
-
if (typeof value !== "string" || value.length === 0) return void 0;
|
|
961
|
-
if (value.length <= maxLength) return value;
|
|
962
|
-
const suffix = `\u2026[truncated:${value.length - maxLength}]`;
|
|
963
|
-
if (maxLength <= suffix.length) return "\u2026";
|
|
964
|
-
const prefixLen = maxLength - suffix.length;
|
|
965
|
-
return `${value.slice(0, prefixLen)}${suffix}`;
|
|
966
|
-
}
|
|
967
|
-
function hqMachineKey(hostname, machineId) {
|
|
968
|
-
const hn = hostname?.trim();
|
|
969
|
-
return hn ? `host:${hn.toLowerCase()}` : `mid:${machineId || "local"}`;
|
|
970
|
-
}
|
|
971
|
-
function recordTimeseriesSignal(persistence, event) {
|
|
972
|
-
try {
|
|
973
|
-
if (event.type === "session.usage") {
|
|
974
|
-
const p = event.payload;
|
|
975
|
-
persistence.timeseries.record({
|
|
976
|
-
ts: Date.parse(event.timestamp) || Date.now(),
|
|
977
|
-
...typeof p.costUsd === "number" ? { costUsd: p.costUsd } : {},
|
|
978
|
-
...typeof p.inputTokens === "number" ? { inputTokens: p.inputTokens } : {},
|
|
979
|
-
...typeof p.outputTokens === "number" ? { outputTokens: p.outputTokens } : {},
|
|
980
|
-
...typeof p.model === "string" && p.model.length > 0 ? { model: p.model } : {},
|
|
981
|
-
...typeof p.provider === "string" && p.provider.length > 0 ? { provider: p.provider } : {},
|
|
982
|
-
...typeof p.cacheRead === "number" ? { cacheRead: p.cacheRead } : {},
|
|
983
|
-
...typeof p.cacheWrite === "number" ? { cacheWrite: p.cacheWrite } : {}
|
|
984
|
-
});
|
|
985
|
-
} else if (event.type === "tool.completed") {
|
|
986
|
-
persistence.timeseries.record({
|
|
987
|
-
ts: Date.parse(event.timestamp) || Date.now(),
|
|
988
|
-
toolCalls: 1
|
|
989
|
-
});
|
|
990
|
-
}
|
|
991
|
-
} catch {
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
|
|
995
858
|
// src/hq-server/routes/auth/password-routes.ts
|
|
996
859
|
async function handleApiAuthStatus(req, res, url, mutableAuth, sessions, requireBrowserAuth, trustedPublicOrigins, secureCookies) {
|
|
997
860
|
const auth = authenticateBrowserRequest(req, url, mutableAuth, sessions);
|
|
@@ -1013,7 +876,7 @@ async function handleApiAuthStatus(req, res, url, mutableAuth, sessions, require
|
|
|
1013
876
|
})
|
|
1014
877
|
);
|
|
1015
878
|
}
|
|
1016
|
-
async function handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, secureCookies, trustedProxyHops) {
|
|
879
|
+
async function handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, secureCookies, trustedProxyHops, dataDir, sessionCapabilities) {
|
|
1017
880
|
if (!mutableAuth.passwordHash) {
|
|
1018
881
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1019
882
|
res.end(
|
|
@@ -1064,6 +927,14 @@ async function handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, se
|
|
|
1064
927
|
return;
|
|
1065
928
|
}
|
|
1066
929
|
const ok = await verifyHqPassword(body.password, mutableAuth.passwordHash);
|
|
930
|
+
if (ok && hqPasswordNeedsUpgrade(mutableAuth.passwordHash)) {
|
|
931
|
+
try {
|
|
932
|
+
const upgraded = await hashHqPassword(body.password);
|
|
933
|
+
await mutateHqAuthFile2(dataDir, (current) => ({ ...current, passwordHash: upgraded }));
|
|
934
|
+
mutableAuth.passwordHash = upgraded;
|
|
935
|
+
} catch {
|
|
936
|
+
}
|
|
937
|
+
}
|
|
1067
938
|
if (!ok || !mutableAuth.cookieSecret) {
|
|
1068
939
|
loginAttempts.recordFailure(clientIp, body.password);
|
|
1069
940
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
@@ -1076,7 +947,8 @@ async function handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, se
|
|
|
1076
947
|
createdAt: Date.now(),
|
|
1077
948
|
kind: "password",
|
|
1078
949
|
pending2fa: true,
|
|
1079
|
-
lastSeenAt: Date.now()
|
|
950
|
+
lastSeenAt: Date.now(),
|
|
951
|
+
...sessionCapabilities !== void 0 ? { capabilities: sessionCapabilities } : {}
|
|
1080
952
|
});
|
|
1081
953
|
setHqSessionCookie(
|
|
1082
954
|
res,
|
|
@@ -1089,7 +961,12 @@ async function handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, se
|
|
|
1089
961
|
}
|
|
1090
962
|
loginAttempts.clearOnSuccess(clientIp, body.password);
|
|
1091
963
|
const sessionId = randomUUID();
|
|
1092
|
-
sessions.set(sessionId, {
|
|
964
|
+
sessions.set(sessionId, {
|
|
965
|
+
createdAt: Date.now(),
|
|
966
|
+
kind: "password",
|
|
967
|
+
lastSeenAt: Date.now(),
|
|
968
|
+
...sessionCapabilities !== void 0 ? { capabilities: sessionCapabilities } : {}
|
|
969
|
+
});
|
|
1093
970
|
setHqSessionCookie(
|
|
1094
971
|
res,
|
|
1095
972
|
serializeHqSessionCookie(sessionId, mutableAuth.cookieSecret),
|
|
@@ -1116,7 +993,7 @@ async function handleApiLogout(req, res, mutableAuth, sessions, secureCookies) {
|
|
|
1116
993
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1117
994
|
res.end(JSON.stringify({ loggedIn: false }));
|
|
1118
995
|
}
|
|
1119
|
-
async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secureCookies, requireBrowserAuth, applyAuthFile) {
|
|
996
|
+
async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secureCookies, requireBrowserAuth, applyAuthFile, loginAttempts, trustedProxyHops) {
|
|
1120
997
|
const auth = authenticateBrowserRequest(
|
|
1121
998
|
req,
|
|
1122
999
|
new URL(req.url ?? "/", "http://localhost"),
|
|
@@ -1156,21 +1033,43 @@ async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secur
|
|
|
1156
1033
|
res.end(JSON.stringify({ error: { code: "BAD_REQUEST", message: "Invalid JSON body." } }));
|
|
1157
1034
|
return;
|
|
1158
1035
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1036
|
+
if (!localOpenBootstrap && mutableAuth.passwordHash !== void 0) {
|
|
1037
|
+
const clientIp = resolveClientAddress(req, trustedProxyHops);
|
|
1038
|
+
const { blocked, retryAfter } = loginAttempts.checkBlocked(clientIp);
|
|
1039
|
+
if (blocked) {
|
|
1040
|
+
res.writeHead(429, {
|
|
1041
|
+
"Content-Type": "application/json",
|
|
1042
|
+
"Retry-After": String(retryAfter)
|
|
1043
|
+
});
|
|
1044
|
+
res.end(
|
|
1045
|
+
JSON.stringify({
|
|
1046
|
+
error: { code: "RATE_LIMITED", message: "Too many attempts. Try again later." }
|
|
1047
|
+
})
|
|
1048
|
+
);
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1161
1051
|
const currentPassword = typeof body.currentPassword === "string" ? body.currentPassword : "";
|
|
1162
|
-
|
|
1052
|
+
let confirmed = currentPassword.length > 0 && await verifyHqPassword(currentPassword, mutableAuth.passwordHash);
|
|
1053
|
+
let replayed = false;
|
|
1054
|
+
if (!confirmed && typeof body.code === "string" && mutableAuth.totpSecret) {
|
|
1055
|
+
const outcome = await consumeTotpCode(body.code, mutableAuth, dataDir, applyAuthFile);
|
|
1056
|
+
confirmed = outcome === "ok";
|
|
1057
|
+
replayed = outcome === "replayed";
|
|
1058
|
+
}
|
|
1059
|
+
if (!confirmed) {
|
|
1060
|
+
recordVerifyFailure(loginAttempts, clientIp);
|
|
1163
1061
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1164
1062
|
res.end(
|
|
1165
1063
|
JSON.stringify({
|
|
1166
1064
|
error: {
|
|
1167
|
-
code: "INVALID_CURRENT_PASSWORD",
|
|
1168
|
-
message: "Current password is required to change or remove it."
|
|
1065
|
+
code: replayed ? "TOTP_ALREADY_USED" : "INVALID_CURRENT_PASSWORD",
|
|
1066
|
+
message: replayed ? "That authenticator code has already been used. Wait for the next one." : "Current password or a TOTP code is required to change or remove it."
|
|
1169
1067
|
}
|
|
1170
1068
|
})
|
|
1171
1069
|
);
|
|
1172
1070
|
return;
|
|
1173
1071
|
}
|
|
1072
|
+
loginAttempts.delete(clientIp);
|
|
1174
1073
|
}
|
|
1175
1074
|
if (req.method === "DELETE") {
|
|
1176
1075
|
if (requireBrowserAuth && mutableAuth.browserTokens.size === 0) {
|
|
@@ -1185,7 +1084,7 @@ async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secur
|
|
|
1185
1084
|
);
|
|
1186
1085
|
return;
|
|
1187
1086
|
}
|
|
1188
|
-
const next2 = await
|
|
1087
|
+
const next2 = await mutateHqAuthFile2(dataDir, (current) => {
|
|
1189
1088
|
const updated = { ...current };
|
|
1190
1089
|
delete updated.passwordHash;
|
|
1191
1090
|
delete updated.cookieSecret;
|
|
@@ -1222,7 +1121,7 @@ async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secur
|
|
|
1222
1121
|
}
|
|
1223
1122
|
const passwordHash = await hashHqPassword(newPassword);
|
|
1224
1123
|
const cookieSecret = mintHqCookieSecret();
|
|
1225
|
-
const next = await
|
|
1124
|
+
const next = await mutateHqAuthFile2(dataDir, (current) => ({
|
|
1226
1125
|
...current,
|
|
1227
1126
|
passwordHash,
|
|
1228
1127
|
cookieSecret
|
|
@@ -1246,25 +1145,18 @@ async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secur
|
|
|
1246
1145
|
|
|
1247
1146
|
// src/hq-server/routes/auth/totp-routes.ts
|
|
1248
1147
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1249
|
-
import { mutateHqAuthFile as
|
|
1148
|
+
import { mutateHqAuthFile as mutateHqAuthFile3, verifyHqPassword as verifyHqPassword2 } from "@wrongstack/core/hq";
|
|
1250
1149
|
import {
|
|
1251
1150
|
buildOtpAuthUri,
|
|
1252
1151
|
generateRecoveryCodes,
|
|
1253
1152
|
generateTotpSecret,
|
|
1254
1153
|
hashRecoveryCode,
|
|
1255
1154
|
verifyRecoveryCode,
|
|
1256
|
-
verifyTotp
|
|
1257
|
-
verifyTotpCounter
|
|
1155
|
+
verifyTotp
|
|
1258
1156
|
} from "@wrongstack/core/security";
|
|
1259
1157
|
var PENDING_2FA_TTL_MS = 5 * 6e4;
|
|
1260
1158
|
var recoveryCodeLock = Promise.resolve();
|
|
1261
1159
|
var MAX_2FA_VERIFY_FAILURES = 5;
|
|
1262
|
-
function recordVerifyFailure(loginAttempts, clientIp) {
|
|
1263
|
-
const prev = loginAttempts.get(clientIp);
|
|
1264
|
-
const count = (prev?.count ?? 0) + 1;
|
|
1265
|
-
loginAttempts.recordFailure(clientIp);
|
|
1266
|
-
return count;
|
|
1267
|
-
}
|
|
1268
1160
|
async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, loginAttempts, dataDir, secureCookies, applyAuthFile, trustedProxyHops) {
|
|
1269
1161
|
if (!mutableAuth.cookieSecret) {
|
|
1270
1162
|
res.writeHead(503, { "Content-Type": "application/json" });
|
|
@@ -1327,9 +1219,9 @@ async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, login
|
|
|
1327
1219
|
);
|
|
1328
1220
|
return;
|
|
1329
1221
|
}
|
|
1330
|
-
const
|
|
1331
|
-
const replayed =
|
|
1332
|
-
if (
|
|
1222
|
+
const outcome = await consumeTotpCode(body.code, mutableAuth, dataDir, applyAuthFile);
|
|
1223
|
+
const replayed = outcome === "replayed";
|
|
1224
|
+
if (outcome !== "ok") {
|
|
1333
1225
|
if (recordVerifyFailure(loginAttempts, clientIp) >= MAX_2FA_VERIFY_FAILURES) {
|
|
1334
1226
|
sessions.delete(sessionId);
|
|
1335
1227
|
}
|
|
@@ -1344,22 +1236,14 @@ async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, login
|
|
|
1344
1236
|
);
|
|
1345
1237
|
return;
|
|
1346
1238
|
}
|
|
1347
|
-
mutableAuth.totpLastUsedCounter = matchedCounter;
|
|
1348
|
-
try {
|
|
1349
|
-
const next = await mutateHqAuthFile2(dataDir, (current) => ({
|
|
1350
|
-
...current,
|
|
1351
|
-
totpLastUsedCounter: matchedCounter
|
|
1352
|
-
}));
|
|
1353
|
-
applyAuthFile(next);
|
|
1354
|
-
} catch {
|
|
1355
|
-
}
|
|
1356
1239
|
loginAttempts.clearOnSuccess(clientIp);
|
|
1357
1240
|
sessions.delete(sessionId);
|
|
1358
1241
|
const fullSessionId = randomUUID2();
|
|
1359
1242
|
sessions.set(fullSessionId, {
|
|
1360
1243
|
createdAt: Date.now(),
|
|
1361
1244
|
kind: "password",
|
|
1362
|
-
lastSeenAt: Date.now()
|
|
1245
|
+
lastSeenAt: Date.now(),
|
|
1246
|
+
...session.capabilities !== void 0 ? { capabilities: session.capabilities } : {}
|
|
1363
1247
|
});
|
|
1364
1248
|
setHqSessionCookie(
|
|
1365
1249
|
res,
|
|
@@ -1399,11 +1283,11 @@ async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, login
|
|
|
1399
1283
|
await recoveryCodeLock;
|
|
1400
1284
|
let releaseLock = () => {
|
|
1401
1285
|
};
|
|
1402
|
-
recoveryCodeLock = new Promise((
|
|
1403
|
-
releaseLock =
|
|
1286
|
+
recoveryCodeLock = new Promise((resolve) => {
|
|
1287
|
+
releaseLock = resolve;
|
|
1404
1288
|
});
|
|
1405
1289
|
try {
|
|
1406
|
-
const next = await
|
|
1290
|
+
const next = await mutateHqAuthFile3(dataDir, (current) => {
|
|
1407
1291
|
const diskHashes = current.totpRecoveryCodes ?? [];
|
|
1408
1292
|
const idx = diskHashes.indexOf(usedHash);
|
|
1409
1293
|
if (idx === -1) {
|
|
@@ -1438,7 +1322,8 @@ async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, login
|
|
|
1438
1322
|
sessions.set(fullSessionId, {
|
|
1439
1323
|
createdAt: Date.now(),
|
|
1440
1324
|
kind: "password",
|
|
1441
|
-
lastSeenAt: Date.now()
|
|
1325
|
+
lastSeenAt: Date.now(),
|
|
1326
|
+
...session.capabilities !== void 0 ? { capabilities: session.capabilities } : {}
|
|
1442
1327
|
});
|
|
1443
1328
|
setHqSessionCookie(
|
|
1444
1329
|
res,
|
|
@@ -1472,7 +1357,7 @@ async function handleApiTotpSetup(req, res, mutableAuth, sessions, dataDir, appl
|
|
|
1472
1357
|
}
|
|
1473
1358
|
const secret = generateTotpSecret();
|
|
1474
1359
|
const uri = buildOtpAuthUri(secret, "HQ");
|
|
1475
|
-
const next = await
|
|
1360
|
+
const next = await mutateHqAuthFile3(dataDir, (current) => ({
|
|
1476
1361
|
...current,
|
|
1477
1362
|
totpPendingSecret: secret
|
|
1478
1363
|
}));
|
|
@@ -1523,7 +1408,7 @@ async function handleApiTotpEnable(req, res, mutableAuth, sessions, dataDir, app
|
|
|
1523
1408
|
}
|
|
1524
1409
|
const recoveryCodes = generateRecoveryCodes();
|
|
1525
1410
|
const recoveryHashes = recoveryCodes.map(hashRecoveryCode);
|
|
1526
|
-
const next = await
|
|
1411
|
+
const next = await mutateHqAuthFile3(dataDir, (current) => {
|
|
1527
1412
|
const updated = { ...current };
|
|
1528
1413
|
updated.totpSecret = pendingSecret;
|
|
1529
1414
|
delete updated.totpPendingSecret;
|
|
@@ -1560,8 +1445,11 @@ async function handleApiTotpDisable(req, res, mutableAuth, sessions, loginAttemp
|
|
|
1560
1445
|
if (typeof body.password === "string" && body.password.length > 0 && mutableAuth.passwordHash) {
|
|
1561
1446
|
confirmed = await verifyHqPassword2(body.password, mutableAuth.passwordHash);
|
|
1562
1447
|
}
|
|
1448
|
+
let replayed = false;
|
|
1563
1449
|
if (!confirmed && typeof body.code === "string" && mutableAuth.totpSecret) {
|
|
1564
|
-
|
|
1450
|
+
const outcome = await consumeTotpCode(body.code, mutableAuth, dataDir, applyAuthFile);
|
|
1451
|
+
confirmed = outcome === "ok";
|
|
1452
|
+
replayed = outcome === "replayed";
|
|
1565
1453
|
}
|
|
1566
1454
|
if (!confirmed) {
|
|
1567
1455
|
recordVerifyFailure(loginAttempts, clientIp);
|
|
@@ -1569,15 +1457,15 @@ async function handleApiTotpDisable(req, res, mutableAuth, sessions, loginAttemp
|
|
|
1569
1457
|
res.end(
|
|
1570
1458
|
JSON.stringify({
|
|
1571
1459
|
error: {
|
|
1572
|
-
code: "CONFIRMATION_REQUIRED",
|
|
1573
|
-
message: "Provide the current password or a TOTP code."
|
|
1460
|
+
code: replayed ? "TOTP_ALREADY_USED" : "CONFIRMATION_REQUIRED",
|
|
1461
|
+
message: replayed ? "That authenticator code has already been used. Wait for the next one." : "Provide the current password or a TOTP code."
|
|
1574
1462
|
}
|
|
1575
1463
|
})
|
|
1576
1464
|
);
|
|
1577
1465
|
return;
|
|
1578
1466
|
}
|
|
1579
1467
|
loginAttempts.delete(clientIp);
|
|
1580
|
-
const next = await
|
|
1468
|
+
const next = await mutateHqAuthFile3(dataDir, (current) => {
|
|
1581
1469
|
const updated = { ...current };
|
|
1582
1470
|
delete updated.totpSecret;
|
|
1583
1471
|
delete updated.totpPendingSecret;
|
|
@@ -2255,7 +2143,7 @@ async function authorizeHqCommand(input) {
|
|
|
2255
2143
|
}
|
|
2256
2144
|
|
|
2257
2145
|
// src/hq-server/routes/mailbox-handlers.ts
|
|
2258
|
-
import * as
|
|
2146
|
+
import * as path3 from "node:path";
|
|
2259
2147
|
import { resolveProjectDir } from "@wrongstack/core/coordination";
|
|
2260
2148
|
|
|
2261
2149
|
// src/hq-server/project-root.ts
|
|
@@ -2311,7 +2199,7 @@ async function handleMailboxGateway(_req, res, match, authorizeMailboxGateway, g
|
|
|
2311
2199
|
);
|
|
2312
2200
|
return;
|
|
2313
2201
|
}
|
|
2314
|
-
const gatewayGlobalRoot =
|
|
2202
|
+
const gatewayGlobalRoot = path3.dirname(dataDir);
|
|
2315
2203
|
const preliminaryAccess = authorizeMailboxGateway(_req, `project:${projectId}`);
|
|
2316
2204
|
if (!preliminaryAccess.allowed) {
|
|
2317
2205
|
res.writeHead(preliminaryAccess.status ?? 401, {
|
|
@@ -2377,7 +2265,7 @@ async function handleApiMailboxSend(_req, res, mutableAuth, sessions, dataDir, g
|
|
|
2377
2265
|
res.end(JSON.stringify({ error: "audience must be all or leaders" }));
|
|
2378
2266
|
return;
|
|
2379
2267
|
}
|
|
2380
|
-
const mbGlobalRoot =
|
|
2268
|
+
const mbGlobalRoot = path3.dirname(dataDir);
|
|
2381
2269
|
const projectRoot = await resolveHqProjectRoot(mbGlobalRoot, {
|
|
2382
2270
|
sessionId: mbody.sessionId,
|
|
2383
2271
|
projectId: mbody.projectId
|
|
@@ -2497,7 +2385,7 @@ async function handleMailboxAction(_req, res, match, mutableAuth, sessions, data
|
|
|
2497
2385
|
res.end(JSON.stringify({ error: "missing sessionId or projectId" }));
|
|
2498
2386
|
return;
|
|
2499
2387
|
}
|
|
2500
|
-
const actGlobalRoot =
|
|
2388
|
+
const actGlobalRoot = path3.dirname(dataDir);
|
|
2501
2389
|
const projectRoot = await resolveHqProjectRoot(actGlobalRoot, {
|
|
2502
2390
|
sessionId: abody.sessionId,
|
|
2503
2391
|
projectId: abody.projectId
|
|
@@ -2593,6 +2481,15 @@ async function handleApiCommand(req, res, url, mutableAuth, sessions, clients, b
|
|
|
2593
2481
|
res.end(JSON.stringify({ error: "unrecognized or malformed command", type: body.type }));
|
|
2594
2482
|
return;
|
|
2595
2483
|
}
|
|
2484
|
+
if (validated.type === "run-command" && isCookieAuth(auth) && auth.capabilities !== void 0 && !auth.capabilities.includes("control.execute")) {
|
|
2485
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
2486
|
+
res.end(
|
|
2487
|
+
JSON.stringify({
|
|
2488
|
+
error: "forbidden: browser session lacks control.execute capability"
|
|
2489
|
+
})
|
|
2490
|
+
);
|
|
2491
|
+
return;
|
|
2492
|
+
}
|
|
2596
2493
|
if (validated.type === "run-command" && !tokenHasCapability(target.authToken, "control.execute")) {
|
|
2597
2494
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
2598
2495
|
res.end(
|
|
@@ -2678,14 +2575,14 @@ async function handleApiTrendsCost(req, res, persistence) {
|
|
|
2678
2575
|
}
|
|
2679
2576
|
|
|
2680
2577
|
// src/hq-server/routes/session-handlers.ts
|
|
2681
|
-
import * as
|
|
2578
|
+
import * as path4 from "node:path";
|
|
2682
2579
|
import {
|
|
2683
2580
|
buildTranscriptFromEvents,
|
|
2684
|
-
resolveHqDataDir
|
|
2581
|
+
resolveHqDataDir
|
|
2685
2582
|
} from "@wrongstack/core/hq";
|
|
2686
2583
|
async function handleApiSessions(res) {
|
|
2687
2584
|
const { getSessionRegistry } = await import("@wrongstack/core/storage");
|
|
2688
|
-
const globalRoot =
|
|
2585
|
+
const globalRoot = path4.dirname(resolveHqDataDir());
|
|
2689
2586
|
try {
|
|
2690
2587
|
const registry = getSessionRegistry(globalRoot);
|
|
2691
2588
|
const sessions = await registry.list();
|
|
@@ -2730,7 +2627,7 @@ async function handleApiSessionEvents(req, res, match, transcripts) {
|
|
|
2730
2627
|
res.end(JSON.stringify({ error: "invalid sessionId encoding" }));
|
|
2731
2628
|
return;
|
|
2732
2629
|
}
|
|
2733
|
-
const globalRoot =
|
|
2630
|
+
const globalRoot = path4.dirname(resolveHqDataDir());
|
|
2734
2631
|
try {
|
|
2735
2632
|
const registry = getSessionRegistry(globalRoot);
|
|
2736
2633
|
const entry = await registry.get(sessionId).catch(() => null);
|
|
@@ -2957,24 +2854,42 @@ function createHqRouter(deps) {
|
|
|
2957
2854
|
trustBoundary,
|
|
2958
2855
|
bootstrapStore,
|
|
2959
2856
|
applyAuthFile,
|
|
2960
|
-
trustedProxyHops = 0
|
|
2857
|
+
trustedProxyHops = 0,
|
|
2858
|
+
ipAllowlist
|
|
2961
2859
|
} = deps;
|
|
2962
2860
|
return async (req, res) => {
|
|
2963
2861
|
try {
|
|
2964
2862
|
const url = new URL(req.url ?? "/", `http://${host}:${listeningPort()}`);
|
|
2965
2863
|
setHqSecurityHeaders(res);
|
|
2864
|
+
if (ipAllowlist && !ipAllowlist.allows(resolveSocketAddress(req))) {
|
|
2865
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
2866
|
+
res.end(JSON.stringify({ error: "forbidden: source IP is not allowed" }));
|
|
2867
|
+
return;
|
|
2868
|
+
}
|
|
2869
|
+
if (url.pathname === "/healthz" && req.method === "GET") {
|
|
2870
|
+
res.writeHead(200, {
|
|
2871
|
+
"Content-Type": "application/json",
|
|
2872
|
+
"Cache-Control": "no-store"
|
|
2873
|
+
});
|
|
2874
|
+
res.end(JSON.stringify({ status: "ok" }));
|
|
2875
|
+
return;
|
|
2876
|
+
}
|
|
2966
2877
|
if (!hasTrustedBrowserOrigin(
|
|
2967
2878
|
req,
|
|
2968
2879
|
host,
|
|
2969
2880
|
listeningPort(),
|
|
2970
2881
|
trustedPublicOrigins,
|
|
2971
|
-
allowFileOrigin
|
|
2882
|
+
allowFileOrigin,
|
|
2883
|
+
// WS-SEC-06: tells the guard whether anything else can authenticate
|
|
2884
|
+
// this request. In open mode a missing Origin is only acceptable on
|
|
2885
|
+
// a loopback bind.
|
|
2886
|
+
hqAuthRequired(mutableAuth, requireBrowserAuth)
|
|
2972
2887
|
)) {
|
|
2973
2888
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
2974
2889
|
res.end(JSON.stringify({ error: "forbidden: untrusted request origin" }));
|
|
2975
2890
|
return;
|
|
2976
2891
|
}
|
|
2977
|
-
if (url.pathname.startsWith("/api/") && url.pathname !== "/api/auth/status" && url.pathname !== "/api/login" && url.pathname !== "/api/login/verify" && url.pathname !== "/api/auth/bootstrap" && hqAuthRequired(mutableAuth, requireBrowserAuth)) {
|
|
2892
|
+
if (url.pathname.startsWith("/api/") && url.pathname !== "/api/auth/status" && url.pathname !== "/api/login" && url.pathname !== "/api/login/verify" && url.pathname !== "/api/mobile/login" && url.pathname !== "/api/mobile/login/verify" && url.pathname !== "/api/auth/bootstrap" && hqAuthRequired(mutableAuth, requireBrowserAuth)) {
|
|
2978
2893
|
const auth = authenticateBrowserRequest(req, url, mutableAuth, sessions);
|
|
2979
2894
|
if (!auth) {
|
|
2980
2895
|
const tokenOnly = mutableAuth.browserTokens.size > 0 && !mutableAuth.passwordHash;
|
|
@@ -3041,7 +2956,7 @@ function createHqRouter(deps) {
|
|
|
3041
2956
|
await handleApiAuthSessionsRevoke(req, res, url, sessions);
|
|
3042
2957
|
return;
|
|
3043
2958
|
}
|
|
3044
|
-
if (url.pathname === "/api/login" && req.method === "POST") {
|
|
2959
|
+
if ((url.pathname === "/api/login" || url.pathname === "/api/mobile/login") && req.method === "POST") {
|
|
3045
2960
|
await handleApiLogin(
|
|
3046
2961
|
req,
|
|
3047
2962
|
res,
|
|
@@ -3049,7 +2964,9 @@ function createHqRouter(deps) {
|
|
|
3049
2964
|
sessions,
|
|
3050
2965
|
loginAttempts,
|
|
3051
2966
|
secureCookies,
|
|
3052
|
-
trustedProxyHops
|
|
2967
|
+
trustedProxyHops,
|
|
2968
|
+
dataDir,
|
|
2969
|
+
url.pathname === "/api/mobile/login" ? ["control.enqueue"] : void 0
|
|
3053
2970
|
);
|
|
3054
2971
|
return;
|
|
3055
2972
|
}
|
|
@@ -3077,7 +2994,7 @@ function createHqRouter(deps) {
|
|
|
3077
2994
|
await handleApiLogout(req, res, mutableAuth, sessions, secureCookies);
|
|
3078
2995
|
return;
|
|
3079
2996
|
}
|
|
3080
|
-
if (url.pathname === "/api/login/verify" && req.method === "POST") {
|
|
2997
|
+
if ((url.pathname === "/api/login/verify" || url.pathname === "/api/mobile/login/verify") && req.method === "POST") {
|
|
3081
2998
|
await handleApiLoginVerify(
|
|
3082
2999
|
req,
|
|
3083
3000
|
res,
|
|
@@ -3122,7 +3039,9 @@ function createHqRouter(deps) {
|
|
|
3122
3039
|
dataDir,
|
|
3123
3040
|
secureCookies,
|
|
3124
3041
|
requireBrowserAuth,
|
|
3125
|
-
applyAuthFile
|
|
3042
|
+
applyAuthFile,
|
|
3043
|
+
loginAttempts,
|
|
3044
|
+
trustedProxyHops
|
|
3126
3045
|
);
|
|
3127
3046
|
return;
|
|
3128
3047
|
}
|
|
@@ -3380,12 +3299,12 @@ import {
|
|
|
3380
3299
|
ensureHqFirstRunAuthFile,
|
|
3381
3300
|
HqInsecureExposureError,
|
|
3382
3301
|
isTokenExpired as isTokenExpired3,
|
|
3383
|
-
resolveHqDataDir as
|
|
3302
|
+
resolveHqDataDir as resolveHqDataDir2
|
|
3384
3303
|
} from "@wrongstack/core/hq";
|
|
3385
3304
|
async function prepareHqServerStart(options, defaults) {
|
|
3386
3305
|
const host = options.host ?? defaults.host;
|
|
3387
3306
|
const port = options.port ?? defaults.port;
|
|
3388
|
-
const dataDir =
|
|
3307
|
+
const dataDir = resolveHqDataDir2(options.dataDir);
|
|
3389
3308
|
const firstRunAuth = await ensureHqFirstRunAuthFile(dataDir, {
|
|
3390
3309
|
warn: (message) => console.warn(
|
|
3391
3310
|
JSON.stringify({
|
|
@@ -3397,6 +3316,7 @@ async function prepareHqServerStart(options, defaults) {
|
|
|
3397
3316
|
),
|
|
3398
3317
|
actor: resolveAuditActor(),
|
|
3399
3318
|
...options.password !== void 0 ? { password: options.password } : {},
|
|
3319
|
+
...options.bootstrapPasswordOnly !== void 0 ? { bootstrapPasswordOnly: options.bootstrapPasswordOnly } : {},
|
|
3400
3320
|
...options.tokenTtlMs !== void 0 ? { tokenTtlMs: options.tokenTtlMs } : {}
|
|
3401
3321
|
});
|
|
3402
3322
|
const { authFile } = firstRunAuth;
|
|
@@ -3435,21 +3355,21 @@ async function prepareHqServerStart(options, defaults) {
|
|
|
3435
3355
|
}
|
|
3436
3356
|
|
|
3437
3357
|
// src/hq-server/startup.ts
|
|
3438
|
-
import * as
|
|
3439
|
-
import * as
|
|
3358
|
+
import * as fs3 from "node:fs/promises";
|
|
3359
|
+
import * as path5 from "node:path";
|
|
3440
3360
|
import { writeHqRuntimeFile } from "@wrongstack/core/hq";
|
|
3441
3361
|
var hqRuntimeMarkerPath2 = hqRuntimeMarkerPath;
|
|
3442
3362
|
async function writeHqRuntimeMarker(dataDir, url) {
|
|
3443
|
-
await
|
|
3363
|
+
await fs3.mkdir(dataDir, { recursive: true });
|
|
3444
3364
|
await writeHqRuntimeFile(dataDir, { url, pid: process.pid });
|
|
3445
3365
|
}
|
|
3446
3366
|
async function clearHqRuntimeMarker(dataDir, url) {
|
|
3447
3367
|
const file = hqRuntimeMarkerPath2(dataDir);
|
|
3448
3368
|
try {
|
|
3449
|
-
const raw = await
|
|
3369
|
+
const raw = await fs3.readFile(file, "utf8");
|
|
3450
3370
|
const parsed = JSON.parse(raw);
|
|
3451
3371
|
if (parsed.url === url && parsed.pid === process.pid) {
|
|
3452
|
-
await
|
|
3372
|
+
await fs3.rm(file, { force: true });
|
|
3453
3373
|
}
|
|
3454
3374
|
} catch {
|
|
3455
3375
|
}
|
|
@@ -3458,6 +3378,8 @@ function writeHqStartupInfo(write, handle) {
|
|
|
3458
3378
|
const startup = handle.firstRunSetup;
|
|
3459
3379
|
const browserUrl = buildHttpUrl(handle.host, handle.port);
|
|
3460
3380
|
const clientUrl = buildClientWsUrl(handle.host, handle.port);
|
|
3381
|
+
const mobileUrl = new URL(browserUrl);
|
|
3382
|
+
mobileUrl.pathname = "/mobile";
|
|
3461
3383
|
write(
|
|
3462
3384
|
`${terminalText("WrongStack HQ", "magenta")} ${terminalText("listening on", "green")} ${terminalLink(browserUrl)}
|
|
3463
3385
|
`
|
|
@@ -3465,13 +3387,39 @@ function writeHqStartupInfo(write, handle) {
|
|
|
3465
3387
|
if (!startup) {
|
|
3466
3388
|
write(`${terminalText("Browser endpoint:", "blue")} ${terminalLink(browserUrl)}
|
|
3467
3389
|
`);
|
|
3390
|
+
write(
|
|
3391
|
+
`${terminalText("Mobile endpoint:", "blue")} ${terminalLink(mobileUrl.toString())} (password required)
|
|
3392
|
+
`
|
|
3393
|
+
);
|
|
3468
3394
|
write(`${terminalText("Client endpoint:", "blue")} ${terminalLink(clientUrl)}
|
|
3469
3395
|
`);
|
|
3470
3396
|
writeHqLanEndpoints(write, handle, void 0);
|
|
3471
3397
|
return;
|
|
3472
3398
|
}
|
|
3399
|
+
if (process.env.WRONGSTACK_HQ_SUPPRESS_STARTUP_SECRETS === "1") {
|
|
3400
|
+
write(`${terminalText("Browser endpoint:", "blue")} ${terminalLink(browserUrl)}
|
|
3401
|
+
`);
|
|
3402
|
+
write(
|
|
3403
|
+
`${terminalText("Mobile endpoint:", "blue")} ${terminalLink(mobileUrl.toString())} (password required)
|
|
3404
|
+
`
|
|
3405
|
+
);
|
|
3406
|
+
write(`${terminalText("Client endpoint:", "blue")} ${terminalLink(clientUrl)}
|
|
3407
|
+
`);
|
|
3408
|
+
write(`Startup credentials suppressed; auth state is stored in ${startup.dataDir}.
|
|
3409
|
+
`);
|
|
3410
|
+
writeHqLanEndpoints(write, handle, void 0);
|
|
3411
|
+
return;
|
|
3412
|
+
}
|
|
3413
|
+
const firstRunMobileUrl = new URL(startup.browserUrl);
|
|
3414
|
+
firstRunMobileUrl.pathname = "/mobile";
|
|
3415
|
+
firstRunMobileUrl.search = "";
|
|
3416
|
+
firstRunMobileUrl.hash = "";
|
|
3473
3417
|
write(`${terminalText("Browser endpoint:", "blue")} ${terminalLink(startup.browserUrl)}
|
|
3474
3418
|
`);
|
|
3419
|
+
write(
|
|
3420
|
+
`${terminalText("Mobile endpoint:", "blue")} ${terminalLink(firstRunMobileUrl.toString())} (password required)
|
|
3421
|
+
`
|
|
3422
|
+
);
|
|
3475
3423
|
write(`${terminalText("Client endpoint:", "blue")} ${terminalLink(startup.clientUrl)}
|
|
3476
3424
|
`);
|
|
3477
3425
|
if (startup.createdAuth) {
|
|
@@ -3489,7 +3437,7 @@ HQ auth loaded from ${startup.dataDir}
|
|
|
3489
3437
|
if (startup.clientEnv.WRONGSTACK_HQ_TOKEN) {
|
|
3490
3438
|
write(` WRONGSTACK_HQ_TOKEN=${startup.clientEnv.WRONGSTACK_HQ_TOKEN}
|
|
3491
3439
|
`);
|
|
3492
|
-
write(`Credentials are stored in ${
|
|
3440
|
+
write(`Credentials are stored in ${path5.join(startup.dataDir, "auth.json")}
|
|
3493
3441
|
`);
|
|
3494
3442
|
}
|
|
3495
3443
|
writeHqLanEndpoints(write, handle, void 0);
|
|
@@ -3567,13 +3515,14 @@ function createHqServerShutdown(params) {
|
|
|
3567
3515
|
}
|
|
3568
3516
|
|
|
3569
3517
|
// src/hq-server/upgrade-handler.ts
|
|
3570
|
-
import {
|
|
3518
|
+
import { hqTokenVerifier as hqTokenVerifier2, isLoopbackHost as isLoopbackHost3 } from "@wrongstack/core/hq";
|
|
3571
3519
|
|
|
3572
3520
|
// src/hq-server/ws.ts
|
|
3573
3521
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
3574
3522
|
import {
|
|
3575
3523
|
HQ_PROTOCOL_VERSION as HQ_PROTOCOL_VERSION2,
|
|
3576
3524
|
parseHqFrame,
|
|
3525
|
+
redactHqEvent as redactHqEvent2,
|
|
3577
3526
|
resolveHqRedactionPolicy,
|
|
3578
3527
|
tightenHqRedactionPolicy as tightenHqRedactionPolicy2,
|
|
3579
3528
|
tokenHasCapability as tokenHasCapability3
|
|
@@ -4236,16 +4185,22 @@ function handleClient(ws, clients, browsers, eventLog, auth, snapshotBroadcaster
|
|
|
4236
4185
|
sendGuarded(ws, JSON.stringify({ type: "hq.kanban_snapshot", payload: p }));
|
|
4237
4186
|
}).catch(() => void 0);
|
|
4238
4187
|
}
|
|
4239
|
-
const event =
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4188
|
+
const event = redactHqEvent2(
|
|
4189
|
+
{
|
|
4190
|
+
id: randomUUID5(),
|
|
4191
|
+
type: "client.hello",
|
|
4192
|
+
schemaVersion: HQ_PROTOCOL_VERSION2,
|
|
4193
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4194
|
+
clientId: payload.client.clientId,
|
|
4195
|
+
projectId: payload.project.projectId,
|
|
4196
|
+
seq: 0,
|
|
4197
|
+
payload: { client: payload.client, project: payload.project }
|
|
4198
|
+
},
|
|
4199
|
+
{
|
|
4200
|
+
policy: tightenHqRedactionPolicy2(declaredRedactionPolicy, auth.getOperatorPolicy()),
|
|
4201
|
+
...payload.project.projectRoot ? { projectRoot: payload.project.projectRoot } : {}
|
|
4202
|
+
}
|
|
4203
|
+
).value;
|
|
4249
4204
|
persistEvent(event);
|
|
4250
4205
|
snapshotBroadcaster.broadcast();
|
|
4251
4206
|
broadcastEvent(event, browsers);
|
|
@@ -4345,12 +4300,24 @@ function handleHqUpgrade(req, socket, head, deps) {
|
|
|
4345
4300
|
socket.destroy();
|
|
4346
4301
|
return;
|
|
4347
4302
|
}
|
|
4303
|
+
if (deps.ipAllowlist && !deps.ipAllowlist.allows(resolveSocketAddress(req))) {
|
|
4304
|
+
socket.write(
|
|
4305
|
+
"HTTP/1.1 403 Forbidden\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n" + JSON.stringify({
|
|
4306
|
+
error: { code: "IP_NOT_ALLOWED", message: "Source IP is not allowed." }
|
|
4307
|
+
})
|
|
4308
|
+
);
|
|
4309
|
+
socket.destroy();
|
|
4310
|
+
return;
|
|
4311
|
+
}
|
|
4348
4312
|
if (!hasTrustedBrowserOrigin2(
|
|
4349
4313
|
req,
|
|
4350
4314
|
deps.host,
|
|
4351
4315
|
deps.listeningPort(),
|
|
4352
4316
|
deps.trustedPublicOrigins,
|
|
4353
|
-
deps.allowFileOrigin
|
|
4317
|
+
deps.allowFileOrigin,
|
|
4318
|
+
// WS-SEC-06 — see the guard. `/ws/browser` and `/ws/client` are the two
|
|
4319
|
+
// surfaces this branch left uncontrolled in open mode.
|
|
4320
|
+
hqAuthRequired(deps.mutableAuth, deps.requireBrowserAuth)
|
|
4354
4321
|
)) {
|
|
4355
4322
|
socket.write(
|
|
4356
4323
|
"HTTP/1.1 403 Forbidden\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n" + JSON.stringify({
|
|
@@ -4471,6 +4438,7 @@ async function startHqServer(options = {}) {
|
|
|
4471
4438
|
}
|
|
4472
4439
|
async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth) {
|
|
4473
4440
|
const trustBoundary = options.trustBoundary ?? createCompatibilityTrustBoundary({ policyId: "hq-trusted-host-compat-v1" });
|
|
4441
|
+
const ipAllowlist = createHqIpAllowlist(options.ipAllowlist);
|
|
4474
4442
|
const authFile = firstRunAuth.authFile;
|
|
4475
4443
|
const reassessExposureFloor = (live) => {
|
|
4476
4444
|
const exposure = assessHqExposure2({
|
|
@@ -4494,14 +4462,15 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
|
|
|
4494
4462
|
}
|
|
4495
4463
|
};
|
|
4496
4464
|
const authState = createHqAuthState(authFile, dataDir, {
|
|
4497
|
-
onApplied: (live) => reassessExposureFloor(live)
|
|
4465
|
+
onApplied: (live) => reassessExposureFloor(live),
|
|
4466
|
+
requireBrowserAuth: options.requireBrowserAuth
|
|
4498
4467
|
});
|
|
4499
4468
|
const { mutableAuth } = authState;
|
|
4500
4469
|
const loginAttempts = new LoginAttemptStore(dataDir);
|
|
4501
4470
|
await loginAttempts.load();
|
|
4502
4471
|
if (!mutableAuth.cookieSecret && mutableAuth.browserTokens.size > 0) {
|
|
4503
4472
|
const secret = mintHqCookieSecret2();
|
|
4504
|
-
const next = await
|
|
4473
|
+
const next = await mutateHqAuthFile4(dataDir, (current) => ({
|
|
4505
4474
|
...current,
|
|
4506
4475
|
cookieSecret: secret
|
|
4507
4476
|
}));
|
|
@@ -4528,10 +4497,12 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
|
|
|
4528
4497
|
browserTokenMode: mutableAuth.browserTokens.size > 0,
|
|
4529
4498
|
clientTokenMode: mutableAuth.clientTokens.size > 0,
|
|
4530
4499
|
passwordMode: mutableAuth.passwordHash !== void 0,
|
|
4500
|
+
ipAllowlistActive: ipAllowlist !== void 0,
|
|
4501
|
+
ipAllowlistRules: ipAllowlist?.entries.length ?? 0,
|
|
4531
4502
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4532
4503
|
})
|
|
4533
4504
|
);
|
|
4534
|
-
return new Promise((
|
|
4505
|
+
return new Promise((resolve, reject) => {
|
|
4535
4506
|
const trustedPublicOrigins = /* @__PURE__ */ new Set();
|
|
4536
4507
|
let listeningPort = port;
|
|
4537
4508
|
const clients = /* @__PURE__ */ new Map();
|
|
@@ -4577,13 +4548,13 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
|
|
|
4577
4548
|
const persistence = createHqPersistence(dataDir);
|
|
4578
4549
|
const auditLog = new HqCommandAuditLog(1e3, (entry) => persistence.commandLog.append(entry));
|
|
4579
4550
|
void (async () => {
|
|
4580
|
-
const projectsDir =
|
|
4581
|
-
const entries = await
|
|
4551
|
+
const projectsDir = path6.join(path6.dirname(dataDir), "projects");
|
|
4552
|
+
const entries = await fs4.readdir(projectsDir, { withFileTypes: true }).catch(() => []);
|
|
4582
4553
|
await Promise.all(
|
|
4583
4554
|
entries.filter((entry) => entry.isDirectory()).map(async (entry) => {
|
|
4584
|
-
const projectDir =
|
|
4585
|
-
const metadataPath =
|
|
4586
|
-
const ownerPid = await
|
|
4555
|
+
const projectDir = path6.join(projectsDir, entry.name);
|
|
4556
|
+
const metadataPath = path6.join(projectDir, ".mailbox-server.json");
|
|
4557
|
+
const ownerPid = await fs4.readFile(metadataPath, "utf8").then((raw) => JSON.parse(raw).pid).catch(() => void 0);
|
|
4587
4558
|
if (ownerPid === void 0) return;
|
|
4588
4559
|
let ownerAlive = false;
|
|
4589
4560
|
try {
|
|
@@ -4593,7 +4564,7 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
|
|
|
4593
4564
|
ownerAlive = false;
|
|
4594
4565
|
}
|
|
4595
4566
|
if (!ownerAlive) {
|
|
4596
|
-
await
|
|
4567
|
+
await fs4.rm(metadataPath, { force: true }).catch(() => {
|
|
4597
4568
|
});
|
|
4598
4569
|
return;
|
|
4599
4570
|
}
|
|
@@ -4722,6 +4693,7 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
|
|
|
4722
4693
|
getTokenStats: () => authState.tokenStats(),
|
|
4723
4694
|
applyAuthFile: (next) => authState.apply(next),
|
|
4724
4695
|
trustedProxyHops: options.trustedProxyHops ?? 0,
|
|
4696
|
+
ipAllowlist,
|
|
4725
4697
|
bootstrapStore
|
|
4726
4698
|
};
|
|
4727
4699
|
const handleRequest = createHqRouter(routerDeps);
|
|
@@ -4757,7 +4729,8 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
|
|
|
4757
4729
|
persistence,
|
|
4758
4730
|
auditLog,
|
|
4759
4731
|
snapshotBroadcaster,
|
|
4760
|
-
wss
|
|
4732
|
+
wss,
|
|
4733
|
+
ipAllowlist
|
|
4761
4734
|
};
|
|
4762
4735
|
httpServer.on("upgrade", (req, socket, head) => {
|
|
4763
4736
|
handleHqUpgrade(req, socket, head, upgradeDeps);
|
|
@@ -4907,7 +4880,7 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
|
|
|
4907
4880
|
port: actualPort,
|
|
4908
4881
|
firstRunSetup: startupInfo
|
|
4909
4882
|
});
|
|
4910
|
-
|
|
4883
|
+
resolve(handle);
|
|
4911
4884
|
})();
|
|
4912
4885
|
});
|
|
4913
4886
|
});
|
|
@@ -4926,4 +4899,4 @@ export {
|
|
|
4926
4899
|
startHqServer,
|
|
4927
4900
|
HqInsecureExposureError2 as HqInsecureExposureError
|
|
4928
4901
|
};
|
|
4929
|
-
//# sourceMappingURL=chunk-
|
|
4902
|
+
//# sourceMappingURL=chunk-PQZPHGGD.js.map
|