@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.
- package/dist/{acp-S4LZQB66.js → acp-IDNS2YVE.js} +3 -3
- package/dist/{auth-G5YKKSRH.js → auth-6T6ZOT2R.js} +5 -5
- package/dist/auth-menu/loopback-server.d.ts +41 -55
- package/dist/auth-menu/openai-codex-oauth.d.ts +36 -123
- package/dist/auto-discover-providers-DRW4OZHG.js +8 -0
- package/dist/boot/dispatch-webui.d.ts +3 -0
- package/dist/{chunk-PEKUKUCH.js → chunk-3MWUZMOL.js} +19 -12
- package/dist/{chunk-TGDHN4LM.js → chunk-4GVH7V3S.js} +4 -24
- package/dist/{chunk-4U3DMA2Y.js → chunk-4WJKVQJY.js} +15 -20
- package/dist/{chunk-C5GH4YBL.js → chunk-GYDQABMA.js} +271 -182
- package/dist/{chunk-4HH3YGG4.js → chunk-MRLYDHQ7.js} +8 -5
- package/dist/{chunk-66T43Q4Y.js → chunk-N7ULWBO5.js} +4 -3
- package/dist/{chunk-ZNZJ34RF.js → chunk-PEMN4T5O.js} +2 -2
- package/dist/chunk-PYTFS4MC.js +238 -0
- package/dist/{chunk-L5CE52XW.js → chunk-V76R7DC5.js} +25 -19
- package/dist/{cli-main-ZDZMCVLM.js → cli-main-7B3G2YSG.js} +943 -160
- package/dist/{execution-BY556FCF.js → execution-SHGM7BVC.js} +76 -8
- package/dist/fleet/budget-source.d.ts +18 -0
- package/dist/fleet/host-status.d.ts +31 -0
- package/dist/fleet/host-types.d.ts +8 -0
- package/dist/fleet/host.d.ts +5 -0
- package/dist/{hq-WO6CLUSI.js → hq-DVDD4XPV.js} +22 -3
- package/dist/hq-server/auth-state.d.ts +29 -1
- package/dist/hq-server/auth.d.ts +16 -0
- package/dist/hq-server/client-address.d.ts +56 -0
- package/dist/hq-server/login-attempt-store.d.ts +30 -10
- package/dist/hq-server/routes/auth-handlers.d.ts +66 -26
- package/dist/hq-server/routes.d.ts +13 -0
- package/dist/hq-server/types.d.ts +10 -0
- package/dist/{hq-server-6TSAORAC.js → hq-server-DQHLNHDL.js} +2 -2
- package/dist/hq-server.d.ts +19 -0
- package/dist/index.js +20 -20
- package/dist/{modeldiag-HENYJ4N7.js → modeldiag-YOQZHBQV.js} +2 -2
- package/dist/{plugin-usage-VTL7ZTVC.js → plugin-usage-EOG4ET4S.js} +2 -2
- package/dist/{plugins-ETY6W3DC.js → plugins-56CMWMPR.js} +3 -3
- package/dist/{providers-models-GDELBHZK.js → providers-models-K6RT7ABI.js} +34 -9
- package/dist/slash-commands/command-context.d.ts +19 -0
- package/dist/slash-commands/intake.d.ts +4 -0
- package/dist/slash-commands/memory-triage.d.ts +47 -0
- package/dist/webui-server/setup-events.d.ts +17 -0
- package/dist/webui-server/terminal-log-view.d.ts +29 -12
- package/dist/{webui-server-PCLSXX6L.js → webui-server-G3EKFXLL.js} +109 -38
- package/dist/webui-server-options.d.ts +16 -0
- package/dist/wiring/brain-and-orchestration.d.ts +3 -0
- package/dist/wiring/director-setup.d.ts +4 -0
- package/dist/wiring/fallback-gate.d.ts +31 -0
- package/dist/wiring/fleet-command-handlers.d.ts +1 -1
- package/dist/wiring/plugins.d.ts +2 -8
- package/dist/wiring/provider-utility-tools.d.ts +26 -1
- package/package.json +23 -22
- package/dist/chunk-GO3TJICK.js +0 -489
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
HqAlertEngine,
|
|
22
22
|
HqBootstrapCodeStore,
|
|
23
23
|
HqCommandAuditLog,
|
|
24
|
-
hqTokenKey as
|
|
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
|
|
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
|
|
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
|
|
320
|
-
operatorPolicyOverride:
|
|
321
|
-
browserTokens: new Set(
|
|
322
|
-
clientTokens: new Set(
|
|
323
|
-
browserTokenObjs:
|
|
324
|
-
clientTokenObjs: new Map(
|
|
325
|
-
|
|
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
|
|
359
|
-
|
|
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
|
-
*
|
|
457
|
-
*
|
|
458
|
-
*
|
|
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(
|
|
469
|
+
static credentialKey(password) {
|
|
461
470
|
const hash = createHash("sha256").update(password, "utf8").digest("hex").slice(0, 16);
|
|
462
|
-
return `cred:${
|
|
471
|
+
return `cred:${hash}`;
|
|
463
472
|
}
|
|
464
473
|
/**
|
|
465
|
-
* Check
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
495
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1457
|
-
const
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
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(
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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-
|
|
4597
|
+
//# sourceMappingURL=chunk-GYDQABMA.js.map
|