@wrongstack/cli 1.0.4 → 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 (26) hide show
  1. package/dist/{acp-AB54FWM2.js → acp-GOQURNC2.js} +9 -6
  2. package/dist/{auth-WDMSNZ3S.js → auth-WBXXIQUO.js} +2 -2
  3. package/dist/boot/short-circuit-desktop.d.ts +1 -0
  4. package/dist/{chunk-ONOT5OJ2.js → chunk-5A5GCSJC.js} +1 -1
  5. package/dist/{chunk-I7V2OYVH.js → chunk-5EA6OTLJ.js} +3 -6
  6. package/dist/{chunk-O4A3HHKD.js → chunk-672UXYL3.js} +2 -2
  7. package/dist/{chunk-U6PGWDMZ.js → chunk-NR7BD73C.js} +2 -2
  8. package/dist/{chunk-3I5ZJXM5.js → chunk-PQZPHGGD.js} +128 -61
  9. package/dist/{chunk-BIWE22O5.js → chunk-QDQPF4PB.js} +5 -5
  10. package/dist/{chunk-VFCJOWJB.js → chunk-VPS2SJW4.js} +4 -5
  11. package/dist/{cli-context-WFC3BNGM.js → cli-context-BYKJDAFF.js} +19 -10
  12. package/dist/{cli-main-GU3YZLMO.js → cli-main-5WZBH3WA.js} +127 -109
  13. package/dist/{execution-RAZDDQFC.js → execution-Z6A26EG6.js} +26 -26
  14. package/dist/{hq-5XHEIIH6.js → hq-V4PHEETW.js} +3 -3
  15. package/dist/hq-server/auth-state.d.ts +6 -0
  16. package/dist/hq-server/auth.d.ts +10 -1
  17. package/dist/hq-server/routes/auth/common.d.ts +34 -0
  18. package/dist/hq-server/routes/auth/password-routes.d.ts +4 -2
  19. package/dist/hq-server/types.d.ts +18 -0
  20. package/dist/{hq-server-CNUTBWTK.js → hq-server-MZ7DV6X7.js} +3 -3
  21. package/dist/index.js +8 -5
  22. package/dist/{mcp-HOMLC4WE.js → mcp-FX7TKPTV.js} +10 -8
  23. package/dist/mcp-serve.d.ts +31 -0
  24. package/dist/{short-circuit-flags-YDMLAQ5R.js → short-circuit-flags-KBTYSJTW.js} +2 -2
  25. package/dist/{subcommands-7N4RIK6H.js → subcommands-DEYSYHIM.js} +2 -2
  26. package/package.json +28 -28
@@ -649,13 +649,16 @@ async function runACPWebSocketServer(deps, port) {
649
649
  const acpToken = process.env["WRONGSTACK_ACP_TOKEN"]?.trim() || randomBytes(32).toString("hex");
650
650
  const gate = createAcpConnectionGate({ host, port, token: acpToken });
651
651
  let liveConnections = 0;
652
- const wss = new WebSocketServer({ host, port, maxPayload: 20 * 1024 * 1024 });
653
- wss.on("connection", (socket, req) => {
654
- const verdict = gate.check(req, liveConnections);
655
- if (!verdict.ok) {
656
- socket.close(verdict.code ?? 1008, verdict.reason ?? "forbidden");
652
+ const verifyClient = (info, cb) => {
653
+ const verdict = gate.check(info.req, liveConnections);
654
+ if (verdict.ok) {
655
+ cb(true);
657
656
  return;
658
657
  }
658
+ cb(false, 403, verdict.reason ?? "forbidden");
659
+ };
660
+ const wss = new WebSocketServer({ host, port, maxPayload: 20 * 1024 * 1024, verifyClient });
661
+ wss.on("connection", (socket) => {
659
662
  liveConnections++;
660
663
  socket.once("close", () => {
661
664
  liveConnections--;
@@ -1099,4 +1102,4 @@ ${renderAcpBenchText(result)}
1099
1102
  export {
1100
1103
  acpCmd
1101
1104
  };
1102
- //# sourceMappingURL=acp-AB54FWM2.js.map
1105
+ //# sourceMappingURL=acp-GOQURNC2.js.map
@@ -14,7 +14,7 @@ import {
14
14
  runOAuthLoginKind,
15
15
  runOAuthLoginMenu,
16
16
  validateFamily
17
- } from "./chunk-ONOT5OJ2.js";
17
+ } from "./chunk-5A5GCSJC.js";
18
18
  import {
19
19
  renderAuthLocalHelpToString,
20
20
  wantsLocalHelp
@@ -619,4 +619,4 @@ async function runAuthRemove(deps, providerId) {
619
619
  export {
620
620
  authCmd
621
621
  };
622
- //# sourceMappingURL=auth-WDMSNZ3S.js.map
622
+ //# sourceMappingURL=auth-WBXXIQUO.js.map
@@ -1,3 +1,4 @@
1
1
  export declare function stripDesktopLauncherArgs(argv: string[]): string[];
2
+ export declare function selectDesktopLauncherExecutable(execPath: string, runningUnderBun: boolean, nodeOverride?: string): string;
2
3
  export declare function handleDesktopShortCircuit(flags: Record<string, string | boolean>, argv: string[]): Promise<number | null>;
3
4
  //# sourceMappingURL=short-circuit-desktop.d.ts.map
@@ -1813,4 +1813,4 @@ export {
1813
1813
  addCustomProvider,
1814
1814
  addKeyForProvider
1815
1815
  };
1816
- //# sourceMappingURL=chunk-ONOT5OJ2.js.map
1816
+ //# sourceMappingURL=chunk-5A5GCSJC.js.map
@@ -16,6 +16,7 @@ import * as fs from "node:fs/promises";
16
16
  import * as os2 from "node:os";
17
17
  import * as path from "node:path";
18
18
  import { resolveHqDataDir } from "@wrongstack/core/hq";
19
+ import { isSafeSessionId as coreIsSafeSessionId } from "@wrongstack/core/utils";
19
20
  import { sanitizeApiError, scrubErrorDetail } from "@wrongstack/core/security";
20
21
  var TRANSCRIPT_RING_MAX = 4e3;
21
22
  var MAX_TRANSCRIPT_SESSIONS = 400;
@@ -99,11 +100,7 @@ function isSafePathSegment(value) {
99
100
  return true;
100
101
  }
101
102
  function isSafeSessionId(value) {
102
- if (value.length === 0 || value.length > MAX_PATH_SEGMENT_LENGTH) return false;
103
- if (value.includes("\\") || value.includes("\0") || value.includes(":")) return false;
104
- const parts = value.split("/");
105
- if (parts.length > 2) return false;
106
- return parts.every((p) => p.length > 0 && p !== "." && p !== "..");
103
+ return coreIsSafeSessionId(value);
107
104
  }
108
105
  function displayHost(host) {
109
106
  return host === "0.0.0.0" ? "127.0.0.1" : host;
@@ -239,4 +236,4 @@ export {
239
236
  recordTimeseriesSignal,
240
237
  sanitizeApiError
241
238
  };
242
- //# sourceMappingURL=chunk-I7V2OYVH.js.map
239
+ //# sourceMappingURL=chunk-5EA6OTLJ.js.map
@@ -1,6 +1,6 @@
1
1
 
2
2
  // src/preflight.ts
3
- import { ensureSessionShell } from "@wrongstack/tools";
3
+ import { ensureSessionShell } from "@wrongstack/tools/session-shell";
4
4
 
5
5
  // src/cli-update-notice.ts
6
6
  import { writeErr } from "@wrongstack/core/utils";
@@ -62,4 +62,4 @@ export {
62
62
  applySessionShellDefault,
63
63
  runPreflight
64
64
  };
65
- //# sourceMappingURL=chunk-O4A3HHKD.js.map
65
+ //# sourceMappingURL=chunk-672UXYL3.js.map
@@ -22,7 +22,7 @@ async function handleHelpVersionShortCircuit(argv) {
22
22
  }
23
23
  if (earlyFlags["help"] === true) {
24
24
  if (positional.length > 0) {
25
- const { subcommandNames } = await import("./subcommands-7N4RIK6H.js");
25
+ const { subcommandNames } = await import("./subcommands-DEYSYHIM.js");
26
26
  if (subcommandNames.includes(positional[0])) {
27
27
  const first = positional[0];
28
28
  const deep = positional[1];
@@ -43,4 +43,4 @@ async function handleHelpVersionShortCircuit(argv) {
43
43
  export {
44
44
  handleHelpVersionShortCircuit
45
45
  };
46
- //# sourceMappingURL=chunk-U6PGWDMZ.js.map
46
+ //# sourceMappingURL=chunk-NR7BD73C.js.map
@@ -24,7 +24,7 @@ import {
24
24
  sanitizeApiError,
25
25
  truncateHqSummary,
26
26
  writeInvalidBody
27
- } from "./chunk-I7V2OYVH.js";
27
+ } from "./chunk-5EA6OTLJ.js";
28
28
  import {
29
29
  createHqIpAllowlist
30
30
  } from "./chunk-Q7E3BPDU.js";
@@ -44,7 +44,7 @@ import {
44
44
  hqTokenKey as hqTokenKey2,
45
45
  isTokenExpired as isTokenExpired4,
46
46
  mintHqCookieSecret as mintHqCookieSecret2,
47
- mutateHqAuthFile as mutateHqAuthFile3,
47
+ mutateHqAuthFile as mutateHqAuthFile4,
48
48
  toAlertMessage,
49
49
  watchHqAuthFile
50
50
  } from "@wrongstack/core/hq";
@@ -135,10 +135,15 @@ function hasTrustedRequestAuthority(req, boundHost, boundPort, trustedPublicOrig
135
135
  return false;
136
136
  }
137
137
  }
138
- 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) {
139
139
  if (!hasTrustedRequestAuthority(req, boundHost, boundPort, trustedPublicOrigins)) return false;
140
140
  const origin = req.headers.origin;
141
- 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
+ }
142
147
  try {
143
148
  const parsed = new URL(origin);
144
149
  if (parsed.protocol === "file:") return allowFileOrigin;
@@ -222,7 +227,7 @@ function isCookieAuth(auth) {
222
227
  return auth !== void 0 && auth.kind === "cookie";
223
228
  }
224
229
  function hqAuthRequired(mutableAuth, requireBrowserAuth) {
225
- 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;
226
231
  }
227
232
  function hqClientAuthRequired(mutableAuth) {
228
233
  return mutableAuth.requireAuthFloor === true || mutableAuth.clientTokens.size > 0;
@@ -364,7 +369,8 @@ function createHqAuthState(authFile, dataDir, opts = {}) {
364
369
  clientTokens: /* @__PURE__ */ new Set(),
365
370
  browserTokenObjs: /* @__PURE__ */ new Map(),
366
371
  clientTokenObjs: /* @__PURE__ */ new Map(),
367
- alertRules: void 0
372
+ alertRules: void 0,
373
+ requireBrowserAuth: opts.requireBrowserAuth
368
374
  };
369
375
  projectAuthFile(mutableAuth, authFile);
370
376
  return {
@@ -727,7 +733,32 @@ async function serveHqStatic(_req, res, urlPath, distDir) {
727
733
  }
728
734
 
729
735
  // src/hq-server/routes/auth/common.ts
730
- 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
+ }
731
762
  function isLoopbackRequest(req) {
732
763
  const address = req.socket.remoteAddress?.replace(/^::ffff:/, "");
733
764
  return address !== void 0 && isLoopbackHost2(address);
@@ -775,7 +806,8 @@ import { randomUUID } from "node:crypto";
775
806
  import {
776
807
  hashHqPassword,
777
808
  mintHqCookieSecret,
778
- mutateHqAuthFile,
809
+ mutateHqAuthFile as mutateHqAuthFile2,
810
+ hqPasswordNeedsUpgrade,
779
811
  verifyHqPassword
780
812
  } from "@wrongstack/core/hq";
781
813
 
@@ -844,7 +876,7 @@ async function handleApiAuthStatus(req, res, url, mutableAuth, sessions, require
844
876
  })
845
877
  );
846
878
  }
847
- async function handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, secureCookies, trustedProxyHops, sessionCapabilities) {
879
+ async function handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, secureCookies, trustedProxyHops, dataDir, sessionCapabilities) {
848
880
  if (!mutableAuth.passwordHash) {
849
881
  res.writeHead(403, { "Content-Type": "application/json" });
850
882
  res.end(
@@ -895,6 +927,14 @@ async function handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, se
895
927
  return;
896
928
  }
897
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
+ }
898
938
  if (!ok || !mutableAuth.cookieSecret) {
899
939
  loginAttempts.recordFailure(clientIp, body.password);
900
940
  res.writeHead(401, { "Content-Type": "application/json" });
@@ -953,7 +993,7 @@ async function handleApiLogout(req, res, mutableAuth, sessions, secureCookies) {
953
993
  res.writeHead(200, { "Content-Type": "application/json" });
954
994
  res.end(JSON.stringify({ loggedIn: false }));
955
995
  }
956
- 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) {
957
997
  const auth = authenticateBrowserRequest(
958
998
  req,
959
999
  new URL(req.url ?? "/", "http://localhost"),
@@ -993,21 +1033,43 @@ async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secur
993
1033
  res.end(JSON.stringify({ error: { code: "BAD_REQUEST", message: "Invalid JSON body." } }));
994
1034
  return;
995
1035
  }
996
- const hasAdminCapability = auth !== void 0 && "capabilities" in auth && auth.capabilities?.includes("auth.admin");
997
- 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
+ }
998
1051
  const currentPassword = typeof body.currentPassword === "string" ? body.currentPassword : "";
999
- 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);
1000
1061
  res.writeHead(403, { "Content-Type": "application/json" });
1001
1062
  res.end(
1002
1063
  JSON.stringify({
1003
1064
  error: {
1004
- code: "INVALID_CURRENT_PASSWORD",
1005
- 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."
1006
1067
  }
1007
1068
  })
1008
1069
  );
1009
1070
  return;
1010
1071
  }
1072
+ loginAttempts.delete(clientIp);
1011
1073
  }
1012
1074
  if (req.method === "DELETE") {
1013
1075
  if (requireBrowserAuth && mutableAuth.browserTokens.size === 0) {
@@ -1022,7 +1084,7 @@ async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secur
1022
1084
  );
1023
1085
  return;
1024
1086
  }
1025
- const next2 = await mutateHqAuthFile(dataDir, (current) => {
1087
+ const next2 = await mutateHqAuthFile2(dataDir, (current) => {
1026
1088
  const updated = { ...current };
1027
1089
  delete updated.passwordHash;
1028
1090
  delete updated.cookieSecret;
@@ -1059,7 +1121,7 @@ async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secur
1059
1121
  }
1060
1122
  const passwordHash = await hashHqPassword(newPassword);
1061
1123
  const cookieSecret = mintHqCookieSecret();
1062
- const next = await mutateHqAuthFile(dataDir, (current) => ({
1124
+ const next = await mutateHqAuthFile2(dataDir, (current) => ({
1063
1125
  ...current,
1064
1126
  passwordHash,
1065
1127
  cookieSecret
@@ -1083,25 +1145,18 @@ async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secur
1083
1145
 
1084
1146
  // src/hq-server/routes/auth/totp-routes.ts
1085
1147
  import { randomUUID as randomUUID2 } from "node:crypto";
1086
- import { mutateHqAuthFile as mutateHqAuthFile2, verifyHqPassword as verifyHqPassword2 } from "@wrongstack/core/hq";
1148
+ import { mutateHqAuthFile as mutateHqAuthFile3, verifyHqPassword as verifyHqPassword2 } from "@wrongstack/core/hq";
1087
1149
  import {
1088
1150
  buildOtpAuthUri,
1089
1151
  generateRecoveryCodes,
1090
1152
  generateTotpSecret,
1091
1153
  hashRecoveryCode,
1092
1154
  verifyRecoveryCode,
1093
- verifyTotp,
1094
- verifyTotpCounter
1155
+ verifyTotp
1095
1156
  } from "@wrongstack/core/security";
1096
1157
  var PENDING_2FA_TTL_MS = 5 * 6e4;
1097
1158
  var recoveryCodeLock = Promise.resolve();
1098
1159
  var MAX_2FA_VERIFY_FAILURES = 5;
1099
- function recordVerifyFailure(loginAttempts, clientIp) {
1100
- const prev = loginAttempts.get(clientIp);
1101
- const count = (prev?.count ?? 0) + 1;
1102
- loginAttempts.recordFailure(clientIp);
1103
- return count;
1104
- }
1105
1160
  async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, loginAttempts, dataDir, secureCookies, applyAuthFile, trustedProxyHops) {
1106
1161
  if (!mutableAuth.cookieSecret) {
1107
1162
  res.writeHead(503, { "Content-Type": "application/json" });
@@ -1164,9 +1219,9 @@ async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, login
1164
1219
  );
1165
1220
  return;
1166
1221
  }
1167
- const matchedCounter = verifyTotpCounter(body.code, mutableAuth.totpSecret);
1168
- const replayed = matchedCounter !== void 0 && mutableAuth.totpLastUsedCounter !== void 0 && matchedCounter <= mutableAuth.totpLastUsedCounter;
1169
- if (matchedCounter === void 0 || replayed) {
1222
+ const outcome = await consumeTotpCode(body.code, mutableAuth, dataDir, applyAuthFile);
1223
+ const replayed = outcome === "replayed";
1224
+ if (outcome !== "ok") {
1170
1225
  if (recordVerifyFailure(loginAttempts, clientIp) >= MAX_2FA_VERIFY_FAILURES) {
1171
1226
  sessions.delete(sessionId);
1172
1227
  }
@@ -1181,15 +1236,6 @@ async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, login
1181
1236
  );
1182
1237
  return;
1183
1238
  }
1184
- mutableAuth.totpLastUsedCounter = matchedCounter;
1185
- try {
1186
- const next = await mutateHqAuthFile2(dataDir, (current) => ({
1187
- ...current,
1188
- totpLastUsedCounter: matchedCounter
1189
- }));
1190
- applyAuthFile(next);
1191
- } catch {
1192
- }
1193
1239
  loginAttempts.clearOnSuccess(clientIp);
1194
1240
  sessions.delete(sessionId);
1195
1241
  const fullSessionId = randomUUID2();
@@ -1241,7 +1287,7 @@ async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, login
1241
1287
  releaseLock = resolve;
1242
1288
  });
1243
1289
  try {
1244
- const next = await mutateHqAuthFile2(dataDir, (current) => {
1290
+ const next = await mutateHqAuthFile3(dataDir, (current) => {
1245
1291
  const diskHashes = current.totpRecoveryCodes ?? [];
1246
1292
  const idx = diskHashes.indexOf(usedHash);
1247
1293
  if (idx === -1) {
@@ -1311,7 +1357,7 @@ async function handleApiTotpSetup(req, res, mutableAuth, sessions, dataDir, appl
1311
1357
  }
1312
1358
  const secret = generateTotpSecret();
1313
1359
  const uri = buildOtpAuthUri(secret, "HQ");
1314
- const next = await mutateHqAuthFile2(dataDir, (current) => ({
1360
+ const next = await mutateHqAuthFile3(dataDir, (current) => ({
1315
1361
  ...current,
1316
1362
  totpPendingSecret: secret
1317
1363
  }));
@@ -1362,7 +1408,7 @@ async function handleApiTotpEnable(req, res, mutableAuth, sessions, dataDir, app
1362
1408
  }
1363
1409
  const recoveryCodes = generateRecoveryCodes();
1364
1410
  const recoveryHashes = recoveryCodes.map(hashRecoveryCode);
1365
- const next = await mutateHqAuthFile2(dataDir, (current) => {
1411
+ const next = await mutateHqAuthFile3(dataDir, (current) => {
1366
1412
  const updated = { ...current };
1367
1413
  updated.totpSecret = pendingSecret;
1368
1414
  delete updated.totpPendingSecret;
@@ -1399,8 +1445,11 @@ async function handleApiTotpDisable(req, res, mutableAuth, sessions, loginAttemp
1399
1445
  if (typeof body.password === "string" && body.password.length > 0 && mutableAuth.passwordHash) {
1400
1446
  confirmed = await verifyHqPassword2(body.password, mutableAuth.passwordHash);
1401
1447
  }
1448
+ let replayed = false;
1402
1449
  if (!confirmed && typeof body.code === "string" && mutableAuth.totpSecret) {
1403
- confirmed = verifyTotp(body.code, mutableAuth.totpSecret);
1450
+ const outcome = await consumeTotpCode(body.code, mutableAuth, dataDir, applyAuthFile);
1451
+ confirmed = outcome === "ok";
1452
+ replayed = outcome === "replayed";
1404
1453
  }
1405
1454
  if (!confirmed) {
1406
1455
  recordVerifyFailure(loginAttempts, clientIp);
@@ -1408,15 +1457,15 @@ async function handleApiTotpDisable(req, res, mutableAuth, sessions, loginAttemp
1408
1457
  res.end(
1409
1458
  JSON.stringify({
1410
1459
  error: {
1411
- code: "CONFIRMATION_REQUIRED",
1412
- 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."
1413
1462
  }
1414
1463
  })
1415
1464
  );
1416
1465
  return;
1417
1466
  }
1418
1467
  loginAttempts.delete(clientIp);
1419
- const next = await mutateHqAuthFile2(dataDir, (current) => {
1468
+ const next = await mutateHqAuthFile3(dataDir, (current) => {
1420
1469
  const updated = { ...current };
1421
1470
  delete updated.totpSecret;
1422
1471
  delete updated.totpPendingSecret;
@@ -2830,7 +2879,11 @@ function createHqRouter(deps) {
2830
2879
  host,
2831
2880
  listeningPort(),
2832
2881
  trustedPublicOrigins,
2833
- 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)
2834
2887
  )) {
2835
2888
  res.writeHead(403, { "Content-Type": "application/json" });
2836
2889
  res.end(JSON.stringify({ error: "forbidden: untrusted request origin" }));
@@ -2912,6 +2965,7 @@ function createHqRouter(deps) {
2912
2965
  loginAttempts,
2913
2966
  secureCookies,
2914
2967
  trustedProxyHops,
2968
+ dataDir,
2915
2969
  url.pathname === "/api/mobile/login" ? ["control.enqueue"] : void 0
2916
2970
  );
2917
2971
  return;
@@ -2985,7 +3039,9 @@ function createHqRouter(deps) {
2985
3039
  dataDir,
2986
3040
  secureCookies,
2987
3041
  requireBrowserAuth,
2988
- applyAuthFile
3042
+ applyAuthFile,
3043
+ loginAttempts,
3044
+ trustedProxyHops
2989
3045
  );
2990
3046
  return;
2991
3047
  }
@@ -3466,6 +3522,7 @@ import { randomUUID as randomUUID5 } from "node:crypto";
3466
3522
  import {
3467
3523
  HQ_PROTOCOL_VERSION as HQ_PROTOCOL_VERSION2,
3468
3524
  parseHqFrame,
3525
+ redactHqEvent as redactHqEvent2,
3469
3526
  resolveHqRedactionPolicy,
3470
3527
  tightenHqRedactionPolicy as tightenHqRedactionPolicy2,
3471
3528
  tokenHasCapability as tokenHasCapability3
@@ -4128,16 +4185,22 @@ function handleClient(ws, clients, browsers, eventLog, auth, snapshotBroadcaster
4128
4185
  sendGuarded(ws, JSON.stringify({ type: "hq.kanban_snapshot", payload: p }));
4129
4186
  }).catch(() => void 0);
4130
4187
  }
4131
- const event = {
4132
- id: randomUUID5(),
4133
- type: "client.hello",
4134
- schemaVersion: HQ_PROTOCOL_VERSION2,
4135
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4136
- clientId: payload.client.clientId,
4137
- projectId: payload.project.projectId,
4138
- seq: 0,
4139
- payload: { client: payload.client, project: payload.project }
4140
- };
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;
4141
4204
  persistEvent(event);
4142
4205
  snapshotBroadcaster.broadcast();
4143
4206
  broadcastEvent(event, browsers);
@@ -4251,7 +4314,10 @@ function handleHqUpgrade(req, socket, head, deps) {
4251
4314
  deps.host,
4252
4315
  deps.listeningPort(),
4253
4316
  deps.trustedPublicOrigins,
4254
- 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)
4255
4321
  )) {
4256
4322
  socket.write(
4257
4323
  "HTTP/1.1 403 Forbidden\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n" + JSON.stringify({
@@ -4396,14 +4462,15 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
4396
4462
  }
4397
4463
  };
4398
4464
  const authState = createHqAuthState(authFile, dataDir, {
4399
- onApplied: (live) => reassessExposureFloor(live)
4465
+ onApplied: (live) => reassessExposureFloor(live),
4466
+ requireBrowserAuth: options.requireBrowserAuth
4400
4467
  });
4401
4468
  const { mutableAuth } = authState;
4402
4469
  const loginAttempts = new LoginAttemptStore(dataDir);
4403
4470
  await loginAttempts.load();
4404
4471
  if (!mutableAuth.cookieSecret && mutableAuth.browserTokens.size > 0) {
4405
4472
  const secret = mintHqCookieSecret2();
4406
- const next = await mutateHqAuthFile3(dataDir, (current) => ({
4473
+ const next = await mutateHqAuthFile4(dataDir, (current) => ({
4407
4474
  ...current,
4408
4475
  cookieSecret: secret
4409
4476
  }));
@@ -4832,4 +4899,4 @@ export {
4832
4899
  startHqServer,
4833
4900
  HqInsecureExposureError2 as HqInsecureExposureError
4834
4901
  };
4835
- //# sourceMappingURL=chunk-3I5ZJXM5.js.map
4902
+ //# sourceMappingURL=chunk-PQZPHGGD.js.map
@@ -1,9 +1,9 @@
1
1
 
2
2
  // src/subcommands/index.ts
3
3
  var loaders = {
4
- acp: async () => (await import("./acp-AB54FWM2.js")).acpCmd,
4
+ acp: async () => (await import("./acp-GOQURNC2.js")).acpCmd,
5
5
  init: async () => (await import("./init-DRMMFG2G.js")).initCmd,
6
- auth: async () => (await import("./auth-WDMSNZ3S.js")).authCmd,
6
+ auth: async () => (await import("./auth-WBXXIQUO.js")).authCmd,
7
7
  update: async () => (await import("./update-5DPB4YD4.js")).updateCmd,
8
8
  sessions: async () => (await import("./sessions-config-UL45XKX7.js")).sessionsCmd,
9
9
  config: async () => (await import("./sessions-config-UL45XKX7.js")).configCmd,
@@ -16,7 +16,7 @@ var loaders = {
16
16
  skills: async () => (await import("./tools-skills-GH24UCBY.js")).skillsCmd,
17
17
  providers: async () => (await import("./providers-models-6MOGAOVK.js")).providersCmd,
18
18
  models: async () => (await import("./providers-models-6MOGAOVK.js")).modelsCmd,
19
- mcp: async () => (await import("./mcp-HOMLC4WE.js")).mcpCmd,
19
+ mcp: async () => (await import("./mcp-FX7TKPTV.js")).mcpCmd,
20
20
  plugin: async () => (await import("./plugin-usage-M6KET4OZ.js")).pluginCmd,
21
21
  plugins: async () => (await import("./plugin-usage-M6KET4OZ.js")).pluginCmd,
22
22
  diag: async () => (await import("./diag-doctor-PCER6KHH.js")).diagCmd,
@@ -31,7 +31,7 @@ var loaders = {
31
31
  quick: async () => (await import("./quick-CNB4Z3HS.js")).quickCmd,
32
32
  bench: async () => (await import("./bench-YBU6BVQP.js")).benchCmd,
33
33
  chronicle: async () => (await import("./chronicle-UTFETHP6.js")).chronicleCmd,
34
- hq: async () => (await import("./hq-5XHEIIH6.js")).hqCmd,
34
+ hq: async () => (await import("./hq-V4PHEETW.js")).hqCmd,
35
35
  mailbox: async () => (await import("./mailbox-serve-X3CR6VFW.js")).mailboxServeCmd,
36
36
  permissions: async () => (await import("./permissions-CFDLXETM.js")).permissionsCmd,
37
37
  project: async () => (await import("./project-AOQI5XTK.js")).projectCmd,
@@ -51,4 +51,4 @@ export {
51
51
  subcommands,
52
52
  subcommandNames
53
53
  };
54
- //# sourceMappingURL=chunk-BIWE22O5.js.map
54
+ //# sourceMappingURL=chunk-QDQPF4PB.js.map
@@ -74,15 +74,14 @@ function stringifyInput(input) {
74
74
  var PREVIEW_MAX_LINES = 40;
75
75
  var PREVIEW_MAX_CHARS = 8e3;
76
76
  function clipPreview(body, decorate = (l) => l) {
77
- const safe = sanitizeTerminalText2(body);
78
- const { text, truncated } = sanitizeTerminalPreview(body, {
77
+ const { text, truncated, sanitizedLength, sanitizedLines } = sanitizeTerminalPreview(body, {
79
78
  maxLines: PREVIEW_MAX_LINES,
80
79
  maxChars: PREVIEW_MAX_CHARS
81
80
  });
82
81
  const rendered = text.split("\n").map(decorate).join("\n");
83
82
  if (!truncated) return rendered;
84
- const hiddenLines = safe.split("\n").length - text.split("\n").length;
85
- const detail = hiddenLines > 0 ? `${hiddenLines} more line${hiddenLines === 1 ? "" : "s"} not shown` : `${safe.length - text.length} more characters not shown`;
83
+ const hiddenLines = sanitizedLines - text.split("\n").length;
84
+ const detail = hiddenLines > 0 ? `${hiddenLines} more line${hiddenLines === 1 ? "" : "s"} not shown` : `${sanitizedLength - text.length} more characters not shown`;
86
85
  return `${rendered}
87
86
  ${color2.dim(`\u2026 ${detail} \u2014 review the file before approving`)}`;
88
87
  }
@@ -481,4 +480,4 @@ export {
481
480
  detectProjectFacts,
482
481
  renderAgentsTemplate
483
482
  };
484
- //# sourceMappingURL=chunk-VFCJOWJB.js.map
483
+ //# sourceMappingURL=chunk-VPS2SJW4.js.map