@wrongstack/cli 1.0.4 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{acp-AB54FWM2.js → acp-A5EQEXQO.js} +69 -11
- package/dist/{auth-WDMSNZ3S.js → auth-3K6EN5JP.js} +3 -3
- package/dist/boot/container-wiring.d.ts +2 -0
- package/dist/boot/short-circuit-desktop.d.ts +1 -0
- package/dist/{chunk-VUVOMXWP.js → chunk-3VXYY5JC.js} +8 -1
- package/dist/{chunk-U6PGWDMZ.js → chunk-4OLAZIIN.js} +2 -2
- package/dist/{chunk-I7V2OYVH.js → chunk-5EA6OTLJ.js} +3 -6
- package/dist/{chunk-O4A3HHKD.js → chunk-672UXYL3.js} +2 -2
- package/dist/{chunk-ONOT5OJ2.js → chunk-FSZ5QU3M.js} +2 -2
- package/dist/{chunk-KNMIBKLV.js → chunk-JY52HWL6.js} +47 -1
- package/dist/{chunk-BIWE22O5.js → chunk-RLCKKLJA.js} +8 -8
- package/dist/{chunk-VFCJOWJB.js → chunk-RMQNCHGO.js} +8 -7
- package/dist/{chunk-3I5ZJXM5.js → chunk-XIZZ2EXG.js} +161 -62
- package/dist/{cli-context-WFC3BNGM.js → cli-context-TIAP6YQK.js} +95 -15
- package/dist/cli-context.d.ts +2 -0
- package/dist/{cli-main-GU3YZLMO.js → cli-main-AKJHDSHX.js} +1111 -721
- package/dist/config-doctor.d.ts +1 -1
- package/dist/execute-deps.d.ts +3 -1
- package/dist/{execution-RAZDDQFC.js → execution-FJE27VQ6.js} +105 -28
- package/dist/{hq-5XHEIIH6.js → hq-G5PCP325.js} +8 -6
- package/dist/hq-command-controller.d.ts +15 -0
- package/dist/hq-server/auth-state.d.ts +6 -0
- package/dist/hq-server/auth.d.ts +10 -1
- package/dist/hq-server/routes/auth/common.d.ts +34 -0
- package/dist/hq-server/routes/auth/password-routes.d.ts +4 -2
- package/dist/hq-server/types.d.ts +18 -0
- package/dist/{hq-server-CNUTBWTK.js → hq-server-G3VVVMIG.js} +3 -3
- package/dist/index.js +8 -5
- package/dist/input-reader.d.ts +2 -2
- package/dist/{mcp-HOMLC4WE.js → mcp-FX7TKPTV.js} +10 -8
- package/dist/mcp-serve.d.ts +31 -0
- package/dist/{modeldiag-46CMWUNM.js → modeldiag-CJSFHZ3J.js} +55 -31
- package/dist/permission-prompt-mirror.d.ts +70 -0
- package/dist/permission-prompt.d.ts +10 -1
- package/dist/{providers-models-6MOGAOVK.js → providers-models-LFNZ5WI7.js} +2 -2
- package/dist/{short-circuit-flags-YDMLAQ5R.js → short-circuit-flags-RAT6UZVI.js} +2 -2
- package/dist/slash-commands/subagent-models.d.ts +4 -0
- package/dist/subagent-models/panel-service.d.ts +28 -0
- package/dist/subcommands/handlers/modeldiag-test.d.ts +7 -0
- package/dist/{subcommands-7N4RIK6H.js → subcommands-VFVUTYAV.js} +2 -2
- package/dist/webui-server/prefs-seeding.d.ts +8 -0
- package/dist/{webui-server-OBV5JYJV.js → webui-server-3NYXBZHX.js} +36 -12
- package/dist/wiring/hq-telemetry.d.ts +7 -0
- package/package.json +28 -28
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
sanitizeApiError,
|
|
25
25
|
truncateHqSummary,
|
|
26
26
|
writeInvalidBody
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-5EA6OTLJ.js";
|
|
28
28
|
import {
|
|
29
29
|
createHqIpAllowlist
|
|
30
30
|
} from "./chunk-Q7E3BPDU.js";
|
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
hqTokenKey as hqTokenKey2,
|
|
45
45
|
isTokenExpired as isTokenExpired4,
|
|
46
46
|
mintHqCookieSecret as mintHqCookieSecret2,
|
|
47
|
-
mutateHqAuthFile as
|
|
47
|
+
mutateHqAuthFile as mutateHqAuthFile4,
|
|
48
48
|
toAlertMessage,
|
|
49
49
|
watchHqAuthFile
|
|
50
50
|
} from "@wrongstack/core/hq";
|
|
@@ -135,10 +135,15 @@ function hasTrustedRequestAuthority(req, boundHost, boundPort, trustedPublicOrig
|
|
|
135
135
|
return false;
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
|
-
function hasTrustedBrowserOrigin(req, boundHost, boundPort, trustedPublicOrigins = /* @__PURE__ */ new Set(), allowFileOrigin = false) {
|
|
138
|
+
function hasTrustedBrowserOrigin(req, boundHost, boundPort, trustedPublicOrigins = /* @__PURE__ */ new Set(), allowFileOrigin = false, credentialedSurface = false) {
|
|
139
139
|
if (!hasTrustedRequestAuthority(req, boundHost, boundPort, trustedPublicOrigins)) return false;
|
|
140
140
|
const origin = req.headers.origin;
|
|
141
|
-
if (origin === void 0)
|
|
141
|
+
if (origin === void 0) {
|
|
142
|
+
if (!credentialedSurface) {
|
|
143
|
+
return boundHost !== void 0 && isLoopbackHost(boundHost);
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
142
147
|
try {
|
|
143
148
|
const parsed = new URL(origin);
|
|
144
149
|
if (parsed.protocol === "file:") return allowFileOrigin;
|
|
@@ -222,7 +227,7 @@ function isCookieAuth(auth) {
|
|
|
222
227
|
return auth !== void 0 && auth.kind === "cookie";
|
|
223
228
|
}
|
|
224
229
|
function hqAuthRequired(mutableAuth, requireBrowserAuth) {
|
|
225
|
-
return requireBrowserAuth === true || mutableAuth.requireAuthFloor === true || mutableAuth.browserTokens.size > 0 || mutableAuth.passwordHash !== void 0;
|
|
230
|
+
return (requireBrowserAuth ?? mutableAuth.requireBrowserAuth) === true || mutableAuth.requireAuthFloor === true || mutableAuth.browserTokens.size > 0 || mutableAuth.passwordHash !== void 0;
|
|
226
231
|
}
|
|
227
232
|
function hqClientAuthRequired(mutableAuth) {
|
|
228
233
|
return mutableAuth.requireAuthFloor === true || mutableAuth.clientTokens.size > 0;
|
|
@@ -364,7 +369,8 @@ function createHqAuthState(authFile, dataDir, opts = {}) {
|
|
|
364
369
|
clientTokens: /* @__PURE__ */ new Set(),
|
|
365
370
|
browserTokenObjs: /* @__PURE__ */ new Map(),
|
|
366
371
|
clientTokenObjs: /* @__PURE__ */ new Map(),
|
|
367
|
-
alertRules: void 0
|
|
372
|
+
alertRules: void 0,
|
|
373
|
+
requireBrowserAuth: opts.requireBrowserAuth
|
|
368
374
|
};
|
|
369
375
|
projectAuthFile(mutableAuth, authFile);
|
|
370
376
|
return {
|
|
@@ -727,7 +733,32 @@ async function serveHqStatic(_req, res, urlPath, distDir) {
|
|
|
727
733
|
}
|
|
728
734
|
|
|
729
735
|
// src/hq-server/routes/auth/common.ts
|
|
730
|
-
import { isLoopbackHost as isLoopbackHost2 } from "@wrongstack/core/hq";
|
|
736
|
+
import { isLoopbackHost as isLoopbackHost2, mutateHqAuthFile } from "@wrongstack/core/hq";
|
|
737
|
+
import { verifyTotpCounter } from "@wrongstack/core/security";
|
|
738
|
+
function recordVerifyFailure(loginAttempts, clientIp) {
|
|
739
|
+
const prev = loginAttempts.get(clientIp);
|
|
740
|
+
const count = (prev?.count ?? 0) + 1;
|
|
741
|
+
loginAttempts.recordFailure(clientIp);
|
|
742
|
+
return count;
|
|
743
|
+
}
|
|
744
|
+
async function consumeTotpCode(code, mutableAuth, dataDir, applyAuthFile) {
|
|
745
|
+
if (!mutableAuth.totpSecret) return "invalid";
|
|
746
|
+
const matchedCounter = verifyTotpCounter(code, mutableAuth.totpSecret);
|
|
747
|
+
if (matchedCounter === void 0) return "invalid";
|
|
748
|
+
if (mutableAuth.totpLastUsedCounter !== void 0 && matchedCounter <= mutableAuth.totpLastUsedCounter) {
|
|
749
|
+
return "replayed";
|
|
750
|
+
}
|
|
751
|
+
mutableAuth.totpLastUsedCounter = matchedCounter;
|
|
752
|
+
try {
|
|
753
|
+
const next = await mutateHqAuthFile(dataDir, (current) => ({
|
|
754
|
+
...current,
|
|
755
|
+
totpLastUsedCounter: matchedCounter
|
|
756
|
+
}));
|
|
757
|
+
applyAuthFile(next);
|
|
758
|
+
} catch {
|
|
759
|
+
}
|
|
760
|
+
return "ok";
|
|
761
|
+
}
|
|
731
762
|
function isLoopbackRequest(req) {
|
|
732
763
|
const address = req.socket.remoteAddress?.replace(/^::ffff:/, "");
|
|
733
764
|
return address !== void 0 && isLoopbackHost2(address);
|
|
@@ -775,7 +806,8 @@ import { randomUUID } from "node:crypto";
|
|
|
775
806
|
import {
|
|
776
807
|
hashHqPassword,
|
|
777
808
|
mintHqCookieSecret,
|
|
778
|
-
mutateHqAuthFile,
|
|
809
|
+
mutateHqAuthFile as mutateHqAuthFile2,
|
|
810
|
+
hqPasswordNeedsUpgrade,
|
|
779
811
|
verifyHqPassword
|
|
780
812
|
} from "@wrongstack/core/hq";
|
|
781
813
|
|
|
@@ -844,7 +876,7 @@ async function handleApiAuthStatus(req, res, url, mutableAuth, sessions, require
|
|
|
844
876
|
})
|
|
845
877
|
);
|
|
846
878
|
}
|
|
847
|
-
async function handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, secureCookies, trustedProxyHops, sessionCapabilities) {
|
|
879
|
+
async function handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, secureCookies, trustedProxyHops, dataDir, sessionCapabilities) {
|
|
848
880
|
if (!mutableAuth.passwordHash) {
|
|
849
881
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
850
882
|
res.end(
|
|
@@ -895,6 +927,14 @@ async function handleApiLogin(req, res, mutableAuth, sessions, loginAttempts, se
|
|
|
895
927
|
return;
|
|
896
928
|
}
|
|
897
929
|
const ok = await verifyHqPassword(body.password, mutableAuth.passwordHash);
|
|
930
|
+
if (ok && hqPasswordNeedsUpgrade(mutableAuth.passwordHash)) {
|
|
931
|
+
try {
|
|
932
|
+
const upgraded = await hashHqPassword(body.password);
|
|
933
|
+
await mutateHqAuthFile2(dataDir, (current) => ({ ...current, passwordHash: upgraded }));
|
|
934
|
+
mutableAuth.passwordHash = upgraded;
|
|
935
|
+
} catch {
|
|
936
|
+
}
|
|
937
|
+
}
|
|
898
938
|
if (!ok || !mutableAuth.cookieSecret) {
|
|
899
939
|
loginAttempts.recordFailure(clientIp, body.password);
|
|
900
940
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
@@ -953,7 +993,7 @@ async function handleApiLogout(req, res, mutableAuth, sessions, secureCookies) {
|
|
|
953
993
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
954
994
|
res.end(JSON.stringify({ loggedIn: false }));
|
|
955
995
|
}
|
|
956
|
-
async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secureCookies, requireBrowserAuth, applyAuthFile) {
|
|
996
|
+
async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secureCookies, requireBrowserAuth, applyAuthFile, loginAttempts, trustedProxyHops) {
|
|
957
997
|
const auth = authenticateBrowserRequest(
|
|
958
998
|
req,
|
|
959
999
|
new URL(req.url ?? "/", "http://localhost"),
|
|
@@ -993,21 +1033,43 @@ async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secur
|
|
|
993
1033
|
res.end(JSON.stringify({ error: { code: "BAD_REQUEST", message: "Invalid JSON body." } }));
|
|
994
1034
|
return;
|
|
995
1035
|
}
|
|
996
|
-
|
|
997
|
-
|
|
1036
|
+
if (!localOpenBootstrap && mutableAuth.passwordHash !== void 0) {
|
|
1037
|
+
const clientIp = resolveClientAddress(req, trustedProxyHops);
|
|
1038
|
+
const { blocked, retryAfter } = loginAttempts.checkBlocked(clientIp);
|
|
1039
|
+
if (blocked) {
|
|
1040
|
+
res.writeHead(429, {
|
|
1041
|
+
"Content-Type": "application/json",
|
|
1042
|
+
"Retry-After": String(retryAfter)
|
|
1043
|
+
});
|
|
1044
|
+
res.end(
|
|
1045
|
+
JSON.stringify({
|
|
1046
|
+
error: { code: "RATE_LIMITED", message: "Too many attempts. Try again later." }
|
|
1047
|
+
})
|
|
1048
|
+
);
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
998
1051
|
const currentPassword = typeof body.currentPassword === "string" ? body.currentPassword : "";
|
|
999
|
-
|
|
1052
|
+
let confirmed = currentPassword.length > 0 && await verifyHqPassword(currentPassword, mutableAuth.passwordHash);
|
|
1053
|
+
let replayed = false;
|
|
1054
|
+
if (!confirmed && typeof body.code === "string" && mutableAuth.totpSecret) {
|
|
1055
|
+
const outcome = await consumeTotpCode(body.code, mutableAuth, dataDir, applyAuthFile);
|
|
1056
|
+
confirmed = outcome === "ok";
|
|
1057
|
+
replayed = outcome === "replayed";
|
|
1058
|
+
}
|
|
1059
|
+
if (!confirmed) {
|
|
1060
|
+
recordVerifyFailure(loginAttempts, clientIp);
|
|
1000
1061
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1001
1062
|
res.end(
|
|
1002
1063
|
JSON.stringify({
|
|
1003
1064
|
error: {
|
|
1004
|
-
code: "INVALID_CURRENT_PASSWORD",
|
|
1005
|
-
message: "Current password is required to change or remove it."
|
|
1065
|
+
code: replayed ? "TOTP_ALREADY_USED" : "INVALID_CURRENT_PASSWORD",
|
|
1066
|
+
message: replayed ? "That authenticator code has already been used. Wait for the next one." : "Current password or a TOTP code is required to change or remove it."
|
|
1006
1067
|
}
|
|
1007
1068
|
})
|
|
1008
1069
|
);
|
|
1009
1070
|
return;
|
|
1010
1071
|
}
|
|
1072
|
+
loginAttempts.delete(clientIp);
|
|
1011
1073
|
}
|
|
1012
1074
|
if (req.method === "DELETE") {
|
|
1013
1075
|
if (requireBrowserAuth && mutableAuth.browserTokens.size === 0) {
|
|
@@ -1022,7 +1084,7 @@ async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secur
|
|
|
1022
1084
|
);
|
|
1023
1085
|
return;
|
|
1024
1086
|
}
|
|
1025
|
-
const next2 = await
|
|
1087
|
+
const next2 = await mutateHqAuthFile2(dataDir, (current) => {
|
|
1026
1088
|
const updated = { ...current };
|
|
1027
1089
|
delete updated.passwordHash;
|
|
1028
1090
|
delete updated.cookieSecret;
|
|
@@ -1059,7 +1121,7 @@ async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secur
|
|
|
1059
1121
|
}
|
|
1060
1122
|
const passwordHash = await hashHqPassword(newPassword);
|
|
1061
1123
|
const cookieSecret = mintHqCookieSecret();
|
|
1062
|
-
const next = await
|
|
1124
|
+
const next = await mutateHqAuthFile2(dataDir, (current) => ({
|
|
1063
1125
|
...current,
|
|
1064
1126
|
passwordHash,
|
|
1065
1127
|
cookieSecret
|
|
@@ -1083,25 +1145,18 @@ async function handleApiPassword(req, res, mutableAuth, sessions, dataDir, secur
|
|
|
1083
1145
|
|
|
1084
1146
|
// src/hq-server/routes/auth/totp-routes.ts
|
|
1085
1147
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1086
|
-
import { mutateHqAuthFile as
|
|
1148
|
+
import { mutateHqAuthFile as mutateHqAuthFile3, verifyHqPassword as verifyHqPassword2 } from "@wrongstack/core/hq";
|
|
1087
1149
|
import {
|
|
1088
1150
|
buildOtpAuthUri,
|
|
1089
1151
|
generateRecoveryCodes,
|
|
1090
1152
|
generateTotpSecret,
|
|
1091
1153
|
hashRecoveryCode,
|
|
1092
1154
|
verifyRecoveryCode,
|
|
1093
|
-
verifyTotp
|
|
1094
|
-
verifyTotpCounter
|
|
1155
|
+
verifyTotp
|
|
1095
1156
|
} from "@wrongstack/core/security";
|
|
1096
1157
|
var PENDING_2FA_TTL_MS = 5 * 6e4;
|
|
1097
1158
|
var recoveryCodeLock = Promise.resolve();
|
|
1098
1159
|
var MAX_2FA_VERIFY_FAILURES = 5;
|
|
1099
|
-
function recordVerifyFailure(loginAttempts, clientIp) {
|
|
1100
|
-
const prev = loginAttempts.get(clientIp);
|
|
1101
|
-
const count = (prev?.count ?? 0) + 1;
|
|
1102
|
-
loginAttempts.recordFailure(clientIp);
|
|
1103
|
-
return count;
|
|
1104
|
-
}
|
|
1105
1160
|
async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, loginAttempts, dataDir, secureCookies, applyAuthFile, trustedProxyHops) {
|
|
1106
1161
|
if (!mutableAuth.cookieSecret) {
|
|
1107
1162
|
res.writeHead(503, { "Content-Type": "application/json" });
|
|
@@ -1164,9 +1219,9 @@ async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, login
|
|
|
1164
1219
|
);
|
|
1165
1220
|
return;
|
|
1166
1221
|
}
|
|
1167
|
-
const
|
|
1168
|
-
const replayed =
|
|
1169
|
-
if (
|
|
1222
|
+
const outcome = await consumeTotpCode(body.code, mutableAuth, dataDir, applyAuthFile);
|
|
1223
|
+
const replayed = outcome === "replayed";
|
|
1224
|
+
if (outcome !== "ok") {
|
|
1170
1225
|
if (recordVerifyFailure(loginAttempts, clientIp) >= MAX_2FA_VERIFY_FAILURES) {
|
|
1171
1226
|
sessions.delete(sessionId);
|
|
1172
1227
|
}
|
|
@@ -1181,15 +1236,6 @@ async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, login
|
|
|
1181
1236
|
);
|
|
1182
1237
|
return;
|
|
1183
1238
|
}
|
|
1184
|
-
mutableAuth.totpLastUsedCounter = matchedCounter;
|
|
1185
|
-
try {
|
|
1186
|
-
const next = await mutateHqAuthFile2(dataDir, (current) => ({
|
|
1187
|
-
...current,
|
|
1188
|
-
totpLastUsedCounter: matchedCounter
|
|
1189
|
-
}));
|
|
1190
|
-
applyAuthFile(next);
|
|
1191
|
-
} catch {
|
|
1192
|
-
}
|
|
1193
1239
|
loginAttempts.clearOnSuccess(clientIp);
|
|
1194
1240
|
sessions.delete(sessionId);
|
|
1195
1241
|
const fullSessionId = randomUUID2();
|
|
@@ -1241,7 +1287,7 @@ async function handleApiLoginVerify(req, res, _url, mutableAuth, sessions, login
|
|
|
1241
1287
|
releaseLock = resolve;
|
|
1242
1288
|
});
|
|
1243
1289
|
try {
|
|
1244
|
-
const next = await
|
|
1290
|
+
const next = await mutateHqAuthFile3(dataDir, (current) => {
|
|
1245
1291
|
const diskHashes = current.totpRecoveryCodes ?? [];
|
|
1246
1292
|
const idx = diskHashes.indexOf(usedHash);
|
|
1247
1293
|
if (idx === -1) {
|
|
@@ -1311,7 +1357,7 @@ async function handleApiTotpSetup(req, res, mutableAuth, sessions, dataDir, appl
|
|
|
1311
1357
|
}
|
|
1312
1358
|
const secret = generateTotpSecret();
|
|
1313
1359
|
const uri = buildOtpAuthUri(secret, "HQ");
|
|
1314
|
-
const next = await
|
|
1360
|
+
const next = await mutateHqAuthFile3(dataDir, (current) => ({
|
|
1315
1361
|
...current,
|
|
1316
1362
|
totpPendingSecret: secret
|
|
1317
1363
|
}));
|
|
@@ -1362,7 +1408,7 @@ async function handleApiTotpEnable(req, res, mutableAuth, sessions, dataDir, app
|
|
|
1362
1408
|
}
|
|
1363
1409
|
const recoveryCodes = generateRecoveryCodes();
|
|
1364
1410
|
const recoveryHashes = recoveryCodes.map(hashRecoveryCode);
|
|
1365
|
-
const next = await
|
|
1411
|
+
const next = await mutateHqAuthFile3(dataDir, (current) => {
|
|
1366
1412
|
const updated = { ...current };
|
|
1367
1413
|
updated.totpSecret = pendingSecret;
|
|
1368
1414
|
delete updated.totpPendingSecret;
|
|
@@ -1399,8 +1445,11 @@ async function handleApiTotpDisable(req, res, mutableAuth, sessions, loginAttemp
|
|
|
1399
1445
|
if (typeof body.password === "string" && body.password.length > 0 && mutableAuth.passwordHash) {
|
|
1400
1446
|
confirmed = await verifyHqPassword2(body.password, mutableAuth.passwordHash);
|
|
1401
1447
|
}
|
|
1448
|
+
let replayed = false;
|
|
1402
1449
|
if (!confirmed && typeof body.code === "string" && mutableAuth.totpSecret) {
|
|
1403
|
-
|
|
1450
|
+
const outcome = await consumeTotpCode(body.code, mutableAuth, dataDir, applyAuthFile);
|
|
1451
|
+
confirmed = outcome === "ok";
|
|
1452
|
+
replayed = outcome === "replayed";
|
|
1404
1453
|
}
|
|
1405
1454
|
if (!confirmed) {
|
|
1406
1455
|
recordVerifyFailure(loginAttempts, clientIp);
|
|
@@ -1408,15 +1457,15 @@ async function handleApiTotpDisable(req, res, mutableAuth, sessions, loginAttemp
|
|
|
1408
1457
|
res.end(
|
|
1409
1458
|
JSON.stringify({
|
|
1410
1459
|
error: {
|
|
1411
|
-
code: "CONFIRMATION_REQUIRED",
|
|
1412
|
-
message: "Provide the current password or a TOTP code."
|
|
1460
|
+
code: replayed ? "TOTP_ALREADY_USED" : "CONFIRMATION_REQUIRED",
|
|
1461
|
+
message: replayed ? "That authenticator code has already been used. Wait for the next one." : "Provide the current password or a TOTP code."
|
|
1413
1462
|
}
|
|
1414
1463
|
})
|
|
1415
1464
|
);
|
|
1416
1465
|
return;
|
|
1417
1466
|
}
|
|
1418
1467
|
loginAttempts.delete(clientIp);
|
|
1419
|
-
const next = await
|
|
1468
|
+
const next = await mutateHqAuthFile3(dataDir, (current) => {
|
|
1420
1469
|
const updated = { ...current };
|
|
1421
1470
|
delete updated.totpSecret;
|
|
1422
1471
|
delete updated.totpPendingSecret;
|
|
@@ -2069,7 +2118,11 @@ async function authorizeHqCommand(input) {
|
|
|
2069
2118
|
// bootstrap exchange, which is the same thing the dashboard does.
|
|
2070
2119
|
actor: { kind: authMethod === "session" ? "user" : "remote-client", id: enqueuedBy },
|
|
2071
2120
|
surface: "hq",
|
|
2072
|
-
capability: command.type === "run-command" ? "hq.control.execute" :
|
|
2121
|
+
capability: command.type === "run-command" ? "hq.control.execute" : (
|
|
2122
|
+
// Approvals get their own capability string so a policy can allow
|
|
2123
|
+
// steering while still refusing remote approval of tool calls.
|
|
2124
|
+
command.type === "approve" || command.type === "answer-input" ? "hq.control.approve" : "hq.control.enqueue"
|
|
2125
|
+
),
|
|
2073
2126
|
subject,
|
|
2074
2127
|
risk: command.type === "run-command" ? "critical" : "high",
|
|
2075
2128
|
scope: { projectId: target.projectId },
|
|
@@ -2432,6 +2485,34 @@ async function handleApiCommand(req, res, url, mutableAuth, sessions, clients, b
|
|
|
2432
2485
|
res.end(JSON.stringify({ error: "unrecognized or malformed command", type: body.type }));
|
|
2433
2486
|
return;
|
|
2434
2487
|
}
|
|
2488
|
+
if ((validated.type === "approve" || validated.type === "answer-input") && isCookieAuth(auth) && auth.capabilities !== void 0 && !auth.capabilities.includes("control.approve")) {
|
|
2489
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
2490
|
+
res.end(
|
|
2491
|
+
JSON.stringify({
|
|
2492
|
+
error: "forbidden: browser session lacks control.approve capability"
|
|
2493
|
+
})
|
|
2494
|
+
);
|
|
2495
|
+
return;
|
|
2496
|
+
}
|
|
2497
|
+
if ((validated.type === "approve" || validated.type === "answer-input") && isTokenAuth(auth) && auth.capabilities !== void 0 && !auth.capabilities.includes("control.approve")) {
|
|
2498
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
2499
|
+
res.end(
|
|
2500
|
+
JSON.stringify({
|
|
2501
|
+
error: "forbidden: token lacks control.approve capability"
|
|
2502
|
+
})
|
|
2503
|
+
);
|
|
2504
|
+
return;
|
|
2505
|
+
}
|
|
2506
|
+
if ((validated.type === "approve" || validated.type === "answer-input") && !target.capabilities.includes("control.approve")) {
|
|
2507
|
+
res.writeHead(409, { "Content-Type": "application/json" });
|
|
2508
|
+
res.end(
|
|
2509
|
+
JSON.stringify({
|
|
2510
|
+
error: "client does not mirror approvals",
|
|
2511
|
+
clientId: body.clientId
|
|
2512
|
+
})
|
|
2513
|
+
);
|
|
2514
|
+
return;
|
|
2515
|
+
}
|
|
2435
2516
|
if (validated.type === "run-command" && isCookieAuth(auth) && auth.capabilities !== void 0 && !auth.capabilities.includes("control.execute")) {
|
|
2436
2517
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
2437
2518
|
res.end(
|
|
@@ -2830,7 +2911,11 @@ function createHqRouter(deps) {
|
|
|
2830
2911
|
host,
|
|
2831
2912
|
listeningPort(),
|
|
2832
2913
|
trustedPublicOrigins,
|
|
2833
|
-
allowFileOrigin
|
|
2914
|
+
allowFileOrigin,
|
|
2915
|
+
// WS-SEC-06: tells the guard whether anything else can authenticate
|
|
2916
|
+
// this request. In open mode a missing Origin is only acceptable on
|
|
2917
|
+
// a loopback bind.
|
|
2918
|
+
hqAuthRequired(mutableAuth, requireBrowserAuth)
|
|
2834
2919
|
)) {
|
|
2835
2920
|
res.writeHead(403, { "Content-Type": "application/json" });
|
|
2836
2921
|
res.end(JSON.stringify({ error: "forbidden: untrusted request origin" }));
|
|
@@ -2912,6 +2997,7 @@ function createHqRouter(deps) {
|
|
|
2912
2997
|
loginAttempts,
|
|
2913
2998
|
secureCookies,
|
|
2914
2999
|
trustedProxyHops,
|
|
3000
|
+
dataDir,
|
|
2915
3001
|
url.pathname === "/api/mobile/login" ? ["control.enqueue"] : void 0
|
|
2916
3002
|
);
|
|
2917
3003
|
return;
|
|
@@ -2985,7 +3071,9 @@ function createHqRouter(deps) {
|
|
|
2985
3071
|
dataDir,
|
|
2986
3072
|
secureCookies,
|
|
2987
3073
|
requireBrowserAuth,
|
|
2988
|
-
applyAuthFile
|
|
3074
|
+
applyAuthFile,
|
|
3075
|
+
loginAttempts,
|
|
3076
|
+
trustedProxyHops
|
|
2989
3077
|
);
|
|
2990
3078
|
return;
|
|
2991
3079
|
}
|
|
@@ -3466,6 +3554,7 @@ import { randomUUID as randomUUID5 } from "node:crypto";
|
|
|
3466
3554
|
import {
|
|
3467
3555
|
HQ_PROTOCOL_VERSION as HQ_PROTOCOL_VERSION2,
|
|
3468
3556
|
parseHqFrame,
|
|
3557
|
+
redactHqEvent as redactHqEvent2,
|
|
3469
3558
|
resolveHqRedactionPolicy,
|
|
3470
3559
|
tightenHqRedactionPolicy as tightenHqRedactionPolicy2,
|
|
3471
3560
|
tokenHasCapability as tokenHasCapability3
|
|
@@ -4128,16 +4217,22 @@ function handleClient(ws, clients, browsers, eventLog, auth, snapshotBroadcaster
|
|
|
4128
4217
|
sendGuarded(ws, JSON.stringify({ type: "hq.kanban_snapshot", payload: p }));
|
|
4129
4218
|
}).catch(() => void 0);
|
|
4130
4219
|
}
|
|
4131
|
-
const event =
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4220
|
+
const event = redactHqEvent2(
|
|
4221
|
+
{
|
|
4222
|
+
id: randomUUID5(),
|
|
4223
|
+
type: "client.hello",
|
|
4224
|
+
schemaVersion: HQ_PROTOCOL_VERSION2,
|
|
4225
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4226
|
+
clientId: payload.client.clientId,
|
|
4227
|
+
projectId: payload.project.projectId,
|
|
4228
|
+
seq: 0,
|
|
4229
|
+
payload: { client: payload.client, project: payload.project }
|
|
4230
|
+
},
|
|
4231
|
+
{
|
|
4232
|
+
policy: tightenHqRedactionPolicy2(declaredRedactionPolicy, auth.getOperatorPolicy()),
|
|
4233
|
+
...payload.project.projectRoot ? { projectRoot: payload.project.projectRoot } : {}
|
|
4234
|
+
}
|
|
4235
|
+
).value;
|
|
4141
4236
|
persistEvent(event);
|
|
4142
4237
|
snapshotBroadcaster.broadcast();
|
|
4143
4238
|
broadcastEvent(event, browsers);
|
|
@@ -4251,7 +4346,10 @@ function handleHqUpgrade(req, socket, head, deps) {
|
|
|
4251
4346
|
deps.host,
|
|
4252
4347
|
deps.listeningPort(),
|
|
4253
4348
|
deps.trustedPublicOrigins,
|
|
4254
|
-
deps.allowFileOrigin
|
|
4349
|
+
deps.allowFileOrigin,
|
|
4350
|
+
// WS-SEC-06 — see the guard. `/ws/browser` and `/ws/client` are the two
|
|
4351
|
+
// surfaces this branch left uncontrolled in open mode.
|
|
4352
|
+
hqAuthRequired(deps.mutableAuth, deps.requireBrowserAuth)
|
|
4255
4353
|
)) {
|
|
4256
4354
|
socket.write(
|
|
4257
4355
|
"HTTP/1.1 403 Forbidden\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n" + JSON.stringify({
|
|
@@ -4396,14 +4494,15 @@ async function startHqServerWithAuth(options, host, port, dataDir, firstRunAuth)
|
|
|
4396
4494
|
}
|
|
4397
4495
|
};
|
|
4398
4496
|
const authState = createHqAuthState(authFile, dataDir, {
|
|
4399
|
-
onApplied: (live) => reassessExposureFloor(live)
|
|
4497
|
+
onApplied: (live) => reassessExposureFloor(live),
|
|
4498
|
+
requireBrowserAuth: options.requireBrowserAuth
|
|
4400
4499
|
});
|
|
4401
4500
|
const { mutableAuth } = authState;
|
|
4402
4501
|
const loginAttempts = new LoginAttemptStore(dataDir);
|
|
4403
4502
|
await loginAttempts.load();
|
|
4404
4503
|
if (!mutableAuth.cookieSecret && mutableAuth.browserTokens.size > 0) {
|
|
4405
4504
|
const secret = mintHqCookieSecret2();
|
|
4406
|
-
const next = await
|
|
4505
|
+
const next = await mutateHqAuthFile4(dataDir, (current) => ({
|
|
4407
4506
|
...current,
|
|
4408
4507
|
cookieSecret: secret
|
|
4409
4508
|
}));
|
|
@@ -4832,4 +4931,4 @@ export {
|
|
|
4832
4931
|
startHqServer,
|
|
4833
4932
|
HqInsecureExposureError2 as HqInsecureExposureError
|
|
4834
4933
|
};
|
|
4835
|
-
//# sourceMappingURL=chunk-
|
|
4934
|
+
//# sourceMappingURL=chunk-XIZZ2EXG.js.map
|