@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.
Files changed (83) hide show
  1. package/dist/{acp-JK4ZQO3R.js → acp-GOQURNC2.js} +128 -43
  2. package/dist/acp-agent-list.d.ts +14 -0
  3. package/dist/acp-hq-telemetry.d.ts +9 -2
  4. package/dist/acp-mcp-servers.d.ts +49 -0
  5. package/dist/acp-server-agent.d.ts +18 -6
  6. package/dist/{auth-XD3GN5RJ.js → auth-WBXXIQUO.js} +3 -3
  7. package/dist/boot/short-circuit-desktop.d.ts +1 -0
  8. package/dist/boot/short-circuit-hq.d.ts +2 -0
  9. package/dist/boot/tool-registry.d.ts +18 -1
  10. package/dist/{chunk-JHPXJRRI.js → chunk-4XCBEUBX.js} +13 -3
  11. package/dist/{chunk-ONOT5OJ2.js → chunk-5A5GCSJC.js} +1 -1
  12. package/dist/chunk-5EA6OTLJ.js +239 -0
  13. package/dist/{chunk-O4A3HHKD.js → chunk-672UXYL3.js} +2 -2
  14. package/dist/{chunk-FPIWTJ2G.js → chunk-6WRKAACA.js} +4 -4
  15. package/dist/chunk-AUQDGU5D.js +32 -0
  16. package/dist/{chunk-VOTTENR5.js → chunk-CDY4D5QK.js} +2 -2
  17. package/dist/{chunk-E6RFXHWD.js → chunk-EK2P53GL.js} +3 -2
  18. package/dist/{chunk-MXEJEYT7.js → chunk-JX6KL7OF.js} +15 -5
  19. package/dist/{chunk-GMY45LO6.js → chunk-KEPABQYT.js} +101 -2
  20. package/dist/{chunk-IMWXXF4J.js → chunk-KNMIBKLV.js} +79 -1
  21. package/dist/{chunk-4NHD5OXJ.js → chunk-NR7BD73C.js} +5 -5
  22. package/dist/{chunk-C2E24IXZ.js → chunk-PQZPHGGD.js} +272 -299
  23. package/dist/chunk-Q7E3BPDU.js +62 -0
  24. package/dist/{chunk-IUEKNHPT.js → chunk-QDQPF4PB.js} +11 -11
  25. package/dist/{chunk-VFCJOWJB.js → chunk-VPS2SJW4.js} +4 -5
  26. package/dist/{chunk-U6KCWRMC.js → chunk-XJ7K67PP.js} +5 -1
  27. package/dist/{cli-context-Q6QP534K.js → cli-context-BYKJDAFF.js} +129 -25
  28. package/dist/{cli-main-JQXCDZEA.js → cli-main-5WZBH3WA.js} +1345 -800
  29. package/dist/{diag-doctor-7LYUYU35.js → diag-doctor-PCER6KHH.js} +2 -2
  30. package/dist/{execution-KH7WJNAX.js → execution-Z6A26EG6.js} +132 -40
  31. package/dist/execution-specialist-trigger.d.ts +28 -0
  32. package/dist/fleet/acp-live-progress.d.ts +19 -0
  33. package/dist/fleet/host-acp-runner-cache.d.ts +3 -1
  34. package/dist/fleet/host-acp.d.ts +14 -2
  35. package/dist/fleet/host.d.ts +1 -0
  36. package/dist/{hq-IG3YRGCO.js → hq-V4PHEETW.js} +127 -8
  37. package/dist/hq-command-controller.d.ts +31 -2
  38. package/dist/hq-server/auth-state.d.ts +6 -0
  39. package/dist/hq-server/auth.d.ts +10 -1
  40. package/dist/hq-server/client-address.d.ts +2 -0
  41. package/dist/hq-server/ip-allowlist.d.ts +16 -0
  42. package/dist/hq-server/preflight.d.ts +1 -0
  43. package/dist/hq-server/routes/auth/common.d.ts +34 -0
  44. package/dist/hq-server/routes/auth/password-routes.d.ts +4 -2
  45. package/dist/hq-server/routes.d.ts +5 -4
  46. package/dist/hq-server/secret-input.d.ts +7 -0
  47. package/dist/hq-server/types.d.ts +21 -2
  48. package/dist/hq-server/upgrade-handler.d.ts +2 -0
  49. package/dist/hq-server/utils.d.ts +6 -0
  50. package/dist/{hq-server-OILU3NPI.js → hq-server-MZ7DV6X7.js} +4 -3
  51. package/dist/hq-server.d.ts +4 -0
  52. package/dist/hq-service-L5INQFM4.js +507 -0
  53. package/dist/hq-service.d.ts +22 -0
  54. package/dist/index.js +9 -6
  55. package/dist/ip-allowlist-NHXO6AKJ.js +9 -0
  56. package/dist/live-settings-input.d.ts +2 -0
  57. package/dist/{mcp-HOMLC4WE.js → mcp-FX7TKPTV.js} +10 -8
  58. package/dist/mcp-serve.d.ts +31 -0
  59. package/dist/{per-subcommand-help-BWBHYVJO.js → per-subcommand-help-DHTYU524.js} +2 -2
  60. package/dist/{plugins-U7KGQAUG.js → plugins-WJLW6ULV.js} +2 -2
  61. package/dist/projects-LMNAGGEU.js +36 -0
  62. package/dist/{proxy-wiring-MGRSKZZB.js → proxy-wiring-K2DORFFL.js} +3 -3
  63. package/dist/short-circuit-flags-KBTYSJTW.js +10 -0
  64. package/dist/slash-commands/acp.d.ts +3 -0
  65. package/dist/slash-commands/command-context.d.ts +22 -0
  66. package/dist/slash-commands/ensemble.d.ts +1 -1
  67. package/dist/slash-commands/profile.d.ts +7 -0
  68. package/dist/subcommands/handlers/hq.d.ts +1 -0
  69. package/dist/subcommands/handlers/projects.d.ts +14 -0
  70. package/dist/{subcommands-CUTGBDKP.js → subcommands-DEYSYHIM.js} +2 -2
  71. package/dist/{version-help-MYKF724O.js → version-help-WXI42PZ3.js} +2 -2
  72. package/dist/{webui-server-QWBUT7BO.js → webui-server-OBV5JYJV.js} +9 -3
  73. package/dist/wiring/cli-slash-commands-setup.d.ts +7 -0
  74. package/dist/wiring/codebase-embeddings.d.ts +2 -2
  75. package/dist/wiring/command-host-state.d.ts +7 -0
  76. package/dist/wiring/pipeline.d.ts +2 -0
  77. package/dist/wiring/provider-runtime-setup.d.ts +1 -1
  78. package/dist/wiring/session-command-handlers.d.ts +2 -1
  79. package/dist/wiring/slash-commands.d.ts +1 -0
  80. package/package.json +28 -28
  81. package/dist/chunk-Q5GTM25S.js +0 -17
  82. package/dist/projects-L6C6XLDY.js +0 -37
  83. 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
- resolveAuditActor
7
- } from "./chunk-Q5GTM25S.js";
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 fs5 from "node:fs/promises";
34
+ import * as fs4 from "node:fs/promises";
12
35
  import * as http from "node:http";
13
- import * as path7 from "node:path";
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 mutateHqAuthFile3,
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) return true;
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 = req.socket.remoteAddress ? normalizeAddress(req.socket.remoteAddress) : void 0;
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, { createdAt: Date.now(), kind: "password", lastSeenAt: Date.now() });
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
- const hasAdminCapability = auth !== void 0 && "capabilities" in auth && auth.capabilities?.includes("auth.admin");
1160
- if (!localOpenBootstrap && mutableAuth.passwordHash !== void 0 && !hasAdminCapability) {
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
- if (!currentPassword || !await verifyHqPassword(currentPassword, mutableAuth.passwordHash)) {
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 mutateHqAuthFile(dataDir, (current) => {
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 mutateHqAuthFile(dataDir, (current) => ({
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 mutateHqAuthFile2, verifyHqPassword as verifyHqPassword2 } from "@wrongstack/core/hq";
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 matchedCounter = verifyTotpCounter(body.code, mutableAuth.totpSecret);
1331
- const replayed = matchedCounter !== void 0 && mutableAuth.totpLastUsedCounter !== void 0 && matchedCounter <= mutableAuth.totpLastUsedCounter;
1332
- if (matchedCounter === void 0 || replayed) {
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((resolve2) => {
1403
- releaseLock = resolve2;
1286
+ recoveryCodeLock = new Promise((resolve) => {
1287
+ releaseLock = resolve;
1404
1288
  });
1405
1289
  try {
1406
- const next = await mutateHqAuthFile2(dataDir, (current) => {
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 mutateHqAuthFile2(dataDir, (current) => ({
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 mutateHqAuthFile2(dataDir, (current) => {
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
- confirmed = verifyTotp(body.code, mutableAuth.totpSecret);
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 mutateHqAuthFile2(dataDir, (current) => {
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 path4 from "node:path";
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 = path4.dirname(dataDir);
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 = path4.dirname(dataDir);
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 = path4.dirname(dataDir);
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 path5 from "node:path";
2578
+ import * as path4 from "node:path";
2682
2579
  import {
2683
2580
  buildTranscriptFromEvents,
2684
- resolveHqDataDir as resolveHqDataDir2
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 = path5.dirname(resolveHqDataDir2());
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 = path5.dirname(resolveHqDataDir2());
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 resolveHqDataDir3
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 = resolveHqDataDir3(options.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 fs4 from "node:fs/promises";
3439
- import * as path6 from "node:path";
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 fs4.mkdir(dataDir, { recursive: true });
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 fs4.readFile(file, "utf8");
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 fs4.rm(file, { force: true });
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 ${path6.join(startup.dataDir, "auth.json")}
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 { isLoopbackHost as isLoopbackHost3, hqTokenVerifier as hqTokenVerifier2 } from "@wrongstack/core/hq";
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
- id: randomUUID5(),
4241
- type: "client.hello",
4242
- schemaVersion: HQ_PROTOCOL_VERSION2,
4243
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4244
- clientId: payload.client.clientId,
4245
- projectId: payload.project.projectId,
4246
- seq: 0,
4247
- payload: { client: payload.client, project: payload.project }
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 mutateHqAuthFile3(dataDir, (current) => ({
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((resolve2, reject) => {
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 = path7.join(path7.dirname(dataDir), "projects");
4581
- const entries = await fs5.readdir(projectsDir, { withFileTypes: true }).catch(() => []);
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 = path7.join(projectsDir, entry.name);
4585
- const metadataPath = path7.join(projectDir, ".mailbox-server.json");
4586
- const ownerPid = await fs5.readFile(metadataPath, "utf8").then((raw) => JSON.parse(raw).pid).catch(() => void 0);
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 fs5.rm(metadataPath, { force: true }).catch(() => {
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
- resolve2(handle);
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-C2E24IXZ.js.map
4902
+ //# sourceMappingURL=chunk-PQZPHGGD.js.map