Skip to content

fix(dashboard): harden keyset cursor parsing and timestamp precision#819

Merged
ding113 merged 2 commits into
devfrom
fix/users-page-bugbot-followup
Feb 23, 2026
Merged

fix(dashboard): harden keyset cursor parsing and timestamp precision#819
ding113 merged 2 commits into
devfrom
fix/users-page-bugbot-followup

Conversation

@ding113

@ding113 ding113 commented Feb 23, 2026

Copy link
Copy Markdown
Owner

Summary

Follow-up to #818. Addresses review comments that were pushed after the squash merge to harden the keyset cursor pagination implementation on the users page.

Related Issues/PRs

Problem

The keyset cursor pagination introduced in #818 had several edge cases that needed hardening:

  1. Cursor validation gaps - No size limits on cursor strings, no validation of cursor id values
  2. Timestamp precision mismatch - PostgreSQL microsecond timestamps vs JavaScript millisecond encoding could cause duplicate rows at page boundaries
  3. Invalid cursor handling - Malformed cursors could cause unexpected behavior
  4. Input validation - getUsersUsageBatch didn't filter non-positive user IDs

Solution

Core Changes

  • Cursor parsing hardening (src/repository/user.ts):

    • Reject cursors > 1024 bytes (DoS prevention)
    • Validate cursor id as finite positive integer (reject NaN/negative/zero)
    • Fall back to OFFSET pagination when cursor isn't valid keyset JSON
  • Timestamp precision fix (src/repository/user.ts):

    • Use date_trunc('milliseconds', ...) to align PostgreSQL microsecond timestamps with JS Date millisecond encoding
    • Prevents off-by-microsecond comparison mismatches at page boundaries
  • Input validation (src/actions/users.ts):

    • Filter id <= 0 in getUsersUsageBatch sanitization (in addition to existing non-integer filter)
  • Cursor encoding refactor (src/repository/user.ts):

    • Replace hardcoded name/createdAt ternary with extensible lookup map
    • Improves maintainability for adding future sortable columns

Files Changed

File Changes
src/repository/user.ts Hardened cursor parsing with size/validation checks, timestamp truncation for precision alignment, extensible cursor encoding
src/actions/users.ts Added id > 0 validation to getUsersUsageBatch

Testing

Automated Tests

  • bun run typecheck - PASS
  • bun run test - PASS

Manual Testing

  • Verify keyset pagination works correctly on /dashboard/users
  • Verify page navigation with sort by createdAt (DESC and ASC)
  • Verify sort by name still works
  • Verify invalid/malformed cursor strings don't crash the query
  • Verify no duplicate rows at page boundaries when sorting by timestamp

Checklist

  • Code follows project conventions
  • Self-review completed
  • Tests pass locally
  • No breaking changes (all changes are defensive)

Description enhanced by Claude AI

Greptile Summary

Hardens keyset cursor pagination from #818 with defensive validation and precision fixes. Adds DoS prevention (1024-byte cursor limit), validates cursor IDs as positive integers, and truncates PostgreSQL timestamps to millisecond precision to match JavaScript Date encoding and prevent duplicate rows at page boundaries. Also includes cache function renaming for clarity (clearAllSessionsCacheclearAllSessionsQueryCache).

Key improvements:

  • Cursor size limit prevents DoS attacks via malicious large cursor strings
  • Number.isFinite() and id > 0 checks reject NaN/Infinity/negative/zero IDs
  • date_trunc('milliseconds', ...) aligns PostgreSQL microsecond timestamps with JS millisecond precision
  • Extensible cursor encoding via lookup map improves maintainability
  • Input sanitization filters non-positive user IDs in getUsersUsageBatch

Issue found:

  • Invalid date parsing in keyset cursor silently falls through without offset fallback, causing query to restart from page 1

Confidence Score: 3/5

  • Safe to merge with one edge case that should be addressed
  • The PR significantly hardens cursor validation and fixes timestamp precision issues, but has one logical gap where invalid date strings in cursors cause silent pagination restart instead of proper fallback. The cache renaming is purely mechanical and safe. All tests pass.
  • Pay close attention to src/repository/user.ts - the invalid date handling in keyset cursor parsing should be fixed before merge

Important Files Changed

Filename Overview
src/repository/user.ts Hardened keyset cursor parsing with DoS prevention, input validation, and timestamp precision fixes; missing fallback for invalid date parsing edge case
src/actions/users.ts Added positive integer validation to prevent invalid user IDs in batch operations
src/lib/cache/session-cache.ts Renamed functions for clarity: clearAllSessionsCacheclearAllSessionsQueryCache, clearAllSessionCacheclearAllCaches

Last reviewed commit: 8218507

Address review bot comments:
- parseKeysetCursor: add 1024 byte length limit, validate id is finite
  positive integer, reject NaN/negative/zero
- Keyset fallback: when cursor is not valid JSON but useKeyset is true,
  fall back to offset instead of silently resetting to page 1
- Timestamp precision: use date_trunc('milliseconds', col) in keyset
  comparisons to match JS Date encoding precision and prevent duplicate
  rows at page boundaries
- Cursor encoding: replace hardcoded name/createdAt ternary with
  extensible lookup map
- getUsersUsageBatch: filter negative/zero IDs in addition to non-integers
@gemini-code-assist

Copy link
Copy Markdown
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 improves the robustness and precision of the dashboard's user pagination system. It addresses several edge cases and potential inconsistencies by hardening cursor parsing, ensuring accurate timestamp comparisons, and refining input validation. These changes lead to a more reliable and predictable user experience when navigating user lists, particularly with keyset pagination.

Highlights

  • Cursor Parsing Hardening: Implemented checks to reject keyset cursors exceeding 1024 bytes, validated cursor id as a finite positive integer, and added a fallback to OFFSET pagination when a cursor is not valid keyset JSON.
  • Timestamp Precision Alignment: Adjusted PostgreSQL queries to use date_trunc('milliseconds', ...) for createdAt comparisons, ensuring alignment with JavaScript Date millisecond encoding in keyset pagination.
  • Input Validation: Added a filter to getUsersUsageBatch to sanitize user IDs, excluding any id <= 0.
  • Cursor Encoding Refinement: Refactored the keyset cursor encoding logic to utilize an extensible lookup map instead of a ternary operator for extracting row values.
Changelog
  • src/actions/users.ts
    • Filtered user IDs to ensure they are positive integers in getUsersUsageBatch.
  • src/repository/user.ts
    • Hardened parseKeysetCursor by adding a maximum length check and validating id as a positive integer.
    • Applied date_trunc('milliseconds') to createdAt comparisons in findUserListBatch for consistent timestamp precision.
    • Implemented a fallback to offset-based pagination when a keyset cursor is not valid JSON.
    • Refactored keyset cursor encoding to use a lookup map for improved extensibility.
Activity
  • No human activity has been recorded on this pull request yet.
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

coderabbitai Bot commented Feb 23, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

增强输入验证和光标处理。GetUsersUsageBatch 过滤非正整数 ID;keyset 光标解析添加长度检查和 ID 验证;分页逻辑增加毫秒精度截断和无效光标回退机制。

Changes

Cohort / File(s) Summary
用户操作验证
src/actions/users.ts
GetUsersUsageBatch 增强输入验证,现在对 sanitizedIds 进行过滤,仅保留大于 0 的整数,排除零值或负整数。
数据库仓储层分页
src/repository/user.ts
Keyset 光标解析增强:新增原始字符串长度检查(> 1024 拒绝)和 ID 正整数有限性验证;按 createdAt 排序时引入毫秒精度截断;无效 keyset 光标回退到数值偏移解析;下一光标生成根据当前排序字段动态选择值。

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 分钟

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% 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标题清晰准确地反映了主要变更内容:加强keyset游标解析和时间戳精度改进,是changeset中的核心改动。
Description check ✅ Passed 拉取请求的描述与变更集相关联,详细说明了游标分页的硬化改进、时间戳精度修复和输入验证,与提供的代码变更相符。

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/users-page-bugbot-followup

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 the size/XS Extra Small PR (< 50 lines) label Feb 23, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
src/actions/users.ts (1)

781-783: 过滤非正整数 ID — LGTM,但存在一处不一致。

添加 id > 0 条件与 parseKeysetCursor 中的 ID 验证保持一致,能避免无效 ID 进入下游批量查询。

但同文件中 batchUpdateUsers(第 866 行)的类似逻辑仍仅使用 Number.isInteger(id) 而未加 id > 0 检查:

const requestedIds = Array.from(new Set(params.userIds)).filter((id) => Number.isInteger(id));

虽然零或负 ID 不会匹配到数据库记录(不会造成数据破坏),但建议统一验证逻辑以保持一致性,也能避免不必要的数据库查询。

♻️ 建议统一 ID 过滤逻辑
-    const requestedIds = Array.from(new Set(params.userIds)).filter((id) => Number.isInteger(id));
+    const requestedIds = Array.from(new Set(params.userIds)).filter(
+      (id) => Number.isInteger(id) && id > 0
+    );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/users.ts` around lines 781 - 783, The ID filtering is
inconsistent: you added id > 0 for sanitizedIds but batchUpdateUsers still only
checks Number.isInteger(id); update the requestedIds construction in
batchUpdateUsers to mirror the earlier check (dedupe via Array.from(new
Set(...)) and filter to Number.isInteger(id) && id > 0) so zero/negative IDs are
excluded before any DB queries; reference the requestedIds variable in
batchUpdateUsers and ensure the same validation as sanitizedIds.
src/repository/user.ts (2)

380-384: cursor 编码使用查找映射 — 可扩展性好,但存在轻微的类型安全隐患。

Record<string, unknown>keysetRowValue[sortBy] 代替硬编码三元表达式,增加了可扩展性。但 sortBy 类型是完整的联合类型(含 "tags", "expiresAt" 等),而此映射只包含 "name""createdAt" 两个 key。虽然运行时被 useKeyset 守卫(只有 KEYSET_SORT_COLUMNS 中的才进入此分支),但在类型层面 keysetRowValue[sortBy] 可能为 undefined,静默传入 encodeKeysetCursor 产生 ""

建议用 satisfies 或从 KEYSET_SORT_COLUMNS 提取类型来收窄:

♻️ 可选的类型收窄建议
+type KeysetSortField = "name" | "createdAt";
+
 if (hasMore) {
   if (useKeyset) {
     const lastRow = usersToReturn[usersToReturn.length - 1];
-    const keysetRowValue: Record<string, unknown> = {
+    const keysetRowValue: Record<KeysetSortField, unknown> = {
       name: lastRow.name,
       createdAt: lastRow.createdAt,
     };
-    nextCursor = encodeKeysetCursor(keysetRowValue[sortBy], lastRow.id);
+    nextCursor = encodeKeysetCursor(keysetRowValue[sortBy as KeysetSortField], lastRow.id);
   } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/repository/user.ts` around lines 380 - 384, The mapping keysetRowValue +
access keysetRowValue[sortBy] is type-unsafe because sortBy is a wider union
than the two keys present; update the code around keysetRowValue and
encodeKeysetCursor to use a narrowed type derived from KEYSET_SORT_COLUMNS (or
use TypeScript's satisfies with an explicit mapped type) so that keysetRowValue
only permits the valid sort keys and key access is non-optional, or
alternatively explicitly narrow sortBy (when guarded by useKeyset) before
indexing; ensure the object includes the corresponding property for every
allowed KEYSET_SORT_COLUMNS entry and pass a properly typed value to
encodeKeysetCursor to avoid undefined/empty-string being sent.

310-320: date_trunc('milliseconds', ...) 正确解决了精度不匹配问题,但需注意索引影响。

date_trunc 将 PostgreSQL 微秒级的 created_at 截断至毫秒以匹配 JS Date 编码精度,从而避免分页边界的重复行问题——思路正确。

date_trunc('milliseconds', ${sortColumn}) 会使 idx_users_created_at 索引无法直接用于该 WHERE 子句(函数包裹导致无法走 B-tree 索引)。若用户表规模较大,keyset 查询可能退化为顺序扫描。

如果未来出现性能瓶颈,可考虑以下方案之一:

  1. 创建表达式索引 CREATE INDEX ON users (date_trunc('milliseconds', created_at));
  2. 改用范围条件(col >= truncated_val AND col < truncated_val + interval '1ms')以保留原始索引的可用性。

当前用户管理页面的数据量通常在可接受范围内,仅作提醒。

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

In `@src/repository/user.ts` around lines 310 - 320, The current keyset branch
uses date_trunc('milliseconds', ${sortColumn}) which breaks B-tree use of the
existing idx_users_created_at index; to fix, either add an expression index for
date_trunc('milliseconds', created_at) or avoid the function: compute the
truncated JS Date value d and replace the date_trunc comparison in the ${sortBy}
=== "createdAt" branch with equivalent range predicates (e.g. ${sortColumn} >=
${d} AND ${sortColumn} < (${d} + sql`interval '1ms'`) and tie-break on
${users.id}) so the original index can be used; update the conditions pushed
where date_trunc is used to one of these two approaches.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/actions/users.ts`:
- Around line 781-783: The ID filtering is inconsistent: you added id > 0 for
sanitizedIds but batchUpdateUsers still only checks Number.isInteger(id); update
the requestedIds construction in batchUpdateUsers to mirror the earlier check
(dedupe via Array.from(new Set(...)) and filter to Number.isInteger(id) && id >
0) so zero/negative IDs are excluded before any DB queries; reference the
requestedIds variable in batchUpdateUsers and ensure the same validation as
sanitizedIds.

In `@src/repository/user.ts`:
- Around line 380-384: The mapping keysetRowValue + access
keysetRowValue[sortBy] is type-unsafe because sortBy is a wider union than the
two keys present; update the code around keysetRowValue and encodeKeysetCursor
to use a narrowed type derived from KEYSET_SORT_COLUMNS (or use TypeScript's
satisfies with an explicit mapped type) so that keysetRowValue only permits the
valid sort keys and key access is non-optional, or alternatively explicitly
narrow sortBy (when guarded by useKeyset) before indexing; ensure the object
includes the corresponding property for every allowed KEYSET_SORT_COLUMNS entry
and pass a properly typed value to encodeKeysetCursor to avoid
undefined/empty-string being sent.
- Around line 310-320: The current keyset branch uses date_trunc('milliseconds',
${sortColumn}) which breaks B-tree use of the existing idx_users_created_at
index; to fix, either add an expression index for date_trunc('milliseconds',
created_at) or avoid the function: compute the truncated JS Date value d and
replace the date_trunc comparison in the ${sortBy} === "createdAt" branch with
equivalent range predicates (e.g. ${sortColumn} >= ${d} AND ${sortColumn} <
(${d} + sql`interval '1ms'`) and tie-break on ${users.id}) so the original index
can be used; update the conditions pushed where date_trunc is used to one of
these two approaches.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between 90fbeaf and deef12b.

📒 Files selected for processing (2)
  • src/actions/users.ts
  • src/repository/user.ts

@github-actions github-actions Bot added the bug Something isn't working label Feb 23, 2026

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

Copy link
Copy Markdown
Contributor

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 introduces several valuable improvements to the dashboard's user list functionality, including hardening of keyset cursor parsing with size limits and ID validation, and fixing timestamp precision using date_trunc for keyset pagination on createdAt. Input validation for user IDs and refactoring cursor encoding to use a lookup map also enhance maintainability. However, a medium-severity Denial of Service (DoS) vulnerability has been identified: the fallback mechanism for keyset cursor pagination treats invalid cursors as numeric offsets without upper-bound limits, allowing an authenticated administrator to cause excessive database load. A suggestion has also been made to simplify cursor parsing logic by removing a redundant check.

Comment thread src/repository/user.ts
}
} else {
// Cursor is not valid keyset JSON -- fall back to offset
offset = Math.max(Number(cursor) || 0, 0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

security-medium medium

In the findUserListBatch function, when a cursor is provided but is not a valid keyset JSON, the code falls back to treating the cursor as a numeric offset. The line offset = Math.max(Number(cursor) || 0, 0); converts the user-provided cursor string to a number without any upper-bound validation. An authenticated administrator could exploit this by providing a very large number (e.g., 9999999999) as the cursor, which would be passed directly to the database query's OFFSET clause. A large offset can cause severe performance degradation or a denial of service by forcing the database to scan and discard a massive number of rows.

      const MAX_OFFSET = 10000; // Define a reasonable upper limit for the offset
      // Cursor is not valid keyset JSON -- fall back to offset
      offset = Math.min(Math.max(Number(cursor) || 0, 0), MAX_OFFSET);

Comment thread src/repository/user.ts
if (typeof parsed === "object" && parsed !== null && "v" in parsed && "id" in parsed) {
return { v: String(parsed.v), id: Number(parsed.id) };
const id = Number(parsed.id);
if (!Number.isFinite(id) || !Number.isInteger(id) || id <= 0) return null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The !Number.isFinite(id) check is redundant here. The Number.isInteger() method returns false for non-finite values such as Infinity, -Infinity, and NaN. Therefore, !Number.isInteger(id) already covers the check for finiteness, making the explicit !Number.isFinite(id) check unnecessary. Removing it simplifies the condition.

      if (!Number.isInteger(id) || id <= 0) return null;

…sionCache

- clearAllSessionsCache -> clearAllSessionsQueryCache (deletes "all_sessions" key only)
- clearAllSessionCache -> clearAllCaches (clears both cache instances entirely)

Update all call sites and test references.
@ding113
ding113 merged commit 9535276 into dev Feb 23, 2026
7 of 8 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Claude Code Hub Roadmap Feb 23, 2026

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

6 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment thread src/repository/user.ts
Comment on lines 310 to 321
if (sortBy === "createdAt") {
const d = new Date(keyset.v);
if (!Number.isNaN(d.getTime())) {
const truncCol = sql`date_trunc('milliseconds', ${sortColumn})`;
if (sortOrder === "asc") {
conditions.push(sql`(${sortColumn}, ${users.id}) > (${d}, ${keyset.id})`);
conditions.push(sql`(${truncCol}, ${users.id}) > (${d}, ${keyset.id})`);
} else {
conditions.push(
sql`(${sortColumn} < ${d} OR (${sortColumn} = ${d} AND ${users.id} > ${keyset.id}))`
sql`(${truncCol} < ${d} OR (${truncCol} = ${d} AND ${users.id} > ${keyset.id}))`
);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

if keyset.v contains an invalid date string (e.g. corrupted cursor), the Number.isNaN(d.getTime()) check fails silently and no condition is added. this causes the query to fetch from the beginning instead of the intended page. consider falling back to offset or returning early:

Suggested change
if (sortBy === "createdAt") {
const d = new Date(keyset.v);
if (!Number.isNaN(d.getTime())) {
const truncCol = sql`date_trunc('milliseconds', ${sortColumn})`;
if (sortOrder === "asc") {
conditions.push(sql`(${sortColumn}, ${users.id}) > (${d}, ${keyset.id})`);
conditions.push(sql`(${truncCol}, ${users.id}) > (${d}, ${keyset.id})`);
} else {
conditions.push(
sql`(${sortColumn} < ${d} OR (${sortColumn} = ${d} AND ${users.id} > ${keyset.id}))`
sql`(${truncCol} < ${d} OR (${truncCol} = ${d} AND ${users.id} > ${keyset.id}))`
);
}
}
if (sortBy === "createdAt") {
const d = new Date(keyset.v);
if (Number.isNaN(d.getTime())) {
// Invalid date in cursor - fall back to offset
offset = Math.max(Number(cursor) || 0, 0);
} else {
const truncCol = sql`date_trunc('milliseconds', ${sortColumn})`;
if (sortOrder === "asc") {
conditions.push(sql`(${truncCol}, ${users.id}) > (${d}, ${keyset.id})`);
} else {
conditions.push(
sql`(${truncCol} < ${d} OR (${truncCol} = ${d} AND ${users.id} > ${keyset.id}))`
);
}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/repository/user.ts
Line: 310-321

Comment:
if `keyset.v` contains an invalid date string (e.g. corrupted cursor), the `Number.isNaN(d.getTime())` check fails silently and no condition is added. this causes the query to fetch from the beginning instead of the intended page. consider falling back to offset or returning early:

```suggestion
      if (sortBy === "createdAt") {
        const d = new Date(keyset.v);
        if (Number.isNaN(d.getTime())) {
          // Invalid date in cursor - fall back to offset
          offset = Math.max(Number(cursor) || 0, 0);
        } else {
          const truncCol = sql`date_trunc('milliseconds', ${sortColumn})`;
          if (sortOrder === "asc") {
            conditions.push(sql`(${truncCol}, ${users.id}) > (${d}, ${keyset.id})`);
          } else {
            conditions.push(
              sql`(${truncCol} < ${d} OR (${truncCol} = ${d} AND ${users.id} > ${keyset.id}))`
            );
          }
        }
```

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

@@ -183,7 +183,7 @@ export function clearActiveSessionsCache() {
/**
* 清空所有 Sessions 的缓存(包括活跃和非活跃)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Medium] [COMMENT-INACCURATE] Comment does not match function behavior

Why this is a problem: The comment states the function clears all sessions cache including active and inactive, but the function only deletes the 'all_sessions' key. This is misleading and could cause developers to misunderstand the function's scope.

Suggested fix:

/**
 * 清空所有 Sessions 查询缓存
 */
export function clearAllSessionsQueryCache() {
  activeSessionsCache.delete("all_sessions");
}

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

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 hardens keyset cursor pagination and renames ambiguous cache functions. The changes are defensive improvements that add validation, handle edge cases gracefully, and fix timestamp precision issues. The refactoring is safe and maintains backward compatibility.

PR Size: XS

  • Lines changed: 55 (34 additions, 21 deletions)
  • Files changed: 6

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 1 0
Tests 0 0 0 0
Simplification 0 0 0 0

Medium Priority Issues (Should Fix)

[COMMENT-INACCURATE] src/lib/cache/session-cache.ts:184

  • The comment "清空所有 Sessions 的缓存(包括活跃和非活跃)" does not match the function behavior
  • The function only deletes the "all_sessions" key, not all active and inactive sessions
  • Suggested fix: Update comment to "清空所有 Sessions 查询缓存"

Positive Findings

  1. Cursor validation is robust: The 1024-byte limit prevents DoS, and the finite positive integer validation for cursor IDs prevents malformed input issues.

  2. Timestamp precision fix is correct: Using date_trunc('milliseconds', ...) properly aligns PostgreSQL microsecond precision with JavaScript Date millisecond encoding, preventing duplicate rows at page boundaries.

  3. Fallback handling is graceful: When keyset cursor parsing fails, the code correctly falls back to offset pagination instead of silently resetting to page 1.

  4. Function renames improve clarity: The new names clearAllSessionsQueryCache and clearAllCaches are more descriptive than the previous ambiguous names.

  5. Input validation is thorough: The addition of id > 0 validation in getUsersUsageBatch properly filters out invalid user IDs.

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean
  • Error handling - Clean
  • Type safety - Clean
  • Documentation accuracy - 1 minor issue
  • Test coverage - Adequate (test mocks updated)
  • Code clarity - Good

Automated review by Claude AI

Comment thread src/repository/user.ts
}
} else {
// Cursor is not valid keyset JSON -- fall back to offset
offset = Math.max(Number(cursor) || 0, 0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[HIGH] [LOGIC-BUG] Offset fallback accepts Infinity/non-integers and can break the query

Why this is a problem: Number(cursor) accepts scientific notation and can return Infinity (e.g. cursor="1e309") or non-integers. That value then flows into query.offset(offset), which can generate invalid SQL/params and crash pagination for malformed cursors.

Suggested fix:

function parseOffsetCursor(raw: string): number {
  if (raw.length > 32) return 0;

  const parsed = Number.parseInt(raw, 10);
  if (!Number.isFinite(parsed) || !Number.isSafeInteger(parsed) || parsed < 0) return 0;

  return parsed;
}

// ...
offset = parseOffsetCursor(cursor);

Comment thread src/repository/user.ts
if (sortBy === "createdAt") {
const d = new Date(keyset.v);
if (!Number.isNaN(d.getTime())) {
const truncCol = sql`date_trunc('milliseconds', ${sortColumn})`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[HIGH] [LOGIC-BUG] Keyset predicate truncates createdAt but ORDER BY still uses microsecond precision

Why this is a problem: Keyset pagination requires the seek predicate and ORDER BY to use the same sort keys. Here the predicate switches to date_trunc('milliseconds', created_at) (millisecond buckets) while orderByClause remains users.createdAt (microseconds). If multiple rows share the same millisecond but differ in microseconds, the query can return rows in an order the cursor cannot represent, which can lead to skipped rows across page boundaries.

Suggested fix (use the same truncCol in both the predicate and ORDER BY when sortBy === "createdAt"):

const keysetSortColumn =
  sortBy === "createdAt" ? sql`date_trunc('milliseconds', ${sortColumn})` : sortColumn;

const orderByClause = sortOrder === "asc" ? asc(keysetSortColumn) : sql`${keysetSortColumn} DESC`;

// ... and reuse keysetSortColumn in the keyset predicate
conditions.push(sql`(${keysetSortColumn}, ${users.id}) > (${d}, ${keyset.id})`);

Comment thread src/repository/user.ts
}

function parseKeysetCursor(raw: string): KeysetCursor | null {
if (raw.length > 1024) return null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[HIGH] [TEST-MISSING-CRITICAL] Cursor hardening + keyset pagination changes are not covered by unit tests

Why this is a problem: This PR changes pagination behavior (cursor length cap, id validation, createdAt truncation, keyset-to-offset fallback) but adds no tests. This violates the repo guideline: CLAUDE.md: "Test Coverage - All new features must have unit test coverage of at least 80%".

Suggested fix (extract pure cursor helpers + add fast unit tests):

// src/repository/_lib/keyset-cursor.ts
export interface KeysetCursor {
  v: string;
  id: number;
}

export function parseKeysetCursor(raw: string): KeysetCursor | null {
  if (raw.length > 1024) return null;
  try {
    const parsed = JSON.parse(raw);
    if (typeof parsed === "object" && parsed !== null && "v" in parsed && "id" in parsed) {
      const id = Number((parsed as { id: unknown }).id);
      if (!Number.isFinite(id) || !Number.isInteger(id) || id <= 0) return null;
      return { v: String((parsed as { v: unknown }).v), id };
    }
  } catch {
    // not JSON
  }
  return null;
}
// tests/unit/repository/keyset-cursor.test.ts
import { describe, expect, it } from "vitest";
import { parseKeysetCursor } from "@/repository/_lib/keyset-cursor";

describe("parseKeysetCursor", () => {
  it("rejects oversize cursors", () => {
    expect(parseKeysetCursor("x".repeat(1025))).toBeNull();
  });

  it("rejects non-positive ids", () => {
    expect(parseKeysetCursor(JSON.stringify({ v: "a", id: 0 }))).toBeNull();
  });
});

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

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 changes cursor parsing and keyset pagination for the users list by adding cursor hard limits, stricter id validation, millisecond truncation for createdAt, and keyset-to-offset fallback. There are correctness risks around offset cursor parsing and createdAt keyset ordering, and the new behavior is not covered by unit tests.

PR Size: XS

  • Lines changed: 25
  • Files changed: 2

Issues Found

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

Critical Issues (Must Fix)

None.

High Priority Issues (Should Fix)

  • src/repository/user.ts:333: offset = Math.max(Number(cursor) || 0, 0); can produce Infinity/non-integers; validate the offset cursor before calling .offset().
  • src/repository/user.ts:313: keyset predicate truncates createdAt to milliseconds while ORDER BY still uses microsecond precision; align ORDER BY with the keyset predicate to avoid skipped rows across pages.
  • src/repository/user.ts:164: cursor hardening + pagination behavior changes landed without tests; add unit coverage per CLAUDE.md: "Test Coverage - All new features must have unit test coverage of at least 80%".

Review Coverage

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

Automated review by Codex AI

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

  • Reviewed PR #819 and gathered diff/metadata (25 lines changed across 2 files)
  • Applied PR label: size/XS
  • Posted 3 inline review comments (all on diff lines) in src/repository/user.ts:
    • src/repository/user.ts:333 [HIGH] [LOGIC-BUG] offset fallback can yield Infinity/non-integers (Number(cursor)) and break .offset()
    • src/repository/user.ts:313 [HIGH] [LOGIC-BUG] createdAt keyset predicate uses date_trunc(...) but ORDER BY still uses microsecond precision (can skip rows)
    • src/repository/user.ts:164 [HIGH] [TEST-MISSING-CRITICAL] cursor hardening/pagination changes landed without unit tests (per CLAUDE.md coverage rule)
  • Submitted the required “Code Review Summary” via gh pr review --comment

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

Labels

bug Something isn't working size/XS Extra Small PR (< 50 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant