Skip to content

Fix user rate limit enforcement#643

Merged
ding113 merged 5 commits intodevfrom
fix/rate-limit-user-limits
Jan 22, 2026
Merged

Fix user rate limit enforcement#643
ding113 merged 5 commits intodevfrom
fix/rate-limit-user-limits

Conversation

@ding113
Copy link
Owner

@ding113 ding113 commented Jan 22, 2026

Summary

Fixes critical rate limit enforcement bug where user-level limits were not properly validated, allowing keys to exceed user quotas.

Problem

Related Issues:

Root Cause:

The validation logic in addKey and editKey actions used incorrect null-checking conditions. The original code used truthy checks (if (data.limit && user.limit)) which treated 0 as falsy, causing validation to be skipped when either the key or user limit was set to 0 (unlimited). This allowed:

  1. Keys to set limits exceeding user limits when user limit was 0
  2. Keys to bypass validation entirely when key limit was 0
  3. User-level concurrent sessions, 5h, weekly, and monthly limits to never be enforced in the proxy pipeline

Related Work:

Solution

1. Fixed Validation Logic in Key Actions (src/actions/keys.ts)

Before:

if (data.limit5hUsd && user.limit5hUsd && data.limit5hUsd > user.limit5hUsd)

After:

if (
  validatedData.limit5hUsd != null &&
  validatedData.limit5hUsd > 0 &&
  user.limit5hUsd != null &&
  user.limit5hUsd > 0 &&
  validatedData.limit5hUsd > user.limit5hUsd
)

Key improvements:

  • Explicit != null checks distinguish between 0 (unlimited) and null (not set)
  • Added > 0 checks to skip validation when either limit is 0 (unlimited)
  • Applied to all limit fields: limit5hUsd, limitDailyUsd, limitWeeklyUsd, limitMonthlyUsd, limitTotalUsd, limitConcurrentSessions

2. Added User Concurrent Session Enforcement (src/app/v1/_lib/proxy/rate-limit-guard.ts)

Added missing user-level concurrent session check (step 4) in the rate limit pipeline:

// 4. User 并发 Session(账号级并发保护)
if (user.limitConcurrentSessions != null && user.limitConcurrentSessions > 0) {
  const userSessionCheck = await RateLimitService.checkSessionLimit(
    user.id,
    "user",
    user.limitConcurrentSessions
  );
  // ... error handling
}

Updated check order:

  • 3-5. Resource/frequency protection: Key concurrent → User concurrent (NEW) → User RPM
  • 6-9. Short-term limits: Key 5h → User 5h → Key daily → User daily
  • 10-13. Long-term limits: Key weekly → User weekly → Key monthly → User monthly

3. Enhanced Session Tracking (src/lib/session-tracker.ts)

  • Added userId parameter to trackSession() and refreshSession()
  • Track user-level active sessions in Redis: user:${userId}:active_sessions (ZSET)
  • Added getUserSessionCount() method for user-level concurrent session counting

4. Extended Rate Limit Service (src/lib/rate-limit/service.ts)

  • Extended checkSessionLimit() to support "user" type (previously only "key" and "provider")
  • Added checkRpmLimit() wrapper method for consistent RPM checking
  • Fixed error messages to correctly label user-type limits

5. Populated Missing User Limit Fields (src/repository/key.ts, src/repository/_shared/transformers.ts)

  • Added missing user limit fields to validateApiKeyAndGetUser() query: limit5hUsd, limitWeeklyUsd, limitMonthlyUsd, limitConcurrentSessions, dailyResetMode, dailyResetTime
  • Fixed toUser() transformer to properly parse numeric limit fields from database strings

Changes

Core Changes

  • src/actions/keys.ts (+67/-27): Fixed null-checking logic for all limit validations in addKey and editKey
  • src/app/v1/_lib/proxy/rate-limit-guard.ts (+47/-7): Added user concurrent session check, updated check order
  • src/lib/session-tracker.ts (+52/-3): Added user-level session tracking support
  • src/lib/rate-limit/service.ts (+23/-4): Extended session limit checking to support user type

Supporting Changes

  • src/repository/key.ts (+12/-0): Populated missing user limit fields in query
  • src/repository/_shared/transformers.ts (+13/-0): Fixed user limit field parsing
  • src/app/v1/_lib/proxy/response-handler.ts (+5/-3): Pass userId to session refresh
  • src/app/v1/_lib/proxy/session-guard.ts (+5/-3): Pass userId to session tracking

Tests Added

  • tests/unit/actions/keys-limit-validation.test.ts (+153): Key limit validation scenarios
  • tests/unit/lib/rate-limit/service-extra.test.ts (+20): RPM limit checking
  • tests/unit/proxy/rate-limit-guard.test.ts (+25/-3): User concurrent session enforcement
  • tests/unit/repository/key.test.ts (+138): User limit field population
  • src/repository/_shared/transformers.test.ts (+47): Limit field transformation

Breaking Changes

No breaking changes

  • All changes are internal fixes to existing validation logic
  • API signatures remain unchanged
  • Existing configurations require no migration

Testing

Automated Tests

  • ✅ Unit tests added for all validation scenarios
  • ✅ Tests cover 0 (unlimited) vs null (not set) edge cases
  • ✅ User concurrent session enforcement tested
  • ✅ Limit field population and transformation tested

Test Scenarios Verified

Scenario 1: Key limit exceeds user limit (should block)

  • User limit: 2 concurrent sessions
  • Key limit: 3 concurrent sessions
  • Result: ✅ Blocked with error message

Scenario 2: User limit is 0 (unlimited, should allow)

  • User limit: 0 (unlimited)
  • Key limit: 3 concurrent sessions
  • Result: ✅ Allowed

Scenario 3: User concurrent session enforcement

  • User limit: 1 concurrent session
  • Current usage: 2 sessions
  • Result: ✅ Blocked with concurrent_sessions error

Checklist

  • ✅ Code follows project conventions
  • ✅ Self-review completed
  • ✅ Tests pass locally
  • ✅ All limit fields properly validated
  • ✅ User-level limits enforced in proxy pipeline

Description enhanced by Claude AI

Greptile Overview

Greptile Summary

Fixed critical rate limit enforcement bug where user-level limits were not validated due to incorrect null-checking (if (data.limit && user.limit) treated 0 as falsy). Updated validation to use explicit != null and > 0 checks, treating 0 as unlimited.

Key Changes:

  • Fixed validation logic in addKey and editKey to properly handle 0 (unlimited) vs null (not set)
  • Added missing user concurrent session enforcement (step 4) in rate limit pipeline
  • Extended session tracking to support user-level counting with Redis ZSET
  • Populated missing user limit fields in validateApiKeyAndGetUser() query
  • Added comprehensive test coverage for validation scenarios

Critical Issue Found:

  • Inconsistent 0 handling in transformers: New parseOptionalNumber() preserves 0, but existing rpm/dailyQuota logic converts 0null. This creates a mismatch where validation expects 0 = unlimited but transformers may return actual 0 values, breaking validation for limit5hUsd, limitWeeklyUsd, and limitMonthlyUsd fields.

Confidence Score: 2/5

  • This PR contains a critical logic bug that breaks the core validation fix it attempts to implement
  • While the PR correctly identifies and fixes the validation logic bug in keys.ts, it introduces a new critical inconsistency in transformers.ts. The new parseOptionalNumber() function preserves 0 values, but the existing rpm and dailyQuota transformation logic converts 0 to null. This inconsistency means the validation in keys.ts (which expects 0 = unlimited and uses > 0 checks) will fail when the database has 0 stored for limit fields, as some will be returned as 0 (breaking validation) while others get converted to null (working correctly). This undermines the entire fix.
  • Pay critical attention to src/repository/_shared/transformers.ts - the inconsistent 0 handling must be fixed before merge

Important Files Changed

Filename Overview
src/repository/_shared/transformers.ts CRITICAL: Inconsistent 0 handling breaks validation - parseOptionalNumber preserves 0, but rpm/dailyQuota convert 0 to null, causing mismatches with validation logic
src/actions/keys.ts Fixed validation logic to properly handle 0 (unlimited) vs null (unset) with explicit != null and > 0 checks across all limit fields
src/app/v1/_lib/proxy/rate-limit-guard.ts Added user concurrent session enforcement (step 4) with proper error handling and reordered check sequence
src/lib/session-tracker.ts Extended session tracking to support user-level counting with new getUserSessionCount() method and user ZSET tracking
src/repository/key.ts Populated missing user limit fields in query to enable validation in key actions

Sequence Diagram

sequenceDiagram
    participant Client
    participant SessionGuard
    participant RateLimitGuard
    participant SessionTracker
    participant RateLimitService
    participant KeyActions
    participant Transformers
    participant Redis
    
    Note over Client,Redis: Key Creation/Edit Flow
    Client->>KeyActions: addKey/editKey(limitConcurrentSessions=3)
    KeyActions->>Transformers: toUser(dbUser)
    Transformers-->>KeyActions: user with limitConcurrentSessions: 0 or null
    KeyActions->>KeyActions: Validate: key.limit vs user.limit
    Note over KeyActions: Check: limit != null AND limit > 0 AND<br/>user.limit != null AND user.limit > 0 AND<br/>key.limit > user.limit
    alt User limit is 0 (unlimited)
        KeyActions-->>Client: Allowed - 0 means unlimited
    else User limit > 0 and key exceeds it
        KeyActions-->>Client: Error: KEY_LIMIT_CONCURRENT_EXCEEDS_USER_LIMIT
    end
    
    Note over Client,Redis: Request Flow with Rate Limit Enforcement
    Client->>SessionGuard: Proxy Request
    SessionGuard->>SessionTracker: trackSession(sessionId, keyId, userId)
    SessionTracker->>Redis: ZADD active sessions for key
    SessionTracker->>Redis: ZADD active sessions for user
    
    SessionGuard->>RateLimitGuard: checkRateLimits()
    
    RateLimitGuard->>RateLimitService: checkSessionLimit(key.id, "key", limit)
    RateLimitService->>SessionTracker: getKeySessionCount(keyId)
    SessionTracker->>Redis: ZCOUNT key active sessions
    Redis-->>SessionTracker: count
    SessionTracker-->>RateLimitService: count
    alt Key concurrent exceeded
        RateLimitService-->>RateLimitGuard: not allowed
        RateLimitGuard-->>Client: Error: concurrent_sessions
    end
    
    RateLimitGuard->>RateLimitService: checkSessionLimit(user.id, "user", limit)
    RateLimitService->>SessionTracker: getUserSessionCount(userId)
    SessionTracker->>Redis: ZCOUNT user active sessions
    Redis-->>SessionTracker: count
    SessionTracker-->>RateLimitService: count
    alt User concurrent exceeded
        RateLimitService-->>RateLimitGuard: not allowed
        RateLimitGuard-->>Client: Error: concurrent_sessions
    end
    
    RateLimitGuard->>RateLimitService: checkRpmLimit(user.id, "user", rpm)
    alt RPM exceeded
        RateLimitService-->>RateLimitGuard: not allowed
        RateLimitGuard-->>Client: Error: rate_limit
    end
    
    RateLimitGuard-->>Client: Request allowed
Loading

@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 addresses an issue where user rate limits were not being properly enforced, particularly for concurrent sessions and when limits were set to zero or null. It introduces robust checks for user-level concurrent session limits, refines existing key-level limit validations to handle null and zero values correctly, and enhances session tracking to support these new user-centric limits. The changes ensure that both key and user rate limits are applied consistently and accurately across the system, closing issue #634.

Highlights

  • Enhanced Key Limit Validation: The validation logic for key-specific rate limits (5-hour, daily, weekly, monthly, total USD, and concurrent sessions) has been refined. It now explicitly checks if both the key's and user's corresponding limit values are not null and greater than zero before comparison, preventing unintended limit bypasses.
  • User Concurrent Session Limit Enforcement: A new rate limit check has been introduced to enforce concurrent session limits at the user level, complementing the existing key-level concurrent session limits. This ensures users cannot exceed their overall session capacity.
  • Improved Session Tracking: The SessionTracker now includes the user ID when tracking and refreshing sessions. This enhancement is crucial for accurately counting active sessions per user, enabling the new user-level concurrent session limits.
  • Refactored RPM Limit Checks: The checkUserRPM function has been refactored into a more generic checkRpmLimit function, which currently supports user-level RPM checks. This provides a more consistent and extensible API for rate limit management.
  • Accurate User Data Transformation: The toUser transformer now correctly parses and assigns various user limit fields (e.g., limit5hUsd, limitWeeklyUsd, limitMonthlyUsd, limitTotalUsd, and limitConcurrentSessions) from the database, handling null, undefined, and string-to-float conversions accurately.
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.

@coderabbitai
Copy link

coderabbitai bot commented Jan 22, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

本次变更在速率限制与会话跟踪中引入用户级并发会话和 RPM 检查,调整代理层检查顺序,增加 key 限额对比用户限额的验证,并在多处将 session-tracker 的接口扩展为携带 userId,同时补充相应的错误消息与测试覆盖。

Changes

Cohort / File(s) 变更摘要(注意点)
密钥操作与验证
src/actions/keys.ts
将对限额的直接输入比较替换为基于 validatedData 的非空/正数校验后再与用户限额比较;统一错误码为新的翻译键(如 KEY_LIMIT_5H_EXCEEDS_USER_LIMIT);同时在 addKey/editKey 路径保持原有控制流与同步。
代理层:速率限制与会话
src/app/v1/_lib/proxy/rate-limit-guard.ts, src/app/v1/_lib/proxy/session-guard.ts, src/app/v1/_lib/proxy/response-handler.ts
重新排序检查为 Key 并发 → User 并发 → User RPM;在会话跟踪/刷新调用中追加 userId 参数;为用户并发超限引入本地化错误与 reset 时间信息;response-handler 传递 userId 给 SessionTracker.refreshSession。
会话跟踪实现
src/lib/session-tracker.ts
引入 user:${userId}:active_sessions 跟踪集合;trackSession/refreshSession 接受可选 userId 并在提供时维护用户级 ZSET;新增 getUserSessionCount(userId)。注意 Redis key 管理与可选 userId 的条件分支。
速率限制服务
src/lib/rate-limit/service.ts
扩展 checkSessionLimit 支持 "user" 类型并基于用户计数决策;新增 checkRpmLimit(id, type: "user", limit),在 type 为 user 时委派到用户 RPM 检查。公共签名新增/修改需注意调用方同步。
数据转换与仓库查询
src/repository/_shared/transformers.ts, src/repository/key.ts
在 toUser()/validateApiKeyAndGetUser 中新增/规范化用户限额字段(limit5hUsdlimitWeeklyUsdlimitMonthlyUsdlimitTotalUsdlimitConcurrentSessions)及重置配置字段的选取与类型转换(string → number
本地化文案
messages/en/errors.json, messages/ja/errors.json, messages/ru/errors.json, messages/zh-CN/errors.json, messages/zh-TW/errors.json
新增六组错误消息键(如 KEY_LIMIT_5H_EXCEEDS_USER_LIMIT / KEY_LIMIT_CONCURRENT_EXCEEDS_USER_LIMIT 等),用于限额超出用户限额的错误文本。
测试与覆盖
src/repository/_shared/transformers.test.ts, tests/unit/actions/keys-limit-validation.test.ts, tests/unit/lib/rate-limit/service-extra.test.ts, tests/unit/proxy/rate-limit-guard.test.ts, tests/unit/repository/key.test.ts
添加/更新多处单元测试:限额字段规范化测试、addKey/editKey 限额验证、user 型 RPM 测试、并发会话限制路径测试、validateApiKeyAndGetUser 的字段解析测试。注意测试中对 SessionTracker/RateLimitService mocks 的签名与行为更新。

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • release v0.4.3 #628: 与本 PR 在 rate-limit、session-tracker、keys 限额验证和 i18n 文案上有直接重叠修改(签名与逻辑路径相同)。
  • fix: 当用户限额未设置时,key 限额不生效 #531: 修改了同一速率限制守卫(rate-limit-guard)与 RateLimitService 的检查顺序与错误处理,代码路径高度相关。
  • release v0.3.41 #522: 与本次变更目标(使用户 5h 限额生效并被 key 继承/校验)在问题背景与实现意图上直接相关。
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR attempts to fix issue #634 (user 5h limit not enforced) but introduces a critical inconsistency in transformers.ts that breaks the core validation fix—parseOptionalNumber preserves 0 while rpm/dailyQuota convert 0 to null, causing validation to fail inconsistently. Fix parseOptionalNumber to convert 0 to null for consistency with existing rpm/dailyQuota logic, ensuring all limit fields treat 0 as unlimited consistently across validation and transformation layers.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title "Fix user rate limit enforcement" accurately summarizes the main change—fixing critical rate limit enforcement bugs where user-level limits were not properly validated.
Description check ✅ Passed The description is directly related to the changeset, clearly explaining the root cause (truthy checks treating 0 as falsy), the solution (explicit != null and > 0 checks), and changes across multiple files.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing user rate limit enforcement—validating key limits against user limits, tracking user sessions, and enforcing user-level concurrent sessions. No unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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 fix/rate-limit-user-limits

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.

@github-actions github-actions bot added bug Something isn't working area:Rate Limit area:session area:core size/L Large PR (< 1000 lines) labels Jan 22, 2026
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 pull request addresses the enforcement of user rate limits by adding checks to ensure that key limits do not exceed user limits. The changes include modifications to the addKey and editKey functions in src/actions/keys.ts to validate limit values, updates to src/app/v1/_lib/proxy/rate-limit-guard.ts to incorporate user-level concurrency limits, and enhancements to session tracking in src/lib/session-tracker.ts. Additionally, tests have been added and modified to ensure the correct enforcement of these limits.

Comment on lines +185 to +190
if (
validatedData.limit5hUsd != null &&
validatedData.limit5hUsd > 0 &&
user.limit5hUsd != null &&
user.limit5hUsd > 0 &&
validatedData.limit5hUsd > user.limit5hUsd
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 conditions for checking the 5-hour USD limit are quite verbose. Consider simplifying the condition by combining the null and greater-than-zero checks into a single expression for better readability and maintainability.

    if (
      validatedData.limit5hUsd > 0 &&
      user.limit5hUsd > 0 &&
      validatedData.limit5hUsd > user.limit5hUsd
    ) {

Comment on lines +251 to 256
validatedData.limitConcurrentSessions != null &&
validatedData.limitConcurrentSessions > 0 &&
user.limitConcurrentSessions != null &&
user.limitConcurrentSessions > 0 &&
validatedData.limitConcurrentSessions > user.limitConcurrentSessions
) {
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Similar to the other limit checks, this condition can be simplified for better readability.

    if (
      validatedData.limitConcurrentSessions > 0 &&
      user.limitConcurrentSessions > 0 &&
      validatedData.limitConcurrentSessions > user.limitConcurrentSessions
    ) {

Comment on lines +520 to +522
: type === "provider"
? await SessionTracker.getProviderSessionCount(id)
: await SessionTracker.getUserSessionCount(id);
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

This ternary operator is getting a bit long. Consider using an if/else if/else block for better readability.

          ? await SessionTracker.getKeySessionCount(id)
          : type === "provider"
            ? await SessionTracker.getProviderSessionCount(id)
            : await SessionTracker.getUserSessionCount(id);

Comment on lines +1954 to +1957
void SessionTracker.refreshSession(session.sessionId, key.id, provider.id, user.id).catch(
(error) => {
logger.error("[ResponseHandler] Failed to refresh session tracker:", error);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Adding the userId to the refreshSession call is a good addition for tracking user-level sessions. Ensure that SessionTracker.refreshSession handles the new userId parameter correctly.

Comment on lines +141 to +144
void SessionTracker.trackSession(sessionId, keyId, session.authState?.user?.id).catch(
(err) => {
logger.error("[ProxySessionGuard] Failed to track session:", err);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Adding the userId to the trackSession call is a good addition for tracking user-level sessions. Ensure that SessionTracker.trackSession handles the new userId parameter correctly.

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: 1

🤖 Fix all issues with AI agents
In `@src/repository/_shared/transformers.ts`:
- Around line 27-43: The current transformer collapses undefined into null and
leaves limitConcurrentSessions unparsed; update the mapping for limit5hUsd,
limitWeeklyUsd, limitMonthlyUsd, limitTotalUsd and limitConcurrentSessions to
preserve undefined vs null and to parse numeric strings with a NaN fallback to
null: for each field on dbUser (e.g., dbUser.limit5hUsd, dbUser.limitWeeklyUsd,
dbUser.limitMonthlyUsd, dbUser.limitTotalUsd) return undefined if the property
is strictly === undefined, return null if === null, otherwise parseFloat(value)
and if Number.isNaN(result) return null; apply the same pattern for
limitConcurrentSessions but use Number.parseInt or Number(value) as appropriate
and NaN -> null so strings are not passed through raw.
🧹 Nitpick comments (2)
src/lib/rate-limit/service.ts (1)

971-985: checkRpmLimit 可考虑简化
type 已限定为 "user",当前 if/else 分支实际不可达;可去掉 type 参数或扩展为联合类型并显式处理非 user,减少死代码。

src/repository/_shared/transformers.test.ts (1)

116-119: 修复类型断言问题

静态分析工具指出 User 类型与 Record<string, unknown> 之间的转换存在类型不兼容问题。建议先转换为 unknown 类型。

♻️ 建议的修复
-      ])("$title", ({ field, value, expected }) => {
-        const result = toUser({ ...baseDbUser, [field]: value });
-        expect((result as Record<string, unknown>)[field]).toBe(expected);
+      ])("$title", ({ field, value, expected }) => {
+        const result = toUser({ ...baseDbUser, [field]: value });
+        expect((result as unknown as Record<string, unknown>)[field]).toBe(expected);
       });

});

const syncUserProviderGroupFromKeysMock = vi.fn(async () => undefined);
vi.mock("../../src/actions/users", () => ({
Copy link
Contributor

Choose a reason for hiding this comment

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

[HIGH] [TEST-BRITTLE] Mock path doesn't match imported module

Why this is a problem: This mock targets "../../src/actions/users", which resolves to tests/src/actions/users from this file and does not match the src/actions/users.ts module that @/actions/keys imports. The unit test can end up running the real syncUserProviderGroupFromKeys implementation.

Suggested fix:

vi.mock("@/actions/users", () => ({
  syncUserProviderGroupFromKeys: syncUserProviderGroupFromKeysMock,
}));

Evidence:

  • tests/unit/actions/keys-limit-validation.test.ts:44: vi.mock("../../src/actions/users", ...)
  • CLAUDE.md:126: - **Path alias**: @/maps to./src/``

return {
ok: false,
error: `Key的5小时消费上限(${data.limit5hUsd})不能超过用户限额(${user.limit5hUsd})`,
error: `Key的5小时消费上限(${validatedData.limit5hUsd})不能超过用户限额(${user.limit5hUsd})`,
Copy link
Contributor

Choose a reason for hiding this comment

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

[MEDIUM] [STANDARD-VIOLATION] User-facing error strings are hardcoded (missing i18n)

Why this is a problem: CLAUDE.md:14 requires: 3. **i18n Required** - All user-facing strings must use i18n (5 languages supported). Never hardcode display text. These addKey limit validation failures return hardcoded Chinese strings, so they cannot be localized via next-intl.

Suggested fix:

return {
  ok: false,
  error: tError("KEY_LIMIT_5H_EXCEEDS_USER_LIMIT", {
    keyLimit: String(validatedData.limit5hUsd),
    userLimit: String(user.limit5hUsd),
  }),
};

Add the new translation key(s) to all 5 locales under messages/*/errors.json, and apply the same pattern for daily/weekly/monthly/total/concurrent checks.

Evidence:

  • src/actions/keys.ts:194: error: \Key的5小时消费上限(${validatedData.limit5hUsd})不能超过用户限额(${user.limit5hUsd})`,`
  • src/actions/keys.ts:207, src/actions/keys.ts:220, src/actions/keys.ts:233, src/actions/keys.ts:246, src/actions/keys.ts:259 (same pattern)

@github-actions
Copy link
Contributor

Code Review Summary

This PR adds user-level concurrent-session enforcement and propagates additional user limit fields into the auth/session pipeline. Two issues in the diff should be addressed before merge: a unit test mock path that likely doesn’t apply, and multiple user-facing error strings hardcoded without i18n.

PR Size: L

  • Lines changed: 657
  • Files changed: 13
  • Split suggestions:
    • PR 1: src/lib/session-tracker.ts + session tracking callsites (src/app/v1/_lib/proxy/session-guard.ts, src/app/v1/_lib/proxy/response-handler.ts)
    • PR 2: rate-limit enforcement changes (src/app/v1/_lib/proxy/rate-limit-guard.ts, src/lib/rate-limit/service.ts)
    • PR 3: key/user limit validation + data shaping + tests (src/actions/keys.ts, src/repository/**/*, tests/unit/**/*)

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 0 1 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 1 0 0
Simplification 0 0 0 0

Critical Issues (Must Fix)

None.

High Priority Issues (Should Fix)

  • [HIGH] [TEST-BRITTLE] (Confidence 85) tests/unit/actions/keys-limit-validation.test.ts:44: vi.mock("../../src/actions/users", ...) doesn’t match the module imported by @/actions/keys, so the test can execute the real syncUserProviderGroupFromKeys.
  • [MEDIUM] [STANDARD-VIOLATION] (Confidence 80) src/actions/keys.ts:194 (also src/actions/keys.ts:207, src/actions/keys.ts:220, src/actions/keys.ts:233, src/actions/keys.ts:246, src/actions/keys.ts:259): hardcoded user-facing error strings violate CLAUDE.md:14 i18n rule.

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 adds user-level concurrent-session enforcement and propagates additional user limit fields into the auth/session pipeline. Two issues in the diff should be addressed before merge: a unit test mock path that likely doesn’t apply, and multiple user-facing error strings hardcoded without i18n.

PR Size: L

  • Lines changed: 657
  • Files changed: 13
  • Split suggestions:
    • PR 1: src/lib/session-tracker.ts + session tracking callsites (src/app/v1/_lib/proxy/session-guard.ts, src/app/v1/_lib/proxy/response-handler.ts)
    • PR 2: rate-limit enforcement changes (src/app/v1/_lib/proxy/rate-limit-guard.ts, src/lib/rate-limit/service.ts)
    • PR 3: key/user limit validation + data shaping + tests (src/actions/keys.ts, src/repository/**/*, tests/unit/**/*)

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 0 1 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 1 0 0
Simplification 0 0 0 0

Critical Issues (Must Fix)

None.

High Priority Issues (Should Fix)

  • [HIGH] [TEST-BRITTLE] (Confidence 85) tests/unit/actions/keys-limit-validation.test.ts:44: vi.mock("../../src/actions/users", ...) doesn’t match the module imported by @/actions/keys, so the test can execute the real syncUserProviderGroupFromKeys.
  • [MEDIUM] [STANDARD-VIOLATION] (Confidence 80) src/actions/keys.ts:194 (also src/actions/keys.ts:207, src/actions/keys.ts:220, src/actions/keys.ts:233, src/actions/keys.ts:246, src/actions/keys.ts:259): hardcoded user-facing error strings violate CLAUDE.md:14 i18n rule.

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 #643 (“Fix user rate limit enforcement”), computed size L (607 additions + 50 deletions, 13 files), and applied label size/L.
  • Posted 2 inline review comments (diff-only):
    • [HIGH] [TEST-BRITTLE] wrong vi.mock target in tests/unit/actions/keys-limit-validation.test.ts:44 (mock won’t match the imported module; fix suggested in comment).
    • [MEDIUM] [STANDARD-VIOLATION] hardcoded user-facing error strings in src/actions/keys.ts:194 (also src/actions/keys.ts:207, src/actions/keys.ts:220, src/actions/keys.ts:233, src/actions/keys.ts:246, src/actions/keys.ts:259) violating i18n rule (fix pattern suggested in comment).
  • Submitted the required “Code Review Summary” as a PR review via REST (POST /pulls/643/reviews) since gh pr review (GraphQL) isn’t usable with this token in this environment.

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.

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +11 to +15
const parseOptionalNumber = (value: unknown): number | null | undefined => {
if (value === undefined) return undefined;
if (value === null) return null;
const parsed = Number.parseFloat(String(value));
return Number.isNaN(parsed) ? null : parsed;
Copy link

Choose a reason for hiding this comment

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

Inconsistent handling of 0 values - parseOptionalNumber preserves 0, but existing rpm and dailyQuota transforms convert 0 to null (lines 29-38). This inconsistency could cause issues since the validation logic in src/actions/keys.ts expects 0 to mean "unlimited" and uses > 0 checks to skip validation. If the database stores 0 but it gets converted to null, the validation will behave differently than intended.

Suggested change
const parseOptionalNumber = (value: unknown): number | null | undefined => {
if (value === undefined) return undefined;
if (value === null) return null;
const parsed = Number.parseFloat(String(value));
return Number.isNaN(parsed) ? null : parsed;
const parseOptionalNumber = (value: unknown): number | null | undefined => {
if (value === undefined) return undefined;
if (value === null) return null;
const parsed = Number.parseFloat(String(value));
if (Number.isNaN(parsed)) return null;
return parsed > 0 ? parsed : null; // Convert 0 to null for consistency
};
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/repository/_shared/transformers.ts
Line: 11:15

Comment:
Inconsistent handling of `0` values - `parseOptionalNumber` preserves `0`, but existing `rpm` and `dailyQuota` transforms convert `0` to `null` (lines 29-38). This inconsistency could cause issues since the validation logic in `src/actions/keys.ts` expects `0` to mean "unlimited" and uses `> 0` checks to skip validation. If the database stores `0` but it gets converted to `null`, the validation will behave differently than intended.

```suggestion
  const parseOptionalNumber = (value: unknown): number | null | undefined => {
    if (value === undefined) return undefined;
    if (value === null) return null;
    const parsed = Number.parseFloat(String(value));
    if (Number.isNaN(parsed)) return null;
    return parsed > 0 ? parsed : null;  // Convert 0 to null for consistency
  };
```

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

)

Fixed TypeScript error TS2352 by adding intermediate 'unknown' cast
when converting User type to Record<string, unknown> for dynamic
field access in test assertions.

Error: Conversion of type 'User' to type 'Record<string, unknown>'
may be a mistake because neither type sufficiently overlaps with
the other.

Solution: Added intermediate cast to 'unknown' as suggested by
TypeScript compiler.

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

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
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.

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines 11 to 45
@@ -24,10 +38,11 @@ export function toUser(dbUser: any): User {
})(),
providerGroup: dbUser?.providerGroup ?? null,
tags: dbUser?.tags ?? [],
limitTotalUsd:
dbUser?.limitTotalUsd !== null && dbUser?.limitTotalUsd !== undefined
? parseFloat(dbUser.limitTotalUsd)
: null,
limit5hUsd: parseOptionalNumber(dbUser?.limit5hUsd),
limitWeeklyUsd: parseOptionalNumber(dbUser?.limitWeeklyUsd),
limitMonthlyUsd: parseOptionalNumber(dbUser?.limitMonthlyUsd),
limitTotalUsd: parseOptionalNumber(dbUser?.limitTotalUsd),
limitConcurrentSessions: parseOptionalInteger(dbUser?.limitConcurrentSessions),
Copy link

Choose a reason for hiding this comment

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

parseOptionalNumber preserves 0 but existing rpm/dailyQuota logic converts 0 to null (lines 29-38). This breaks the validation in src/actions/keys.ts which expects 0 to mean "unlimited" and uses > 0 checks. If the database stores 0 for limit5hUsd/limitWeeklyUsd/limitMonthlyUsd but these fields get returned as actual 0 instead of being converted to null like rpm/dailyQuota, validation will fail incorrectly.

Suggested change
const parseOptionalNumber = (value: unknown): number | null | undefined => {
if (value === undefined) return undefined;
if (value === null) return null;
const parsed = Number.parseFloat(String(value));
if (Number.isNaN(parsed)) return null;
// Convert 0 to null for consistency with rpm/dailyQuota (0 = unlimited)
return parsed > 0 ? parsed : null;
};
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/repository/_shared/transformers.ts
Line: 11:45

Comment:
`parseOptionalNumber` preserves `0` but existing `rpm`/`dailyQuota` logic converts `0` to `null` (lines 29-38). This breaks the validation in `src/actions/keys.ts` which expects `0` to mean "unlimited" and uses `> 0` checks. If the database stores `0` for `limit5hUsd`/`limitWeeklyUsd`/`limitMonthlyUsd` but these fields get returned as actual `0` instead of being converted to `null` like `rpm`/`dailyQuota`, validation will fail incorrectly.

```suggestion
  const parseOptionalNumber = (value: unknown): number | null | undefined => {
    if (value === undefined) return undefined;
    if (value === null) return null;
    const parsed = Number.parseFloat(String(value));
    if (Number.isNaN(parsed)) return null;
    // Convert 0 to null for consistency with rpm/dailyQuota (0 = unlimited)
    return parsed > 0 ? parsed : null;
  };
```

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

@ding113 ding113 merged commit 085c86b into dev Jan 22, 2026
8 of 12 checks passed
@github-project-automation github-project-automation bot moved this from Backlog to Done in Claude Code Hub Roadmap Jan 22, 2026
@github-actions github-actions bot mentioned this pull request Jan 25, 2026
6 tasks
@ding113 ding113 deleted the fix/rate-limit-user-limits branch January 27, 2026 09:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core area:Rate Limit area:session bug Something isn't working size/L Large PR (< 1000 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant

Comments