@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.
Files changed (30) hide show
  1. package/README.md +34 -0
  2. package/package.json +1 -1
  3. package/src/cli/dist/index.js +34 -2
  4. package/src/server/dist/index.js +446 -102
  5. package/src/web/dist/assets/{About-Bxxb0hlP.js → About-B4P46oBY.js} +2 -2
  6. package/src/web/dist/assets/{ApiKeys-B57jV3AH.js → ApiKeys-DIj-z9NS.js} +2 -2
  7. package/src/web/dist/assets/{Button-DIJdkdyt.js → Button-Df2SC9Z6.js} +1 -1
  8. package/src/web/dist/assets/{Dashboard-Cxu97ZgH.js → Dashboard-ofxha89A.js} +1 -1
  9. package/src/web/dist/assets/{FormField-JMg-tjrG.js → FormField-IhMRFkvg.js} +1 -1
  10. package/src/web/dist/assets/{Help-Bcnhv1mL.js → Help-DWp0N8et.js} +1 -1
  11. package/src/web/dist/assets/{Input-BOaaGybK.js → Input-BR1EKmIk.js} +1 -1
  12. package/src/web/dist/assets/Login-D8amVPUK.js +1 -0
  13. package/src/web/dist/assets/{Logs-DhIgaGtG.js → Logs-DRwOarji.js} +1 -1
  14. package/src/web/dist/assets/ModelManagement-CtaCkRed.js +1 -0
  15. package/src/web/dist/assets/PageSection-D21hdQmW.js +1 -0
  16. package/src/web/dist/assets/Settings-C2-eI9Yb.js +1 -0
  17. package/src/web/dist/assets/{StatusBadge-DOVCfoYm.js → StatusBadge-CMf3e9GN.js} +1 -1
  18. package/src/web/dist/assets/{copy-CG_L9fTW.js → copy-C0txfzHv.js} +1 -1
  19. package/src/web/dist/assets/index-BRI-aVAi.css +1 -0
  20. package/src/web/dist/assets/index-DtM-AT3x.js +148 -0
  21. package/src/web/dist/assets/{index-mV9BV7rj.js → index-oWYCCpBl.js} +16 -16
  22. package/src/web/dist/assets/{info-DNl8lKHj.js → info-jEzfgMxW.js} +1 -1
  23. package/src/web/dist/assets/useApiQuery-Dz2idWoC.js +1 -0
  24. package/src/web/dist/index.html +2 -2
  25. package/src/web/dist/assets/ModelManagement-BR4fCLUq.js +0 -1
  26. package/src/web/dist/assets/PageSection-Dndgc9r9.js +0 -1
  27. package/src/web/dist/assets/Settings-B83xPKID.js +0 -1
  28. package/src/web/dist/assets/index-CD8snFBp.css +0 -1
  29. package/src/web/dist/assets/index-COB9rriK.js +0 -143
  30. package/src/web/dist/assets/useApiQuery-mnvEnqVq.js +0 -6
@@ -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 data = JSON.parse(trimmed.slice(5).trim());
2222
- if (data?.usage) {
2223
- usagePrompt2 = data.usage.input_tokens ?? usagePrompt2;
2224
- usageCompletion2 = data.usage.output_tokens ?? usageCompletion2;
2225
- const maybeCached = resolveCachedTokens(data.usage);
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 = data.usage;
2183
+ lastUsagePayload = payload2.usage;
2230
2184
  }
2231
- const deltaText = data?.delta?.text;
2232
- if (typeof deltaText === "string") {
2233
- if (!firstTokenAt2 && deltaText.length > 0) {
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
- return getConfig();
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" ? buildAnthropicBody(testPayload, {
3687
- maxTokens: 256,
3900
+ const providerBody = provider.type === "anthropic" ? {
3901
+ model: targetModel,
3902
+ stream: false,
3903
+ max_tokens: 256,
3688
3904
  temperature: 0,
3689
- toolChoice: void 0,
3690
- overrideTools: void 0
3691
- }) : buildProviderBody(testPayload, {
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);