jinzd-ai-cli 0.4.113 → 0.4.115
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/auth-7KK5BOCA.js +12 -0
- package/dist/{batch-NF26OYWB.js → batch-SHNIUSW2.js} +2 -2
- package/dist/{chunk-TFYDLG7E.js → chunk-FMPWML3F.js} +63 -11
- package/dist/{chunk-2WAEM5B6.js → chunk-KQZU2VS5.js} +1 -1
- package/dist/{chunk-BYNY5JPB.js → chunk-O7NM4WTS.js} +86 -7
- package/dist/{chunk-SRU5SYZI.js → chunk-OHUHYWBR.js} +1 -1
- package/dist/{chunk-SIDKPVRD.js → chunk-PEMNYHIS.js} +1 -1
- package/dist/{chunk-3GUNDGUV.js → chunk-TJGRPTJS.js} +52 -11
- package/dist/{chunk-WLZ2PWQV.js → chunk-UZLNS3QG.js} +1 -1
- package/dist/{constants-2Z7YP252.js → constants-Y6LRE5TI.js} +1 -1
- package/dist/electron-server.js +247 -30
- package/dist/{hub-KRNH76Y3.js → hub-E3WMJGYK.js} +1 -1
- package/dist/index.js +123 -37
- package/dist/{run-tests-YOBAV24V.js → run-tests-7VYL7OVA.js} +1 -1
- package/dist/{run-tests-BUDXHVNF.js → run-tests-TWE7TJ4T.js} +2 -2
- package/dist/{server-ABNZXOV2.js → server-3P5BYK74.js} +32 -13
- package/dist/{server-MJGOR6FU.js → server-RODHACCH.js} +64 -11
- package/dist/{task-orchestrator-KZISH5DT.js → task-orchestrator-24IGVXYP.js} +3 -3
- package/dist/web/client/style.css +129 -129
- package/package.json +2 -1
- package/dist/auth-SC6KHHI3.js +0 -8
package/dist/electron-server.js
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
VERSION,
|
|
37
37
|
buildUserIdentityPrompt,
|
|
38
38
|
runTestsTool
|
|
39
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-KQZU2VS5.js";
|
|
40
40
|
import {
|
|
41
41
|
hasSemanticIndex,
|
|
42
42
|
semanticSearch
|
|
@@ -604,10 +604,20 @@ var ClaudeProvider = class extends BaseProvider {
|
|
|
604
604
|
]
|
|
605
605
|
};
|
|
606
606
|
async initialize(apiKey, options) {
|
|
607
|
-
|
|
607
|
+
const clientOptions = {
|
|
608
608
|
apiKey,
|
|
609
609
|
baseURL: options?.baseUrl
|
|
610
|
-
}
|
|
610
|
+
};
|
|
611
|
+
const proxyUrl = options?.proxy;
|
|
612
|
+
try {
|
|
613
|
+
const { Agent, ProxyAgent, fetch: undiciFetch } = await import("undici");
|
|
614
|
+
const STREAM_BODY_TIMEOUT = 30 * 60 * 1e3;
|
|
615
|
+
const STREAM_HEADERS_TIMEOUT = 5 * 60 * 1e3;
|
|
616
|
+
const dispatcher = proxyUrl ? new ProxyAgent({ uri: proxyUrl, bodyTimeout: STREAM_BODY_TIMEOUT, headersTimeout: STREAM_HEADERS_TIMEOUT }) : new Agent({ bodyTimeout: STREAM_BODY_TIMEOUT, headersTimeout: STREAM_HEADERS_TIMEOUT });
|
|
617
|
+
clientOptions.fetch = ((url, init) => undiciFetch(url, { ...init, dispatcher }));
|
|
618
|
+
} catch {
|
|
619
|
+
}
|
|
620
|
+
this.client = new Anthropic(clientOptions);
|
|
611
621
|
}
|
|
612
622
|
/**
|
|
613
623
|
* 将内部 MessageContentPart[] 格式转换为 Anthropic SDK 期望的 ContentBlockParam[]。
|
|
@@ -1464,13 +1474,20 @@ var OpenAICompatibleProvider = class extends BaseProvider {
|
|
|
1464
1474
|
timeout: this.defaultTimeout
|
|
1465
1475
|
};
|
|
1466
1476
|
const proxyUrl = options?.proxy;
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1477
|
+
try {
|
|
1478
|
+
const { Agent, ProxyAgent, fetch: undiciFetch } = await import("undici");
|
|
1479
|
+
const STREAM_BODY_TIMEOUT = 30 * 60 * 1e3;
|
|
1480
|
+
const STREAM_HEADERS_TIMEOUT = 5 * 60 * 1e3;
|
|
1481
|
+
const dispatcher = proxyUrl ? new ProxyAgent({
|
|
1482
|
+
uri: proxyUrl,
|
|
1483
|
+
bodyTimeout: STREAM_BODY_TIMEOUT,
|
|
1484
|
+
headersTimeout: STREAM_HEADERS_TIMEOUT
|
|
1485
|
+
}) : new Agent({
|
|
1486
|
+
bodyTimeout: STREAM_BODY_TIMEOUT,
|
|
1487
|
+
headersTimeout: STREAM_HEADERS_TIMEOUT
|
|
1488
|
+
});
|
|
1489
|
+
clientOptions.fetch = ((url, init) => undiciFetch(url, { ...init, dispatcher }));
|
|
1490
|
+
} catch {
|
|
1474
1491
|
}
|
|
1475
1492
|
this.client = new OpenAI(clientOptions);
|
|
1476
1493
|
}
|
|
@@ -2268,6 +2285,40 @@ function peelMetaNarration(content) {
|
|
|
2268
2285
|
}
|
|
2269
2286
|
return out.trim();
|
|
2270
2287
|
}
|
|
2288
|
+
var META_NARRATION_HARD_MARKERS = [
|
|
2289
|
+
/\[⚠️\s*CONTENT GENERATION MODE\]/,
|
|
2290
|
+
/CONTENT_ONLY_STREAM_REMINDER\b/,
|
|
2291
|
+
/<system-reminder>/i
|
|
2292
|
+
];
|
|
2293
|
+
var META_NARRATION_HEURISTICS = [
|
|
2294
|
+
/\bthe user (?:is asking me|wants me|is requesting|expects me)\b/i,
|
|
2295
|
+
/\blet me (?:re-?read|re-?consider|reconsider|think about|carefully (?:re-?read|consider))\b/i,
|
|
2296
|
+
/\bI'?m (?:in (?:a )?content-only|in CONTENT-ONLY|currently in)\b/i,
|
|
2297
|
+
/\bI think (?:there might be|I should|I cannot|the (?:user|best)|maybe)\b/i,
|
|
2298
|
+
/\bWait,?\s+let me\b/i,
|
|
2299
|
+
/\bActually,?\s+I\b/i,
|
|
2300
|
+
/\bI need to be honest with the user\b/i,
|
|
2301
|
+
/\bI(?:'m| am) in a special mode\b/i,
|
|
2302
|
+
/\bGiven that I cannot\b/i
|
|
2303
|
+
];
|
|
2304
|
+
function detectMetaNarration(content) {
|
|
2305
|
+
if (!content) return null;
|
|
2306
|
+
const head = content.slice(0, 2e3);
|
|
2307
|
+
for (const re of META_NARRATION_HARD_MARKERS) {
|
|
2308
|
+
if (re.test(head)) return re.source;
|
|
2309
|
+
}
|
|
2310
|
+
if (/^#{1,3}\s+\S/m.test(head)) return null;
|
|
2311
|
+
let hits = 0;
|
|
2312
|
+
let firstMatch = "";
|
|
2313
|
+
for (const re of META_NARRATION_HEURISTICS) {
|
|
2314
|
+
if (re.test(head)) {
|
|
2315
|
+
hits++;
|
|
2316
|
+
if (!firstMatch) firstMatch = re.source;
|
|
2317
|
+
if (hits >= 2) return `meta-narration:${firstMatch}`;
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
return null;
|
|
2321
|
+
}
|
|
2271
2322
|
function looksLikeDocumentBody(content) {
|
|
2272
2323
|
if (!content || content.length < 200) return false;
|
|
2273
2324
|
if (/^#{1,6}\s+\S/m.test(content)) return true;
|
|
@@ -3742,10 +3793,36 @@ function currentAbortSignal() {
|
|
|
3742
3793
|
return controller.signal;
|
|
3743
3794
|
}
|
|
3744
3795
|
|
|
3796
|
+
// src/tools/session-context.ts
|
|
3797
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
3798
|
+
var als = new AsyncLocalStorage();
|
|
3799
|
+
var DEFAULT_SESSION_KEY = "__default__";
|
|
3800
|
+
function getCurrentSessionKey() {
|
|
3801
|
+
const ctx = als.getStore();
|
|
3802
|
+
if (!ctx || !ctx.sessionKey) return DEFAULT_SESSION_KEY;
|
|
3803
|
+
return ctx.sessionKey;
|
|
3804
|
+
}
|
|
3805
|
+
function runWithSessionKey(sessionKey, fn) {
|
|
3806
|
+
const key = sessionKey && sessionKey.length > 0 ? sessionKey : DEFAULT_SESSION_KEY;
|
|
3807
|
+
return als.run({ sessionKey: key }, fn);
|
|
3808
|
+
}
|
|
3809
|
+
|
|
3745
3810
|
// src/tools/builtin/bash.ts
|
|
3746
3811
|
var IS_WINDOWS = platform() === "win32";
|
|
3747
3812
|
var SHELL = IS_WINDOWS ? "powershell.exe" : process.env["SHELL"] ?? "/bin/bash";
|
|
3748
|
-
var
|
|
3813
|
+
var cwdBySession = /* @__PURE__ */ new Map();
|
|
3814
|
+
function getCwd() {
|
|
3815
|
+
const key = getCurrentSessionKey();
|
|
3816
|
+
let cwd = cwdBySession.get(key);
|
|
3817
|
+
if (!cwd) {
|
|
3818
|
+
cwd = process.cwd();
|
|
3819
|
+
cwdBySession.set(key, cwd);
|
|
3820
|
+
}
|
|
3821
|
+
return cwd;
|
|
3822
|
+
}
|
|
3823
|
+
function setCwd(next) {
|
|
3824
|
+
cwdBySession.set(getCurrentSessionKey(), next);
|
|
3825
|
+
}
|
|
3749
3826
|
var bashTool = {
|
|
3750
3827
|
definition: {
|
|
3751
3828
|
name: "bash",
|
|
@@ -3790,17 +3867,19 @@ Important rules:
|
|
|
3790
3867
|
if (!command.trim()) {
|
|
3791
3868
|
throw new ToolError("bash", "command is required");
|
|
3792
3869
|
}
|
|
3793
|
-
|
|
3870
|
+
let currentCwd = getCwd();
|
|
3871
|
+
if (!existsSync4(currentCwd)) {
|
|
3794
3872
|
const fallback = process.cwd();
|
|
3795
3873
|
process.stderr.write(
|
|
3796
|
-
`[bash] Previous cwd "${
|
|
3874
|
+
`[bash] Previous cwd "${currentCwd}" no longer exists, reset to "${fallback}"
|
|
3797
3875
|
`
|
|
3798
3876
|
);
|
|
3799
|
-
|
|
3877
|
+
currentCwd = fallback;
|
|
3878
|
+
setCwd(fallback);
|
|
3800
3879
|
}
|
|
3801
|
-
let effectiveCwd =
|
|
3880
|
+
let effectiveCwd = currentCwd;
|
|
3802
3881
|
if (cwdArg) {
|
|
3803
|
-
const resolved = resolve(
|
|
3882
|
+
const resolved = resolve(currentCwd, cwdArg);
|
|
3804
3883
|
if (!existsSync4(resolved)) {
|
|
3805
3884
|
throw new ToolError(
|
|
3806
3885
|
"bash",
|
|
@@ -3808,7 +3887,7 @@ Important rules:
|
|
|
3808
3887
|
);
|
|
3809
3888
|
}
|
|
3810
3889
|
effectiveCwd = resolved;
|
|
3811
|
-
|
|
3890
|
+
setCwd(resolved);
|
|
3812
3891
|
}
|
|
3813
3892
|
let actualCommand;
|
|
3814
3893
|
if (IS_WINDOWS) {
|
|
@@ -3857,7 +3936,7 @@ Important rules:
|
|
|
3857
3936
|
}
|
|
3858
3937
|
updateCwdFromCommand(command, effectiveCwd);
|
|
3859
3938
|
pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, effectiveCwd);
|
|
3860
|
-
const result =
|
|
3939
|
+
const result = Buffer.isBuffer(stdout) ? stdout.toString("utf-8") : String(stdout ?? "");
|
|
3861
3940
|
return result || "(command completed with no output)";
|
|
3862
3941
|
} catch (err) {
|
|
3863
3942
|
pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, effectiveCwd);
|
|
@@ -4064,7 +4143,7 @@ function updateCwdFromCommand(command, baseCwd) {
|
|
|
4064
4143
|
try {
|
|
4065
4144
|
const newDir = resolve(baseCwd, target);
|
|
4066
4145
|
if (existsSync4(newDir)) {
|
|
4067
|
-
|
|
4146
|
+
setCwd(newDir);
|
|
4068
4147
|
}
|
|
4069
4148
|
} catch {
|
|
4070
4149
|
}
|
|
@@ -4885,6 +4964,16 @@ var ToolExecutor = class {
|
|
|
4885
4964
|
* 通过 /yolo 命令切换。destructive 操作仍会显示警告但不阻塞。
|
|
4886
4965
|
*/
|
|
4887
4966
|
sessionAutoApprove = false;
|
|
4967
|
+
/**
|
|
4968
|
+
* Logical session key used to scope per-session state in stateful tools
|
|
4969
|
+
* (currently only `bash`'s persistent cwd). Web mode sets this per-tab so
|
|
4970
|
+
* concurrent tabs don't share cwd. CLI/REPL leaves it undefined (the bash
|
|
4971
|
+
* tool falls back to a default key).
|
|
4972
|
+
*/
|
|
4973
|
+
sessionKey;
|
|
4974
|
+
setSessionKey(key) {
|
|
4975
|
+
this.sessionKey = key;
|
|
4976
|
+
}
|
|
4888
4977
|
/**
|
|
4889
4978
|
* 由外部(repl.ts SIGINT handler)调用,将当前 confirm() 等待视为用户按 N 取消。
|
|
4890
4979
|
* 若当前没有 confirm() 进行中,无操作。
|
|
@@ -4917,6 +5006,9 @@ var ToolExecutor = class {
|
|
|
4917
5006
|
if (opts.defaultPermission) this.defaultPermission = opts.defaultPermission;
|
|
4918
5007
|
}
|
|
4919
5008
|
async execute(call) {
|
|
5009
|
+
return runWithSessionKey(this.sessionKey, () => this.executeInner(call));
|
|
5010
|
+
}
|
|
5011
|
+
async executeInner(call) {
|
|
4920
5012
|
const tool = this.registry.get(call.name);
|
|
4921
5013
|
if (!tool) {
|
|
4922
5014
|
return {
|
|
@@ -10046,6 +10138,7 @@ var SessionHandler = class _SessionHandler {
|
|
|
10046
10138
|
this.mcpManager = shared.mcpManager;
|
|
10047
10139
|
this.skillManager = shared.skillManager;
|
|
10048
10140
|
this.toolExecutor = new ToolExecutorWeb(shared.toolRegistry, ws);
|
|
10141
|
+
this.toolExecutor.setSessionKey(`web-${Math.random().toString(36).slice(2)}-${Date.now()}`);
|
|
10049
10142
|
this.currentProvider = this.config.get("defaultProvider");
|
|
10050
10143
|
const allDefaultModels = this.config.get("defaultModels");
|
|
10051
10144
|
try {
|
|
@@ -10800,6 +10893,31 @@ ${summaryResult.content}`,
|
|
|
10800
10893
|
await new Promise((resolve7, reject) => {
|
|
10801
10894
|
fileStream.end((err) => err ? reject(err) : resolve7());
|
|
10802
10895
|
});
|
|
10896
|
+
const metaMatch = detectMetaNarration(fullContent);
|
|
10897
|
+
if (metaMatch) {
|
|
10898
|
+
try {
|
|
10899
|
+
unlinkSync4(saveToFile);
|
|
10900
|
+
} catch {
|
|
10901
|
+
}
|
|
10902
|
+
isError = true;
|
|
10903
|
+
summary = `[save_last_response REJECTED] Your output was internal reasoning / meta-narration (e.g. "Let me re-read\u2026", "the user is asking me to\u2026") instead of the requested document body (matched: ${metaMatch}). ${saveToFile} was NOT saved.
|
|
10904
|
+
|
|
10905
|
+
This fresh stream has NO tools. Produce ONLY the document body: start with a markdown heading and write the full content. Do NOT narrate that you will produce the document \u2014 produce it.`;
|
|
10906
|
+
if (teeUsage) {
|
|
10907
|
+
roundUsage.inputTokens += teeUsage.inputTokens;
|
|
10908
|
+
roundUsage.outputTokens += teeUsage.outputTokens;
|
|
10909
|
+
roundUsage.cacheCreationTokens += teeUsage.cacheCreationTokens ?? 0;
|
|
10910
|
+
roundUsage.cacheReadTokens += teeUsage.cacheReadTokens ?? 0;
|
|
10911
|
+
}
|
|
10912
|
+
this.send({
|
|
10913
|
+
type: "tool_call_result",
|
|
10914
|
+
callId: call.id,
|
|
10915
|
+
result: summary,
|
|
10916
|
+
isError: true,
|
|
10917
|
+
endTime: Date.now()
|
|
10918
|
+
});
|
|
10919
|
+
return { content: "", summary, isError: true };
|
|
10920
|
+
}
|
|
10803
10921
|
const pseudoMatch = detectPseudoToolCalls(fullContent);
|
|
10804
10922
|
if (pseudoMatch) {
|
|
10805
10923
|
const cleaned = stripPseudoToolCalls(fullContent);
|
|
@@ -10837,9 +10955,13 @@ ${summaryResult.content}`,
|
|
|
10837
10955
|
} catch {
|
|
10838
10956
|
}
|
|
10839
10957
|
}
|
|
10958
|
+
try {
|
|
10959
|
+
unlinkSync4(saveToFile);
|
|
10960
|
+
} catch {
|
|
10961
|
+
}
|
|
10840
10962
|
isError = true;
|
|
10841
10963
|
const msg = err instanceof Error ? err.message : String(err);
|
|
10842
|
-
summary = `[save_last_response failed] ${msg}
|
|
10964
|
+
summary = `[save_last_response failed] streaming was interrupted: ${msg}. ${saveToFile} (partial) was deleted. Retry \u2014 and consider producing a more compact output (split very large reports across multiple save_last_response calls if the previous attempt timed out).`;
|
|
10843
10965
|
}
|
|
10844
10966
|
this.send({
|
|
10845
10967
|
type: "tool_call_result",
|
|
@@ -11918,7 +12040,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
|
11918
12040
|
case "test": {
|
|
11919
12041
|
this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
|
|
11920
12042
|
try {
|
|
11921
|
-
const { executeTests } = await import("./run-tests-
|
|
12043
|
+
const { executeTests } = await import("./run-tests-7VYL7OVA.js");
|
|
11922
12044
|
const argStr = args.join(" ").trim();
|
|
11923
12045
|
let testArgs = {};
|
|
11924
12046
|
if (argStr) {
|
|
@@ -12902,12 +13024,16 @@ async function setupProxy(configProxy) {
|
|
|
12902
13024
|
}
|
|
12903
13025
|
|
|
12904
13026
|
// src/web/auth.ts
|
|
12905
|
-
import { existsSync as existsSync21, readFileSync as readFileSync14, writeFileSync as writeFileSync9, mkdirSync as mkdirSync10, readdirSync as readdirSync10, copyFileSync } from "fs";
|
|
13027
|
+
import { existsSync as existsSync21, readFileSync as readFileSync14, writeFileSync as writeFileSync9, mkdirSync as mkdirSync10, readdirSync as readdirSync10, copyFileSync, renameSync as renameSync2, unlinkSync as unlinkSync5 } from "fs";
|
|
12906
13028
|
import { join as join14 } from "path";
|
|
12907
13029
|
import { createHmac, randomBytes, timingSafeEqual, pbkdf2Sync } from "crypto";
|
|
12908
13030
|
var USERS_FILE = "users.json";
|
|
12909
|
-
var TOKEN_EXPIRY_HOURS = 24
|
|
13031
|
+
var TOKEN_EXPIRY_HOURS = 24;
|
|
13032
|
+
var TOKEN_EXPIRY_MS = TOKEN_EXPIRY_HOURS * 3600 * 1e3;
|
|
12910
13033
|
var USERS_DIR = "users";
|
|
13034
|
+
var LOGIN_MAX_FAILS = 5;
|
|
13035
|
+
var LOGIN_LOCKOUT_MS = 15 * 60 * 1e3;
|
|
13036
|
+
var loginAttempts = /* @__PURE__ */ new Map();
|
|
12911
13037
|
var AuthManager = class {
|
|
12912
13038
|
usersFile;
|
|
12913
13039
|
baseDir;
|
|
@@ -12958,16 +13084,30 @@ var AuthManager = class {
|
|
|
12958
13084
|
this.save();
|
|
12959
13085
|
return null;
|
|
12960
13086
|
}
|
|
12961
|
-
/**
|
|
13087
|
+
/**
|
|
13088
|
+
* Authenticate user. Returns JWT token or null on failure.
|
|
13089
|
+
*
|
|
13090
|
+
* Audit closure (5th audit, v0.4.114): integrates failed-login lockout
|
|
13091
|
+
* (CWE-307). After {@link LOGIN_MAX_FAILS} consecutive failures within a
|
|
13092
|
+
* lockout window, further attempts return null without checking the
|
|
13093
|
+
* password (avoiding pbkdf2 work + leaking timing). Successful login
|
|
13094
|
+
* resets the counter.
|
|
13095
|
+
*/
|
|
12962
13096
|
login(username, password) {
|
|
12963
13097
|
username = username.trim().toLowerCase();
|
|
13098
|
+
const lockState = this.getLockState(username);
|
|
13099
|
+
if (lockState.locked) return null;
|
|
12964
13100
|
const user = this.db.users.find((u) => u.username === username);
|
|
12965
|
-
if (!user)
|
|
13101
|
+
if (!user) {
|
|
13102
|
+
this.recordFailedLogin(username);
|
|
13103
|
+
return null;
|
|
13104
|
+
}
|
|
12966
13105
|
const isLegacy = !user.hashVersion || user.hashVersion < 2;
|
|
12967
13106
|
const hash = isLegacy ? this.hashPasswordLegacy(password, user.salt) : this.hashPassword(password, user.salt);
|
|
12968
13107
|
const a = Buffer.from(hash, "utf-8");
|
|
12969
13108
|
const b = Buffer.from(user.passwordHash, "utf-8");
|
|
12970
13109
|
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
|
13110
|
+
this.recordFailedLogin(username);
|
|
12971
13111
|
return null;
|
|
12972
13112
|
}
|
|
12973
13113
|
if (isLegacy) {
|
|
@@ -12977,8 +13117,34 @@ var AuthManager = class {
|
|
|
12977
13117
|
user.hashVersion = 2;
|
|
12978
13118
|
this.save();
|
|
12979
13119
|
}
|
|
13120
|
+
loginAttempts.delete(username);
|
|
12980
13121
|
return this.createToken(username);
|
|
12981
13122
|
}
|
|
13123
|
+
/**
|
|
13124
|
+
* Returns current lockout state for a username and lazily expires it.
|
|
13125
|
+
* Exposed (read-only) for tests and the `aicli user` CLI status output.
|
|
13126
|
+
*/
|
|
13127
|
+
getLockState(username) {
|
|
13128
|
+
username = username.trim().toLowerCase();
|
|
13129
|
+
const state = loginAttempts.get(username);
|
|
13130
|
+
if (!state) return { locked: false, remainingMs: 0, fails: 0 };
|
|
13131
|
+
if (state.lockedUntil > 0 && Date.now() >= state.lockedUntil) {
|
|
13132
|
+
loginAttempts.delete(username);
|
|
13133
|
+
return { locked: false, remainingMs: 0, fails: 0 };
|
|
13134
|
+
}
|
|
13135
|
+
if (state.lockedUntil > 0) {
|
|
13136
|
+
return { locked: true, remainingMs: state.lockedUntil - Date.now(), fails: state.fails };
|
|
13137
|
+
}
|
|
13138
|
+
return { locked: false, remainingMs: 0, fails: state.fails };
|
|
13139
|
+
}
|
|
13140
|
+
recordFailedLogin(username) {
|
|
13141
|
+
const state = loginAttempts.get(username) ?? { fails: 0, lockedUntil: 0 };
|
|
13142
|
+
state.fails += 1;
|
|
13143
|
+
if (state.fails >= LOGIN_MAX_FAILS) {
|
|
13144
|
+
state.lockedUntil = Date.now() + LOGIN_LOCKOUT_MS;
|
|
13145
|
+
}
|
|
13146
|
+
loginAttempts.set(username, state);
|
|
13147
|
+
}
|
|
12982
13148
|
/** Verify a token. Returns username or null. */
|
|
12983
13149
|
verifyToken(token) {
|
|
12984
13150
|
try {
|
|
@@ -12991,12 +13157,29 @@ var AuthManager = class {
|
|
|
12991
13157
|
Buffer.from(payloadB64, "base64url").toString("utf-8")
|
|
12992
13158
|
);
|
|
12993
13159
|
if (Date.now() > payload.exp) return null;
|
|
12994
|
-
|
|
13160
|
+
const user = this.db.users.find((u) => u.username === payload.username);
|
|
13161
|
+
if (!user) return null;
|
|
13162
|
+
if (user.tokensRevokedBefore && (!payload.iat || payload.iat < user.tokensRevokedBefore)) {
|
|
13163
|
+
return null;
|
|
13164
|
+
}
|
|
12995
13165
|
return payload.username;
|
|
12996
13166
|
} catch {
|
|
12997
13167
|
return null;
|
|
12998
13168
|
}
|
|
12999
13169
|
}
|
|
13170
|
+
/**
|
|
13171
|
+
* Revoke every outstanding token for the given user by bumping the
|
|
13172
|
+
* `tokensRevokedBefore` watermark to now. Audit closure (5th audit,
|
|
13173
|
+
* v0.4.114). Returns true if the user existed.
|
|
13174
|
+
*/
|
|
13175
|
+
logoutAll(username) {
|
|
13176
|
+
username = username.trim().toLowerCase();
|
|
13177
|
+
const user = this.db.users.find((u) => u.username === username);
|
|
13178
|
+
if (!user) return false;
|
|
13179
|
+
user.tokensRevokedBefore = Date.now() + 1;
|
|
13180
|
+
this.save();
|
|
13181
|
+
return true;
|
|
13182
|
+
}
|
|
13000
13183
|
/** Get user's data directory (absolute path) */
|
|
13001
13184
|
getUserDataDir(username) {
|
|
13002
13185
|
const user = this.db.users.find((u) => u.username === username);
|
|
@@ -13028,6 +13211,7 @@ var AuthManager = class {
|
|
|
13028
13211
|
user.passwordHash = this.hashPassword(newPassword, salt);
|
|
13029
13212
|
user.salt = salt;
|
|
13030
13213
|
user.hashVersion = 2;
|
|
13214
|
+
user.tokensRevokedBefore = Date.now() + 1;
|
|
13031
13215
|
this.save();
|
|
13032
13216
|
return null;
|
|
13033
13217
|
}
|
|
@@ -13089,7 +13273,17 @@ var AuthManager = class {
|
|
|
13089
13273
|
}
|
|
13090
13274
|
saveDB(db) {
|
|
13091
13275
|
mkdirSync10(this.baseDir, { recursive: true });
|
|
13092
|
-
|
|
13276
|
+
const tmp = `${this.usersFile}.tmp`;
|
|
13277
|
+
try {
|
|
13278
|
+
writeFileSync9(tmp, JSON.stringify(db, null, 2), "utf-8");
|
|
13279
|
+
renameSync2(tmp, this.usersFile);
|
|
13280
|
+
} catch (err) {
|
|
13281
|
+
try {
|
|
13282
|
+
unlinkSync5(tmp);
|
|
13283
|
+
} catch {
|
|
13284
|
+
}
|
|
13285
|
+
throw err;
|
|
13286
|
+
}
|
|
13093
13287
|
}
|
|
13094
13288
|
/** Legacy hash — kept only for migrating old users (v0.2.x) */
|
|
13095
13289
|
hashPasswordLegacy(password, salt) {
|
|
@@ -13099,9 +13293,11 @@ var AuthManager = class {
|
|
|
13099
13293
|
return pbkdf2Sync(password, salt, 1e5, 64, "sha512").toString("hex");
|
|
13100
13294
|
}
|
|
13101
13295
|
createToken(username) {
|
|
13296
|
+
const now = Date.now();
|
|
13102
13297
|
const payload = {
|
|
13103
13298
|
username,
|
|
13104
|
-
|
|
13299
|
+
iat: now,
|
|
13300
|
+
exp: now + TOKEN_EXPIRY_HOURS * 3600 * 1e3
|
|
13105
13301
|
};
|
|
13106
13302
|
const payloadB64 = Buffer.from(JSON.stringify(payload), "utf-8").toString("base64url");
|
|
13107
13303
|
const signature = this.sign(payloadB64);
|
|
@@ -13196,6 +13392,27 @@ async function startWebServer(options = {}) {
|
|
|
13196
13392
|
skillManager
|
|
13197
13393
|
};
|
|
13198
13394
|
const app = express();
|
|
13395
|
+
app.use((_req, res, next) => {
|
|
13396
|
+
res.setHeader(
|
|
13397
|
+
"Content-Security-Policy",
|
|
13398
|
+
[
|
|
13399
|
+
"default-src 'self'",
|
|
13400
|
+
"script-src 'self'",
|
|
13401
|
+
"style-src 'self' 'unsafe-inline'",
|
|
13402
|
+
"img-src 'self' data: blob:",
|
|
13403
|
+
"font-src 'self' data:",
|
|
13404
|
+
"connect-src 'self' ws: wss:",
|
|
13405
|
+
"frame-ancestors 'none'",
|
|
13406
|
+
"base-uri 'self'",
|
|
13407
|
+
"form-action 'self'"
|
|
13408
|
+
].join("; ")
|
|
13409
|
+
);
|
|
13410
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
13411
|
+
res.setHeader("Referrer-Policy", "no-referrer");
|
|
13412
|
+
res.setHeader("X-Frame-Options", "DENY");
|
|
13413
|
+
res.setHeader("Permissions-Policy", "geolocation=(), microphone=(), camera=()");
|
|
13414
|
+
next();
|
|
13415
|
+
});
|
|
13199
13416
|
const server = createServer(app);
|
|
13200
13417
|
const WS_MAX_PAYLOAD = 1 * 1024 * 1024;
|
|
13201
13418
|
const WS_MSG_RATE_PER_SEC = 30;
|
|
@@ -13280,7 +13497,7 @@ async function startWebServer(options = {}) {
|
|
|
13280
13497
|
}
|
|
13281
13498
|
const token = authManager.login(username, password);
|
|
13282
13499
|
console.log(` \u2713 User registered via API: ${username}${firstRun ? " (first-run)" : ""}`);
|
|
13283
|
-
res.cookie("aicli_token", token, { httpOnly: true, sameSite: "strict", maxAge:
|
|
13500
|
+
res.cookie("aicli_token", token, { httpOnly: true, sameSite: "strict", maxAge: TOKEN_EXPIRY_MS });
|
|
13284
13501
|
res.json({ success: true, username });
|
|
13285
13502
|
});
|
|
13286
13503
|
app.post("/api/auth/login", (req, res) => {
|
|
@@ -13294,7 +13511,7 @@ async function startWebServer(options = {}) {
|
|
|
13294
13511
|
res.status(401).json({ error: "Invalid username or password" });
|
|
13295
13512
|
return;
|
|
13296
13513
|
}
|
|
13297
|
-
res.cookie("aicli_token", token, { httpOnly: true, sameSite: "strict", maxAge:
|
|
13514
|
+
res.cookie("aicli_token", token, { httpOnly: true, sameSite: "strict", maxAge: TOKEN_EXPIRY_MS });
|
|
13298
13515
|
res.json({ success: true, username });
|
|
13299
13516
|
});
|
|
13300
13517
|
app.post("/api/auth/logout", (_req, res) => {
|
|
@@ -386,7 +386,7 @@ ${content}`);
|
|
|
386
386
|
}
|
|
387
387
|
}
|
|
388
388
|
async function runTaskMode(config, providers, configManager, topic) {
|
|
389
|
-
const { TaskOrchestrator } = await import("./task-orchestrator-
|
|
389
|
+
const { TaskOrchestrator } = await import("./task-orchestrator-24IGVXYP.js");
|
|
390
390
|
const orchestrator = new TaskOrchestrator(config, providers, configManager);
|
|
391
391
|
let interrupted = false;
|
|
392
392
|
const onSigint = () => {
|