Skip to content

feat: security auth overhaul and provider batch operations#806

Merged
ding113 merged 60 commits intodevfrom
feat/security-auth-overhaul
Feb 19, 2026
Merged

feat: security auth overhaul and provider batch operations#806
ding113 merged 60 commits intodevfrom
feat/security-auth-overhaul

Conversation

@ding113
Copy link
Owner

@ding113 ding113 commented Feb 19, 2026

Summary

  • Security auth overhaul (waves 1-7): opaque session tokens, Redis session store, cookie hardening, CSRF origin guard, anti-bruteforce login abuse policy, constant-time token comparison, security headers (CSP/HSTS/X-Frame), dual-read auth migration, logout session revocation
  • Login page redesign: split panel layout with animations, redirect safety, loading states, failure taxonomy, a11y overlay, i18n parity (5 languages)
  • Provider batch operations: batch patch preview/apply/undo pipeline with preimage capture, 9-field sectioned batch dialog, per-provider field compatibility checks, build-patch-draft builder, advanced routing fields (thinking budget, adaptive thinking, priority, weight), type/group quick-select toolbar
  • Undo engine: in-memory snapshot store with TTL, per-provider restoration for both single edits and batch deletions
  • Reusable components: adaptive thinking editor, thinking budget editor extraction

Key changes

Security (19 files)

  • src/lib/auth.ts - opaque session mode, session rotation, expiration checks
  • src/lib/auth-session-store/ - Redis-backed session store with lazy-load
  • src/lib/security/ - CSRF guard, login abuse policy, security headers, constant-time compare
  • src/app/api/auth/login/route.ts - hardened login with bruteforce protection
  • src/app/api/auth/logout/route.ts - session revocation on logout

Provider batch (25 files)

  • src/actions/providers.ts - batch patch preview, apply, undo server actions
  • src/lib/provider-patch-contract.ts - validation, normalization, field mapping
  • src/repository/provider.ts - batch DB operations with preimage capture
  • src/app/[locale]/dashboard/settings/providers/_components/batch-edit/ - UI components

Tests (55 files, ~19k lines added)

  • 14 security test suites covering session, CSRF, bruteforce, headers, regression
  • 20+ provider batch test suites covering contract, preview, apply, undo engines
  • UI component tests for batch dialog, toolbar, form tabs, thinking editors

Test plan

  • All existing tests pass (bun run test - 2791+ passing)
  • Zero TypeScript errors (bun run typecheck)
  • Security regression suite green (tests/security/full-security-regression.test.ts)
  • Provider batch contract tests cover validation, normalization, and error codes
  • Undo engine tests verify preimage capture and per-provider restoration
  • Manual: verify login flow with opaque sessions
  • Manual: verify batch edit dialog with preview step
  • Manual: verify undo toast after batch operations

Greptile Summary

This PR implements a comprehensive security authentication overhaul and provider batch operations system. The security changes migrate from API-key-as-session-token to opaque session IDs stored in Redis, adding multiple layers of protection including CSRF origin guards, constant-time token comparison, bruteforce protection with LRU eviction, security headers (CSP/HSTS/X-Frame), and session rotation with server-side revocation. The provider batch operations add a preview/apply/undo pipeline with preimage capture, supporting 9-field sectioned editing with per-provider field compatibility checks. The login UI was redesigned with split panels, animations, and redirect safety.

Key accomplishments:

  • Opaque session tokens with Redis backing prevent token reuse after key rotation
  • Atomic GET+DEL via Lua script prevents TOCTOU races on single-use tokens
  • Login abuse policy with LRU bump strategy mitigates 10k entry exhaustion attacks
  • Comprehensive test coverage with 55+ new test files covering security regression, session contracts, and provider batch operations
  • Provider undo engine with 30-second TTL allows rollback of batch operations

Issues found:

  • Login route missing key parameter in recordFailure() and recordSuccess() calls (lines 191, 212, 273) - prevents key-based rate limiting from working correctly for invalid/missing keys and successful logins

Confidence Score: 4/5

  • Safe to merge after fixing key-based rate limiting in login route
  • The PR demonstrates excellent security engineering with opaque sessions, defense-in-depth protections, and comprehensive test coverage (2791+ passing tests). However, the missing key parameter in three recordFailure/recordSuccess calls creates a gap in the bruteforce protection where key-based rate limiting won't trigger correctly. This is a straightforward fix but important for the security posture.
  • src/app/api/auth/login/route.ts requires fixing the missing key parameter in recordFailure/recordSuccess calls

Important Files Changed

Filename Overview
src/lib/auth.ts comprehensive opaque session implementation with dual-read migration, constant-time comparisons, and defense-in-depth expiry checks
src/lib/security/login-abuse-policy.ts bruteforce protection with LRU eviction strategy to mitigate 10k entry limit exhaustion attacks
src/lib/auth-session-store/redis-session-store.ts Redis-backed session store with session rotation, TTL management, and revocation support
src/app/api/auth/login/route.ts hardened login endpoint with CSRF guard, bruteforce protection, opaque session creation, but missing key parameter in some recordFailure calls
src/lib/redis/redis-kv-store.ts atomic GET+DEL via Lua script prevents TOCTOU race on single-use tokens
src/lib/provider-patch-contract.ts comprehensive validation and normalization for provider batch patch operations with field mapping
src/lib/providers/undo-store.ts Redis-backed undo store with 30-second TTL for operation rollback
src/actions/providers.ts extensive provider batch operations with preview/apply/undo pipeline and preimage capture

Sequence Diagram

sequenceDiagram
    participant Client
    participant LoginRoute as /api/auth/login
    participant CSRFGuard
    participant LoginPolicy as Bruteforce Policy
    participant AuthLib as auth.ts
    participant RedisSession as Redis Session Store
    participant DB as Database
    
    Client->>LoginRoute: POST /api/auth/login {key}
    LoginRoute->>CSRFGuard: check(request)
    CSRFGuard-->>LoginRoute: allowed/rejected
    
    alt CSRF rejected
        LoginRoute-->>Client: 403 CSRF_REJECTED
    end
    
    LoginRoute->>LoginPolicy: check(clientIp, key)
    LoginPolicy-->>LoginRoute: allowed/rate_limited
    
    alt Rate limited
        LoginRoute-->>Client: 429 RATE_LIMITED + Retry-After
    end
    
    LoginRoute->>AuthLib: validateKey(key)
    AuthLib->>DB: validateApiKeyAndGetUser(key)
    DB-->>AuthLib: {user, key} or null
    
    alt Invalid key
        AuthLib-->>LoginRoute: null
        LoginRoute->>LoginPolicy: recordFailure(clientIp, key)
        LoginRoute-->>Client: 401 KEY_INVALID
    end
    
    alt Valid key - opaque mode
        AuthLib-->>LoginRoute: {user, key}
        LoginRoute->>RedisSession: create(keyFingerprint, userId, userRole)
        RedisSession->>RedisSession: generate sessionId (sid_uuid)
        RedisSession-->>LoginRoute: sessionData {sessionId, expiresAt}
        LoginRoute->>LoginRoute: setAuthCookie(sessionId)
        LoginRoute->>LoginPolicy: recordSuccess(clientIp, key)
        LoginRoute-->>Client: 200 {ok, user, redirectTo, loginType}
    end
    
    Note over Client,DB: Subsequent authenticated requests
    
    Client->>AuthLib: getSession() [with cookie]
    AuthLib->>RedisSession: read(sessionId)
    RedisSession-->>AuthLib: sessionData or null
    
    alt Session expired
        AuthLib-->>Client: Redirect to login
    end
    
    AuthLib->>AuthLib: convertToAuthSession(sessionData)
    AuthLib->>DB: findKeyList(userId)
    DB-->>AuthLib: [keys]
    AuthLib->>AuthLib: verify keyFingerprint matches
    AuthLib-->>Client: AuthSession {user, key}
    
    Note over Client,DB: Logout flow
    
    Client->>LoginRoute: POST /api/auth/logout
    LoginRoute->>CSRFGuard: check(request)
    LoginRoute->>RedisSession: revoke(sessionId)
    RedisSession->>RedisSession: DEL session key
    LoginRoute->>LoginRoute: clearAuthCookie()
    LoginRoute-->>Client: 200 {ok}
Loading

Last reviewed commit: 4a0f06e

@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2026

📝 Walkthrough

Walkthrough

大规模引入提供商批量编辑的预览/应用/撤销流水线、撤销快照存储与类型;重构登录页与重定向安全;实现会话令牌模式及 Redis 会话存储;添加多项安全工具与大量 i18n 文本更新,并扩展单元/集成测试覆盖面(安全、批量补丁、UI)。

Changes

Cohort / File(s) Summary
提供商批量补丁核心与类型
src/actions/providers.ts, src/lib/provider-patch-contract.ts, src/lib/provider-batch-patch-error-codes.ts, src/types/provider.ts
新增批量补丁合约、验证/归一化、预览/应用/撤销引擎、撤销/预览令牌与 TTL、字段映射及大量导出类型与接口。
撤销与通用 Redis KV 存储
src/lib/providers/undo-store.ts, src/lib/redis/redis-kv-store.ts
新增撤销快照存储(token 化、TTL、消费语义)及通用 RedisKVStore(set/get/getAndDelete/delete、Lua 原子操作)。
仓库恢复与批量更新扩展
src/repository/provider.ts, src/repository/index.ts
新增单条与批量恢复接口(restoreProvider/restoreProvidersBatch);扩展 BatchProviderUpdates 支持多种新增字段(modelRedirects/allowedModels/anthropic/gemini 等)。
前端批量编辑 UI 与表单改造
src/app/[locale]/settings/providers/_components/...
引入 FormMode=batch、脏字段追踪、补丁草稿构建、编辑→预览→应用对话流、按类型/按组选择、预览分组与排除、撤销交互与相关 toolbar/preview 组件变更。
新增表单编辑器组件
src/app/[locale]/settings/providers/_components/thinking-budget-editor.tsx, .../adaptive-thinking-editor.tsx
新增 ThinkingBudgetEditor 与 AdaptiveThinkingEditor 用于 Anthropic / thinking-budget 配置(支持批量与单体模式)。
表单导航布局
src/app/[locale]/settings/providers/_components/forms/provider-form/components/form-tab-nav.tsx
FormTabNav 新增 layout?: "vertical"
登录页改版与重定向安全
src/app/[locale]/login/page.tsx, src/app/[locale]/login/loading.tsx, src/app/[locale]/login/redirect-safety.ts
重构登录页为左右面板响应式布局、状态机、版本信息与密码可见性切换;新增 sanitize/resolveLoginRedirectTarget 以防不安全重定向。
认证与会话架构增强
src/lib/auth.ts, src/lib/auth-session-store/*, src/lib/auth-session-store/redis-session-store.ts
导出 AUTH_COOKIE_NAME;新增 SESSION_TOKEN_MODE 支持(legacy
安全工具与策略
src/lib/security/{constant-time-compare,csrf-origin-guard,login-abuse-policy,security-headers.ts}, src/lib/security/auth-response-headers.ts
新增恒定时间比较、CSRF 源守卫、登录滥用政策、可配置安全头构建与为 auth 路由注入安全头的组合工具。
登录/登出路由与安全
src/app/api/auth/login/route.ts, src/app/api/auth/logout/route.ts
在登录/登出中加入 CSRF 检查、登录滥用速率限制、会话令牌模式与不透明会话流、细化失败分类与日志;logout POST 改为接收 NextRequest。
CORS / 代理与预认证
src/app/v1/_lib/cors.ts, src/app/v1/_lib/proxy/auth-guard.ts, src/proxy.ts
CORS 支持 allowCredentials 选项;proxy auth-guard 增加预认证速率限制与 Gemini key 支持;proxyHandler 简化为基于 AUTH_COOKIE_NAME 的存在性检查(并改为同步)。
i18n 文本扩展
messages/*/auth.json, messages/*/settings/providers/batchEdit.json
为多语言(en/ja/ru/zh-CN/zh-TW)添加 API Key 标签、密码显示切换、brand.tagline;批量编辑文本大规模扩展(选择、preview/undo/toast/batchNotes、isEnabled/groupTag 结构化等)。
Usage 文档认证上下文
src/app/[locale]/usage-doc/_components/usage-doc-auth-context.tsx, src/app/[locale]/usage-doc/layout.tsx, .../page.tsx
新增 UsageDocAuthProvider 与 useUsageDocAuth,并在 usage-doc layout 中使用以替换基于 cookie 的本地检查。
小规模前端/导出调整
src/app/[locale]/settings/providers/_components/add-provider-dialog.tsx, src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx, src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx, src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx
清理重复导入、引入 undoProviderPatch/undoProviderDelete 使用与撤销 toast 流程集成、为表单添加撤销错误码处理。
Redis 会话/撤销/KV 相关实现
src/lib/auth-session-store/redis-session-store.ts, src/lib/redis/redis-kv-store.ts
新增 RedisSessionStore 实现、RedisKVStore 公用封装与相关类型与错误处理。
测试大规模扩展
tests/security/*, tests/unit/**/* (多文件)
新增/扩展大量单元与集成测试,覆盖安全(CSRF、暴力破解、session、headers)、批量补丁合约/预览/应用/撤销引擎、撤销存储、登录 UI 重设计、i18n 键一致性等。

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR标题清晰准确地总结了主要变更:安全认证大改及提供商批量操作。
Description check ✅ Passed PR描述详细阐述了改动内容,包括安全认证重构(7波次)、登录页面重设计、提供商批量操作、撤销引擎等核心功能。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/security-auth-overhaul

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @ding113, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the application's security posture by implementing advanced authentication mechanisms and robust session management. Concurrently, it introduces powerful batch editing capabilities for providers, improving administrative efficiency and data integrity with a new undo system. The user experience is also refined through a redesigned login interface, ensuring a more secure, intuitive, and manageable platform.

Highlights

  • Security Overhaul: Implemented a comprehensive security overhaul across seven waves, including opaque session tokens, Redis session storage, cookie hardening, CSRF origin guarding, anti-bruteforce login abuse policies, constant-time token comparison, and robust security headers (CSP/HSTS/X-Frame).
  • Login Page Redesign: Redesigned the login page with a modern split-panel layout, animations, enhanced redirect safety, clear loading states, detailed failure taxonomy, improved accessibility, and internationalization support for five languages.
  • Provider Batch Operations: Introduced a powerful batch operations pipeline for providers, enabling batch patch preview, application, and undo with preimage capture. This includes a 9-field sectioned batch dialog, per-provider field compatibility checks, a build-patch-draft builder, and advanced routing fields.
  • Undo Engine: Developed an in-memory undo engine with TTL-based snapshot storage, allowing per-provider restoration for both single edits and batch deletions, enhancing data management safety.
  • Reusable Components: Extracted and created reusable UI components for adaptive thinking and thinking budget editors, promoting modularity and consistency.
Changelog
  • messages/en/auth.json
    • Added new translation keys for password visibility toggle and brand tagline.
  • messages/en/settings/providers/batchEdit.json
    • Introduced new translation keys for batch edit dialog, sections, fields, affected providers, preview, batch notes, selection hint, and undo functionality.
  • messages/ja/auth.json
    • Added new translation keys for password visibility toggle and brand tagline.
  • messages/ja/settings/providers/batchEdit.json
    • Introduced new translation keys for batch edit dialog, sections, fields, affected providers, preview, batch notes, selection hint, and undo functionality.
  • messages/ru/auth.json
    • Added new translation keys for password visibility toggle and brand tagline.
  • messages/ru/settings/providers/batchEdit.json
    • Introduced new translation keys for batch edit dialog, sections, fields, affected providers, preview, batch notes, selection hint, and undo functionality.
  • messages/zh-CN/auth.json
    • Added new translation keys for password visibility toggle and brand tagline.
  • messages/zh-CN/settings/providers/batchEdit.json
    • Introduced new translation keys for batch edit dialog, sections, fields, affected providers, preview, batch notes, selection hint, and undo functionality.
  • messages/zh-TW/auth.json
    • Added new translation keys for password visibility toggle and brand tagline.
  • messages/zh-TW/settings/providers/batchEdit.json
    • Introduced new translation keys for batch edit dialog, sections, fields, affected providers, preview, batch notes, selection hint, and undo functionality.
  • src/actions/providers.ts
    • Added new actions for batch patch preview, apply, and undo operations.
    • Modified 'editProvider' and 'removeProvider' to return undo tokens for single operations.
    • Implemented internal snapshot management and cleanup for batch operations.
    • Introduced helper functions for Zod error handling, provider ID deduplication, and patch field analysis.
  • src/app/[locale]/login/loading.tsx
    • Updated the login page loading skeleton to reflect the new split-panel layout.
  • src/app/[locale]/login/page.tsx
    • Implemented a redesigned login page with a split-panel layout and animations.
    • Added a password visibility toggle to the API key input field.
    • Integrated fetching of version information and system site title for display in the footer.
    • Enhanced error handling to provide more specific failure taxonomy and guidance.
    • Utilized 'redirect-safety' utility for secure login redirects.
  • src/app/[locale]/login/redirect-safety.ts
    • Added new utility functions 'sanitizeRedirectPath' and 'resolveLoginRedirectTarget' to ensure safe and valid redirection targets after login.
  • src/app/[locale]/settings/providers/_components/adaptive-thinking-editor.tsx
    • Added a new reusable component for editing Anthropic Adaptive Thinking configurations.
  • src/app/[locale]/settings/providers/_components/add-provider-dialog.tsx
    • Reordered imports for consistency.
  • src/app/[locale]/settings/providers/_components/batch-edit/build-patch-draft.ts
    • Added a new utility to construct a provider batch patch draft from the form state, including unit conversions for time-based fields.
  • src/app/[locale]/settings/providers/_components/batch-edit/provider-batch-dialog.tsx
    • Refactored the batch edit dialog into a multi-step process (edit, preview, apply).
    • Integrated the new provider form context for batch mode.
    • Implemented logic for previewing changes and applying them, including undo functionality.
  • src/app/[locale]/settings/providers/_components/batch-edit/provider-batch-preview-step.tsx
    • Added a new component to display a detailed preview of batch changes, including affected providers, fields, and skip reasons.
  • src/app/[locale]/settings/providers/_components/batch-edit/provider-batch-toolbar.tsx
    • Added quick-select dropdowns to the batch toolbar for selecting providers by type or group.
  • src/app/[locale]/settings/providers/_components/forms/provider-form/components/form-tab-nav.tsx
    • Added a new 'horizontal' layout option for the form tab navigation.
  • src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx
    • Updated 'editProvider' and 'removeProvider' calls to handle undo tokens and display undo toasts upon successful operation.
  • src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx
    • Modified 'createInitialState' to support a 'batch' mode with neutral defaults.
    • Implemented dirty field tracking for batch mode actions.
    • Wrapped the dispatch function to automatically mark fields as dirty when changed.
  • src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts
    • Extended 'FormMode' to include 'batch' and added 'BatchState' interface for batch-specific form fields.
  • src/app/[locale]/settings/providers/_components/forms/provider-form/sections/basic-info-section.tsx
    • Adapted the basic info section to display batch-specific UI, including an 'isEnabled' tri-state selector and a summary of affected providers when in batch mode.
  • src/app/[locale]/settings/providers/_components/forms/provider-form/sections/network-section.tsx
    • Hid the proxy test button when the form is in batch editing mode.
  • src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx
    • Hid provider type and group tag fields in batch mode.
    • Integrated the new 'AdaptiveThinkingEditor' and 'ThinkingBudgetEditor' components.
    • Added badges to provider-specific sections (Codex, Anthropic, Gemini) to indicate their applicability in batch mode.
  • src/app/[locale]/settings/providers/_components/forms/provider-form/sections/testing-section.tsx
    • Hid the API test section when the form is in batch editing mode.
  • src/app/[locale]/settings/providers/_components/provider-manager.tsx
    • Implemented 'handleSelectByType' and 'handleSelectByGroup' functions to allow batch selection based on provider attributes.
    • Updated 'ProviderBatchToolbar' and 'ProviderBatchDialog' to pass necessary props for new batch selection and editing features.
  • src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx
    • Updated 'removeProvider' call to handle undo tokens and display undo toasts upon successful deletion.
  • src/app/[locale]/settings/providers/_components/thinking-budget-editor.tsx
    • Added a new reusable component for editing Anthropic Thinking Budget preferences.
  • src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx
    • Updated 'removeProvider' call to handle undo tokens and display undo toasts upon successful deletion.
  • src/app/[locale]/usage-doc/_components/usage-doc-auth-context.tsx
    • Added a new client-side context for managing the login state within the usage documentation.
  • src/app/[locale]/usage-doc/layout.tsx
    • Wrapped the usage documentation content with 'UsageDocAuthProvider' to provide login state to client components.
  • src/app/[locale]/usage-doc/page.tsx
    • Refactored the usage documentation page to consume login state from 'UsageDocAuthContext' instead of client-side cookie checks.
  • src/app/api/auth/login/route.ts
    • Implemented CSRF origin guarding for login requests.
    • Integrated a login abuse policy to protect against brute-force attacks.
    • Introduced opaque session creation and dual-read authentication migration logic.
    • Enhanced error responses with specific error codes and guidance for HTTP/HTTPS mismatches.
  • src/app/api/auth/logout/route.ts
    • Implemented CSRF origin guarding for logout requests.
    • Added logic to revoke opaque sessions from the session store during logout.
  • src/app/v1/_lib/cors.ts
    • Modified 'buildCorsHeaders' to conditionally reflect the 'Origin' header only when 'allowCredentials' is explicitly enabled, enhancing security for proxy API requests.
  • src/app/v1/_lib/proxy/auth-guard.ts
    • Integrated a 'LoginAbusePolicy' for pre-authentication rate limiting on API endpoints to prevent brute-force attacks.
  • src/lib/api/action-adapter-openapi.ts
    • Updated authentication logic to use the new 'validateAuthToken' function and 'AUTH_COOKIE_NAME' constant.
  • src/lib/auth-session-store/index.ts
    • Added new interfaces 'SessionData' and 'SessionStore' to define the contract for session management.
  • src/lib/auth-session-store/redis-session-store.ts
    • Added a new Redis-backed implementation of 'SessionStore' for managing opaque session tokens.
  • src/lib/auth.ts
    • Introduced 'withNoStoreHeaders' for cache control in auth responses.
    • Defined 'SessionTokenMode' and related types/functions for managing opaque session tokens and migration strategies.
    • Added 'toKeyFingerprint' for hashing API keys.
    • Implemented 'validateAuthToken' for a unified token validation process supporting both legacy and opaque tokens.
    • Updated 'validateKey' to use 'constantTimeEqual' for secure comparisons.
  • src/lib/config/env.schema.ts
    • Added 'SESSION_TOKEN_MODE' environment variable to control session token behavior (legacy, dual, opaque).
  • src/lib/provider-batch-patch-error-codes.ts
    • Added a new file defining specific error codes for provider batch operations.
  • src/lib/provider-patch-contract.ts
    • Added a new file defining the contract for provider batch patches, including normalization, validation, and application logic for various provider fields.
  • src/lib/providers/undo-store.ts
    • Added a new in-memory store for managing undo snapshots with a time-to-live (TTL).
  • src/lib/security/auth-response-headers.ts
    • Added a new utility to apply common security headers to authentication-related Next.js responses.
  • src/lib/security/constant-time-compare.ts
    • Added a new utility function for constant-time string comparison to mitigate timing attacks.
  • src/lib/security/csrf-origin-guard.ts
    • Added a new utility for CSRF origin validation, checking 'sec-fetch-site' and 'Origin' headers.
  • src/lib/security/login-abuse-policy.ts
    • Added a new policy implementation to protect against login brute-force attacks by tracking attempts per IP and key.
  • src/lib/security/security-headers.ts
    • Added a new utility for building various HTTP security headers, including Content Security Policy (CSP), HTTP Strict Transport Security (HSTS), and X-Frame-Options.
  • src/proxy.ts
    • Simplified the authentication check in the proxy middleware to only verify the existence of an auth cookie, delegating full session validation to downstream layouts.
    • Removed 'READ_ONLY_PATH_PATTERNS' as read-only access is now handled by the session validation logic.
  • src/repository/index.ts
    • Exported new 'restoreProvider' and 'restoreProvidersBatch' functions for undo functionality.
  • src/repository/provider.ts
    • Implemented 'restoreProvider' and 'restoreProvidersBatch' functions to revert soft-deleted providers and their associated endpoints.
    • Expanded the 'BatchProviderUpdates' interface to support a wider range of provider fields for batch modifications.
  • tests/api/action-adapter-auth-session.unit.test.ts
    • Updated tests to use 'validateAuthToken' instead of 'validateKey' for session validation.
  • tests/security/auth-bruteforce-integration.test.ts
    • Added new integration tests for the login route's anti-bruteforce protection, verifying rate limiting and reset behavior.
  • tests/security/auth-csrf-route-integration.test.ts
    • Added new integration tests for CSRF origin guarding on login and logout routes, ensuring proper blocking of cross-site requests.
  • tests/security/auth-dual-read.test.ts
    • Added new tests for the dual-read session resolver, verifying its behavior in legacy, dual, and opaque modes, including fallback mechanisms.
  • tests/security/constant-time-compare.test.ts
    • Added new unit tests for the 'constantTimeEqual' utility, ensuring secure string comparisons.
  • tests/security/csrf-origin-guard.test.ts
    • Added new unit tests for the 'createCsrfOriginGuard' utility, covering various origin and fetch-site scenarios.
  • tests/security/full-security-regression.test.ts
    • Added a comprehensive security regression test suite covering session contract, session store, cookie hardening, anti-bruteforce, CSRF guard, and security headers.
  • tests/security/login-abuse-policy.test.ts
    • Added new unit tests for the 'LoginAbusePolicy', verifying its rate-limiting, lockout, and reset functionalities.
  • tests/security/proxy-auth-rate-limit.test.ts
    • Added new tests for the proxy's pre-authentication rate limiter, focusing on its integration with 'LoginAbusePolicy' and IP extraction logic.
  • tests/security/security-headers-integration.test.ts
    • Added new integration tests for security headers on auth routes, ensuring proper application of CSP, HSTS, and other headers.
  • tests/security/security-headers.test.ts
    • Added new unit tests for the 'buildSecurityHeaders' utility, covering various configuration options.
  • tests/security/session-contract.test.ts
    • Added new unit tests for the session token contract and migration flags, verifying mode-specific behaviors.
  • tests/security/session-cookie-hardening.test.ts
    • Added new tests for session cookie hardening, verifying 'HttpOnly', 'Secure', and 'Cache-Control' headers.
  • tests/security/session-fixation-rotation.test.ts
    • Added new tests for session fixation rotation and logout revocation, verifying session store interactions.
  • tests/security/session-login-integration.test.ts
    • Added new integration tests for the login route's session token modes (legacy, dual, opaque), verifying cookie and session store interactions.
  • tests/security/session-store.test.ts
    • Added new unit tests for the 'RedisSessionStore', covering session creation, reading, revocation, and rotation.
  • tests/unit/actions/provider-undo-delete.test.ts
    • Added new unit tests for provider batch delete and undo actions, verifying token generation, expiration, and restoration.
  • tests/unit/actions/provider-undo-edit.test.ts
    • Added new unit tests for provider single edit and undo actions, verifying token generation, preimage capture, and restoration.
  • tests/unit/actions/providers-apply-engine.test.ts
    • Added new unit tests for the provider batch apply engine, verifying correct ID and update mapping, cache invalidation, and idempotency.
  • tests/unit/actions/providers-batch-field-mapping.test.ts
    • Added new unit tests for batch update providers, verifying correct mapping of advanced fields to repository format.
  • tests/unit/actions/providers-patch-actions-contract.test.ts
    • Added new unit tests for provider batch patch action contracts, verifying admin role enforcement, preview payload structure, and error handling.
  • tests/unit/actions/providers-preview-engine.test.ts
    • Added new unit tests for the provider batch preview engine, verifying correct row generation, field compatibility, and summary calculations.
  • tests/unit/actions/providers-undo-engine.test.ts
    • Added new unit tests for the provider batch undo engine, verifying preimage restoration, batching of updates, and token consumption.
  • tests/unit/actions/providers-undo-store.test.ts
    • Added new unit tests for the in-memory undo store, verifying snapshot storage, TTL, and token consumption.
  • tests/unit/api/auth-login-failure-taxonomy.test.ts
    • Added new unit tests for the login route's failure taxonomy, verifying specific error codes and guidance for different failure scenarios.
  • tests/unit/api/auth-login-route.test.ts
    • Added new unit tests for the login route, covering various success and failure scenarios, locale detection, and error handling.
  • tests/unit/auth/auth-cookie-constant-sync.test.ts
    • Added new unit tests to ensure the 'AUTH_COOKIE_NAME' constant is stable and used consistently across core auth layers.
  • tests/unit/auth/login-redirect-safety.test.ts
    • Added new unit tests for login redirect safety, verifying path sanitization and target resolution.
  • tests/unit/auth/opaque-admin-session.test.ts
    • Added new unit tests for opaque sessions with admin tokens, verifying resolution and security checks.
  • tests/unit/auth/set-auth-cookie-options.test.ts
    • Added new unit tests for 'setAuthCookie' options, verifying 'HttpOnly', 'Secure', 'SameSite', and 'Max-Age' settings.
  • tests/unit/i18n/auth-login-keys.test.ts
    • Added new unit tests to ensure key parity across all 'auth.json' locale files.
  • tests/unit/login/login-footer-system-name.test.tsx
    • Added new unit tests for the login page footer, verifying dynamic system name display and fallback behavior.
  • tests/unit/login/login-footer-version.test.tsx
    • Added new unit tests for the login page footer, verifying version display and update hints.
  • tests/unit/login/login-loading-state.test.tsx
    • Added new unit tests for the login page loading state, verifying overlay visibility and input disabling during submission.
  • tests/unit/login/login-overlay-a11y.test.tsx
    • Added new unit tests for login page accessibility, verifying ARIA attributes and focus management.
  • tests/unit/login/login-regression-matrix.test.tsx
    • Added a comprehensive regression test matrix for the login route, covering various success and failure scenarios with different user roles and security configurations.
  • tests/unit/login/login-ui-redesign.test.tsx
    • Added new unit tests for the login page UI redesign, verifying password toggle, theme switcher, and responsive elements.
  • tests/unit/login/login-visual-regression.test.tsx
    • Added new visual regression tests for the login page, verifying structural elements and responsive classes.
  • tests/unit/proxy/proxy-auth-cookie-passthrough.test.ts
    • Added new unit tests for proxy authentication cookie passthrough, verifying redirection and public path handling.
  • tests/unit/repository/provider-batch-update-advanced-fields.test.ts
    • Added new unit tests for provider batch updates, verifying correct mapping of advanced fields to the repository.
  • tests/unit/repository/provider-restore.test.ts
    • Added new unit tests for provider restore functionality, verifying single and batch restoration, time limits, and endpoint handling.
  • tests/unit/settings/providers/adaptive-thinking-editor.test.tsx
    • Added new unit tests for the 'AdaptiveThinkingEditor' component, verifying state changes and disabled states.
  • tests/unit/settings/providers/build-patch-draft.test.ts
    • Added new unit tests for 'buildPatchDraftFromFormState', verifying correct draft generation from form state and dirty fields.
  • tests/unit/settings/providers/form-tab-nav.test.tsx
    • Added new unit tests for the 'FormTabNav' component, verifying horizontal layout, active tab highlighting, and status dots.
  • tests/unit/settings/providers/provider-batch-dialog-step1.test.tsx
    • Added new unit tests for the first step of the provider batch dialog, verifying UI structure, button states, and navigation.
  • tests/unit/settings/providers/provider-batch-preview-step.test.tsx
    • Added new unit tests for the provider batch preview step, verifying row rendering, grouping, summary, and exclusion functionality.
  • tests/unit/settings/providers/thinking-budget-editor.test.tsx
    • Added new unit tests for the 'ThinkingBudgetEditor' component, verifying state changes and disabled states.
Activity
  • The pull request introduces a major security overhaul, addressing multiple vulnerabilities and enhancing authentication mechanisms.
  • Significant work has been done on the login page, including a complete redesign and improved user experience.
  • New batch operations for providers have been implemented, allowing for more efficient management of multiple providers.
  • An undo engine has been developed to provide rollback capabilities for both single and batch operations.
  • Extensive testing has been conducted, with 14 security test suites and over 20 provider batch test suites added, totaling approximately 19,000 new lines of test code.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions github-actions bot added enhancement New feature or request area:session area:provider area:UI size/XL Extra Large PR (> 1000 lines) labels Feb 19, 2026
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

111 files reviewed, 5 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +118 to +129
// 3. x-forwarded-for: take the rightmost (last) entry, which is the IP
// appended by the closest trusted proxy. The leftmost entry is
// client-controlled and can be spoofed.
const forwarded = request.headers.get("x-forwarded-for");
if (forwarded) {
const ips = forwarded
.split(",")
.map((s) => s.trim())
.filter(Boolean);
if (ips.length > 0) {
return ips[ips.length - 1];
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rightmost x-forwarded-for IP can be spoofed if intermediary proxies append IPs

The comment says "rightmost (last) entry, which is the IP appended by the closest trusted proxy" but this isn't always true. If an attacker controls a proxy in the chain, they can append arbitrary IPs. The rightmost IP is only trustworthy if you control all proxies in the chain.

For bruteforce protection, this could allow an attacker to bypass rate limits by rotating the spoofed IP in x-forwarded-for.

Consider:

  • Document the deployment assumption (all proxies in chain are trusted)
  • Or use a configurable "trusted proxy depth" approach
  • Or validate that the deployment only has one trusted proxy layer
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/api/auth/login/route.ts
Line: 118-129

Comment:
rightmost `x-forwarded-for` IP can be spoofed if intermediary proxies append IPs

The comment says "rightmost (last) entry, which is the IP appended by the closest trusted proxy" but this isn't always true. If an attacker controls a proxy in the chain, they can append arbitrary IPs. The rightmost IP is only trustworthy if you control all proxies in the chain.

For bruteforce protection, this could allow an attacker to bypass rate limits by rotating the spoofed IP in `x-forwarded-for`.

Consider:
- Document the deployment assumption (all proxies in chain are trusted)
- Or use a configurable "trusted proxy depth" approach
- Or validate that the deployment only has one trusted proxy layer

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 208 to +210
const session = await validateKey(key, { allowReadOnlyAccess: true });
if (!session) {
return NextResponse.json({ error: t?.("apiKeyInvalidOrExpired") }, { status: 401 });
loginPolicy.recordFailure(clientIp);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rate limit failure not recorded for missing/invalid key

When the key is missing or malformed (line 190-206), login failure is not recorded with loginPolicy.recordFailure(clientIp). This creates a gap where an attacker can spam requests with empty/invalid keys without triggering bruteforce protection.

Suggested change
const session = await validateKey(key, { allowReadOnlyAccess: true });
if (!session) {
return NextResponse.json({ error: t?.("apiKeyInvalidOrExpired") }, { status: 401 });
loginPolicy.recordFailure(clientIp);
const session = await validateKey(key, { allowReadOnlyAccess: true });
if (!session) {
loginPolicy.recordFailure(clientIp, key);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/api/auth/login/route.ts
Line: 208-210

Comment:
rate limit failure not recorded for missing/invalid key

When the key is missing or malformed (line 190-206), login failure is not recorded with `loginPolicy.recordFailure(clientIp)`. This creates a gap where an attacker can spam requests with empty/invalid keys without triggering bruteforce protection.

```suggestion
    const session = await validateKey(key, { allowReadOnlyAccess: true });
    if (!session) {
      loginPolicy.recordFailure(clientIp, key);
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +27 to +28
const MAX_TRACKED_ENTRIES = 10_000;
const SWEEP_INTERVAL_MS = 60_000;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10k entry limit can be exhausted by attacker distributing across IPs

MAX_TRACKED_ENTRIES = 10_000 with FIFO eviction allows an attacker with a large IP pool to evade tracking by filling the map with 10k distinct IPs, causing older entries to be deleted before lockout expires.

Consider:

  • LRU eviction instead of FIFO (keeps active attackers tracked longer)
  • Redis-backed store for production scale
  • Lower the max entries and rely more on lockout duration
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/security/login-abuse-policy.ts
Line: 27-28

Comment:
10k entry limit can be exhausted by attacker distributing across IPs

`MAX_TRACKED_ENTRIES = 10_000` with FIFO eviction allows an attacker with a large IP pool to evade tracking by filling the map with 10k distinct IPs, causing older entries to be deleted before lockout expires.

Consider:
- LRU eviction instead of FIFO (keeps active attackers tracked longer)
- Redis-backed store for production scale
- Lower the max entries and rely more on lockout duration

How can I resolve this? If you propose a fix, please make it concise.


import { PROVIDER_BATCH_PATCH_ERROR_CODES } from "@/lib/provider-batch-patch-error-codes";

const UNDO_SNAPSHOT_TTL_MS = 10_000;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10-second undo window may be too short for batch operations

UNDO_SNAPSHOT_TTL_MS = 10_000 (10 seconds) may be too short for users to realize they need to undo a batch operation, especially if they navigate away or review changes. Production deployments may need 30-60 seconds.

Consider making this configurable via environment variable with a reasonable default.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/providers/undo-store.ts
Line: 5

Comment:
10-second undo window may be too short for batch operations

`UNDO_SNAPSHOT_TTL_MS = 10_000` (10 seconds) may be too short for users to realize they need to undo a batch operation, especially if they navigate away or review changes. Production deployments may need 30-60 seconds.

Consider making this configurable via environment variable with a reasonable default.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +273 to +279
if (sessionData) {
if (sessionData.expiresAt <= Date.now()) {
logger.warn("Opaque session expired (application-level check)", {
sessionId: sessionData.sessionId,
expiresAt: sessionData.expiresAt,
});
return null;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

application-level expiry check after Redis read provides defense-in-depth

Session expiry checked both at application level and by Redis TTL. Ensure Redis TTL is always ≤ the expiresAt duration to prevent time windows where expired sessions could be read before TTL triggers.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/auth.ts
Line: 273-279

Comment:
application-level expiry check after Redis read provides defense-in-depth

Session expiry checked both at application level and by Redis TTL. Ensure Redis TTL is always ≤ the `expiresAt` duration to prevent time windows where expired sessions could be read before TTL triggers.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This is a substantial pull request that introduces a major security overhaul and a new provider batch operations feature. The changes are extensive and well-implemented. The security enhancements, including opaque session tokens, CSRF protection, and anti-brute-force measures, are particularly valuable and well-executed. The new batch editing and undo functionality significantly improves the user experience for managing providers.

My review focuses on two key areas for improvement: a critical architectural concern regarding the scalability of the new undo/preview feature's storage mechanism, and a medium-severity performance optimization for session validation. Addressing these points will ensure the new features are robust and performant at scale.

Comment on lines +1249 to +1251
const providerBatchPatchPreviewStore = new Map<string, ProviderBatchPatchPreviewSnapshot>();
const providerPatchUndoStore = new Map<string, ProviderPatchUndoSnapshot>();
const providerDeleteUndoStore = new Map<string, ProviderDeleteUndoSnapshot>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The use of in-memory Map objects for providerBatchPatchPreviewStore, providerPatchUndoStore, and providerDeleteUndoStore introduces a significant scalability and reliability issue. These stores will not be shared across multiple server instances or serverless function invocations. This means a user's request for preview, apply, or undo might be routed to a different instance that does not have the required snapshot, leading to feature failure. For these features to work reliably in a distributed environment, a shared store like Redis should be used, similar to how the session store is implemented.

Comment on lines +371 to +378
const keyList = await findKeyList(sessionData.userId);

for (const key of keyList) {
const keyFingerprint = await toKeyFingerprint(key.key);
if (constantTimeEqual(keyFingerprint, expectedFingerprint)) {
return validateKey(key.key, options);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation of convertToAuthSession fetches all keys for a given userId and then iterates through them, hashing each one to find a match against the session's keyFingerprint. This can lead to performance degradation for users with a large number of API keys, as it involves multiple hashing operations on every authenticated request. A more efficient approach would be to either store the keyId in the session data to allow for a direct lookup, or to add an indexed key_fingerprint column to the keys table in the database for a direct query.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/api/action-adapter-auth-session.unit.test.ts (1)

4-15: ⚠️ Potential issue | 🟡 Minor

注释中的函数名已过时,应更新为 validateAuthToken

第 8 行注释仍写的是 validateKey,与本次更改后验证 validateAuthToken 的实际测试行为不符,会误导后续维护者。

✏️ 建议修改
- * - adapter 层使用 hono 读取 Cookie/Authorization 并 validateKey
+ * - adapter 层使用 hono 读取 Cookie/Authorization 并 validateAuthToken
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/api/action-adapter-auth-session.unit.test.ts` around lines 4 - 15,
Update the outdated function name in the test comment: replace the reference to
validateKey with the current validateAuthToken so the comment matches the test
behavior; locate the comment block in
tests/api/action-adapter-auth-session.unit.test.ts that describes adapter auth
and change the mention of validateKey to validateAuthToken to avoid confusion
for future maintainers.
🟡 Minor comments (25)
src/app/v1/_lib/cors.ts-29-32 (1)

29-32: ⚠️ Potential issue | 🟡 Minor

重新审视 Origin 反射的安全设计

buildCorsHeaders 函数中,当 allowCredentials && options.origin 同时为真时,代码会反射任意请求 Origin 并设置 Access-Control-Allow-Credentials: true。虽然注释已说明这一风险,但函数本身不进行任何 Origin 校验,而是将所有安全责任推给调用方。

关键发现:

  • 生产环境的 CORS 中间件 registerCors 并未传入 allowCredentials 标志,因此当前未暴露此风险
  • applyCorsbuildPreflightResponse 作为导出函数,其 API 签名允许调用方传入该标志而无任何防护
  • 这是一个API 设计缺陷——如果有人直接调用这些函数并传入 allowCredentials: true,就会面临任意 Origin 反射的风险

建议:要么在函数内添加 Origin 白名单校验(需增加参数),要么不公开 allowCredentials 选项,确保通过中间件严格控制 CORS 行为。当前的做法虽然生产安全,但 API 设计本身不够健壮。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/v1/_lib/cors.ts` around lines 29 - 32, The buildCorsHeaders function
currently reflects any request Origin when allowCredentials && options.origin is
true without validating it; update the API to prevent unsafe Origin reflection
by either removing/privatizing the allowCredentials option from public exports
(applyCors, buildPreflightResponse) or adding an explicit origin whitelist check
inside buildCorsHeaders that validates options.origin against a supplied
allowlist before setting Access-Control-Allow-Origin and
Access-Control-Allow-Credentials. Specifically: modify the signatures of
buildCorsHeaders/applyCors/buildPreflightResponse to accept a validatedOrigins
list (or stop accepting allowCredentials from callers), enforce whitelist
validation inside buildCorsHeaders, and ensure registerCors continues to call
into the safe path so callers cannot enable reflection by passing
allowCredentials: true directly.
messages/ja/auth.json-25-27 (1)

25-27: ⚠️ Potential issue | 🟡 Minor

日语 brand.tagline 的用词与其他语言存在细微差异。

"統合API管理コンソール"(统合 = 整合/合并)与其他语言环境的含义(zh-CN: "统一",en: "Unified")略有出入。建议将 "統合" 统一改为 "統一" 以保持跨语言语义一致性。

✏️ 建议修改
-    "tagline": "統合API管理コンソール"
+    "tagline": "統一API管理コンソール"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@messages/ja/auth.json` around lines 25 - 27, Change the Japanese translation
for the brand tagline to match the same semantic meaning used in other locales
by replacing "統合" with "統一" in the JSON key "brand.tagline"
(messages/ja/auth.json) so the value becomes "統一API管理コンソール".
src/app/api/auth/logout/route.ts-15-15 (1)

15-15: ⚠️ Potential issue | 🟡 Minor

process.env.VITEST 检测嵌入生产代码中是反模式

enforceInDevelopment: process.env.VITEST === "true" 使生产路由模块感知测试框架,违背了关注点分离原则。若该配置行为需要按环境区分,应使用专用的环境变量(如 ENFORCE_CSRF_IN_DEV)或在测试 setup 文件中注入配置,而非将测试基础设施标识符硬编码到业务代码。

建议修改
 const csrfGuard = createCsrfOriginGuard({
   allowedOrigins: [],
   allowSameOrigin: true,
-  enforceInDevelopment: process.env.VITEST === "true",
+  enforceInDevelopment: process.env.ENFORCE_CSRF_IN_DEV === "true",
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/auth/logout/route.ts` at line 15, The route currently uses
process.env.VITEST to set enforceInDevelopment which leaks test infra into
production; change the check in the logout route's configuration (the
enforceInDevelopment assignment) to read a purpose-specific environment variable
(e.g. ENFORCE_CSRF_IN_DEV) or use an injected config value from test setup
instead of process.env.VITEST, and update any test setup to set that env/config
when running tests so production code no longer depends on VITEST.
src/app/api/auth/logout/route.ts-30-44 (1)

30-44: ⚠️ Potential issue | 🟡 Minor

resolveSessionTokenMode 静默吞掉异常,可能遮蔽配置错误并跳过会话撤销

resolveSessionTokenMode 捕获所有异常但不记录任何日志,默默回退到 "legacy" 模式。若 getSessionTokenMode() 因环境配置错误而抛出,这将导致会话撤销(第 56–68 行的 mode !== "legacy" 分支)被静默跳过,Redis 中的会话可能无法被正确清除。resolveAuthCookieToken 同样存在相同问题。建议至少在回退时记录一条警告日志。

建议修改
 function resolveSessionTokenMode(): SessionTokenMode {
   try {
     return getSessionTokenMode();
-  } catch {
+  } catch (err) {
+    logger.warn("[AuthLogout] Failed to resolve session token mode, defaulting to legacy", {
+      error: err instanceof Error ? err.message : String(err),
+    });
     return "legacy";
   }
 }

 async function resolveAuthCookieToken(): Promise<string | undefined> {
   try {
     return await getAuthCookie();
-  } catch {
+  } catch (err) {
+    logger.warn("[AuthLogout] Failed to read auth cookie", {
+      error: err instanceof Error ? err.message : String(err),
+    });
     return undefined;
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/auth/logout/route.ts` around lines 30 - 44, Both
resolveSessionTokenMode and resolveAuthCookieToken currently swallow all
exceptions and silently return fallbacks, which can hide configuration errors
and prevent proper session revocation; update each function to catch errors but
log a warning including the caught error details and context (e.g., "Failed to
get session token mode, falling back to 'legacy'" and "Failed to read auth
cookie, treating as undefined") before returning the existing fallback values;
reference resolveSessionTokenMode -> getSessionTokenMode and
resolveAuthCookieToken -> getAuthCookie so you add processLogger.warn (or the
existing logger used in this module) with the error object/string when handling
the catch and then return the same fallback as before.
tests/security/session-store.test.ts-93-98 (1)

93-98: ⚠️ Potential issue | 🟡 Minor

应导出 buildSessionKey 函数供测试使用,避免 Redis key 格式硬编码导致的耦合问题

测试在多个位置(第 93、125、131、145、155 等行)直接使用硬编码的 "cch:session:" 前缀。虽然当前前缀值与 SESSION_KEY_PREFIX 常量一致,但测试无法使用 buildSessionKey 函数(该函数未导出),导致测试与实现细节耦合。若 SESSION_KEY_PREFIX 变更,tests 使用硬编码前缀仍可能误判通过。建议将 buildSessionKey 导出,让测试直接使用该函数构造 key,保持一致性。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/security/session-store.test.ts` around lines 93 - 98, Export the
existing buildSessionKey function (which composes keys using SESSION_KEY_PREFIX)
from the module that defines RedisSessionStore so tests can use it instead of
hard-coding "cch:session:"; change its declaration to a named export (or add a
named export wrapper) and update the tests to call
buildSessionKey(session.sessionId) where they currently assemble the literal
prefix, ensuring Redis key construction stays in sync with SESSION_KEY_PREFIX
and rerun tests.
tests/security/auth-dual-read.test.ts-197-225 (1)

197-225: ⚠️ Potential issue | 🟡 Minor

修复会话时间戳单位不匹配

会话数据的时间戳使用 10 位数值(秒级,createdAt: 1_700_000_000expiresAt: 1_700_000_600),而 auth.ts 第 274 行的验证逻辑与 Date.now()(毫秒级)进行直接比较:if (sessionData.expiresAt <= Date.now())。这导致 1_700_000_600 <= 1739904000000 判定为真,会话被标记为已过期,测试将返回 null 而非预期的 authResult

应改用 Date.now() 相对时间戳以保持单位一致,与 tests/security/session-store.test.ts 中的其他测试对齐:

建议的修复
 mockReadSession.mockResolvedValue({
   sessionId: "sid_opaque_found",
   keyFingerprint: toFingerprint(keyString),
   userId: 21,
   userRole: "user",
-  createdAt: 1_700_000_000,
-  expiresAt: 1_700_000_600,
+  createdAt: Date.now(),
+  expiresAt: Date.now() + 3_600_000,
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/security/auth-dual-read.test.ts` around lines 197 - 225, The session
expiry check compares sessionData.expiresAt (seconds) against Date.now()
(milliseconds) causing valid sessions to be seen as expired; update the
expiry/comparison in the auth validation (where sessionData.expiresAt is
compared to Date.now()) to use consistent units—either convert Date.now() to
seconds (e.g. Math.floor(Date.now()/1000)) or multiply sessionData.expiresAt by
1000 before comparing—so that the check (sessionData.expiresAt <= now) uses the
same time unit and the test using createdAt/expiresAt as 10‑digit seconds
timestamps passes.
src/repository/provider.ts-972-972 (1)

972-972: ⚠️ Potential issue | 🟡 Minor

anthropicAdaptiveThinking 使用宽泛的 object 类型,缺乏运行时类型安全。

BatchProviderUpdatesanthropicAdaptiveThinking 定义为 object | null,而 Provider 接口和 ProviderBatchApplyUpdates 中使用的是 AnthropicAdaptiveThinkingConfig | null。使用 object 允许任意形状的对象直接写入数据库,可能导致无效数据。

建议使用具体类型
+import type { AnthropicAdaptiveThinkingConfig } from "@/types/provider";
+
 export interface BatchProviderUpdates {
   // ...
-  anthropicAdaptiveThinking?: object | null;
+  anthropicAdaptiveThinking?: AnthropicAdaptiveThinkingConfig | null;
   // ...
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/repository/provider.ts` at line 972, The BatchProviderUpdates type uses a
too-broad anthropicAdaptiveThinking?: object | null which allows arbitrary
shapes; change that property to use the concrete type
AnthropicAdaptiveThinkingConfig | null to match Provider and
ProviderBatchApplyUpdates, update any related imports/exports so the symbol
AnthropicAdaptiveThinkingConfig is referenced in the BatchProviderUpdates
definition, and if applicable add or reuse existing runtime validation when
writing to the DB to ensure only valid AnthropicAdaptiveThinkingConfig shapes
are persisted.
src/app/v1/_lib/proxy/auth-guard.ts-43-63 (1)

43-63: ⚠️ Potential issue | 🟡 Minor

限流错误消息语言与文件其他部分不一致。

第 50 行的 "Too many authentication failures. Please retry later." 为英文,而同文件中其余认证错误消息(第 122、142、163、186 行)均为中文。建议统一风格,或根据项目约定决定 API 错误消息的语言策略。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/v1/_lib/proxy/auth-guard.ts` around lines 43 - 63, The rate-limit
error message returned when proxyAuthPolicy.check blocks an IP uses English;
update the message passed to ProxyResponses.buildError in the pre-auth block
(the code using extractClientIp, proxyAuthPolicy.check, rateLimitDecision and
retryAfter) to match the file’s Chinese error wording used elsewhere (keep the
same error key "rate_limit_error" and preserve Retry-After header behavior), so
the API error language is consistent with the other authentication error
responses.
src/app/[locale]/login/loading.tsx-30-30 (1)

30-30: ⚠️ Potential issue | 🟡 Minor

LoadingState 不传 label 会对非中文用户显示硬编码的 "加载中"

loading.tsx 无法获取 locale 参数,因此无法调用 getTranslations。由于 LoadingState 的默认值为 label = "加载中",所有语言的用户都会看到这段中文文字,违反了 i18n 规范。

建议传入空字符串以移除可见文字,同时保留无障碍属性(role="status"aria-live):

🛠️ 建议修复
-        <LoadingState className="text-center" />
+        <LoadingState label="" className="text-center" />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/login/loading.tsx at line 30, LoadingState currently
renders with its default label "加载中" for all locales because loading.tsx can't
access locale/getTranslations; update the call site to pass label="" to
LoadingState (e.g., LoadingState label param) so no visible hard-coded text is
shown while preserving its accessibility attributes (role="status", aria-live)
and visual spinner behavior.
tests/unit/actions/providers-undo-store.test.ts-26-60 (1)

26-60: ⚠️ Potential issue | 🟡 Minor

UNDO_SNAPSHOT_TTL_MS 应调整为更长的时间,当前 10 秒对用户操作过短

代码中 UNDO_SNAPSHOT_TTL_MS 设置为 10000 毫秒(10 秒),但 10 秒的时间窗口对需要用户看到 toast 提示、阅读、思考并点击"撤销"的场景来说太短。考虑网络延迟、用户分心或其他中断场景,这个时间容易超时。建议将 TTL 增加至 30 秒或 60 秒,以提供更好的用户体验。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/actions/providers-undo-store.test.ts` around lines 26 - 60,
Increase the undo snapshot TTL by updating the UNDO_SNAPSHOT_TTL_MS constant
(used by storeUndoSnapshot/consumeUndoToken) from 10_000 to a larger value
(e.g., 30_000 or 60_000), and then update the unit tests to match the new TTL:
adjust the expected expiresAt in the first test to reflect the new expiry
timestamp and change the vi.advanceTimersByTime call in the second test to
exceed the new TTL (e.g., 30_001 or 60_001) so the UNDO_EXPIRED assertion still
holds.
tests/security/session-fixation-rotation.test.ts-84-90 (1)

84-90: ⚠️ Potential issue | 🟡 Minor

simulatePostLoginSessionRotation 直接调用 mock,未测试任何应用逻辑

该辅助函数接受 rotate 参数并直接调用(第 88 行),测试本身传入的就是 mockRotate(第 172 行)。这实际上是在验证 mock 自身的行为,而非应用的 session rotation 代码路径(RedisSessionStore.rotate())。建议改为通过登录路由或 RedisSessionStore 的实际实例来验证 rotation 语义。

Also applies to: 161-177

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/security/session-fixation-rotation.test.ts` around lines 84 - 90, The
helper simulatePostLoginSessionRotation currently just invokes the passed-in
mock (mockRotate) so tests validate the mock instead of application logic;
replace its usage so the test exercises the real rotation path by either
invoking the actual RedisSessionStore.rotate() on a real or test instance of
RedisSessionStore or by performing a full login request against the app's login
route that triggers session rotation, then assert the new session id returned by
that real code; update tests referring to simulatePostLoginSessionRotation and
mockRotate to call the store instance or login endpoint and verify
RedisSessionStore.rotate() behavior rather than the mock.
tests/unit/login/login-visual-regression.test.tsx-51-56 (1)

51-56: ⚠️ Potential issue | 🟡 Minor

afterEachact() 未 await,React 19 下可能无法正确清理异步副作用

LoginPage 内部含 Suspense 和可能的 async effects,同步 act() 不会等待这些副作用完成。document.body.removeChild(container) 可能在卸载 Promise 解决之前运行,导致后续 state 更新尝试访问已移除的节点。

🛠️ 建议修复
- afterEach(() => {
-   act(() => {
-     root.unmount();
-   });
-   document.body.removeChild(container);
- });
+ afterEach(async () => {
+   await act(async () => {
+     root.unmount();
+   });
+   document.body.removeChild(container);
+ });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/login/login-visual-regression.test.tsx` around lines 51 - 56, Make
the afterEach handler async and await React's act around the unmount to ensure
async Suspense/effects inside LoginPage finish before removing the container:
change the afterEach to async, call await act(async () => { root.unmount(); });
and only then call document.body.removeChild(container); this ensures
root.unmount and any async cleanup (Suspense/async effects) complete before DOM
removal.
src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx-214-246 (1)

214-246: ⚠️ Potential issue | 🟡 Minor

res.data 可能为 undefined:建议在访问 undoToken/operationId 前添加防御性检查

removeProvider 的成功路径在某些场景下未填充 data(例如旧版兼容路径或服务端跳过 undo 追踪),res.data.undoToken 会在 try/catch 内抛出,导致删除实际成功但 toast 静默失败。TypeScript 类型约束足以防止编译期问题,但建议加一道运行时防护以避免难以调试的静默行为:

🛠️ 建议修复
  if (res.ok) {
-   const undoToken = res.data.undoToken;
-   const operationId = res.data.operationId;
+   const undoToken = res.data?.undoToken;
+   const operationId = res.data?.operationId;
+
+   if (!undoToken || !operationId) {
+     queryClient.invalidateQueries({ queryKey: ["providers"] });
+     queryClient.invalidateQueries({ queryKey: ["providers-health"] });
+     queryClient.invalidateQueries({ queryKey: ["provider-vendors"] });
+     return;
+   }

    toast.success(tBatchEdit("undo.singleDeleteSuccess"), {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/settings/providers/_components/provider-rich-list-item.tsx
around lines 214 - 246, Check that res.data is defined before reading
res.data.undoToken and res.data.operationId in the success branch (after
res.ok). If res.data is falsy, still show the success toast and run the
queryClient.invalidateQueries calls but do not attach an undo action or attempt
to call undoProviderDelete; if res.data exists, capture undoToken and
operationId and only then render the toast.action with an onClick that calls
undoProviderDelete. Also ensure the onClick closure references the locally
guarded undoToken/operationId (not assumed properties on res) and keep the
existing undoResult handling that checks
PROVIDER_BATCH_PATCH_ERROR_CODES.UNDO_EXPIRED and invalidates queries after a
successful undo.
tests/security/session-login-integration.test.ts-193-236 (1)

193-236: ⚠️ Potential issue | 🟡 Minor

循环内 vi.clearAllMocks() 后未重新设置 mockGetTranslations

第 200 行 vi.clearAllMocks() 清除了所有 mock 实现,但循环内没有重新调用 mockGetTranslations.mockResolvedValue(...)。路由通过 t?.("...") 可选链降级为 undefined,不影响主断言,但翻译相关的逻辑未被真实覆盖。建议在循环开头补充:

🛠️ 建议修复
  for (const mode of modes) {
    vi.clearAllMocks();
+   const mockT = vi.fn((key: string) => `translated:${key}`);
+   mockGetTranslations.mockResolvedValue(mockT);
    mockGetSessionTokenMode.mockReturnValue(mode);
    mockValidateKey.mockResolvedValue(readonlySession);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/security/session-login-integration.test.ts` around lines 193 - 236, The
test clears all mocks inside the loop using vi.clearAllMocks(), but does not
re-establish the translation mock, so mockGetTranslations is lost; after each
vi.clearAllMocks() (inside the for loop in the "all modes preserve readonly
redirect semantics" test) call mockGetTranslations.mockResolvedValue(...) to
restore the translations mock (same value used outside the loop), ensuring
t?.("...") behavior remains deterministic for each iteration and that
translation-related logic is properly covered.
tests/unit/login/login-footer-system-name.test.tsx-4-4 (1)

4-4: ⚠️ Potential issue | 🟡 Minor

应使用 @/ 路径别名,而非相对路径

当前使用了 ../../../src/app/[locale]/login/page 相对路径导入,而同 PR 其他登录测试文件均使用 @/app/[locale]/login/page

-import LoginPage from "../../../src/app/[locale]/login/page";
+import LoginPage from "@/app/[locale]/login/page";

As per coding guidelines: "Use path alias @/ to reference files in ./src/ directory"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/login/login-footer-system-name.test.tsx` at line 4, Replace the
relative import of the LoginPage in
tests/unit/login/login-footer-system-name.test.tsx with the project path alias;
change the import that currently references
"../../../src/app/[locale]/login/page" to use "@/app/[locale]/login/page" so it
matches other login tests and the coding guideline for referencing files under
src.
tests/unit/settings/providers/provider-batch-dialog-step1.test.tsx-8-8 (1)

8-8: ⚠️ Potential issue | 🟡 Minor

移除未使用的 eightProviders 测试夹具

第 319-321 行定义的 eightProviders 在整个测试文件中未被使用,仅定义了 twoProviders 在默认属性中使用。删除未使用的 eightProviders 以保持代码清洁。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/settings/providers/provider-batch-dialog-step1.test.tsx` at line
8, Remove the unused test fixture variable named eightProviders from the test
file (it’s defined but never referenced); keep the existing twoProviders fixture
used in default props and update any nearby comments if they reference
eightProviders so the file contains only used fixtures (search for the
identifier eightProviders and delete its declaration and any related unused
references).
src/lib/auth-session-store/redis-session-store.ts-34-60 (1)

34-60: ⚠️ Potential issue | 🟡 Minor

parseSessionData 的校验应与 isOpaqueSessionContract 保持一致

isOpaqueSessionContractsrc/lib/auth.ts)额外验证了 sessionId.length > 0keyFingerprint.length > 0userRole.length > 0expiresAt > createdAt,但 parseSessionData 缺少这些检查。虽然下游代码(validateAuthToken)会通过 isOpaqueSessionContract 进行二次校验,但两处校验逻辑的不一致会增加维护成本且易引入 bug。

建议补充以下验证以对齐校验逻辑:

建议补充的校验
     if (typeof obj.sessionId !== "string") return null;
+    if (obj.sessionId.length === 0) return null;
     if (typeof obj.keyFingerprint !== "string") return null;
+    if (obj.keyFingerprint.length === 0) return null;
     if (typeof obj.userRole !== "string") return null;
+    if (obj.userRole.length === 0) return null;
     if (typeof obj.userId !== "number" || !Number.isInteger(obj.userId)) return null;
     if (!Number.isFinite(obj.createdAt) || typeof obj.createdAt !== "number") return null;
     if (!Number.isFinite(obj.expiresAt) || typeof obj.expiresAt !== "number") return null;
+    if (obj.expiresAt <= obj.createdAt) return null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/auth-session-store/redis-session-store.ts` around lines 34 - 60,
parseSessionData currently validates field types but misses the same stricter
checks performed by isOpaqueSessionContract; update parseSessionData (in
src/lib/auth-session-store/redis-session-store.ts) to also ensure sessionId,
keyFingerprint, and userRole are non-empty strings (length > 0) and that
expiresAt is strictly greater than createdAt (expiresAt > createdAt), while
preserving the existing type and integer/finite checks for userId, createdAt,
and expiresAt so parsed SessionData matches isOpaqueSessionContract exactly.
tests/unit/settings/providers/adaptive-thinking-editor.test.tsx-135-143 (1)

135-143: ⚠️ Potential issue | 🟡 Minor

测试之间未清理 mock 函数的调用记录。

mockOnEnabledChangemockOnConfigChangedescribe 块顶层创建,但测试之间未调用 vi.clearAllMocks() 或对这些 mock 单独 mockClear()。后续测试可能看到前置测试遗留的调用记录,导致断言产生误判。

建议添加 beforeEach 清理
   const mockOnEnabledChange = vi.fn();
   const mockOnConfigChange = vi.fn();
 
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
   it("renders correctly in disabled state (switch off)", () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/settings/providers/adaptive-thinking-editor.test.tsx` around lines
135 - 143, The tests create top-level mocks mockOnEnabledChange and
mockOnConfigChange but never clear their call history between tests, which can
cause cross-test leakage; add a beforeEach hook inside the
describe("AdaptiveThinkingEditor") block that resets mocks (e.g., call
vi.clearAllMocks() or individually call mockOnEnabledChange.mockClear() and
mockOnConfigChange.mockClear()) so each test starts with a fresh mock state and
assertions are deterministic.
src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx-324-350 (1)

324-350: ⚠️ Potential issue | 🟡 Minor

撤销窗口 TTL 与 toast 时长完全一致,存在竞态风险

toast 的 duration: 10000undo-store.ts 中的 UNDO_SNAPSHOT_TTL_MS = 10_000 完全相同。用户在 toast 即将消失前点击撤销按钮时,由于网络延迟或处理时间,服务端 token 可能已过期,导致撤销失败。建议将 toast 时长略短于服务端 TTL(例如 8000),或将服务端 TTL 延长到 15_000,以留出安全余量。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/settings/providers/_components/vendor-keys-compact-list.tsx
around lines 324 - 350, The toast duration equals the server-side undo TTL,
creating a race where clicking the toast action (which calls undoProviderDelete
with data.undoToken/data.operationId) may hit an expired token; update the toast
duration in vendor-keys-compact-list.tsx to be slightly shorter (e.g. 8000) or
alternatively increase the server TTL constant UNDO_SNAPSHOT_TTL_MS in
undo-store.ts to 15000 so there is a safety buffer; ensure the change is applied
where toast.success is called (duration) and/or where UNDO_SNAPSHOT_TTL_MS is
defined so the client-visible undo button window reliably precedes server
expiry.
src/app/api/auth/login/route.ts-101-103 (1)

101-103: ⚠️ Potential issue | 🟡 Minor

shouldIncludeFailureTaxonomy 仅通过 x-forwarded-proto header 存在与否来判断,容易被客户端伪造。

任何客户端都可以手动添加 x-forwarded-proto 头来获取更详细的错误代码(如 KEY_REQUIREDKEY_INVALID)。虽然这些错误码不泄露敏感数据,但如果设计意图是限制 taxonomy 只对受信任的代理环境暴露,当前实现无法满足。

如果这是有意为之(例如方便调试),建议补充注释说明。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/auth/login/route.ts` around lines 101 - 103, The helper
shouldIncludeFailureTaxonomy currently trusts any presence of the
x-forwarded-proto header (request: NextRequest), which is spoofable by clients;
change it to validate the header against a trusted-source signal (e.g., verify
x-forwarded-for matches a configured list of trusted proxies from an env var
like TRUSTED_PROXIES, require an internal-only header/cookie set by your reverse
proxy, or check a runtime flag indicating internal deployment) or, if the
behavior is intentionally permissive for debugging, add a clear comment above
shouldIncludeFailureTaxonomy documenting that intent and the security tradeoffs
so reviewers know the decision is deliberate.
src/app/[locale]/settings/providers/_components/batch-edit/provider-batch-dialog.tsx-197-197 (1)

197-197: ⚠️ Potential issue | 🟡 Minor

"Unknown error" 为硬编码英文字符串,应使用 i18n。

Lines 197 和 252 中的 "Unknown error" 是用户可见的 toast 消息内容的一部分,应通过翻译函数处理。

As per coding guidelines, "All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text".

修复建议
-      const message = error instanceof Error ? error.message : "Unknown error";
+      const message = error instanceof Error ? error.message : t("toast.unknownError");

需在对应的 batchEdit.json 翻译文件中为 5 种语言添加 toast.unknownError 键。

Also applies to: 252-252

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/app/`[locale]/settings/providers/_components/batch-edit/provider-batch-dialog.tsx
at line 197, Replace the hardcoded "Unknown error" user-facing string with the
i18n translation key (e.g., use the component's translation function like
t('toast.unknownError')) in both places where it's used (the occurrence inside
the error handling that sets const message and the other occurrence around line
with similar toast usage); also add the toast.unknownError key to the
batchEdit.json translation files for all supported locales (zh-CN, zh-TW, en,
ja, ru) so the toast shows localized text.
src/app/[locale]/login/page.tsx-16-16 (1)

16-16: ⚠️ Potential issue | 🟡 Minor

请用 @/ 路径别名替代相对导入

相对路径不符合项目约定,建议改用 @/ 路径别名以保持一致的导入规范。

Suggested fix
-import { resolveLoginRedirectTarget } from "./redirect-safety";
+import { resolveLoginRedirectTarget } from "@/app/[locale]/login/redirect-safety";
As per coding guidelines: Use path alias `@/` to reference files in `./src/` directory
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/login/page.tsx at line 16, 在
src/app/[locale]/login/page.tsx 中将相对导入 resolveLoginRedirectTarget 从
"./redirect-safety" 改为使用项目路径别名,例如使用 "@/..." 形式引用相同模块(保持导入的符号名
resolveLoginRedirectTarget 不变),以遵循项目对 `@/` 路径别名的约定并替换该相对导入。
src/app/[locale]/login/page.tsx-34-35 (1)

34-35: ⚠️ Potential issue | 🟡 Minor

请将默认站点标题与 API Key 标签纳入 i18n

这两处是用户可见文本,目前硬编码会导致多语言不一致;建议改为翻译键或由系统设置/翻译提供默认值。

As per coding guidelines: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text.

Also applies to: 336-338

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/login/page.tsx around lines 34 - 35, DEFAULT_SITE_TITLE and
the visible "API Key" label are hardcoded; replace them with i18n lookups (e.g.,
use the translation hook/utility your app uses such as
t('login.defaultSiteTitle') and t('login.apiKeyLabel')) and add corresponding
keys/translations for zh-CN, zh-TW, en, ja, ru in the locale files; ensure
DEFAULT_SITE_TITLE constant is removed or set from t(...) and update any usages
(page.tsx references to DEFAULT_SITE_TITLE and the API key input/label
rendering) to use the translation keys so all user-facing text is localized.
src/actions/providers.ts-1680-1691 (1)

1680-1691: ⚠️ Potential issue | 🟡 Minor

skipReason 文本硬编码且未本地化

skipReason 会被直接渲染到预览文案中,当前英文硬编码在多语言界面下会显得突兀。建议返回稳定的 reason code/枚举,让前端按 locale 映射文案。

As per coding guidelines: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text.
Based on learnings: In the ding113/claude-code-hub repository, Server Actions (files under src/actions/*.ts) currently return hardcoded Chinese error messages directly. This is a codebase-wide architectural decision that applies to all action files (e.g., model-prices.ts, users.ts, system-config.ts). Changing this pattern requires a coordinated i18n refactor across all Server Actions, which should be handled as a separate unified task rather than piecemeal changes in individual PRs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/providers.ts` around lines 1680 - 1691, The skipReason is
currently hardcoded user-facing text; change it to return a stable reason
code/enum instead of the literal message so the frontend can map to localized
strings. In the block that sets isCompatible/skipReason (references:
isClaudeOnly, isCodexOnly, isGeminiOnly, provider.providerType, isCompatible,
skipReason), replace the message assignments with predefined keys (e.g.,
"only_applicable_claude", "only_applicable_codex", "only_applicable_gemini") or
an exported enum constant, and ensure the function returns that code (not human
text) so the UI can perform i18n. Ensure existing boolean logic remains
unchanged and update any callers/tests expecting a string message to handle the
reason code.
src/lib/provider-patch-contract.ts-278-343 (1)

278-343: ⚠️ Potential issue | 🟡 Minor

校验错误消息为硬编码英文,可能直接透传到前端

这些 message 会被 actions 透传给 UI,建议改为返回 error code / message key,由前端按 locale 映射文案,避免多语言不一致。

As per coding guidelines: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/provider-patch-contract.ts` around lines 278 - 343, The validation
messages in createInvalidPatchShapeError/createInvalidRootPatchShapeError and
calls from normalizePatchField are hardcoded English strings; change them to
return a locale-agnostic error identifier and optional params instead of display
text so the UI can map to translations. Update createInvalidPatchShapeError and
createInvalidRootPatchShapeError signatures to include an errorKey (e.g.
"patch.invalid_shape", "patch.unknown_keys", "patch.must_be_object",
"patch.choose_one_mode", "patch.set_requires_value", "patch.invalid_set_value")
and an optional params object, and replace all literal message arguments in
normalizePatchField with the corresponding errorKey and params; keep
PROVIDER_PATCH_ERROR_CODES and field unchanged so callers can still detect error
type. Ensure the ProviderPatchResult error shape is extended to include errorKey
and params for i18n consumption.

Comment on lines +2 to +7
import { createCsrfOriginGuard } from "../../src/lib/security/csrf-origin-guard";
import { LoginAbusePolicy } from "../../src/lib/security/login-abuse-policy";
import {
buildSecurityHeaders,
DEFAULT_SECURITY_HEADERS_CONFIG,
} from "../../src/lib/security/security-headers";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

应使用 @/ 路径别名代替相对路径。

静态导入使用了 ../../src/lib/... 形式的相对路径,不符合编码规范。

As per coding guidelines: "Use path alias @/ to reference files in ./src/ directory"

建议修改为路径别名
-import { createCsrfOriginGuard } from "../../src/lib/security/csrf-origin-guard";
-import { LoginAbusePolicy } from "../../src/lib/security/login-abuse-policy";
-import {
-  buildSecurityHeaders,
-  DEFAULT_SECURITY_HEADERS_CONFIG,
-} from "../../src/lib/security/security-headers";
+import { createCsrfOriginGuard } from "@/lib/security/csrf-origin-guard";
+import { LoginAbusePolicy } from "@/lib/security/login-abuse-policy";
+import {
+  buildSecurityHeaders,
+  DEFAULT_SECURITY_HEADERS_CONFIG,
+} from "@/lib/security/security-headers";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { createCsrfOriginGuard } from "../../src/lib/security/csrf-origin-guard";
import { LoginAbusePolicy } from "../../src/lib/security/login-abuse-policy";
import {
buildSecurityHeaders,
DEFAULT_SECURITY_HEADERS_CONFIG,
} from "../../src/lib/security/security-headers";
import { createCsrfOriginGuard } from "@/lib/security/csrf-origin-guard";
import { LoginAbusePolicy } from "@/lib/security/login-abuse-policy";
import {
buildSecurityHeaders,
DEFAULT_SECURITY_HEADERS_CONFIG,
} from "@/lib/security/security-headers";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/security/full-security-regression.test.ts` around lines 2 - 7, Replace
the relative imports in this test with the project path alias; update the import
statements that bring in createCsrfOriginGuard, LoginAbusePolicy,
buildSecurityHeaders, and DEFAULT_SECURITY_HEADERS_CONFIG so they use
"@/lib/security/..." (aliasing ./src/) instead of "../../src/lib/..."; ensure
each import specifier references the same exported symbols
(createCsrfOriginGuard, LoginAbusePolicy, buildSecurityHeaders,
DEFAULT_SECURITY_HEADERS_CONFIG) and that your tsconfig/module resolver supports
the "@/..." alias.

Comment on lines +92 to +119
it("SESSION_TOKEN_MODE defaults to opaque", async () => {
delete process.env.SESSION_TOKEN_MODE;

vi.resetModules();
const { getSessionTokenMode } = await import("../../src/lib/auth");

expect(getSessionTokenMode()).toBe("opaque");
});

it("OpaqueSessionContract has required fields", async () => {
vi.resetModules();
const { isOpaqueSessionContract } = await import("../../src/lib/auth");

const contract = {
sessionId: "sid_opaque_session_123",
keyFingerprint: "sha256:abc123",
createdAt: 1_700_000_000,
expiresAt: 1_700_000_300,
userId: 42,
userRole: "admin",
};

expect(isOpaqueSessionContract(contract)).toBe(true);

const missingUserRole = { ...contract } as Partial<typeof contract>;
delete missingUserRole.userRole;
expect(isOpaqueSessionContract(missingUserRole)).toBe(false);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

动态导入同样应使用 @/ 路径别名。

await import("../../src/lib/auth")await import("../../src/lib/auth-session-store/redis-session-store") 也应改为 @/lib/auth@/lib/auth-session-store/redis-session-store

As per coding guidelines: "Use path alias @/ to reference files in ./src/ directory"

建议修改动态导入路径
-      const { getSessionTokenMode } = await import("../../src/lib/auth");
+      const { getSessionTokenMode } = await import("@/lib/auth");
-      const { isOpaqueSessionContract } = await import("../../src/lib/auth");
+      const { isOpaqueSessionContract } = await import("@/lib/auth");
-      const { RedisSessionStore } = await import(
-        "../../src/lib/auth-session-store/redis-session-store"
-      );
+      const { RedisSessionStore } = await import(
+        "@/lib/auth-session-store/redis-session-store"
+      );
-      const { AUTH_COOKIE_NAME, setAuthCookie } = await import("../../src/lib/auth");
+      const { AUTH_COOKIE_NAME, setAuthCookie } = await import("@/lib/auth");
-      const { setAuthCookie } = await import("../../src/lib/auth");
+      const { setAuthCookie } = await import("@/lib/auth");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/security/full-security-regression.test.ts` around lines 92 - 119,
Update the dynamic imports in the test to use the project path alias instead of
relative paths: replace await import("../../src/lib/auth") with await
import("@/lib/auth") (so getSessionTokenMode is imported via "@/lib/auth") and
similarly replace any await
import("../../src/lib/auth-session-store/redis-session-store") with await
import("@/lib/auth-session-store/redis-session-store") (so
isOpaqueSessionContract remains sourced from "@/lib/auth"); keep
vi.resetModules() as-is and only change the module specifiers.

@@ -0,0 +1,253 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { PROVIDER_BATCH_PATCH_ERROR_CODES } from "../../../src/lib/provider-batch-patch-error-codes";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

应使用 @/ 路径别名替代相对路径。

Line 2 及后续所有动态 import(lines 63, 78, 90, 101, 112, 125, 157, 180, 202, 219, 234)使用了 ../../../src/ 相对路径,违反了编码规范中要求使用 @/ 别名的规则。

As per coding guidelines, "Use path alias @/ to reference files in ./src/ directory".

修复示例(静态 import)
-import { PROVIDER_BATCH_PATCH_ERROR_CODES } from "../../../src/lib/provider-batch-patch-error-codes";
+import { PROVIDER_BATCH_PATCH_ERROR_CODES } from "@/lib/provider-batch-patch-error-codes";
修复示例(动态 import)
-    const { batchDeleteProviders } = await import("../../../src/actions/providers");
+    const { batchDeleteProviders } = await import("@/actions/providers");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { PROVIDER_BATCH_PATCH_ERROR_CODES } from "../../../src/lib/provider-batch-patch-error-codes";
import { PROVIDER_BATCH_PATCH_ERROR_CODES } from "@/lib/provider-batch-patch-error-codes";
Suggested change
import { PROVIDER_BATCH_PATCH_ERROR_CODES } from "../../../src/lib/provider-batch-patch-error-codes";
const { batchDeleteProviders } = await import("@/actions/providers");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/actions/provider-undo-delete.test.ts` at line 2, Replace all
relative imports referencing ../../../src/ with the project path alias `@/` so
imports conform to the coding guideline; specifically change the static import
of PROVIDER_BATCH_PATCH_ERROR_CODES and every dynamic import occurrences (the
dynamic import calls around lines where modules are loaded — e.g., the import
expressions that currently use "../../../src/..." at lines referenced in the
review) to use "@/..." paths instead, ensuring module specifiers match the
src-relative alias and updating any test runner/tsconfig configs if necessary to
resolve the alias.

Comment on lines +96 to +97
expect(result.error).toBe("无权限执行此操作");
expect(deleteProvidersBatchMock).not.toHaveBeenCalled();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, check if src/actions/providers.ts exists and search for hardcoded Chinese strings
fd 'providers.ts' --type f src/actions/

Repository: ding113/claude-code-hub

Length of output: 91


🏁 Script executed:

# Check the test file to see the assertions at the specified lines
head -n 230 tests/unit/actions/provider-undo-delete.test.ts | tail -n +90

Repository: ding113/claude-code-hub

Length of output: 5011


🏁 Script executed:

# Look for hardcoded Chinese error messages in src/actions/providers.ts
rg -n '无权限|请选择|批量操作|删除供应商|撤销' --type ts src/actions/

Repository: ding113/claude-code-hub

Length of output: 5843


服务端操作中硬编码的中文错误消息缺少 i18n 国际化支持

测试正确反映了源码行为,但 src/actions/providers.ts 中的多个错误消息("无权限执行此操作""请选择要删除的供应商""单次批量操作最多支持""撤销窗口已过期" 等)均为硬编码中文,未通过 i18n 支持 5 种语言。这违反了编码规范"所有用户展示字符串必须使用 i18n,支持 zh-CN、zh-TW、en、ja、ru 5 种语言"。

注:此问题为代码库级的架构性问题,不仅限于 providers.ts,所有 src/actions/*.ts 文件中的错误消息均存在此情况。建议作为统一的 i18n 重构任务处理,而非在单个 PR 中逐个修改。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/actions/provider-undo-delete.test.ts` around lines 96 - 97, The
tests and source use hardcoded Chinese error strings (e.g., "无权限执行此操作",
"请选择要删除的供应商", "单次批量操作最多支持", "撤销窗口已过期") in src/actions/providers.ts and other
src/actions/*.ts files; replace these with i18n lookups and update tests to
assert against localized messages or mock the i18n function. Specifically, in
providers.ts refactor all user-facing error strings to call the project i18n API
(e.g., the central translate function used elsewhere) and ensure tests (like
provider-undo-delete.test.ts) either import/mock that i18n function or compare
against i18n(key) instead of literal Chinese strings so coverage supports
zh-CN/zh-TW/en/ja/ru; treat this as a cross-file refactor rather than per-PR
one-off edits.

Comment on lines +97 to +110
vi.mock("./forms/provider-form/components/section-card", () => ({
SmartInputWrapper: ({ label, children }: { label: string; children: React.ReactNode }) => (
<div data-testid="smart-input-wrapper">
<label>{label}</label>
{children}
</div>
),
ToggleRow: ({ label, children }: { label: string; children: React.ReactNode }) => (
<div data-testid="toggle-row">
<label>{label}</label>
{children}
</div>
),
}));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the adaptive-thinking-editor component file
echo "=== Locating AdaptiveThinkingEditor component ==="
fd "adaptive-thinking-editor" -e tsx -e ts --type f

echo -e "\n=== Checking test file location ==="
ls -la tests/unit/settings/providers/adaptive-thinking-editor.test.tsx

echo -e "\n=== Finding section-card component ==="
fd "section-card" -e tsx -e ts --type f

Repository: ding113/claude-code-hub

Length of output: 543


🏁 Script executed:

#!/bin/bash
# Once we find the adaptive-thinking-editor component, check its imports
echo "=== Looking for AdaptiveThinkingEditor component ==="
find . -name "*adaptive-thinking-editor*" -type f

# Also check the directory structure
echo -e "\n=== Directory structure of tests/unit/settings/providers/ ==="
tree tests/unit/settings/providers/ || find tests/unit/settings/providers/ -type f -name "*.tsx" -o -name "*.ts"

Repository: ding113/claude-code-hub

Length of output: 1189


🏁 Script executed:

#!/bin/bash
# Search for import statements related to section-card or SmartInputWrapper/ToggleRow
echo "=== Finding imports of section-card, SmartInputWrapper, or ToggleRow ==="
rg "import.*(?:section-card|SmartInputWrapper|ToggleRow)" -g "*.tsx" -g "*.ts" -A 2

echo -e "\n=== Checking src directory structure ==="
find src -type d | grep -E "(settings|providers|forms)" | head -20

Repository: ding113/claude-code-hub

Length of output: 3944


mock 路径相对路径解析错误,导致 mock 无法生效。

vi.mock("./forms/provider-form/components/section-card", ...) 中的相对路径会从测试文件所在目录(tests/unit/settings/providers/)开始解析,而非源文件 src/app/[locale]/settings/providers/_components/adaptive-thinking-editor.tsx 所在目录。实际导入的 section-card 组件路径为 src/app/[locale]/settings/providers/_components/forms/provider-form/components/section-card.tsx,两者不匹配导致 mock 无法拦截导入。

建议改为使用路径别名:@/app/[locale]/settings/providers/_components/forms/provider-form/components/section-card

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/settings/providers/adaptive-thinking-editor.test.tsx` around lines
97 - 110, The vi.mock call for the section-card module uses a relative path that
resolves from the test file location, so update the module specifier in the
vi.mock(...) call to the project alias used by the source file import (use
"@/app/[locale]/settings/providers/_components/forms/provider-form/components/section-card")
so the mock intercepts imports; keep the exported mock shape (SmartInputWrapper
and ToggleRow) unchanged.

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

This PR implements a comprehensive security auth overhaul and provider batch operations feature. The implementation is well-designed with proper security controls, comprehensive test coverage, and clear documentation.

PR Size: XL

  • Lines changed: 20,068 (19,172 additions, 896 deletions)
  • Files changed: 111

Note: This is a large PR. Consider splitting future changes of this magnitude into multiple PRs for easier review:

  • Security auth overhaul (session tokens, CSRF, rate limiting)
  • Provider batch operations (preview, apply, undo)
  • Login page redesign
  • Test additions

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 0 0 0
Security 0 0 0 0
Error Handling 0 0 0 0
Types 0 0 0 0
Comments/Docs 0 0 0 0
Tests 0 0 0 0
Simplification 0 0 0 0

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Properly implemented
  • Error handling - Appropriate with logging
  • Type safety - No unsafe any usage found
  • Documentation accuracy - Comments match code
  • Test coverage - Comprehensive (55+ test files, 2791+ tests)
  • Code clarity - Good

Security Implementation Notes

The security implementations are well-designed:

  1. IP extraction (login/route.ts:118-129): Uses rightmost x-forwarded-for entry, which is the correct approach when trusted proxies are configured. The code comment explains this design decision.

  2. Rate limiting: Dual-scope (IP + key) rate limiting with configurable thresholds. IP-based limiting applies to all requests; key-based limiting only activates when a key is provided.

  3. CSRF protection: Origin-based guard with same-origin bypass and configurable enforcement.

  4. Constant-time comparison: Properly implemented using crypto.timingSafeEqual with length-padding to prevent timing attacks.

  5. Session management: Opaque session tokens with Redis-backed storage, dual-read migration support, and proper revocation on logout.


Automated review by Claude AI

>
<div className="space-y-2">
<Label htmlFor="apiKey">API Key</Label>
<div className="relative">
Copy link
Contributor

@github-actions github-actions bot Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] [STANDARD-VIOLATION] Hardcoded UI string bypasses i18n

Evidence: src/app/[locale]/login/page.tsx:338 contains:

<Label htmlFor="apiKey">API Key</Label>

Why this is a problem: This renders English text for all locales. The project standard requires: i18n Required - All user-facing strings must use i18n (5 languages supported). Never hardcode display text.

Suggested fix:

// src/app/[locale]/login/page.tsx
<Label htmlFor="apiKey">{t("form.apiKeyLabel")}</Label>

Also add form.apiKeyLabel to messages/{en,zh-CN,zh-TW,ja,ru}/auth.json.

label: t("toast.undo"),
onClick: async () => {
const undoResult = await undoProviderPatch({ undoToken, operationId });
if (undoResult.ok) {
Copy link
Contributor

@github-actions github-actions bot Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] [ERROR-NO-USER-FEEDBACK] Undo action lacks exception handling + non-i18n fallback

Evidence:

  • src/app/[locale]/settings/providers/_components/batch-edit/provider-batch-dialog.tsx:239 runs a server action inside a toast action without a try/catch:
const undoResult = await undoProviderPatch({ undoToken, operationId });
  • The same file uses a hardcoded user-facing fallback string (e.g. src/app/[locale]/settings/providers/_components/batch-edit/provider-batch-dialog.tsx:197, :252):
const message = error instanceof Error ? error.message : "Unknown error";

Why this is a problem:

  • If the server action call rejects (network/server error), this will surface as an unhandled promise rejection and the user gets no toast feedback.
  • "Unknown error" bypasses the i18n requirement (i18n Required - All user-facing strings must use i18n (5 languages supported). Never hardcode display text).

Suggested fix:

action: {
  label: t("toast.undo"),
  onClick: async () => {
    try {
      const undoResult = await undoProviderPatch({ undoToken, operationId });

      if (undoResult.ok) {
        toast.success(t("toast.undoSuccess", { count: undoResult.data.revertedCount }));
        await queryClient.invalidateQueries({ queryKey: ["providers"] });
        return;
      }

      if (undoResult.errorCode === PROVIDER_BATCH_PATCH_ERROR_CODES.UNDO_EXPIRED) {
        toast.error(t("toast.undoExpired"));
        return;
      }

      toast.error(t("undo.failed"));
    } catch (error) {
      const message = error instanceof Error ? error.message : t("toast.unknownError");
      toast.error(t("toast.undoFailed", { error: message }));
    }
  },
}

Also replace the other "Unknown error" fallbacks with t("toast.unknownError") and add that key to messages/{en,zh-CN,zh-TW,ja,ru}/settings/providers/batchEdit.json.

async function resolveAuthCookieToken(): Promise<string | undefined> {
try {
return await getAuthCookie();
} catch {
Copy link
Contributor

@github-actions github-actions bot Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[High] [ERROR-SILENT] Logout can skip server-side session revocation without logging

Evidence:

  • src/app/api/auth/logout/route.ts:30 swallows exceptions from getSessionTokenMode():
function resolveSessionTokenMode(): SessionTokenMode {
  try {
    return getSessionTokenMode();
  } catch {
    return "legacy";
  }
}
  • src/app/api/auth/logout/route.ts:41 swallows exceptions from getAuthCookie():
async function resolveAuthCookieToken(): Promise<string | undefined> {
  try {
    return await getAuthCookie();
  } catch {
    return undefined;
  }
}

Why this is a problem: If getAuthCookie() / getSessionTokenMode() throws unexpectedly, the outer revocation try/catch never sees the error. Logout returns 200 and clears the client cookie, but the opaque session may remain valid and the failure is invisible in logs.

Suggested fix (let the existing outer try/catch log failures):

function resolveSessionTokenMode(): SessionTokenMode {
  return getSessionTokenMode();
}

async function resolveAuthCookieToken(): Promise<string | undefined> {
  return await getAuthCookie();
}

If you want to keep the fallback behavior, at least logger.warn(...) in the catch and rethrow or return a sentinel that triggers logging.

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

This PR is extremely large and mixes auth/security overhaul, login UI changes, provider batch operations, and extensive tests in a single diff, which increases review and rollback risk.
The inline comments focus on a few concrete, diff-local issues that can lead to user-facing inconsistencies or make session revocation failures invisible.

PR Size: XL

  • Lines changed: 20068
  • Files changed: 111
  • Split suggestions (XL):
    • Split auth/session/security changes (routes + session store + guards) from provider batch operations.
    • Split login UI redesign from backend auth route changes.
    • Split i18n message updates and test-only additions into follow-up PRs where possible.

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 0 1 0
Security 0 1 0 0
Error Handling 0 1 1 0
Types 0 0 0 0
Comments/Docs 0 0 0 0
Tests 0 0 0 0
Simplification 0 0 0 0

Critical Issues (Must Fix)

None.

High Priority Issues (Should Fix)

  • [High] swallows errors when reading auth state, which can skip opaque session revocation without logging.
  • [Medium] hardcodes a user-facing label (bypasses i18n).
  • [Medium] undo action lacks exception handling and uses a hardcoded fallback error string.

Review Coverage

  • Logic and correctness
  • Security (OWASP Top 10)
  • Error handling
  • Type safety
  • Documentation accuracy
  • Test coverage
  • Code clarity

Automated review by Codex AI

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

This PR is extremely large and mixes auth/security overhaul, login UI changes, provider batch operations, and extensive tests in a single diff, which increases review and rollback risk.
The inline comments focus on a few concrete, diff-local issues that can lead to user-facing inconsistencies or make session revocation failures invisible.

PR Size: XL

  • Lines changed: 20068
  • Files changed: 111
  • Split suggestions (XL):
    • Split auth/session/security changes (routes + session store + guards) from provider batch operations.
    • Split login UI redesign from backend auth route changes.
    • Split i18n message updates and test-only additions into follow-up PRs where possible.

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 0 1 0
Security 0 1 0 0
Error Handling 0 1 1 0
Types 0 0 0 0
Comments/Docs 0 0 0 0
Tests 0 0 0 0
Simplification 0 0 0 0

Critical Issues (Must Fix)

None.

High Priority Issues (Should Fix)

  • [High] src/app/api/auth/logout/route.ts:41 swallows errors when reading auth state, which can skip opaque session revocation without logging.
  • [Medium] src/app/[locale]/login/page.tsx:338 hardcodes a user-facing label (bypasses i18n).
  • [Medium] src/app/[locale]/settings/providers/_components/batch-edit/provider-batch-dialog.tsx:239 undo action lacks exception handling and uses a hardcoded fallback error string.

Review Coverage

  • Logic and correctness
  • Security (OWASP Top 10)
  • Error handling
  • Type safety
  • Documentation accuracy
  • Test coverage
  • Code clarity

Automated review by Codex AI

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Identified PR #806 and applied the size label size/XL (20068 lines changed across 111 files).
  • Posted 3 inline review comments (diff-only) with concrete fixes:
    • src/app/[locale]/login/page.tsx:338 — hardcoded "API Key" label bypasses i18n.
    • src/app/[locale]/settings/providers/_components/batch-edit/provider-batch-dialog.tsx:239 — undo toast action needs try/catch + replace hardcoded "Unknown error" with i18n.
    • src/app/api/auth/logout/route.ts:41 — inner try/catch swallows errors and can skip opaque session revocation without logging.
  • Submitted a review summary on the PR (includes XL split suggestions).

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

114 files reviewed, 6 comments

Edit Code Review Agent Settings | Greptile

import { PROVIDER_BATCH_PATCH_ERROR_CODES } from "@/lib/provider-batch-patch-error-codes";
import { RedisKVStore } from "@/lib/redis/redis-kv-store";

const UNDO_SNAPSHOT_TTL_SECONDS = 30;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

30-second TTL increased from 10s, but still might be too short for batch operations where users navigate away or review changes before realizing they need to undo

Suggested change
const UNDO_SNAPSHOT_TTL_SECONDS = 30;
const UNDO_SNAPSHOT_TTL_SECONDS = 60;

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/providers/undo-store.ts
Line: 7

Comment:
30-second TTL increased from 10s, but still might be too short for batch operations where users navigate away or review changes before realizing they need to undo

```suggestion
const UNDO_SNAPSHOT_TTL_SECONDS = 60;
```

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +190 to +191
if (!key || typeof key !== "string") {
loginPolicy.recordFailure(clientIp);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing key parameter in recordFailure() call means key-based rate limiting won't trigger for malformed keys, only IP-based limiting applies

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/api/auth/login/route.ts
Line: 190-191

Comment:
missing `key` parameter in `recordFailure()` call means key-based rate limiting won't trigger for malformed keys, only IP-based limiting applies

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +159 to +161
// LRU bump: delete + re-insert so locked entries survive eviction
this.attempts.delete(scopeKey);
this.attempts.set(scopeKey, { ...record, lockedUntil });
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

implemented LRU bump strategy to mitigate the 10k entry limit; delete + re-insert moves entries to end of Map iteration order so eviction removes least-recently-used first

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/security/login-abuse-policy.ts
Line: 159-161

Comment:
implemented LRU bump strategy to mitigate the 10k entry limit; delete + re-insert moves entries to end of Map iteration order so eviction removes least-recently-used first

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +274 to +279
if (sessionData.expiresAt <= Date.now()) {
logger.warn("Opaque session expired (application-level check)", {
sessionId: sessionData.sessionId,
expiresAt: sessionData.expiresAt,
});
return null;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added defense-in-depth expiry check at application level after Redis read; verify Redis TTL matches expiresAt duration

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/auth.ts
Line: 274-279

Comment:
added defense-in-depth expiry check at application level after Redis read; verify Redis TTL matches `expiresAt` duration

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +24 to +27
const LUA_GET_AND_DEL = `
local val = redis.call('GET', KEYS[1])
if val then redis.call('DEL', KEYS[1]) end
return val`;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

implemented atomic GET+DEL via Lua script to prevent TOCTOU race on single-use tokens

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/redis/redis-kv-store.ts
Line: 24-27

Comment:
implemented atomic GET+DEL via Lua script to prevent TOCTOU race on single-use tokens

How can I resolve this? If you propose a fix, please make it concise.

const session = await validateKey(key, { allowReadOnlyAccess: true });
if (!session) {
return NextResponse.json({ error: t?.("apiKeyInvalidOrExpired") }, { status: 401 });
loginPolicy.recordFailure(clientIp);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing key parameter in second recordFailure() call means invalid key attempts aren't tracked for key-based rate limiting, only IP-based limiting applies

Suggested change
loginPolicy.recordFailure(clientIp);
loginPolicy.recordFailure(clientIp, key);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/api/auth/login/route.ts
Line: 212

Comment:
missing `key` parameter in second `recordFailure()` call means invalid key attempts aren't tracked for key-based rate limiting, only IP-based limiting applies

```suggestion
      loginPolicy.recordFailure(clientIp, key);
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/actions/providers.ts (1)

3861-3863: ⚠️ Potential issue | 🟡 Minor

注释中包含 emoji,需移除以符合规范。
As per coding guidelines: "/*.{js,ts,tsx,jsx}: Never use emoji characters in any code, comments, or string literals".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/providers.ts` around lines 3861 - 3863, The inline comment
containing an emoji in the object literal near the Response API fix must be
cleaned up: in src/actions/providers.ts locate the object with properties type:
"message" and role: "user" (the comment that currently reads "⭐ 修复 `#189`:
Response API 要求 input 数组中的每个元素必须包含 type 字段") and remove the emoji character(s)
so the comment reads plain text (e.g., "修复 `#189`: Response API 要求 input
数组中的每个元素必须包含 type 字段") or delete the comment entirely to comply with the
no-emoji rule.
🧹 Nitpick comments (11)
src/app/api/auth/logout/route.ts (1)

18-28: getLogoutSessionStore 存在并发初始化竞态条件,且内联类型注解可读性差

1. 竞态条件getLogoutSessionStoreawait import(...) 前做了 if (!sessionStoreInstance) 检查,但这两个操作之间存在异步间隙。若两个并发退出请求同时通过 null 检查,将各自创建一个 RedisSessionStore 实例,后一次赋值会覆盖前一次,被覆盖的实例及其潜在 Redis 连接将被泄漏。

建议改用 Promise 单例模式,确保初始化逻辑只执行一次:

2. 内联 import() 类型注解:行 18-20 的内联动态 import() 类型写法可读性差,建议在文件顶部通过 import type 导入。

♻️ 建议的修复方案
+import type { RedisSessionStore } from "@/lib/auth-session-store/redis-session-store";

-let sessionStoreInstance:
-  | import("@/lib/auth-session-store/redis-session-store").RedisSessionStore
-  | null = null;
+let sessionStorePromise: Promise<RedisSessionStore> | null = null;

 async function getLogoutSessionStore() {
-  if (!sessionStoreInstance) {
-    const { RedisSessionStore } = await import("@/lib/auth-session-store/redis-session-store");
-    sessionStoreInstance = new RedisSessionStore();
-  }
-  return sessionStoreInstance;
+  if (!sessionStorePromise) {
+    sessionStorePromise = import("@/lib/auth-session-store/redis-session-store").then(
+      ({ RedisSessionStore }) => new RedisSessionStore(),
+    );
+  }
+  return sessionStorePromise;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/auth/logout/route.ts` around lines 18 - 28, getLogoutSessionStore
currently has a race: multiple concurrent callers can pass the null check and
each call will dynamically import and construct a RedisSessionStore, leaking
instances; also the inline dynamic import type annotation is hard to read. Fix
by making sessionStoreInstance hold a Promise that is set on first call (a
"promise singleton") so concurrent callers await the same initialization Promise
and only one RedisSessionStore is constructed, e.g. replace the nullable
instance with a Promise<RedisSessionStore> | null and ensure
getLogoutSessionStore sets that Promise before awaiting the import/constructor;
also move the type import to the top with an `import type { RedisSessionStore }
from "@/lib/auth-session-store/redis-session-store"` and use that type for the
Promise and return signature to improve readability.
src/repository/provider.ts (1)

1355-1381: restoreProvidersBatch 在单个事务内串行执行 N 次多步查询,大批量时事务持锁时间较长

当前实现对每个 id 顺序执行 restoreProviderInTransaction,后者最多包含以下 DB 往返:

  1. SELECT 候选供应商
  2. UPDATE providers(乐观并发)
  3. SELECT 活跃引用(activeReference
  4. SELECT 活跃端点(activeEndpoint
  5. SELECT 待恢复端点,UPDATE providerEndpoints

对于 N 个供应商,这意味着 O(N × 5) 次串行查询共享同一个事务,事务持续时间随 N 线性增长。

虽然 60 秒的 TTL(PROVIDER_RESTORE_MAX_AGE_MS)使实际批量规模保持较小,但建议在函数文档中明确说明此约束,或在批量较大时(如 uniqueIds.length > 10)记录警告日志,以便运维排查。

建议在大批量时添加日志警告
+  if (uniqueIds.length > 10) {
+    logger.warn("restoreProvidersBatch:large_batch", { count: uniqueIds.length });
+  }
+
   const restoredCount = await db.transaction(async (tx) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/repository/provider.ts` around lines 1355 - 1381, restoreProvidersBatch
performs sequential multi-step DB work for each id inside one long transaction
(calling restoreProviderInTransaction repeatedly), which can cause long-held
locks for large batches; update the function to (1) add a clear doc comment
noting the 60s TTL constraint (PROVIDER_RESTORE_MAX_AGE_MS) and expected small
batch sizes, and (2) emit a warning log when uniqueIds.length exceeds a safe
threshold (e.g., > 10) before starting the transaction so operators are alerted
to potential long transactions; reference restoreProvidersBatch, uniqueIds,
restoredCount, and PROVIDER_RESTORE_MAX_AGE_MS in the message.
src/app/[locale]/settings/providers/_components/thinking-budget-editor.tsx (1)

35-35: 建议将魔法数字提取为具名常量。

"10240"(自定义模式默认值)和 "32000"(最大值)直接以字符串字面量散落在代码中,语义不清晰,且与 min="1024" / max="32000" 的 HTML 属性分散维护,一旦 Anthropic 调整限制,需多处同步修改。

♻️ 建议提取常量
+"use client";
+
+import { Info } from "lucide-react";
 ...

+const THINKING_BUDGET_DEFAULT = "10240";
+const THINKING_BUDGET_MAX = "32000";
+const THINKING_BUDGET_MIN = 1024;
+
 const handleModeChange = (val: string) => {
   if (val === "inherit") {
     onChange("inherit");
   } else {
-    onChange("10240");
+    onChange(THINKING_BUDGET_DEFAULT);
   }
 };

 const handleMaxOut = () => {
-  onChange("32000");
+  onChange(THINKING_BUDGET_MAX);
 };

 // Input 的 min/max 也统一使用常量
-min="1024"
-max="32000"
+min={String(THINKING_BUDGET_MIN)}
+max={THINKING_BUDGET_MAX}

Also applies to: 43-45

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/settings/providers/_components/thinking-budget-editor.tsx
at line 35, The hard-coded magic strings for the thinking budget ("10240"
default and "32000" max, and the min "1024" used in attributes) should be
extracted into named constants to centralize semantics and avoid duplication;
update the onChange("10240") call, the input's min/max attributes, and any other
occurrences (lines ~43-45) to use constants like DEFAULT_CUSTOM_THINKING_BUDGET,
MIN_THINKING_BUDGET, and MAX_THINKING_BUDGET (choose string or numeric types
consistently with the input handling and parse/format where needed) so future
Anthropic limit changes require a single update and the code is clearer.
src/lib/providers/undo-store.ts (1)

23-31: UNDO_CONFLICT 代码出现在导出类型中,但 consumeUndoToken 从未返回它——建议缩窄函数返回类型或补充注释。

ConsumeUndoResultcode 联合类型中包含 "UNDO_CONFLICT",但 consumeUndoToken 的实现始终只返回 UNDO_EXPIRED。这意味着 UNDO_CONFLICT 的语义是由上层调用方(如 src/actions/providers.ts)在完成状态校验后自行构造并返回的。当前代码对读者造成困惑:consumeUndoToken 的签名暗示它可能返回 UNDO_CONFLICT,但实际上不会。

建议两种方案之一:

♻️ 方案 A:为 consumeUndoToken 声明更窄的返回类型
+type ConsumeTokenResult =
+  | { ok: true; snapshot: UndoSnapshot }
+  | { ok: false; code: "UNDO_EXPIRED" };
+
-export async function consumeUndoToken(token: string): Promise<ConsumeUndoResult> {
+export async function consumeUndoToken(token: string): Promise<ConsumeTokenResult> {

上层调用方可将 ConsumeTokenResult 与自己的冲突检测结果合并为 ConsumeUndoResult

♻️ 方案 B:补充设计意图注释
+/**
+ * 消费撤销令牌,原子地获取并删除快照。
+ * 注意:此函数只返回 ok:true 或 code:UNDO_EXPIRED。
+ * UNDO_CONFLICT 由上层调用方在完成状态一致性检查后自行构造返回。
+ */
 export async function consumeUndoToken(token: string): Promise<ConsumeUndoResult> {

Also applies to: 61-80

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/providers/undo-store.ts` around lines 23 - 31, ConsumeUndoResult
declares a possible code "UNDO_CONFLICT" but the implementation of
consumeUndoToken never returns it, causing confusion; either narrow the
signature or document intent. Option A (recommended): remove "UNDO_CONFLICT"
from ConsumeUndoResult and export a narrower result used by consumeUndoToken
(e.g., only ok:true|{ok:false;code:"UNDO_EXPIRED"}) then let callers in
src/actions/providers.ts merge that with their conflict-detection to produce the
full ConsumeUndoResult. Option B: add a clear comment above ConsumeUndoResult
and on consumeUndoToken explaining that consumeUndoToken only returns
UNDO_EXPIRED and that UNDO_CONFLICT is created by upstream callers (e.g., in
src/actions/providers.ts) after additional validation.
src/app/[locale]/settings/providers/_components/batch-edit/provider-batch-dialog.tsx (1)

36-47: 建议将相对导入改为 @/ 别名路径。
当前新增的相对路径位于 src 内,按规范应统一使用别名,便于全局重构与一致性。
As per coding guidelines: "Use path alias @/ to reference files in ./src/ directory".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/app/`[locale]/settings/providers/_components/batch-edit/provider-batch-dialog.tsx
around lines 36 - 47, Several imports (FormTabNav, ProviderFormProvider,
useProviderForm, BasicInfoSection, LimitsSection, NetworkSection,
RoutingSection, TestingSection) use relative paths; change them to use the
project path alias (start with "@/") so they reference the same modules via the
src alias instead of "../...". Update the import statements that currently
import those symbols from "../forms/..." to import them from the corresponding
"@/..." alias equivalents, keeping the same exported names and ensuring module
resolution/build passes.
src/app/[locale]/login/loading.tsx (1)

4-4: 建议改为具名导出以符合导出规范。

根据编码规范 **/*.{ts,tsx} 应优先使用具名导出。但需要注意,项目中所有 loading.tsx 文件(共 9 个)都统一采用默认导出模式,包括:

  • dashboard/loading.tsx
  • dashboard/quotas/loading.tsx
  • my-usage/loading.tsx
  • 等其他文件

建议如果应用此更改,应同时更新所有 loading.tsx 文件以保持一致性。

建议修改
-export default function LoginLoading() {
+export function LoginLoading() {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/login/loading.tsx at line 4, The component LoginLoading is
currently a default export; change it to a named export by exporting the
LoginLoading function as a named export (export function LoginLoading...) and
remove the default export usage; update any import sites that rely on the
default import to use the named import { LoginLoading } instead. If you intend
to apply this convention project-wide, apply the same change to all other
loading.tsx files (e.g., dashboard/loading.tsx, dashboard/quotas/loading.tsx,
my-usage/loading.tsx) and update their consumers accordingly to maintain
consistency.
src/lib/security/login-abuse-policy.ts (2)

157-167: checkScopecount >= threshold 分支是可以保留的防御性代码,但值得注释说明

该分支仅在 lockedUntil 未设置但 count >= threshold 时触发。然而根据 recordFailureForScope 的实现,每次 count 达到阈值时 lockedUntil 都会被立即设置。因此,在当前实现下此分支在正常情况下不可达——它是防止将来代码变更破坏不变量的安全兜底。建议加注释说明,避免后续维护者误删。

+    // Defensive guard: in normal operation recordFailureForScope always sets
+    // lockedUntil when count reaches threshold, making this branch unreachable.
+    // It remains as a safety net against future invariant breaks.
     if (record.count >= threshold) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/security/login-abuse-policy.ts` around lines 157 - 167, The branch in
checkScope that handles record.count >= threshold is defensive/unreachable under
current logic because recordFailureForScope always sets lockedUntil when count
reaches the threshold; add a clear comment above that branch explaining it is an
intentional safety/defensive fallback to preserve behavior if invariants change,
reference the related functions/variables (checkScope, recordFailureForScope,
attempts, scopeKey, lockedUntil) and state that it ensures locked entries
survive LRU eviction and prevents accidental removal by future maintainers.

27-28: 内存级限流在分布式部署下无法同步状态

LoginAbusePolicy 将所有 AttemptRecord 存储在进程内存 Map 中,无法跨 Node.js 进程共享。在多 Pod/多实例部署场景下,攻击者可通过向不同实例轮发请求来绕过每实例限流阈值。MAX_TRACKED_ENTRIESSWEEP_INTERVAL_MS 的内存管理设计良好,但仅对单进程有效。

鉴于本 PR 已引入 Redis 基础设施,后续可考虑将限流计数迁移至 Redis(滑动窗口计数器),使限流在水平扩展场景下保持一致。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/security/login-abuse-policy.ts` around lines 27 - 28, The
LoginAbusePolicy currently stores AttemptRecord instances in an in-memory Map
(and relies on MAX_TRACKED_ENTRIES and SWEEP_INTERVAL_MS) which won’t work
across multiple Node.js processes; replace the per-process Map usage in
LoginAbusePolicy with Redis-backed counters/sets using the repo’s existing Redis
client, implement a distributed sliding-window rate limit (use INCR with EXPIRE
for fixed-window or a small Lua script / sorted-set approach for true
sliding-window) keyed by the same identifiers you currently use for
AttemptRecord, remove or make MAX_TRACKED_ENTRIES/SWEEP_INTERVAL_MS local-cache
fallbacks only, and ensure checks and increments are atomic so limits are
enforced consistently across pods.
src/app/api/auth/login/route.ts (2)

21-27: csrfGuardloginPolicy 均为模块级内存单例,在多进程/无服务器部署下行为受限

  • loginPolicy 的频率限制状态仅存储在当前 Node.js 进程内存中。在多实例水平扩展(如 Kubernetes 多 Pod)或无服务器环境(如 Vercel 冷启动)下,每个实例拥有独立的计数器,攻击者可通过轮换请求目标不同实例来绕过限流。
  • 鉴于本 PR 已引入 Redis 基础设施(RedisSessionStore),建议考虑将限流状态迁移到 Redis,或使用专用的分布式限流方案(如 ioredis + 滑动窗口 Lua 脚本)以保证多实例一致性。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/auth/login/route.ts` around lines 21 - 27, csrfGuard and
loginPolicy are module-level singletons holding in-process state; migrate
login/CSRF state to a shared Redis-backed store to support multi-process/edge
deployments: modify LoginAbusePolicy to accept a Redis-backed counter/TTL
interface (or replace with a distributed limiter library) and update usages
where createCsrfOriginGuard and csrfGuard rely on in-memory data to use the
RedisSessionStore (or a Redis-based rate-limiter) so counters/blocks are
consistent across instances; ensure constructor/factory for LoginAbusePolicy and
the CSRF guard accept an injected redis client or adapter and fallback to the
existing in-memory behavior for local dev/tests.

53-93: getAuthErrorTranslationsgetAuthSecurityTranslations 存在重复代码

两个函数的 try/catch/fallback 结构完全相同,仅 namespace 参数不同,违反 DRY 原则。

建议重构:提取公共帮助函数
+async function getTranslationsWithFallback(locale: Locale, namespace: string) {
+  try {
+    return await getTranslations({ locale, namespace });
+  } catch (error) {
+    logger.warn(`Login route: failed to load ${namespace} translations`, {
+      locale,
+      error: error instanceof Error ? error.message : String(error),
+    });
+    try {
+      return await getTranslations({ locale: defaultLocale, namespace });
+    } catch (fallbackError) {
+      logger.error(`Login route: failed to load default ${namespace} translations`, {
+        locale: defaultLocale,
+        error: fallbackError instanceof Error ? fallbackError.message : String(fallbackError),
+      });
+      return null;
+    }
+  }
+}
+
-async function getAuthErrorTranslations(locale: Locale) {
-  try {
-    return await getTranslations({ locale, namespace: "auth.errors" });
-  } catch (error) {
-    ...
-  }
-}
-
-async function getAuthSecurityTranslations(locale: Locale) {
-  try {
-    return await getTranslations({ locale, namespace: "auth.security" });
-  } catch (error) {
-    ...
-  }
-}
+const getAuthErrorTranslations = (locale: Locale) =>
+  getTranslationsWithFallback(locale, "auth.errors");
+const getAuthSecurityTranslations = (locale: Locale) =>
+  getTranslationsWithFallback(locale, "auth.security");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/auth/login/route.ts` around lines 53 - 93, Extract the shared
try/catch/fallback logic into a single helper (e.g.
getAuthTranslations(namespace: string, locale: Locale)) that calls
getTranslations({ locale, namespace }), falls back to getTranslations({ locale:
defaultLocale, namespace }) on error, logs using logger.warn/logger.error
(include locale, defaultLocale, namespace and the error message), and returns
the same nullable translation result; then have getAuthErrorTranslations and
getAuthSecurityTranslations simply call getAuthTranslations("auth.errors",
locale) and getAuthTranslations("auth.security", locale) respectively so no
duplicated try/catch blocks remain.
tests/security/login-abuse-policy.test.ts (1)

80-138: 缺少"IP 已被封锁时提供 key 参数"场景的测试用例

check(ip, key?) 在 IP scope 已封锁时会提前返回 ipDecision,不再检查 key scope(对应 login-abuse-policy.ts 第 80 行的 if (!ipDecision.allowed || !key) { return ipDecision; })。但当前测试中,"supports key-based throttling" 仅覆盖了 IP 正常 + key 封锁 的情形,未覆盖 IP 封锁 + 同时提供 key 的情形(应返回 reason: "ip_rate_limited",而非 "key_rate_limited")。

建议补充如下测试:

it("IP blocked takes priority over key check", () => {
  const policy = new LoginAbusePolicy({
    maxAttemptsPerIp: 1,
    maxAttemptsPerKey: 10,
    lockoutSeconds: 60,
  });
  const ip = "10.0.0.1";
  const key = "user@example.com";

  policy.recordFailure(ip, key);

  const decision = policy.check(ip, key);
  expect(decision.allowed).toBe(false);
  expect(decision.reason).toBe("ip_rate_limited");
});

同样建议补充 recordSuccess(ip, key) 同时重置 key scope 的测试,以验证第 112~113 行的 reset(ip, key) 调用正确清除了 key 的跟踪记录。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/security/login-abuse-policy.test.ts` around lines 80 - 138, Add two
tests to cover missing scenarios for LoginAbusePolicy: (1) verify that when an
IP is rate-limited, calling check(ip, key) still returns the IP decision (reason
"ip_rate_limited") rather than the key decision by creating a policy with
maxAttemptsPerIp:1, recording one failure with both ip and key, then asserting
check(ip, key) is blocked with reason "ip_rate_limited"; (2) verify
recordSuccess(ip, key) resets both IP and key tracking by recording failures for
both scopes, calling recordSuccess(ip, key) (which calls reset(ip, key)), and
asserting subsequent check(...) for that key and/or ip returns { allowed: true
}; use the existing methods recordFailure, recordSuccess, and check on the
LoginAbusePolicy instance to implement these tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@messages/ja/auth.json`:
- Around line 26-28: brand.tagline currently uses "統合" while form.description
uses "統一", causing inconsistent terminology on the same page; pick the correct
canonical term (either "統合" or "統一") based on zh-CN/EN source or product style,
then update the value of "brand.tagline" or "form.description" so both use the
same term; ensure you change the string only (preserve JSON structure) and
verify the keys "brand.tagline" and "form.description" match the chosen
terminology across the file.

In `@src/actions/providers.ts`:
- Around line 1859-1925: preimage currently captures all changedFields for every
matchedProviders before you apply type-specific filtering, so the undo can
restore fields that were never written; instead, build preimage only from the
actual fields that will be applied per provider. Concretely: move or recreate
the preimage population to occur after you decide which updates will be applied
per provider/type (use the same logic that creates providersByType and
filterRepositoryUpdatesByProviderType), and for each provider only record the
providerKey entries that exist in the filtered updates for that provider's
providerType (use PATCH_FIELD_TO_PROVIDER_KEY to map field names and
repositoryUpdates/filtered to decide which keys are applied). Ensure the object
saved to providerPatchUndoStore.set includes this filtered preimage so undo only
writes back fields that were actually updated.
- Around line 1661-1676: The current skipReason assignments in the compatibility
check (within the block using isClaudeOnly/isCodexOnly/isGeminiOnly and helpers
isClaudeProviderType/isCodexProviderType/isGeminiProviderType) set hard-coded
English messages; change these to structured reason codes instead (e.g. set
skipReason = "claude_only", "codex_only" or "gemini_only") so the preview
returns a code rather than a literal message; ensure the variable name used in
this function (skipReason) is assigned the code values and that any callers
(e.g. the preview payload consumed by provider-batch-preview-step.tsx) will
receive the code for UI-side i18n rendering.

In `@src/app/`[locale]/login/page.tsx:
- Line 34: Replace the hardcoded DEFAULT_SITE_TITLE string with an i18n key
lookup: remove the constant DEFAULT_SITE_TITLE and fetch the fallback title via
the app’s translation function inside the page component (e.g., useTranslation/t
or the project’s i18n helper) using a key like "site.defaultTitle"; add that key
to all locale resource files (zh-CN, zh-TW, en, ja, ru) so the API-empty
fallback displays localized text; ensure any place referencing
DEFAULT_SITE_TITLE now calls the translator (e.g., t('site.defaultTitle'))
during component initialization/rendering.
- Around line 208-224: The loading overlay currently uses role="dialog" and
aria-modal="true" (the div rendered when isLoading is true, containing Loader2
and the paragraph with aria-live) but has no focus management or focusable
controls, creating an accessibility trap; change the overlay to a non-modal
status region by removing role="dialog" and aria-modal="true" and instead use
role="status" (or rely on the existing aria-live on the paragraph) so screen
readers announce the login progress without requiring focus handling. Also
address the password visibility toggle (the button rendered with tabIndex={-1})
by removing the tabIndex restriction so keyboard users can reach the control
unless there is an explicit reason to keep it.

In `@src/app/api/auth/login/route.ts`:
- Around line 101-103: The function shouldIncludeFailureTaxonomy currently gates
inclusion of the errorCode field based on the presence of the
"x-forwarded-proto" header, making the API response shape dependent on
deployment topology; update the code so the API always emits an errorCode (or
remove the conditional entirely) instead of checking
request.headers.has("x-forwarded-proto"). Locate shouldIncludeFailureTaxonomy
and the response construction in route.ts (where error / errorCode are composed)
and change the logic to unconditionally include errorCode in the JSON error
response (or eliminate the helper and always set a stable error payload format)
so clients receive a consistent contract regardless of proxy headers.
- Around line 168-185: The rate-limit response is using the wrong translation
key; update the error message selection in the block that checks
loginPolicy.check(clientIp) (variables: decision, loginPolicy,
withAuthResponseHeaders, NextResponse, t) to use a semantically correct key such
as t?.("auth.errors.tooManyAttempts") or t?.("auth.errors.rateLimited") instead
of t?.("loginFailed"), and keep a sensible fallback like t?.("serverError") ??
"Too many attempts"; also ensure you add the corresponding translation entry
(auth.errors.tooManyAttempts or auth.errors.rateLimited) to the i18n resource
files.
- Around line 135-145: getLoginSessionStore 存在并发竞态:多个请求可能同时通过
sessionStoreInstance===null 并各自创建
RedisSessionStore,导致重复实例化;修复方式是在模块作用域引入一个用于创建实例的 Promise 缓存(例如
sessionStorePromise)并在 getLoginSessionStore 中先检查该 Promise,再在首次调用时赋值为
import("@/lib/auth-session-store/redis-session-store").then(m=>new
m.RedisSessionStore()), 其后 await 该 Promise 并将解析值存到
sessionStoreInstance(或直接返回解析值),确保只有一个 RedisSessionStore
被构造;引用符号:getLoginSessionStore、sessionStoreInstance、RedisSessionStore、"@/lib/auth-session-store/redis-session-store"。

In `@src/app/api/auth/logout/route.ts`:
- Around line 62-74: The logout currently calls resolveAuthCookieToken() and
passes that value to store.revoke(), but in "dual" mode the cookie contains the
API key (not the opaque session ID) so revoke fails; update the logic so that
when mode === "dual" you either (A) change the login flow to write the opaque
session ID into the auth cookie instead of the API key, or (B) keep login as-is
and, in the logout path, detect dual mode and perform a reverse lookup: take the
API key returned by resolveAuthCookieToken(), query the session store (or Redis)
to find the corresponding opaque session ID (e.g., via a new lookup method such
as getSessionIdByApiKey or store.findByApiKey), then call
store.revoke(opaqueSessionId); for non-dual modes continue using
resolveAuthCookieToken() result directly and call store.revoke(sessionId).

In `@src/repository/provider.ts`:
- Around line 968-1013: 在 BatchProviderUpdates 接口上加入一行注释,说明之所以没有包含
tpm、rpm、rpd、cc(Tokens Per Minute、Requests Per Minute、Requests Per
Day、并发连接数)是有意为之,因为这些字段是针对单个供应商的专属速率限额,不适合批量覆盖;引用相关符号以便定位:在声明
BatchProviderUpdates 的位置上添加注释,提及 updateProvider 和 updateProvidersBatch
接受/处理这些速率字段但 BatchProviderUpdates 刻意省略它们以避免批量操作造成不当覆盖。

---

Outside diff comments:
In `@src/actions/providers.ts`:
- Around line 3861-3863: The inline comment containing an emoji in the object
literal near the Response API fix must be cleaned up: in
src/actions/providers.ts locate the object with properties type: "message" and
role: "user" (the comment that currently reads "⭐ 修复 `#189`: Response API 要求 input
数组中的每个元素必须包含 type 字段") and remove the emoji character(s) so the comment reads
plain text (e.g., "修复 `#189`: Response API 要求 input 数组中的每个元素必须包含 type 字段") or
delete the comment entirely to comply with the no-emoji rule.

---

Duplicate comments:
In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx:
- Around line 394-396: The conditional rendering for the 1M Context Window
missing "claude-auth" was fixed; ensure the SmartInputWrapper rendering
condition for the 1M Context Window (the expression that uses providerType and
isBatch) includes providerType === "claude-auth" alongside "claude" and isBatch,
matching the Anthropic Overrides check elsewhere, so update the conditional that
controls SmartInputWrapper to: (providerType === "claude" || providerType ===
"claude-auth" || isBatch).

In `@src/app/`[locale]/settings/providers/_components/thinking-budget-editor.tsx:
- Around line 39-41: handleInputChange currently should forward the raw input
string instead of converting empty strings to "inherit"; update the handler
(handleInputChange) to call onChange(e.target.value) and remove any legacy logic
that maps empty values to "inherit" (and remove related conditional branches or
constants tied to that behavior) so clearing the input no longer hides the
control; ensure any callers of onChange and unit/interaction tests expect the
raw empty string rather than "inherit".

In `@src/app/api/auth/logout/route.ts`:
- Around line 1-16: This route uses NextRequest/NextResponse but must be
converted to Hono-style handlers; replace the Next.js handler with a Hono
app/route handler (use Hono Context for request/response) and update how you
read/write cookies and headers: call createCsrfOriginGuard (csrfGuard) against
the Hono request, obtain the auth token via getAuthCookie from the Hono
request/cookie API, determine session mode with getSessionTokenMode, clear the
cookie using clearAuthCookie applied to the Hono response, and apply
withAuthResponseHeaders to the final Hono Response; export the Hono route (e.g.,
app.post or app.route) instead of exporting a Next.js handler so the file
matches the other routes using Hono.

---

Nitpick comments:
In `@src/app/`[locale]/login/loading.tsx:
- Line 4: The component LoginLoading is currently a default export; change it to
a named export by exporting the LoginLoading function as a named export (export
function LoginLoading...) and remove the default export usage; update any import
sites that rely on the default import to use the named import { LoginLoading }
instead. If you intend to apply this convention project-wide, apply the same
change to all other loading.tsx files (e.g., dashboard/loading.tsx,
dashboard/quotas/loading.tsx, my-usage/loading.tsx) and update their consumers
accordingly to maintain consistency.

In
`@src/app/`[locale]/settings/providers/_components/batch-edit/provider-batch-dialog.tsx:
- Around line 36-47: Several imports (FormTabNav, ProviderFormProvider,
useProviderForm, BasicInfoSection, LimitsSection, NetworkSection,
RoutingSection, TestingSection) use relative paths; change them to use the
project path alias (start with "@/") so they reference the same modules via the
src alias instead of "../...". Update the import statements that currently
import those symbols from "../forms/..." to import them from the corresponding
"@/..." alias equivalents, keeping the same exported names and ensuring module
resolution/build passes.

In `@src/app/`[locale]/settings/providers/_components/thinking-budget-editor.tsx:
- Line 35: The hard-coded magic strings for the thinking budget ("10240" default
and "32000" max, and the min "1024" used in attributes) should be extracted into
named constants to centralize semantics and avoid duplication; update the
onChange("10240") call, the input's min/max attributes, and any other
occurrences (lines ~43-45) to use constants like DEFAULT_CUSTOM_THINKING_BUDGET,
MIN_THINKING_BUDGET, and MAX_THINKING_BUDGET (choose string or numeric types
consistently with the input handling and parse/format where needed) so future
Anthropic limit changes require a single update and the code is clearer.

In `@src/app/api/auth/login/route.ts`:
- Around line 21-27: csrfGuard and loginPolicy are module-level singletons
holding in-process state; migrate login/CSRF state to a shared Redis-backed
store to support multi-process/edge deployments: modify LoginAbusePolicy to
accept a Redis-backed counter/TTL interface (or replace with a distributed
limiter library) and update usages where createCsrfOriginGuard and csrfGuard
rely on in-memory data to use the RedisSessionStore (or a Redis-based
rate-limiter) so counters/blocks are consistent across instances; ensure
constructor/factory for LoginAbusePolicy and the CSRF guard accept an injected
redis client or adapter and fallback to the existing in-memory behavior for
local dev/tests.
- Around line 53-93: Extract the shared try/catch/fallback logic into a single
helper (e.g. getAuthTranslations(namespace: string, locale: Locale)) that calls
getTranslations({ locale, namespace }), falls back to getTranslations({ locale:
defaultLocale, namespace }) on error, logs using logger.warn/logger.error
(include locale, defaultLocale, namespace and the error message), and returns
the same nullable translation result; then have getAuthErrorTranslations and
getAuthSecurityTranslations simply call getAuthTranslations("auth.errors",
locale) and getAuthTranslations("auth.security", locale) respectively so no
duplicated try/catch blocks remain.

In `@src/app/api/auth/logout/route.ts`:
- Around line 18-28: getLogoutSessionStore currently has a race: multiple
concurrent callers can pass the null check and each call will dynamically import
and construct a RedisSessionStore, leaking instances; also the inline dynamic
import type annotation is hard to read. Fix by making sessionStoreInstance hold
a Promise that is set on first call (a "promise singleton") so concurrent
callers await the same initialization Promise and only one RedisSessionStore is
constructed, e.g. replace the nullable instance with a
Promise<RedisSessionStore> | null and ensure getLogoutSessionStore sets that
Promise before awaiting the import/constructor; also move the type import to the
top with an `import type { RedisSessionStore } from
"@/lib/auth-session-store/redis-session-store"` and use that type for the
Promise and return signature to improve readability.

In `@src/lib/providers/undo-store.ts`:
- Around line 23-31: ConsumeUndoResult declares a possible code "UNDO_CONFLICT"
but the implementation of consumeUndoToken never returns it, causing confusion;
either narrow the signature or document intent. Option A (recommended): remove
"UNDO_CONFLICT" from ConsumeUndoResult and export a narrower result used by
consumeUndoToken (e.g., only ok:true|{ok:false;code:"UNDO_EXPIRED"}) then let
callers in src/actions/providers.ts merge that with their conflict-detection to
produce the full ConsumeUndoResult. Option B: add a clear comment above
ConsumeUndoResult and on consumeUndoToken explaining that consumeUndoToken only
returns UNDO_EXPIRED and that UNDO_CONFLICT is created by upstream callers
(e.g., in src/actions/providers.ts) after additional validation.

In `@src/lib/security/login-abuse-policy.ts`:
- Around line 157-167: The branch in checkScope that handles record.count >=
threshold is defensive/unreachable under current logic because
recordFailureForScope always sets lockedUntil when count reaches the threshold;
add a clear comment above that branch explaining it is an intentional
safety/defensive fallback to preserve behavior if invariants change, reference
the related functions/variables (checkScope, recordFailureForScope, attempts,
scopeKey, lockedUntil) and state that it ensures locked entries survive LRU
eviction and prevents accidental removal by future maintainers.
- Around line 27-28: The LoginAbusePolicy currently stores AttemptRecord
instances in an in-memory Map (and relies on MAX_TRACKED_ENTRIES and
SWEEP_INTERVAL_MS) which won’t work across multiple Node.js processes; replace
the per-process Map usage in LoginAbusePolicy with Redis-backed counters/sets
using the repo’s existing Redis client, implement a distributed sliding-window
rate limit (use INCR with EXPIRE for fixed-window or a small Lua script /
sorted-set approach for true sliding-window) keyed by the same identifiers you
currently use for AttemptRecord, remove or make
MAX_TRACKED_ENTRIES/SWEEP_INTERVAL_MS local-cache fallbacks only, and ensure
checks and increments are atomic so limits are enforced consistently across
pods.

In `@src/repository/provider.ts`:
- Around line 1355-1381: restoreProvidersBatch performs sequential multi-step DB
work for each id inside one long transaction (calling
restoreProviderInTransaction repeatedly), which can cause long-held locks for
large batches; update the function to (1) add a clear doc comment noting the 60s
TTL constraint (PROVIDER_RESTORE_MAX_AGE_MS) and expected small batch sizes, and
(2) emit a warning log when uniqueIds.length exceeds a safe threshold (e.g., >
10) before starting the transaction so operators are alerted to potential long
transactions; reference restoreProvidersBatch, uniqueIds, restoredCount, and
PROVIDER_RESTORE_MAX_AGE_MS in the message.

In `@tests/security/login-abuse-policy.test.ts`:
- Around line 80-138: Add two tests to cover missing scenarios for
LoginAbusePolicy: (1) verify that when an IP is rate-limited, calling check(ip,
key) still returns the IP decision (reason "ip_rate_limited") rather than the
key decision by creating a policy with maxAttemptsPerIp:1, recording one failure
with both ip and key, then asserting check(ip, key) is blocked with reason
"ip_rate_limited"; (2) verify recordSuccess(ip, key) resets both IP and key
tracking by recording failures for both scopes, calling recordSuccess(ip, key)
(which calls reset(ip, key)), and asserting subsequent check(...) for that key
and/or ip returns { allowed: true }; use the existing methods recordFailure,
recordSuccess, and check on the LoginAbusePolicy instance to implement these
tests.

Comment on lines +26 to +28
"brand": {
"tagline": "統合API管理コンソール"
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

同一页面内术语不一致:brand.taglineform.description 用词不统一。

brand.tagline(第 27 行)使用「統合」(tōgō,统合/集成),而同文件 form.description(第 4 行)使用「統一」(tōitsu,统一/单一化)来指代同一个控制台。两词在日语中含义有所不同,用户在同一登录页面会看到两种不同说法,可能产生困惑。对比 zh-CN/auth.json,两处均使用「统一」,建议日语保持一致。

🔧 建议修复(以统一使用「統合」为例,或参照英文原文确认规范术语后再定)
-    "description": "API キーを使用して統一管理コンソールにアクセスします",
+    "description": "API キーを使用して統合管理コンソールにアクセスします",

或统一改为「統一」:

-    "tagline": "統合API管理コンソール"
+    "tagline": "統一API管理コンソール"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@messages/ja/auth.json` around lines 26 - 28, brand.tagline currently uses
"統合" while form.description uses "統一", causing inconsistent terminology on the
same page; pick the correct canonical term (either "統合" or "統一") based on
zh-CN/EN source or product style, then update the value of "brand.tagline" or
"form.description" so both use the same term; ensure you change the string only
(preserve JSON structure) and verify the keys "brand.tagline" and
"form.description" match the chosen terminology across the file.

Comment on lines +1661 to +1676
const isClaudeOnly = CLAUDE_ONLY_FIELDS.has(field);
const isCodexOnly = CODEX_ONLY_FIELDS.has(field);
const isGeminiOnly = GEMINI_ONLY_FIELDS.has(field);

let isCompatible = true;
let skipReason = "";
if (isClaudeOnly && !isClaudeProviderType(provider.providerType)) {
isCompatible = false;
skipReason = `Field "${field}" is only applicable to claude/claude-auth providers`;
} else if (isCodexOnly && !isCodexProviderType(provider.providerType)) {
isCompatible = false;
skipReason = `Field "${field}" is only applicable to codex providers`;
} else if (isGeminiOnly && !isGeminiProviderType(provider.providerType)) {
isCompatible = false;
skipReason = `Field "${field}" is only applicable to gemini/gemini-cli providers`;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n "skipReason|fieldSkipped" -g "**/*.{ts,tsx,js,jsx}" -C2

Repository: ding113/claude-code-hub

Length of output: 4889


skipReason 包含硬编码的英文提示,违反多语言要求

在 src/actions/providers.ts 行 1669、1672、1675 的三处 skipReason 赋值均使用了硬编码英文字符串。这些字符串经由 preview 结果返回到前端,在 provider-batch-preview-step.tsx 中被传入 i18n 函数 t("preview.fieldSkipped", { reason: row.skipReason }),直接暴露给用户而未进行翻译。

建议改为返回 reasonCode(如 "claude_only", "codex_only", "gemini_only"),由 UI 侧根据 reasonCode 使用 i18n 文案渲染。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/providers.ts` around lines 1661 - 1676, The current skipReason
assignments in the compatibility check (within the block using
isClaudeOnly/isCodexOnly/isGeminiOnly and helpers
isClaudeProviderType/isCodexProviderType/isGeminiProviderType) set hard-coded
English messages; change these to structured reason codes instead (e.g. set
skipReason = "claude_only", "codex_only" or "gemini_only") so the preview
returns a code rather than a literal message; ensure the variable name used in
this function (skipReason) is assigned the code values and that any callers
(e.g. the preview payload consumed by provider-batch-preview-step.tsx) will
receive the code for UI-side i18n rendering.

Comment on lines +1859 to +1925
const allProviders = await findAllProvidersFresh();
const effectiveIdSet = new Set(effectiveProviderIds);
const matchedProviders = allProviders.filter((p) => effectiveIdSet.has(p.id));
const changedFields = getChangedPatchFields(normalizedPatch.data);
const preimage: Record<number, Record<string, unknown>> = {};
for (const provider of matchedProviders) {
const fieldValues: Record<string, unknown> = {};
for (const field of changedFields) {
const providerKey = PATCH_FIELD_TO_PROVIDER_KEY[field];
fieldValues[providerKey] = provider[providerKey];
}
preimage[provider.id] = fieldValues;
}

const repositoryUpdates = mapApplyUpdatesToRepositoryFormat(updatesResult.data);

const hasTypeSpecificFields = changedFields.some(
(f) => CLAUDE_ONLY_FIELDS.has(f) || CODEX_ONLY_FIELDS.has(f) || GEMINI_ONLY_FIELDS.has(f)
);

let dbUpdatedCount: number;
if (!hasTypeSpecificFields) {
dbUpdatedCount = await updateProvidersBatch(effectiveProviderIds, repositoryUpdates);
} else {
const providersByType = new Map<string, number[]>();
for (const provider of matchedProviders) {
const type = provider.providerType;
if (!providersByType.has(type)) providersByType.set(type, []);
providersByType.get(type)!.push(provider.id);
}

dbUpdatedCount = 0;
for (const [type, ids] of providersByType) {
const filtered = filterRepositoryUpdatesByProviderType(repositoryUpdates, type);
if (Object.keys(filtered).length > 0) {
dbUpdatedCount += await updateProvidersBatch(ids, filtered);
}
}
}

await publishProviderCacheInvalidation();

const appliedAt = new Date(nowMs).toISOString();
const undoToken = createProviderPatchUndoToken();
const undoExpiresAtMs = nowMs + PROVIDER_PATCH_UNDO_TTL_SECONDS * 1000;

const applyResult: ApplyProviderBatchPatchResult = {
operationId: createProviderPatchOperationId(),
appliedAt,
updatedCount: dbUpdatedCount,
undoToken,
undoExpiresAt: new Date(undoExpiresAtMs).toISOString(),
};

snapshot.applied = true;
if (parsed.data.idempotencyKey) {
snapshot.appliedResultByIdempotencyKey[parsed.data.idempotencyKey] = applyResult;
}
await providerBatchPatchPreviewStore.set(parsed.data.previewToken, snapshot);

await providerPatchUndoStore.set(undoToken, {
undoToken,
operationId: applyResult.operationId,
providerIds: effectiveProviderIds,
preimage,
patch: normalizedPatch.data,
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

预览/应用按类型过滤后,preimage 仍包含被过滤字段,撤销可能覆盖无关修改。
preimage 是在按类型过滤前按 changedFields 全量采集的;若某些字段因 providerType 被过滤未写入 DB,撤销时仍会把这些字段回写为旧值,可能覆盖后续独立修改。建议 preimage 仅记录“实际会写入”的字段集合。

修复思路示例
-      for (const field of changedFields) {
+      const applicableFields = getApplicableFieldsForProvider(provider, changedFields);
+      for (const field of applicableFields) {
         const providerKey = PATCH_FIELD_TO_PROVIDER_KEY[field];
         fieldValues[providerKey] = provider[providerKey];
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/providers.ts` around lines 1859 - 1925, preimage currently
captures all changedFields for every matchedProviders before you apply
type-specific filtering, so the undo can restore fields that were never written;
instead, build preimage only from the actual fields that will be applied per
provider. Concretely: move or recreate the preimage population to occur after
you decide which updates will be applied per provider/type (use the same logic
that creates providersByType and filterRepositoryUpdatesByProviderType), and for
each provider only record the providerKey entries that exist in the filtered
updates for that provider's providerType (use PATCH_FIELD_TO_PROVIDER_KEY to map
field names and repositoryUpdates/filtered to decide which keys are applied).
Ensure the object saved to providerPatchUndoStore.set includes this filtered
preimage so undo only writes back fields that were actually updated.

hasUpdate: boolean;
}

const DEFAULT_SITE_TITLE = "Claude Code Hub";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

DEFAULT_SITE_TITLE 硬编码字符串违反 i18n 规范。

"Claude Code Hub" 作为站点标题兜底值会在 API 返回空值时直接展示给用户,属于用户可见文本,应通过 i18n 键管理。

🛡️ 建议修复
-const DEFAULT_SITE_TITLE = "Claude Code Hub";
+// 在 messages/{locale}/customs.json(或 auth.json)中添加 brand.defaultTitle 键

然后在组件初始化时:

-const [siteTitle, setSiteTitle] = useState(DEFAULT_SITE_TITLE);
+const [siteTitle, setSiteTitle] = useState(() => tCustoms("brand.defaultTitle"));

As per coding guidelines: "All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/login/page.tsx at line 34, Replace the hardcoded
DEFAULT_SITE_TITLE string with an i18n key lookup: remove the constant
DEFAULT_SITE_TITLE and fetch the fallback title via the app’s translation
function inside the page component (e.g., useTranslation/t or the project’s i18n
helper) using a key like "site.defaultTitle"; add that key to all locale
resource files (zh-CN, zh-TW, en, ja, ru) so the API-empty fallback displays
localized text; ensure any place referencing DEFAULT_SITE_TITLE now calls the
translator (e.g., t('site.defaultTitle')) during component
initialization/rendering.

Comment on lines +208 to +224
{isLoading && (
<div
data-testid="loading-overlay"
role="dialog"
aria-modal="true"
aria-label={t("login.loggingIn")}
className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-background/80 backdrop-blur-sm transition-all duration-200"
>
<Loader2 className="h-12 w-12 animate-spin motion-reduce:animate-none text-primary" />
<p
className="mt-4 text-lg font-medium text-muted-foreground"
role="status"
aria-live="polite"
>
{t("login.loggingIn")}
</p>
</div>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

role="dialog" 配合 aria-modal="true" 但缺少焦点管理——无障碍重大缺陷。

WAI-ARIA 规范要求:当 role="dialog" 出现时,焦点必须被移入对话框内的可聚焦元素;aria-modal="true" 会通知辅助技术将对话框外的内容视为不可交互。当前加载遮罩中没有任何可聚焦元素,也没有焦点转移代码。结果是:屏幕阅读器用户的焦点停留在已禁用的提交按钮上,部分屏幕阅读器会因 aria-modal 而将页面其余部分标记为不可访问,导致用户陷入无法导航的状态。

加载遮罩不是一个真正的对话框——应改用 role="status" 或依赖已有的 aria-live="polite" 段落来播报状态。

🛡️ 建议修复
-      <div
-        data-testid="loading-overlay"
-        role="dialog"
-        aria-modal="true"
-        aria-label={t("login.loggingIn")}
-        className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-background/80 backdrop-blur-sm transition-all duration-200"
-      >
+      <div
+        data-testid="loading-overlay"
+        aria-live="assertive"
+        aria-busy="true"
+        className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-background/80 backdrop-blur-sm transition-all duration-200"
+      >

附:第 358 行密码可见性切换按钮的 tabIndex={-1} 使仅使用键盘的视力正常用户无法通过 Tab 访问该控件,可考虑移除 tabIndex={-1} 以提升键盘可访问性(或至少确认此为有意设计决策)。

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{isLoading && (
<div
data-testid="loading-overlay"
role="dialog"
aria-modal="true"
aria-label={t("login.loggingIn")}
className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-background/80 backdrop-blur-sm transition-all duration-200"
>
<Loader2 className="h-12 w-12 animate-spin motion-reduce:animate-none text-primary" />
<p
className="mt-4 text-lg font-medium text-muted-foreground"
role="status"
aria-live="polite"
>
{t("login.loggingIn")}
</p>
</div>
{isLoading && (
<div
data-testid="loading-overlay"
aria-live="assertive"
aria-busy="true"
className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-background/80 backdrop-blur-sm transition-all duration-200"
>
<Loader2 className="h-12 w-12 animate-spin motion-reduce:animate-none text-primary" />
<p
className="mt-4 text-lg font-medium text-muted-foreground"
role="status"
aria-live="polite"
>
{t("login.loggingIn")}
</p>
</div>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/login/page.tsx around lines 208 - 224, The loading overlay
currently uses role="dialog" and aria-modal="true" (the div rendered when
isLoading is true, containing Loader2 and the paragraph with aria-live) but has
no focus management or focusable controls, creating an accessibility trap;
change the overlay to a non-modal status region by removing role="dialog" and
aria-modal="true" and instead use role="status" (or rely on the existing
aria-live on the paragraph) so screen readers announce the login progress
without requiring focus handling. Also address the password visibility toggle
(the button rendered with tabIndex={-1}) by removing the tabIndex restriction so
keyboard users can reach the control unless there is an explicit reason to keep
it.

Comment on lines +101 to +103
function shouldIncludeFailureTaxonomy(request: NextRequest): boolean {
return request.headers.has("x-forwarded-proto");
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

shouldIncludeFailureTaxonomy 导致 API 响应结构依赖部署拓扑

function shouldIncludeFailureTaxonomy(request: NextRequest): boolean {
  return request.headers.has("x-forwarded-proto");
}

该函数仅依据代理头是否存在来决定是否在响应中包含 errorCode 字段,导致:

  • 有反向代理时(生产环境):响应包含 { error, errorCode }
  • 无反向代理时(本地开发或直连场景):响应仅包含 { error }

这对客户端 SDK 或集成方来说是不稳定的契约——errorCode 字段的存在与否取决于部署配置而非 API 设计本身。建议始终包含 errorCode 字段,或彻底移除该条件分支。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/auth/login/route.ts` around lines 101 - 103, The function
shouldIncludeFailureTaxonomy currently gates inclusion of the errorCode field
based on the presence of the "x-forwarded-proto" header, making the API response
shape dependent on deployment topology; update the code so the API always emits
an errorCode (or remove the conditional entirely) instead of checking
request.headers.has("x-forwarded-proto"). Locate shouldIncludeFailureTaxonomy
and the response construction in route.ts (where error / errorCode are composed)
and change the logic to unconditionally include errorCode in the JSON error
response (or eliminate the helper and always set a stable error payload format)
so clients receive a consistent contract regardless of proxy headers.

Comment on lines +135 to +145
let sessionStoreInstance:
| import("@/lib/auth-session-store/redis-session-store").RedisSessionStore
| null = null;

async function getLoginSessionStore() {
if (!sessionStoreInstance) {
const { RedisSessionStore } = await import("@/lib/auth-session-store/redis-session-store");
sessionStoreInstance = new RedisSessionStore();
}
return sessionStoreInstance;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*redis-session-store*" | head -20

Repository: ding113/claude-code-hub

Length of output: 118


🏁 Script executed:

fd -type f -name "*session*store*" | grep -E "\.(ts|tsx|js|jsx)$"

Repository: ding113/claude-code-hub

Length of output: 237


🏁 Script executed:

cat -n src/app/api/auth/login/route.ts | sed -n '135,150p'

Repository: ding113/claude-code-hub

Length of output: 747


🏁 Script executed:

cat -n ./src/lib/auth-session-store/redis-session-store.ts

Repository: ding113/claude-code-hub

Length of output: 8053


🏁 Script executed:

wc -l ./src/lib/auth-session-store/redis-session-store.ts

Repository: ding113/claude-code-hub

Length of output: 122


🏁 Script executed:

find . -type f -name "redis.ts" -o -name "redis.js" | grep -E "lib.*redis"

Repository: ding113/claude-code-hub

Length of output: 49


🏁 Script executed:

cat -n src/lib/redis.ts | head -80

Repository: ding113/claude-code-hub

Length of output: 115


🏁 Script executed:

find . -type f -name "*.ts" -o -name "*.tsx" | xargs grep -l "getRedisClient" | head -10

Repository: ding113/claude-code-hub

Length of output: 450


🏁 Script executed:

fd "redis" -e ts -e tsx | grep -v node_modules

Repository: ding113/claude-code-hub

Length of output: 249


🏁 Script executed:

cat -n src/lib/redis/redis-kv-store.ts | head -100

Repository: ding113/claude-code-hub

Length of output: 3482


🏁 Script executed:

find . -type f -name "*.ts" -o -name "*.tsx" | xargs grep -l "export.*getRedisClient" | head -5

Repository: ding113/claude-code-hub

Length of output: 117


🏁 Script executed:

cat -n src/lib/redis/client.ts

Repository: ding113/claude-code-hub

Length of output: 6221


🏁 Script executed:

cat -n src/lib/redis/index.ts

Repository: ding113/claude-code-hub

Length of output: 385


getLoginSessionStore() 存在竞态条件,会导致多个 RedisSessionStore 实例被创建

当两个并发请求同时检查 sessionStoreInstance === null 时,会各自通过条件判断进入 if 分支,分别执行 await import() 并创建独立的 RedisSessionStore 实例。第二个赋值会覆盖第一个,导致第一个实例被遗弃。虽然 RedisSessionStore 本身不直接管理 Redis 连接(连接由全局的 getRedisClient() 单例管理),但创建多个实例仍然是不必要的浪费,且违反单例模式的设计意图。

建议用 Promise 缓存代替实例缓存:

建议修复
-let sessionStoreInstance:
-  | import("@/lib/auth-session-store/redis-session-store").RedisSessionStore
-  | null = null;
+let sessionStorePromise:
+  | Promise<import("@/lib/auth-session-store/redis-session-store").RedisSessionStore>
+  | null = null;

 async function getLoginSessionStore() {
-  if (!sessionStoreInstance) {
-    const { RedisSessionStore } = await import("@/lib/auth-session-store/redis-session-store");
-    sessionStoreInstance = new RedisSessionStore();
+  if (!sessionStorePromise) {
+    sessionStorePromise = import("@/lib/auth-session-store/redis-session-store").then(
+      ({ RedisSessionStore }) => new RedisSessionStore(),
+    );
   }
-  return sessionStoreInstance;
+  return sessionStorePromise;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/auth/login/route.ts` around lines 135 - 145, getLoginSessionStore
存在并发竞态:多个请求可能同时通过 sessionStoreInstance===null 并各自创建
RedisSessionStore,导致重复实例化;修复方式是在模块作用域引入一个用于创建实例的 Promise 缓存(例如
sessionStorePromise)并在 getLoginSessionStore 中先检查该 Promise,再在首次调用时赋值为
import("@/lib/auth-session-store/redis-session-store").then(m=>new
m.RedisSessionStore()), 其后 await 该 Promise 并将解析值存到
sessionStoreInstance(或直接返回解析值),确保只有一个 RedisSessionStore
被构造;引用符号:getLoginSessionStore、sessionStoreInstance、RedisSessionStore、"@/lib/auth-session-store/redis-session-store"。

Comment on lines +168 to +185
const decision = loginPolicy.check(clientIp);
if (!decision.allowed) {
const response = withAuthResponseHeaders(
NextResponse.json(
{
error: t?.("loginFailed") ?? t?.("serverError") ?? "Too many attempts",
errorCode: "RATE_LIMITED",
},
{ status: 429 }
)
);

if (decision.retryAfterSeconds != null) {
response.headers.set("Retry-After", String(decision.retryAfterSeconds));
}

return response;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

限流响应(429)使用了错误的翻译键 loginFailed

t?.("loginFailed") ?? t?.("serverError") ?? "Too many attempts" 中,loginFailed 键对应的文案通常是"登录失败",用于认证失败场景,不适合描述"请求频率过高"的限流情况。"Too many attempts" 仅在两个翻译键都缺失时才作为兜底出现。

建议在 auth.errors 命名空间中添加专用的 tooManyAttemptsrateLimited 翻译键:

建议修复:使用语义匹配的翻译键
-error: t?.("loginFailed") ?? t?.("serverError") ?? "Too many attempts",
+error: t?.("tooManyAttempts") ?? t?.("loginFailed") ?? "Too many attempts",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/auth/login/route.ts` around lines 168 - 185, The rate-limit
response is using the wrong translation key; update the error message selection
in the block that checks loginPolicy.check(clientIp) (variables: decision,
loginPolicy, withAuthResponseHeaders, NextResponse, t) to use a semantically
correct key such as t?.("auth.errors.tooManyAttempts") or
t?.("auth.errors.rateLimited") instead of t?.("loginFailed"), and keep a
sensible fallback like t?.("serverError") ?? "Too many attempts"; also ensure
you add the corresponding translation entry (auth.errors.tooManyAttempts or
auth.errors.rateLimited) to the i18n resource files.

Comment on lines +62 to +74
if (mode !== "legacy") {
try {
const sessionId = await resolveAuthCookieToken();
if (sessionId) {
const store = await getLogoutSessionStore();
await store.revoke(sessionId);
}
} catch (error) {
logger.warn("[AuthLogout] Failed to revoke opaque session during logout", {
error: error instanceof Error ? error.message : String(error),
});
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Search for dual mode, SessionTokenMode, and AUTH_COOKIE usage
rg -n "dual|SessionTokenMode|setAuthCookie|AUTH_COOKIE" --type ts -C 4 src/lib/auth.ts

Repository: ding113/claude-code-hub

Length of output: 3254


🏁 Script executed:

# Also check the logout route itself to understand the context better
cat -n src/app/api/auth/logout/route.ts | head -80

Repository: ding113/claude-code-hub

Length of output: 2908


🏁 Script executed:

# Look for where mode is determined and how it affects session handling
rg -n "mode.*dual|dual.*mode" --type ts -C 3 src/

Repository: ding113/claude-code-hub

Length of output: 1845


🏁 Script executed:

# Check how createOpaqueSession works and how the opaque session ID is handled
rg -n "createOpaqueSession" --type ts -C 5 src/

Repository: ding113/claude-code-hub

Length of output: 2199


🏁 Script executed:

# Look at the RedisSessionStore.revoke() implementation to understand its behavior
cat -n src/lib/auth-session-store/redis-session-store.ts | head -100

Repository: ding113/claude-code-hub

Length of output: 3815


🏁 Script executed:

# Check the login route more carefully to see how opaque sessions are created
rg -n "createOpaqueSession|setAuthCookie" --type ts -B 2 -A 8 src/app/api/auth/login/route.ts

Repository: ding113/claude-code-hub

Length of output: 1910


🏁 Script executed:

# Check what createOpaqueSession returns - especially the sessionId structure
rg -n "class RedisSessionStore|async create" --type ts -A 15 src/lib/auth-session-store/redis-session-store.ts | head -60

Repository: ding113/claude-code-hub

Length of output: 1306


🏁 Script executed:

# Check what store.create() returns to understand the sessionId format
rg -n "return.*sessionId|interface SessionData" --type ts -B 2 -A 5 src/lib/auth-session-store/

Repository: ding113/claude-code-hub

Length of output: 1076


🏁 Script executed:

# Check the revoke() method implementation in RedisSessionStore
rg -n "async revoke|revoke\(" --type ts -A 10 src/lib/auth-session-store/redis-session-store.ts

Repository: ding113/claude-code-hub

Length of output: 814


🏁 Script executed:

# Also check if there's a get() method to see how session lookup works
rg -n "async get\|async revoke" --type ts -A 8 src/lib/auth-session-store/redis-session-store.ts

Repository: ding113/claude-code-hub

Length of output: 49


修复 "dual" 模式下的会话撤销逻辑

当前代码在 "dual" 模式下存在会话撤销缺陷:登录时 cookie 设置为 API 密钥(行 247),而不透明会话的 ID 仅存储在 Redis 中。logout 时调用 store.revoke(sessionId) 传入的是 API 密钥而非不透明 session ID,导致 Redis 查找失败,会话无法被撤销,只能等待自然过期。

在 "opaque" 模式下,cookie 存储的是实际的 opaque sessionId(行 258),revoke 调用能正确匹配 Redis key。建议:

  • 修改 dual 模式在登录时将不透明 sessionId 存储到 cookie,或
  • 在 logout 时根据 mode 采用不同的撤销策略(dual 模式下需要从 API 密钥反向查找对应的不透明 session ID)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/auth/logout/route.ts` around lines 62 - 74, The logout currently
calls resolveAuthCookieToken() and passes that value to store.revoke(), but in
"dual" mode the cookie contains the API key (not the opaque session ID) so
revoke fails; update the logic so that when mode === "dual" you either (A)
change the login flow to write the opaque session ID into the auth cookie
instead of the API key, or (B) keep login as-is and, in the logout path, detect
dual mode and perform a reverse lookup: take the API key returned by
resolveAuthCookieToken(), query the session store (or Redis) to find the
corresponding opaque session ID (e.g., via a new lookup method such as
getSessionIdByApiKey or store.findByApiKey), then call
store.revoke(opaqueSessionId); for non-dual modes continue using
resolveAuthCookieToken() result directly and call store.revoke(sessionId).

Comment on lines 968 to 1013
export interface BatchProviderUpdates {
isEnabled?: boolean;
priority?: number;
weight?: number;
costMultiplier?: string;
groupTag?: string | null;
modelRedirects?: Record<string, string> | null;
allowedModels?: string[] | null;
anthropicThinkingBudgetPreference?: string | null;
anthropicAdaptiveThinking?: AnthropicAdaptiveThinkingConfig | null;
// Routing
preserveClientIp?: boolean;
groupPriorities?: Record<string, number> | null;
cacheTtlPreference?: string | null;
swapCacheTtlBilling?: boolean;
context1mPreference?: string | null;
codexReasoningEffortPreference?: string | null;
codexReasoningSummaryPreference?: string | null;
codexTextVerbosityPreference?: string | null;
codexParallelToolCallsPreference?: string | null;
anthropicMaxTokensPreference?: string | null;
geminiGoogleSearchPreference?: string | null;
// Rate Limit
limit5hUsd?: string | null;
limitDailyUsd?: string | null;
dailyResetMode?: string;
dailyResetTime?: string;
limitWeeklyUsd?: string | null;
limitMonthlyUsd?: string | null;
limitTotalUsd?: string | null;
limitConcurrentSessions?: number;
// Circuit Breaker
circuitBreakerFailureThreshold?: number;
circuitBreakerOpenDuration?: number;
circuitBreakerHalfOpenSuccessThreshold?: number;
maxRetryAttempts?: number | null;
// Network
proxyUrl?: string | null;
proxyFallbackToDirect?: boolean;
firstByteTimeoutStreamingMs?: number;
streamingIdleTimeoutMs?: number;
requestTimeoutNonStreamingMs?: number;
// MCP
mcpPassthroughType?: string;
mcpPassthroughUrl?: string | null;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check the fields in createProvider (around line 221-224)
sed -n '215,235p' src/repository/provider.ts | cat -n

Repository: ding113/claude-code-hub

Length of output: 1194


🏁 Script executed:

# Check the fields in updateProvider (around line 651-654)
sed -n '645,665p' src/repository/provider.ts | cat -n

Repository: ding113/claude-code-hub

Length of output: 1314


🏁 Script executed:

# Check all occurrences of tpm, rpm, rpd, cc in the file
rg -n "(tpm|rpm|rpd|cc)" src/repository/provider.ts | head -40

Repository: ding113/claude-code-hub

Length of output: 2045


🏁 Script executed:

# Check the BatchProviderUpdates interface definition (lines 968-1013)
sed -n '968,1013p' src/repository/provider.ts | cat -n

Repository: ding113/claude-code-hub

Length of output: 1987


🏁 Script executed:

# Check updateProvidersBatch setClauses to see what fields are handled
sed -n '1042,1150p' src/repository/provider.ts | cat -n

Repository: ding113/claude-code-hub

Length of output: 5232


BatchProviderUpdates 接口中补充注释,说明为何省略 tpmrpmrpdcc 字段

updateProvider 支持 tpmrpmrpdcc 字段(分别对应 Tokens Per Minute、Requests Per Minute、Requests Per Day 及并发连接数),但 BatchProviderUpdatesupdateProvidersBatch 中均不包含这些字段。

由于这四个字段代表单个供应商的专属速率限额,不适合作为批量覆写操作的对象,其省略是合理的设计决策。但请在 BatchProviderUpdates 接口上添加注释明确说明这一点,以避免后续维护者疑惑。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/repository/provider.ts` around lines 968 - 1013, 在 BatchProviderUpdates
接口上加入一行注释,说明之所以没有包含 tpm、rpm、rpd、cc(Tokens Per Minute、Requests Per
Minute、Requests Per Day、并发连接数)是有意为之,因为这些字段是针对单个供应商的专属速率限额,不适合批量覆盖;引用相关符号以便定位:在声明
BatchProviderUpdates 的位置上添加注释,提及 updateProvider 和 updateProvidersBatch
接受/处理这些速率字段但 BatchProviderUpdates 刻意省略它们以避免批量操作造成不当覆盖。

…nti-bruteforce, CSRF guard, security headers
… revocation, bruteforce integration, CSRF guard
…te, quality gates, migration runbook, go-live checklist
Changes session authentication from legacy API key cookies to opaque session identifiers with Redis backing. The opaque mode prevents API key exposure in cookies and enables proper session lifecycle management.

Key changes:
- Default SESSION_TOKEN_MODE changed from "legacy" to "opaque"
- Session IDs now prefixed with "sid_" for clear identification
- Unified authentication through validateAuthToken() wrapper
- Redis session creation now throws on failure instead of silent fallback
- Session rotation preserves new session even if old revocation fails
- Extracted auth response headers to dedicated module
- Fixed redirect sanitization bypass in login flow
- Updated all tests to reflect opaque-first behavior

BREAKING CHANGE: SESSION_TOKEN_MODE now defaults to "opaque". Existing deployments using legacy mode must explicitly set SESSION_TOKEN_MODE=legacy in environment variables to maintain current behavior. The "dual" mode remains available for gradual migration.
Vitest's `mockReset: true` config resets all mock implementations
between tests, including inline `vi.fn()` created inside `vi.mock()`
factories. For indirect imports (modules imported by the dynamically
imported route), the factory is NOT re-executed after
`vi.resetModules()`, so the reset mock returns undefined.

Fix: use `vi.hoisted()` variables re-initialized in `beforeEach`, or
plain arrow functions that are immune to mockReset.

- auth-bruteforce: getEnvConfig uses plain function (no vi.fn())
- session-login: toKeyFingerprint + getEnvConfig use hoisted variables
- redis-session-store: formatting only (biome auto-fix)
ding113 and others added 21 commits February 19, 2026 15:25
Removed duplicate/orphan test code that was outside of any describe
block, causing parse errors during format check.

CI Run: https://github.com/ding113/claude-code-hub/actions/runs/22132400374

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Both thinking-budget-editor and adaptive-thinking-editor tests now use
the same createRoot + happy-dom pattern, removing the broken dependency
on @testing-library/react which is not installed in this project.
Map model_redirects, allowed_models, anthropic_thinking_budget_preference,
and anthropic_adaptive_thinking through batchUpdateProviders. Empty
allowed_models array normalizes to null (allow-all).
Add dropdown menus for selecting providers by type and by group tag.
Selection is additive - adds matching providers to existing selection.
…ibility

Enhance previewProviderBatchPatch to produce before/after rows for each
provider-field combination. Anthropic-only fields on non-claude providers
are marked as skipped. Rows stored in preview snapshot for apply engine.
…dialog

Replace toggle-to-enable pattern with empty-means-no-change semantics.
Add three sections (basic, routing, anthropic) reusing ThinkingBudgetEditor
and AdaptiveThinkingEditor. Show affected-provider summary with masked keys.
…ure, and exclusion support

- Enhanced applyProviderBatchPatch to write to DB via updateProvidersBatch
- Added excludeProviderIds support for selective application
- Capture preimage of changed fields for undo support
- Map snake_case patch fields to camelCase repository format
- Invalidate provider cache after successful write
- Store preimage and patch in undo snapshot for T10
- 15 new tests covering writes, exclusion, idempotency, field mapping
…-provider restoration

- Enhanced undoProviderPatch to read preimage and write original values back to DB
- Group providers by identical preimage values to minimize DB round-trips
- Handle costMultiplier number-to-string conversion for repository interface
- Invalidate provider cache after successful undo writes
- Consume undo token only after successful writes (preserved for retry on failure)
- Updated contract test to populate preimage during apply step
- 10 new undo engine tests covering all edge cases
…, and auth hardening

Security:
- Add constant-time string comparison utility to prevent timing attacks
- Harden login route with improved token validation
- Add rate limiting to proxy auth guard
- Tighten CORS origin validation

Provider batch edit:
- Add build-patch-draft module for generating per-provider patches
- Add provider-batch-preview-step with field-level diff display
- Extend provider-form-context with batch mode support
- Refactor form sections (basic-info, network, routing, testing) for batch compatibility
- Expand provider-patch-contract with apply/undo engine types
- Add repository helpers for bulk provider operations
- Update i18n messages across all 5 languages

Tests:
- Add constant-time compare and proxy auth rate limit security tests
- Add build-patch-draft, preview step, form context, and undo toast unit tests
- Update existing batch dialog and patch contract tests
- Add generic RedisKVStore<T> with atomic get-and-delete via Lua script
- Migrate undo store from in-memory Map to Redis persistence
- Increase undo snapshot TTL from 10s to 30s
- Extend auth session TTL from 1 day to 7 days
- Add LRU eviction to login abuse policy
- Record login failures in auth route
- Add error logging to logout route
- Type anthropicAdaptiveThinking as AnthropicAdaptiveThinkingConfig
- i18n: add apiKeyLabel and unknownError keys across 5 locales
- i18n: internationalize batch edit preview field labels
- Fix thinking budget editor input handling
- Add claude-auth routing group for 1M context window
- Fix provider manager group filtering for default label
- Add comprehensive tests for RedisKVStore, auth, undo, and providers
@ding113 ding113 force-pushed the feat/security-auth-overhaul branch from 7c52261 to 4a0f06e Compare February 19, 2026 07:27
@ding113 ding113 merged commit 61cce9b into dev Feb 19, 2026
6 of 9 checks passed
@github-project-automation github-project-automation bot moved this from Backlog to Done in Claude Code Hub Roadmap Feb 19, 2026
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

114 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

if (!key) {
return NextResponse.json({ error: t?.("apiKeyRequired") }, { status: 400 });
if (!key || typeof key !== "string") {
loginPolicy.recordFailure(clientIp);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing key parameter means invalid key attempts aren't tracked for key-based rate limiting

Suggested change
loginPolicy.recordFailure(clientIp);
loginPolicy.recordFailure(clientIp, key);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/api/auth/login/route.ts
Line: 191

Comment:
missing `key` parameter means invalid key attempts aren't tracked for key-based rate limiting

```suggestion
      loginPolicy.recordFailure(clientIp, key);
```

How can I resolve this? If you propose a fix, please make it concise.

const session = await validateKey(key, { allowReadOnlyAccess: true });
if (!session) {
return NextResponse.json({ error: t?.("apiKeyInvalidOrExpired") }, { status: 401 });
loginPolicy.recordFailure(clientIp);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing key parameter means invalid key attempts aren't tracked for key-based rate limiting

Suggested change
loginPolicy.recordFailure(clientIp);
loginPolicy.recordFailure(clientIp, key);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/api/auth/login/route.ts
Line: 212

Comment:
missing `key` parameter means invalid key attempts aren't tracked for key-based rate limiting

```suggestion
      loginPolicy.recordFailure(clientIp, key);
```

How can I resolve this? If you propose a fix, please make it concise.

}
}

loginPolicy.recordSuccess(clientIp);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing key parameter means successful logins don't reset key-based rate limiting

Suggested change
loginPolicy.recordSuccess(clientIp);
loginPolicy.recordSuccess(clientIp, key);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/api/auth/login/route.ts
Line: 273

Comment:
missing `key` parameter means successful logins don't reset key-based rate limiting

```suggestion
    loginPolicy.recordSuccess(clientIp, key);
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (17)
messages/ja/settings/providers/batchEdit.json-81-92 (1)

81-92: ⚠️ Potential issue | 🟡 Minor

undo 节与 toast 节存在重复字符串值

两个节中有完全相同的文本值:

undo.button (Line 82) "元に戻す"
toast.undo (Line 98) "元に戻す"
undo.expired (Line 84) "元に戻す期限が切れました"
toast.undoExpired (Line 101) "元に戻す期限が切れました"

这两组键的值完全一致,建议确认它们是否确实对应不同的 UI 使用场景(例如,一组用于内联 UI 组件,另一组用于 Toast 通知),还是其中一组存在冗余。如果两者用途相同,未来维护时若只更新其中一处,可能导致两个 UI 位置文案不一致。请验证其他语言的 locale 文件是否有相同的结构,以确认这是有意为之的设计。

Also applies to: 93-104

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@messages/ja/settings/providers/batchEdit.json` around lines 81 - 92, The undo
and toast sections contain duplicate Japanese strings (e.g., undo.button vs
toast.undo, undo.expired vs toast.undoExpired); inspect whether these keys serve
distinct UI contexts or are redundant, then either (a) consolidate by removing
the redundant keys and point callers to a single canonical key (e.g., use
undo.button and toast.undo reference it) or (b) keep both but harmonize their
values and add a comment explaining the difference; update all callers to use
the chosen canonical keys (search for undo.button, toast.undo, undo.expired,
toast.undoExpired) and verify/align the same change across other locale files to
keep translations consistent.
src/lib/security/csrf-origin-guard.ts-40-43 (1)

40-43: ⚠️ Potential issue | 🟡 Minor

same-origin 快速路径跳过了 Origin 头的校验,存在非浏览器客户端绕过的风险

sec-fetch-site === "same-origin"allowSameOrigin === true 时,代码直接放行,即使请求中同时携带了不在白名单中的 Origin 头也不会被检查。

Sec-Fetch-* 是浏览器的禁止修改头(Forbidden Headers),正常浏览器 CSRF 攻击无法伪造,因此浏览器发起的 CSRF 仍然会被正确的 sec-fetch-site: cross-site 拦截。但非浏览器 HTTP 客户端(如 curl、自动化脚本、服务端转发)可以人工设置 Sec-Fetch-Site: same-origin,结合有效凭证可绕过来源白名单,不符合纵深防御原则。

建议:当 sec-fetch-site === "same-origin" 且请求中同时存在 Origin 头时,额外校验该 Origin 是否符合预期(需在 config 中额外传入 server origin)。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/security/csrf-origin-guard.ts` around lines 40 - 43, The fast-path
that returns allowed when fetchSite === "same-origin" and allowSameOrigin is
true skips Origin header validation and can be bypassed by non-browser clients;
update the logic in csrf-origin-guard (look for the fetchSite, allowSameOrigin
check) so that when fetchSite === "same-origin" and an Origin header is present
you also validate that Origin against the configured trusted origin(s) (e.g., a
new serverOrigin or allowedOrigins value provided in config) and only return {
allowed: true } if that Origin matches the whitelist; otherwise treat it as
disallowed.
tests/security/csrf-origin-guard.test.ts-17-132 (1)

17-132: ⚠️ Potential issue | 🟡 Minor

缺少对多个关键分支的测试覆盖,可能不满足 80% 覆盖率要求

当前测试套件未覆盖以下场景,导致 createCsrfOriginGuard 的部分条件分支未被验证:

  1. allowSameOrigin: false + sec-fetch-site: same-origin:当前仅测试了 allowSameOrigin: true 路径(Line 17-31),布尔条件 fetchSite === "same-origin" && allowSameOriginfalse 分支(应穿透到 Origin 检查)没有覆盖。

  2. sec-fetch-site: same-sitesec-fetch-site: none:这两个值既不触发同源快路径,也不触发跨域缺失 Origin 的拦截路径,应验证其走到 Origin 白名单检查。

  3. sec-fetch-site: same-origin + 不在白名单的 Origin + allowSameOrigin: true:此组合当前被放行(快路径不检查 Origin),需要一个明确的测试用例来记录这一已知行为(见安全审查意见)。

建议补充如下测试用例:

建议补充的测试用例
+  it("falls through to Origin check when allowSameOrigin is false and sec-fetch-site is same-origin", () => {
+    const guard = createCsrfOriginGuard({
+      allowedOrigins: ["https://example.com"],
+      allowSameOrigin: false,
+      enforceInDevelopment: true,
+    });
+
+    const allowed = guard.check(
+      createRequest({ "sec-fetch-site": "same-origin", origin: "https://example.com" })
+    );
+    expect(allowed).toEqual({ allowed: true });
+
+    const blocked = guard.check(
+      createRequest({ "sec-fetch-site": "same-origin", origin: "https://evil.com" })
+    );
+    expect(blocked.allowed).toBe(false);
+  });
+
+  it("allows same-site requests with Origin in allowlist", () => {
+    const guard = createCsrfOriginGuard({
+      allowedOrigins: ["https://example.com"],
+      allowSameOrigin: false,
+      enforceInDevelopment: true,
+    });
+    const result = guard.check(
+      createRequest({ "sec-fetch-site": "same-site", origin: "https://example.com" })
+    );
+    expect(result).toEqual({ allowed: true });
+  });
+
+  it("documents that same-origin fast-path allows requests even with non-allowlisted Origin", () => {
+    const guard = createCsrfOriginGuard({
+      allowedOrigins: [],
+      allowSameOrigin: true,
+      enforceInDevelopment: true,
+    });
+    const result = guard.check(
+      createRequest({ "sec-fetch-site": "same-origin", origin: "https://evil.example.com" })
+    );
+    // Documents known behavior: Origin header is not checked when sec-fetch-site is same-origin
+    expect(result).toEqual({ allowed: true });
+  });

基于代码规范,所有新功能的单元测试覆盖率不得低于 80%。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/security/csrf-origin-guard.test.ts` around lines 17 - 132, Tests for
createCsrfOriginGuard are missing coverage for several branches: the path where
allowSameOrigin is false but sec-fetch-site === "same-origin" (so requests
should fall through to Origin checks), the sec-fetch-site values "same-site" and
"none" (which should be validated against the allowedOrigins), and the explicit
case sec-fetch-site === "same-origin" with allowSameOrigin === true but an
Origin not present in allowedOrigins (to document the existing fast-path
behavior). Add unit tests that call guard.check (produced by
createCsrfOriginGuard) with combinations: (1) allowSameOrigin: false +
sec-fetch-site: "same-origin" + an Origin both allowed and disallowed, asserting
the Origin allowlist logic runs; (2) sec-fetch-site: "same-site" and "none" with
both allowed and disallowed Origin values to ensure whitelist behavior; and (3)
allowSameOrigin: true + sec-fetch-site: "same-origin" + Origin not in
allowedOrigins to assert the request is allowed (or to capture the documented
behavior), so all conditional branches in createCsrfOriginGuard are exercised.
src/app/api/auth/login/route.ts-186-208 (1)

186-208: ⚠️ Potential issue | 🟡 Minor

request.json() 解析异常会被 catch 以 500 返回,而非 400。

如果请求体不是合法 JSON(如空 body、格式错误),await request.json() 会抛出异常,进入第 296 行的 catch 块并返回 500 SERVER_ERROR。对客户端而言更合理的响应是 400 Bad Request。

建议修复:在 try 顶部单独捕获 JSON 解析
  try {
-   const { key } = await request.json();
+   let key: unknown;
+   try {
+     ({ key } = await request.json());
+   } catch {
+     loginPolicy.recordFailure(clientIp);
+     return withAuthResponseHeaders(
+       NextResponse.json(
+         { error: t?.("apiKeyRequired") ?? "Invalid request body", errorCode: "KEY_REQUIRED" },
+         { status: 400 }
+       )
+     );
+   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/auth/login/route.ts` around lines 186 - 208, The code currently
lets JSON parse errors from await request.json() fall into the outer catch and
return a 500; instead, wrap just the request.json() call in its own try/catch so
JSON parse failures return a 400 Bad Request. In the new inner catch call
loginPolicy.recordFailure(clientIp) and return the same withAuthResponseHeaders
responses used for missing/invalid key (using t?.("apiKeyRequired") and the
errorCode "KEY_REQUIRED" when shouldIncludeFailureTaxonomy(request) is true),
then continue with the existing logic for a successfully parsed body.
tests/security/security-headers-integration.test.ts-3-3 (1)

3-3: ⚠️ Potential issue | 🟡 Minor

导入路径应统一使用 @/ 别名

第 3 行和第 99、102 行使用了相对路径 "../../src/..." 进行导入,与第 20-43 行的 vi.mock() 中使用的 @/ 别名不一致。根据编码规范要求统一使用 @/ 别名来引用 ./src/ 目录下的文件,其他测试文件(如 notifier-circuit-breaker.test.ts)中的动态导入已成功使用 @/ 别名。

建议修复
-import { applyCors } from "../../src/app/v1/_lib/cors";
+import { applyCors } from "@/app/v1/_lib/cors";
-    const loginRoute = await import("../../src/app/api/auth/login/route");
+    const loginRoute = await import("@/app/api/auth/login/route");
     loginPost = loginRoute.POST;

-    const logoutRoute = await import("../../src/app/api/auth/logout/route");
+    const logoutRoute = await import("@/app/api/auth/logout/route");
     logoutPost = logoutRoute.POST;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/security/security-headers-integration.test.ts` at line 3, The test file
uses relative imports for modules like applyCors instead of the project alias;
update the import statements that reference "../../src/..." (including the
import of applyCors and the other two relative imports near the mock section) to
use the "@/..." alias (e.g., import applyCors from "@/app/v1/_lib/cors") so they
match the vi.mock() usages; ensure only the import paths change and keep the
same exported symbols/import names.
messages/zh-CN/settings/providers/batchEdit.json-80-80 (1)

80-80: ⚠️ Potential issue | 🟡 Minor

术语不一致:"服务商" 应统一为 "供应商"

第 80 行使用了"服务商",而同文件其他所有涉及提供商的译文均使用"供应商"(如第 52、85、86 行等)。请保持术语一致。

建议修复
-  "selectionHint": "选择多个服务商后可进行批量操作",
+  "selectionHint": "选择多个供应商后可进行批量操作",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@messages/zh-CN/settings/providers/batchEdit.json` at line 80, The text for
the JSON key "selectionHint" uses the inconsistent term "服务商"; update its value
to use the project's standard term "供应商" so it matches other entries (e.g., the
strings around lines using "供应商"), e.g. change "选择多个服务商后可进行批量操作" to the
equivalent with "供应商" to keep terminology consistent across the file.
tests/security/session-fixation-rotation.test.ts-84-90 (1)

84-90: ⚠️ Potential issue | 🟡 Minor

"post-login rotation" 测试仅验证 mock 本身,未覆盖任何生产代码路径

simulatePostLoginSessionRotation 直接接受 mockRotate 作为 rotate 参数并调用它,等同于:

const result = await mockRotate(oldSessionId);

该测试断言 mockRotate 被正确调用并返回预设值,与登录路由或会话旋转的实际实现毫无关联。相较之下,同文件中的 logout 测试通过 loadLogoutPost() 真正导入了生产路由,才具有实质意义。

建议将此用例改为通过动态导入真实登录路由(类似 loadLogoutPost())来触发 rotate() 调用,以验证登录成功后 setAuthCookie 接收到旋转后的 session ID。

Also applies to: 161-177

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/security/session-fixation-rotation.test.ts` around lines 84 - 90, The
test currently only calls the provided mock via
simulatePostLoginSessionRotation, so it never exercises production code; replace
the mock-level helper with a test that dynamically imports and invokes the real
login POST handler (similar to loadLogoutPost()), trigger a successful login
flow so the route executes its rotate() path, and assert that the real rotate()
is called and setAuthCookie is invoked with the rotated sessionId; locate
references to simulatePostLoginSessionRotation and mockRotate in the test and
change them to import the actual login route (e.g., loadLoginPost or the router
handler), wire any necessary mocks/stubs for external deps but let the route
call rotate(), and verify setAuthCookie received the rotated id returned by
rotate().
tests/unit/auth/auth-cookie-constant-sync.test.ts-18-19 (1)

18-19: ⚠️ Potential issue | 🟡 Minor

正则表达式未覆盖反引号模板字符串形式的硬编码

当前正则 /["']auth-token["']/ 仅匹配单引号和双引号字符串,无法检测到 `auth-token` 这类模板字面量写法。若将来某处无意间使用模板字符串引用了硬编码值,测试会静默放行。

建议修复
-    expect(proxySource).not.toMatch(/["']auth-token["']/);
-    expect(actionAdapterSource).not.toMatch(/["']auth-token["']/);
+    expect(proxySource).not.toMatch(/["'`]auth-token["'`]/);
+    expect(actionAdapterSource).not.toMatch(/["'`]auth-token["'`]/);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/auth/auth-cookie-constant-sync.test.ts` around lines 18 - 19, The
current assertions on proxySource and actionAdapterSource use the regex
/["']auth-token["']/ which only matches single- and double-quoted literals and
misses backtick template literals; update both expectations to use a regex that
also matches backticks (e.g., replace /["']auth-token["']/ with
/(?:["'`])auth-token(?:["'`])/) so proxySource and actionAdapterSource will fail
if `auth-token` is hardcoded with backticks.
src/app/[locale]/login/loading.tsx-30-30 (1)

30-30: ⚠️ Potential issue | 🟡 Minor

LoadingState 传入空 label 导致无障碍文本缺失

LoadingState 渲染一个 role="status" aria-busy="true"div,当 label="" 时,该区域没有任何文本内容。屏幕阅读器会感知到这个活跃区域,但无法向用户传达正在加载的内容,未能满足 WCAG 4.1.3 状态消息的要求。

建议修复
-          <LoadingState label="" className="text-center" />
+          <LoadingState label={t("loading")} className="text-center" />

其中 "loading" 对应已有的 i18n 键;若尚不存在,可添加一个通用的加载提示文案。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/login/loading.tsx at line 30, LoadingState is being
rendered with an empty label which leaves the role="status" aria-busy="true"
region with no accessible text; update the call to LoadingState to pass a
meaningful localized label (e.g., the existing i18n key "loading") instead of ""
so screen readers receive a status message—locate the LoadingState usage in this
file and replace label="" with the translated string (via your i18n helper like
useTranslations or equivalent) or add a fallback "loading" i18n key if missing.
src/app/[locale]/settings/providers/_components/adaptive-thinking-editor.tsx-74-105 (1)

74-105: ⚠️ Potential issue | 🟡 Minor

TooltipTrigger asChild 绑定在非聚焦 div 上,键盘用户无法触发提示

三个 Tooltip 块(第 74–105、111–145、152–173 行)均将 TooltipTrigger asChild 应用于 <div>。根据 Radix UI 官方文档,TooltipTrigger 的子元素必须是可聚焦的交互式元素。当改为 div 时,由于 div 默认无法获得焦点(tabIndex 未设置),键盘用户无法触发 Tooltip,导致帮助文本对键盘用户不可见。

建议将 Tooltip 仅包裹 Info 图标,将其改为可聚焦的 <button>

建议修复(以 effort 段为例,其余两处同理)
-          <SmartInputWrapper
-            label={t("sections.routing.anthropicOverrides.adaptiveThinking.effort.label")}
-          >
-            <Tooltip>
-              <TooltipTrigger asChild>
-                <div className="flex gap-2 items-center">
-                  <Select
-                    value={config.effort}
-                    onValueChange={(val) =>
-                      handleEffortChange(val as AnthropicAdaptiveThinkingEffort)
-                    }
-                    disabled={disabled}
-                  >
-                    <SelectTrigger className="w-40">
-                      <SelectValue />
-                    </SelectTrigger>
-                    <SelectContent>
-                      {(["low", "medium", "high", "max"] as const).map((level) => (
-                        <SelectItem key={level} value={level}>
-                          {t(`sections.routing.anthropicOverrides.adaptiveThinking.effort.options.${level}`)}
-                        </SelectItem>
-                      ))}
-                    </SelectContent>
-                  </Select>
-                  <Info className="h-4 w-4 text-muted-foreground shrink-0" />
-                </div>
-              </TooltipTrigger>
-              <TooltipContent side="top" className="max-w-xs">
-                <p className="text-sm">
-                  {t("sections.routing.anthropicOverrides.adaptiveThinking.effort.help")}
-                </p>
-              </TooltipContent>
-            </Tooltip>
-          </SmartInputWrapper>
+          <SmartInputWrapper
+            label={t("sections.routing.anthropicOverrides.adaptiveThinking.effort.label")}
+          >
+            <div className="flex gap-2 items-center">
+              <Select
+                value={config.effort}
+                onValueChange={(val) =>
+                  handleEffortChange(val as AnthropicAdaptiveThinkingEffort)
+                }
+                disabled={disabled}
+              >
+                <SelectTrigger className="w-40">
+                  <SelectValue />
+                </SelectTrigger>
+                <SelectContent>
+                  {(["low", "medium", "high", "max"] as const).map((level) => (
+                    <SelectItem key={level} value={level}>
+                      {t(`sections.routing.anthropicOverrides.adaptiveThinking.effort.options.${level}`)}
+                    </SelectItem>
+                  ))}
+                </SelectContent>
+              </Select>
+              <Tooltip>
+                <TooltipTrigger asChild>
+                  <button
+                    type="button"
+                    className="p-0 h-4 w-4 text-muted-foreground hover:text-foreground"
+                  >
+                    <Info className="h-4 w-4 shrink-0" />
+                    <span className="sr-only">
+                      {t("sections.routing.anthropicOverrides.adaptiveThinking.effort.label")}
+                    </span>
+                  </button>
+                </TooltipTrigger>
+                <TooltipContent side="top" className="max-w-xs">
+                  <p className="text-sm">
+                    {t("sections.routing.anthropicOverrides.adaptiveThinking.effort.help")}
+                  </p>
+                </TooltipContent>
+              </Tooltip>
+            </div>
+          </SmartInputWrapper>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/settings/providers/_components/adaptive-thinking-editor.tsx
around lines 74 - 105, The TooltipTrigger is currently wrapping a non-focusable
<div> (around Select and Info) so keyboard users cannot open the tooltip; update
the three Tooltip blocks (the one containing Select and Info for effort, and the
two similar blocks) so the Tooltip only wraps the Info icon and the Trigger is a
focusable element: replace the div-wrapped TooltipTrigger with TooltipTrigger
asChild around a button (or make the Info component render a <button> with
proper aria-label and focus styles) and leave the Select outside the Tooltip;
ensure TooltipContent remains unchanged and that the button is
keyboard-focusable so tooltip appears for keyboard users.
src/app/[locale]/settings/providers/_components/forms/provider-form/components/form-tab-nav.tsx-61-66 (1)

61-66: ⚠️ Potential issue | 🟡 Minor

水平布局按钮缺少 focus-visible:ring 样式,与桌面布局不一致。

桌面布局(第 120 行)的按钮包含 focus-visible:ring-2 focus-visible:ring-ring,但水平布局仅有 focus-visible:outline-none,键盘导航时没有可见的焦点指示器,影响无障碍访问。平板布局(第 177 行)同样缺少此样式。

建议修复
                 className={cn(
                   "relative flex items-center gap-2 px-4 py-3 text-sm font-medium transition-all whitespace-nowrap",
-                  "hover:text-foreground focus-visible:outline-none",
+                  "hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
                   isActive ? "text-primary" : "text-muted-foreground",
                   disabled && "opacity-50 cursor-not-allowed"
                 )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/components/form-tab-nav.tsx
around lines 61 - 66, 在水平布局的按钮 className(在 form-tab-nav.tsx 中使用 cn(...) 并包含
isActive/disabled 条件的那一行)仅有 "focus-visible:outline-none",缺少与桌面布局一致的可见焦点指示器;将
"focus-visible:ring-2 focus-visible:ring-ring" 添加到该 className
的焦点样式中(并同样检查并在平板布局的按钮定义处添加相同样式),以确保键盘导航时有可见的 focus ring;参考的标识符:cn(...),
isActive, disabled, className 属性。
tests/unit/login/login-ui-redesign.test.tsx-44-55 (1)

44-55: ⚠️ Potential issue | 🟡 Minor

global.fetchafterEach 中未恢复,会污染后续测试套件。

beforeEach 覆盖了 global.fetch,但 afterEach 没有将其恢复为原始值。同一 PR 中的 login-footer-system-name.test.tsxlogin-loading-state.test.tsx 都正确保存并恢复了 global.fetch

建议修复
+const globalFetch = global.fetch;
+
 describe("LoginPage UI Redesign", () => {
   let container: HTMLDivElement;
   let root: ReturnType<typeof createRoot>;

   beforeEach(() => {
     container = document.createElement("div");
     document.body.appendChild(container);
     root = createRoot(container);
     vi.clearAllMocks();
     global.fetch = vi.fn().mockResolvedValue({
       ok: true,
       json: async () => ({}),
     });
   });

   afterEach(() => {
     act(() => {
       root.unmount();
     });
     document.body.removeChild(container);
+    global.fetch = globalFetch;
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/login/login-ui-redesign.test.tsx` around lines 44 - 55, Tests
overwrite global.fetch in the beforeEach but never restore it in afterEach,
which can leak into other tests; update the test setup to save the original
global.fetch (e.g., const originalFetch = global.fetch) before mocking in the
beforeEach and then restore it in afterEach by assigning global.fetch =
originalFetch (perform restore after unmount/removeChild cleanup), referencing
the existing beforeEach/afterEach and the global.fetch mock in
login-ui-redesign.test.tsx to locate where to add the save/restore.
src/app/[locale]/settings/providers/_components/batch-edit/build-patch-draft.ts-199-205 (1)

199-205: ⚠️ Potential issue | 🟡 Minor

limitConcurrentSessionsnull 时的处理与其他 rate limit 字段不一致。

其他 rate limit 字段为 null 时均使用 { clear: true }(如 limit5hUsdlimitDailyUsdlimitTotalUsd 等),但 limitConcurrentSessionsnull 时使用 { set: 0 }。如果 0 在此字段有特殊含义(如表示"禁止并发"),请添加注释说明;否则建议统一为 { clear: true }

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/app/`[locale]/settings/providers/_components/batch-edit/build-patch-draft.ts
around lines 199 - 205, The handling of rateLimit.limitConcurrentSessions is
inconsistent: currently when state.rateLimit.limitConcurrentSessions === null
the code sets draft.limit_concurrent_sessions = { set: 0 } while other rate
limit nulls use { clear: true }. Update the branch in the
dirtyFields.has("rateLimit.limitConcurrentSessions") block to use
draft.limit_concurrent_sessions = { clear: true } when the state value is null
to match the other fields; if 0 has a special semantic meaning for
limit_concurrent_sessions, instead keep the { set: 0 } but add a clear comment
above that branch explaining the special meaning and why it differs from the
other rate limit fields (referencing
dirtyFields.has("rateLimit.limitConcurrentSessions"),
state.rateLimit.limitConcurrentSessions, and draft.limit_concurrent_sessions).
tests/unit/actions/provider-undo-edit.test.ts-2-2 (1)

2-2: ⚠️ Potential issue | 🟡 Minor

静态导入使用相对路径,违反 @/ 路径别名规范

第 2 行及后续所有动态 import() 调用均使用 "../../../src/..." 相对路径。根据编码规范,引用 ./src/ 目录下文件应统一使用 @/ 别名。

🛠️ 建议修复(静态导入)
-import { PROVIDER_BATCH_PATCH_ERROR_CODES } from "../../../src/lib/provider-batch-patch-error-codes";
+import { PROVIDER_BATCH_PATCH_ERROR_CODES } from "@/lib/provider-batch-patch-error-codes";

所有动态 import("../../../src/actions/providers") 也应改为 import("@/actions/providers")

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/actions/provider-undo-edit.test.ts` at line 2, Replace all
relative "../../../src/..." imports with the project alias "@/..." to conform to
the path-alias rule: change the static import of
PROVIDER_BATCH_PATCH_ERROR_CODES to import from
"@/lib/provider-batch-patch-error-codes" and update every dynamic import(...)
like import("../../../src/actions/providers") to import("@/actions/providers");
ensure you update all occurrences in this test file (e.g., references to
PROVIDER_BATCH_PATCH_ERROR_CODES and any dynamic import calls) so they use the
"@/..." alias consistently.
src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx-324-349 (1)

324-349: ⚠️ Potential issue | 🟡 Minor

Toast 展示时长(10s)与撤销令牌有效期(30s)不一致,导致静默 UX 缺口

duration: 10000(10 秒)后 toast 消失,但 providers-undo-store.test.ts 中的断言(setexMock 以 TTL=30 调用)证明撤销令牌实际有效期为 30 秒。Toast 消失后用户失去撤销入口,但令牌本身还有约 20 秒可用,这两者之间存在静默缺口。

建议将 toast 持续时间与令牌有效期保持一致(同为 30 秒),或缩短令牌 TTL:

🛠️ 建议修复
      toast.success(tBatchEdit("undo.singleDeleteSuccess"), {
-       duration: 10000,
+       duration: 30000,
        action: {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/settings/providers/_components/vendor-keys-compact-list.tsx
around lines 324 - 349, The toast shown after deletion uses duration: 10000
(10s) but the undo token TTL is 30s (per providers-undo-store.test), creating a
UX gap; update the toast success/action block (the toast.success call and its
options near the undoProviderDelete usage and tBatchEdit labels) to use
duration: 30000 so the toast stays visible for the same 30s as the undo token,
or alternatively reduce the undo token TTL in the providers-undo-store
(setexMock) to 10s—pick one approach and make the corresponding change
consistently (toast duration in the vendor-keys-compact-list toast.success or
token TTL in providers-undo-store) so both values match.
src/app/[locale]/settings/providers/_components/batch-edit/provider-batch-dialog.tsx-226-252 (1)

226-252: ⚠️ Potential issue | 🟡 Minor

批量编辑撤销成功后仅失效 ["providers"] 查询,与单条编辑/删除撤销不一致。

行 242 仅调用 queryClient.invalidateQueries({ queryKey: ["providers"] }),而 provider-form/index.tsx 中的撤销回调(行 387-390、479-482)会同时失效 providersproviders-healthproviders-statisticsprovider-vendors 四个查询。BatchConfirmDialog 中删除撤销(行 420)也存在同样的问题。

这会导致撤销后 UI 中健康状态/统计数据/供应商分组不更新。

补全查询失效列表
                 if (undoResult.ok) {
                   toast.success(t("toast.undoSuccess", { count: undoResult.data.revertedCount }));
-                  queryClient.invalidateQueries({ queryKey: ["providers"] });
+                  void Promise.all([
+                    queryClient.invalidateQueries({ queryKey: ["providers"] }),
+                    queryClient.invalidateQueries({ queryKey: ["providers-health"] }),
+                    queryClient.invalidateQueries({ queryKey: ["providers-statistics"] }),
+                    queryClient.invalidateQueries({ queryKey: ["provider-vendors"] }),
+                  ]);
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/app/`[locale]/settings/providers/_components/batch-edit/provider-batch-dialog.tsx
around lines 226 - 252, 当前批量编辑的撤销处理(provider-batch-dialog.tsx 中 result.ok 分支内的
undo 点击回调)只失效 ["providers"],与 provider-form 中撤销回调(provider-form/index.tsx)和
BatchConfirmDialog 的删除撤销不同步,导致健康/统计/供应商分组不更新;请在 provider-batch-dialog.tsx 的
undoProviderPatch 成功分支以及 BatchConfirmDialog 的删除撤销处理处,像 provider-form 那样同时调用
queryClient.invalidateQueries 对以下查询键进行失效:["providers"], ["providers-health"],
["providers-statistics"], ["provider-vendors"](可一次性逐个调用或循环调用),以确保 UI
在撤销后与单条编辑/删除行为一致。
tests/unit/settings/providers/build-patch-draft.test.ts-420-467 (1)

420-467: ⚠️ Potential issue | 🟡 Minor

电路熔断器和网络超时字段对 undefined 的处理策略确实存在显著差异,建议补充文档说明。

验证表明这种差异是有意为之的,但在代码中完全缺乏说明:

  • 电路熔断器字段failureThresholdopenDurationMinuteshalfOpenSuccessThreshold):当值为 undefined 时,映射为 { set: 0 }——表示显式重置为默认值 0
  • 网络超时字段firstByteTimeoutStreamingSecondsstreamingIdleTimeoutSecondsrequestTimeoutNonStreamingSeconds):当值为 undefined 时,完全跳过,不包含在 draft 中——表示"不修改此字段"

这种语义差异在 buildPatchDraftFromFormState 函数实现中缺少注释说明。建议在代码中添加注释,明确解释两组字段的不同策略原因(例如,电路熔断器是否需要显式的"恢复默认值"语义,而网络超时是否允许保持不变)。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/settings/providers/build-patch-draft.test.ts` around lines 420 -
467, In buildPatchDraftFromFormState, add a clear inline comment near the logic
that maps circuit breaker fields (failureThreshold, openDurationMinutes,
halfOpenSuccessThreshold -> circuit_breaker_failure_threshold,
circuit_breaker_open_duration, etc.) and the logic that maps network timeout
fields (firstByteTimeoutStreamingSeconds, streamingIdleTimeoutSeconds,
requestTimeoutNonStreamingSeconds -> first_byte_timeout_streaming_ms, etc.)
explaining that the circuit-breaker fields intentionally map undefined -> { set:
0 } to explicitly reset them to a zero default, whereas network timeout fields
intentionally skip including the key when undefined to leave the server-side
value unchanged; reference the function name buildPatchDraftFromFormState and
the specific output keys (e.g., circuit_breaker_open_duration,
circuit_breaker_failure_threshold, first_byte_timeout_streaming_ms) in the
comment so future readers can quickly find and understand the divergent
semantics.

Comment on lines +202 to +226
function isValidSetValue(field: ProviderBatchPatchField, value: unknown): boolean {
switch (field) {
case "is_enabled":
case "preserve_client_ip":
case "swap_cache_ttl_billing":
case "proxy_fallback_to_direct":
return typeof value === "boolean";
case "priority":
case "weight":
case "cost_multiplier":
case "limit_5h_usd":
case "limit_daily_usd":
case "limit_weekly_usd":
case "limit_monthly_usd":
case "limit_total_usd":
case "limit_concurrent_sessions":
case "circuit_breaker_failure_threshold":
case "circuit_breaker_open_duration":
case "circuit_breaker_half_open_success_threshold":
case "max_retry_attempts":
case "first_byte_timeout_streaming_ms":
case "streaming_idle_timeout_ms":
case "request_timeout_non_streaming_ms":
return typeof value === "number" && Number.isFinite(value);
case "group_tag":
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find schema files and inspect CreateProviderSchema/UpdateProviderSchema definitions
find . -type f -name "*.ts" -o -name "*.tsx" | head -20

Repository: ding113/claude-code-hub

Length of output: 668


🏁 Script executed:

# Search for CreateProviderSchema and UpdateProviderSchema
rg -n "CreateProviderSchema|UpdateProviderSchema" --type ts --type tsx

Repository: ding113/claude-code-hub

Length of output: 94


🏁 Script executed:

# Also examine the patch contract file to see full validation logic
cat -n src/lib/provider-patch-contract.ts | head -250

Repository: ding113/claude-code-hub

Length of output: 8852


🏁 Script executed:

# Get rest of the validation function and search for schema files
tail -n +250 src/lib/provider-patch-contract.ts | head -100

Repository: ding113/claude-code-hub

Length of output: 3266


🏁 Script executed:

# Search for CreateProviderSchema and UpdateProviderSchema definitions
rg -n "CreateProviderSchema|UpdateProviderSchema" -A 5 --type ts

Repository: ding113/claude-code-hub

Length of output: 14858


🏁 Script executed:

# Search for provider schema files
fd -type f -name "*.ts" | xargs grep -l "CreateProviderSchema\|UpdateProviderSchema" | head -10

Repository: ding113/claude-code-hub

Length of output: 237


🏁 Script executed:

# Inspect the actual numeric field definitions in CreateProviderSchema and UpdateProviderSchema
sed -n '400,500p' src/lib/validation/schemas.ts

Repository: ding113/claude-code-hub

Length of output: 3560


🏁 Script executed:

# Continue reading the schema definitions
sed -n '500,650p' src/lib/validation/schemas.ts

Repository: ding113/claude-code-hub

Length of output: 5241


补丁字段校验弱于 Create/Update Schema,多个字段约束未同步导致绕过校验风险

isValidSetValue() 仅校验数值类型和有限性,缺失整数性、范围、特殊规则等约束。例如:

  • weight 在 Schema 中要求整数 1-100,但补丁允许 0.5 或 150
  • priority 在 Schema 中要求整数 ≥0,但补丁允许 -1
  • limit_5h_usd 等金额字段在 Schema 中有上限(10000/50000/200000/10000000),但补丁无限制
  • circuit_breaker_open_duration 在 Schema 中要求整数且 ≥1000ms,但补丁可接受任意数值
  • request_timeout_non_streaming_ms 等超时字段有特殊规则(0=禁用或指定范围),但补丁不区分

需对齐 CreateProviderSchema/UpdateProviderSchema 的所有约束条件(.int().min().max()、范围、特殊值规则等)。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/provider-patch-contract.ts` around lines 202 - 226, isValidSetValue
currently only checks basic types and finite numbers; update it to enforce the
same constraints as CreateProviderSchema/UpdateProviderSchema (e.g., integer
checks, min/max bounds, and special-value rules) for each field referenced
(notably weight, priority,
limit_5h_usd/limit_daily_usd/limit_weekly_usd/limit_monthly_usd/limit_total_usd,
circuit_breaker_open_duration, circuit_breaker_failure_threshold,
circuit_breaker_half_open_success_threshold, max_retry_attempts,
first_byte_timeout_streaming_ms, streaming_idle_timeout_ms,
request_timeout_non_streaming_ms, and any other numeric/time/limit fields);
implement integer assertions (Number.isInteger) where schemas use .int(),
enforce .min()/.max() ranges to match schema limits (e.g., weight 1–100,
priority >=0, specific upper bounds for limit_*), and encode special rules such
as “0 = disabled” or minimum ms (e.g., circuit_breaker_open_duration >=1000) for
timeout fields so the patch validation cannot accept out-of-range or fractional
values.

Comment on lines +315 to +354
if (!isRecord(input)) {
return createInvalidPatchShapeError(field, "Patch input must be an object");
}

const unknownKeys = Object.keys(input).filter((key) => !PATCH_INPUT_KEYS.has(key));
if (unknownKeys.length > 0) {
return createInvalidPatchShapeError(
field,
`Patch input contains unknown keys: ${unknownKeys.join(",")}`
);
}

const hasSet = Object.hasOwn(input, "set");
const hasClear = input.clear === true;
const hasNoChange = input.no_change === true;
const modeCount = [hasSet, hasClear, hasNoChange].filter(Boolean).length;

if (modeCount !== 1) {
return createInvalidPatchShapeError(field, "Patch input must choose exactly one mode");
}

if (hasSet) {
if (input.set === undefined) {
return createInvalidPatchShapeError(field, "set mode requires a defined value");
}

if (!isValidSetValue(field, input.set)) {
return createInvalidPatchShapeError(field, "set mode value is invalid for this field");
}

return { ok: true, data: { mode: "set", value: input.set as T } };
}

if (hasNoChange) {
return { ok: true, data: createNoChangePatch() };
}

if (!CLEARABLE_FIELDS[field]) {
return createInvalidPatchShapeError(field, "clear mode is not supported for this field");
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

补丁校验错误消息为硬编码英文,违反 i18n 规范

这些错误消息会沿着 actions 返回给前端,属于用户可见文本,应改为 i18n key 或错误码,由调用方映射为多语言文案。

As per coding guidelines: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/provider-patch-contract.ts` around lines 315 - 354, Replace all
hardcoded, user-facing English strings passed to createInvalidPatchShapeError in
provider-patch-contract (occurrences around the validation checks using
isRecord, unknownKeys/PATCH_INPUT_KEYS, modeCount, hasSet, hasNoChange, and
CLEARABLE_FIELDS) with i18n keys or standardized error codes (e.g.,
PATCH_INVALID_SHAPE, PATCH_UNKNOWN_KEYS, PATCH_MODE_COUNT, PATCH_SET_REQUIRED,
PATCH_SET_INVALID, PATCH_CLEAR_UNSUPPORTED) and move variable details (like
unknownKeys.join(",")) into structured error metadata rather than the display
message; ensure callers map these keys/codes to localized text for zh-CN, zh-TW,
en, ja, ru.

Comment on lines +18 to +20
function normalizeOrigin(origin: string): string {
return origin.trim().toLowerCase();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

normalizeOrigin 缺少 URL 规范化,可能导致合法来源被误拒

当前实现仅做 trim().toLowerCase(),但未处理以下等效来源形式:

  • https://example.com:443https://example.com(RFC 6454 中默认端口等效)
  • http://example.com:80http://example.com
  • https://example.com/https://example.com(尾随斜杠)

allowedOrigins 中配置了含默认端口的来源(如 https://example.com:443),而浏览器发送的是不含端口的 https://example.com,将被错误拒绝。

建议使用 URL 构造函数进行规范化:

提议的修复:使用 URL 进行规范化
 function normalizeOrigin(origin: string): string {
-  return origin.trim().toLowerCase();
+  try {
+    const url = new URL(origin.trim());
+    // URL constructor already normalizes scheme/host to lowercase and drops default ports
+    return url.origin.toLowerCase();
+  } catch {
+    return origin.trim().toLowerCase();
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/security/csrf-origin-guard.ts` around lines 18 - 20, normalizeOrigin
currently only trims and lowercases input, which fails to canonicalize
equivalent origins (e.g. default ports and trailing slashes) and can cause valid
origins to be rejected; update the normalizeOrigin function to parse the input
using the URL constructor (or a robust URL parser), normalize scheme and
hostname to lowercase, omit default ports (remove :80 for http and :443 for
https), remove any trailing slash from pathname, and return the canonical origin
string so comparisons against allowedOrigins (and functions that call
normalizeOrigin) correctly treat "https://example.com" and
"https://example.com:443/" as equal.

import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import LoginPage from "../../../src/app/[locale]/login/page";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

使用 @/ 路径别名代替相对路径。

同一 PR 中的其他登录测试文件(如 login-ui-redesign.test.tsxlogin-loading-state.test.tsx)均使用 @/app/[locale]/login/page,此处应保持一致。

建议修复
-import LoginPage from "../../../src/app/[locale]/login/page";
+import LoginPage from "@/app/[locale]/login/page";

As per coding guidelines: "Use path alias @/ to reference files in ./src/ directory".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import LoginPage from "../../../src/app/[locale]/login/page";
import LoginPage from "@/app/[locale]/login/page";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/login/login-footer-system-name.test.tsx` at line 4, Replace the
relative import string "../../../src/app/[locale]/login/page" with the
path-alias import "@/app/[locale]/login/page" in the test so it matches other
login tests; update the import statement that brings in LoginPage (the imported
symbol LoginPage) to use the "@/..." alias.

Comment on lines +57 to +65
const render = async () => {
await act(async () => {
root.render(<LoginPage />);
});

await act(async () => {
await Promise.resolve();
});
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the test file to see the actual code at lines 57-65
cat -n tests/unit/login/login-footer-version.test.tsx | head -80 | tail -30

Repository: ding113/claude-code-hub

Length of output: 1099


🏁 Script executed:

# Get the full test file to understand context
wc -l tests/unit/login/login-footer-version.test.tsx

Repository: ding113/claude-code-hub

Length of output: 117


🏁 Script executed:

# Find LoginPage component
fd -name "*LoginPage*" -type f

Repository: ding113/claude-code-hub

Length of output: 297


🏁 Script executed:

# Find login-related components
fd -name "*login*" -type f | grep -E "\.(tsx?|jsx?)$" | head -20

Repository: ding113/claude-code-hub

Length of output: 297


🏁 Script executed:

# Find LoginPage component files
find . -name "*.tsx" -o -name "*.ts" | grep -i login | grep -v test | grep -v node_modules

Repository: ding113/claude-code-hub

Length of output: 256


🏁 Script executed:

# Search for LoginPage component definition
rg "export.*LoginPage|function LoginPage|const LoginPage" --type ts --type tsx | grep -v test

Repository: ding113/claude-code-hub

Length of output: 94


🏁 Script executed:

# Search for login-footer-version or version fetch in login components
rg "login-footer-version|hasUpdate" --type ts --type tsx | head -20

Repository: ding113/claude-code-hub

Length of output: 94


🏁 Script executed:

# Read the login page component
cat -n ./src/app/[locale]/login/page.tsx

Repository: ding113/claude-code-hub

Length of output: 19615


🏁 Script executed:

# Search for version fetch logic without specifying file type
rg "login-footer-version|hasUpdate|/api/version" -A 5 -B 5

Repository: ding113/claude-code-hub

Length of output: 21349


🏁 Script executed:

# Look for LoginPage in test imports
head -30 tests/unit/login/login-footer-version.test.tsx

Repository: ding113/claude-code-hub

Length of output: 1176


🏁 Script executed:

# Search for flushMicrotasks helper function definition
rg "flushMicrotasks|function.*flush" --type ts --type tsx tests/

Repository: ding113/claude-code-hub

Length of output: 94


🏁 Script executed:

# Get the complete login-footer-version.test.tsx file to see all details
cat tests/unit/login/login-footer-version.test.tsx

Repository: ding113/claude-code-hub

Length of output: 3251


🏁 Script executed:

# Search for flushMicrotasks definition across test files
rg "flushMicrotasks" tests/

Repository: ding113/claude-code-hub

Length of output: 3042


🏁 Script executed:

# Check the login-footer-system-name.test.tsx file to see its complete render pattern
cat tests/unit/login/login-footer-system-name.test.tsx | head -100

Repository: ding113/claude-code-hub

Length of output: 2942


🏁 Script executed:

# Look for test utilities or setup files that might define async helpers
find tests -name "*.ts" -o -name "*.tsx" | xargs grep -l "flushMicrotasks\|act" | head -5

Repository: ding113/claude-code-hub

Length of output: 324


🏁 Script executed:

# Check if there's a vitest config that might affect promise handling
find . -name "vitest.config.*" -o -name "vite.config.*" | head -5

Repository: ding113/claude-code-hub

Length of output: 85


🏁 Script executed:

# See if the test file has been recently modified or if there are any CI issues noted
git log --oneline tests/unit/login/login-footer-version.test.tsx 2>/dev/null | head -5 || echo "Git not available or file new"

Repository: ding113/claude-code-hub

Length of output: 139


🏁 Script executed:

# Check if there are any existing issues or test flakiness reported
rg "flaky|intermittent|unstable" tests/unit/login/login-footer-version.test.tsx

Repository: ding113/claude-code-hub

Length of output: 49


🏁 Script executed:

# Verify the exact async behavior needed by checking one more similar test helper
rg -A 10 "const flushMicrotasks" tests/unit/lib/provider-endpoints/probe-scheduler.test.ts

Repository: ding113/claude-code-hub

Length of output: 49


单次 Promise.resolve() 不足以刷新 Suspense + useEffect 的完整异步链

LoginPage 通过 <Suspense> 包裹 LoginPageContent,其中 useEffect 发起 fetch("/api/version").json()setVersionInfo() 的异步链。完整执行路径需要至少 3–4 个微任务队列轮次,而当前第二个 act 仅刷新一次 Promise.resolve()。同项目中相似测试(login-footer-system-name.test.tsx)使用两次 Promise.resolve() 调用,其他测试使用 6 次以上。当前实现在 CI 环境中可能导致测试不稳定。

建议改用多次微任务刷新:

修复方案
  const render = async () => {
    await act(async () => {
      root.render(<LoginPage />);
    });
+
+  // 刷新多个微任务轮次以确保 Suspense 解析、useEffect 执行、fetch 完成和状态更新
+  await act(async () => {
+    await Promise.resolve();
+    await Promise.resolve();
+  });
 };

或参考 login-footer-system-name.test.tsxflushMicrotasks() 模式,更清晰地表达意图。

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const render = async () => {
await act(async () => {
root.render(<LoginPage />);
});
await act(async () => {
await Promise.resolve();
});
};
const render = async () => {
await act(async () => {
root.render(<LoginPage />);
});
// 刷新多个微任务轮次以确保 Suspense 解析、useEffect 执行、fetch 完成和状态更新
await act(async () => {
await Promise.resolve();
await Promise.resolve();
});
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/login/login-footer-version.test.tsx` around lines 57 - 65, The
test's render helper (the async render function that calls
root.render(<LoginPage />) inside act and then awaits a single
Promise.resolve()) doesn't flush enough microtask ticks to let Suspense +
LoginPageContent's useEffect fetch("/api/version") -> .json() ->
setVersionInfo() complete reliably; update that helper (the second act after
root.render) to await multiple microtask ticks (e.g., call Promise.resolve()
several times or loop awaiting Promise.resolve() 3–6 times) or replace it with
the existing flushMicrotasks() utility used in login-footer-system-name.test.tsx
so the full async chain in LoginPageContent runs to completion before
assertions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:provider area:session area:UI enhancement New feature or request size/XL Extra Large PR (> 1000 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant