@chenpu17/cc-gw 0.3.7 → 0.3.8
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/README.md +34 -0
- package/package.json +1 -1
- package/src/cli/dist/index.js +34 -2
- package/src/server/dist/index.js +446 -102
- package/src/web/dist/assets/{About-Bxxb0hlP.js → About-B4P46oBY.js} +2 -2
- package/src/web/dist/assets/{ApiKeys-B57jV3AH.js → ApiKeys-DIj-z9NS.js} +2 -2
- package/src/web/dist/assets/{Button-DIJdkdyt.js → Button-Df2SC9Z6.js} +1 -1
- package/src/web/dist/assets/{Dashboard-Cxu97ZgH.js → Dashboard-ofxha89A.js} +1 -1
- package/src/web/dist/assets/{FormField-JMg-tjrG.js → FormField-IhMRFkvg.js} +1 -1
- package/src/web/dist/assets/{Help-Bcnhv1mL.js → Help-DWp0N8et.js} +1 -1
- package/src/web/dist/assets/{Input-BOaaGybK.js → Input-BR1EKmIk.js} +1 -1
- package/src/web/dist/assets/Login-D8amVPUK.js +1 -0
- package/src/web/dist/assets/{Logs-DhIgaGtG.js → Logs-DRwOarji.js} +1 -1
- package/src/web/dist/assets/ModelManagement-CtaCkRed.js +1 -0
- package/src/web/dist/assets/PageSection-D21hdQmW.js +1 -0
- package/src/web/dist/assets/Settings-C2-eI9Yb.js +1 -0
- package/src/web/dist/assets/{StatusBadge-DOVCfoYm.js → StatusBadge-CMf3e9GN.js} +1 -1
- package/src/web/dist/assets/{copy-CG_L9fTW.js → copy-C0txfzHv.js} +1 -1
- package/src/web/dist/assets/index-BRI-aVAi.css +1 -0
- package/src/web/dist/assets/index-DtM-AT3x.js +148 -0
- package/src/web/dist/assets/{index-mV9BV7rj.js → index-oWYCCpBl.js} +16 -16
- package/src/web/dist/assets/{info-DNl8lKHj.js → info-jEzfgMxW.js} +1 -1
- package/src/web/dist/assets/useApiQuery-Dz2idWoC.js +1 -0
- package/src/web/dist/index.html +2 -2
- package/src/web/dist/assets/ModelManagement-BR4fCLUq.js +0 -1
- package/src/web/dist/assets/PageSection-Dndgc9r9.js +0 -1
- package/src/web/dist/assets/Settings-B83xPKID.js +0 -1
- package/src/web/dist/assets/index-CD8snFBp.css +0 -1
- package/src/web/dist/assets/index-COB9rriK.js +0 -143
- package/src/web/dist/assets/useApiQuery-mnvEnqVq.js +0 -6
package/src/server/dist/index.js
CHANGED
|
@@ -75,6 +75,33 @@ function sanitizeModelRoutes(input) {
|
|
|
75
75
|
}
|
|
76
76
|
return sanitized;
|
|
77
77
|
}
|
|
78
|
+
function sanitizeWebAuth(input) {
|
|
79
|
+
if (!input || typeof input !== "object") {
|
|
80
|
+
return {
|
|
81
|
+
enabled: false
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const source = input;
|
|
85
|
+
const config = {
|
|
86
|
+
enabled: Boolean(source.enabled)
|
|
87
|
+
};
|
|
88
|
+
const usernameRaw = source.username;
|
|
89
|
+
if (typeof usernameRaw === "string") {
|
|
90
|
+
const trimmed = usernameRaw.trim();
|
|
91
|
+
if (trimmed) {
|
|
92
|
+
config.username = trimmed;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const hashRaw = source.passwordHash;
|
|
96
|
+
if (typeof hashRaw === "string" && hashRaw.trim().length > 0) {
|
|
97
|
+
config.passwordHash = hashRaw.trim();
|
|
98
|
+
}
|
|
99
|
+
const saltRaw = source.passwordSalt;
|
|
100
|
+
if (typeof saltRaw === "string" && saltRaw.trim().length > 0) {
|
|
101
|
+
config.passwordSalt = saltRaw.trim();
|
|
102
|
+
}
|
|
103
|
+
return config;
|
|
104
|
+
}
|
|
78
105
|
function resolveEndpointRouting(source, fallback) {
|
|
79
106
|
const defaultsRaw = typeof source === "object" && source !== null ? source.defaults : void 0;
|
|
80
107
|
const routesRaw = typeof source === "object" && source !== null ? source.modelRoutes : void 0;
|
|
@@ -125,6 +152,9 @@ function parseConfig(raw) {
|
|
|
125
152
|
if (typeof data.responseLogging !== "boolean") {
|
|
126
153
|
data.responseLogging = data.requestLogging !== false;
|
|
127
154
|
}
|
|
155
|
+
if (typeof data.bodyLimit !== "number" || !Number.isFinite(data.bodyLimit) || data.bodyLimit <= 0) {
|
|
156
|
+
data.bodyLimit = 10 * 1024 * 1024;
|
|
157
|
+
}
|
|
128
158
|
const endpointRouting = {};
|
|
129
159
|
const sourceRouting = data.endpointRouting && typeof data.endpointRouting === "object" ? data.endpointRouting : {};
|
|
130
160
|
const fallbackAnthropic = {
|
|
@@ -187,6 +217,10 @@ function parseConfig(raw) {
|
|
|
187
217
|
}
|
|
188
218
|
}
|
|
189
219
|
data.routingPresets = routingPresets;
|
|
220
|
+
const webAuth = sanitizeWebAuth(data.webAuth);
|
|
221
|
+
if (webAuth) {
|
|
222
|
+
data.webAuth = webAuth;
|
|
223
|
+
}
|
|
190
224
|
return data;
|
|
191
225
|
}
|
|
192
226
|
function loadConfig() {
|
|
@@ -571,86 +605,6 @@ function buildProviderBody(payload, options = {}) {
|
|
|
571
605
|
}
|
|
572
606
|
return body;
|
|
573
607
|
}
|
|
574
|
-
function buildAnthropicContentFromText(text) {
|
|
575
|
-
if (!text || text.length === 0) {
|
|
576
|
-
return [];
|
|
577
|
-
}
|
|
578
|
-
return [
|
|
579
|
-
{
|
|
580
|
-
type: "text",
|
|
581
|
-
text
|
|
582
|
-
}
|
|
583
|
-
];
|
|
584
|
-
}
|
|
585
|
-
function buildAnthropicBody(payload, options = {}) {
|
|
586
|
-
const messages = [];
|
|
587
|
-
for (const message of payload.messages) {
|
|
588
|
-
const blocks = [];
|
|
589
|
-
if (message.text) {
|
|
590
|
-
blocks.push(...buildAnthropicContentFromText(message.text));
|
|
591
|
-
}
|
|
592
|
-
if (message.role === "user" && message.toolResults?.length) {
|
|
593
|
-
for (const result of message.toolResults) {
|
|
594
|
-
const content = typeof result.content === "string" ? [{ type: "text", text: result.content }] : [{ type: "text", text: JSON.stringify(result.content ?? "") }];
|
|
595
|
-
blocks.push({
|
|
596
|
-
type: "tool_result",
|
|
597
|
-
tool_use_id: result.id,
|
|
598
|
-
content,
|
|
599
|
-
cache_control: result.cacheControl
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
if (message.role === "assistant" && message.toolCalls?.length) {
|
|
604
|
-
for (const call of message.toolCalls) {
|
|
605
|
-
blocks.push({
|
|
606
|
-
type: "tool_use",
|
|
607
|
-
id: call.id,
|
|
608
|
-
name: call.name,
|
|
609
|
-
input: call.arguments ?? {},
|
|
610
|
-
cache_control: call.cacheControl
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
if (message.role === "assistant" || message.role === "user") {
|
|
615
|
-
if (blocks.length === 0) {
|
|
616
|
-
blocks.push({ type: "text", text: "" });
|
|
617
|
-
}
|
|
618
|
-
messages.push({
|
|
619
|
-
role: message.role,
|
|
620
|
-
content: blocks
|
|
621
|
-
});
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
const body = {
|
|
625
|
-
system: payload.system ?? void 0,
|
|
626
|
-
messages
|
|
627
|
-
};
|
|
628
|
-
if (options.maxTokens) {
|
|
629
|
-
body.max_tokens = options.maxTokens;
|
|
630
|
-
}
|
|
631
|
-
if (typeof options.temperature === "number") {
|
|
632
|
-
body.temperature = options.temperature;
|
|
633
|
-
}
|
|
634
|
-
if (payload.original && typeof payload.original === "object") {
|
|
635
|
-
const original = payload.original;
|
|
636
|
-
if (original.metadata && typeof original.metadata === "object") {
|
|
637
|
-
body.metadata = original.metadata;
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
const tools = options.overrideTools ?? payload.tools;
|
|
641
|
-
if (tools && tools.length > 0) {
|
|
642
|
-
body.tools = tools.map((tool) => ({
|
|
643
|
-
type: "tool",
|
|
644
|
-
name: tool.name,
|
|
645
|
-
description: tool.description,
|
|
646
|
-
input_schema: tool.input_schema ?? tool.parameters ?? {}
|
|
647
|
-
}));
|
|
648
|
-
}
|
|
649
|
-
if (options.toolChoice) {
|
|
650
|
-
body.tool_choice = options.toolChoice;
|
|
651
|
-
}
|
|
652
|
-
return body;
|
|
653
|
-
}
|
|
654
608
|
|
|
655
609
|
// providers/openai.ts
|
|
656
610
|
import { fetch } from "undici";
|
|
@@ -2216,21 +2170,39 @@ async function registerMessagesRoute(app) {
|
|
|
2216
2170
|
if (trimmed.startsWith("event:")) {
|
|
2217
2171
|
currentEvent = trimmed.slice(6).trim();
|
|
2218
2172
|
} else if (trimmed.startsWith("data:")) {
|
|
2219
|
-
if (currentEvent === "message_delta" || currentEvent === "message_stop") {
|
|
2173
|
+
if (currentEvent === "message_delta" || currentEvent === "message_stop" || currentEvent === "content_block_delta") {
|
|
2220
2174
|
try {
|
|
2221
|
-
const
|
|
2222
|
-
if (
|
|
2223
|
-
usagePrompt2 =
|
|
2224
|
-
usageCompletion2 =
|
|
2225
|
-
const maybeCached = resolveCachedTokens(
|
|
2175
|
+
const payload2 = JSON.parse(trimmed.slice(5).trim());
|
|
2176
|
+
if (payload2?.usage) {
|
|
2177
|
+
usagePrompt2 = payload2.usage.input_tokens ?? usagePrompt2;
|
|
2178
|
+
usageCompletion2 = payload2.usage.output_tokens ?? usageCompletion2;
|
|
2179
|
+
const maybeCached = resolveCachedTokens(payload2.usage);
|
|
2226
2180
|
if (maybeCached !== null) {
|
|
2227
2181
|
usageCached2 = maybeCached;
|
|
2228
2182
|
}
|
|
2229
|
-
lastUsagePayload =
|
|
2183
|
+
lastUsagePayload = payload2.usage;
|
|
2230
2184
|
}
|
|
2231
|
-
|
|
2232
|
-
if (
|
|
2233
|
-
|
|
2185
|
+
let deltaText = null;
|
|
2186
|
+
if (currentEvent === "content_block_delta") {
|
|
2187
|
+
const delta = payload2?.delta;
|
|
2188
|
+
if (delta && typeof delta === "object") {
|
|
2189
|
+
const maybeText = delta.text;
|
|
2190
|
+
if (typeof maybeText === "string") {
|
|
2191
|
+
deltaText = maybeText;
|
|
2192
|
+
} else if (Array.isArray(maybeText)) {
|
|
2193
|
+
deltaText = maybeText.filter((item) => typeof item === "string").join("");
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
} else {
|
|
2197
|
+
const maybeText = payload2?.delta?.text;
|
|
2198
|
+
if (typeof maybeText === "string") {
|
|
2199
|
+
deltaText = maybeText;
|
|
2200
|
+
} else if (Array.isArray(maybeText)) {
|
|
2201
|
+
deltaText = maybeText.filter((item) => typeof item === "string").join("");
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
if (deltaText && deltaText.length > 0) {
|
|
2205
|
+
if (!firstTokenAt2) {
|
|
2234
2206
|
firstTokenAt2 = Date.now();
|
|
2235
2207
|
}
|
|
2236
2208
|
accumulatedContent2 += deltaText;
|
|
@@ -3452,6 +3424,126 @@ async function getApiKeyUsageMetrics(days = 7, limit = 10, endpoint) {
|
|
|
3452
3424
|
}));
|
|
3453
3425
|
}
|
|
3454
3426
|
|
|
3427
|
+
// security/webAuth.ts
|
|
3428
|
+
import { randomBytes as randomBytes3, scryptSync, timingSafeEqual } from "crypto";
|
|
3429
|
+
var SESSION_COOKIE_NAME = "ccgw_session";
|
|
3430
|
+
var SESSION_TTL_MS = 1e3 * 60 * 60 * 12;
|
|
3431
|
+
var sessions = /* @__PURE__ */ new Map();
|
|
3432
|
+
function derive(password, salt) {
|
|
3433
|
+
return scryptSync(password, salt, 64);
|
|
3434
|
+
}
|
|
3435
|
+
function createPasswordRecord(password) {
|
|
3436
|
+
const salt = randomBytes3(16).toString("hex");
|
|
3437
|
+
const hash = derive(password, salt).toString("base64");
|
|
3438
|
+
return {
|
|
3439
|
+
passwordHash: hash,
|
|
3440
|
+
passwordSalt: salt
|
|
3441
|
+
};
|
|
3442
|
+
}
|
|
3443
|
+
function verifyPassword(password, record) {
|
|
3444
|
+
if (!record?.passwordHash || !record?.passwordSalt)
|
|
3445
|
+
return false;
|
|
3446
|
+
try {
|
|
3447
|
+
const expected = Buffer.from(record.passwordHash, "base64");
|
|
3448
|
+
const actual = derive(password, record.passwordSalt);
|
|
3449
|
+
if (expected.length !== actual.length)
|
|
3450
|
+
return false;
|
|
3451
|
+
return timingSafeEqual(expected, actual);
|
|
3452
|
+
} catch {
|
|
3453
|
+
return false;
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
function purgeExpiredSessions() {
|
|
3457
|
+
const now = Date.now();
|
|
3458
|
+
for (const [token, session] of sessions.entries()) {
|
|
3459
|
+
if (session.expiresAt <= now) {
|
|
3460
|
+
sessions.delete(token);
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
function buildCookieString(token, ttlMs) {
|
|
3465
|
+
const expires = new Date(Date.now() + ttlMs);
|
|
3466
|
+
const parts = [
|
|
3467
|
+
`${SESSION_COOKIE_NAME}=${token}`,
|
|
3468
|
+
"Path=/",
|
|
3469
|
+
"HttpOnly",
|
|
3470
|
+
"SameSite=Strict",
|
|
3471
|
+
`Max-Age=${Math.floor(ttlMs / 1e3)}`,
|
|
3472
|
+
`Expires=${expires.toUTCString()}`
|
|
3473
|
+
];
|
|
3474
|
+
return parts.join("; ");
|
|
3475
|
+
}
|
|
3476
|
+
function parseCookie(header) {
|
|
3477
|
+
if (!header)
|
|
3478
|
+
return {};
|
|
3479
|
+
const entries = header.split(";").map((part) => part.trim());
|
|
3480
|
+
const result = {};
|
|
3481
|
+
for (const entry of entries) {
|
|
3482
|
+
const [key, ...rest] = entry.split("=");
|
|
3483
|
+
if (!key)
|
|
3484
|
+
continue;
|
|
3485
|
+
result[key] = rest.join("=");
|
|
3486
|
+
}
|
|
3487
|
+
return result;
|
|
3488
|
+
}
|
|
3489
|
+
function getSessionByToken(token) {
|
|
3490
|
+
if (!token)
|
|
3491
|
+
return null;
|
|
3492
|
+
purgeExpiredSessions();
|
|
3493
|
+
const session = sessions.get(token);
|
|
3494
|
+
if (!session)
|
|
3495
|
+
return null;
|
|
3496
|
+
if (session.expiresAt <= Date.now()) {
|
|
3497
|
+
sessions.delete(token);
|
|
3498
|
+
return null;
|
|
3499
|
+
}
|
|
3500
|
+
session.expiresAt = Date.now() + SESSION_TTL_MS;
|
|
3501
|
+
sessions.set(token, session);
|
|
3502
|
+
return session;
|
|
3503
|
+
}
|
|
3504
|
+
function readSession(request) {
|
|
3505
|
+
const cookieHeader = request.headers.cookie;
|
|
3506
|
+
const cookies = parseCookie(typeof cookieHeader === "string" ? cookieHeader : void 0);
|
|
3507
|
+
const token = cookies[SESSION_COOKIE_NAME] ?? null;
|
|
3508
|
+
return getSessionByToken(token);
|
|
3509
|
+
}
|
|
3510
|
+
function issueSession(username) {
|
|
3511
|
+
purgeExpiredSessions();
|
|
3512
|
+
const token = randomBytes3(32).toString("base64url");
|
|
3513
|
+
const record = {
|
|
3514
|
+
token,
|
|
3515
|
+
username,
|
|
3516
|
+
expiresAt: Date.now() + SESSION_TTL_MS
|
|
3517
|
+
};
|
|
3518
|
+
sessions.set(token, record);
|
|
3519
|
+
return record;
|
|
3520
|
+
}
|
|
3521
|
+
function setSessionCookie(reply, token) {
|
|
3522
|
+
reply.header("Set-Cookie", buildCookieString(token, SESSION_TTL_MS));
|
|
3523
|
+
}
|
|
3524
|
+
function clearSessionCookie(reply) {
|
|
3525
|
+
reply.header("Set-Cookie", `${SESSION_COOKIE_NAME}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0; Expires=${(/* @__PURE__ */ new Date(0)).toUTCString()}`);
|
|
3526
|
+
}
|
|
3527
|
+
function revokeSession(token) {
|
|
3528
|
+
if (!token)
|
|
3529
|
+
return;
|
|
3530
|
+
sessions.delete(token);
|
|
3531
|
+
}
|
|
3532
|
+
function revokeAllSessions() {
|
|
3533
|
+
sessions.clear();
|
|
3534
|
+
}
|
|
3535
|
+
function getSessionToken(request) {
|
|
3536
|
+
const cookieHeader = request.headers.cookie;
|
|
3537
|
+
const cookies = parseCookie(typeof cookieHeader === "string" ? cookieHeader : void 0);
|
|
3538
|
+
return cookies[SESSION_COOKIE_NAME] ?? null;
|
|
3539
|
+
}
|
|
3540
|
+
function sanitizeUsername(username) {
|
|
3541
|
+
if (typeof username !== "string")
|
|
3542
|
+
return void 0;
|
|
3543
|
+
const trimmed = username.trim();
|
|
3544
|
+
return trimmed || void 0;
|
|
3545
|
+
}
|
|
3546
|
+
|
|
3455
3547
|
// routes/admin.ts
|
|
3456
3548
|
async function registerAdminRoutes(app) {
|
|
3457
3549
|
try {
|
|
@@ -3485,15 +3577,96 @@ async function registerAdminRoutes(app) {
|
|
|
3485
3577
|
return config.providers;
|
|
3486
3578
|
});
|
|
3487
3579
|
app.get("/api/config", async () => {
|
|
3488
|
-
|
|
3580
|
+
const config = getConfig();
|
|
3581
|
+
if (config.webAuth) {
|
|
3582
|
+
const { passwordHash, passwordSalt, ...rest } = config.webAuth;
|
|
3583
|
+
return {
|
|
3584
|
+
...config,
|
|
3585
|
+
webAuth: rest
|
|
3586
|
+
};
|
|
3587
|
+
}
|
|
3588
|
+
return config;
|
|
3489
3589
|
});
|
|
3490
3590
|
app.get("/api/config/info", async () => {
|
|
3491
3591
|
const config = getConfig();
|
|
3592
|
+
const sanitizedWebAuth = config.webAuth ? (() => {
|
|
3593
|
+
const { passwordHash, passwordSalt, ...rest } = config.webAuth;
|
|
3594
|
+
return rest;
|
|
3595
|
+
})() : void 0;
|
|
3492
3596
|
return {
|
|
3493
|
-
config,
|
|
3597
|
+
config: sanitizedWebAuth ? { ...config, webAuth: sanitizedWebAuth } : config,
|
|
3494
3598
|
path: CONFIG_PATH
|
|
3495
3599
|
};
|
|
3496
3600
|
});
|
|
3601
|
+
app.get("/api/auth/web", async () => {
|
|
3602
|
+
const config = getConfig();
|
|
3603
|
+
const auth = config.webAuth ?? { enabled: false };
|
|
3604
|
+
return {
|
|
3605
|
+
enabled: Boolean(auth.enabled),
|
|
3606
|
+
username: auth.username ?? "",
|
|
3607
|
+
hasPassword: Boolean(auth.passwordHash && auth.passwordSalt)
|
|
3608
|
+
};
|
|
3609
|
+
});
|
|
3610
|
+
app.post("/api/auth/web", async (request, reply) => {
|
|
3611
|
+
const body = request.body;
|
|
3612
|
+
if (!body || typeof body.enabled !== "boolean") {
|
|
3613
|
+
reply.code(400);
|
|
3614
|
+
return { error: "Invalid payload" };
|
|
3615
|
+
}
|
|
3616
|
+
const current = getConfig();
|
|
3617
|
+
const currentAuth = current.webAuth ?? { enabled: false };
|
|
3618
|
+
const nextAuth = { ...currentAuth };
|
|
3619
|
+
const normalizedUsername = sanitizeUsername(body.username);
|
|
3620
|
+
const rawPassword = typeof body.password === "string" ? body.password : void 0;
|
|
3621
|
+
const enforcingPassword = Boolean(rawPassword && rawPassword.length > 0);
|
|
3622
|
+
if (enforcingPassword && rawPassword.length < 6) {
|
|
3623
|
+
reply.code(400);
|
|
3624
|
+
return { error: "Password must be at least 6 characters" };
|
|
3625
|
+
}
|
|
3626
|
+
const willEnable = body.enabled;
|
|
3627
|
+
if (willEnable) {
|
|
3628
|
+
if (!normalizedUsername) {
|
|
3629
|
+
reply.code(400);
|
|
3630
|
+
return { error: "Username is required when enabling authentication" };
|
|
3631
|
+
}
|
|
3632
|
+
nextAuth.enabled = true;
|
|
3633
|
+
nextAuth.username = normalizedUsername;
|
|
3634
|
+
const usernameChanged = normalizedUsername !== (currentAuth.username ?? void 0);
|
|
3635
|
+
if (enforcingPassword) {
|
|
3636
|
+
const record = createPasswordRecord(rawPassword);
|
|
3637
|
+
nextAuth.passwordHash = record.passwordHash;
|
|
3638
|
+
nextAuth.passwordSalt = record.passwordSalt;
|
|
3639
|
+
} else if (!currentAuth.passwordHash || !currentAuth.passwordSalt || usernameChanged) {
|
|
3640
|
+
reply.code(400);
|
|
3641
|
+
return { error: "Password must be provided when enabling authentication" };
|
|
3642
|
+
}
|
|
3643
|
+
} else {
|
|
3644
|
+
nextAuth.enabled = false;
|
|
3645
|
+
nextAuth.username = normalizedUsername ?? currentAuth.username ?? void 0;
|
|
3646
|
+
if (enforcingPassword) {
|
|
3647
|
+
const record = createPasswordRecord(rawPassword);
|
|
3648
|
+
nextAuth.passwordHash = record.passwordHash;
|
|
3649
|
+
nextAuth.passwordSalt = record.passwordSalt;
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
const nextConfig = {
|
|
3653
|
+
...current,
|
|
3654
|
+
webAuth: nextAuth
|
|
3655
|
+
};
|
|
3656
|
+
updateConfig(nextConfig);
|
|
3657
|
+
const updated = getConfig();
|
|
3658
|
+
if (!willEnable || enforcingPassword || (normalizedUsername ?? "") !== (currentAuth.username ?? "")) {
|
|
3659
|
+
revokeAllSessions();
|
|
3660
|
+
}
|
|
3661
|
+
return {
|
|
3662
|
+
success: true,
|
|
3663
|
+
auth: {
|
|
3664
|
+
enabled: Boolean(updated.webAuth?.enabled),
|
|
3665
|
+
username: updated.webAuth?.username ?? "",
|
|
3666
|
+
hasPassword: Boolean(updated.webAuth?.passwordHash && updated.webAuth?.passwordSalt)
|
|
3667
|
+
}
|
|
3668
|
+
};
|
|
3669
|
+
});
|
|
3497
3670
|
app.put("/api/config", async (request, reply) => {
|
|
3498
3671
|
const body = request.body;
|
|
3499
3672
|
if (!body || typeof body.port !== "number") {
|
|
@@ -3666,6 +3839,48 @@ async function registerAdminRoutes(app) {
|
|
|
3666
3839
|
statusText: "No model configured for provider"
|
|
3667
3840
|
};
|
|
3668
3841
|
}
|
|
3842
|
+
const rawBody = request.body;
|
|
3843
|
+
const maskSensitiveHeaders = (headers) => {
|
|
3844
|
+
if (!headers)
|
|
3845
|
+
return null;
|
|
3846
|
+
const masked = {};
|
|
3847
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
3848
|
+
const lower = key.toLowerCase();
|
|
3849
|
+
if (lower.includes("authorization") || lower.includes("api-key")) {
|
|
3850
|
+
masked[key] = "<redacted>";
|
|
3851
|
+
} else {
|
|
3852
|
+
masked[key] = value;
|
|
3853
|
+
}
|
|
3854
|
+
}
|
|
3855
|
+
return masked;
|
|
3856
|
+
};
|
|
3857
|
+
const providedHeaders = (() => {
|
|
3858
|
+
if (!rawBody || typeof rawBody !== "object")
|
|
3859
|
+
return null;
|
|
3860
|
+
const candidate = rawBody.headers;
|
|
3861
|
+
if (!candidate || typeof candidate !== "object")
|
|
3862
|
+
return null;
|
|
3863
|
+
const normalized = {};
|
|
3864
|
+
for (const [key, value] of Object.entries(candidate)) {
|
|
3865
|
+
if (typeof value !== "string")
|
|
3866
|
+
continue;
|
|
3867
|
+
const trimmedKey = key.trim();
|
|
3868
|
+
if (!trimmedKey)
|
|
3869
|
+
continue;
|
|
3870
|
+
normalized[trimmedKey.toLowerCase()] = value;
|
|
3871
|
+
}
|
|
3872
|
+
return Object.keys(normalized).length > 0 ? normalized : null;
|
|
3873
|
+
})();
|
|
3874
|
+
const providedQuery = (() => {
|
|
3875
|
+
if (!rawBody || typeof rawBody !== "object")
|
|
3876
|
+
return null;
|
|
3877
|
+
const raw = rawBody.query;
|
|
3878
|
+
if (typeof raw === "string") {
|
|
3879
|
+
const trimmed = raw.trim();
|
|
3880
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
3881
|
+
}
|
|
3882
|
+
return null;
|
|
3883
|
+
})();
|
|
3669
3884
|
const testPayload = normalizeClaudePayload({
|
|
3670
3885
|
model: targetModel,
|
|
3671
3886
|
stream: false,
|
|
@@ -3680,15 +3895,23 @@ async function registerAdminRoutes(app) {
|
|
|
3680
3895
|
}
|
|
3681
3896
|
]
|
|
3682
3897
|
}
|
|
3683
|
-
]
|
|
3684
|
-
system: "You are a connection diagnostic assistant."
|
|
3898
|
+
]
|
|
3685
3899
|
});
|
|
3686
|
-
const providerBody = provider.type === "anthropic" ?
|
|
3687
|
-
|
|
3900
|
+
const providerBody = provider.type === "anthropic" ? {
|
|
3901
|
+
model: targetModel,
|
|
3902
|
+
stream: false,
|
|
3903
|
+
max_tokens: 256,
|
|
3688
3904
|
temperature: 0,
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3905
|
+
messages: [
|
|
3906
|
+
{
|
|
3907
|
+
role: "user",
|
|
3908
|
+
content: [
|
|
3909
|
+
{ type: "text", text: "You are a connection diagnostic assistant." },
|
|
3910
|
+
{ type: "text", text: "\u4F60\u597D\uFF0C\u8FD9\u662F\u4E00\u6B21\u8FDE\u63A5\u6D4B\u8BD5\u3002\u8BF7\u7B80\u77ED\u56DE\u5E94\u4EE5\u786E\u8BA4\u670D\u52A1\u53EF\u7528\u3002" }
|
|
3911
|
+
]
|
|
3912
|
+
}
|
|
3913
|
+
]
|
|
3914
|
+
} : buildProviderBody(testPayload, {
|
|
3692
3915
|
maxTokens: 256,
|
|
3693
3916
|
temperature: 0,
|
|
3694
3917
|
toolChoice: void 0,
|
|
@@ -3699,15 +3922,38 @@ async function registerAdminRoutes(app) {
|
|
|
3699
3922
|
const upstream = await connector.send({
|
|
3700
3923
|
model: targetModel,
|
|
3701
3924
|
body: providerBody,
|
|
3702
|
-
stream: false
|
|
3925
|
+
stream: false,
|
|
3926
|
+
headers: providedHeaders ?? void 0,
|
|
3927
|
+
query: providedQuery ?? void 0
|
|
3703
3928
|
});
|
|
3704
3929
|
const duration = Date.now() - startedAt;
|
|
3705
3930
|
if (upstream.status >= 400) {
|
|
3706
3931
|
const errorText = upstream.body ? await new Response(upstream.body).text() : "";
|
|
3932
|
+
const credentialRestricted = errorText.includes("only authorized for use with Claude Code");
|
|
3933
|
+
let requestBodyPreview;
|
|
3934
|
+
try {
|
|
3935
|
+
requestBodyPreview = JSON.stringify(providerBody).slice(0, 1e3);
|
|
3936
|
+
} catch {
|
|
3937
|
+
requestBodyPreview = "[unserializable body]";
|
|
3938
|
+
}
|
|
3939
|
+
request.log.warn(
|
|
3940
|
+
{
|
|
3941
|
+
event: "provider.test.failure",
|
|
3942
|
+
provider: provider.id,
|
|
3943
|
+
status: upstream.status,
|
|
3944
|
+
statusText: errorText || "Upstream error",
|
|
3945
|
+
headers: maskSensitiveHeaders(providedHeaders),
|
|
3946
|
+
query: providedQuery ?? null,
|
|
3947
|
+
durationMs: duration,
|
|
3948
|
+
model: targetModel,
|
|
3949
|
+
requestBody: requestBodyPreview
|
|
3950
|
+
},
|
|
3951
|
+
"provider test request failed"
|
|
3952
|
+
);
|
|
3707
3953
|
return {
|
|
3708
3954
|
ok: false,
|
|
3709
3955
|
status: upstream.status,
|
|
3710
|
-
statusText: errorText || "Upstream error",
|
|
3956
|
+
statusText: credentialRestricted ? "\u6D4B\u8BD5\u8BF7\u6C42\u88AB\u62D2\u7EDD\uFF1A\u8BE5\u51ED\u8BC1\u4EC5\u6388\u6743\u5728 Claude Code \u5185\u4F7F\u7528\u3002\u8BF7\u76F4\u63A5\u5728 IDE \u4E2D\u53D1\u8D77\u4E00\u6B21\u5BF9\u8BDD\u786E\u8BA4\u771F\u5B9E\u8FDE\u901A\u6027\u3002" : errorText || "Upstream error",
|
|
3711
3957
|
durationMs: duration
|
|
3712
3958
|
};
|
|
3713
3959
|
}
|
|
@@ -3717,6 +3963,19 @@ async function registerAdminRoutes(app) {
|
|
|
3717
3963
|
parsed = raw ? JSON.parse(raw) : null;
|
|
3718
3964
|
} catch {
|
|
3719
3965
|
const fallbackSample = raw?.trim() ?? "";
|
|
3966
|
+
request.log.warn(
|
|
3967
|
+
{
|
|
3968
|
+
event: "provider.test.invalid_json",
|
|
3969
|
+
provider: provider.id,
|
|
3970
|
+
status: upstream.status,
|
|
3971
|
+
headers: maskSensitiveHeaders(providedHeaders),
|
|
3972
|
+
query: providedQuery ?? null,
|
|
3973
|
+
durationMs: duration,
|
|
3974
|
+
model: targetModel,
|
|
3975
|
+
sample: fallbackSample ? fallbackSample.slice(0, 500) : ""
|
|
3976
|
+
},
|
|
3977
|
+
"provider test response not valid JSON"
|
|
3978
|
+
);
|
|
3720
3979
|
if (provider.type && provider.type !== "anthropic") {
|
|
3721
3980
|
return {
|
|
3722
3981
|
ok: fallbackSample.length > 0,
|
|
@@ -3941,6 +4200,64 @@ async function registerAdminRoutes(app) {
|
|
|
3941
4200
|
}
|
|
3942
4201
|
var isEndpoint = (value) => value === "anthropic" || value === "openai";
|
|
3943
4202
|
|
|
4203
|
+
// routes/auth.ts
|
|
4204
|
+
async function registerAuthRoutes(app) {
|
|
4205
|
+
app.get("/auth/session", async (request) => {
|
|
4206
|
+
const config = getConfig();
|
|
4207
|
+
const webAuth = config.webAuth;
|
|
4208
|
+
if (!webAuth?.enabled) {
|
|
4209
|
+
return {
|
|
4210
|
+
authEnabled: false,
|
|
4211
|
+
authenticated: true
|
|
4212
|
+
};
|
|
4213
|
+
}
|
|
4214
|
+
const session = readSession(request);
|
|
4215
|
+
if (!session) {
|
|
4216
|
+
return {
|
|
4217
|
+
authEnabled: true,
|
|
4218
|
+
authenticated: false
|
|
4219
|
+
};
|
|
4220
|
+
}
|
|
4221
|
+
return {
|
|
4222
|
+
authEnabled: true,
|
|
4223
|
+
authenticated: true,
|
|
4224
|
+
username: session.username
|
|
4225
|
+
};
|
|
4226
|
+
});
|
|
4227
|
+
app.post("/auth/login", async (request, reply) => {
|
|
4228
|
+
const config = getConfig();
|
|
4229
|
+
const webAuth = config.webAuth;
|
|
4230
|
+
if (!webAuth?.enabled) {
|
|
4231
|
+
return {
|
|
4232
|
+
success: true,
|
|
4233
|
+
authEnabled: false
|
|
4234
|
+
};
|
|
4235
|
+
}
|
|
4236
|
+
const body = request.body;
|
|
4237
|
+
const username = sanitizeUsername(body?.username);
|
|
4238
|
+
const password = typeof body?.password === "string" ? body.password : void 0;
|
|
4239
|
+
if (!username || !password) {
|
|
4240
|
+
reply.code(400);
|
|
4241
|
+
return { error: "Missing username or password" };
|
|
4242
|
+
}
|
|
4243
|
+
if (!webAuth.username || username !== webAuth.username || !verifyPassword(password, webAuth)) {
|
|
4244
|
+
reply.code(401);
|
|
4245
|
+
return { error: "Invalid credentials" };
|
|
4246
|
+
}
|
|
4247
|
+
const session = issueSession(username);
|
|
4248
|
+
setSessionCookie(reply, session.token);
|
|
4249
|
+
return { success: true };
|
|
4250
|
+
});
|
|
4251
|
+
app.post("/auth/logout", async (request, reply) => {
|
|
4252
|
+
const token = getSessionToken(request);
|
|
4253
|
+
if (token) {
|
|
4254
|
+
revokeSession(token);
|
|
4255
|
+
}
|
|
4256
|
+
clearSessionCookie(reply);
|
|
4257
|
+
return { success: true };
|
|
4258
|
+
});
|
|
4259
|
+
}
|
|
4260
|
+
|
|
3944
4261
|
// tasks/maintenance.ts
|
|
3945
4262
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
3946
4263
|
var timersStarted = false;
|
|
@@ -3995,11 +4312,37 @@ async function createServer() {
|
|
|
3995
4312
|
const config = cachedConfig2 ?? loadConfig();
|
|
3996
4313
|
const requestLogEnabled = config.requestLogging !== false;
|
|
3997
4314
|
const responseLogEnabled = config.responseLogging !== false;
|
|
4315
|
+
const bodyLimit = typeof config.bodyLimit === "number" && Number.isFinite(config.bodyLimit) && config.bodyLimit > 0 ? config.bodyLimit : 10 * 1024 * 1024;
|
|
3998
4316
|
const app = Fastify({
|
|
3999
4317
|
logger: {
|
|
4000
4318
|
level: config.logLevel ?? "info"
|
|
4001
4319
|
},
|
|
4002
|
-
disableRequestLogging: true
|
|
4320
|
+
disableRequestLogging: true,
|
|
4321
|
+
bodyLimit
|
|
4322
|
+
});
|
|
4323
|
+
app.addHook("onRequest", async (request, reply) => {
|
|
4324
|
+
const authConfig = (cachedConfig2 ?? getConfig()).webAuth;
|
|
4325
|
+
if (!authConfig?.enabled) {
|
|
4326
|
+
return;
|
|
4327
|
+
}
|
|
4328
|
+
const rawUrl = request.raw?.url ?? request.url ?? "";
|
|
4329
|
+
if (!rawUrl)
|
|
4330
|
+
return;
|
|
4331
|
+
if (rawUrl.startsWith("/auth/") || rawUrl.startsWith("/anthropic") || rawUrl.startsWith("/openai") || rawUrl.startsWith("/assets/") || rawUrl.startsWith("/favicon.ico") || rawUrl === "/" || rawUrl.startsWith("/ui") || rawUrl.startsWith("/health")) {
|
|
4332
|
+
return;
|
|
4333
|
+
}
|
|
4334
|
+
if (request.method === "OPTIONS") {
|
|
4335
|
+
return;
|
|
4336
|
+
}
|
|
4337
|
+
if (rawUrl.startsWith("/api/")) {
|
|
4338
|
+
const session = readSession(request);
|
|
4339
|
+
if (session) {
|
|
4340
|
+
return;
|
|
4341
|
+
}
|
|
4342
|
+
reply.code(401);
|
|
4343
|
+
reply.header("Cache-Control", "no-store");
|
|
4344
|
+
await reply.send({ error: "Authentication required" });
|
|
4345
|
+
}
|
|
4003
4346
|
});
|
|
4004
4347
|
if (requestLogEnabled) {
|
|
4005
4348
|
app.addHook("onRequest", (request, _reply, done) => {
|
|
@@ -4079,6 +4422,7 @@ async function createServer() {
|
|
|
4079
4422
|
} else {
|
|
4080
4423
|
app.log.warn("\u672A\u627E\u5230 Web UI \u6784\u5EFA\u4EA7\u7269\uFF0C/ui \u76EE\u5F55\u5C06\u4E0D\u53EF\u7528\u3002");
|
|
4081
4424
|
}
|
|
4425
|
+
await registerAuthRoutes(app);
|
|
4082
4426
|
await registerMessagesRoute(app);
|
|
4083
4427
|
await registerOpenAiRoutes(app);
|
|
4084
4428
|
await registerAdminRoutes(app);
|