@wrongstack/cli 1.0.4 → 1.0.7

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 (44) hide show
  1. package/dist/{acp-AB54FWM2.js → acp-A5EQEXQO.js} +69 -11
  2. package/dist/{auth-WDMSNZ3S.js → auth-3K6EN5JP.js} +3 -3
  3. package/dist/boot/container-wiring.d.ts +2 -0
  4. package/dist/boot/short-circuit-desktop.d.ts +1 -0
  5. package/dist/{chunk-VUVOMXWP.js → chunk-3VXYY5JC.js} +8 -1
  6. package/dist/{chunk-U6PGWDMZ.js → chunk-4OLAZIIN.js} +2 -2
  7. package/dist/{chunk-I7V2OYVH.js → chunk-5EA6OTLJ.js} +3 -6
  8. package/dist/{chunk-O4A3HHKD.js → chunk-672UXYL3.js} +2 -2
  9. package/dist/{chunk-ONOT5OJ2.js → chunk-FSZ5QU3M.js} +2 -2
  10. package/dist/{chunk-KNMIBKLV.js → chunk-JY52HWL6.js} +47 -1
  11. package/dist/{chunk-BIWE22O5.js → chunk-RLCKKLJA.js} +8 -8
  12. package/dist/{chunk-VFCJOWJB.js → chunk-RMQNCHGO.js} +8 -7
  13. package/dist/{chunk-3I5ZJXM5.js → chunk-XIZZ2EXG.js} +161 -62
  14. package/dist/{cli-context-WFC3BNGM.js → cli-context-TIAP6YQK.js} +95 -15
  15. package/dist/cli-context.d.ts +2 -0
  16. package/dist/{cli-main-GU3YZLMO.js → cli-main-AKJHDSHX.js} +1111 -721
  17. package/dist/config-doctor.d.ts +1 -1
  18. package/dist/execute-deps.d.ts +3 -1
  19. package/dist/{execution-RAZDDQFC.js → execution-FJE27VQ6.js} +105 -28
  20. package/dist/{hq-5XHEIIH6.js → hq-G5PCP325.js} +8 -6
  21. package/dist/hq-command-controller.d.ts +15 -0
  22. package/dist/hq-server/auth-state.d.ts +6 -0
  23. package/dist/hq-server/auth.d.ts +10 -1
  24. package/dist/hq-server/routes/auth/common.d.ts +34 -0
  25. package/dist/hq-server/routes/auth/password-routes.d.ts +4 -2
  26. package/dist/hq-server/types.d.ts +18 -0
  27. package/dist/{hq-server-CNUTBWTK.js → hq-server-G3VVVMIG.js} +3 -3
  28. package/dist/index.js +8 -5
  29. package/dist/input-reader.d.ts +2 -2
  30. package/dist/{mcp-HOMLC4WE.js → mcp-FX7TKPTV.js} +10 -8
  31. package/dist/mcp-serve.d.ts +31 -0
  32. package/dist/{modeldiag-46CMWUNM.js → modeldiag-CJSFHZ3J.js} +55 -31
  33. package/dist/permission-prompt-mirror.d.ts +70 -0
  34. package/dist/permission-prompt.d.ts +10 -1
  35. package/dist/{providers-models-6MOGAOVK.js → providers-models-LFNZ5WI7.js} +2 -2
  36. package/dist/{short-circuit-flags-YDMLAQ5R.js → short-circuit-flags-RAT6UZVI.js} +2 -2
  37. package/dist/slash-commands/subagent-models.d.ts +4 -0
  38. package/dist/subagent-models/panel-service.d.ts +28 -0
  39. package/dist/subcommands/handlers/modeldiag-test.d.ts +7 -0
  40. package/dist/{subcommands-7N4RIK6H.js → subcommands-VFVUTYAV.js} +2 -2
  41. package/dist/webui-server/prefs-seeding.d.ts +8 -0
  42. package/dist/{webui-server-OBV5JYJV.js → webui-server-3NYXBZHX.js} +36 -12
  43. package/dist/wiring/hq-telemetry.d.ts +7 -0
  44. package/package.json +28 -28
@@ -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;
@@ -2069,7 +2118,11 @@ async function authorizeHqCommand(input) {
2069
2118
  // bootstrap exchange, which is the same thing the dashboard does.
2070
2119
  actor: { kind: authMethod === "session" ? "user" : "remote-client", id: enqueuedBy },
2071
2120
  surface: "hq",
2072
- capability: command.type === "run-command" ? "hq.control.execute" : "hq.control.enqueue",
2121
+ capability: command.type === "run-command" ? "hq.control.execute" : (
2122
+ // Approvals get their own capability string so a policy can allow
2123
+ // steering while still refusing remote approval of tool calls.
2124
+ command.type === "approve" || command.type === "answer-input" ? "hq.control.approve" : "hq.control.enqueue"
2125
+ ),
2073
2126
  subject,
2074
2127
  risk: command.type === "run-command" ? "critical" : "high",
2075
2128
  scope: { projectId: target.projectId },
@@ -2432,6 +2485,34 @@ async function handleApiCommand(req, res, url, mutableAuth, sessions, clients, b
2432
2485
  res.end(JSON.stringify({ error: "unrecognized or malformed command", type: body.type }));
2433
2486
  return;
2434
2487
  }
2488
+ if ((validated.type === "approve" || validated.type === "answer-input") && isCookieAuth(auth) && auth.capabilities !== void 0 && !auth.capabilities.includes("control.approve")) {
2489
+ res.writeHead(403, { "Content-Type": "application/json" });
2490
+ res.end(
2491
+ JSON.stringify({
2492
+ error: "forbidden: browser session lacks control.approve capability"
2493
+ })
2494
+ );
2495
+ return;
2496
+ }
2497
+ if ((validated.type === "approve" || validated.type === "answer-input") && isTokenAuth(auth) && auth.capabilities !== void 0 && !auth.capabilities.includes("control.approve")) {
2498
+ res.writeHead(403, { "Content-Type": "application/json" });
2499
+ res.end(
2500
+ JSON.stringify({
2501
+ error: "forbidden: token lacks control.approve capability"
2502
+ })
2503
+ );
2504
+ return;
2505
+ }
2506
+ if ((validated.type === "approve" || validated.type === "answer-input") && !target.capabilities.includes("control.approve")) {
2507
+ res.writeHead(409, { "Content-Type": "application/json" });
2508
+ res.end(
2509
+ JSON.stringify({
2510
+ error: "client does not mirror approvals",
2511
+ clientId: body.clientId
2512
+ })
2513
+ );
2514
+ return;
2515
+ }
2435
2516
  if (validated.type === "run-command" && isCookieAuth(auth) && auth.capabilities !== void 0 && !auth.capabilities.includes("control.execute")) {
2436
2517
  res.writeHead(403, { "Content-Type": "application/json" });
2437
2518
  res.end(
@@ -2830,7 +2911,11 @@ function createHqRouter(deps) {
2830
2911
  host,
2831
2912
  listeningPort(),
2832
2913
  trustedPublicOrigins,
2833
- allowFileOrigin
2914
+ allowFileOrigin,
2915
+ // WS-SEC-06: tells the guard whether anything else can authenticate
2916
+ // this request. In open mode a missing Origin is only acceptable on
2917
+ // a loopback bind.
2918
+ hqAuthRequired(mutableAuth, requireBrowserAuth)
2834
2919
  )) {
2835
2920
  res.writeHead(403, { "Content-Type": "application/json" });
2836
2921
  res.end(JSON.stringify({ error: "forbidden: untrusted request origin" }));
@@ -2912,6 +2997,7 @@ function createHqRouter(deps) {
2912
2997
  loginAttempts,
2913
2998
  secureCookies,
2914
2999
  trustedProxyHops,
3000
+ dataDir,
2915
3001
  url.pathname === "/api/mobile/login" ? ["control.enqueue"] : void 0
2916
3002
  );
2917
3003
  return;
@@ -2985,7 +3071,9 @@ function createHqRouter(deps) {
2985
3071
  dataDir,
2986
3072
  secureCookies,
2987
3073
  requireBrowserAuth,
2988
- applyAuthFile
3074
+ applyAuthFile,
3075
+ loginAttempts,
3076
+ trustedProxyHops
2989
3077
  );
2990
3078
  return;
2991
3079
  }
@@ -3466,6 +3554,7 @@ import { randomUUID as randomUUID5 } from "node:crypto";
3466
3554
  import {
3467
3555
  HQ_PROTOCOL_VERSION as HQ_PROTOCOL_VERSION2,
3468
3556
  parseHqFrame,
3557
+ redactHqEvent as redactHqEvent2,
3469
3558
  resolveHqRedactionPolicy,
3470
3559
  tightenHqRedactionPolicy as tightenHqRedactionPolicy2,
3471
3560
  tokenHasCapability as tokenHasCapability3
@@ -4128,16 +4217,22 @@ function handleClient(ws, clients, browsers, eventLog, auth, snapshotBroadcaster
4128
4217
  sendGuarded(ws, JSON.stringify({ type: "hq.kanban_snapshot", payload: p }));
4129
4218
  }).catch(() => void 0);
4130
4219
  }
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
- };
4220
+ const event = redactHqEvent2(
4221
+ {
4222
+ id: randomUUID5(),
4223
+ type: "client.hello",
4224
+ schemaVersion: HQ_PROTOCOL_VERSION2,
4225
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4226
+ clientId: payload.client.clientId,
4227
+ projectId: payload.project.projectId,
4228
+ seq: 0,
4229
+ payload: { client: payload.client, project: payload.project }
4230
+ },
4231
+ {
4232
+ policy: tightenHqRedactionPolicy2(declaredRedactionPolicy, auth.getOperatorPolicy()),
4233
+ ...payload.project.projectRoot ? { projectRoot: payload.project.projectRoot } : {}
4234
+ }
4235
+ ).value;
4141
4236
  persistEvent(event);
4142
4237
  snapshotBroadcaster.broadcast();
4143
4238
  broadcastEvent(event, browsers);
@@ -4251,7 +4346,10 @@ function handleHqUpgrade(req, socket, head, deps) {
4251
4346
  deps.host,
4252
4347
  deps.listeningPort(),
4253
4348
  deps.trustedPublicOrigins,
4254
- deps.allowFileOrigin
4349
+ deps.allowFileOrigin,
4350
+ // WS-SEC-06 — see the guard. `/ws/browser` and `/ws/client` are the two
4351
+ // surfaces this branch left uncontrolled in open mode.
4352
+ hqAuthRequired(deps.mutableAuth, deps.requireBrowserAuth)
4255
4353
  )) {
4256
4354
  socket.write(
4257
4355
  "HTTP/1.1 403 Forbidden\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n" + JSON.stringify({
@@ -4396,14 +4494,15 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
4396
4494
  }
4397
4495
  };
4398
4496
  const authState = createHqAuthState(authFile, dataDir, {
4399
- onApplied: (live) => reassessExposureFloor(live)
4497
+ onApplied: (live) => reassessExposureFloor(live),
4498
+ requireBrowserAuth: options.requireBrowserAuth
4400
4499
  });
4401
4500
  const { mutableAuth } = authState;
4402
4501
  const loginAttempts = new LoginAttemptStore(dataDir);
4403
4502
  await loginAttempts.load();
4404
4503
  if (!mutableAuth.cookieSecret && mutableAuth.browserTokens.size > 0) {
4405
4504
  const secret = mintHqCookieSecret2();
4406
- const next = await mutateHqAuthFile3(dataDir, (current) => ({
4505
+ const next = await mutateHqAuthFile4(dataDir, (current) => ({
4407
4506
  ...current,
4408
4507
  cookieSecret: secret
4409
4508
  }));
@@ -4832,4 +4931,4 @@ export {
4832
4931
  startHqServer,
4833
4932
  HqInsecureExposureError2 as HqInsecureExposureError
4834
4933
  };
4835
- //# sourceMappingURL=chunk-3I5ZJXM5.js.map
4934
+ //# sourceMappingURL=chunk-XIZZ2EXG.js.map