@wrongstack/cli 0.298.3 → 0.300.0

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 (51) hide show
  1. package/dist/{acp-S4LZQB66.js → acp-IDNS2YVE.js} +3 -3
  2. package/dist/{auth-G5YKKSRH.js → auth-6T6ZOT2R.js} +5 -5
  3. package/dist/auth-menu/loopback-server.d.ts +41 -55
  4. package/dist/auth-menu/openai-codex-oauth.d.ts +36 -123
  5. package/dist/auto-discover-providers-DRW4OZHG.js +8 -0
  6. package/dist/boot/dispatch-webui.d.ts +3 -0
  7. package/dist/{chunk-PEKUKUCH.js → chunk-3MWUZMOL.js} +19 -12
  8. package/dist/{chunk-TGDHN4LM.js → chunk-4GVH7V3S.js} +4 -24
  9. package/dist/{chunk-4U3DMA2Y.js → chunk-4WJKVQJY.js} +15 -20
  10. package/dist/{chunk-C5GH4YBL.js → chunk-GYDQABMA.js} +271 -182
  11. package/dist/{chunk-4HH3YGG4.js → chunk-MRLYDHQ7.js} +8 -5
  12. package/dist/{chunk-66T43Q4Y.js → chunk-N7ULWBO5.js} +4 -3
  13. package/dist/{chunk-ZNZJ34RF.js → chunk-PEMN4T5O.js} +2 -2
  14. package/dist/chunk-PYTFS4MC.js +238 -0
  15. package/dist/{chunk-L5CE52XW.js → chunk-V76R7DC5.js} +25 -19
  16. package/dist/{cli-main-ZDZMCVLM.js → cli-main-7B3G2YSG.js} +943 -160
  17. package/dist/{execution-BY556FCF.js → execution-SHGM7BVC.js} +76 -8
  18. package/dist/fleet/budget-source.d.ts +18 -0
  19. package/dist/fleet/host-status.d.ts +31 -0
  20. package/dist/fleet/host-types.d.ts +8 -0
  21. package/dist/fleet/host.d.ts +5 -0
  22. package/dist/{hq-WO6CLUSI.js → hq-DVDD4XPV.js} +22 -3
  23. package/dist/hq-server/auth-state.d.ts +29 -1
  24. package/dist/hq-server/auth.d.ts +16 -0
  25. package/dist/hq-server/client-address.d.ts +56 -0
  26. package/dist/hq-server/login-attempt-store.d.ts +30 -10
  27. package/dist/hq-server/routes/auth-handlers.d.ts +66 -26
  28. package/dist/hq-server/routes.d.ts +13 -0
  29. package/dist/hq-server/types.d.ts +10 -0
  30. package/dist/{hq-server-6TSAORAC.js → hq-server-DQHLNHDL.js} +2 -2
  31. package/dist/hq-server.d.ts +19 -0
  32. package/dist/index.js +20 -20
  33. package/dist/{modeldiag-HENYJ4N7.js → modeldiag-YOQZHBQV.js} +2 -2
  34. package/dist/{plugin-usage-VTL7ZTVC.js → plugin-usage-EOG4ET4S.js} +2 -2
  35. package/dist/{plugins-ETY6W3DC.js → plugins-56CMWMPR.js} +3 -3
  36. package/dist/{providers-models-GDELBHZK.js → providers-models-K6RT7ABI.js} +34 -9
  37. package/dist/slash-commands/command-context.d.ts +19 -0
  38. package/dist/slash-commands/intake.d.ts +4 -0
  39. package/dist/slash-commands/memory-triage.d.ts +47 -0
  40. package/dist/webui-server/setup-events.d.ts +17 -0
  41. package/dist/webui-server/terminal-log-view.d.ts +29 -12
  42. package/dist/{webui-server-PCLSXX6L.js → webui-server-G3EKFXLL.js} +109 -38
  43. package/dist/webui-server-options.d.ts +16 -0
  44. package/dist/wiring/brain-and-orchestration.d.ts +3 -0
  45. package/dist/wiring/director-setup.d.ts +4 -0
  46. package/dist/wiring/fallback-gate.d.ts +31 -0
  47. package/dist/wiring/fleet-command-handlers.d.ts +1 -1
  48. package/dist/wiring/plugins.d.ts +2 -8
  49. package/dist/wiring/provider-utility-tools.d.ts +26 -1
  50. package/package.json +23 -22
  51. package/dist/chunk-GO3TJICK.js +0 -489
@@ -21,7 +21,7 @@ import {
21
21
  HqAlertEngine,
22
22
  HqBootstrapCodeStore,
23
23
  HqCommandAuditLog,
24
- hqTokenKey as hqTokenKey3,
24
+ hqTokenKey as hqTokenKey2,
25
25
  hqTokenVerifier as hqTokenVerifier2,
26
26
  mintHqCookieSecret as mintHqCookieSecret2,
27
27
  mutateHqAuthFile as mutateHqAuthFile2,
@@ -174,6 +174,10 @@ function parseCookieHeader(cookieHeader) {
174
174
  }
175
175
  return out;
176
176
  }
177
+ function readHqSessionCookie(cookieHeader) {
178
+ const cookies = parseCookieHeader(cookieHeader);
179
+ return cookies[HQ_SESSION_COOKIE_SECURE] ?? cookies[HQ_SESSION_COOKIE];
180
+ }
177
181
  function setHqSessionCookie(res, value, secure) {
178
182
  const name = cookieName(secure);
179
183
  const parts = [
@@ -265,8 +269,7 @@ function authenticateBrowserRequest(req, url, mutableAuth, sessions) {
265
269
  }
266
270
  }
267
271
  if (mutableAuth.cookieSecret) {
268
- const cookies = parseCookieHeader(req.headers.cookie);
269
- const raw = cookies[HQ_SESSION_COOKIE] ?? cookies["__Host-hq.session"];
272
+ const raw = readHqSessionCookie(req.headers.cookie);
270
273
  if (raw) {
271
274
  const sessionId = parseHqSessionCookie(raw, mutableAuth.cookieSecret);
272
275
  if (sessionId) {
@@ -312,25 +315,38 @@ var TOKEN_EXPIRY_WARNING_MS = 24 * 60 * 60 * 1e3;
312
315
  function liveTokens(list) {
313
316
  return (list ?? []).filter((token) => !isTokenExpired2(token));
314
317
  }
315
- function createHqAuthState(authFile, dataDir) {
318
+ function projectAuthFile(mutableAuth, next) {
319
+ mutableAuth.operatorPolicy = {
320
+ ...DEFAULT_HQ_REDACTION_POLICY,
321
+ ...next.redactionPolicy ?? {}
322
+ };
323
+ mutableAuth.operatorPolicyOverride = next.redactionPolicy;
324
+ mutableAuth.browserTokens = new Set(liveTokens(next.browserTokens).map(hqTokenKey));
325
+ mutableAuth.clientTokens = new Set(liveTokens(next.clientTokens).map(hqTokenKey));
326
+ mutableAuth.browserTokenObjs = browserTokenMap(next.browserTokens);
327
+ mutableAuth.clientTokenObjs = new Map(
328
+ liveTokens(next.clientTokens).map((token) => [hqTokenKey(token), token])
329
+ );
330
+ mutableAuth.passwordHash = next.passwordHash;
331
+ mutableAuth.cookieSecret = next.cookieSecret;
332
+ mutableAuth.totpSecret = next.totpSecret;
333
+ mutableAuth.totpPendingSecret = next.totpPendingSecret;
334
+ mutableAuth.totpRecoveryCodes = next.totpRecoveryCodes;
335
+ mutableAuth.alertRules = next.alertRules;
336
+ }
337
+ function createHqAuthState(authFile, dataDir, opts = {}) {
316
338
  let rawBrowserTokens = authFile.browserTokens ?? [];
317
339
  let rawClientTokens = authFile.clientTokens ?? [];
318
340
  const mutableAuth = {
319
- operatorPolicy: { ...DEFAULT_HQ_REDACTION_POLICY, ...authFile.redactionPolicy ?? {} },
320
- operatorPolicyOverride: authFile.redactionPolicy,
321
- browserTokens: new Set(liveTokens(authFile.browserTokens).map(hqTokenKey)),
322
- clientTokens: new Set(liveTokens(authFile.clientTokens).map(hqTokenKey)),
323
- browserTokenObjs: browserTokenMap(authFile.browserTokens),
324
- clientTokenObjs: new Map(
325
- liveTokens(authFile.clientTokens).map((token) => [hqTokenKey(token), token])
326
- ),
327
- passwordHash: authFile.passwordHash,
328
- cookieSecret: authFile.cookieSecret,
329
- totpSecret: authFile.totpSecret,
330
- totpPendingSecret: authFile.totpPendingSecret,
331
- totpRecoveryCodes: authFile.totpRecoveryCodes,
332
- alertRules: authFile.alertRules
341
+ operatorPolicy: { ...DEFAULT_HQ_REDACTION_POLICY },
342
+ operatorPolicyOverride: void 0,
343
+ browserTokens: /* @__PURE__ */ new Set(),
344
+ clientTokens: /* @__PURE__ */ new Set(),
345
+ browserTokenObjs: /* @__PURE__ */ new Map(),
346
+ clientTokenObjs: /* @__PURE__ */ new Map(),
347
+ alertRules: void 0
333
348
  };
349
+ projectAuthFile(mutableAuth, authFile);
334
350
  return {
335
351
  mutableAuth,
336
352
  tokenStats: () => {
@@ -355,23 +371,8 @@ function createHqAuthState(authFile, dataDir) {
355
371
  auditPrunedTokens("client", rawClientTokens, newClient, next, dataDir);
356
372
  rawBrowserTokens = newBrowser;
357
373
  rawClientTokens = newClient;
358
- mutableAuth.operatorPolicy = {
359
- ...DEFAULT_HQ_REDACTION_POLICY,
360
- ...next.redactionPolicy ?? {}
361
- };
362
- mutableAuth.operatorPolicyOverride = next.redactionPolicy;
363
- mutableAuth.browserTokens = new Set(liveTokens(next.browserTokens).map(hqTokenKey));
364
- mutableAuth.clientTokens = new Set(liveTokens(next.clientTokens).map(hqTokenKey));
365
- mutableAuth.browserTokenObjs = browserTokenMap(next.browserTokens);
366
- mutableAuth.clientTokenObjs = new Map(
367
- liveTokens(next.clientTokens).map((token) => [hqTokenKey(token), token])
368
- );
369
- mutableAuth.passwordHash = next.passwordHash;
370
- mutableAuth.cookieSecret = next.cookieSecret;
371
- mutableAuth.totpSecret = next.totpSecret;
372
- mutableAuth.totpPendingSecret = next.totpPendingSecret;
373
- mutableAuth.totpRecoveryCodes = next.totpRecoveryCodes;
374
- mutableAuth.alertRules = next.alertRules;
374
+ projectAuthFile(mutableAuth, next);
375
+ opts.onApplied?.(mutableAuth);
375
376
  }
376
377
  };
377
378
  }
@@ -453,24 +454,41 @@ var LoginAttemptStore = class _LoginAttemptStore {
453
454
  }
454
455
  }
455
456
  /**
456
- * Compound rate-limit key for public-relay mode: IP + SHA-256(password).
457
- * Limits how often the SAME password can be tried from different IPs,
458
- * so a rotating-IP attacker can't bypass per-IP backoff.
457
+ * Rate-limit key for a *candidate password*, independent of source IP.
458
+ *
459
+ * WS-104: this used to be `cred:${ip}:${hash}`. Embedding the IP made the
460
+ * documented purpose — "limit how often the SAME password can be tried from
461
+ * different IPs" — structurally impossible, because a rotating-IP attacker
462
+ * got a fresh counter with every hop, exactly the case the key exists for.
463
+ * The IP-scoped counter is already kept separately under the bare `ip` key.
464
+ *
465
+ * Backoff caps at 16s (see {@link recordFailure}), so a global per-password
466
+ * counter throttles guessing without becoming a lockout an attacker could
467
+ * aim at the operator.
459
468
  */
460
- static credentialKey(ip, password) {
469
+ static credentialKey(password) {
461
470
  const hash = createHash("sha256").update(password, "utf8").digest("hex").slice(0, 16);
462
- return `cred:${ip}:${hash}`;
471
+ return `cred:${hash}`;
463
472
  }
464
473
  /**
465
- * Check both IP and credential keys; return the stricter (later blockedUntil).
474
+ * Check the IP key and when a candidate password is supplied — the
475
+ * credential key too, returning the stricter (later `blockedUntil`).
476
+ *
477
+ * WS-104: `handleApiLogin` called this with `''` before parsing the request
478
+ * body ("we don't have the password yet") and never called it again, so the
479
+ * credential entries {@link recordFailure} wrote were only ever *written*.
480
+ * Callers now re-check once the password is known; passing no password (or
481
+ * an empty one) means "IP scope only".
466
482
  */
467
483
  checkBlocked(ip, password) {
468
- const ipEntry = this.store.get(ip);
469
- const credKey = _LoginAttemptStore.credentialKey(ip, password);
470
- const credEntry = this.store.get(credKey);
471
484
  const now = Date.now();
485
+ const ipEntry = this.store.get(ip);
472
486
  const ipBlocked = ipEntry && ipEntry.blockedUntil > now ? ipEntry.blockedUntil : 0;
473
- const credBlocked = credEntry && credEntry.blockedUntil > now ? credEntry.blockedUntil : 0;
487
+ let credBlocked = 0;
488
+ if (password !== void 0 && password.length > 0) {
489
+ const credEntry = this.store.get(_LoginAttemptStore.credentialKey(password));
490
+ credBlocked = credEntry && credEntry.blockedUntil > now ? credEntry.blockedUntil : 0;
491
+ }
474
492
  const blockedUntil = Math.max(ipBlocked, credBlocked);
475
493
  if (blockedUntil > now) {
476
494
  return { blocked: true, retryAfter: Math.ceil((blockedUntil - now) / 1e3) };
@@ -478,7 +496,11 @@ var LoginAttemptStore = class _LoginAttemptStore {
478
496
  return { blocked: false, retryAfter: 0 };
479
497
  }
480
498
  /**
481
- * Record a failed attempt on both IP and credential keys.
499
+ * Record a failed attempt on the IP key, and on the credential key when a
500
+ * candidate password is supplied. Omit the password for flows that have no
501
+ * one (2FA verification, TOTP disable) so they do not accumulate a
502
+ * meaningless `cred:sha256("")` counter shared by every such flow.
503
+ *
482
504
  * Returns the updated IP entry (for the response Retry-After header).
483
505
  */
484
506
  recordFailure(ip, password, maxBackoffMs = 16e3) {
@@ -491,16 +513,21 @@ var LoginAttemptStore = class _LoginAttemptStore {
491
513
  };
492
514
  };
493
515
  this.set(ip, backoff(this.store.get(ip)));
494
- const credKey = _LoginAttemptStore.credentialKey(ip, password);
495
- this.set(credKey, backoff(this.store.get(credKey)));
516
+ if (password !== void 0 && password.length > 0) {
517
+ const credKey = _LoginAttemptStore.credentialKey(password);
518
+ this.set(credKey, backoff(this.store.get(credKey)));
519
+ }
496
520
  return this.store.get(ip);
497
521
  }
498
522
  /**
499
- * Clear both IP and credential entries on successful login.
523
+ * Clear the IP entry — and the credential entry when a password is given —
524
+ * on successful login.
500
525
  */
501
526
  clearOnSuccess(ip, password) {
502
527
  this.delete(ip);
503
- this.delete(_LoginAttemptStore.credentialKey(ip, password));
528
+ if (password !== void 0 && password.length > 0) {
529
+ this.delete(_LoginAttemptStore.credentialKey(password));
530
+ }
504
531
  }
505
532
  /** Number of tracked entries (for diagnostics). */
506
533
  get size() {
@@ -1180,7 +1207,14 @@ function buildSnapshot(clients, options) {
1180
1207
  completedTasks: f.payload.completedTasks,
1181
1208
  failedTasks: f.payload.failedTasks,
1182
1209
  ...f.payload.totalCostUsd !== void 0 ? { totalCostUsd: f.payload.totalCostUsd } : {},
1183
- lastActivityAt: f.lastActivityAt
1210
+ lastActivityAt: f.lastActivityAt,
1211
+ ...f.payload.maxConcurrent !== void 0 ? { maxConcurrent: f.payload.maxConcurrent } : {},
1212
+ ...f.payload.maxSpawns !== void 0 ? { maxSpawns: f.payload.maxSpawns } : {},
1213
+ ...f.payload.usedSpawns !== void 0 ? { usedSpawns: f.payload.usedSpawns } : {},
1214
+ ...f.payload.remainingSpawns !== void 0 ? { remainingSpawns: f.payload.remainingSpawns } : {},
1215
+ ...f.payload.effectiveSource !== void 0 ? { effectiveSource: f.payload.effectiveSource } : {},
1216
+ ...f.payload.checkpointMaxSpawns !== void 0 ? { checkpointMaxSpawns: f.payload.checkpointMaxSpawns } : {},
1217
+ ...f.payload.ceilingMismatch ? { ceilingMismatch: true } : {}
1184
1218
  }));
1185
1219
  const mcpServers = [];
1186
1220
  const seenMcp = /* @__PURE__ */ new Set();
@@ -1398,11 +1432,8 @@ async function authorizeHqCommand(input) {
1398
1432
  // src/hq-server/routes/auth-handlers.ts
1399
1433
  import { randomUUID } from "node:crypto";
1400
1434
  import {
1401
- DEFAULT_HQ_REDACTION_POLICY as DEFAULT_HQ_REDACTION_POLICY2,
1402
1435
  hashHqPassword,
1403
- hqTokenKey as hqTokenKey2,
1404
1436
  isLoopbackHost as isLoopbackHost2,
1405
- isTokenExpired as isTokenExpired4,
1406
1437
  mintHqCookieSecret,
1407
1438
  mutateHqAuthFile,
1408
1439
  verifyHqPassword
@@ -1413,13 +1444,92 @@ import {
1413
1444
  generateTotpSecret,
1414
1445
  hashRecoveryCode,
1415
1446
  verifyRecoveryCode,
1416
- verifyTotp
1447
+ verifyTotp,
1448
+ verifyTotpCounter
1417
1449
  } from "@wrongstack/core/security";
1450
+
1451
+ // src/hq-server/client-address.ts
1452
+ import { isIP as isIP2 } from "node:net";
1453
+ function normalizeAddress(raw) {
1454
+ const trimmed = raw.trim().replace(/^\[(.*)\]$/, "$1");
1455
+ return trimmed.replace(/^::ffff:/i, "");
1456
+ }
1457
+ function parseForwardedEntry(entry) {
1458
+ const value = entry.trim();
1459
+ if (!value) return void 0;
1460
+ const bracketed = /^\[([^\]]+)\](?::\d+)?$/.exec(value);
1461
+ if (bracketed?.[1] !== void 0) {
1462
+ const inner = normalizeAddress(bracketed[1]);
1463
+ return isIP2(inner) !== 0 ? inner : void 0;
1464
+ }
1465
+ const normalized = normalizeAddress(value);
1466
+ if (isIP2(normalized) !== 0) return normalized;
1467
+ const lastColon = normalized.lastIndexOf(":");
1468
+ if (lastColon > 0 && normalized.indexOf(":") === lastColon) {
1469
+ const host = normalized.slice(0, lastColon);
1470
+ if (isIP2(host) === 4) return host;
1471
+ }
1472
+ return void 0;
1473
+ }
1474
+ function forwardedChain(req) {
1475
+ const header = req.headers["x-forwarded-for"];
1476
+ if (header === void 0) return [];
1477
+ const raw = Array.isArray(header) ? header.join(",") : header;
1478
+ return raw.split(",").map((entry) => parseForwardedEntry(entry)).filter((entry) => entry !== void 0);
1479
+ }
1480
+ function resolveClientAddress(req, trustedProxyHops = 0) {
1481
+ const socketAddress = req.socket.remoteAddress ? normalizeAddress(req.socket.remoteAddress) : void 0;
1482
+ const hops = Number.isFinite(trustedProxyHops) ? Math.max(0, Math.trunc(trustedProxyHops)) : 0;
1483
+ if (hops === 0) return socketAddress ?? "unknown";
1484
+ const chain = [...socketAddress !== void 0 ? [socketAddress] : [], ...forwardedChain(req).reverse()];
1485
+ if (hops >= chain.length) return socketAddress ?? "unknown";
1486
+ return chain[hops] ?? socketAddress ?? "unknown";
1487
+ }
1488
+
1489
+ // src/hq-server/routes/auth-handlers.ts
1418
1490
  import { readHqAuthAuditTail } from "@wrongstack/core/hq";
1419
1491
  function isLoopbackRequest(req) {
1420
1492
  const address = req.socket.remoteAddress?.replace(/^::ffff:/, "");
1421
1493
  return address !== void 0 && isLoopbackHost2(address);
1422
1494
  }
1495
+ var HQ_AUTH_ADMIN_CAPABILITY = "auth.admin";
1496
+ function callerCanAdministerAuth(auth) {
1497
+ if (auth === void 0) return false;
1498
+ if (auth.kind === "cookie" && auth.tokenId === void 0) return true;
1499
+ if (auth.capabilities === void 0) return true;
1500
+ return auth.capabilities.includes(HQ_AUTH_ADMIN_CAPABILITY);
1501
+ }
1502
+ function writeAuthAdminRequired(res) {
1503
+ res.writeHead(403, { "Content-Type": "application/json" });
1504
+ res.end(
1505
+ JSON.stringify({
1506
+ error: {
1507
+ code: "AUTH_ADMIN_REQUIRED",
1508
+ message: `This endpoint requires the '${HQ_AUTH_ADMIN_CAPABILITY}' capability.`
1509
+ }
1510
+ })
1511
+ );
1512
+ }
1513
+ function authorizeAuthAdmin(req, res, mutableAuth, sessions) {
1514
+ const auth = authenticateBrowserRequest(
1515
+ req,
1516
+ new URL(req.url ?? "/", "http://localhost"),
1517
+ mutableAuth,
1518
+ sessions
1519
+ );
1520
+ if (!auth) {
1521
+ res.writeHead(401, { "Content-Type": "application/json" });
1522
+ res.end(
1523
+ JSON.stringify({ error: { code: "UNAUTHORIZED", message: "Authentication required." } })
1524
+ );
1525
+ return void 0;
1526
+ }
1527
+ if (!callerCanAdministerAuth(auth)) {
1528
+ writeAuthAdminRequired(res);
1529
+ return void 0;
1530
+ }
1531
+ return auth;
1532
+ }
1423
1533
  async function handleApiAuthStatus(req, res, url, mutableAuth, sessions, requireBrowserAuth, trustedPublicOrigins, secureCookies) {
1424
1534
  const auth = authenticateBrowserRequest(req, url, mutableAuth, sessions);
1425
1535
  const openMode = mutableAuth.browserTokens.size === 0 && mutableAuth.passwordHash === void 0 && mutableAuth.requireAuthFloor !== true;
@@ -1440,7 +1550,7 @@ async function handleApiAuthStatus(req, res, url, mutableAuth, sessions, require
1440
1550
  })
1441
1551
  );
1442
1552
  }
1443
- async function handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, secureCookies) {
1553
+ async function handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, secureCookies, trustedProxyHops) {
1444
1554
  if (!mutableAuth.passwordHash) {
1445
1555
  res.writeHead(403, { "Content-Type": "application/json" });
1446
1556
  res.end(
@@ -1453,9 +1563,8 @@ async function handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, se
1453
1563
  );
1454
1564
  return;
1455
1565
  }
1456
- const clientIp = req.socket.remoteAddress ?? "unknown";
1457
- const { blocked, retryAfter } = loginAttempts.checkBlocked(clientIp, "");
1458
- if (blocked) {
1566
+ const clientIp = resolveClientAddress(req, trustedProxyHops);
1567
+ const writeRateLimited = (retryAfter) => {
1459
1568
  res.writeHead(429, {
1460
1569
  "Content-Type": "application/json",
1461
1570
  "Retry-After": String(retryAfter)
@@ -1468,6 +1577,10 @@ async function handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, se
1468
1577
  }
1469
1578
  })
1470
1579
  );
1580
+ };
1581
+ const ipCheck = loginAttempts.checkBlocked(clientIp);
1582
+ if (ipCheck.blocked) {
1583
+ writeRateLimited(ipCheck.retryAfter);
1471
1584
  return;
1472
1585
  }
1473
1586
  let body;
@@ -1482,6 +1595,11 @@ async function handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, se
1482
1595
  res.end(JSON.stringify({ error: { code: "BAD_REQUEST", message: "password is required" } }));
1483
1596
  return;
1484
1597
  }
1598
+ const credCheck = loginAttempts.checkBlocked(clientIp, body.password);
1599
+ if (credCheck.blocked) {
1600
+ writeRateLimited(credCheck.retryAfter);
1601
+ return;
1602
+ }
1485
1603
  const ok = await verifyHqPassword(body.password, mutableAuth.passwordHash);
1486
1604
  if (!ok || !mutableAuth.cookieSecret) {
1487
1605
  loginAttempts.recordFailure(clientIp, body.password);
@@ -1525,8 +1643,7 @@ async function handleApiLogout(req, res, mutableAuth, sessions, secureCookies) {
1525
1643
  sessions
1526
1644
  );
1527
1645
  if (isCookieAuth(auth)) {
1528
- const cookies = parseCookieHeader(req.headers.cookie);
1529
- const raw = cookies[HQ_SESSION_COOKIE] ?? cookies["__Host-hq.session"];
1646
+ const raw = readHqSessionCookie(req.headers.cookie);
1530
1647
  if (raw) {
1531
1648
  const sessionId = parseHqSessionCookie(raw, mutableAuth.cookieSecret ?? "");
1532
1649
  if (sessionId) sessions.delete(sessionId);
@@ -1536,7 +1653,7 @@ async function handleApiLogout(req, res, mutableAuth, sessions, secureCookies) {
1536
1653
  res.writeHead(200, { "Content-Type": "application/json" });
1537
1654
  res.end(JSON.stringify({ loggedIn: false }));
1538
1655
  }
1539
- async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secureCookies, requireBrowserAuth) {
1656
+ async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secureCookies, requireBrowserAuth, applyAuthFile) {
1540
1657
  const auth = authenticateBrowserRequest(
1541
1658
  req,
1542
1659
  new URL(req.url ?? "/", "http://localhost"),
@@ -1576,7 +1693,7 @@ async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secur
1576
1693
  res.end(JSON.stringify({ error: { code: "BAD_REQUEST", message: "Invalid JSON body." } }));
1577
1694
  return;
1578
1695
  }
1579
- const hasAdminCapability = auth !== void 0 && "capabilities" in auth && auth.capabilities !== void 0 && auth.capabilities.includes("auth.admin");
1696
+ const hasAdminCapability = auth !== void 0 && "capabilities" in auth && auth.capabilities?.includes("auth.admin");
1580
1697
  if (!localOpenBootstrap && mutableAuth.passwordHash !== void 0 && !hasAdminCapability) {
1581
1698
  const currentPassword = typeof body.currentPassword === "string" ? body.currentPassword : "";
1582
1699
  if (!currentPassword || !await verifyHqPassword(currentPassword, mutableAuth.passwordHash)) {
@@ -1611,7 +1728,7 @@ async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secur
1611
1728
  delete updated.totpRecoveryCodes;
1612
1729
  return updated;
1613
1730
  });
1614
- applyAuthFile(mutableAuth, next2);
1731
+ applyAuthFile(next2);
1615
1732
  sessions.clear();
1616
1733
  clearHqSessionCookie(res, secureCookies);
1617
1734
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -1644,7 +1761,7 @@ async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secur
1644
1761
  passwordHash,
1645
1762
  cookieSecret
1646
1763
  }));
1647
- applyAuthFile(mutableAuth, next);
1764
+ applyAuthFile(next);
1648
1765
  sessions.clear();
1649
1766
  if (isCookieAuth(auth) || localOpenBootstrap) {
1650
1767
  const sessionId = randomUUID();
@@ -1660,35 +1777,6 @@ async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secur
1660
1777
  })
1661
1778
  );
1662
1779
  }
1663
- function applyAuthFile(mutableAuth, next) {
1664
- mutableAuth.operatorPolicy = {
1665
- ...DEFAULT_HQ_REDACTION_POLICY2,
1666
- ...next.redactionPolicy ?? {}
1667
- };
1668
- mutableAuth.operatorPolicyOverride = next.redactionPolicy;
1669
- const liveBrowserTokens = (next.browserTokens ?? []).filter((t) => !isTokenExpired4(t));
1670
- mutableAuth.browserTokens = new Set(liveBrowserTokens.map(hqTokenKey2));
1671
- mutableAuth.clientTokens = new Set((next.clientTokens ?? []).map(hqTokenKey2));
1672
- mutableAuth.browserTokenObjs = new Map(
1673
- liveBrowserTokens.map((t) => [
1674
- hqTokenKey2(t),
1675
- {
1676
- id: t.id,
1677
- ...t.capabilities !== void 0 ? { capabilities: t.capabilities } : {},
1678
- ...t.expiresAt !== void 0 ? { expiresAt: t.expiresAt } : {}
1679
- }
1680
- ])
1681
- );
1682
- mutableAuth.clientTokenObjs = new Map(
1683
- (next.clientTokens ?? []).map((token) => [hqTokenKey2(token), token])
1684
- );
1685
- mutableAuth.passwordHash = next.passwordHash;
1686
- mutableAuth.cookieSecret = next.cookieSecret;
1687
- mutableAuth.alertRules = next.alertRules;
1688
- mutableAuth.totpSecret = next.totpSecret;
1689
- mutableAuth.totpPendingSecret = next.totpPendingSecret;
1690
- mutableAuth.totpRecoveryCodes = next.totpRecoveryCodes;
1691
- }
1692
1780
  async function handleApiBootstrap(req, res, mutableAuth, sessions, secureCookies, bootstrapStore) {
1693
1781
  if (!mutableAuth.cookieSecret) {
1694
1782
  res.writeHead(503, { "Content-Type": "application/json" });
@@ -1821,24 +1909,23 @@ var MAX_2FA_VERIFY_FAILURES = 5;
1821
1909
  function recordVerifyFailure(loginAttempts, clientIp) {
1822
1910
  const prev = loginAttempts.get(clientIp);
1823
1911
  const count = (prev?.count ?? 0) + 1;
1824
- loginAttempts.recordFailure(clientIp, "");
1912
+ loginAttempts.recordFailure(clientIp);
1825
1913
  return count;
1826
1914
  }
1827
- async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, loginAttempts, dataDir, secureCookies) {
1915
+ async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, loginAttempts, dataDir, secureCookies, applyAuthFile, trustedProxyHops) {
1828
1916
  if (!mutableAuth.cookieSecret) {
1829
1917
  res.writeHead(503, { "Content-Type": "application/json" });
1830
1918
  res.end(JSON.stringify({ error: { code: "NO_COOKIE_SECRET" } }));
1831
1919
  return;
1832
1920
  }
1833
- const clientIp = req.socket.remoteAddress ?? "unknown";
1834
- const { blocked, retryAfter } = loginAttempts.checkBlocked(clientIp, "");
1921
+ const clientIp = resolveClientAddress(req, trustedProxyHops);
1922
+ const { blocked, retryAfter } = loginAttempts.checkBlocked(clientIp);
1835
1923
  if (blocked) {
1836
1924
  res.writeHead(429, { "Content-Type": "application/json", "Retry-After": String(retryAfter) });
1837
1925
  res.end(JSON.stringify({ error: { code: "RATE_LIMITED", message: "Too many attempts. Try again later." } }));
1838
1926
  return;
1839
1927
  }
1840
- const cookies = parseCookieHeader(req.headers.cookie);
1841
- const raw = cookies[HQ_SESSION_COOKIE] ?? cookies["__Host-hq.session"];
1928
+ const raw = readHqSessionCookie(req.headers.cookie);
1842
1929
  const sessionId = raw ? parseHqSessionCookie(raw, mutableAuth.cookieSecret) : void 0;
1843
1930
  if (!sessionId) {
1844
1931
  res.writeHead(401, { "Content-Type": "application/json" });
@@ -1846,7 +1933,7 @@ async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, login
1846
1933
  return;
1847
1934
  }
1848
1935
  const session = sessions.get(sessionId);
1849
- if (!session || !session.pending2fa) {
1936
+ if (!session?.pending2fa) {
1850
1937
  res.writeHead(401, { "Content-Type": "application/json" });
1851
1938
  res.end(JSON.stringify({ error: { code: "NO_PENDING_SESSION", message: "Session is not pending 2FA." } }));
1852
1939
  return;
@@ -1871,15 +1958,25 @@ async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, login
1871
1958
  res.end(JSON.stringify({ error: { code: "TOTP_NOT_ENABLED", message: "2FA is not configured." } }));
1872
1959
  return;
1873
1960
  }
1874
- if (!verifyTotp(body.code, mutableAuth.totpSecret)) {
1961
+ const matchedCounter = verifyTotpCounter(body.code, mutableAuth.totpSecret);
1962
+ const replayed = matchedCounter !== void 0 && mutableAuth.totpLastUsedCounter !== void 0 && matchedCounter <= mutableAuth.totpLastUsedCounter;
1963
+ if (matchedCounter === void 0 || replayed) {
1875
1964
  if (recordVerifyFailure(loginAttempts, clientIp) >= MAX_2FA_VERIFY_FAILURES) {
1876
1965
  sessions.delete(sessionId);
1877
1966
  }
1878
1967
  res.writeHead(401, { "Content-Type": "application/json" });
1879
- res.end(JSON.stringify({ error: { code: "INVALID_TOTP", message: "Invalid authenticator code." } }));
1968
+ res.end(
1969
+ JSON.stringify({
1970
+ error: {
1971
+ code: replayed ? "TOTP_ALREADY_USED" : "INVALID_TOTP",
1972
+ message: replayed ? "That authenticator code has already been used. Wait for the next one." : "Invalid authenticator code."
1973
+ }
1974
+ })
1975
+ );
1880
1976
  return;
1881
1977
  }
1882
- loginAttempts.clearOnSuccess(clientIp, "");
1978
+ mutableAuth.totpLastUsedCounter = matchedCounter;
1979
+ loginAttempts.clearOnSuccess(clientIp);
1883
1980
  sessions.delete(sessionId);
1884
1981
  const fullSessionId = randomUUID();
1885
1982
  sessions.set(fullSessionId, { createdAt: Date.now(), kind: "password", lastSeenAt: Date.now() });
@@ -1935,11 +2032,11 @@ async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, login
1935
2032
  res.end(JSON.stringify({ error: { code: "RECOVERY_CODE_ALREADY_USED", message: "This recovery code has already been used." } }));
1936
2033
  return;
1937
2034
  }
1938
- applyAuthFile(mutableAuth, next);
2035
+ applyAuthFile(next);
1939
2036
  } finally {
1940
2037
  releaseLock();
1941
2038
  }
1942
- loginAttempts.clearOnSuccess(clientIp, "");
2039
+ loginAttempts.clearOnSuccess(clientIp);
1943
2040
  sessions.delete(sessionId);
1944
2041
  const fullSessionId = randomUUID();
1945
2042
  sessions.set(fullSessionId, { createdAt: Date.now(), kind: "password", lastSeenAt: Date.now() });
@@ -1957,18 +2054,8 @@ async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, login
1957
2054
  JSON.stringify({ error: { code: "BAD_REQUEST", message: "Provide a `code` or `recoveryCode`." } })
1958
2055
  );
1959
2056
  }
1960
- async function handleApiTotpSetup(req, res, mutableAuth, sessions, dataDir) {
1961
- const auth = authenticateBrowserRequest(
1962
- req,
1963
- new URL(req.url ?? "/", "http://localhost"),
1964
- mutableAuth,
1965
- sessions
1966
- );
1967
- if (!auth) {
1968
- res.writeHead(401, { "Content-Type": "application/json" });
1969
- res.end(JSON.stringify({ error: { code: "UNAUTHORIZED", message: "Authentication required." } }));
1970
- return;
1971
- }
2057
+ async function handleApiTotpSetup(req, res, mutableAuth, sessions, dataDir, applyAuthFile) {
2058
+ if (!authorizeAuthAdmin(req, res, mutableAuth, sessions)) return;
1972
2059
  if (mutableAuth.totpSecret) {
1973
2060
  res.writeHead(409, { "Content-Type": "application/json" });
1974
2061
  res.end(
@@ -1982,22 +2069,12 @@ async function handleApiTotpSetup(req, res, mutableAuth, sessions, dataDir) {
1982
2069
  ...current,
1983
2070
  totpPendingSecret: secret
1984
2071
  }));
1985
- applyAuthFile(mutableAuth, next);
2072
+ applyAuthFile(next);
1986
2073
  res.writeHead(200, { "Content-Type": "application/json" });
1987
2074
  res.end(JSON.stringify({ secret, uri, enabled: false }));
1988
2075
  }
1989
- async function handleApiTotpEnable(req, res, mutableAuth, sessions, dataDir) {
1990
- const auth = authenticateBrowserRequest(
1991
- req,
1992
- new URL(req.url ?? "/", "http://localhost"),
1993
- mutableAuth,
1994
- sessions
1995
- );
1996
- if (!auth) {
1997
- res.writeHead(401, { "Content-Type": "application/json" });
1998
- res.end(JSON.stringify({ error: { code: "UNAUTHORIZED", message: "Authentication required." } }));
1999
- return;
2000
- }
2076
+ async function handleApiTotpEnable(req, res, mutableAuth, sessions, dataDir, applyAuthFile) {
2077
+ if (!authorizeAuthAdmin(req, res, mutableAuth, sessions)) return;
2001
2078
  if (mutableAuth.totpSecret) {
2002
2079
  res.writeHead(409, { "Content-Type": "application/json" });
2003
2080
  res.end(
@@ -2040,25 +2117,15 @@ async function handleApiTotpEnable(req, res, mutableAuth, sessions, dataDir) {
2040
2117
  updated.totpRecoveryCodes = recoveryHashes;
2041
2118
  return updated;
2042
2119
  });
2043
- applyAuthFile(mutableAuth, next);
2120
+ applyAuthFile(next);
2044
2121
  sessions.clear();
2045
2122
  res.writeHead(200, { "Content-Type": "application/json" });
2046
2123
  res.end(JSON.stringify({ enabled: true, recoveryCodes }));
2047
2124
  }
2048
- async function handleApiTotpDisable(req, res, mutableAuth, sessions, loginAttempts, dataDir) {
2049
- const auth = authenticateBrowserRequest(
2050
- req,
2051
- new URL(req.url ?? "/", "http://localhost"),
2052
- mutableAuth,
2053
- sessions
2054
- );
2055
- if (!auth) {
2056
- res.writeHead(401, { "Content-Type": "application/json" });
2057
- res.end(JSON.stringify({ error: { code: "UNAUTHORIZED", message: "Authentication required." } }));
2058
- return;
2059
- }
2060
- const clientIp = req.socket.remoteAddress ?? "unknown";
2061
- const { blocked, retryAfter } = loginAttempts.checkBlocked(clientIp, "");
2125
+ async function handleApiTotpDisable(req, res, mutableAuth, sessions, loginAttempts, dataDir, applyAuthFile, trustedProxyHops) {
2126
+ if (!authorizeAuthAdmin(req, res, mutableAuth, sessions)) return;
2127
+ const clientIp = resolveClientAddress(req, trustedProxyHops);
2128
+ const { blocked, retryAfter } = loginAttempts.checkBlocked(clientIp);
2062
2129
  if (blocked) {
2063
2130
  res.writeHead(429, { "Content-Type": "application/json", "Retry-After": String(retryAfter) });
2064
2131
  res.end(JSON.stringify({ error: { code: "RATE_LIMITED", message: "Too many attempts. Try again later." } }));
@@ -2095,7 +2162,7 @@ async function handleApiTotpDisable(req, res, mutableAuth, sessions, loginAttemp
2095
2162
  delete updated.totpRecoveryCodes;
2096
2163
  return updated;
2097
2164
  });
2098
- applyAuthFile(mutableAuth, next);
2165
+ applyAuthFile(next);
2099
2166
  res.writeHead(200, { "Content-Type": "application/json" });
2100
2167
  res.end(JSON.stringify({ enabled: false }));
2101
2168
  }
@@ -2105,7 +2172,7 @@ function handleApiAuthSessions(_req, res, mutableAuth, sessions) {
2105
2172
  for (const [id, session] of sessions) {
2106
2173
  if (session.pending2fa) continue;
2107
2174
  list.push({
2108
- // Truncated for display only — the client sends the full ID to revoke.
2175
+ // The client sends the full ID back to revoke; `shortId` is for display.
2109
2176
  id,
2110
2177
  shortId: id.slice(0, 8),
2111
2178
  kind: session.kind,
@@ -2431,8 +2498,6 @@ var isTokenAuth2 = isTokenAuth;
2431
2498
  var isCookieAuth2 = isCookieAuth;
2432
2499
  var hqAuthRequired2 = hqAuthRequired;
2433
2500
  var parseHqSessionCookie2 = parseHqSessionCookie;
2434
- var parseCookieHeader2 = parseCookieHeader;
2435
- var HQ_SESSION_COOKIE2 = HQ_SESSION_COOKIE;
2436
2501
  var decodePathSegment2 = decodePathSegment;
2437
2502
  var readRequestBody2 = readRequestBody;
2438
2503
  var writeInvalidBody2 = writeInvalidBody;
@@ -2468,7 +2533,9 @@ function createHqRouter(deps) {
2468
2533
  getMailboxGateway,
2469
2534
  getTokenStats,
2470
2535
  trustBoundary,
2471
- bootstrapStore
2536
+ bootstrapStore,
2537
+ applyAuthFile,
2538
+ trustedProxyHops = 0
2472
2539
  } = deps;
2473
2540
  return async (req, res) => {
2474
2541
  try {
@@ -2516,6 +2583,21 @@ function createHqRouter(deps) {
2516
2583
  await handleApiAuthStatus(req, res, url, mutableAuth, sessions, requireBrowserAuth, trustedPublicOrigins, secureCookies);
2517
2584
  return;
2518
2585
  }
2586
+ if (url.pathname === "/api/auth/audit" && req.method === "GET" || url.pathname.startsWith("/api/auth/sessions") && (req.method === "GET" || req.method === "DELETE") || url.pathname.startsWith("/api/auth/totp/") && req.method === "POST") {
2587
+ const auth = authenticateBrowserRequest2(req, url, mutableAuth, sessions);
2588
+ if (auth !== void 0 && !callerCanAdministerAuth(auth)) {
2589
+ res.writeHead(403, { "Content-Type": "application/json" });
2590
+ res.end(
2591
+ JSON.stringify({
2592
+ error: {
2593
+ code: "AUTH_ADMIN_REQUIRED",
2594
+ message: "This endpoint requires the 'auth.admin' capability."
2595
+ }
2596
+ })
2597
+ );
2598
+ return;
2599
+ }
2600
+ }
2519
2601
  if (url.pathname === "/api/auth/audit" && req.method === "GET") {
2520
2602
  handleApiAuthAudit(req, res, dataDir);
2521
2603
  return;
@@ -2529,7 +2611,7 @@ function createHqRouter(deps) {
2529
2611
  return;
2530
2612
  }
2531
2613
  if (url.pathname === "/api/login" && req.method === "POST") {
2532
- await handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, secureCookies);
2614
+ await handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, secureCookies, trustedProxyHops);
2533
2615
  return;
2534
2616
  }
2535
2617
  if (url.pathname === "/api/auth/bootstrap" && req.method === "POST") {
@@ -2557,23 +2639,23 @@ function createHqRouter(deps) {
2557
2639
  return;
2558
2640
  }
2559
2641
  if (url.pathname === "/api/login/verify" && req.method === "POST") {
2560
- await handleApiLoginVerify(req, res, url, mutableAuth, sessions, loginAttempts, dataDir, secureCookies);
2642
+ await handleApiLoginVerify(req, res, url, mutableAuth, sessions, loginAttempts, dataDir, secureCookies, applyAuthFile, trustedProxyHops);
2561
2643
  return;
2562
2644
  }
2563
2645
  if (url.pathname === "/api/auth/totp/setup" && req.method === "POST") {
2564
- await handleApiTotpSetup(req, res, mutableAuth, sessions, dataDir);
2646
+ await handleApiTotpSetup(req, res, mutableAuth, sessions, dataDir, applyAuthFile);
2565
2647
  return;
2566
2648
  }
2567
2649
  if (url.pathname === "/api/auth/totp/enable" && req.method === "POST") {
2568
- await handleApiTotpEnable(req, res, mutableAuth, sessions, dataDir);
2650
+ await handleApiTotpEnable(req, res, mutableAuth, sessions, dataDir, applyAuthFile);
2569
2651
  return;
2570
2652
  }
2571
2653
  if (url.pathname === "/api/auth/totp/disable" && req.method === "POST") {
2572
- await handleApiTotpDisable(req, res, mutableAuth, sessions, loginAttempts, dataDir);
2654
+ await handleApiTotpDisable(req, res, mutableAuth, sessions, loginAttempts, dataDir, applyAuthFile, trustedProxyHops);
2573
2655
  return;
2574
2656
  }
2575
2657
  if (url.pathname === "/api/auth/password" && (req.method === "POST" || req.method === "DELETE")) {
2576
- await handleApiPassword(req, res, mutableAuth, sessions, dataDir, secureCookies, requireBrowserAuth);
2658
+ await handleApiPassword(req, res, mutableAuth, sessions, dataDir, secureCookies, requireBrowserAuth, applyAuthFile);
2577
2659
  return;
2578
2660
  }
2579
2661
  if (url.pathname === "/api/snapshot" && req.method === "GET") {
@@ -3896,7 +3978,30 @@ async function startHqServer(options = {}) {
3896
3978
  async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth) {
3897
3979
  const trustBoundary = options.trustBoundary ?? createCompatibilityTrustBoundary({ policyId: "hq-trusted-host-compat-v1" });
3898
3980
  const authFile = firstRunAuth.authFile;
3899
- const authState = createHqAuthState(authFile, dataDir);
3981
+ const reassessExposureFloor = (live) => {
3982
+ const exposure = assessHqExposure2({
3983
+ host,
3984
+ hasBrowserTokens: live.browserTokens.size > 0,
3985
+ hasPassword: live.passwordHash !== void 0,
3986
+ allowInsecure: options.allowInsecureOpen
3987
+ });
3988
+ const previousFloor = live.requireAuthFloor === true;
3989
+ live.requireAuthFloor = exposure.kind === "refuse";
3990
+ if (live.requireAuthFloor && !previousFloor) {
3991
+ console.error(
3992
+ JSON.stringify({
3993
+ level: "error",
3994
+ event: "hq.auth.open_mode_refused",
3995
+ message: "Last HQ credential removed while bound to a non-loopback address. Refusing to serve unauthenticated: requests will return 401 until a token or password is configured, or HQ is restarted with --host 127.0.0.1.",
3996
+ host,
3997
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3998
+ })
3999
+ );
4000
+ }
4001
+ };
4002
+ const authState = createHqAuthState(authFile, dataDir, {
4003
+ onApplied: (live) => reassessExposureFloor(live)
4004
+ });
3900
4005
  const { mutableAuth } = authState;
3901
4006
  if (!mutableAuth.cookieSecret && mutableAuth.browserTokens.size > 0) {
3902
4007
  const secret = mintHqCookieSecret2();
@@ -3904,7 +4009,7 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
3904
4009
  ...current,
3905
4010
  cookieSecret: secret
3906
4011
  }));
3907
- mutableAuth.cookieSecret = next.cookieSecret;
4012
+ authState.apply(next);
3908
4013
  console.warn(
3909
4014
  JSON.stringify({
3910
4015
  level: "info",
@@ -4193,6 +4298,10 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
4193
4298
  authorizeMailboxGateway,
4194
4299
  getMailboxGateway,
4195
4300
  getTokenStats: () => authState.tokenStats(),
4301
+ // Single projection + single floor evaluation, shared with the reload
4302
+ // watcher below. See `createHqAuthState`'s `onApplied` hook.
4303
+ applyAuthFile: (next) => authState.apply(next),
4304
+ trustedProxyHops: options.trustedProxyHops ?? 0,
4196
4305
  bootstrapStore
4197
4306
  };
4198
4307
  const handleRequest = createHqRouter(routerDeps);
@@ -4260,8 +4369,7 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
4260
4369
  }
4261
4370
  const tokenValid = timingSafeTokenMatch(tokenSet, supplied) !== void 0;
4262
4371
  const cookieValid = pathname === "/ws/browser" && mutableAuth.cookieSecret !== void 0 && (() => {
4263
- const cookies = parseCookieHeader2(req.headers.cookie);
4264
- const raw = cookies[HQ_SESSION_COOKIE2] ?? cookies["__Host-hq.session"];
4372
+ const raw = readHqSessionCookie(req.headers.cookie);
4265
4373
  if (!raw) return false;
4266
4374
  const sessionId = parseHqSessionCookie2(raw, mutableAuth.cookieSecret);
4267
4375
  if (sessionId === void 0) return false;
@@ -4327,25 +4435,6 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
4327
4435
  dataDir,
4328
4436
  (next) => {
4329
4437
  authState.apply(next);
4330
- const exposure = assessHqExposure2({
4331
- host,
4332
- hasBrowserTokens: mutableAuth.browserTokens.size > 0,
4333
- hasPassword: mutableAuth.passwordHash !== void 0,
4334
- allowInsecure: options.allowInsecureOpen
4335
- });
4336
- const previousFloor = mutableAuth.requireAuthFloor === true;
4337
- mutableAuth.requireAuthFloor = exposure.kind === "refuse";
4338
- if (mutableAuth.requireAuthFloor && !previousFloor) {
4339
- console.error(
4340
- JSON.stringify({
4341
- level: "error",
4342
- event: "hq.auth.open_mode_refused",
4343
- message: "Last HQ credential removed while bound to a non-loopback address. Refusing to serve unauthenticated: requests will return 401 until a token or password is configured, or HQ is restarted with --host 127.0.0.1.",
4344
- host,
4345
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
4346
- })
4347
- );
4348
- }
4349
4438
  if ((options.requireBrowserAuth || mutableAuth.requireAuthFloor) && mutableAuth.browserTokens.size === 0 && mutableAuth.passwordHash === void 0) {
4350
4439
  sessions.clear();
4351
4440
  for (const browser of browsers) browser.close(1008, "Browser authentication removed");
@@ -4401,7 +4490,7 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
4401
4490
  const hqUrl = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${actualPort}`;
4402
4491
  await writeHqRuntimeMarker(dataDir, hqUrl).catch(() => {
4403
4492
  });
4404
- const browserTokenObj = (firstRunAuth.browserToken !== void 0 ? mutableAuth.browserTokenObjs.get(hqTokenKey3(firstRunAuth.browserToken)) : void 0) ?? mutableAuth.browserTokenObjs.values().next().value;
4493
+ const browserTokenObj = (firstRunAuth.browserToken !== void 0 ? mutableAuth.browserTokenObjs.get(hqTokenKey2(firstRunAuth.browserToken)) : void 0) ?? mutableAuth.browserTokenObjs.values().next().value;
4405
4494
  let browserUrl;
4406
4495
  if (browserTokenObj) {
4407
4496
  const code = bootstrapStore.issue({
@@ -4505,4 +4594,4 @@ export {
4505
4594
  startHqServer,
4506
4595
  HqInsecureExposureError2 as HqInsecureExposureError
4507
4596
  };
4508
- //# sourceMappingURL=chunk-C5GH4YBL.js.map
4597
+ //# sourceMappingURL=chunk-GYDQABMA.js.map