@wrongstack/cli 0.299.0 → 0.301.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-DATHHPNT.js} +36 -17
- package/dist/acp-server-agent.d.ts +1 -1
- package/dist/{audit-2ZSFGNIF.js → audit-QRCLEHHW.js} +5 -1
- package/dist/{auth-G5YKKSRH.js → auth-JASC4K3F.js} +151 -38
- 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/boot/launch-menu.d.ts +2 -4
- package/dist/boot/tool-registry.d.ts +3 -0
- package/dist/chimera-reviewer-policy.d.ts +10 -3
- package/dist/chimera-work-registry.d.ts +19 -0
- package/dist/{chronicle-QQEQFSSE.js → chronicle-63MFCZN6.js} +26 -5
- package/dist/{chunk-YWUPHRGB.js → chunk-263G3FMB.js} +33 -10
- package/dist/{chunk-2SLSUDYS.js → chunk-2KDOFTTM.js} +312 -211
- package/dist/{chunk-TGDHN4LM.js → chunk-4GVH7V3S.js} +4 -24
- package/dist/chunk-CSAPOCBP.js +24 -0
- package/dist/{chunk-66T43Q4Y.js → chunk-ETOEGL3V.js} +6 -9
- package/dist/{chunk-PEKUKUCH.js → chunk-GUHQQG63.js} +32 -14
- package/dist/chunk-KE7E7DPX.js +38 -0
- package/dist/{chunk-ZNZJ34RF.js → chunk-MHF2IJDE.js} +21 -4
- package/dist/{chunk-4HH3YGG4.js → chunk-MRLYDHQ7.js} +8 -5
- package/dist/{chunk-L5CE52XW.js → chunk-O7XNGCRT.js} +317 -309
- package/dist/chunk-PYTFS4MC.js +238 -0
- package/dist/{chunk-DPJZQCVP.js → chunk-YMXXOOFN.js} +1 -1
- package/dist/{chunk-4U3DMA2Y.js → chunk-ZECLGUTF.js} +28 -21
- package/dist/{cli-main-3MQDFSYK.js → cli-main-XWC57MGE.js} +849 -242
- package/dist/{execution-ANOYMWPO.js → execution-LHMSMDHB.js} +174 -55
- package/dist/execution-chimera-cascade.d.ts +3 -4
- package/dist/execution-chimera-review.d.ts +2 -3
- package/dist/execution-cleanup.d.ts +3 -2
- package/dist/execution.d.ts +1 -1
- package/dist/{export-TPORYVRZ.js → export-DOBGPY23.js} +12 -1
- package/dist/fleet/budget-source.d.ts +18 -0
- package/dist/fleet/host-context.d.ts +1 -1
- package/dist/fleet/host-helpers.d.ts +14 -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/fleet/status-broadcast.d.ts +1 -1
- package/dist/{hq-BDO56CX6.js → hq-LN5CJHCM.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 +23 -1
- package/dist/hq-server/types.d.ts +10 -0
- package/dist/hq-server/ws.d.ts +17 -6
- package/dist/{hq-server-RJLJJZU7.js → hq-server-S7HZG3PA.js} +3 -2
- package/dist/hq-server.d.ts +19 -0
- package/dist/index.js +69 -104
- package/dist/live-settings-input.d.ts +5 -0
- package/dist/{mcp-CYLPUEHC.js → mcp-OHPJH73U.js} +6 -2
- package/dist/{modeldiag-HENYJ4N7.js → modeldiag-3DOK4BWW.js} +3 -3
- package/dist/{plugin-usage-VTL7ZTVC.js → plugin-usage-YEVLOR33.js} +7 -3
- package/dist/{plugins-ETY6W3DC.js → plugins-K3BAUP4D.js} +3 -3
- package/dist/profile-config-path.d.ts +0 -2
- package/dist/provider-helpers.d.ts +0 -5
- package/dist/{providers-models-GDELBHZK.js → providers-models-WEZ4EGLL.js} +38 -12
- package/dist/{replay-KCTXNMZQ.js → replay-FCBNHW3K.js} +5 -1
- package/dist/{rewind-EJMVFVLS.js → rewind-AOD3JIYX.js} +5 -1
- package/dist/{sessions-config-NLCMK3BY.js → sessions-config-XXN7267N.js} +8 -3
- package/dist/slash-commands/command-context.d.ts +19 -0
- package/dist/subcommands/flags.d.ts +23 -0
- package/dist/terminal-format.d.ts +15 -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-DD35QFNO.js} +113 -50
- package/dist/webui-server-options.d.ts +16 -0
- package/dist/wiring/brain-and-orchestration.d.ts +4 -1
- 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/mailbox-bridge-bootstrap.d.ts +3 -1
- package/dist/wiring/management-tools.d.ts +9 -1
- package/dist/wiring/pipeline.d.ts +5 -5
- package/dist/wiring/plugins.d.ts +3 -10
- package/dist/wiring/provider-utility-tools.d.ts +27 -1
- package/dist/wiring/tools.d.ts +4 -5
- package/package.json +23 -23
- package/dist/chunk-GO3TJICK.js +0 -489
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
terminalLink,
|
|
3
|
+
terminalText
|
|
4
|
+
} from "./chunk-KE7E7DPX.js";
|
|
1
5
|
import {
|
|
2
6
|
resolveAuditActor
|
|
3
7
|
} from "./chunk-Q5GTM25S.js";
|
|
@@ -21,7 +25,7 @@ import {
|
|
|
21
25
|
HqAlertEngine,
|
|
22
26
|
HqBootstrapCodeStore,
|
|
23
27
|
HqCommandAuditLog,
|
|
24
|
-
hqTokenKey as
|
|
28
|
+
hqTokenKey as hqTokenKey2,
|
|
25
29
|
hqTokenVerifier as hqTokenVerifier2,
|
|
26
30
|
mintHqCookieSecret as mintHqCookieSecret2,
|
|
27
31
|
mutateHqAuthFile as mutateHqAuthFile2,
|
|
@@ -174,6 +178,10 @@ function parseCookieHeader(cookieHeader) {
|
|
|
174
178
|
}
|
|
175
179
|
return out;
|
|
176
180
|
}
|
|
181
|
+
function readHqSessionCookie(cookieHeader) {
|
|
182
|
+
const cookies = parseCookieHeader(cookieHeader);
|
|
183
|
+
return cookies[HQ_SESSION_COOKIE_SECURE] ?? cookies[HQ_SESSION_COOKIE];
|
|
184
|
+
}
|
|
177
185
|
function setHqSessionCookie(res, value, secure) {
|
|
178
186
|
const name = cookieName(secure);
|
|
179
187
|
const parts = [
|
|
@@ -265,8 +273,7 @@ function authenticateBrowserRequest(req, url, mutableAuth, sessions) {
|
|
|
265
273
|
}
|
|
266
274
|
}
|
|
267
275
|
if (mutableAuth.cookieSecret) {
|
|
268
|
-
const
|
|
269
|
-
const raw = cookies[HQ_SESSION_COOKIE] ?? cookies["__Host-hq.session"];
|
|
276
|
+
const raw = readHqSessionCookie(req.headers.cookie);
|
|
270
277
|
if (raw) {
|
|
271
278
|
const sessionId = parseHqSessionCookie(raw, mutableAuth.cookieSecret);
|
|
272
279
|
if (sessionId) {
|
|
@@ -312,25 +319,38 @@ var TOKEN_EXPIRY_WARNING_MS = 24 * 60 * 60 * 1e3;
|
|
|
312
319
|
function liveTokens(list) {
|
|
313
320
|
return (list ?? []).filter((token) => !isTokenExpired2(token));
|
|
314
321
|
}
|
|
315
|
-
function
|
|
322
|
+
function projectAuthFile(mutableAuth, next) {
|
|
323
|
+
mutableAuth.operatorPolicy = {
|
|
324
|
+
...DEFAULT_HQ_REDACTION_POLICY,
|
|
325
|
+
...next.redactionPolicy ?? {}
|
|
326
|
+
};
|
|
327
|
+
mutableAuth.operatorPolicyOverride = next.redactionPolicy;
|
|
328
|
+
mutableAuth.browserTokens = new Set(liveTokens(next.browserTokens).map(hqTokenKey));
|
|
329
|
+
mutableAuth.clientTokens = new Set(liveTokens(next.clientTokens).map(hqTokenKey));
|
|
330
|
+
mutableAuth.browserTokenObjs = browserTokenMap(next.browserTokens);
|
|
331
|
+
mutableAuth.clientTokenObjs = new Map(
|
|
332
|
+
liveTokens(next.clientTokens).map((token) => [hqTokenKey(token), token])
|
|
333
|
+
);
|
|
334
|
+
mutableAuth.passwordHash = next.passwordHash;
|
|
335
|
+
mutableAuth.cookieSecret = next.cookieSecret;
|
|
336
|
+
mutableAuth.totpSecret = next.totpSecret;
|
|
337
|
+
mutableAuth.totpPendingSecret = next.totpPendingSecret;
|
|
338
|
+
mutableAuth.totpRecoveryCodes = next.totpRecoveryCodes;
|
|
339
|
+
mutableAuth.alertRules = next.alertRules;
|
|
340
|
+
}
|
|
341
|
+
function createHqAuthState(authFile, dataDir, opts = {}) {
|
|
316
342
|
let rawBrowserTokens = authFile.browserTokens ?? [];
|
|
317
343
|
let rawClientTokens = authFile.clientTokens ?? [];
|
|
318
344
|
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
|
|
345
|
+
operatorPolicy: { ...DEFAULT_HQ_REDACTION_POLICY },
|
|
346
|
+
operatorPolicyOverride: void 0,
|
|
347
|
+
browserTokens: /* @__PURE__ */ new Set(),
|
|
348
|
+
clientTokens: /* @__PURE__ */ new Set(),
|
|
349
|
+
browserTokenObjs: /* @__PURE__ */ new Map(),
|
|
350
|
+
clientTokenObjs: /* @__PURE__ */ new Map(),
|
|
351
|
+
alertRules: void 0
|
|
333
352
|
};
|
|
353
|
+
projectAuthFile(mutableAuth, authFile);
|
|
334
354
|
return {
|
|
335
355
|
mutableAuth,
|
|
336
356
|
tokenStats: () => {
|
|
@@ -355,23 +375,8 @@ function createHqAuthState(authFile, dataDir) {
|
|
|
355
375
|
auditPrunedTokens("client", rawClientTokens, newClient, next, dataDir);
|
|
356
376
|
rawBrowserTokens = newBrowser;
|
|
357
377
|
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;
|
|
378
|
+
projectAuthFile(mutableAuth, next);
|
|
379
|
+
opts.onApplied?.(mutableAuth);
|
|
375
380
|
}
|
|
376
381
|
};
|
|
377
382
|
}
|
|
@@ -453,24 +458,41 @@ var LoginAttemptStore = class _LoginAttemptStore {
|
|
|
453
458
|
}
|
|
454
459
|
}
|
|
455
460
|
/**
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
*
|
|
461
|
+
* Rate-limit key for a *candidate password*, independent of source IP.
|
|
462
|
+
*
|
|
463
|
+
* WS-104: this used to be `cred:${ip}:${hash}`. Embedding the IP made the
|
|
464
|
+
* documented purpose — "limit how often the SAME password can be tried from
|
|
465
|
+
* different IPs" — structurally impossible, because a rotating-IP attacker
|
|
466
|
+
* got a fresh counter with every hop, exactly the case the key exists for.
|
|
467
|
+
* The IP-scoped counter is already kept separately under the bare `ip` key.
|
|
468
|
+
*
|
|
469
|
+
* Backoff caps at 16s (see {@link recordFailure}), so a global per-password
|
|
470
|
+
* counter throttles guessing without becoming a lockout an attacker could
|
|
471
|
+
* aim at the operator.
|
|
459
472
|
*/
|
|
460
|
-
static credentialKey(
|
|
473
|
+
static credentialKey(password) {
|
|
461
474
|
const hash = createHash("sha256").update(password, "utf8").digest("hex").slice(0, 16);
|
|
462
|
-
return `cred:${
|
|
475
|
+
return `cred:${hash}`;
|
|
463
476
|
}
|
|
464
477
|
/**
|
|
465
|
-
* Check
|
|
478
|
+
* Check the IP key and — when a candidate password is supplied — the
|
|
479
|
+
* credential key too, returning the stricter (later `blockedUntil`).
|
|
480
|
+
*
|
|
481
|
+
* WS-104: `handleApiLogin` called this with `''` before parsing the request
|
|
482
|
+
* body ("we don't have the password yet") and never called it again, so the
|
|
483
|
+
* credential entries {@link recordFailure} wrote were only ever *written*.
|
|
484
|
+
* Callers now re-check once the password is known; passing no password (or
|
|
485
|
+
* an empty one) means "IP scope only".
|
|
466
486
|
*/
|
|
467
487
|
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
488
|
const now = Date.now();
|
|
489
|
+
const ipEntry = this.store.get(ip);
|
|
472
490
|
const ipBlocked = ipEntry && ipEntry.blockedUntil > now ? ipEntry.blockedUntil : 0;
|
|
473
|
-
|
|
491
|
+
let credBlocked = 0;
|
|
492
|
+
if (password !== void 0 && password.length > 0) {
|
|
493
|
+
const credEntry = this.store.get(_LoginAttemptStore.credentialKey(password));
|
|
494
|
+
credBlocked = credEntry && credEntry.blockedUntil > now ? credEntry.blockedUntil : 0;
|
|
495
|
+
}
|
|
474
496
|
const blockedUntil = Math.max(ipBlocked, credBlocked);
|
|
475
497
|
if (blockedUntil > now) {
|
|
476
498
|
return { blocked: true, retryAfter: Math.ceil((blockedUntil - now) / 1e3) };
|
|
@@ -478,7 +500,11 @@ var LoginAttemptStore = class _LoginAttemptStore {
|
|
|
478
500
|
return { blocked: false, retryAfter: 0 };
|
|
479
501
|
}
|
|
480
502
|
/**
|
|
481
|
-
* Record a failed attempt on
|
|
503
|
+
* Record a failed attempt on the IP key, and on the credential key when a
|
|
504
|
+
* candidate password is supplied. Omit the password for flows that have no
|
|
505
|
+
* one (2FA verification, TOTP disable) so they do not accumulate a
|
|
506
|
+
* meaningless `cred:sha256("")` counter shared by every such flow.
|
|
507
|
+
*
|
|
482
508
|
* Returns the updated IP entry (for the response Retry-After header).
|
|
483
509
|
*/
|
|
484
510
|
recordFailure(ip, password, maxBackoffMs = 16e3) {
|
|
@@ -491,16 +517,21 @@ var LoginAttemptStore = class _LoginAttemptStore {
|
|
|
491
517
|
};
|
|
492
518
|
};
|
|
493
519
|
this.set(ip, backoff(this.store.get(ip)));
|
|
494
|
-
|
|
495
|
-
|
|
520
|
+
if (password !== void 0 && password.length > 0) {
|
|
521
|
+
const credKey = _LoginAttemptStore.credentialKey(password);
|
|
522
|
+
this.set(credKey, backoff(this.store.get(credKey)));
|
|
523
|
+
}
|
|
496
524
|
return this.store.get(ip);
|
|
497
525
|
}
|
|
498
526
|
/**
|
|
499
|
-
* Clear
|
|
527
|
+
* Clear the IP entry — and the credential entry when a password is given —
|
|
528
|
+
* on successful login.
|
|
500
529
|
*/
|
|
501
530
|
clearOnSuccess(ip, password) {
|
|
502
531
|
this.delete(ip);
|
|
503
|
-
|
|
532
|
+
if (password !== void 0 && password.length > 0) {
|
|
533
|
+
this.delete(_LoginAttemptStore.credentialKey(password));
|
|
534
|
+
}
|
|
504
535
|
}
|
|
505
536
|
/** Number of tracked entries (for diagnostics). */
|
|
506
537
|
get size() {
|
|
@@ -1180,7 +1211,14 @@ function buildSnapshot(clients, options) {
|
|
|
1180
1211
|
completedTasks: f.payload.completedTasks,
|
|
1181
1212
|
failedTasks: f.payload.failedTasks,
|
|
1182
1213
|
...f.payload.totalCostUsd !== void 0 ? { totalCostUsd: f.payload.totalCostUsd } : {},
|
|
1183
|
-
lastActivityAt: f.lastActivityAt
|
|
1214
|
+
lastActivityAt: f.lastActivityAt,
|
|
1215
|
+
...f.payload.maxConcurrent !== void 0 ? { maxConcurrent: f.payload.maxConcurrent } : {},
|
|
1216
|
+
...f.payload.maxSpawns !== void 0 ? { maxSpawns: f.payload.maxSpawns } : {},
|
|
1217
|
+
...f.payload.usedSpawns !== void 0 ? { usedSpawns: f.payload.usedSpawns } : {},
|
|
1218
|
+
...f.payload.remainingSpawns !== void 0 ? { remainingSpawns: f.payload.remainingSpawns } : {},
|
|
1219
|
+
...f.payload.effectiveSource !== void 0 ? { effectiveSource: f.payload.effectiveSource } : {},
|
|
1220
|
+
...f.payload.checkpointMaxSpawns !== void 0 ? { checkpointMaxSpawns: f.payload.checkpointMaxSpawns } : {},
|
|
1221
|
+
...f.payload.ceilingMismatch ? { ceilingMismatch: true } : {}
|
|
1184
1222
|
}));
|
|
1185
1223
|
const mcpServers = [];
|
|
1186
1224
|
const seenMcp = /* @__PURE__ */ new Set();
|
|
@@ -1398,11 +1436,8 @@ async function authorizeHqCommand(input) {
|
|
|
1398
1436
|
// src/hq-server/routes/auth-handlers.ts
|
|
1399
1437
|
import { randomUUID } from "node:crypto";
|
|
1400
1438
|
import {
|
|
1401
|
-
DEFAULT_HQ_REDACTION_POLICY as DEFAULT_HQ_REDACTION_POLICY2,
|
|
1402
1439
|
hashHqPassword,
|
|
1403
|
-
hqTokenKey as hqTokenKey2,
|
|
1404
1440
|
isLoopbackHost as isLoopbackHost2,
|
|
1405
|
-
isTokenExpired as isTokenExpired4,
|
|
1406
1441
|
mintHqCookieSecret,
|
|
1407
1442
|
mutateHqAuthFile,
|
|
1408
1443
|
verifyHqPassword
|
|
@@ -1413,13 +1448,92 @@ import {
|
|
|
1413
1448
|
generateTotpSecret,
|
|
1414
1449
|
hashRecoveryCode,
|
|
1415
1450
|
verifyRecoveryCode,
|
|
1416
|
-
verifyTotp
|
|
1451
|
+
verifyTotp,
|
|
1452
|
+
verifyTotpCounter
|
|
1417
1453
|
} from "@wrongstack/core/security";
|
|
1454
|
+
|
|
1455
|
+
// src/hq-server/client-address.ts
|
|
1456
|
+
import { isIP as isIP2 } from "node:net";
|
|
1457
|
+
function normalizeAddress(raw) {
|
|
1458
|
+
const trimmed = raw.trim().replace(/^\[(.*)\]$/, "$1");
|
|
1459
|
+
return trimmed.replace(/^::ffff:/i, "");
|
|
1460
|
+
}
|
|
1461
|
+
function parseForwardedEntry(entry) {
|
|
1462
|
+
const value = entry.trim();
|
|
1463
|
+
if (!value) return void 0;
|
|
1464
|
+
const bracketed = /^\[([^\]]+)\](?::\d+)?$/.exec(value);
|
|
1465
|
+
if (bracketed?.[1] !== void 0) {
|
|
1466
|
+
const inner = normalizeAddress(bracketed[1]);
|
|
1467
|
+
return isIP2(inner) !== 0 ? inner : void 0;
|
|
1468
|
+
}
|
|
1469
|
+
const normalized = normalizeAddress(value);
|
|
1470
|
+
if (isIP2(normalized) !== 0) return normalized;
|
|
1471
|
+
const lastColon = normalized.lastIndexOf(":");
|
|
1472
|
+
if (lastColon > 0 && normalized.indexOf(":") === lastColon) {
|
|
1473
|
+
const host = normalized.slice(0, lastColon);
|
|
1474
|
+
if (isIP2(host) === 4) return host;
|
|
1475
|
+
}
|
|
1476
|
+
return void 0;
|
|
1477
|
+
}
|
|
1478
|
+
function forwardedChain(req) {
|
|
1479
|
+
const header = req.headers["x-forwarded-for"];
|
|
1480
|
+
if (header === void 0) return [];
|
|
1481
|
+
const raw = Array.isArray(header) ? header.join(",") : header;
|
|
1482
|
+
return raw.split(",").map((entry) => parseForwardedEntry(entry)).filter((entry) => entry !== void 0);
|
|
1483
|
+
}
|
|
1484
|
+
function resolveClientAddress(req, trustedProxyHops = 0) {
|
|
1485
|
+
const socketAddress = req.socket.remoteAddress ? normalizeAddress(req.socket.remoteAddress) : void 0;
|
|
1486
|
+
const hops = Number.isFinite(trustedProxyHops) ? Math.max(0, Math.trunc(trustedProxyHops)) : 0;
|
|
1487
|
+
if (hops === 0) return socketAddress ?? "unknown";
|
|
1488
|
+
const chain = [...socketAddress !== void 0 ? [socketAddress] : [], ...forwardedChain(req).reverse()];
|
|
1489
|
+
if (hops >= chain.length) return socketAddress ?? "unknown";
|
|
1490
|
+
return chain[hops] ?? socketAddress ?? "unknown";
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// src/hq-server/routes/auth-handlers.ts
|
|
1418
1494
|
import { readHqAuthAuditTail } from "@wrongstack/core/hq";
|
|
1419
1495
|
function isLoopbackRequest(req) {
|
|
1420
1496
|
const address = req.socket.remoteAddress?.replace(/^::ffff:/, "");
|
|
1421
1497
|
return address !== void 0 && isLoopbackHost2(address);
|
|
1422
1498
|
}
|
|
1499
|
+
var HQ_AUTH_ADMIN_CAPABILITY = "auth.admin";
|
|
1500
|
+
function callerCanAdministerAuth(auth) {
|
|
1501
|
+
if (auth === void 0) return false;
|
|
1502
|
+
if (auth.kind === "cookie" && auth.tokenId === void 0) return true;
|
|
1503
|
+
if (auth.capabilities === void 0) return true;
|
|
1504
|
+
return auth.capabilities.includes(HQ_AUTH_ADMIN_CAPABILITY);
|
|
1505
|
+
}
|
|
1506
|
+
function writeAuthAdminRequired(res) {
|
|
1507
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1508
|
+
res.end(
|
|
1509
|
+
JSON.stringify({
|
|
1510
|
+
error: {
|
|
1511
|
+
code: "AUTH_ADMIN_REQUIRED",
|
|
1512
|
+
message: `This endpoint requires the '${HQ_AUTH_ADMIN_CAPABILITY}' capability.`
|
|
1513
|
+
}
|
|
1514
|
+
})
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1517
|
+
function authorizeAuthAdmin(req, res, mutableAuth, sessions) {
|
|
1518
|
+
const auth = authenticateBrowserRequest(
|
|
1519
|
+
req,
|
|
1520
|
+
new URL(req.url ?? "/", "http://localhost"),
|
|
1521
|
+
mutableAuth,
|
|
1522
|
+
sessions
|
|
1523
|
+
);
|
|
1524
|
+
if (!auth) {
|
|
1525
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1526
|
+
res.end(
|
|
1527
|
+
JSON.stringify({ error: { code: "UNAUTHORIZED", message: "Authentication required." } })
|
|
1528
|
+
);
|
|
1529
|
+
return void 0;
|
|
1530
|
+
}
|
|
1531
|
+
if (!callerCanAdministerAuth(auth)) {
|
|
1532
|
+
writeAuthAdminRequired(res);
|
|
1533
|
+
return void 0;
|
|
1534
|
+
}
|
|
1535
|
+
return auth;
|
|
1536
|
+
}
|
|
1423
1537
|
async function handleApiAuthStatus(req, res, url, mutableAuth, sessions, requireBrowserAuth, trustedPublicOrigins, secureCookies) {
|
|
1424
1538
|
const auth = authenticateBrowserRequest(req, url, mutableAuth, sessions);
|
|
1425
1539
|
const openMode = mutableAuth.browserTokens.size === 0 && mutableAuth.passwordHash === void 0 && mutableAuth.requireAuthFloor !== true;
|
|
@@ -1440,7 +1554,7 @@ async function handleApiAuthStatus(req, res, url, mutableAuth, sessions, require
|
|
|
1440
1554
|
})
|
|
1441
1555
|
);
|
|
1442
1556
|
}
|
|
1443
|
-
async function handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, secureCookies) {
|
|
1557
|
+
async function handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, secureCookies, trustedProxyHops) {
|
|
1444
1558
|
if (!mutableAuth.passwordHash) {
|
|
1445
1559
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1446
1560
|
res.end(
|
|
@@ -1453,9 +1567,8 @@ async function handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, se
|
|
|
1453
1567
|
);
|
|
1454
1568
|
return;
|
|
1455
1569
|
}
|
|
1456
|
-
const clientIp = req
|
|
1457
|
-
const
|
|
1458
|
-
if (blocked) {
|
|
1570
|
+
const clientIp = resolveClientAddress(req, trustedProxyHops);
|
|
1571
|
+
const writeRateLimited = (retryAfter) => {
|
|
1459
1572
|
res.writeHead(429, {
|
|
1460
1573
|
"Content-Type": "application/json",
|
|
1461
1574
|
"Retry-After": String(retryAfter)
|
|
@@ -1468,6 +1581,10 @@ async function handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, se
|
|
|
1468
1581
|
}
|
|
1469
1582
|
})
|
|
1470
1583
|
);
|
|
1584
|
+
};
|
|
1585
|
+
const ipCheck = loginAttempts.checkBlocked(clientIp);
|
|
1586
|
+
if (ipCheck.blocked) {
|
|
1587
|
+
writeRateLimited(ipCheck.retryAfter);
|
|
1471
1588
|
return;
|
|
1472
1589
|
}
|
|
1473
1590
|
let body;
|
|
@@ -1482,6 +1599,11 @@ async function handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, se
|
|
|
1482
1599
|
res.end(JSON.stringify({ error: { code: "BAD_REQUEST", message: "password is required" } }));
|
|
1483
1600
|
return;
|
|
1484
1601
|
}
|
|
1602
|
+
const credCheck = loginAttempts.checkBlocked(clientIp, body.password);
|
|
1603
|
+
if (credCheck.blocked) {
|
|
1604
|
+
writeRateLimited(credCheck.retryAfter);
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1485
1607
|
const ok = await verifyHqPassword(body.password, mutableAuth.passwordHash);
|
|
1486
1608
|
if (!ok || !mutableAuth.cookieSecret) {
|
|
1487
1609
|
loginAttempts.recordFailure(clientIp, body.password);
|
|
@@ -1525,8 +1647,7 @@ async function handleApiLogout(req, res, mutableAuth, sessions, secureCookies) {
|
|
|
1525
1647
|
sessions
|
|
1526
1648
|
);
|
|
1527
1649
|
if (isCookieAuth(auth)) {
|
|
1528
|
-
const
|
|
1529
|
-
const raw = cookies[HQ_SESSION_COOKIE] ?? cookies["__Host-hq.session"];
|
|
1650
|
+
const raw = readHqSessionCookie(req.headers.cookie);
|
|
1530
1651
|
if (raw) {
|
|
1531
1652
|
const sessionId = parseHqSessionCookie(raw, mutableAuth.cookieSecret ?? "");
|
|
1532
1653
|
if (sessionId) sessions.delete(sessionId);
|
|
@@ -1536,7 +1657,7 @@ async function handleApiLogout(req, res, mutableAuth, sessions, secureCookies) {
|
|
|
1536
1657
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1537
1658
|
res.end(JSON.stringify({ loggedIn: false }));
|
|
1538
1659
|
}
|
|
1539
|
-
async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secureCookies, requireBrowserAuth) {
|
|
1660
|
+
async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secureCookies, requireBrowserAuth, applyAuthFile) {
|
|
1540
1661
|
const auth = authenticateBrowserRequest(
|
|
1541
1662
|
req,
|
|
1542
1663
|
new URL(req.url ?? "/", "http://localhost"),
|
|
@@ -1611,7 +1732,7 @@ async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secur
|
|
|
1611
1732
|
delete updated.totpRecoveryCodes;
|
|
1612
1733
|
return updated;
|
|
1613
1734
|
});
|
|
1614
|
-
applyAuthFile(
|
|
1735
|
+
applyAuthFile(next2);
|
|
1615
1736
|
sessions.clear();
|
|
1616
1737
|
clearHqSessionCookie(res, secureCookies);
|
|
1617
1738
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -1644,7 +1765,7 @@ async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secur
|
|
|
1644
1765
|
passwordHash,
|
|
1645
1766
|
cookieSecret
|
|
1646
1767
|
}));
|
|
1647
|
-
applyAuthFile(
|
|
1768
|
+
applyAuthFile(next);
|
|
1648
1769
|
sessions.clear();
|
|
1649
1770
|
if (isCookieAuth(auth) || localOpenBootstrap) {
|
|
1650
1771
|
const sessionId = randomUUID();
|
|
@@ -1660,35 +1781,6 @@ async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secur
|
|
|
1660
1781
|
})
|
|
1661
1782
|
);
|
|
1662
1783
|
}
|
|
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
1784
|
async function handleApiBootstrap(req, res, mutableAuth, sessions, secureCookies, bootstrapStore) {
|
|
1693
1785
|
if (!mutableAuth.cookieSecret) {
|
|
1694
1786
|
res.writeHead(503, { "Content-Type": "application/json" });
|
|
@@ -1821,24 +1913,23 @@ var MAX_2FA_VERIFY_FAILURES = 5;
|
|
|
1821
1913
|
function recordVerifyFailure(loginAttempts, clientIp) {
|
|
1822
1914
|
const prev = loginAttempts.get(clientIp);
|
|
1823
1915
|
const count = (prev?.count ?? 0) + 1;
|
|
1824
|
-
loginAttempts.recordFailure(clientIp
|
|
1916
|
+
loginAttempts.recordFailure(clientIp);
|
|
1825
1917
|
return count;
|
|
1826
1918
|
}
|
|
1827
|
-
async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, loginAttempts, dataDir, secureCookies) {
|
|
1919
|
+
async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, loginAttempts, dataDir, secureCookies, applyAuthFile, trustedProxyHops) {
|
|
1828
1920
|
if (!mutableAuth.cookieSecret) {
|
|
1829
1921
|
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1830
1922
|
res.end(JSON.stringify({ error: { code: "NO_COOKIE_SECRET" } }));
|
|
1831
1923
|
return;
|
|
1832
1924
|
}
|
|
1833
|
-
const clientIp = req
|
|
1834
|
-
const { blocked, retryAfter } = loginAttempts.checkBlocked(clientIp
|
|
1925
|
+
const clientIp = resolveClientAddress(req, trustedProxyHops);
|
|
1926
|
+
const { blocked, retryAfter } = loginAttempts.checkBlocked(clientIp);
|
|
1835
1927
|
if (blocked) {
|
|
1836
1928
|
res.writeHead(429, { "Content-Type": "application/json", "Retry-After": String(retryAfter) });
|
|
1837
1929
|
res.end(JSON.stringify({ error: { code: "RATE_LIMITED", message: "Too many attempts. Try again later." } }));
|
|
1838
1930
|
return;
|
|
1839
1931
|
}
|
|
1840
|
-
const
|
|
1841
|
-
const raw = cookies[HQ_SESSION_COOKIE] ?? cookies["__Host-hq.session"];
|
|
1932
|
+
const raw = readHqSessionCookie(req.headers.cookie);
|
|
1842
1933
|
const sessionId = raw ? parseHqSessionCookie(raw, mutableAuth.cookieSecret) : void 0;
|
|
1843
1934
|
if (!sessionId) {
|
|
1844
1935
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
@@ -1871,15 +1962,25 @@ async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, login
|
|
|
1871
1962
|
res.end(JSON.stringify({ error: { code: "TOTP_NOT_ENABLED", message: "2FA is not configured." } }));
|
|
1872
1963
|
return;
|
|
1873
1964
|
}
|
|
1874
|
-
|
|
1965
|
+
const matchedCounter = verifyTotpCounter(body.code, mutableAuth.totpSecret);
|
|
1966
|
+
const replayed = matchedCounter !== void 0 && mutableAuth.totpLastUsedCounter !== void 0 && matchedCounter <= mutableAuth.totpLastUsedCounter;
|
|
1967
|
+
if (matchedCounter === void 0 || replayed) {
|
|
1875
1968
|
if (recordVerifyFailure(loginAttempts, clientIp) >= MAX_2FA_VERIFY_FAILURES) {
|
|
1876
1969
|
sessions.delete(sessionId);
|
|
1877
1970
|
}
|
|
1878
1971
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1879
|
-
res.end(
|
|
1972
|
+
res.end(
|
|
1973
|
+
JSON.stringify({
|
|
1974
|
+
error: {
|
|
1975
|
+
code: replayed ? "TOTP_ALREADY_USED" : "INVALID_TOTP",
|
|
1976
|
+
message: replayed ? "That authenticator code has already been used. Wait for the next one." : "Invalid authenticator code."
|
|
1977
|
+
}
|
|
1978
|
+
})
|
|
1979
|
+
);
|
|
1880
1980
|
return;
|
|
1881
1981
|
}
|
|
1882
|
-
|
|
1982
|
+
mutableAuth.totpLastUsedCounter = matchedCounter;
|
|
1983
|
+
loginAttempts.clearOnSuccess(clientIp);
|
|
1883
1984
|
sessions.delete(sessionId);
|
|
1884
1985
|
const fullSessionId = randomUUID();
|
|
1885
1986
|
sessions.set(fullSessionId, { createdAt: Date.now(), kind: "password", lastSeenAt: Date.now() });
|
|
@@ -1935,11 +2036,11 @@ async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, login
|
|
|
1935
2036
|
res.end(JSON.stringify({ error: { code: "RECOVERY_CODE_ALREADY_USED", message: "This recovery code has already been used." } }));
|
|
1936
2037
|
return;
|
|
1937
2038
|
}
|
|
1938
|
-
applyAuthFile(
|
|
2039
|
+
applyAuthFile(next);
|
|
1939
2040
|
} finally {
|
|
1940
2041
|
releaseLock();
|
|
1941
2042
|
}
|
|
1942
|
-
loginAttempts.clearOnSuccess(clientIp
|
|
2043
|
+
loginAttempts.clearOnSuccess(clientIp);
|
|
1943
2044
|
sessions.delete(sessionId);
|
|
1944
2045
|
const fullSessionId = randomUUID();
|
|
1945
2046
|
sessions.set(fullSessionId, { createdAt: Date.now(), kind: "password", lastSeenAt: Date.now() });
|
|
@@ -1957,18 +2058,8 @@ async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, login
|
|
|
1957
2058
|
JSON.stringify({ error: { code: "BAD_REQUEST", message: "Provide a `code` or `recoveryCode`." } })
|
|
1958
2059
|
);
|
|
1959
2060
|
}
|
|
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
|
-
}
|
|
2061
|
+
async function handleApiTotpSetup(req, res, mutableAuth, sessions, dataDir, applyAuthFile) {
|
|
2062
|
+
if (!authorizeAuthAdmin(req, res, mutableAuth, sessions)) return;
|
|
1972
2063
|
if (mutableAuth.totpSecret) {
|
|
1973
2064
|
res.writeHead(409, { "Content-Type": "application/json" });
|
|
1974
2065
|
res.end(
|
|
@@ -1982,22 +2073,12 @@ async function handleApiTotpSetup(req, res, mutableAuth, sessions, dataDir) {
|
|
|
1982
2073
|
...current,
|
|
1983
2074
|
totpPendingSecret: secret
|
|
1984
2075
|
}));
|
|
1985
|
-
applyAuthFile(
|
|
2076
|
+
applyAuthFile(next);
|
|
1986
2077
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1987
2078
|
res.end(JSON.stringify({ secret, uri, enabled: false }));
|
|
1988
2079
|
}
|
|
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
|
-
}
|
|
2080
|
+
async function handleApiTotpEnable(req, res, mutableAuth, sessions, dataDir, applyAuthFile) {
|
|
2081
|
+
if (!authorizeAuthAdmin(req, res, mutableAuth, sessions)) return;
|
|
2001
2082
|
if (mutableAuth.totpSecret) {
|
|
2002
2083
|
res.writeHead(409, { "Content-Type": "application/json" });
|
|
2003
2084
|
res.end(
|
|
@@ -2040,25 +2121,15 @@ async function handleApiTotpEnable(req, res, mutableAuth, sessions, dataDir) {
|
|
|
2040
2121
|
updated.totpRecoveryCodes = recoveryHashes;
|
|
2041
2122
|
return updated;
|
|
2042
2123
|
});
|
|
2043
|
-
applyAuthFile(
|
|
2124
|
+
applyAuthFile(next);
|
|
2044
2125
|
sessions.clear();
|
|
2045
2126
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2046
2127
|
res.end(JSON.stringify({ enabled: true, recoveryCodes }));
|
|
2047
2128
|
}
|
|
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, "");
|
|
2129
|
+
async function handleApiTotpDisable(req, res, mutableAuth, sessions, loginAttempts, dataDir, applyAuthFile, trustedProxyHops) {
|
|
2130
|
+
if (!authorizeAuthAdmin(req, res, mutableAuth, sessions)) return;
|
|
2131
|
+
const clientIp = resolveClientAddress(req, trustedProxyHops);
|
|
2132
|
+
const { blocked, retryAfter } = loginAttempts.checkBlocked(clientIp);
|
|
2062
2133
|
if (blocked) {
|
|
2063
2134
|
res.writeHead(429, { "Content-Type": "application/json", "Retry-After": String(retryAfter) });
|
|
2064
2135
|
res.end(JSON.stringify({ error: { code: "RATE_LIMITED", message: "Too many attempts. Try again later." } }));
|
|
@@ -2095,7 +2166,7 @@ async function handleApiTotpDisable(req, res, mutableAuth, sessions, loginAttemp
|
|
|
2095
2166
|
delete updated.totpRecoveryCodes;
|
|
2096
2167
|
return updated;
|
|
2097
2168
|
});
|
|
2098
|
-
applyAuthFile(
|
|
2169
|
+
applyAuthFile(next);
|
|
2099
2170
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2100
2171
|
res.end(JSON.stringify({ enabled: false }));
|
|
2101
2172
|
}
|
|
@@ -2105,7 +2176,7 @@ function handleApiAuthSessions(_req, res, mutableAuth, sessions) {
|
|
|
2105
2176
|
for (const [id, session] of sessions) {
|
|
2106
2177
|
if (session.pending2fa) continue;
|
|
2107
2178
|
list.push({
|
|
2108
|
-
//
|
|
2179
|
+
// The client sends the full ID back to revoke; `shortId` is for display.
|
|
2109
2180
|
id,
|
|
2110
2181
|
shortId: id.slice(0, 8),
|
|
2111
2182
|
kind: session.kind,
|
|
@@ -2431,8 +2502,6 @@ var isTokenAuth2 = isTokenAuth;
|
|
|
2431
2502
|
var isCookieAuth2 = isCookieAuth;
|
|
2432
2503
|
var hqAuthRequired2 = hqAuthRequired;
|
|
2433
2504
|
var parseHqSessionCookie2 = parseHqSessionCookie;
|
|
2434
|
-
var parseCookieHeader2 = parseCookieHeader;
|
|
2435
|
-
var HQ_SESSION_COOKIE2 = HQ_SESSION_COOKIE;
|
|
2436
2505
|
var decodePathSegment2 = decodePathSegment;
|
|
2437
2506
|
var readRequestBody2 = readRequestBody;
|
|
2438
2507
|
var writeInvalidBody2 = writeInvalidBody;
|
|
@@ -2468,16 +2537,18 @@ function createHqRouter(deps) {
|
|
|
2468
2537
|
getMailboxGateway,
|
|
2469
2538
|
getTokenStats,
|
|
2470
2539
|
trustBoundary,
|
|
2471
|
-
bootstrapStore
|
|
2540
|
+
bootstrapStore,
|
|
2541
|
+
applyAuthFile,
|
|
2542
|
+
trustedProxyHops = 0
|
|
2472
2543
|
} = deps;
|
|
2473
2544
|
return async (req, res) => {
|
|
2474
2545
|
try {
|
|
2475
|
-
const url = new URL(req.url ?? "/", `http://${host}:${listeningPort}`);
|
|
2546
|
+
const url = new URL(req.url ?? "/", `http://${host}:${listeningPort()}`);
|
|
2476
2547
|
setHqSecurityHeaders2(res);
|
|
2477
2548
|
if (!hasTrustedBrowserOrigin2(
|
|
2478
2549
|
req,
|
|
2479
2550
|
host,
|
|
2480
|
-
listeningPort,
|
|
2551
|
+
listeningPort(),
|
|
2481
2552
|
trustedPublicOrigins,
|
|
2482
2553
|
allowFileOrigin
|
|
2483
2554
|
)) {
|
|
@@ -2516,6 +2587,21 @@ function createHqRouter(deps) {
|
|
|
2516
2587
|
await handleApiAuthStatus(req, res, url, mutableAuth, sessions, requireBrowserAuth, trustedPublicOrigins, secureCookies);
|
|
2517
2588
|
return;
|
|
2518
2589
|
}
|
|
2590
|
+
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") {
|
|
2591
|
+
const auth = authenticateBrowserRequest2(req, url, mutableAuth, sessions);
|
|
2592
|
+
if (auth !== void 0 && !callerCanAdministerAuth(auth)) {
|
|
2593
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
2594
|
+
res.end(
|
|
2595
|
+
JSON.stringify({
|
|
2596
|
+
error: {
|
|
2597
|
+
code: "AUTH_ADMIN_REQUIRED",
|
|
2598
|
+
message: "This endpoint requires the 'auth.admin' capability."
|
|
2599
|
+
}
|
|
2600
|
+
})
|
|
2601
|
+
);
|
|
2602
|
+
return;
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2519
2605
|
if (url.pathname === "/api/auth/audit" && req.method === "GET") {
|
|
2520
2606
|
handleApiAuthAudit(req, res, dataDir);
|
|
2521
2607
|
return;
|
|
@@ -2529,7 +2615,7 @@ function createHqRouter(deps) {
|
|
|
2529
2615
|
return;
|
|
2530
2616
|
}
|
|
2531
2617
|
if (url.pathname === "/api/login" && req.method === "POST") {
|
|
2532
|
-
await handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, secureCookies);
|
|
2618
|
+
await handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, secureCookies, trustedProxyHops);
|
|
2533
2619
|
return;
|
|
2534
2620
|
}
|
|
2535
2621
|
if (url.pathname === "/api/auth/bootstrap" && req.method === "POST") {
|
|
@@ -2557,23 +2643,23 @@ function createHqRouter(deps) {
|
|
|
2557
2643
|
return;
|
|
2558
2644
|
}
|
|
2559
2645
|
if (url.pathname === "/api/login/verify" && req.method === "POST") {
|
|
2560
|
-
await handleApiLoginVerify(req, res, url, mutableAuth, sessions, loginAttempts, dataDir, secureCookies);
|
|
2646
|
+
await handleApiLoginVerify(req, res, url, mutableAuth, sessions, loginAttempts, dataDir, secureCookies, applyAuthFile, trustedProxyHops);
|
|
2561
2647
|
return;
|
|
2562
2648
|
}
|
|
2563
2649
|
if (url.pathname === "/api/auth/totp/setup" && req.method === "POST") {
|
|
2564
|
-
await handleApiTotpSetup(req, res, mutableAuth, sessions, dataDir);
|
|
2650
|
+
await handleApiTotpSetup(req, res, mutableAuth, sessions, dataDir, applyAuthFile);
|
|
2565
2651
|
return;
|
|
2566
2652
|
}
|
|
2567
2653
|
if (url.pathname === "/api/auth/totp/enable" && req.method === "POST") {
|
|
2568
|
-
await handleApiTotpEnable(req, res, mutableAuth, sessions, dataDir);
|
|
2654
|
+
await handleApiTotpEnable(req, res, mutableAuth, sessions, dataDir, applyAuthFile);
|
|
2569
2655
|
return;
|
|
2570
2656
|
}
|
|
2571
2657
|
if (url.pathname === "/api/auth/totp/disable" && req.method === "POST") {
|
|
2572
|
-
await handleApiTotpDisable(req, res, mutableAuth, sessions, loginAttempts, dataDir);
|
|
2658
|
+
await handleApiTotpDisable(req, res, mutableAuth, sessions, loginAttempts, dataDir, applyAuthFile, trustedProxyHops);
|
|
2573
2659
|
return;
|
|
2574
2660
|
}
|
|
2575
2661
|
if (url.pathname === "/api/auth/password" && (req.method === "POST" || req.method === "DELETE")) {
|
|
2576
|
-
await handleApiPassword(req, res, mutableAuth, sessions, dataDir, secureCookies, requireBrowserAuth);
|
|
2662
|
+
await handleApiPassword(req, res, mutableAuth, sessions, dataDir, secureCookies, requireBrowserAuth, applyAuthFile);
|
|
2577
2663
|
return;
|
|
2578
2664
|
}
|
|
2579
2665
|
if (url.pathname === "/api/snapshot" && req.method === "GET") {
|
|
@@ -3081,17 +3167,11 @@ async function handleMailboxAction(_req, res, match, mutableAuth, sessions, data
|
|
|
3081
3167
|
// src/hq-server/startup.ts
|
|
3082
3168
|
import * as fs4 from "node:fs/promises";
|
|
3083
3169
|
import * as path6 from "node:path";
|
|
3170
|
+
import { writeHqRuntimeFile } from "@wrongstack/core/hq";
|
|
3084
3171
|
var hqRuntimeMarkerPath2 = hqRuntimeMarkerPath;
|
|
3085
3172
|
async function writeHqRuntimeMarker(dataDir, url) {
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
{ url, pid: process.pid, updatedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
3089
|
-
null,
|
|
3090
|
-
2
|
|
3091
|
-
);
|
|
3092
|
-
await fs4.mkdir(path6.dirname(file), { recursive: true });
|
|
3093
|
-
await fs4.writeFile(file, `${payload}
|
|
3094
|
-
`, { encoding: "utf8", mode: 384 });
|
|
3173
|
+
await fs4.mkdir(dataDir, { recursive: true });
|
|
3174
|
+
await writeHqRuntimeFile(dataDir, { url, pid: process.pid });
|
|
3095
3175
|
}
|
|
3096
3176
|
async function clearHqRuntimeMarker(dataDir, url) {
|
|
3097
3177
|
const file = hqRuntimeMarkerPath2(dataDir);
|
|
@@ -3106,19 +3186,23 @@ async function clearHqRuntimeMarker(dataDir, url) {
|
|
|
3106
3186
|
}
|
|
3107
3187
|
function writeHqStartupInfo(write, handle) {
|
|
3108
3188
|
const startup = handle.firstRunSetup;
|
|
3109
|
-
|
|
3110
|
-
|
|
3189
|
+
const browserUrl = buildHttpUrl(handle.host, handle.port);
|
|
3190
|
+
const clientUrl = buildClientWsUrl(handle.host, handle.port);
|
|
3191
|
+
write(
|
|
3192
|
+
`${terminalText("WrongStack HQ", "magenta")} ${terminalText("listening on", "green")} ${terminalLink(browserUrl)}
|
|
3193
|
+
`
|
|
3194
|
+
);
|
|
3111
3195
|
if (!startup) {
|
|
3112
|
-
write(
|
|
3196
|
+
write(`${terminalText("Browser endpoint:", "blue")} ${terminalLink(browserUrl)}
|
|
3113
3197
|
`);
|
|
3114
|
-
write(
|
|
3198
|
+
write(`${terminalText("Client endpoint:", "blue")} ${terminalLink(clientUrl)}
|
|
3115
3199
|
`);
|
|
3116
3200
|
writeHqLanEndpoints(write, handle, void 0);
|
|
3117
3201
|
return;
|
|
3118
3202
|
}
|
|
3119
|
-
write(
|
|
3203
|
+
write(`${terminalText("Browser endpoint:", "blue")} ${terminalLink(startup.browserUrl)}
|
|
3120
3204
|
`);
|
|
3121
|
-
write(
|
|
3205
|
+
write(`${terminalText("Client endpoint:", "blue")} ${terminalLink(startup.clientUrl)}
|
|
3122
3206
|
`);
|
|
3123
3207
|
if (startup.createdAuth) {
|
|
3124
3208
|
write(`
|
|
@@ -3144,13 +3228,23 @@ function writeHqLanEndpoints(write, handle, browserToken) {
|
|
|
3144
3228
|
if (handle.host !== "0.0.0.0" && handle.host !== "::") return;
|
|
3145
3229
|
const ips = lanIPv4Addresses();
|
|
3146
3230
|
if (ips.length === 0) return;
|
|
3147
|
-
write(
|
|
3231
|
+
write(
|
|
3232
|
+
`
|
|
3233
|
+
${terminalText("Reachable from other machines on your network:", "blue", { bold: true })}
|
|
3234
|
+
`
|
|
3235
|
+
);
|
|
3148
3236
|
for (const ip of ips) {
|
|
3149
|
-
|
|
3237
|
+
const url = buildHttpUrl(ip, handle.port, browserToken);
|
|
3238
|
+
write(` ${terminalLink(url)}
|
|
3150
3239
|
`);
|
|
3151
3240
|
}
|
|
3152
|
-
|
|
3153
|
-
|
|
3241
|
+
const firstIp = ips[0];
|
|
3242
|
+
if (firstIp === void 0) return;
|
|
3243
|
+
const clientUrl = buildHttpUrl(firstIp, handle.port);
|
|
3244
|
+
write(
|
|
3245
|
+
` ${terminalText("On another machine, set", "muted")} WRONGSTACK_HQ_URL=${terminalLink(clientUrl)}
|
|
3246
|
+
`
|
|
3247
|
+
);
|
|
3154
3248
|
}
|
|
3155
3249
|
|
|
3156
3250
|
// src/hq-server/ws.ts
|
|
@@ -3281,7 +3375,7 @@ function nextPeerEventSeq(eventLog) {
|
|
|
3281
3375
|
peerEventSeq += 1;
|
|
3282
3376
|
return peerEventSeq;
|
|
3283
3377
|
}
|
|
3284
|
-
function fanoutKanbanDelta(message, clients,
|
|
3378
|
+
function fanoutKanbanDelta(message, clients, _browsers, projectId) {
|
|
3285
3379
|
let clientsNotified = 0;
|
|
3286
3380
|
for (const peer of clients.values()) {
|
|
3287
3381
|
if (peer.projectId === projectId) {
|
|
@@ -3289,12 +3383,7 @@ function fanoutKanbanDelta(message, clients, browsers, projectId) {
|
|
|
3289
3383
|
clientsNotified++;
|
|
3290
3384
|
}
|
|
3291
3385
|
}
|
|
3292
|
-
|
|
3293
|
-
for (const browserWs of browsers) {
|
|
3294
|
-
sendGuarded(browserWs, message);
|
|
3295
|
-
browsersNotified++;
|
|
3296
|
-
}
|
|
3297
|
-
return { clientsNotified, browsersNotified };
|
|
3386
|
+
return { clientsNotified, browsersNotified: 0 };
|
|
3298
3387
|
}
|
|
3299
3388
|
function handleClientResume(ws, clients, eventLog, frame, snapshotBroadcaster) {
|
|
3300
3389
|
const client = clients.get(ws);
|
|
@@ -3896,7 +3985,30 @@ async function startHqServer(options = {}) {
|
|
|
3896
3985
|
async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth) {
|
|
3897
3986
|
const trustBoundary = options.trustBoundary ?? createCompatibilityTrustBoundary({ policyId: "hq-trusted-host-compat-v1" });
|
|
3898
3987
|
const authFile = firstRunAuth.authFile;
|
|
3899
|
-
const
|
|
3988
|
+
const reassessExposureFloor = (live) => {
|
|
3989
|
+
const exposure = assessHqExposure2({
|
|
3990
|
+
host,
|
|
3991
|
+
hasBrowserTokens: live.browserTokens.size > 0,
|
|
3992
|
+
hasPassword: live.passwordHash !== void 0,
|
|
3993
|
+
allowInsecure: options.allowInsecureOpen
|
|
3994
|
+
});
|
|
3995
|
+
const previousFloor = live.requireAuthFloor === true;
|
|
3996
|
+
live.requireAuthFloor = exposure.kind === "refuse";
|
|
3997
|
+
if (live.requireAuthFloor && !previousFloor) {
|
|
3998
|
+
console.error(
|
|
3999
|
+
JSON.stringify({
|
|
4000
|
+
level: "error",
|
|
4001
|
+
event: "hq.auth.open_mode_refused",
|
|
4002
|
+
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.",
|
|
4003
|
+
host,
|
|
4004
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4005
|
+
})
|
|
4006
|
+
);
|
|
4007
|
+
}
|
|
4008
|
+
};
|
|
4009
|
+
const authState = createHqAuthState(authFile, dataDir, {
|
|
4010
|
+
onApplied: (live) => reassessExposureFloor(live)
|
|
4011
|
+
});
|
|
3900
4012
|
const { mutableAuth } = authState;
|
|
3901
4013
|
if (!mutableAuth.cookieSecret && mutableAuth.browserTokens.size > 0) {
|
|
3902
4014
|
const secret = mintHqCookieSecret2();
|
|
@@ -3904,7 +4016,7 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
|
|
|
3904
4016
|
...current,
|
|
3905
4017
|
cookieSecret: secret
|
|
3906
4018
|
}));
|
|
3907
|
-
|
|
4019
|
+
authState.apply(next);
|
|
3908
4020
|
console.warn(
|
|
3909
4021
|
JSON.stringify({
|
|
3910
4022
|
level: "info",
|
|
@@ -4170,7 +4282,7 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
|
|
|
4170
4282
|
const routerDeps = {
|
|
4171
4283
|
trustBoundary,
|
|
4172
4284
|
host,
|
|
4173
|
-
listeningPort,
|
|
4285
|
+
listeningPort: () => listeningPort,
|
|
4174
4286
|
trustedPublicOrigins,
|
|
4175
4287
|
allowFileOrigin: options.allowFileOrigin,
|
|
4176
4288
|
mutableAuth,
|
|
@@ -4193,6 +4305,10 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
|
|
|
4193
4305
|
authorizeMailboxGateway,
|
|
4194
4306
|
getMailboxGateway,
|
|
4195
4307
|
getTokenStats: () => authState.tokenStats(),
|
|
4308
|
+
// Single projection + single floor evaluation, shared with the reload
|
|
4309
|
+
// watcher below. See `createHqAuthState`'s `onApplied` hook.
|
|
4310
|
+
applyAuthFile: (next) => authState.apply(next),
|
|
4311
|
+
trustedProxyHops: options.trustedProxyHops ?? 0,
|
|
4196
4312
|
bootstrapStore
|
|
4197
4313
|
};
|
|
4198
4314
|
const handleRequest = createHqRouter(routerDeps);
|
|
@@ -4260,8 +4376,7 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
|
|
|
4260
4376
|
}
|
|
4261
4377
|
const tokenValid = timingSafeTokenMatch(tokenSet, supplied) !== void 0;
|
|
4262
4378
|
const cookieValid = pathname === "/ws/browser" && mutableAuth.cookieSecret !== void 0 && (() => {
|
|
4263
|
-
const
|
|
4264
|
-
const raw = cookies[HQ_SESSION_COOKIE2] ?? cookies["__Host-hq.session"];
|
|
4379
|
+
const raw = readHqSessionCookie(req.headers.cookie);
|
|
4265
4380
|
if (!raw) return false;
|
|
4266
4381
|
const sessionId = parseHqSessionCookie2(raw, mutableAuth.cookieSecret);
|
|
4267
4382
|
if (sessionId === void 0) return false;
|
|
@@ -4327,25 +4442,6 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
|
|
|
4327
4442
|
dataDir,
|
|
4328
4443
|
(next) => {
|
|
4329
4444
|
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
4445
|
if ((options.requireBrowserAuth || mutableAuth.requireAuthFloor) && mutableAuth.browserTokens.size === 0 && mutableAuth.passwordHash === void 0) {
|
|
4350
4446
|
sessions.clear();
|
|
4351
4447
|
for (const browser of browsers) browser.close(1008, "Browser authentication removed");
|
|
@@ -4401,7 +4497,7 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
|
|
|
4401
4497
|
const hqUrl = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${actualPort}`;
|
|
4402
4498
|
await writeHqRuntimeMarker(dataDir, hqUrl).catch(() => {
|
|
4403
4499
|
});
|
|
4404
|
-
const browserTokenObj = (firstRunAuth.browserToken !== void 0 ? mutableAuth.browserTokenObjs.get(
|
|
4500
|
+
const browserTokenObj = (firstRunAuth.browserToken !== void 0 ? mutableAuth.browserTokenObjs.get(hqTokenKey2(firstRunAuth.browserToken)) : void 0) ?? mutableAuth.browserTokenObjs.values().next().value;
|
|
4405
4501
|
let browserUrl;
|
|
4406
4502
|
if (browserTokenObj) {
|
|
4407
4503
|
const code = bootstrapStore.issue({
|
|
@@ -4456,8 +4552,13 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
|
|
|
4456
4552
|
snapshotBroadcaster.close();
|
|
4457
4553
|
bootstrapStore.clear();
|
|
4458
4554
|
persistence.timeseries.flush();
|
|
4459
|
-
for (const ws of browsers
|
|
4460
|
-
|
|
4555
|
+
for (const ws of [...browsers, ...clients.keys()]) {
|
|
4556
|
+
try {
|
|
4557
|
+
ws.close(1001, "HQ shutting down");
|
|
4558
|
+
ws.terminate();
|
|
4559
|
+
} catch {
|
|
4560
|
+
}
|
|
4561
|
+
}
|
|
4461
4562
|
wss.close();
|
|
4462
4563
|
for (const { router } of mailboxGateways.values()) router.close();
|
|
4463
4564
|
httpServer.close(() => {
|
|
@@ -4505,4 +4606,4 @@ export {
|
|
|
4505
4606
|
startHqServer,
|
|
4506
4607
|
HqInsecureExposureError2 as HqInsecureExposureError
|
|
4507
4608
|
};
|
|
4508
|
-
//# sourceMappingURL=chunk-
|
|
4609
|
+
//# sourceMappingURL=chunk-2KDOFTTM.js.map
|