Skip to content

feat(security): P1 Security Foundation - JTI, Rate Limiting, Timing-Safe - #167

Merged
2witstudios merged 25 commits into
masterfrom
feat/p1-security-foundation
Jan 8, 2026
Merged

2witstudios merged 25 commits into
masterfrom
feat/p1-security-foundation

Conversation

@2witstudios

@2witstudios 2witstudios commented Jan 6, 2026 •

Copy link
Copy Markdown
Owner

Summary

Implements P1 Security Foundation with three parallel security improvements:

  • P1-T1: JTI Integration - Wire JTI (JWT ID) tracking into service token lifecycle for token revocation support
  • P1-T5: Distributed Rate Limiting - Replace instance-local rate limiting with Redis-based distributed rate limiting across all auth routes
  • P1-T6: Timing-Safe Comparison - Fix timing attack vulnerability by replacing === with crypto.timingSafeEqual() for device token comparisons

Changes

  • packages/lib/src/services/service-auth.ts - Added JTI recording on token creation, JTI validation on verification with fail-closed in production
  • packages/lib/src/auth/secure-compare.ts - New timing-safe string comparison utility
  • apps/web/src/app/api/auth/login/route.ts - Added distributed rate limiting with X-RateLimit headers
  • apps/web/src/app/api/auth/signup/route.ts - Added distributed rate limiting
  • apps/web/src/app/api/auth/refresh/route.ts - Added distributed rate limiting (IP-only)
  • apps/web/src/app/api/auth/mobile/login/route.ts - Added distributed rate limiting
  • apps/web/src/app/api/account/devices/route.ts - Fixed timing attack with secureCompare
  • apps/web/src/app/api/account/devices/[deviceId]/route.ts - Fixed timing attack with secureCompare

Test Coverage

  • 20 tests for JTI integration (service-auth.test.ts)
  • 28 tests for timing-safe comparison (secure-compare.test.ts)
  • 22 new tests for distributed rate limiting across auth routes
  • 108 total auth tests passing

Test plan

  • All 108 auth tests pass
  • All 20 JTI integration tests pass
  • All 28 secure-compare tests pass
  • Typecheck passes
  • Verified no === token comparisons remain in auth paths via grep

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Global distributed rate limiting with Retry-After and X-RateLimit headers across auth flows; client IP extraction standardized.
    • JTI tracking for service tokens and token-hash migration tooling.
    • Load-test workflow and supporting seed/migration scripts.
  • Security

    • Timing-safe token comparison added.
    • CSRF validation strengthened and safer fail-closed behavior when security backends are unavailable.
  • Tests

    • Expanded and migrated tests to cover distributed rate limiting, secure-compare, and JTI behavior.

✏️ Tip: You can customize this high-level summary in your review settings.

## P1-T1: JTI Integration
- Wire JTI recording into createServiceToken()
- Wire JTI validation into verifyServiceToken()
- Fail-closed in production when Redis unavailable
- Graceful degradation in dev/test environments
- Add 20 tests for service token JTI lifecycle

## P1-T5: Distributed Rate Limiting
- Add checkDistributedRateLimit to all 4 auth routes:
  - login/route.ts (IP + email)
  - signup/route.ts (IP + email)
  - refresh/route.ts (IP only)
  - mobile/login/route.ts (IP + email)
- Add X-RateLimit-* headers to all responses
- Reset distributed rate limits on successful auth
- Add 22 new tests across auth test files

## P1-T6: Timing-Safe Device Token Comparison
- Create secureCompare() utility using crypto.timingSafeEqual
- Fix devices/route.ts to use secureCompare
- Fix devices/[deviceId]/route.ts to use secureCompare
- Add 28 tests for secure comparison

Total: 70 new tests, 108 auth tests passing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jan 6, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Introduces timing-safe string comparison, separates Redis clients for sessions and rate-limiting, migrates many auth routes/tests to distributed rate limiting with Redis, adds JTI tracking and token-hash migration, centralizes client IP extraction, and updates test/config and load-test infrastructure.

Changes

Cohort / File(s) Summary
Timing-safe compare
packages/lib/src/auth/secure-compare.ts, packages/lib/src/__tests__/secure-compare.test.ts, packages/lib/src/auth/index.ts, apps/web/src/app/api/account/devices/route.ts, apps/web/src/app/api/account/devices/[deviceId]/route.ts
New secureCompare util using crypto.timingSafeEqual; device endpoints switched from === to timing-safe compare. Tests added covering edge cases.
Distributed rate limiting core & Redis split
packages/lib/src/security/distributed-rate-limit.ts, packages/lib/src/security/security-redis.ts, packages/lib/src/security/..., packages/lib/package.json
New distributed rate-limit implementation; separate Redis clients for sessions vs rate-limits (REDIS_SESSION_URL, REDIS_RATE_LIMIT_URL); production fail-closed semantics; added OAUTH_VERIFY limit; package exports updated.
Auth routes — rate-limit migration & IP helper
apps/web/src/app/api/auth/*/route.ts, apps/web/src/app/api/auth/mobile/*/route.ts, apps/web/src/app/api/auth/device/refresh/route.ts
Replaced in-process rate limiting with checkDistributedRateLimit/resetDistributedRateLimit and added X-RateLimit-* headers, Retry-After handling, parallel resets via Promise.allSettled, and getClientIP usage.
Auth tests — rate-limit mocks update
apps/web/src/app/api/auth/__tests__/*, apps/web/src/app/api/auth/__tests__/login.test.ts, ...mobile-login.test.ts
Tests updated to mock @pagespace/lib/security (checkDistributedRateLimit, resetDistributedRateLimit, DISTRIBUTED_RATE_LIMITS) and assert distributed key formats, 429/headers, and reset behavior. Legacy rate-limit mocks removed.
JTI / service tokens
packages/lib/src/services/service-auth.ts, packages/lib/src/services/__tests__/service-auth.test.ts
Service token creation now generates and records JTI in Redis; verification consults JTI revocation; duration parsing and caps added; production fail-closed for Redis unavailability; tests added.
Token-hash migration
packages/db/src/schema/auth.ts, packages/db/drizzle/0033_furry_vampiro.sql, packages/db/drizzle/meta/_journal.json, scripts/migrate-token-hashes.ts
Added tokenHash/tokenPrefix columns and partial unique indexes; migration script to compute/store hashes (dry-run, batching, progress).
Auth helpers / exports
apps/web/src/lib/auth/auth-helpers.ts, apps/web/src/lib/auth/index.ts, apps/web/src/app/api/auth/logout/route.ts
Added exported getClientIP utility; logout and other routes use it; auth index re-exports CSRF validator.
Test & vitest config
apps/web/vitest.config.ts, apps/web/src/test/setup.ts, packages/lib/src/test/setup.ts, packages/lib/src/test/setup.ts
Vitest aliases for @pagespace/lib/security and @pagespace/lib/secure-compare; test env var handling updated to derive REDIS_SESSION_URL/REDIS_RATE_LIMIT_URL when needed.
Security tests & mocks
packages/lib/src/security/__tests__/*, packages/lib/src/security/__tests__/security-redis.test.ts, distributed-rate-limit.test.ts
Shared mock Redis instance and updated tests to use tryGetRateLimitRedisClient and new fail-closed semantics; shared-state mock adjustments.
Load testing & infra
.github/workflows/load-test.yml, tests/load/auth-baseline.k6.js, scripts/seed-loadtest-user.ts, tests/load/results/.gitkeep, .gitignore
Added weekly k6 load-test workflow, enhanced k6 script (CSRF, cookie handling, metrics), seed script for load-test user, and artifacts gitignore.
Docker / env / workflows
docker-compose.yml, docker-compose.dev.yml, docker-compose.security.yml, .env.example, .github/workflows/test.yml, turbo.json
Added redis-sessions service and env vars for session/rate-limit Redis; updated test workflow and example env.
Plan & docs
plan.md
Expanded lessons-learned, migration/runbook notes, CSP groundwork, and security rollout phases.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant AuthRoute as Auth Route
    participant DistributedRL as Distributed Rate Limit
    participant RLRedis as RateLimit Redis
    participant AuthLogic as Auth Logic
    participant SessionRedis as Session Redis

    Client->>AuthRoute: POST /auth/login (email,password)
    AuthRoute->>DistributedRL: checkDistributedRateLimit(login:ip:<IP>)
    DistributedRL->>RLRedis: check/increment key
    RLRedis-->>DistributedRL: allowed/denied + retryAfter

    alt denied
        DistributedRL-->>AuthRoute: denied
        AuthRoute-->>Client: 429 + Retry-After + X-RateLimit headers
    else allowed
        AuthRoute->>DistributedRL: checkDistributedRateLimit(login:email:<email>)
        DistributedRL->>RLRedis: check/increment email key
        RLRedis-->>DistributedRL: allowed/denied

        alt denied
            DistributedRL-->>AuthRoute: denied
            AuthRoute-->>Client: 429 + headers
        else allowed
            AuthRoute->>AuthLogic: validate credentials
            AuthLogic-->>AuthRoute: success + tokens
            AuthRoute->>SessionRedis: store session/refresh data
            AuthRoute->>DistributedRL: resetDistributedRateLimit(login:ip, login:email)
            AuthRoute-->>Client: 200 + Set-Cookie + X-RateLimit headers
        end
    end
Loading
sequenceDiagram
    participant Issuer as Service Token Issuer
    participant ServiceAuth as Service Auth
    participant JtiRedis as Session Redis (JTI)
    participant Verifier as Service Verifier

    Issuer->>ServiceAuth: createServiceToken(params)
    ServiceAuth->>ServiceAuth: generate jti (cuid2)
    ServiceAuth->>JtiRedis: recordJTI(jti, ttl)
    alt redis ok (prod)
        JtiRedis-->>ServiceAuth: recorded
        ServiceAuth-->>Issuer: token (with jti)
    else redis error (prod)
        JtiRedis-->>ServiceAuth: error
        ServiceAuth-->>Issuer: error (fail-closed)
    else redis error (dev)
        ServiceAuth-->>Issuer: token (logs warning)
    end

    Verifier->>ServiceAuth: verify(token)
    ServiceAuth->>JtiRedis: isJTIRevoked(jti)
    alt not revoked
        JtiRedis-->>ServiceAuth: ok
        ServiceAuth-->>Verifier: valid
    else revoked
        JtiRedis-->>ServiceAuth: revoked
        ServiceAuth-->>Verifier: invalid
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • PageSpace#159: Overlapping work introducing distributed rate limiting, Redis client separation, token hashing/migrations, and JTI tracking across auth/security code paths.
  • PageSpace#158: Adds timing-safe secureCompare utility and related usages—directly related to secure-compare additions here.
  • PageSpace#160: Adds secureCompare usage and integrates distributed security APIs used throughout the auth routes changed in this PR.

Poem

🐰
I hopped through code at break of night,
Split Redis keys to set things right,
JTIs hidden, tokens safe and neat,
Rate limits hum in steady beat.
A tiny rabbit guards the stack—secure and bright!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.00% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: three P1 security improvements (JTI, Distributed Rate Limiting, and Timing-Safe comparison) form the core of this changeset.

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


📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 92399ce and 406e8ae.

📒 Files selected for processing (1)
  • .github/workflows/load-test.yml
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Run `pnpm build`, `pnpm typecheck`, and relevant `db:*` tasks before opening a pull request

Applied to files:

  • .github/workflows/load-test.yml
🪛 Checkov (3.2.334)
.github/workflows/load-test.yml

[medium] 70-71: Basic Auth Credentials

(CKV_SECRET_4)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Unit Tests
🔇 Additional comments (4)
.github/workflows/load-test.yml (4)

70-71: Static analysis false positive for test credentials.

The static analysis tool flagged the DATABASE_URL for embedded credentials. This is a false positive—the connection string uses standard test credentials (postgres/postgres) for an ephemeral PostgreSQL service that only exists during the CI workflow run. This is acceptable practice for test environments.


1-124: Well-structured load testing workflow.

The workflow follows GitHub Actions best practices:

  • Service containers (PostgreSQL, Redis) with proper health checks ensure dependencies are ready before tests run
  • Health check loop (lines 88-101) robustly waits for server readiness with timeout handling
  • Manual trigger with configurable parameters allows flexible testing
  • Artifact upload with if: always() ensures results are captured even on test failures

The configuration aligns well with the PR's distributed rate limiting and Redis-backed authentication features.


76-83: No action needed. The load test (tests/load/auth-baseline.k6.js) only exercises email/password authentication flows (login-csrf, login, token validation, and refresh), not OAuth endpoints. OAuth environment variables are not required for this test.

Likely an incorrect or invalid review comment.


103-116: Add environment variables for test user credentials to the k6 step.

The k6 script exists and correctly outputs results to tests/load/results/baseline-latest.json via its handleSummary function, so the results directory concern is invalid. However, the workflow doesn't pass TEST_EMAIL and TEST_PASSWORD to the k6 container. The script will fall back to defaults (loadtest@example.com / LoadTest123!), but you should explicitly set these in the workflow env block to match the test user created during database migration, or ensure the default test user is seeded in the database.

Likely an incorrect or invalid review comment.


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.

@coderabbitai coderabbitai 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.

Actionable comments posted: 0

🧹 Nitpick comments (5)
packages/lib/src/auth/secure-compare.ts (1)

25-42: LGTM! Sound timing-safe implementation for token comparison.

The implementation correctly uses crypto.timingSafeEqual and employs the self-comparison technique on Line 37 to maintain constant execution time even when lengths differ, preventing timing leaks from the comparison operation itself.

Minor consideration: The length check on Line 35 (bufA.length !== bufB.length) is not itself constant-time, which could theoretically leak length information through timing. However, this is an acceptable trade-off because:

  1. Perfect constant-time comparison across all possible lengths requires padding, which adds complexity
  2. The primary use case (token comparison) involves fixed-length tokens
  3. The implementation prevents the more critical timing leak from the comparison operation itself
Optional: Document length-timing consideration

If you want to be explicit about this limitation, consider adding a note to the JSDoc:

  * Timing-safe comparison of secret values to prevent timing attacks.
  *
  * Uses crypto.timingSafeEqual under the hood. When lengths differ,
  * performs a constant-time comparison against self to avoid leaking
  * length information through timing.
+ *
+ * Note: The length check itself is not constant-time, so length differences
+ * may still be detectable through timing in adversarial scenarios. This is
+ * acceptable for fixed-length tokens (the primary use case).
  *
packages/lib/src/services/service-auth.ts (2)

51-74: Duration parsing is functional but could be more robust.

The durationToSeconds function handles common units well. Consider these edge cases:

  1. Leading zeros (e.g., '007d') - currently works due to parseInt
  2. Invalid numeric values (e.g., '0m', negative values) - returns 0 or negative seconds
  3. Mixed case units not supported (e.g., '5M') - returns default

For a security-critical expiration, consider adding validation:

🔎 Optional: Add validation for edge cases
 function durationToSeconds(duration: string): number {
   const match = duration.match(/^(\d+)([smhd])$/);
   if (!match) return DEFAULT_SERVICE_TOKEN_EXPIRY_SECONDS;
 
   const value = parseInt(match[1], 10);
   const unit = match[2];
+
+  if (value <= 0 || !Number.isFinite(value)) {
+    return DEFAULT_SERVICE_TOKEN_EXPIRY_SECONDS;
+  }
 
   switch (unit) {

146-155: Consider adding observability for Redis failures during token creation.

The empty catch block silently swallows Redis errors during JTI recording. While graceful degradation is correct, adding logging would help with debugging and monitoring.

🔎 Add logging for Redis errors
   // Record JTI in Redis for tracking/revocation (graceful degradation)
   try {
     const redis = await tryGetSecurityRedisClient();
     if (redis) {
       await recordJTI(jti, options.subject, expiresInSeconds);
     }
-  } catch {
-    // Log but don't fail token creation - graceful degradation
+  } catch (error) {
+    // Log but don't fail token creation - graceful degradation
+    console.warn('Failed to record JTI in Redis:', error);
   }
apps/web/src/app/api/auth/signup/route.ts (1)

149-193: Consider parallelizing distributed rate limit checks for better latency.

The two checkDistributedRateLimit calls are independent and could be executed concurrently. While signup is less latency-sensitive than login, this would reduce Redis round-trip time.

🔎 Optional optimization using Promise.all
     // Distributed rate limiting (P1-T5)
-    const distributedIpLimit = await checkDistributedRateLimit(
-      `signup:ip:${clientIP}`,
-      DISTRIBUTED_RATE_LIMITS.SIGNUP
-    );
-    const distributedEmailLimit = await checkDistributedRateLimit(
-      `signup:email:${email.toLowerCase()}`,
-      DISTRIBUTED_RATE_LIMITS.SIGNUP
-    );
+    const [distributedIpLimit, distributedEmailLimit] = await Promise.all([
+      checkDistributedRateLimit(`signup:ip:${clientIP}`, DISTRIBUTED_RATE_LIMITS.SIGNUP),
+      checkDistributedRateLimit(`signup:email:${email.toLowerCase()}`, DISTRIBUTED_RATE_LIMITS.SIGNUP),
+    ]);
apps/web/src/app/api/auth/login/route.ts (1)

136-178: Consider parallelizing distributed rate limit checks.

Same optimization opportunity as the signup route - the two checkDistributedRateLimit calls are independent.

🔎 Optional optimization using Promise.all
     // Distributed rate limiting (P1-T5)
-    const distributedIpLimit = await checkDistributedRateLimit(
-      `login:ip:${clientIP}`,
-      DISTRIBUTED_RATE_LIMITS.LOGIN
-    );
-    const distributedEmailLimit = await checkDistributedRateLimit(
-      `login:email:${email.toLowerCase()}`,
-      DISTRIBUTED_RATE_LIMITS.LOGIN
-    );
+    const [distributedIpLimit, distributedEmailLimit] = await Promise.all([
+      checkDistributedRateLimit(`login:ip:${clientIP}`, DISTRIBUTED_RATE_LIMITS.LOGIN),
+      checkDistributedRateLimit(`login:email:${email.toLowerCase()}`, DISTRIBUTED_RATE_LIMITS.LOGIN),
+    ]);
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 935ea45 and 1da154e.

📒 Files selected for processing (17)
  • apps/web/src/app/api/account/devices/[deviceId]/route.ts
  • apps/web/src/app/api/account/devices/route.ts
  • apps/web/src/app/api/auth/__tests__/login.test.ts
  • apps/web/src/app/api/auth/__tests__/mobile-login.test.ts
  • apps/web/src/app/api/auth/__tests__/refresh.test.ts
  • apps/web/src/app/api/auth/__tests__/signup.test.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/vitest.config.ts
  • packages/lib/package.json
  • packages/lib/src/__tests__/secure-compare.test.ts
  • packages/lib/src/auth/index.ts
  • packages/lib/src/auth/secure-compare.ts
  • packages/lib/src/services/__tests__/service-auth.test.ts
  • packages/lib/src/services/service-auth.ts
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • packages/lib/package.json
  • apps/web/src/app/api/account/devices/[deviceId]/route.ts
  • apps/web/src/app/api/auth/__tests__/login.test.ts
  • apps/web/src/app/api/auth/__tests__/signup.test.ts
  • apps/web/src/app/api/auth/signup/route.ts
  • packages/lib/src/services/__tests__/service-auth.test.ts
  • packages/lib/src/__tests__/secure-compare.test.ts
  • packages/lib/src/auth/index.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/__tests__/mobile-login.test.ts
  • apps/web/src/app/api/account/devices/route.ts
  • apps/web/vitest.config.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/__tests__/refresh.test.ts
  • packages/lib/src/auth/secure-compare.ts
  • packages/lib/src/services/service-auth.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
apps/web/src/app/**/route.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/src/app/**/route.{ts,tsx}: In Next.js 15 route handlers, params in dynamic routes are Promise objects and MUST be awaited before destructuring
Use Response.json() or NextResponse.json() for returning JSON from route handlers
Get request body using const body = await request.json();
Get search parameters using const { searchParams } = new URL(request.url);

apps/web/src/app/**/route.{ts,tsx}: In Next.js 15 dynamic routes, params are Promise objects and MUST be awaited before destructuring: const { id } = await context.params;
In Route Handlers, get request body with const body = await request.json();
In Route Handlers, get search parameters with const { searchParams } = new URL(request.url);
In Route Handlers, return JSON using Response.json(data) or NextResponse.json(data)
For permission logic, use centralized functions from @pagespace/lib/permissions: getUserAccessLevel(), canUserEditPage()

Files:

  • apps/web/src/app/api/account/devices/[deviceId]/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/account/devices/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Use camelCase for variable and function names
Use UPPER_SNAKE_CASE for constants
Use PascalCase for type and enum names
Use kebab-case for filenames, except React hooks (camelCase with use prefix), Zustand stores (camelCase with use prefix), and React components (PascalCase)
Lint with Next/ESLint as configured in apps/web/eslint.config.mjs
Message content should always use the message parts structure with { parts: [{ type: 'text', text: '...' }] }
Use centralized permission functions from @pagespace/lib/permissions (e.g., getUserAccessLevel, canUserEditPage) instead of implementing permission logic locally
Always use Drizzle client from @pagespace/db package for database access
Use ESM modules throughout the codebase

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Write code that is explicit over implicit and self-documenting

Files:

  • apps/web/src/app/api/account/devices/[deviceId]/route.ts
  • apps/web/src/app/api/auth/__tests__/login.test.ts
  • apps/web/src/app/api/auth/__tests__/signup.test.ts
  • apps/web/src/app/api/auth/signup/route.ts
  • packages/lib/src/services/__tests__/service-auth.test.ts
  • packages/lib/src/__tests__/secure-compare.test.ts
  • packages/lib/src/auth/index.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/__tests__/mobile-login.test.ts
  • apps/web/src/app/api/account/devices/route.ts
  • apps/web/vitest.config.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/__tests__/refresh.test.ts
  • packages/lib/src/auth/secure-compare.ts
  • packages/lib/src/services/service-auth.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: React hook files should use camelCase matching the exported hook name (e.g., useAuth.ts)
Zustand store files should use camelCase with use prefix (e.g., useAuthStore.ts)

Files:

  • apps/web/src/app/api/account/devices/[deviceId]/route.ts
  • apps/web/src/app/api/auth/__tests__/login.test.ts
  • apps/web/src/app/api/auth/__tests__/signup.test.ts
  • apps/web/src/app/api/auth/signup/route.ts
  • packages/lib/src/services/__tests__/service-auth.test.ts
  • packages/lib/src/__tests__/secure-compare.test.ts
  • packages/lib/src/auth/index.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/__tests__/mobile-login.test.ts
  • apps/web/src/app/api/account/devices/route.ts
  • apps/web/vitest.config.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/__tests__/refresh.test.ts
  • packages/lib/src/auth/secure-compare.ts
  • packages/lib/src/services/service-auth.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
apps/web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/**/*.{ts,tsx}: Use message parts structure for message content: { parts: [{ type: 'text', text: '...' }] }
For database access, always use Drizzle client from @pagespace/db: import { db, pages } from '@pagespace/db';
Use centralized Drizzle ORM with PostgreSQL for all database operations - no direct SQL or other ORMs
Use Socket.IO for real-time collaboration features - imported from the realtime service at port 3001
Use Vercel AI SDK with async/await for all AI operations and streaming
Use Next.js 15 App Router and TypeScript for all routes and components

Files:

  • apps/web/src/app/api/account/devices/[deviceId]/route.ts
  • apps/web/src/app/api/auth/__tests__/login.test.ts
  • apps/web/src/app/api/auth/__tests__/signup.test.ts
  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/__tests__/mobile-login.test.ts
  • apps/web/src/app/api/account/devices/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/__tests__/refresh.test.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
**/*auth*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*auth*.{ts,tsx}: Use custom JWT authentication with jose library for user management
Use bcryptjs for password hashing

Files:

  • packages/lib/src/services/__tests__/service-auth.test.ts
  • packages/lib/src/services/service-auth.ts
🧠 Learnings (18)
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Use TypeScript strict mode and ESM modules throughout the codebase

Applied to files:

  • packages/lib/package.json
  • packages/lib/src/auth/index.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to packages/lib/**/*.test.ts : Write unit tests for shared utilities in `packages/lib` with test files named `*.test.ts` alongside source or in `__tests__/` directory

Applied to files:

  • packages/lib/package.json
  • packages/lib/src/services/__tests__/service-auth.test.ts
  • packages/lib/src/__tests__/secure-compare.test.ts
  • apps/web/vitest.config.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*.{ts,tsx} : Use ESM modules throughout the codebase

Applied to files:

  • packages/lib/package.json
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to **/*.{ts,tsx} : Use ESM modules and enforce TypeScript strict mode

Applied to files:

  • packages/lib/package.json
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to **/__tests__/**/*.test.ts : Unit tests should be placed next to source files or in `__tests__/` directories with `*.test.ts` extension. Add a `test` script to the package and run with `pnpm --filter <pkg> test`

Applied to files:

  • packages/lib/package.json
  • packages/lib/src/__tests__/secure-compare.test.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to apps/processor/**/*.test.ts : Write unit tests for the processor service with test files named `*.test.ts` alongside source or in `__tests__/` directory

Applied to files:

  • packages/lib/package.json
  • packages/lib/src/__tests__/secure-compare.test.ts
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/**/*.{ts,tsx} : For database access, always use Drizzle client from `pagespace/db`: `import { db, pages } from 'pagespace/db';`

Applied to files:

  • apps/web/src/app/api/account/devices/[deviceId]/route.ts
  • apps/web/src/app/api/account/devices/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to **/*.{ts,tsx} : Always use the Drizzle client and database exports from `pagespace/db` (e.g., `import { db, pages } from 'pagespace/db'`) for all database access

Applied to files:

  • apps/web/src/app/api/account/devices/[deviceId]/route.ts
  • apps/web/src/app/api/account/devices/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/**/*.ts : Always use Drizzle client and queries from `pagespace/db` for database access instead of direct queries

Applied to files:

  • apps/web/src/app/api/account/devices/[deviceId]/route.ts
  • apps/web/src/app/api/account/devices/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/**/*.ts : Always use centralized permission functions from `pagespace/lib/permissions` for access control logic (e.g., `getUserAccessLevel`, `canUserEditPage`)

Applied to files:

  • apps/web/src/app/api/account/devices/[deviceId]/route.ts
  • apps/web/src/app/api/auth/__tests__/login.test.ts
  • apps/web/src/app/api/account/devices/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to app/**/*.{ts,tsx} : Import and use `getUserAccessLevel()` and `canUserEditPage()` from `pagespace/lib/permissions` for centralized permission logic

Applied to files:

  • apps/web/src/app/api/account/devices/[deviceId]/route.ts
  • apps/web/src/app/api/auth/__tests__/login.test.ts
  • apps/web/src/app/api/auth/__tests__/signup.test.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/account/devices/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*.{ts,tsx} : Always use Drizzle client from `pagespace/db` package for database access

Applied to files:

  • apps/web/src/app/api/account/devices/[deviceId]/route.ts
  • apps/web/src/app/api/account/devices/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*.{ts,tsx} : Use centralized permission functions from `pagespace/lib/permissions` (e.g., `getUserAccessLevel`, `canUserEditPage`) instead of implementing permission logic locally

Applied to files:

  • apps/web/src/app/api/account/devices/[deviceId]/route.ts
  • apps/web/src/app/api/auth/__tests__/login.test.ts
  • apps/web/src/app/api/auth/__tests__/signup.test.ts
  • apps/web/src/app/api/account/devices/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : For permission logic, use centralized functions from `pagespace/lib/permissions`: `getUserAccessLevel()`, `canUserEditPage()`

Applied to files:

  • apps/web/src/app/api/auth/__tests__/login.test.ts
  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/account/devices/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*auth*.{ts,tsx} : Use bcryptjs for password hashing

Applied to files:

  • apps/web/src/app/api/auth/signup/route.ts
  • packages/lib/src/auth/index.ts
  • apps/web/src/app/api/account/devices/route.ts
  • packages/lib/src/auth/secure-compare.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*auth*.{ts,tsx} : Use custom JWT authentication with jose library for user management

Applied to files:

  • apps/web/src/app/api/auth/signup/route.ts
  • packages/lib/src/services/__tests__/service-auth.test.ts
  • packages/lib/src/services/service-auth.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Use monorepo structure with pnpm workspaces: `apps/web`, `apps/realtime`, `apps/processor`, and `packages/db`, `packages/lib`

Applied to files:

  • apps/web/vitest.config.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: This is a monorepo using pnpm workspaces with structure: `apps/web` (Next.js frontend/backend), `apps/realtime` (Socket.IO service), `apps/processor` (Express file/OCR pipeline), `packages/db` (Drizzle ORM), `packages/lib` (shared utilities)

Applied to files:

  • apps/web/vitest.config.ts
🧬 Code graph analysis (8)
apps/web/src/app/api/account/devices/[deviceId]/route.ts (1)
packages/lib/src/auth/secure-compare.ts (1)
  • secureCompare (25-42)
apps/web/src/app/api/auth/__tests__/signup.test.ts (1)
apps/web/src/app/api/auth/signup/route.ts (1)
  • POST (45-377)
apps/web/src/app/api/auth/signup/route.ts (1)
packages/lib/src/logging/logger-config.ts (1)
  • logAuthEvent (154-174)
packages/lib/src/services/__tests__/service-auth.test.ts (1)
packages/lib/src/services/service-auth.ts (3)
  • ServiceTokenOptions (31-39)
  • createServiceToken (105-157)
  • verifyServiceToken (159-213)
packages/lib/src/__tests__/secure-compare.test.ts (1)
packages/lib/src/auth/secure-compare.ts (1)
  • secureCompare (25-42)
apps/web/src/app/api/auth/login/route.ts (1)
packages/lib/src/security/security-redis.ts (1)
  • checkRateLimit (181-217)
apps/web/src/app/api/auth/__tests__/mobile-login.test.ts (1)
apps/web/src/app/api/auth/mobile/login/route.ts (1)
  • POST (34-214)
apps/web/src/app/api/auth/__tests__/refresh.test.ts (1)
apps/web/src/app/api/auth/refresh/route.ts (1)
  • POST (14-197)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Lint & TypeScript Check
  • GitHub Check: Unit Tests
🔇 Additional comments (41)
packages/lib/src/auth/index.ts (1)

14-14: LGTM! Clean export addition.

The export follows the established pattern and correctly exposes the new timing-safe comparison utility.

apps/web/src/app/api/account/devices/[deviceId]/route.ts (2)

3-3: LGTM! Proper security import.

Correctly imports the timing-safe comparison utility from the centralized auth module.


38-38: Excellent security improvement!

Replacing the vulnerable === comparison with secureCompare prevents timing attacks on device token comparisons. This is a critical security fix that protects against attackers inferring token values through timing analysis.

apps/web/src/app/api/account/devices/route.ts (2)

3-3: LGTM! Consistent security import.

Properly imports the timing-safe comparison utility.


56-56: Excellent security hardening!

Consistent with the device deletion route, this replaces vulnerable === with timing-safe secureCompare to prevent timing attacks when identifying the current device. The conditional check ensures currentDeviceToken exists before comparison.

apps/web/vitest.config.ts (1)

49-50: LGTM! Test configuration for new security modules.

The path aliases correctly map the new security and secure-compare modules for Vitest, enabling test files to import these utilities. The aliases follow the established pattern and align with the monorepo structure.

packages/lib/src/services/__tests__/service-auth.test.ts (4)

1-23: Well-structured test setup with proper mock hoisting.

The mocks are correctly placed before imports to ensure they're applied during module resolution. The predictable JTI value (DEFAULT_JTI) enables reliable assertions on token lifecycle.


55-146: Comprehensive JTI recording tests.

The test suite thoroughly covers:

  • JTI recording with Redis available
  • Expiration duration conversions (5m, 1h, 30s, 7d)
  • Graceful degradation when Redis is unavailable or throws
  • Unique JTI per token

149-233: Good coverage of JTI validation scenarios.

Tests appropriately cover the fail-closed security model:

  • Revoked tokens rejected
  • Production fails when Redis unavailable
  • Development/test environments gracefully degrade

235-296: End-to-end and backward compatibility tests look good.

The lifecycle tests verify JTI flows through creation → verification correctly, and backward compatibility tests confirm JWT structure remains valid without Redis.

packages/lib/package.json (2)

190-200: New package exports correctly configured.

The ./secure-compare and ./security exports follow the established pattern with proper types, import, and require entry points.


305-311: typesVersions mapping updated correctly.

The type declaration paths for the new exports align with the export definitions.

apps/web/src/app/api/auth/__tests__/login.test.ts (2)

64-77: Distributed rate limiting mock correctly configured.

The mock provides checkDistributedRateLimit, resetDistributedRateLimit, and DISTRIBUTED_RATE_LIMITS with appropriate default values matching the expected interface.


542-647: Comprehensive distributed rate limiting test suite.

The tests cover:

  • IP and email rate limit checks with correct key formats
  • 429 responses with Retry-After and X-RateLimit-* headers when limits exceeded
  • Rate limit reset on successful login
  • Headers included in successful responses

One minor observation: The tests use expect.stringContaining() for key verification (Lines 560, 571, 630, 633), which is appropriate for flexibility but consider using exact key format assertions like 'login:ip:192.168.1.1' for stricter validation if the key format is contractual.

packages/lib/src/services/service-auth.ts (1)

195-204: Fail-closed JTI validation in production is the correct security posture.

The implementation properly:

  • Checks JTI revocation when Redis is available
  • Fails verification in production when Redis is unavailable (fail-closed)
  • Allows graceful degradation in non-production environments
apps/web/src/app/api/auth/refresh/route.ts (3)

4-8: Imports for distributed rate limiting correctly added.

The imports from @pagespace/lib/security provide the necessary utilities for Redis-backed rate limiting.


45-66: Distributed rate limiting integration is correct.

The implementation:

  • Uses refresh:ip:${clientIP} key format (appropriate for refresh which doesn't require email-based limiting)
  • Returns proper 429 response with Retry-After and X-RateLimit-* headers
  • Places check after local rate limit but before expensive operations

186-194: Rate limit reset and response headers correctly implemented.

The distributed rate limit is reset on success, and response headers include rate limit information for client visibility.

apps/web/src/app/api/auth/__tests__/refresh.test.ts (2)

71-84: Distributed rate limiting mock correctly configured.

The mock structure is consistent with other auth test files, providing all necessary exports from @pagespace/lib/security.


552-655: Thorough distributed rate limiting test coverage for refresh.

The tests appropriately verify:

  • checkDistributedRateLimit called with exact key format refresh:ip:<IP>
  • 429 response includes correct X-RateLimit-* headers
  • resetDistributedRateLimit called on success
  • Refresh uses IP-only rate limiting (documented in test at Line 635)
apps/web/src/app/api/auth/mobile/login/route.ts (4)

13-17: Imports for distributed rate limiting correctly added.

The imports from @pagespace/lib/security are consistent with other auth routes.


83-125: Distributed rate limiting correctly implemented for mobile login.

The implementation mirrors the web login route:

  • Checks both IP and email rate limits
  • Returns 429 with appropriate headers when either limit is exceeded
  • Uses consistent key formats (login:ip:* and login:email:*)

161-163: Distributed rate limits correctly reset on successful login.

Both IP and email rate limits are reset after successful authentication, consistent with the web login implementation.


193-208: Rate limit headers correctly added to success response.

The X-RateLimit-Remaining header uses Math.min() of both IP and email attempts remaining, which correctly reflects the more constrained limit. This is appropriate behavior.

apps/web/src/app/api/auth/signup/route.ts (2)

5-9: LGTM!

Clean import of distributed rate limiting utilities. The module structure follows the established pattern for security-related exports.


243-245: LGTM!

Distributed rate limits are correctly reset on successful signup for both IP and email keys. The key format matches the check keys.

packages/lib/src/__tests__/secure-compare.test.ts (6)

1-9: LGTM!

Well-structured test file with clear documentation explaining the purpose. The import path correctly references the source module.


10-35: LGTM!

Solid coverage of basic comparison scenarios. The first/last character difference tests are particularly important for validating timing-safe behavior.


37-58: LGTM!

Comprehensive length-handling tests. The JWT-length test (200+ chars) is particularly valuable for ensuring realistic token scenarios work correctly.


60-104: LGTM!

Excellent coverage of type-safety edge cases. These tests validate that the function properly guards against type coercion attacks that could bypass security checks.


106-146: LGTM!

Thorough testing of Unicode, emoji, and special character handling. The device token scenarios with realistic JWT formats validate the actual use cases for this function.


148-168: LGTM!

The timing-safe properties section appropriately acknowledges the limitations of unit testing for timing guarantees while still verifying the behavioral contract. The documentation serves as useful reference for the expected implementation requirements.

apps/web/src/app/api/auth/login/route.ts (3)

13-17: LGTM!

Clean import of distributed rate limiting utilities, consistent with the signup route pattern.


272-274: LGTM!

Good use of Math.min to report the more restrictive remaining count between IP and email limits. The nullish coalescing provides appropriate defaults when attemptsRemaining is undefined.


234-237: LGTM!

Distributed rate limits are correctly reset on successful login for both IP and email keys.

apps/web/src/app/api/auth/__tests__/signup.test.ts (3)

75-88: LGTM!

Well-structured mock for the distributed rate limiting module. The mock configuration includes all three rate limit types (LOGIN, SIGNUP, REFRESH) with appropriate defaults.


139-143: LGTM!

Imports align with the mocked module and the route implementation.


655-748: LGTM!

Comprehensive test coverage for distributed rate limiting in signup:

  • Validates correct rate limit keys are used
  • Tests both IP and email limit exceeded scenarios
  • Verifies X-RateLimit headers in 429 responses
  • Confirms reset behavior on successful signup
  • Validates key format with regex patterns
apps/web/src/app/api/auth/__tests__/mobile-login.test.ts (3)

54-67: LGTM!

Mock configuration is consistent with signup tests and correctly provides the distributed rate limiting module exports.


77-81: LGTM!

Imports are consistent with the mocked module and other test files.


523-657: LGTM!

Excellent test coverage for distributed rate limiting in mobile login:

  • Validates correct rate limit keys
  • Tests IP and email limit exceeded scenarios with proper headers
  • Confirms reset behavior on successful login
  • Importantly: Tests that mobile login shares rate limit keys with web login (lines 633-656), which is a critical security property preventing attackers from bypassing limits by switching clients
  • Verifies X-RateLimit headers on successful responses

Remove backwards-compatible in-memory rate limiting, leaving only
distributed Redis-based rate limiting for all auth routes.

- Remove checkRateLimit/resetRateLimit imports and calls
- Update tests to use checkDistributedRateLimit exclusively
- Clean up P1-T5 comments

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 0

🧹 Nitpick comments (2)
apps/web/src/app/api/auth/login/route.ts (1)

231-232: X-RateLimit-Remaining calculation may report stale values after reset.

After successful login, resetDistributedRateLimit is called (lines 195-196), but the X-RateLimit-Remaining header still uses the pre-reset attemptsRemaining values from the earlier check. After a reset, the remaining should logically be maxAttempts, not the value from before the reset.

🔎 Consider using maxAttempts after reset
-  headers.set('X-RateLimit-Remaining', String(Math.min(distributedIpLimit.attemptsRemaining ?? DISTRIBUTED_RATE_LIMITS.LOGIN.maxAttempts, distributedEmailLimit.attemptsRemaining ?? DISTRIBUTED_RATE_LIMITS.LOGIN.maxAttempts)));
+  // After successful login and rate limit reset, remaining attempts are back to max
+  headers.set('X-RateLimit-Remaining', String(DISTRIBUTED_RATE_LIMITS.LOGIN.maxAttempts));
apps/web/src/app/api/auth/refresh/route.ts (1)

174-175: Same X-RateLimit-Remaining consideration as login route.

After resetDistributedRateLimit is called (line 169), the attemptsRemaining value from the earlier check is stale. Consider using DISTRIBUTED_RATE_LIMITS.REFRESH.maxAttempts instead.

🔎 Consider using maxAttempts after reset
  headers.set('X-RateLimit-Limit', String(DISTRIBUTED_RATE_LIMITS.REFRESH.maxAttempts));
- headers.set('X-RateLimit-Remaining', String(distributedIpLimit.attemptsRemaining ?? DISTRIBUTED_RATE_LIMITS.REFRESH.maxAttempts));
+ // After successful refresh and rate limit reset, remaining attempts are back to max
+ headers.set('X-RateLimit-Remaining', String(DISTRIBUTED_RATE_LIMITS.REFRESH.maxAttempts));
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1da154e and c58ffd4.

📒 Files selected for processing (8)
  • apps/web/src/app/api/auth/__tests__/login.test.ts
  • apps/web/src/app/api/auth/__tests__/mobile-login.test.ts
  • apps/web/src/app/api/auth/__tests__/refresh.test.ts
  • apps/web/src/app/api/auth/__tests__/signup.test.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/src/app/api/auth/mobile/login/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Use camelCase for variable and function names
Use UPPER_SNAKE_CASE for constants
Use PascalCase for type and enum names
Use kebab-case for filenames, except React hooks (camelCase with use prefix), Zustand stores (camelCase with use prefix), and React components (PascalCase)
Lint with Next/ESLint as configured in apps/web/eslint.config.mjs
Message content should always use the message parts structure with { parts: [{ type: 'text', text: '...' }] }
Use centralized permission functions from @pagespace/lib/permissions (e.g., getUserAccessLevel, canUserEditPage) instead of implementing permission logic locally
Always use Drizzle client from @pagespace/db package for database access
Use ESM modules throughout the codebase

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Write code that is explicit over implicit and self-documenting

Files:

  • apps/web/src/app/api/auth/__tests__/refresh.test.ts
  • apps/web/src/app/api/auth/__tests__/mobile-login.test.ts
  • apps/web/src/app/api/auth/__tests__/login.test.ts
  • apps/web/src/app/api/auth/__tests__/signup.test.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: React hook files should use camelCase matching the exported hook name (e.g., useAuth.ts)
Zustand store files should use camelCase with use prefix (e.g., useAuthStore.ts)

Files:

  • apps/web/src/app/api/auth/__tests__/refresh.test.ts
  • apps/web/src/app/api/auth/__tests__/mobile-login.test.ts
  • apps/web/src/app/api/auth/__tests__/login.test.ts
  • apps/web/src/app/api/auth/__tests__/signup.test.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • apps/web/src/app/api/auth/__tests__/refresh.test.ts
  • apps/web/src/app/api/auth/__tests__/mobile-login.test.ts
  • apps/web/src/app/api/auth/__tests__/login.test.ts
  • apps/web/src/app/api/auth/__tests__/signup.test.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
apps/web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/**/*.{ts,tsx}: Use message parts structure for message content: { parts: [{ type: 'text', text: '...' }] }
For database access, always use Drizzle client from @pagespace/db: import { db, pages } from '@pagespace/db';
Use centralized Drizzle ORM with PostgreSQL for all database operations - no direct SQL or other ORMs
Use Socket.IO for real-time collaboration features - imported from the realtime service at port 3001
Use Vercel AI SDK with async/await for all AI operations and streaming
Use Next.js 15 App Router and TypeScript for all routes and components

Files:

  • apps/web/src/app/api/auth/__tests__/refresh.test.ts
  • apps/web/src/app/api/auth/__tests__/mobile-login.test.ts
  • apps/web/src/app/api/auth/__tests__/login.test.ts
  • apps/web/src/app/api/auth/__tests__/signup.test.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
apps/web/src/app/**/route.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/src/app/**/route.{ts,tsx}: In Next.js 15 route handlers, params in dynamic routes are Promise objects and MUST be awaited before destructuring
Use Response.json() or NextResponse.json() for returning JSON from route handlers
Get request body using const body = await request.json();
Get search parameters using const { searchParams } = new URL(request.url);

apps/web/src/app/**/route.{ts,tsx}: In Next.js 15 dynamic routes, params are Promise objects and MUST be awaited before destructuring: const { id } = await context.params;
In Route Handlers, get request body with const body = await request.json();
In Route Handlers, get search parameters with const { searchParams } = new URL(request.url);
In Route Handlers, return JSON using Response.json(data) or NextResponse.json(data)
For permission logic, use centralized functions from @pagespace/lib/permissions: getUserAccessLevel(), canUserEditPage()

Files:

  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
🧠 Learnings (11)
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*auth*.{ts,tsx} : Use bcryptjs for password hashing

Applied to files:

  • apps/web/src/app/api/auth/__tests__/login.test.ts
  • apps/web/src/app/api/auth/__tests__/signup.test.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*auth*.{ts,tsx} : Use custom JWT authentication with jose library for user management

Applied to files:

  • apps/web/src/app/api/auth/__tests__/login.test.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to app/**/*.{ts,tsx} : Import and use `getUserAccessLevel()` and `canUserEditPage()` from `pagespace/lib/permissions` for centralized permission logic

Applied to files:

  • apps/web/src/app/api/auth/__tests__/login.test.ts
  • apps/web/src/app/api/auth/__tests__/signup.test.ts
  • apps/web/src/app/api/auth/refresh/route.ts
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : For permission logic, use centralized functions from `pagespace/lib/permissions`: `getUserAccessLevel()`, `canUserEditPage()`

Applied to files:

  • apps/web/src/app/api/auth/__tests__/login.test.ts
  • apps/web/src/app/api/auth/__tests__/signup.test.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*.{ts,tsx} : Use centralized permission functions from `pagespace/lib/permissions` (e.g., `getUserAccessLevel`, `canUserEditPage`) instead of implementing permission logic locally

Applied to files:

  • apps/web/src/app/api/auth/__tests__/login.test.ts
  • apps/web/src/app/api/auth/__tests__/signup.test.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/**/*.{ts,tsx} : For database access, always use Drizzle client from `pagespace/db`: `import { db, pages } from 'pagespace/db';`

Applied to files:

  • apps/web/src/app/api/auth/__tests__/signup.test.ts
  • apps/web/src/app/api/auth/refresh/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to **/*.{ts,tsx} : Always use the Drizzle client and database exports from `pagespace/db` (e.g., `import { db, pages } from 'pagespace/db'`) for all database access

Applied to files:

  • apps/web/src/app/api/auth/__tests__/signup.test.ts
  • apps/web/src/app/api/auth/refresh/route.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*.{ts,tsx} : Always use Drizzle client from `pagespace/db` package for database access

Applied to files:

  • apps/web/src/app/api/auth/__tests__/signup.test.ts
  • apps/web/src/app/api/auth/refresh/route.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/**/*.ts : Always use Drizzle client and queries from `pagespace/db` for database access instead of direct queries

Applied to files:

  • apps/web/src/app/api/auth/__tests__/signup.test.ts
  • apps/web/src/app/api/auth/refresh/route.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/**/*.ts : Always use centralized permission functions from `pagespace/lib/permissions` for access control logic (e.g., `getUserAccessLevel`, `canUserEditPage`)

Applied to files:

  • apps/web/src/app/api/auth/__tests__/signup.test.ts
  • apps/web/src/app/api/auth/refresh/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: This is a monorepo using pnpm workspaces with structure: `apps/web` (Next.js frontend/backend), `apps/realtime` (Socket.IO service), `apps/processor` (Express file/OCR pipeline), `packages/db` (Drizzle ORM), `packages/lib` (shared utilities)

Applied to files:

  • apps/web/src/app/api/auth/__tests__/signup.test.ts
🧬 Code graph analysis (3)
apps/web/src/app/api/auth/__tests__/mobile-login.test.ts (3)
apps/web/src/app/api/auth/login/route.ts (1)
  • POST (33-258)
apps/web/src/app/api/auth/mobile/login/route.ts (1)
  • POST (31-172)
apps/web/src/app/api/auth/refresh/route.ts (1)
  • POST (14-178)
apps/web/src/app/api/auth/__tests__/login.test.ts (3)
apps/web/src/app/api/auth/login/route.ts (1)
  • POST (33-258)
apps/web/src/app/api/auth/mobile/login/route.ts (1)
  • POST (31-172)
apps/web/src/app/api/auth/refresh/route.ts (1)
  • POST (14-178)
apps/web/src/app/api/auth/__tests__/signup.test.ts (1)
apps/web/src/app/api/auth/signup/route.ts (1)
  • POST (45-337)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Lint & TypeScript Check
  • GitHub Check: Unit Tests
🔇 Additional comments (12)
apps/web/src/app/api/auth/login/route.ts (2)

10-14: LGTM - Distributed rate limiting imports are correctly structured.

The imports for checkDistributedRateLimit, resetDistributedRateLimit, and DISTRIBUTED_RATE_LIMITS from @pagespace/lib/security are properly organized and align with the PR's security foundation goals.


99-141: Distributed rate limiting implementation looks correct.

The dual rate limiting strategy (IP + email) provides defense-in-depth against both distributed attacks and targeted account attacks. The 429 responses include appropriate Retry-After and X-RateLimit-* headers for client-side handling.

apps/web/src/app/api/auth/__tests__/refresh.test.ts (2)

67-80: LGTM - Distributed rate limiting mock is correctly configured.

The mock structure properly exposes checkDistributedRateLimit, resetDistributedRateLimit, and DISTRIBUTED_RATE_LIMITS with appropriate default return values. The REFRESH config (maxAttempts: 10) differs appropriately from LOGIN (5) and SIGNUP (3).


546-658: Comprehensive test coverage for distributed rate limiting.

The test suite thoroughly covers:

  • IP-based rate limit checks with correct key format (refresh:ip:)
  • 429 responses with Retry-After and X-RateLimit-* headers
  • Rate limit reset on successful refresh
  • Verification that refresh uses IP-only (no email) rate limiting

This aligns well with the refresh route implementation.

apps/web/src/app/api/auth/__tests__/mobile-login.test.ts (2)

49-62: LGTM - Distributed rate limiting mock matches other auth test files.

The mock configuration is consistent with login.test.ts and signup.test.ts, ensuring uniform test behavior across the auth module.


625-648: Good test for verifying shared rate limits between mobile and web login.

This test explicitly confirms that mobile login uses the same rate limit keys (login:ip: and login:email:) as web login, ensuring that attackers cannot bypass rate limits by switching between platforms.

apps/web/src/app/api/auth/refresh/route.ts (2)

4-8: LGTM - Distributed rate limiting imports are correctly structured.

Imports align with the security module exports and are consistent with other auth routes.


27-48: Correct implementation of IP-only rate limiting for refresh.

Since refresh tokens are cookie-based and don't include an email in the request body, IP-only rate limiting is the appropriate strategy here. The 429 response includes proper headers for client handling.

apps/web/src/app/api/auth/__tests__/login.test.ts (2)

59-72: LGTM - Distributed rate limiting mock is correctly configured.

The mock setup is consistent across all auth test files, ensuring predictable test behavior. The progressiveDelay field in the config indicates support for progressive backoff, which is a good security practice.


551-656: Comprehensive distributed rate limiting test coverage.

The test suite covers:

  • IP and email rate limit checks with correct DISTRIBUTED_RATE_LIMITS.LOGIN config
  • 429 responses with Retry-After and X-RateLimit-* headers for both IP and email limits
  • Rate limit reset calls on successful login
  • X-RateLimit headers presence on successful responses

The tests use expect.stringContaining for flexible key matching while still ensuring the correct identifiers are included.

apps/web/src/app/api/auth/__tests__/signup.test.ts (2)

71-84: LGTM - Distributed rate limiting mock with appropriate SIGNUP configuration.

The SIGNUP rate limit config (maxAttempts: 3, windowMs: 3600000) is stricter than LOGIN (5 attempts, 900000ms), which is appropriate since signup is typically a one-time action and should be more aggressively rate-limited.


648-741: Comprehensive distributed rate limiting test coverage for signup.

The test suite properly covers:

  • Correct key format verification (signup:ip: and signup:email:)
  • 429 responses with headers showing SIGNUP limits (3 max attempts)
  • Rate limit reset on successful signup
  • Explicit verification that signup keys differ from login keys

This ensures signup has isolated rate limiting from login attempts.

2witstudios and others added 2 commits January 5, 2026 23:36
- Fix asymmetric Redis behavior in service-auth.ts (fail-closed in production)
- Add distributed rate limiting to mobile/signup, mobile/refresh, device/refresh
- Migrate OAuth routes from legacy to distributed rate limiting
- Parallelize rate limit checks for better performance
- Add duration validation to durationToSeconds() with max 30-day cap
- Fix stale X-RateLimit-Remaining headers after rate limit reset
- Clean up console.log in integration tests

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace legacy checkRateLimit/RATE_LIMIT_CONFIGS mocks with
checkDistributedRateLimit/DISTRIBUTED_RATE_LIMITS in:
- login-redirect.test.ts
- signup-redirect.test.ts
- google-callback-redirect.test.ts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (2)
apps/web/src/app/api/auth/google/callback/route.ts (1)

106-113: Add X-RateLimit headers to align with PR objectives.

The distributed rate limiting is correctly implemented, but the PR summary explicitly states "add X-RateLimit headers" as part of P1-T5. The ipRateLimit response object should contain fields like remaining, limit, and resetAt that should be exposed to clients via response headers.

🔎 Suggested implementation for X-RateLimit headers

Add headers to both the rate-limited response (line 112) and all success responses (lines 307, 341):

 const ipRateLimit = await checkDistributedRateLimit(
   `oauth:callback:ip:${clientIP}`,
   DISTRIBUTED_RATE_LIMITS.LOGIN
 );
 if (!ipRateLimit.allowed) {
   const baseUrl = process.env.NEXTAUTH_URL || process.env.WEB_APP_URL || req.url;
-  return NextResponse.redirect(new URL('/auth/signin?error=rate_limit', baseUrl));
+  const redirectResponse = NextResponse.redirect(new URL('/auth/signin?error=rate_limit', baseUrl));
+  redirectResponse.headers.set('X-RateLimit-Limit', String(ipRateLimit.limit));
+  redirectResponse.headers.set('X-RateLimit-Remaining', '0');
+  redirectResponse.headers.set('X-RateLimit-Reset', String(ipRateLimit.resetAt));
+  return redirectResponse;
 }

And for success responses:

     const headers = new Headers();
     headers.append('Set-Cookie', accessTokenCookie);
     headers.append('Set-Cookie', refreshTokenCookie);
+    headers.set('X-RateLimit-Limit', String(ipRateLimit.limit));
+    headers.set('X-RateLimit-Remaining', String(ipRateLimit.remaining));
+    headers.set('X-RateLimit-Reset', String(ipRateLimit.resetAt));

     return NextResponse.redirect(redirectUrl, { headers });

Similar headers should be added to the desktop platform response at line 307.

apps/web/src/app/api/auth/google/signin/route.ts (1)

94-115: Add rate limiting to GET endpoint.

The GET endpoint for OAuth flow initiation has no rate limiting, unlike the POST endpoint (line 29-48). Since the GET endpoint is publicly accessible and can be called without authentication, it should have the same IP-based rate limiting applied to prevent abuse:

// Add before generating OAuth URL (after try block)
const clientIP = req.headers.get('x-forwarded-for')?.split(',')[0] ||
                 req.headers.get('x-real-ip') ||
                 'unknown';

const ipRateLimit = await checkDistributedRateLimit(
  `oauth:signin:ip:${clientIP}`,
  DISTRIBUTED_RATE_LIMITS.LOGIN
);
if (!ipRateLimit.allowed) {
  return Response.json(
    {
      error: 'Too many login attempts from this IP address. Please try again later.',
      retryAfter: ipRateLimit.retryAfter,
    },
    {
      status: 429,
      headers: {
        'Retry-After': String(ipRateLimit.retryAfter || 900),
        'X-RateLimit-Limit': String(DISTRIBUTED_RATE_LIMITS.LOGIN.maxAttempts),
        'X-RateLimit-Remaining': '0',
      },
    }
  );
}
🤖 Fix all issues with AI Agents
In @apps/web/src/app/api/auth/mobile/login/route.ts:
- Around line 148-150: The X-RateLimit-Remaining header is using the pre-reset
values (distributedIpLimit.attemptsRemaining /
distributedEmailLimit.attemptsRemaining); after the reset you must report
maxAttempts instead of the old values. Fix by computing remaining per-scope
using the post-reset state (e.g., remainingIp =
distributedIpLimit.attemptsRemaining ??
DISTRIBUTED_RATE_LIMITS.LOGIN.maxAttempts and remainingEmail =
distributedEmailLimit.attemptsRemaining ??
DISTRIBUTED_RATE_LIMITS.LOGIN.maxAttempts) or explicitly set to
DISTRIBUTED_RATE_LIMITS.LOGIN.maxAttempts when the reset logic ran, then use
Math.min(remainingIp, remainingEmail) to set 'X-RateLimit-Remaining' so it
reflects the reset.

In @packages/lib/src/services/service-auth.ts:
- Around line 239-248: The code calls isJTIRevoked(claims.jti) without ensuring
claims.jti exists or is a string; update the JTI validation block in the
function using tryGetSecurityRedisClient to first check that typeof claims.jti
=== 'string' (and non-empty) before invoking isJTIRevoked, skip the revocation
check for tokens lacking a JTI (or optionally log/debug that JTI is missing) to
preserve backward compatibility, and keep the existing production fail-closed
behavior only for when Redis is unavailable (i.e., do not call isJTIRevoked with
undefined).
🧹 Nitpick comments (4)
apps/web/src/app/api/auth/signup/route.ts (1)

200-201: Consider parallelizing rate limit resets for consistency.

The rate limit checks use Promise.all for parallel execution, but the resets are sequential. For consistency and minor performance improvement:

🔎 Proposed fix
-    await resetDistributedRateLimit(`signup:ip:${clientIP}`);
-    await resetDistributedRateLimit(`signup:email:${email.toLowerCase()}`);
+    await Promise.all([
+      resetDistributedRateLimit(`signup:ip:${clientIP}`),
+      resetDistributedRateLimit(`signup:email:${email.toLowerCase()}`),
+    ]);
apps/web/src/app/api/auth/mobile/login/route.ts (1)

117-118: Consider parallelizing rate limit resets.

Same optional optimization as in the signup route.

🔎 Proposed fix
-    await resetDistributedRateLimit(`login:ip:${clientIP}`);
-    await resetDistributedRateLimit(`login:email:${email.toLowerCase()}`);
+    await Promise.all([
+      resetDistributedRateLimit(`login:ip:${clientIP}`),
+      resetDistributedRateLimit(`login:email:${email.toLowerCase()}`),
+    ]);
apps/web/src/app/api/auth/login/route.ts (1)

191-192: Consider parallelizing rate limit resets.

Same optional optimization as in the other auth routes for consistency.

🔎 Proposed fix
-    await resetDistributedRateLimit(`login:ip:${clientIP}`);
-    await resetDistributedRateLimit(`login:email:${email.toLowerCase()}`);
+    await Promise.all([
+      resetDistributedRateLimit(`login:ip:${clientIP}`),
+      resetDistributedRateLimit(`login:email:${email.toLowerCase()}`),
+    ]);
apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts (1)

134-137: Consider extracting OAuth verification rate limit config to a constant.

The inline rate limit config differs from other limits that use DISTRIBUTED_RATE_LIMITS constants. Additionally, the hardcoded '10' on line 155 duplicates the maxAttempts value, creating a maintenance risk if the config changes.

🔎 Proposed refactor

Add a constant to DISTRIBUTED_RATE_LIMITS (in the security module):

OAUTH_VERIFY: { maxAttempts: 10, windowMs: 5 * 60 * 1000 }

Then update this file:

 const oauthRateLimit = await checkDistributedRateLimit(
   `mobile:oauth:verify:${clientIP}`,
-  { maxAttempts: 10, windowMs: 5 * 60 * 1000 } // 10 attempts per 5 minutes
+  DISTRIBUTED_RATE_LIMITS.OAUTH_VERIFY
 );
-'X-RateLimit-Limit': '10',
+'X-RateLimit-Limit': String(DISTRIBUTED_RATE_LIMITS.OAUTH_VERIFY.maxAttempts),

Also applies to: 155-156

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c58ffd4 and 2ed8ce7.

📒 Files selected for processing (13)
  • apps/web/src/app/api/auth/device/refresh/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/google/signin/route.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
  • apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts
  • apps/web/src/app/api/auth/mobile/refresh/route.ts
  • apps/web/src/app/api/auth/mobile/signup/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
  • packages/lib/src/security/__tests__/security-redis.integration.test.ts
  • packages/lib/src/services/__tests__/service-auth.test.ts
  • packages/lib/src/services/service-auth.ts
🧰 Additional context used
📓 Path-based instructions (6)
apps/web/src/app/**/route.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/src/app/**/route.{ts,tsx}: In Next.js 15 route handlers, params in dynamic routes are Promise objects and MUST be awaited before destructuring
Use Response.json() or NextResponse.json() for returning JSON from route handlers
Get request body using const body = await request.json();
Get search parameters using const { searchParams } = new URL(request.url);

apps/web/src/app/**/route.{ts,tsx}: In Next.js 15 dynamic routes, params are Promise objects and MUST be awaited before destructuring: const { id } = await context.params;
In Route Handlers, get request body with const body = await request.json();
In Route Handlers, get search parameters with const { searchParams } = new URL(request.url);
In Route Handlers, return JSON using Response.json(data) or NextResponse.json(data)
For permission logic, use centralized functions from @pagespace/lib/permissions: getUserAccessLevel(), canUserEditPage()

Files:

  • apps/web/src/app/api/auth/mobile/login/route.ts
  • apps/web/src/app/api/auth/google/signin/route.ts
  • apps/web/src/app/api/auth/device/refresh/route.ts
  • apps/web/src/app/api/auth/mobile/refresh/route.ts
  • apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/mobile/signup/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Use camelCase for variable and function names
Use UPPER_SNAKE_CASE for constants
Use PascalCase for type and enum names
Use kebab-case for filenames, except React hooks (camelCase with use prefix), Zustand stores (camelCase with use prefix), and React components (PascalCase)
Lint with Next/ESLint as configured in apps/web/eslint.config.mjs
Message content should always use the message parts structure with { parts: [{ type: 'text', text: '...' }] }
Use centralized permission functions from @pagespace/lib/permissions (e.g., getUserAccessLevel, canUserEditPage) instead of implementing permission logic locally
Always use Drizzle client from @pagespace/db package for database access
Use ESM modules throughout the codebase

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Write code that is explicit over implicit and self-documenting

Files:

  • apps/web/src/app/api/auth/mobile/login/route.ts
  • packages/lib/src/security/__tests__/security-redis.integration.test.ts
  • apps/web/src/app/api/auth/google/signin/route.ts
  • apps/web/src/app/api/auth/device/refresh/route.ts
  • packages/lib/src/services/service-auth.ts
  • apps/web/src/app/api/auth/mobile/refresh/route.ts
  • packages/lib/src/services/__tests__/service-auth.test.ts
  • apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/mobile/signup/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: React hook files should use camelCase matching the exported hook name (e.g., useAuth.ts)
Zustand store files should use camelCase with use prefix (e.g., useAuthStore.ts)

Files:

  • apps/web/src/app/api/auth/mobile/login/route.ts
  • packages/lib/src/security/__tests__/security-redis.integration.test.ts
  • apps/web/src/app/api/auth/google/signin/route.ts
  • apps/web/src/app/api/auth/device/refresh/route.ts
  • packages/lib/src/services/service-auth.ts
  • apps/web/src/app/api/auth/mobile/refresh/route.ts
  • packages/lib/src/services/__tests__/service-auth.test.ts
  • apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/mobile/signup/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • apps/web/src/app/api/auth/mobile/login/route.ts
  • packages/lib/src/security/__tests__/security-redis.integration.test.ts
  • apps/web/src/app/api/auth/google/signin/route.ts
  • apps/web/src/app/api/auth/device/refresh/route.ts
  • packages/lib/src/services/service-auth.ts
  • apps/web/src/app/api/auth/mobile/refresh/route.ts
  • packages/lib/src/services/__tests__/service-auth.test.ts
  • apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/mobile/signup/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
apps/web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/**/*.{ts,tsx}: Use message parts structure for message content: { parts: [{ type: 'text', text: '...' }] }
For database access, always use Drizzle client from @pagespace/db: import { db, pages } from '@pagespace/db';
Use centralized Drizzle ORM with PostgreSQL for all database operations - no direct SQL or other ORMs
Use Socket.IO for real-time collaboration features - imported from the realtime service at port 3001
Use Vercel AI SDK with async/await for all AI operations and streaming
Use Next.js 15 App Router and TypeScript for all routes and components

Files:

  • apps/web/src/app/api/auth/mobile/login/route.ts
  • apps/web/src/app/api/auth/google/signin/route.ts
  • apps/web/src/app/api/auth/device/refresh/route.ts
  • apps/web/src/app/api/auth/mobile/refresh/route.ts
  • apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/mobile/signup/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
**/*auth*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*auth*.{ts,tsx}: Use custom JWT authentication with jose library for user management
Use bcryptjs for password hashing

Files:

  • packages/lib/src/services/service-auth.ts
  • packages/lib/src/services/__tests__/service-auth.test.ts
🧠 Learnings (13)
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to app/**/*.{ts,tsx} : Import and use `getUserAccessLevel()` and `canUserEditPage()` from `pagespace/lib/permissions` for centralized permission logic

Applied to files:

  • apps/web/src/app/api/auth/mobile/login/route.ts
  • apps/web/src/app/api/auth/device/refresh/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/mobile/signup/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*.{ts,tsx} : Use centralized permission functions from `pagespace/lib/permissions` (e.g., `getUserAccessLevel`, `canUserEditPage`) instead of implementing permission logic locally

Applied to files:

  • apps/web/src/app/api/auth/mobile/login/route.ts
  • apps/web/src/app/api/auth/device/refresh/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/mobile/signup/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/**/*.ts : Always use centralized permission functions from `pagespace/lib/permissions` for access control logic (e.g., `getUserAccessLevel`, `canUserEditPage`)

Applied to files:

  • apps/web/src/app/api/auth/device/refresh/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/mobile/signup/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : For permission logic, use centralized functions from `pagespace/lib/permissions`: `getUserAccessLevel()`, `canUserEditPage()`

Applied to files:

  • apps/web/src/app/api/auth/device/refresh/route.ts
  • apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/mobile/signup/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*auth*.{ts,tsx} : Use custom JWT authentication with jose library for user management

Applied to files:

  • packages/lib/src/services/service-auth.ts
  • packages/lib/src/services/__tests__/service-auth.test.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to packages/lib/**/*.test.ts : Write unit tests for shared utilities in `packages/lib` with test files named `*.test.ts` alongside source or in `__tests__/` directory

Applied to files:

  • packages/lib/src/services/__tests__/service-auth.test.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to apps/processor/**/*.test.ts : Write unit tests for the processor service with test files named `*.test.ts` alongside source or in `__tests__/` directory

Applied to files:

  • packages/lib/src/services/__tests__/service-auth.test.ts
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/**/*.{ts,tsx} : For database access, always use Drizzle client from `pagespace/db`: `import { db, pages } from 'pagespace/db';`

Applied to files:

  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/mobile/signup/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to **/*.{ts,tsx} : Always use the Drizzle client and database exports from `pagespace/db` (e.g., `import { db, pages } from 'pagespace/db'`) for all database access

Applied to files:

  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/**/*.ts : Always use Drizzle client and queries from `pagespace/db` for database access instead of direct queries

Applied to files:

  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*.{ts,tsx} : Always use Drizzle client from `pagespace/db` package for database access

Applied to files:

  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*auth*.{ts,tsx} : Use bcryptjs for password hashing

Applied to files:

  • apps/web/src/app/api/auth/signup/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: This is a monorepo using pnpm workspaces with structure: `apps/web` (Next.js frontend/backend), `apps/realtime` (Socket.IO service), `apps/processor` (Express file/OCR pipeline), `packages/db` (Drizzle ORM), `packages/lib` (shared utilities)

Applied to files:

  • apps/web/src/app/api/auth/signup/route.ts
🧬 Code graph analysis (2)
packages/lib/src/services/service-auth.ts (2)
packages/lib/src/logging/logger-config.ts (1)
  • loggers (8-18)
apps/processor/src/logger.ts (1)
  • error (57-63)
apps/web/src/app/api/auth/signup/route.ts (1)
packages/lib/src/logging/logger-config.ts (1)
  • logAuthEvent (154-174)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Static Security Analysis
  • GitHub Check: Lint & TypeScript Check
  • GitHub Check: Unit Tests
🔇 Additional comments (40)
apps/web/src/app/api/auth/google/callback/route.ts (2)

4-9: LGTM! Distributed rate limiting imports added correctly.

The migration from local to distributed rate limiting utilities is implemented correctly with all necessary imports from @pagespace/lib/security.


235-235: LGTM! Rate limit reset correctly implemented.

The distributed rate limit reset uses the same key pattern as the check and is appropriately placed after successful authentication, aligning with the PR objective to "reset limits on successful auth."

apps/web/src/app/api/auth/mobile/refresh/route.ts (4)

12-16: LGTM: Distributed rate limiting imports.

The imports for distributed rate limiting are correct and consistent with the broader migration across auth endpoints.


48-63: LGTM: Rate limit denial response includes proper headers.

The 429 response correctly includes Retry-After, X-RateLimit-Limit, and X-RateLimit-Remaining headers as per distributed rate limiting standards.


133-134: LGTM: Rate limit reset on successful refresh.

The rate limit is properly reset after successful authentication, which is the expected behavior for distributed rate limiting.


42-46: Inconsistent rate limit key patterns across refresh endpoints.

The rate limit key pattern is inconsistent:

  • Standard web refresh uses refresh:ip:${clientIP}
  • Mobile and device refresh routes use refresh:device:ip:${clientIP}

This creates separate rate limit buckets for device-based refresh versus web-based refresh. While this may be intentional to isolate device authentication attempts, the pattern lacks documentation explaining the design decision. The test suite also only validates the standard refresh:ip:... pattern, suggesting this inconsistency may be unintended.

Consolidate the rate limit key pattern across all three refresh endpoints to either:

  1. Use refresh:ip:${clientIP} uniformly (if device and web should share buckets), or
  2. Document explicitly why the refresh:device:ip:${clientIP} pattern is necessary for mobile/device routes (if intentional separation is required)
packages/lib/src/security/__tests__/security-redis.integration.test.ts (2)

20-32: LGTM: Graceful Redis connection handling.

The Redis connection setup with lazy connect and short timeout is appropriate for integration tests. The silent fallback when Redis is unavailable reduces test noise.


57-57: LGTM: Silent skip pattern for unavailable Redis.

The change from verbose skip logging to silent early returns (if (!redis) return;) is appropriate. This reduces noise in CI environments while still preserving test execution when Redis is available.

Also applies to: 84-84, 121-121, 162-162, 184-184, 210-210, 234-234, 255-255, 262-262

apps/web/src/app/api/auth/device/refresh/route.ts (3)

15-19: LGTM: Distributed rate limiting imports.

The imports are correct and consistent with the distributed rate limiting migration.


48-69: Rate limit key pattern matches mobile/refresh route.

This route uses refresh:device:ip:${clientIP} which matches the mobile refresh route. This creates a separate rate limit bucket from web refresh endpoints, allowing device-specific rate limiting policies. Verify this is the intended behavior for your rate limiting strategy.


199-200: LGTM: Rate limit reset on successful refresh.

The distributed rate limit is properly reset after successful authentication.

apps/web/src/app/api/auth/mobile/signup/route.ts (4)

11-15: LGTM: Distributed rate limiting imports.

The imports are correct and support the distributed rate limiting migration.


72-76: Excellent: Parallel rate limit checks for performance and security.

Using Promise.all to check both IP and email rate limits concurrently is a good optimization. Separate limits for IP and email help prevent email enumeration attacks while maintaining effective spam protection.


78-94: LGTM: Proper rate limit denial responses with headers.

Both rate limit denial paths correctly include Retry-After, X-RateLimit-Limit, and X-RateLimit-Remaining headers, providing clear feedback to clients.

Also applies to: 96-112


171-174: LGTM: Parallel rate limit resets optimize latency.

Resetting both IP and email rate limits concurrently using Promise.all minimizes latency on successful signup.

apps/web/src/app/api/auth/google/signin/route.ts (2)

3-6: LGTM: Distributed rate limiting imports.

The imports are correct for the distributed rate limiting implementation.


25-48: LGTM: Distributed rate limiting on OAuth signin with proper headers.

The rate limiting implementation is correct:

  • Uses namespace oauth:signin:ip:${clientIP} to separate OAuth attempts from regular login
  • Applies DISTRIBUTED_RATE_LIMITS.LOGIN for consistent policy
  • Returns proper 429 response with Retry-After, X-RateLimit-Limit, and X-RateLimit-Remaining headers

Note: Rate limit reset should occur in the callback route after successful OAuth completion.

apps/web/src/app/api/auth/signup/route.ts (2)

4-9: LGTM - Clean import refactoring for distributed rate limiting.

The imports are properly organized, bringing in checkDistributedRateLimit, resetDistributedRateLimit, and DISTRIBUTED_RATE_LIMITS from the centralized security module.


113-153: LGTM - Well-structured parallel distributed rate limiting.

The parallel rate limit checks via Promise.all are efficient. The response headers (Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining) follow standard conventions, and error messages are appropriately informative without leaking sensitive details.

apps/web/src/app/api/auth/refresh/route.ts (5)

3-8: LGTM - Proper import updates for distributed rate limiting.

Clean import refactoring bringing in the distributed rate limiting utilities from the security module.


27-48: LGTM - IP-only rate limiting is appropriate for refresh endpoint.

Since the refresh endpoint doesn't require email in the request body (token comes from cookie), IP-only rate limiting is the correct approach. The fallback Retry-After of 300 seconds is reasonable for this endpoint.


50-66: LGTM - Device token validation enforces device revocation.

This security enhancement ensures that revoked devices cannot continue accessing the account via refresh tokens. The optional validation (only when header is present) maintains backward compatibility.


68-109: LGTM - Robust token reuse detection with comprehensive revocation.

The transaction-based approach ensures atomicity, and the security measures (tokenVersion bump + device token revocation) effectively prevent token reuse attacks. The revokedReason field provides good audit trail.


168-176: LGTM - Rate limit headers correctly reflect post-reset state.

After resetting the rate limit, setting X-RateLimit-Remaining to maxAttempts accurately reflects the refreshed quota.

apps/web/src/app/api/auth/mobile/login/route.ts (2)

10-14: LGTM - Consistent import pattern for distributed rate limiting.


46-84: LGTM - Parallel distributed rate limiting matches web login pattern.

The implementation is consistent with the web login route, using the same DISTRIBUTED_RATE_LIMITS.LOGIN configuration and parallel checks.

apps/web/src/app/api/auth/login/route.ts (4)

10-14: LGTM - Consistent distributed rate limiting imports.


40-88: LGTM - CSRF validation correctly precedes rate limiting.

The Login CSRF protection using the double-submit pattern is properly implemented, with validation occurring before body parsing and rate limit consumption.


99-137: LGTM - Parallel distributed rate limiting with proper error responses.

The implementation is consistent across login routes, with appropriate error messages and standard rate limit headers.


227-229: LGTM - Rate limit headers correctly reflect post-reset state.

The comment clearly explains that after reset, remaining attempts are back to max. This is the correct pattern that should be followed in the mobile login route as well.

apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts (1)

80-327: Distributed rate limiting migration looks well-implemented.

The overall migration from local to distributed rate limiting is correct:

  • Rate limit keys are well-namespaced (mobile:oauth:ip:, mobile:oauth:verify:, mobile:oauth:email:)
  • X-RateLimit headers properly communicate limit state to clients
  • Email normalization is consistent between check and reset
  • The parallel reset via Promise.all is efficient (pending the error handling fix noted above)
packages/lib/src/services/service-auth.ts (3)

1-30: LGTM!

The imports and interface updates are well-structured. The jti field is correctly added to ServiceTokenClaims, and the security-redis utilities are properly imported for JTI lifecycle management.


64-105: LGTM!

The durationToSeconds function is well-implemented with proper validation, defaulting behavior, and maximum duration capping. The defensive default case in the switch statement (lines 94-95) is technically unreachable due to the regex constraint, but it's acceptable as defensive coding.


176-198: Solid fail-closed implementation.

The JTI recording logic correctly enforces fail-closed behavior in production while allowing graceful degradation in dev/test. The token is signed before Redis recording, but since errors prevent the function from returning the token, this is safe.

Consider adding the jti to the warning log metadata (line 194-197) to aid debugging when JTI recording fails:

🔎 Optional improvement for debugging
     loggers.api.warn('Failed to record JTI in Redis, token created without tracking', {
       error: error instanceof Error ? error.message : String(error),
       subject: options.subject,
+      jti,
     });
packages/lib/src/services/__tests__/service-auth.test.ts (6)

1-36: LGTM!

The mock setup is well-structured with mocks defined before imports (leveraging Vitest's hoisting behavior). The deterministic JTI value (DEFAULT_JTI) enables predictable assertions throughout the tests.


45-67: LGTM!

Proper test isolation with environment variable backup/restore and mock clearing between tests. The validOptions fixture provides a clean minimal configuration for the tests.


68-187: Comprehensive test coverage for JTI recording.

The tests thoroughly cover all scenarios: Redis availability, custom durations, graceful degradation in dev/test, and fail-closed behavior in production. The unique JTI test (lines 176-186) effectively verifies per-token uniqueness.


189-273: LGTM!

The verification tests are well-structured with proper setup isolation. The vi.clearAllMocks() after token creation (line 196) cleverly separates creation-side effects from verification assertions. Good coverage of revocation, Redis availability, and error propagation scenarios.


275-336: LGTM!

The end-to-end lifecycle tests effectively validate the complete JTI flow from creation through verification, including the revocation scenario. The backward compatibility tests (lines 307-335) provide good coverage for JWT format and Redis-unavailable scenarios.


338-421: LGTM!

Thorough duration validation tests covering edge cases: zero values, negative values, invalid formats, exceeding maximum, and the boundary case of exactly 30 days. The assertions correctly verify both warning emissions and the resulting JTI TTL values.

Comment thread apps/web/src/app/api/auth/mobile/login/route.ts Outdated
Comment thread apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts Outdated
Comment thread packages/lib/src/services/service-auth.ts

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Fix all issues with AI Agents
In @apps/web/src/app/api/auth/__tests__/google-callback-redirect.test.ts:
- Around line 58-64: The DISTRIBUTED_RATE_LIMITS mock is missing properties used
elsewhere; update the mocked object for DISTRIBUTED_RATE_LIMITS (specifically
the LOGIN entry) to include blockDurationMs and progressiveDelay so it matches
production and other tests—set LOGIN to include maxAttempts: 5, windowMs:
900000, blockDurationMs: 900000 and progressiveDelay: true in the vi.mock for
'@pagespace/lib/security' so code referencing
DISTRIBUTED_RATE_LIMITS.LOGIN.blockDurationMs or .progressiveDelay won't fail.
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2ed8ce7 and 54378f6.

📒 Files selected for processing (3)
  • apps/web/src/app/api/auth/__tests__/google-callback-redirect.test.ts
  • apps/web/src/app/api/auth/__tests__/login-redirect.test.ts
  • apps/web/src/app/api/auth/__tests__/signup-redirect.test.ts
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Use camelCase for variable and function names
Use UPPER_SNAKE_CASE for constants
Use PascalCase for type and enum names
Use kebab-case for filenames, except React hooks (camelCase with use prefix), Zustand stores (camelCase with use prefix), and React components (PascalCase)
Lint with Next/ESLint as configured in apps/web/eslint.config.mjs
Message content should always use the message parts structure with { parts: [{ type: 'text', text: '...' }] }
Use centralized permission functions from @pagespace/lib/permissions (e.g., getUserAccessLevel, canUserEditPage) instead of implementing permission logic locally
Always use Drizzle client from @pagespace/db package for database access
Use ESM modules throughout the codebase

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Write code that is explicit over implicit and self-documenting

Files:

  • apps/web/src/app/api/auth/__tests__/google-callback-redirect.test.ts
  • apps/web/src/app/api/auth/__tests__/signup-redirect.test.ts
  • apps/web/src/app/api/auth/__tests__/login-redirect.test.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: React hook files should use camelCase matching the exported hook name (e.g., useAuth.ts)
Zustand store files should use camelCase with use prefix (e.g., useAuthStore.ts)

Files:

  • apps/web/src/app/api/auth/__tests__/google-callback-redirect.test.ts
  • apps/web/src/app/api/auth/__tests__/signup-redirect.test.ts
  • apps/web/src/app/api/auth/__tests__/login-redirect.test.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • apps/web/src/app/api/auth/__tests__/google-callback-redirect.test.ts
  • apps/web/src/app/api/auth/__tests__/signup-redirect.test.ts
  • apps/web/src/app/api/auth/__tests__/login-redirect.test.ts
apps/web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/**/*.{ts,tsx}: Use message parts structure for message content: { parts: [{ type: 'text', text: '...' }] }
For database access, always use Drizzle client from @pagespace/db: import { db, pages } from '@pagespace/db';
Use centralized Drizzle ORM with PostgreSQL for all database operations - no direct SQL or other ORMs
Use Socket.IO for real-time collaboration features - imported from the realtime service at port 3001
Use Vercel AI SDK with async/await for all AI operations and streaming
Use Next.js 15 App Router and TypeScript for all routes and components

Files:

  • apps/web/src/app/api/auth/__tests__/google-callback-redirect.test.ts
  • apps/web/src/app/api/auth/__tests__/signup-redirect.test.ts
  • apps/web/src/app/api/auth/__tests__/login-redirect.test.ts
🧠 Learnings (2)
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*auth*.{ts,tsx} : Use bcryptjs for password hashing

Applied to files:

  • apps/web/src/app/api/auth/__tests__/login-redirect.test.ts
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/**/*.{ts,tsx} : For database access, always use Drizzle client from `pagespace/db`: `import { db, pages } from 'pagespace/db';`

Applied to files:

  • apps/web/src/app/api/auth/__tests__/login-redirect.test.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Unit Tests
🔇 Additional comments (4)
apps/web/src/app/api/auth/__tests__/google-callback-redirect.test.ts (2)

89-89: LGTM: Import correctly updated for distributed rate limiting.


39-56: The mock setup is correct and complete. logSecurityEvent is not imported or used in the Google callback route handler, so it should not be included in the test mock. While logSecurityEvent appears in the mocks for login-redirect.test.ts and signup-redirect.test.ts, each test file should only mock the functions actually used by its corresponding route implementation.

Likely an incorrect or invalid review comment.

apps/web/src/app/api/auth/__tests__/login-redirect.test.ts (1)

39-40: LGTM: Distributed rate limiting migration completed correctly.

All changes properly integrate distributed rate limiting:

  • Added logSecurityEvent to server mock for security event logging
  • Configured distributed rate limiting mock with LOGIN settings
  • Updated imports and mock setup in beforeEach
  • Test logic remains intact while infrastructure aligns with new security foundation

Also applies to: 42-48, 80-80, 87-87

apps/web/src/app/api/auth/__tests__/signup-redirect.test.ts (1)

41-42: LGTM: Distributed rate limiting migration for signup properly configured.

All changes correctly implement distributed rate limiting:

  • Added logSecurityEvent to server mock
  • Configured SIGNUP rate limiting with windowMs: 3600000 (1 hour)
  • Updated imports and mock setup appropriately

Note: The SIGNUP window (1 hour) is 4× longer than LOGIN (15 minutes), which is appropriate given signup operations are typically more resource-intensive and warrant stricter rate limiting.

Also applies to: 44-50, 95-95, 103-103

Security fixes:
- Add JTI existence check before revocation validation (backward compat)
- Add rate limiting to OAuth GET endpoint (was missing)
- Fix stale X-RateLimit-Remaining after rate limit reset
- Use Promise.allSettled for rate limit resets (prevent 500 after auth)

Performance:
- Parallelize rate limit resets in login, signup, mobile/login routes
- Add OAUTH_VERIFY config constant to DISTRIBUTED_RATE_LIMITS

Tests:
- Add missing blockDurationMs and progressiveDelay to test mocks

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (1)
apps/web/src/app/api/auth/google/signin/route.ts (1)

75-76: Add startup validation for required OAuth environment variables or replace non-null assertions with explicit checks.

The non-null assertions (!) on GOOGLE_OAUTH_CLIENT_ID, GOOGLE_OAUTH_REDIRECT_URI, and OAUTH_STATE_SECRET will throw a generic runtime error if these environment variables are missing, with no meaningful error message. Additionally, the approach is inconsistent—callback/route.ts passes potentially undefined values to OAuth2Client (lines 25–27), while signin/route.ts uses non-null assertions (lines 62, 75–76, 112–113).

Consider either:

  1. Validating these required variables at application startup (Next.js config or initialization) and failing fast with a clear error, or
  2. Following the defensive pattern in oauth-utils.ts (line 21): check for missing variables explicitly and return/throw a meaningful error.

The oauth-utils.ts approach (checking if (!process.env.GOOGLE_OAUTH_CLIENT_ID)) should be the model for all OAuth-related code paths.

🧹 Nitpick comments (3)
apps/web/src/app/api/auth/__tests__/signup-redirect.test.ts (1)

108-108: Minor inconsistency in mock value.

The mock returns attemptsRemaining: 5, but DISTRIBUTED_RATE_LIMITS.SIGNUP.maxAttempts is 3. While this doesn't affect test correctness (since tests only check that the rate limit is allowed), it's inconsistent with the actual config.

🔎 Suggested fix
-    (checkDistributedRateLimit as Mock).mockResolvedValue({ allowed: true, attemptsRemaining: 5 });
+    (checkDistributedRateLimit as Mock).mockResolvedValue({ allowed: true, attemptsRemaining: 3 });
apps/web/src/app/api/auth/google/signin/route.ts (1)

29-32: Consider using OAUTH_VERIFY instead of LOGIN rate limit config.

The route uses DISTRIBUTED_RATE_LIMITS.LOGIN for OAuth signin rate limiting, but there's a dedicated OAUTH_VERIFY config (added in this PR) that might be more semantically appropriate. However, using LOGIN (5 attempts/15 min) is more restrictive than OAUTH_VERIFY (10 attempts/5 min), so this is a security-conscious choice.

apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts (1)

269-274: Partially addressed previous review - add failure logging for observability.

The code correctly uses Promise.allSettled to prevent rate limit reset failures from returning a 500 error after successful authentication. However, the previous review comment also suggested logging reset failures for observability, which was not implemented.

Without logging, operators cannot detect if rate limit resets are consistently failing (e.g., due to Redis connection issues), which could lead to legitimate users being rate-limited after successful authentication.

🔎 Complete the fix with failure logging
-// Reset rate limits on successful authentication (graceful - failures don't affect successful auth)
-await Promise.allSettled([
+// Reset rate limits on successful authentication (graceful - failures don't affect successful auth)
+const resetResults = await Promise.allSettled([
   resetDistributedRateLimit(`mobile:oauth:ip:${clientIP}`),
   resetDistributedRateLimit(`mobile:oauth:verify:${clientIP}`),
   resetDistributedRateLimit(`mobile:oauth:email:${userInfo.email.toLowerCase()}`),
 ]);
+
+// Log any reset failures but don't fail the request
+const failures = resetResults.filter((r) => r.status === 'rejected');
+if (failures.length > 0) {
+  loggers.auth.warn('Failed to reset some rate limits after successful auth', {
+    userId: user.id,
+    failures: failures.length,
+  });
+}

Based on the previous review comment addressing the same concern.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 54378f6 and e6dbf5c.

📒 Files selected for processing (10)
  • apps/web/src/app/api/auth/__tests__/google-callback-redirect.test.ts
  • apps/web/src/app/api/auth/__tests__/login-redirect.test.ts
  • apps/web/src/app/api/auth/__tests__/signup-redirect.test.ts
  • apps/web/src/app/api/auth/google/signin/route.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
  • apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
  • packages/lib/src/security/distributed-rate-limit.ts
  • packages/lib/src/services/service-auth.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/app/api/auth/tests/google-callback-redirect.test.ts
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Use camelCase for variable and function names
Use UPPER_SNAKE_CASE for constants
Use PascalCase for type and enum names
Use kebab-case for filenames, except React hooks (camelCase with use prefix), Zustand stores (camelCase with use prefix), and React components (PascalCase)
Lint with Next/ESLint as configured in apps/web/eslint.config.mjs
Message content should always use the message parts structure with { parts: [{ type: 'text', text: '...' }] }
Use centralized permission functions from @pagespace/lib/permissions (e.g., getUserAccessLevel, canUserEditPage) instead of implementing permission logic locally
Always use Drizzle client from @pagespace/db package for database access
Use ESM modules throughout the codebase

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Write code that is explicit over implicit and self-documenting

Files:

  • packages/lib/src/security/distributed-rate-limit.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/__tests__/login-redirect.test.ts
  • packages/lib/src/services/service-auth.ts
  • apps/web/src/app/api/auth/google/signin/route.ts
  • apps/web/src/app/api/auth/__tests__/signup-redirect.test.ts
  • apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: React hook files should use camelCase matching the exported hook name (e.g., useAuth.ts)
Zustand store files should use camelCase with use prefix (e.g., useAuthStore.ts)

Files:

  • packages/lib/src/security/distributed-rate-limit.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/__tests__/login-redirect.test.ts
  • packages/lib/src/services/service-auth.ts
  • apps/web/src/app/api/auth/google/signin/route.ts
  • apps/web/src/app/api/auth/__tests__/signup-redirect.test.ts
  • apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • packages/lib/src/security/distributed-rate-limit.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/__tests__/login-redirect.test.ts
  • packages/lib/src/services/service-auth.ts
  • apps/web/src/app/api/auth/google/signin/route.ts
  • apps/web/src/app/api/auth/__tests__/signup-redirect.test.ts
  • apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
apps/web/src/app/**/route.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/src/app/**/route.{ts,tsx}: In Next.js 15 route handlers, params in dynamic routes are Promise objects and MUST be awaited before destructuring
Use Response.json() or NextResponse.json() for returning JSON from route handlers
Get request body using const body = await request.json();
Get search parameters using const { searchParams } = new URL(request.url);

apps/web/src/app/**/route.{ts,tsx}: In Next.js 15 dynamic routes, params are Promise objects and MUST be awaited before destructuring: const { id } = await context.params;
In Route Handlers, get request body with const body = await request.json();
In Route Handlers, get search parameters with const { searchParams } = new URL(request.url);
In Route Handlers, return JSON using Response.json(data) or NextResponse.json(data)
For permission logic, use centralized functions from @pagespace/lib/permissions: getUserAccessLevel(), canUserEditPage()

Files:

  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/google/signin/route.ts
  • apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
apps/web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/**/*.{ts,tsx}: Use message parts structure for message content: { parts: [{ type: 'text', text: '...' }] }
For database access, always use Drizzle client from @pagespace/db: import { db, pages } from '@pagespace/db';
Use centralized Drizzle ORM with PostgreSQL for all database operations - no direct SQL or other ORMs
Use Socket.IO for real-time collaboration features - imported from the realtime service at port 3001
Use Vercel AI SDK with async/await for all AI operations and streaming
Use Next.js 15 App Router and TypeScript for all routes and components

Files:

  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/__tests__/login-redirect.test.ts
  • apps/web/src/app/api/auth/google/signin/route.ts
  • apps/web/src/app/api/auth/__tests__/signup-redirect.test.ts
  • apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
**/*auth*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*auth*.{ts,tsx}: Use custom JWT authentication with jose library for user management
Use bcryptjs for password hashing

Files:

  • packages/lib/src/services/service-auth.ts
🧠 Learnings (13)
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : For permission logic, use centralized functions from `pagespace/lib/permissions`: `getUserAccessLevel()`, `canUserEditPage()`

Applied to files:

  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/google/signin/route.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*.{ts,tsx} : Use centralized permission functions from `pagespace/lib/permissions` (e.g., `getUserAccessLevel`, `canUserEditPage`) instead of implementing permission logic locally

Applied to files:

  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/**/*.{ts,tsx} : For database access, always use Drizzle client from `pagespace/db`: `import { db, pages } from 'pagespace/db';`

Applied to files:

  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/__tests__/login-redirect.test.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to app/**/*.{ts,tsx} : Import and use `getUserAccessLevel()` and `canUserEditPage()` from `pagespace/lib/permissions` for centralized permission logic

Applied to files:

  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/**/*.ts : Always use centralized permission functions from `pagespace/lib/permissions` for access control logic (e.g., `getUserAccessLevel`, `canUserEditPage`)

Applied to files:

  • apps/web/src/app/api/auth/signup/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to **/*.{ts,tsx} : Always use the Drizzle client and database exports from `pagespace/db` (e.g., `import { db, pages } from 'pagespace/db'`) for all database access

Applied to files:

  • apps/web/src/app/api/auth/signup/route.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*auth*.{ts,tsx} : Use bcryptjs for password hashing

Applied to files:

  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/__tests__/login-redirect.test.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/**/*.ts : Always use Drizzle client and queries from `pagespace/db` for database access instead of direct queries

Applied to files:

  • apps/web/src/app/api/auth/signup/route.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*auth*.{ts,tsx} : Use custom JWT authentication with jose library for user management

Applied to files:

  • apps/web/src/app/api/auth/signup/route.ts
  • packages/lib/src/services/service-auth.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*.{ts,tsx} : Always use Drizzle client from `pagespace/db` package for database access

Applied to files:

  • apps/web/src/app/api/auth/signup/route.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : Get search parameters using `const { searchParams } = new URL(request.url);`

Applied to files:

  • apps/web/src/app/api/auth/google/signin/route.ts
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : In Route Handlers, get search parameters with `const { searchParams } = new URL(request.url);`

Applied to files:

  • apps/web/src/app/api/auth/google/signin/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to app/api/**/*.{ts,tsx} : Use `const body = await request.json();` to extract request bodies, `const { searchParams } = new URL(request.url);` for query parameters, and `return Response.json(data)` or `return NextResponse.json(data)` for responses

Applied to files:

  • apps/web/src/app/api/auth/google/signin/route.ts
🧬 Code graph analysis (4)
apps/web/src/app/api/auth/login/route.ts (1)
packages/lib/src/security/distributed-rate-limit.ts (3)
  • checkDistributedRateLimit (204-263)
  • DISTRIBUTED_RATE_LIMITS (319-368)
  • resetDistributedRateLimit (268-282)
apps/web/src/app/api/auth/__tests__/login-redirect.test.ts (1)
packages/lib/src/security/distributed-rate-limit.ts (1)
  • checkDistributedRateLimit (204-263)
apps/web/src/app/api/auth/google/signin/route.ts (1)
packages/lib/src/security/distributed-rate-limit.ts (2)
  • checkDistributedRateLimit (204-263)
  • DISTRIBUTED_RATE_LIMITS (319-368)
apps/web/src/app/api/auth/mobile/login/route.ts (1)
packages/lib/src/security/distributed-rate-limit.ts (3)
  • checkDistributedRateLimit (204-263)
  • DISTRIBUTED_RATE_LIMITS (319-368)
  • resetDistributedRateLimit (268-282)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Unit Tests
  • GitHub Check: Static Security Analysis
🔇 Additional comments (27)
packages/lib/src/services/service-auth.ts (5)

1-4: LGTM! Imports align with JTI tracking requirements.

The new security utilities and logging imports support the Redis-backed JTI tracking feature as described in the PR objectives.


48-105: LGTM! Duration parsing is robust with proper validation.

The durationToSeconds function includes comprehensive validation:

  • Format validation via regex
  • Positive value enforcement
  • 30-day maximum cap with logging
  • Safe fallback to 5-minute default

This ensures service tokens have validated, bounded lifetimes.


123-134: LGTM! JTI correctly added to reserved claims.

This prevents accidental or malicious override of the JTI field via additionalClaims.


136-201: LGTM! JTI recording follows fail-closed pattern correctly.

The implementation ensures:

  • Cryptographically secure JTI generation via cuid2
  • Validated token duration (capped at 30 days)
  • Redis-backed JTI tracking with proper expiration
  • Fail-closed behavior in production (token issuance fails if Redis unavailable)
  • Graceful degradation in dev/test with appropriate logging

The error handling at lines 180-198 correctly implements the security posture described in the PR objectives.


239-259: LGTM! Past review comment addressed—JTI validation is now correct.

The implementation properly handles:

  • JTI existence check (line 244): validates claims.jti is a non-empty string before calling isJTIRevoked
  • Backward compatibility (lines 249-256): legacy tokens without JTI are accepted with debug logging
  • Fail-closed behavior (lines 257-259): production requires Redis availability

This addresses the previous review concern about potential type mismatches when claims.jti is undefined.

packages/lib/src/security/distributed-rate-limit.ts (1)

344-349: LGTM!

The new OAUTH_VERIFY rate limit configuration is well-structured and appropriately configured. The limits (10 attempts in 5 minutes) are reasonable for OAuth verification flows and consistent with the REFRESH configuration pattern.

apps/web/src/app/api/auth/mobile/login/route.ts (4)

10-14: LGTM!

Clean import of distributed rate limiting utilities from the centralized security module.


46-50: LGTM!

Parallel rate limit checks using Promise.all is an efficient approach. Both IP-based and email-based limits are properly scoped with appropriate key prefixes.


116-120: LGTM!

Using Promise.allSettled for rate limit resets is the correct pattern - it ensures that a failure in one reset doesn't prevent the other from completing, and neither blocks the successful authentication response.


150-154: LGTM - past issue resolved.

The X-RateLimit-Remaining header now correctly reports maxAttempts after a successful login and rate limit reset, rather than using the stale pre-reset values. This aligns with the web login route behavior.

apps/web/src/app/api/auth/__tests__/login-redirect.test.ts (2)

42-53: LGTM!

The mock configuration for @pagespace/lib/security correctly mirrors the actual DISTRIBUTED_RATE_LIMITS.LOGIN configuration values, ensuring tests accurately reflect production behavior.


92-92: LGTM!

The checkDistributedRateLimit mock is correctly configured with mockResolvedValue to simulate an allowed rate limit check with 5 remaining attempts.

apps/web/src/app/api/auth/__tests__/signup-redirect.test.ts (1)

44-55: LGTM!

The mock configuration for DISTRIBUTED_RATE_LIMITS.SIGNUP correctly reflects the actual signup rate limit configuration (3 attempts per hour).

apps/web/src/app/api/auth/google/signin/route.ts (2)

3-6: LGTM!

Clean import of distributed rate limiting utilities.


94-108: Good addition of rate limiting to GET endpoint.

Adding rate limiting to the GET handler is an important security improvement that prevents automated OAuth initiation attacks. The redirect-based response is appropriate for browser flows.

apps/web/src/app/api/auth/login/route.ts (4)

10-14: LGTM!

Clean import of distributed rate limiting utilities from the centralized security module.


99-103: LGTM!

Parallel rate limit checks are correctly implemented with appropriate key prefixes for IP and email-based limiting.


190-194: LGTM!

Using Promise.allSettled ensures rate limit reset failures don't block the successful authentication response. This is the correct resilience pattern.


229-231: LGTM!

Rate limit headers correctly report maxAttempts for both limit and remaining after a successful login with rate limit reset.

apps/web/src/app/api/auth/signup/route.ts (4)

5-9: LGTM!

Clean import of distributed rate limiting utilities from the centralized security module.


113-117: LGTM!

Parallel rate limit checks with appropriate key prefixes. Using signup: prefix correctly separates these limits from login attempts.


119-153: LGTM!

The 429 responses include appropriate headers with the 1-hour default Retry-After (3600 seconds), which matches the SIGNUP configuration's blockDurationMs.


199-203: LGTM!

Using Promise.allSettled for rate limit resets is the correct pattern - ensuring reset failures don't affect the successful signup flow.

apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts (4)

59-63: LGTM - Distributed rate limiting imports are correct.

The imports properly bring in the distributed rate limiting utilities needed for the refactored implementation.


111-130: LGTM - IP-based distributed rate limiting implemented correctly.

The implementation properly uses distributed rate limiting with appropriate response headers and fallback values.


134-160: LGTM - OAuth verification rate limiting implemented correctly.

The OAuth-specific rate limiting adds defense-in-depth against token verification spam, with proper response headers.


195-214: LGTM - Email-based rate limiting implemented correctly.

Email normalization with toLowerCase() ensures consistent rate limiting, and the implementation properly uses distributed rate limit values.

@2witstudios

Copy link
Copy Markdown
Owner Author

Code Review Remediation Complete

All actionable comments from the code review have been addressed in commit e6dbf5c:

Fixed Issues:

Security (HIGH priority):

  • ✅ Added JTI existence check before isJTIRevoked() - backward compatible with pre-tracking tokens
  • ✅ Added rate limiting to OAuth GET endpoint (google/signin/route.ts)
  • ✅ Fixed stale X-RateLimit-Remaining to use maxAttempts after reset
  • ✅ Changed Promise.all → Promise.allSettled for rate limit resets to prevent 500 after successful auth

Performance (MEDIUM priority):

  • ✅ Parallelized rate limit resets in login/route.ts, signup/route.ts, mobile/login/route.ts
  • ✅ Added OAUTH_VERIFY config constant to DISTRIBUTED_RATE_LIMITS
  • ✅ Updated mobile/oauth/google/exchange/route.ts to use the constant

Tests (LOW priority):

  • ✅ Added missing blockDurationMs and progressiveDelay to test mocks

Note on X-RateLimit Headers for OAuth Callback:

The OAuth callback route uses HTTP redirects (307), which don't preserve custom headers in the browser's redirect chain. Adding X-RateLimit headers to redirect responses wouldn't be visible to clients. The rate limiting still works correctly - only the headers aren't exposed on redirects.

- Add failure logging for Promise.allSettled rate limit resets
  - mobile/oauth/google/exchange/route.ts
  - login/route.ts
  - signup/route.ts
  - mobile/login/route.ts
- Add OAuth environment variable validation in google/signin/route.ts
  - Validate GOOGLE_OAUTH_CLIENT_ID, GOOGLE_OAUTH_REDIRECT_URI, OAUTH_STATE_SECRET
  - Return appropriate error responses instead of throwing on missing vars
- Fix test mock attemptsRemaining value (5 → 3) to match SIGNUP config

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@2witstudios

Copy link
Copy Markdown
Owner Author

Code Review Remediation - Round 2 Complete

Additional feedback addressed in commit d41c172:

Fixed Issues:

Observability (added per review feedback):

  • ✅ Added failure logging for Promise.allSettled rate limit resets in 4 routes:
    • mobile/oauth/google/exchange/route.ts
    • login/route.ts
    • signup/route.ts
    • mobile/login/route.ts
  • Now logs warning with failure count and reasons when resets fail

Security (configuration):

  • ✅ Added OAuth environment variable validation in google/signin/route.ts (both POST and GET handlers)
  • Prevents runtime errors from non-null assertions (!) when env vars missing
  • Returns appropriate error responses with diagnostic logging

Tests (mock consistency):

  • ✅ Fixed attemptsRemaining mock value in signup-redirect.test.ts: 5 → 3 (matches DISTRIBUTED_RATE_LIMITS.SIGNUP.maxAttempts)

All tests pass (194 auth tests) and typecheck succeeds.

@coderabbitai coderabbitai 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.

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (1)
apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts (1)

111-214: Standardize rate limit key naming across auth routes for consistency and maintainability.

The rate limit keys in the mobile OAuth exchange route use inconsistent patterns compared to other auth routes. Mobile login/signup use login:ip: and signup:ip: (without platform prefix), while mobile OAuth uses mobile:oauth:ip:, and mobile refresh uses refresh:device:ip:. Align the naming convention across all mobile auth routes to avoid confusion and potential key conflicts—consider whether mobile routes should consistently use a platform prefix (e.g., mobile:login:ip: for all) or omit it entirely to match web routes.

🧹 Nitpick comments (1)
apps/web/src/app/api/auth/google/signin/route.ts (1)

38-40: Extract IP extraction logic to a utility function.

The IP extraction logic is duplicated across multiple auth routes (google/signin, mobile/login, signup). Consider extracting this to a shared utility function.

🔎 Proposed refactor

Create a utility function in @pagespace/lib/server or a local utils file:

export function getClientIP(req: Request): string {
  return req.headers.get('x-forwarded-for')?.split(',')[0] ||
         req.headers.get('x-real-ip') ||
         'unknown';
}

Then use it in all auth routes:

-const clientIP = req.headers.get('x-forwarded-for')?.split(',')[0] ||
-                 req.headers.get('x-real-ip') ||
-                 'unknown';
+const clientIP = getClientIP(req);
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e6dbf5c and d41c172.

📒 Files selected for processing (6)
  • apps/web/src/app/api/auth/__tests__/signup-redirect.test.ts
  • apps/web/src/app/api/auth/google/signin/route.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
  • apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/src/app/api/auth/tests/signup-redirect.test.ts
  • apps/web/src/app/api/auth/login/route.ts
🧰 Additional context used
📓 Path-based instructions (5)
apps/web/src/app/**/route.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/src/app/**/route.{ts,tsx}: In Next.js 15 route handlers, params in dynamic routes are Promise objects and MUST be awaited before destructuring
Use Response.json() or NextResponse.json() for returning JSON from route handlers
Get request body using const body = await request.json();
Get search parameters using const { searchParams } = new URL(request.url);

apps/web/src/app/**/route.{ts,tsx}: In Next.js 15 dynamic routes, params are Promise objects and MUST be awaited before destructuring: const { id } = await context.params;
In Route Handlers, get request body with const body = await request.json();
In Route Handlers, get search parameters with const { searchParams } = new URL(request.url);
In Route Handlers, return JSON using Response.json(data) or NextResponse.json(data)
For permission logic, use centralized functions from @pagespace/lib/permissions: getUserAccessLevel(), canUserEditPage()

Files:

  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/google/signin/route.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
  • apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Use camelCase for variable and function names
Use UPPER_SNAKE_CASE for constants
Use PascalCase for type and enum names
Use kebab-case for filenames, except React hooks (camelCase with use prefix), Zustand stores (camelCase with use prefix), and React components (PascalCase)
Lint with Next/ESLint as configured in apps/web/eslint.config.mjs
Message content should always use the message parts structure with { parts: [{ type: 'text', text: '...' }] }
Use centralized permission functions from @pagespace/lib/permissions (e.g., getUserAccessLevel, canUserEditPage) instead of implementing permission logic locally
Always use Drizzle client from @pagespace/db package for database access
Use ESM modules throughout the codebase

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Write code that is explicit over implicit and self-documenting

Files:

  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/google/signin/route.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
  • apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: React hook files should use camelCase matching the exported hook name (e.g., useAuth.ts)
Zustand store files should use camelCase with use prefix (e.g., useAuthStore.ts)

Files:

  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/google/signin/route.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
  • apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/google/signin/route.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
  • apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts
apps/web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/**/*.{ts,tsx}: Use message parts structure for message content: { parts: [{ type: 'text', text: '...' }] }
For database access, always use Drizzle client from @pagespace/db: import { db, pages } from '@pagespace/db';
Use centralized Drizzle ORM with PostgreSQL for all database operations - no direct SQL or other ORMs
Use Socket.IO for real-time collaboration features - imported from the realtime service at port 3001
Use Vercel AI SDK with async/await for all AI operations and streaming
Use Next.js 15 App Router and TypeScript for all routes and components

Files:

  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/google/signin/route.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
  • apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts
🧠 Learnings (13)
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/**/*.{ts,tsx} : For database access, always use Drizzle client from `pagespace/db`: `import { db, pages } from 'pagespace/db';`

Applied to files:

  • apps/web/src/app/api/auth/signup/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to app/**/*.{ts,tsx} : Import and use `getUserAccessLevel()` and `canUserEditPage()` from `pagespace/lib/permissions` for centralized permission logic

Applied to files:

  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/**/*.ts : Always use centralized permission functions from `pagespace/lib/permissions` for access control logic (e.g., `getUserAccessLevel`, `canUserEditPage`)

Applied to files:

  • apps/web/src/app/api/auth/signup/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to **/*.{ts,tsx} : Always use the Drizzle client and database exports from `pagespace/db` (e.g., `import { db, pages } from 'pagespace/db'`) for all database access

Applied to files:

  • apps/web/src/app/api/auth/signup/route.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*auth*.{ts,tsx} : Use bcryptjs for password hashing

Applied to files:

  • apps/web/src/app/api/auth/signup/route.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*.{ts,tsx} : Use centralized permission functions from `pagespace/lib/permissions` (e.g., `getUserAccessLevel`, `canUserEditPage`) instead of implementing permission logic locally

Applied to files:

  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/**/*.ts : Always use Drizzle client and queries from `pagespace/db` for database access instead of direct queries

Applied to files:

  • apps/web/src/app/api/auth/signup/route.ts
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : For permission logic, use centralized functions from `pagespace/lib/permissions`: `getUserAccessLevel()`, `canUserEditPage()`

Applied to files:

  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/google/signin/route.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*auth*.{ts,tsx} : Use custom JWT authentication with jose library for user management

Applied to files:

  • apps/web/src/app/api/auth/signup/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: This is a monorepo using pnpm workspaces with structure: `apps/web` (Next.js frontend/backend), `apps/realtime` (Socket.IO service), `apps/processor` (Express file/OCR pipeline), `packages/db` (Drizzle ORM), `packages/lib` (shared utilities)

Applied to files:

  • apps/web/src/app/api/auth/signup/route.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : Get search parameters using `const { searchParams } = new URL(request.url);`

Applied to files:

  • apps/web/src/app/api/auth/google/signin/route.ts
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : In Route Handlers, get search parameters with `const { searchParams } = new URL(request.url);`

Applied to files:

  • apps/web/src/app/api/auth/google/signin/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to app/api/**/*.{ts,tsx} : Use `const body = await request.json();` to extract request bodies, `const { searchParams } = new URL(request.url);` for query parameters, and `return Response.json(data)` or `return NextResponse.json(data)` for responses

Applied to files:

  • apps/web/src/app/api/auth/google/signin/route.ts
🧬 Code graph analysis (3)
apps/web/src/app/api/auth/signup/route.ts (2)
packages/lib/src/security/distributed-rate-limit.ts (3)
  • checkDistributedRateLimit (204-263)
  • DISTRIBUTED_RATE_LIMITS (319-368)
  • resetDistributedRateLimit (268-282)
packages/lib/src/logging/logger-config.ts (2)
  • logAuthEvent (154-174)
  • loggers (8-18)
apps/web/src/app/api/auth/google/signin/route.ts (2)
packages/lib/src/logging/logger-config.ts (1)
  • loggers (8-18)
packages/lib/src/security/distributed-rate-limit.ts (2)
  • checkDistributedRateLimit (204-263)
  • DISTRIBUTED_RATE_LIMITS (319-368)
apps/web/src/app/api/auth/mobile/login/route.ts (1)
packages/lib/src/security/distributed-rate-limit.ts (3)
  • checkDistributedRateLimit (204-263)
  • DISTRIBUTED_RATE_LIMITS (319-368)
  • resetDistributedRateLimit (268-282)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Unit Tests
  • GitHub Check: Lint & TypeScript Check
🔇 Additional comments (13)
apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts (5)

195-214: LGTM: Email rate limiting implementation is correct.

The email-based rate limiting properly uses lowercased email addresses for consistency and shares the LOGIN configuration with IP-based limits, which is appropriate for login attempts.


269-283: LGTM: Rate limit reset handles failures gracefully.

The use of Promise.allSettled ensures rate limit reset failures don't affect the successful authentication response. The error logging provides good observability while maintaining graceful degradation.

This correctly addresses the past review comment about preventing 500 errors when Redis is unavailable.


111-130: The fallback retry-after value of 900 seconds correctly matches the DISTRIBUTED_RATE_LIMITS.LOGIN configuration (windowMs: 15 * 60 * 1000 milliseconds). No consistency issue exists—the implementation properly handles rate-limit headers during Redis unavailability.


134-160: The fallback retry-after value of 300 seconds is correctly aligned with DISTRIBUTED_RATE_LIMITS.OAUTH_VERIFY.windowMs (5 × 60 × 1000 milliseconds). The implementation is proper.


59-63: The review comment's verification concerns have been resolved. DISTRIBUTED_RATE_LIMITS.OAUTH_VERIFY is properly defined and exported from @pagespace/lib/security with appropriate configuration (maxAttempts: 10, windowMs: 5 minutes, blockDurationMs: 5 minutes). The fallback retry-after values in the route handler correctly align with their respective rate limit window configurations: IP and email limits use 900 seconds (matching LOGIN's 15-minute window), and OAuth verification uses 300 seconds (matching OAUTH_VERIFY's 5-minute window).

apps/web/src/app/api/auth/google/signin/route.ts (2)

17-28: LGTM - OAuth environment validation.

The environment variable validation is thorough and includes proper logging for observability. The 500 response is appropriate for a configuration error.


107-152: LGTM - GET handler with appropriate redirect flow.

The GET handler correctly:

  • Validates only the OAuth environment variables it needs (CLIENT_ID and REDIRECT_URI, not STATE_SECRET since no state parameter is used)
  • Enforces rate limiting before generating the OAuth URL
  • Uses redirects for error handling (appropriate for a GET endpoint)

Note: X-RateLimit headers are not set because they won't be preserved through the redirect chain, as documented in the PR objectives.

apps/web/src/app/api/auth/mobile/login/route.ts (3)

46-84: LGTM - Parallel distributed rate limiting.

The parallel rate limit checks using Promise.all provide good performance while enforcing both IP-based and email-based limits. Error responses correctly include Retry-After and X-RateLimit-* headers.


117-129: LGTM - Robust rate limit reset with observability.

The use of Promise.allSettled ensures that rate limit reset failures don't block successful authentication. The failure logging provides good observability while maintaining a graceful user experience.


159-174: LGTM - Correct X-RateLimit headers after reset.

The headers correctly reflect the post-reset state by using DISTRIBUTED_RATE_LIMITS.LOGIN.maxAttempts for X-RateLimit-Remaining, as addressed in commit e6dbf5c. This is consistent with other auth routes.

apps/web/src/app/api/auth/signup/route.ts (3)

113-153: LGTM - Distributed rate limiting for signup.

The distributed rate limiting correctly uses DISTRIBUTED_RATE_LIMITS.SIGNUP config (3 attempts per hour, stricter than login's 5 per 15 minutes). The fallback retryAfter value of 3600 seconds matches the SIGNUP config's 1-hour block duration.


199-212: LGTM - Consistent rate limit reset pattern.

The rate limit reset logic follows the same robust pattern as other auth routes, using Promise.allSettled for graceful failure handling and logging failures for observability.


45-344: LGTM - Well-structured signup flow with security layers.

The signup implementation correctly integrates distributed rate limiting into a comprehensive flow that includes:

  • Login CSRF protection
  • Rate limiting before expensive operations (user existence check, password hashing)
  • Graceful rate limit reset
  • Non-blocking verification email

The security layers are well-implemented and consistent with the other auth routes.

Code review feedback (Round 3):

1. Extract IP extraction to getClientIP utility function
   - Added getClientIP() in auth-helpers.ts
   - Centralized IP extraction logic (x-forwarded-for, x-real-ip, fallback)
   - Updated all 12 auth routes to use the utility
   - Reduces code duplication and ensures consistent behavior

2. Standardize rate limit key naming in mobile OAuth exchange
   - Changed from `mobile:oauth:*` to `oauth:exchange:*`
   - Now matches pattern of `oauth:signin:*` and `oauth:callback:*`
   - Consistent `{action}:{subaction}:{scope}` naming convention

Files changed:
- auth-helpers.ts: New getClientIP utility function
- auth/index.ts: Export getClientIP and validateLoginCSRFToken
- 12 auth routes: Use getClientIP instead of inline extraction
- mobile/oauth/google/exchange/route.ts: Standardized rate limit keys
- logout.test.ts: Updated mock to use importOriginal pattern

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@2witstudios

Copy link
Copy Markdown
Owner Author

Code Review Remediation - Round 3 Complete

Additional feedback addressed in commit 11fe272:

1. IP Extraction Utility Function

  • Created getClientIP(request) utility in auth-helpers.ts
  • Centralized IP extraction logic: x-forwarded-for → x-real-ip → 'unknown'
  • Updated all 12 auth routes to use the utility
  • Reduces code duplication and ensures consistent behavior across routes

2. Rate Limit Key Naming Standardization

  • Changed mobile OAuth exchange keys from mobile:oauth:* to oauth:exchange:*
  • Now matches pattern used elsewhere: oauth:signin:*, oauth:callback:*
  • Consistent naming convention: {action}:{subaction}:{scope}

Key pattern summary:

Route Pattern
Login (web/mobile) login:ip:, login:email:
Signup (web/mobile) signup:ip:, signup:email:
OAuth signin oauth:signin:ip:
OAuth callback oauth:callback:ip:
OAuth exchange oauth:exchange:ip:, oauth:exchange:verify:, oauth:exchange:email:
Refresh refresh:ip:, refresh:device:ip:

All tests pass (194 auth tests) and typecheck succeeds.

@coderabbitai coderabbitai 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.

Actionable comments posted: 0

🧹 Nitpick comments (5)
apps/web/src/app/api/auth/mobile/refresh/route.ts (1)

134-139: Missing X-RateLimit headers on success response.

Other routes (e.g., apps/web/src/app/api/auth/refresh/route.ts lines 173-175, apps/web/src/app/api/auth/login/route.ts lines 236-238) include X-RateLimit-Limit and X-RateLimit-Remaining headers on successful responses. This route omits them, creating inconsistent API behavior for clients.

🔎 Proposed fix
     // Reset rate limit on successful refresh
     await resetDistributedRateLimit(`refresh:device:ip:${clientIP}`);

     // Return tokens (device-token-only pattern - no refreshToken)
-    return Response.json({
-      token: accessToken,
-      csrfToken,
-      deviceToken: activeDeviceToken,
-    }, { status: 200 });
+    return Response.json(
+      {
+        token: accessToken,
+        csrfToken,
+        deviceToken: activeDeviceToken,
+      },
+      {
+        status: 200,
+        headers: {
+          'X-RateLimit-Limit': String(DISTRIBUTED_RATE_LIMITS.REFRESH.maxAttempts),
+          'X-RateLimit-Remaining': String(DISTRIBUTED_RATE_LIMITS.REFRESH.maxAttempts),
+        },
+      }
+    );
apps/web/src/app/api/auth/refresh/route.ts (1)

167-175: Consider adding observability for reset failures.

Other routes (login, signup) wrap resets in Promise.allSettled and log failures. While this route only has one reset, an unhandled rejection from resetDistributedRateLimit could cause issues. For consistency and observability, consider wrapping in try-catch or using the same pattern.

🔎 Proposed fix
   // Reset rate limit on successful refresh
-  await resetDistributedRateLimit(`refresh:ip:${clientIP}`);
+  try {
+    await resetDistributedRateLimit(`refresh:ip:${clientIP}`);
+  } catch (error) {
+    loggers.auth.warn('Rate limit reset failed after successful refresh', {
+      error: error instanceof Error ? error.message : String(error),
+    });
+  }

Note: You'll need to add loggers to the imports from @pagespace/lib/server.

apps/web/src/app/api/auth/device/refresh/route.ts (2)

197-199: Consider adding observability for reset failures.

For consistency with login/signup routes that use Promise.allSettled and log failures, consider wrapping this reset in error handling to avoid silent failures and improve observability.

🔎 Proposed fix
     // Reset rate limit on successful refresh
-    await resetDistributedRateLimit(`refresh:device:ip:${clientIP}`);
+    try {
+      await resetDistributedRateLimit(`refresh:device:ip:${clientIP}`);
+    } catch (error) {
+      loggers.auth.warn('Rate limit reset failed after successful device refresh', {
+        error: error instanceof Error ? error.message : String(error),
+      });
+    }

229-245: Missing X-RateLimit headers on success responses.

Both the web platform (lines 229-236) and mobile/desktop (lines 240-245) response paths omit X-RateLimit-Limit and X-RateLimit-Remaining headers that are included in other routes like login/route.ts and refresh/route.ts.

🔎 Proposed fix for web platform response
       const headers = new Headers();
       headers.append('Set-Cookie', accessTokenCookie);
       headers.append('Set-Cookie', refreshTokenCookie);
+      headers.set('X-RateLimit-Limit', String(DISTRIBUTED_RATE_LIMITS.REFRESH.maxAttempts));
+      headers.set('X-RateLimit-Remaining', String(DISTRIBUTED_RATE_LIMITS.REFRESH.maxAttempts));

       return Response.json(
🔎 Proposed fix for mobile/desktop response
     // For mobile/desktop, return tokens in JSON (existing behavior)
-    return Response.json({
-      token: accessToken,
-      refreshToken,
-      csrfToken,
-      deviceToken: activeDeviceToken,
-    });
+    return Response.json(
+      {
+        token: accessToken,
+        refreshToken,
+        csrfToken,
+        deviceToken: activeDeviceToken,
+      },
+      {
+        headers: {
+          'X-RateLimit-Limit': String(DISTRIBUTED_RATE_LIMITS.REFRESH.maxAttempts),
+          'X-RateLimit-Remaining': String(DISTRIBUTED_RATE_LIMITS.REFRESH.maxAttempts),
+        },
+      }
+    );
apps/web/src/app/api/auth/google/callback/route.ts (1)

233-234: Consider adding observability for reset failures.

For consistency with login/signup routes and to aid debugging, consider wrapping this reset in error handling.

🔎 Proposed fix
     // Reset rate limits on successful login
-    await resetDistributedRateLimit(`oauth:callback:ip:${clientIP}`);
+    try {
+      await resetDistributedRateLimit(`oauth:callback:ip:${clientIP}`);
+    } catch (error) {
+      loggers.auth.warn('Rate limit reset failed after successful OAuth callback', {
+        error: error instanceof Error ? error.message : String(error),
+      });
+    }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d41c172 and 11fe272.

📒 Files selected for processing (14)
  • apps/web/src/app/api/auth/__tests__/logout.test.ts
  • apps/web/src/app/api/auth/device/refresh/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/google/signin/route.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/logout/route.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
  • apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts
  • apps/web/src/app/api/auth/mobile/refresh/route.ts
  • apps/web/src/app/api/auth/mobile/signup/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/lib/auth/auth-helpers.ts
  • apps/web/src/lib/auth/index.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/src/app/api/auth/mobile/signup/route.ts
  • apps/web/src/app/api/auth/mobile/login/route.ts
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Use camelCase for variable and function names
Use UPPER_SNAKE_CASE for constants
Use PascalCase for type and enum names
Use kebab-case for filenames, except React hooks (camelCase with use prefix), Zustand stores (camelCase with use prefix), and React components (PascalCase)
Lint with Next/ESLint as configured in apps/web/eslint.config.mjs
Message content should always use the message parts structure with { parts: [{ type: 'text', text: '...' }] }
Use centralized permission functions from @pagespace/lib/permissions (e.g., getUserAccessLevel, canUserEditPage) instead of implementing permission logic locally
Always use Drizzle client from @pagespace/db package for database access
Use ESM modules throughout the codebase

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Write code that is explicit over implicit and self-documenting

Files:

  • apps/web/src/lib/auth/index.ts
  • apps/web/src/lib/auth/auth-helpers.ts
  • apps/web/src/app/api/auth/__tests__/logout.test.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/logout/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/device/refresh/route.ts
  • apps/web/src/app/api/auth/google/signin/route.ts
  • apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts
  • apps/web/src/app/api/auth/mobile/refresh/route.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: React hook files should use camelCase matching the exported hook name (e.g., useAuth.ts)
Zustand store files should use camelCase with use prefix (e.g., useAuthStore.ts)

Files:

  • apps/web/src/lib/auth/index.ts
  • apps/web/src/lib/auth/auth-helpers.ts
  • apps/web/src/app/api/auth/__tests__/logout.test.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/logout/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/device/refresh/route.ts
  • apps/web/src/app/api/auth/google/signin/route.ts
  • apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts
  • apps/web/src/app/api/auth/mobile/refresh/route.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • apps/web/src/lib/auth/index.ts
  • apps/web/src/lib/auth/auth-helpers.ts
  • apps/web/src/app/api/auth/__tests__/logout.test.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/logout/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/device/refresh/route.ts
  • apps/web/src/app/api/auth/google/signin/route.ts
  • apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts
  • apps/web/src/app/api/auth/mobile/refresh/route.ts
apps/web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/**/*.{ts,tsx}: Use message parts structure for message content: { parts: [{ type: 'text', text: '...' }] }
For database access, always use Drizzle client from @pagespace/db: import { db, pages } from '@pagespace/db';
Use centralized Drizzle ORM with PostgreSQL for all database operations - no direct SQL or other ORMs
Use Socket.IO for real-time collaboration features - imported from the realtime service at port 3001
Use Vercel AI SDK with async/await for all AI operations and streaming
Use Next.js 15 App Router and TypeScript for all routes and components

Files:

  • apps/web/src/lib/auth/index.ts
  • apps/web/src/lib/auth/auth-helpers.ts
  • apps/web/src/app/api/auth/__tests__/logout.test.ts
  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/logout/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/device/refresh/route.ts
  • apps/web/src/app/api/auth/google/signin/route.ts
  • apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts
  • apps/web/src/app/api/auth/mobile/refresh/route.ts
**/*auth*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*auth*.{ts,tsx}: Use custom JWT authentication with jose library for user management
Use bcryptjs for password hashing

Files:

  • apps/web/src/lib/auth/auth-helpers.ts
apps/web/src/app/**/route.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/src/app/**/route.{ts,tsx}: In Next.js 15 route handlers, params in dynamic routes are Promise objects and MUST be awaited before destructuring
Use Response.json() or NextResponse.json() for returning JSON from route handlers
Get request body using const body = await request.json();
Get search parameters using const { searchParams } = new URL(request.url);

apps/web/src/app/**/route.{ts,tsx}: In Next.js 15 dynamic routes, params are Promise objects and MUST be awaited before destructuring: const { id } = await context.params;
In Route Handlers, get request body with const body = await request.json();
In Route Handlers, get search parameters with const { searchParams } = new URL(request.url);
In Route Handlers, return JSON using Response.json(data) or NextResponse.json(data)
For permission logic, use centralized functions from @pagespace/lib/permissions: getUserAccessLevel(), canUserEditPage()

Files:

  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/logout/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/device/refresh/route.ts
  • apps/web/src/app/api/auth/google/signin/route.ts
  • apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts
  • apps/web/src/app/api/auth/mobile/refresh/route.ts
🧠 Learnings (14)
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : For permission logic, use centralized functions from `pagespace/lib/permissions`: `getUserAccessLevel()`, `canUserEditPage()`

Applied to files:

  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/google/signin/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to app/**/*.{ts,tsx} : Import and use `getUserAccessLevel()` and `canUserEditPage()` from `pagespace/lib/permissions` for centralized permission logic

Applied to files:

  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/logout/route.ts
  • apps/web/src/app/api/auth/device/refresh/route.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/**/*.ts : Always use centralized permission functions from `pagespace/lib/permissions` for access control logic (e.g., `getUserAccessLevel`, `canUserEditPage`)

Applied to files:

  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*.{ts,tsx} : Use centralized permission functions from `pagespace/lib/permissions` (e.g., `getUserAccessLevel`, `canUserEditPage`) instead of implementing permission logic locally

Applied to files:

  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/logout/route.ts
  • apps/web/src/app/api/auth/device/refresh/route.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*auth*.{ts,tsx} : Use custom JWT authentication with jose library for user management

Applied to files:

  • apps/web/src/app/api/auth/login/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/logout/route.ts
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/**/*.{ts,tsx} : For database access, always use Drizzle client from `pagespace/db`: `import { db, pages } from 'pagespace/db';`

Applied to files:

  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to **/*.{ts,tsx} : Always use the Drizzle client and database exports from `pagespace/db` (e.g., `import { db, pages } from 'pagespace/db'`) for all database access

Applied to files:

  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/**/*.ts : Always use Drizzle client and queries from `pagespace/db` for database access instead of direct queries

Applied to files:

  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/signup/route.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*.{ts,tsx} : Always use Drizzle client from `pagespace/db` package for database access

Applied to files:

  • apps/web/src/app/api/auth/google/callback/route.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*auth*.{ts,tsx} : Use bcryptjs for password hashing

Applied to files:

  • apps/web/src/app/api/auth/signup/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: This is a monorepo using pnpm workspaces with structure: `apps/web` (Next.js frontend/backend), `apps/realtime` (Socket.IO service), `apps/processor` (Express file/OCR pipeline), `packages/db` (Drizzle ORM), `packages/lib` (shared utilities)

Applied to files:

  • apps/web/src/app/api/auth/signup/route.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : Get search parameters using `const { searchParams } = new URL(request.url);`

Applied to files:

  • apps/web/src/app/api/auth/google/signin/route.ts
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : In Route Handlers, get search parameters with `const { searchParams } = new URL(request.url);`

Applied to files:

  • apps/web/src/app/api/auth/google/signin/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to app/api/**/*.{ts,tsx} : Use `const body = await request.json();` to extract request bodies, `const { searchParams } = new URL(request.url);` for query parameters, and `return Response.json(data)` or `return NextResponse.json(data)` for responses

Applied to files:

  • apps/web/src/app/api/auth/google/signin/route.ts
🧬 Code graph analysis (9)
apps/web/src/lib/auth/auth-helpers.ts (1)
apps/web/src/lib/auth/index.ts (1)
  • getClientIP (300-300)
apps/web/src/app/api/auth/login/route.ts (4)
apps/web/src/lib/auth/auth-helpers.ts (1)
  • getClientIP (15-21)
apps/web/src/lib/auth/index.ts (1)
  • getClientIP (300-300)
packages/lib/src/security/distributed-rate-limit.ts (3)
  • checkDistributedRateLimit (204-263)
  • DISTRIBUTED_RATE_LIMITS (319-368)
  • resetDistributedRateLimit (268-282)
packages/lib/src/logging/logger-config.ts (1)
  • loggers (8-18)
apps/web/src/app/api/auth/signup/route.ts (4)
apps/web/src/lib/auth/auth-helpers.ts (1)
  • getClientIP (15-21)
apps/web/src/lib/auth/index.ts (1)
  • getClientIP (300-300)
packages/lib/src/security/distributed-rate-limit.ts (3)
  • checkDistributedRateLimit (204-263)
  • DISTRIBUTED_RATE_LIMITS (319-368)
  • resetDistributedRateLimit (268-282)
packages/lib/src/logging/logger-config.ts (2)
  • logAuthEvent (154-174)
  • loggers (8-18)
apps/web/src/app/api/auth/logout/route.ts (1)
apps/web/src/lib/auth/auth-helpers.ts (1)
  • getClientIP (15-21)
apps/web/src/app/api/auth/refresh/route.ts (2)
apps/web/src/lib/auth/auth-helpers.ts (1)
  • getClientIP (15-21)
apps/web/src/lib/auth/index.ts (1)
  • getClientIP (300-300)
apps/web/src/app/api/auth/device/refresh/route.ts (1)
apps/web/src/lib/auth/auth-helpers.ts (1)
  • getClientIP (15-21)
apps/web/src/app/api/auth/google/signin/route.ts (4)
packages/lib/src/logging/logger-config.ts (1)
  • loggers (8-18)
apps/web/src/lib/auth/auth-helpers.ts (1)
  • getClientIP (15-21)
apps/web/src/lib/auth/index.ts (1)
  • getClientIP (300-300)
packages/lib/src/security/distributed-rate-limit.ts (2)
  • checkDistributedRateLimit (204-263)
  • DISTRIBUTED_RATE_LIMITS (319-368)
apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts (3)
apps/web/src/lib/auth/auth-helpers.ts (1)
  • getClientIP (15-21)
apps/web/src/lib/auth/index.ts (1)
  • getClientIP (300-300)
packages/lib/src/security/distributed-rate-limit.ts (3)
  • checkDistributedRateLimit (204-263)
  • DISTRIBUTED_RATE_LIMITS (319-368)
  • resetDistributedRateLimit (268-282)
apps/web/src/app/api/auth/mobile/refresh/route.ts (1)
apps/web/src/lib/auth/auth-helpers.ts (1)
  • getClientIP (15-21)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Unit Tests
  • GitHub Check: Lint & TypeScript Check
🔇 Additional comments (16)
apps/web/src/app/api/auth/mobile/oauth/google/exchange/route.ts (2)

107-128: LGTM! IP-based rate limiting correctly implemented.

The IP rate limiting uses the appropriate configuration (DISTRIBUTED_RATE_LIMITS.LOGIN) with clear X-RateLimit headers and proper Retry-After fallback. The getClientIP helper provides centralized IP extraction.


267-281: LGTM! Rate limit reset failures handled gracefully.

The use of Promise.allSettled ensures that rate limit reset failures don't affect the successful authentication response. The logging provides good observability for debugging. The type guard correctly filters rejected promises, and the error message extraction safely handles different error types.

apps/web/src/lib/auth/index.ts (1)

300-301: LGTM! Barrel exports follow existing patterns.

The new exports for getClientIP and validateLoginCSRFToken are consistent with the module's barrel export pattern and support the centralized IP extraction and CSRF validation used throughout the auth flows.

apps/web/src/app/api/auth/logout/route.ts (1)

19-19: LGTM! Centralized IP extraction improves maintainability.

Replacing inline IP extraction logic with the getClientIP helper reduces code duplication and ensures consistent IP handling across all auth routes.

apps/web/src/app/api/auth/__tests__/logout.test.ts (1)

41-54: LGTM! Modern Vitest mocking pattern improves test maintainability.

The async factory mock with importOriginal preserves the original module's exports while overriding specific functions for testing. This approach is more maintainable than static mocks and prevents test breakage when new exports are added to the auth module.

apps/web/src/lib/auth/auth-helpers.ts (1)

7-21: LGTM! IP extraction follows best practices.

The implementation correctly:

  • Parses x-forwarded-for to extract the original client IP (first value before proxies)
  • Falls back to x-real-ip (common nginx header)
  • Uses 'unknown' as a safe fallback for rate limiting

The JSDoc is clear and includes a practical example. The type annotation properly supports both standard Request and Next.js NextRequest types.

apps/web/src/app/api/auth/mobile/refresh/route.ts (1)

40-44: LGTM!

The distributed rate limiting implementation is correct. The rate limit key refresh:device:ip:${clientIP} appropriately separates mobile device refresh attempts from web refresh attempts, and the check occurs before any expensive validation operations.

apps/web/src/app/api/auth/refresh/route.ts (1)

24-47: LGTM!

The distributed rate limiting implementation is well-structured:

  • IP-only limiting is appropriate for refresh endpoints (no email available)
  • Rate limit check occurs early before any database operations
  • 429 response includes proper Retry-After and X-RateLimit-* headers
apps/web/src/app/api/auth/google/signin/route.ts (2)

16-62: LGTM!

The POST handler properly:

  • Validates all required OAuth environment variables upfront with diagnostic logging
  • Applies distributed rate limiting before OAuth URL generation
  • Signs the state parameter with HMAC-SHA256 for tampering prevention
  • Returns appropriate 429 responses with rate limit headers

106-128: LGTM!

The GET handler correctly:

  • Validates only the environment variables it uses (CLIENT_ID, REDIRECT_URI)
  • Applies distributed rate limiting with redirect-based error handling appropriate for browser navigation
  • Omits OAUTH_STATE_SECRET check since GET doesn't use state signing
apps/web/src/app/api/auth/google/callback/route.ts (1)

102-112: LGTM!

The distributed rate limiting is well-positioned after parsing/validating the callback parameters but before the expensive Google token exchange. Using DISTRIBUTED_RATE_LIMITS.LOGIN is consistent with the sign-in route, treating the entire OAuth flow under the same limits.

apps/web/src/app/api/auth/signup/route.ts (2)

111-151: LGTM!

Excellent implementation of distributed rate limiting:

  • Parallel checks with Promise.all for performance
  • Separate IP and email-based limits prevent both brute-force and email enumeration
  • Proper 429 responses with Retry-After and X-RateLimit-* headers
  • CSRF validation occurs before rate limiting, preventing CSRF bypass attacks

197-210: LGTM!

The Promise.allSettled pattern with observability logging is the gold standard for graceful rate limit resets. Failures are logged without blocking the successful signup flow.

apps/web/src/app/api/auth/login/route.ts (3)

97-135: LGTM!

Excellent implementation matching the signup pattern:

  • Parallel rate limit checks for performance
  • Separate IP and email limits with DISTRIBUTED_RATE_LIMITS.LOGIN
  • Proper 429 responses with all required headers

188-201: LGTM!

The Promise.allSettled pattern with failure logging provides robust reset handling without blocking successful authentication.


233-238: LGTM!

Correctly includes X-RateLimit-Limit and X-RateLimit-Remaining headers on the success response, providing clients with rate limit status after successful authentication.

Address Round 4 code review comments from PR #167:

- Add X-RateLimit-Limit and X-RateLimit-Remaining headers to
  mobile/refresh and device/refresh success responses for consistency
  with login and web refresh routes
- Add try-catch logging around resetDistributedRateLimit calls in
  refresh, device/refresh, and google/callback routes for observability

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
apps/web/src/app/api/auth/google/callback/route.ts (1)

75-78: Add early environment variable validation for OAUTH_STATE_SECRET.

The callback route is missing the early validation that exists in the signin route. Unlike apps/web/src/app/api/auth/google/signin/route.ts which validates OAUTH_STATE_SECRET at the handler entry (lines 18-25), the callback route uses process.env.OAUTH_STATE_SECRET! on line 76 without prior validation. This non-null assertion could cause a runtime error if the environment variable is missing.

Add early validation matching the signin route pattern:

if (!process.env.OAUTH_STATE_SECRET) {
  loggers.auth.error('Missing OAUTH_STATE_SECRET environment variable');
  return NextResponse.redirect(new URL('/auth/signin?error=oauth_error', baseUrl));
}
🤖 Fix all issues with AI Agents
In @apps/web/src/app/api/auth/mobile/refresh/route.ts:
- Around line 131-133: Wrap the await
resetDistributedRateLimit(`refresh:device:ip:${clientIP}`) call in a try-catch
so Redis failures don't turn a successful refresh into a 500; i.e., in route
handler catch any error from resetDistributedRateLimit, log it (use the same
logger as other refresh routes, e.g. processLogger.warn or processLogger.error)
with context like "failed to reset rate limit for refresh:device" and do not
rethrow so the success response is still returned.
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 11fe272 and 13ed32c.

📒 Files selected for processing (4)
  • apps/web/src/app/api/auth/device/refresh/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
  • apps/web/src/app/api/auth/mobile/refresh/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
🧰 Additional context used
📓 Path-based instructions (5)
apps/web/src/app/**/route.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

apps/web/src/app/**/route.{ts,tsx}: In Next.js 15 route handlers, params in dynamic routes are Promise objects and MUST be awaited before destructuring
Use Response.json() or NextResponse.json() for returning JSON from route handlers
Get request body using const body = await request.json();
Get search parameters using const { searchParams } = new URL(request.url);

apps/web/src/app/**/route.{ts,tsx}: In Next.js 15 dynamic routes, params are Promise objects and MUST be awaited before destructuring: const { id } = await context.params;
In Route Handlers, get request body with const body = await request.json();
In Route Handlers, get search parameters with const { searchParams } = new URL(request.url);
In Route Handlers, return JSON using Response.json(data) or NextResponse.json(data)
For permission logic, use centralized functions from @pagespace/lib/permissions: getUserAccessLevel(), canUserEditPage()

Files:

  • apps/web/src/app/api/auth/device/refresh/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/mobile/refresh/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Use camelCase for variable and function names
Use UPPER_SNAKE_CASE for constants
Use PascalCase for type and enum names
Use kebab-case for filenames, except React hooks (camelCase with use prefix), Zustand stores (camelCase with use prefix), and React components (PascalCase)
Lint with Next/ESLint as configured in apps/web/eslint.config.mjs
Message content should always use the message parts structure with { parts: [{ type: 'text', text: '...' }] }
Use centralized permission functions from @pagespace/lib/permissions (e.g., getUserAccessLevel, canUserEditPage) instead of implementing permission logic locally
Always use Drizzle client from @pagespace/db package for database access
Use ESM modules throughout the codebase

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Write code that is explicit over implicit and self-documenting

Files:

  • apps/web/src/app/api/auth/device/refresh/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/mobile/refresh/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: React hook files should use camelCase matching the exported hook name (e.g., useAuth.ts)
Zustand store files should use camelCase with use prefix (e.g., useAuthStore.ts)

Files:

  • apps/web/src/app/api/auth/device/refresh/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/mobile/refresh/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • apps/web/src/app/api/auth/device/refresh/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/mobile/refresh/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
apps/web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/**/*.{ts,tsx}: Use message parts structure for message content: { parts: [{ type: 'text', text: '...' }] }
For database access, always use Drizzle client from @pagespace/db: import { db, pages } from '@pagespace/db';
Use centralized Drizzle ORM with PostgreSQL for all database operations - no direct SQL or other ORMs
Use Socket.IO for real-time collaboration features - imported from the realtime service at port 3001
Use Vercel AI SDK with async/await for all AI operations and streaming
Use Next.js 15 App Router and TypeScript for all routes and components

Files:

  • apps/web/src/app/api/auth/device/refresh/route.ts
  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/mobile/refresh/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
🧠 Learnings (9)
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to app/**/*.{ts,tsx} : Import and use `getUserAccessLevel()` and `canUserEditPage()` from `pagespace/lib/permissions` for centralized permission logic

Applied to files:

  • apps/web/src/app/api/auth/device/refresh/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*.{ts,tsx} : Use centralized permission functions from `pagespace/lib/permissions` (e.g., `getUserAccessLevel`, `canUserEditPage`) instead of implementing permission logic locally

Applied to files:

  • apps/web/src/app/api/auth/device/refresh/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/**/*.{ts,tsx} : For database access, always use Drizzle client from `pagespace/db`: `import { db, pages } from 'pagespace/db';`

Applied to files:

  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to **/*.{ts,tsx} : Always use the Drizzle client and database exports from `pagespace/db` (e.g., `import { db, pages } from 'pagespace/db'`) for all database access

Applied to files:

  • apps/web/src/app/api/auth/refresh/route.ts
  • apps/web/src/app/api/auth/google/callback/route.ts
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : For permission logic, use centralized functions from `pagespace/lib/permissions`: `getUserAccessLevel()`, `canUserEditPage()`

Applied to files:

  • apps/web/src/app/api/auth/google/callback/route.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/**/*.ts : Always use centralized permission functions from `pagespace/lib/permissions` for access control logic (e.g., `getUserAccessLevel`, `canUserEditPage`)

Applied to files:

  • apps/web/src/app/api/auth/google/callback/route.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/**/*.ts : Always use Drizzle client and queries from `pagespace/db` for database access instead of direct queries

Applied to files:

  • apps/web/src/app/api/auth/google/callback/route.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*.{ts,tsx} : Always use Drizzle client from `pagespace/db` package for database access

Applied to files:

  • apps/web/src/app/api/auth/google/callback/route.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*auth*.{ts,tsx} : Use custom JWT authentication with jose library for user management

Applied to files:

  • apps/web/src/app/api/auth/google/callback/route.ts
🧬 Code graph analysis (3)
apps/web/src/app/api/auth/refresh/route.ts (2)
apps/web/src/lib/auth/auth-helpers.ts (1)
  • getClientIP (15-21)
packages/lib/src/logging/logger-config.ts (1)
  • loggers (8-18)
apps/web/src/app/api/auth/mobile/refresh/route.ts (2)
apps/web/src/lib/auth/auth-helpers.ts (1)
  • getClientIP (15-21)
apps/web/src/lib/auth/index.ts (1)
  • getClientIP (300-300)
apps/web/src/app/api/auth/google/callback/route.ts (2)
apps/web/src/lib/auth/auth-helpers.ts (1)
  • getClientIP (15-21)
apps/web/src/lib/auth/index.ts (1)
  • getClientIP (300-300)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Unit Tests
  • GitHub Check: Lint & TypeScript Check
🔇 Additional comments (14)
apps/web/src/app/api/auth/mobile/refresh/route.ts (3)

12-19: LGTM - Imports updated correctly for distributed rate limiting.

The imports are correctly updated to use the centralized getClientIP helper and distributed rate limiting utilities from @pagespace/lib/security.


38-61: LGTM - Distributed rate limiting check is well implemented.

The rate limit check correctly:

  • Uses IP-based key with consistent naming pattern (refresh:device:ip:${clientIP})
  • Returns appropriate 429 response with Retry-After and X-RateLimit-* headers
  • Provides a fallback of 300 seconds for retryAfter

139-145: LGTM - Success response includes rate limit headers.

The success response correctly includes X-RateLimit-Limit and X-RateLimit-Remaining headers set to maxAttempts after the reset.

apps/web/src/app/api/auth/device/refresh/route.ts (4)

15-24: LGTM - Imports correctly updated.

Distributed rate limiting utilities and centralized IP helper are properly imported.


44-67: LGTM - Distributed rate limiting check implementation.

The rate limit check is correctly implemented with:

  • IP-based key matching the mobile refresh route pattern
  • Proper 429 response with retry and limit headers
  • Fallback for retryAfter value

197-204: LGTM - Rate limit reset properly wrapped in try-catch.

Good defensive implementation - failures are logged but don't block the success flow, maintaining observability without impacting user experience.


234-235: LGTM - Rate limit headers consistently added to both response paths.

Both web platform (cookie-based) and mobile/desktop (JSON) responses include the X-RateLimit-Limit and X-RateLimit-Remaining headers.

Also applies to: 253-257

apps/web/src/app/api/auth/refresh/route.ts (4)

3-13: LGTM - Imports correctly updated for distributed rate limiting.

The imports are properly updated to use centralized utilities.


24-47: Verify rate limit key naming pattern is intentional.

This route uses refresh:ip:${clientIP} while device/refresh and mobile/refresh use refresh:device:ip:${clientIP}. This creates separate rate limit buckets for:

  • Web cookie-based refresh (refresh:ip:)
  • Device-token-based refresh (refresh:device:ip:)

If this separation is intentional (different limits for different auth flows), the implementation is correct. Please confirm this is the desired behavior.


167-174: LGTM - Rate limit reset properly wrapped in try-catch.

Failures are logged without blocking the success flow, consistent with other refresh routes.


176-183: LGTM - Success response includes rate limit headers.

Headers are correctly set after rate limit reset, showing full quota available.

apps/web/src/app/api/auth/google/callback/route.ts (3)

4-18: LGTM - Imports correctly updated for distributed rate limiting.

The imports are properly updated to use centralized utilities and the getClientIP helper.


103-112: LGTM - Distributed rate limiting check with redirect on breach.

The implementation correctly:

  • Uses IP-based key with OAuth-specific naming (oauth:callback:ip:)
  • Uses DISTRIBUTED_RATE_LIMITS.LOGIN config (appropriate for OAuth login flow)
  • Redirects to sign-in page on rate limit breach (correct for OAuth callback which can't return JSON)

234-240: LGTM - Rate limit reset properly wrapped in try-catch.

Failures are logged without affecting the OAuth success flow, consistent with other auth routes.

Comment thread apps/web/src/app/api/auth/mobile/refresh/route.ts
2witstudios and others added 3 commits January 6, 2026 10:24
Wrap resetDistributedRateLimit call in try-catch to prevent Redis
failures from turning successful refreshes into 500 errors.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Address 6 issues identified in code review:

P0-P1 fixes (immediate):
- P0-T1: Update file list to match actual implementation
- P0-T3: Add tokenPrefix backfill to migration (9-step process)
- P1-T3: Fix tokenPrefix comment "8 chars" → "12 chars"

P3-P4 fixes (documented for later phases):
- P3-T5: Add hex validation before timingSafeEqual (DoS prevention)
- P4-T1: Implement nonce-based CSP (Next.js hydration compatibility)
- P4-T4: Add iterative decode loop for path traversal (double-encoding)

All schema changes maintain .notNull() with proper migration backfill.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
P0-T1: Redis Session Isolation
- Add docker-compose.security.yml with redis-sessions service (port 6380)
- Update security-redis.ts with separate session/rate-limit clients
- Add REDIS_SESSION_URL and REDIS_RATE_LIMIT_URL to .env.example
- Update docker-compose files with service dependencies

P0-T3: Token Hash Migration Schema
- Add tokenHash and tokenPrefix columns to refresh_tokens and mcp_tokens
- Add partial unique indexes for migration phase (null-safe)
- Create migrate-token-hashes.ts batch backfill script with dry-run mode
- Generate migration 0033 with column additions and indexes

P0-T4: Load Test CI Infrastructure
- Add load-test.yml GitHub Actions workflow (weekly + manual trigger)
- Update .gitignore for k6 load test results
- Add .gitkeep to tests/load/results directory

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 6

Caution

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

⚠️ Outside diff range comments (1)
packages/lib/src/security/security-redis.ts (1)

358-380: Update health check to use specialized Redis clients.

The health check function uses the deprecated getSecurityRedisClient() and only verifies the shared Redis client. Since the actual security operations now use getSessionRedisClient() and getRateLimitRedisClient(), the health check should verify these specialized clients instead.

🔎 Proposed health check update
 export async function checkSecurityRedisHealth(): Promise<{
   available: boolean;
   latencyMs?: number;
   error?: string;
+  session?: { available: boolean; error?: string };
+  rateLimit?: { available: boolean; error?: string };
 }> {
   const start = Date.now();
+  const results: {
+    available: boolean;
+    latencyMs: number;
+    error?: string;
+    session?: { available: boolean; error?: string };
+    rateLimit?: { available: boolean; error?: string };
+  } = {
+    available: true,
+    latencyMs: 0,
+  };

   try {
-    const redis = await getSecurityRedisClient();
-    await redis.ping();
-
-    return {
-      available: true,
-      latencyMs: Date.now() - start,
-    };
+    const sessionClient = await getSessionRedisClient();
+    await sessionClient.ping();
+    results.session = { available: true };
   } catch (error) {
-    return {
-      available: false,
-      latencyMs: Date.now() - start,
-      error: error instanceof Error ? error.message : String(error),
-    };
+    results.available = false;
+    results.session = {
+      available: false,
+      error: error instanceof Error ? error.message : String(error),
+    };
   }
+
+  try {
+    const rateLimitClient = await getRateLimitRedisClient();
+    await rateLimitClient.ping();
+    results.rateLimit = { available: true };
+  } catch (error) {
+    results.available = false;
+    results.rateLimit = {
+      available: false,
+      error: error instanceof Error ? error.message : String(error),
+    };
+  }
+
+  results.latencyMs = Date.now() - start;
+  return results;
 }
🤖 Fix all issues with AI Agents
In @.env.example:
- Around line 113-118: The .env.example uses docker-compose-style interpolation
(${REDIS_PASSWORD}) which many .env parsers won't resolve; update the
REDIS_SESSION_URL and REDIS_RATE_LIMIT_URL entries to use explicit literal
placeholders (e.g., REDIS_PASSWORD_PLACEHOLDER) or add a clear comment above
those lines instructing users to replace ${REDIS_PASSWORD} with their actual
password when copying to .env; ensure you update both REDIS_SESSION_URL and
REDIS_RATE_LIMIT_URL and mention the REDIS_PASSWORD token so users know to
substitute it manually.

In @.github/workflows/load-test.yml:
- Around line 89-96: The workflow currently uses the archived action
grafana/k6-action@v0.3.1 in the "Run k6 load test" step; replace it with an
actively maintained alternative (for example a community fork or the official k6
Docker-based invocation) to ensure future patches and security fixes; update the
step referencing grafana/k6-action@v0.3.1 to either a maintained GitHub Action
or switch to running k6 via docker run -v $(pwd):/scripts loadimpact/k6 or a
maintained action, and keep the existing inputs (filename:
tests/load/auth-baseline.k6.js, env K6_VUS, K6_DURATION, BASE_URL) intact when
migrating so behavior remains the same.

In @packages/lib/src/security/security-redis.ts:
- Around line 29-47: getSessionRedisClient can throw when REDIS_SESSION_URL is
missing, breaking graceful degradation because recordJTI, isJTIRevoked, and
revokeJTI call it directly; add a safe availability check (either have those JTI
functions use the existing tryGetSecurityRedisClient or implement a new
tryGetSessionRedisClient wrapper) and update recordJTI/isJTIRevoked/revokeJTI to
call the try* wrapper, returning safe defaults (no-op or false) in
non-production when the session client is unavailable and rethrowing in
production.

In @scripts/migrate-token-hashes.ts:
- Around line 29-151: Both migrateRefreshTokens and migrateMcpTokens duplicate
the same migration logic; extract a single generic function (e.g.,
migrateTokenTable) that accepts tableName (string), table (refreshTokens |
mcpTokens), batchSize and dryRun, move the shared logic for counting unmigrated
rows, batching, hashing via hashToken/getTokenPrefix, progress printing and
returning processedTotal into it, then call migrateTokenTable('refresh_tokens',
refreshTokens, ...) and migrateTokenTable('mcp_tokens', mcpTokens, ...); inside
the generic function use the table.id/table.token/table.tokenHash symbols to
build the select/update and perform parameterized updates via the query builder
(avoid constructing VALUES SQL strings) to replace the duplicated tx.execute
blocks.
- Around line 66-75: The current batch UPDATE builds a VALUES list by
concatenating u.id, u.tokenHash and u.tokenPrefix into sql.raw, creating SQL
injection and quoting issues; change this to use parameterized queries instead
of string interpolation: for example iterate updates and call tx.execute with a
parameterized UPDATE (referencing refresh_tokens, token_hash, token_prefix) or
use Drizzle's bulk/batch update API to supply arrays of parameters rather than
constructing a VALUES string, and replace the tx.execute(sql.raw(...)) usage
with the parameterized/prepared-statement approach to safely bind id, tokenHash,
and tokenPrefix.
- Around line 128-137: The SQL built in migrate-token-hashes.ts uses string
concatenation of updates into VALUES, creating an SQL injection risk; change the
tx.execute call in the block that builds values (which maps updates ->
`('${u.id}', '${u.tokenHash}', '${u.tokenPrefix}')`) to use parameterized
queries instead—either construct a parameter list and use placeholders for each
id/hash/prefix in the VALUES clause or execute a parameterized UPDATE per row
using tx.execute with parameters; ensure you reference the same symbols
(updates, tx.execute, mcp_tokens, token_hash, token_prefix) and stop
interpolating raw values into the SQL string.
🧹 Nitpick comments (4)
docker-compose.security.yml (1)

14-16: Consider documenting maxmemory behavior for production sizing.

The 512MB memory limit with volatile-ttl policy will evict keys with TTLs when memory is full. For production deployments with high traffic, monitor memory usage to ensure:

  • Rate limit counters aren't prematurely evicted
  • JTI tracking data persists for token validity periods
  • Session data remains available
.github/workflows/load-test.yml (1)

76-87: Consider adding a health check after server startup.

The sleep 10 assumes the server will be ready within 10 seconds, but if startup fails, the k6 test will proceed against a non-existent endpoint. Consider polling the server's health endpoint instead:

🔎 Suggested improvement: Add health check polling
       run: |
         pnpm --filter web build
         pnpm --filter web start &
-        sleep 10
+        # Wait for server to be ready
+        timeout 30 bash -c 'until curl -f http://localhost:3000/api/health 2>/dev/null; do sleep 1; done'
+        echo "Server is ready"

Note: This assumes a /api/health endpoint exists. Adjust the path if needed, or add a simple health endpoint if one doesn't exist.

packages/lib/src/security/security-redis.ts (2)

21-23: Consider adding a cleanup function for Redis clients.

The module-level Redis clients are never explicitly closed. While this is acceptable for long-running production servers, it can cause resource leaks during testing or development hot-reloads.

🔎 Proposed cleanup utility

Add this export at the end of the file to enable graceful shutdown:

/**
 * Close all Redis clients for graceful shutdown.
 * Call this during application shutdown or in test cleanup.
 */
export async function closeSecurityRedisClients(): Promise<void> {
  const promises: Promise<void>[] = [];
  
  if (sessionRedisClient) {
    promises.push(sessionRedisClient.quit().catch(() => {}));
    sessionRedisClient = null;
  }
  
  if (rateLimitRedisClient) {
    promises.push(rateLimitRedisClient.quit().catch(() => {}));
    rateLimitRedisClient = null;
  }
  
  await Promise.allSettled(promises);
}

207-217: Consider renaming to clarify no Redis interaction.

The function name suggests Redis operations, but the implementation only logs. While the comment explains this is handled at the database level, the name could mislead callers who might expect Redis state changes.

💡 Alternative naming

Consider renaming to better reflect its purpose:

-export async function revokeAllUserJTIs(userId: string): Promise<void> {
+export async function logUserJTIRevocation(userId: string): Promise<void> {

Or add a more explicit JSDoc:

/**
 * Logs that all JTIs for a user will be invalidated.
 * Actual revocation is handled by bumping tokenVersion in the database.
 * This function does NOT modify Redis directly.
 */
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 547285c and 0ffde0a.

📒 Files selected for processing (13)
  • .env.example
  • .github/workflows/load-test.yml
  • .gitignore
  • docker-compose.dev.yml
  • docker-compose.security.yml
  • docker-compose.yml
  • packages/db/drizzle/0033_furry_vampiro.sql
  • packages/db/drizzle/meta/0033_snapshot.json
  • packages/db/drizzle/meta/_journal.json
  • packages/db/src/schema/auth.ts
  • packages/lib/src/security/security-redis.ts
  • scripts/migrate-token-hashes.ts
  • tests/load/results/.gitkeep
✅ Files skipped from review due to trivial changes (1)
  • tests/load/results/.gitkeep
🧰 Additional context used
📓 Path-based instructions (7)
.env*

📄 CodeRabbit inference engine (AGENTS.md)

Never commit secrets to version control; use .env.example for base config and .env for runtime values

Files:

  • .env.example
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Use camelCase for variable and function names
Use UPPER_SNAKE_CASE for constants
Use PascalCase for type and enum names
Use kebab-case for filenames, except React hooks (camelCase with use prefix), Zustand stores (camelCase with use prefix), and React components (PascalCase)
Lint with Next/ESLint as configured in apps/web/eslint.config.mjs
Message content should always use the message parts structure with { parts: [{ type: 'text', text: '...' }] }
Use centralized permission functions from @pagespace/lib/permissions (e.g., getUserAccessLevel, canUserEditPage) instead of implementing permission logic locally
Always use Drizzle client from @pagespace/db package for database access
Use ESM modules throughout the codebase

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Write code that is explicit over implicit and self-documenting

Files:

  • packages/db/src/schema/auth.ts
  • scripts/migrate-token-hashes.ts
  • packages/lib/src/security/security-redis.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: React hook files should use camelCase matching the exported hook name (e.g., useAuth.ts)
Zustand store files should use camelCase with use prefix (e.g., useAuthStore.ts)

Files:

  • packages/db/src/schema/auth.ts
  • scripts/migrate-token-hashes.ts
  • packages/lib/src/security/security-redis.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • packages/db/src/schema/auth.ts
  • scripts/migrate-token-hashes.ts
  • packages/db/drizzle/meta/_journal.json
  • packages/lib/src/security/security-redis.ts
packages/db/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Drizzle ORM for database queries with PostgreSQL

Files:

  • packages/db/src/schema/auth.ts
**/*auth*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*auth*.{ts,tsx}: Use custom JWT authentication with jose library for user management
Use bcryptjs for password hashing

Files:

  • packages/db/src/schema/auth.ts
packages/db/src/schema/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Database schema changes must be made in packages/db/src/schema/ and then pnpm db:generate must be run to create migrations

Files:

  • packages/db/src/schema/auth.ts
🧠 Learnings (6)
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to packages/db/src/schema.ts : Update database schema in `packages/db/src/schema.ts` and generate migrations with `pnpm db:generate`

Applied to files:

  • packages/db/src/schema/auth.ts
  • scripts/migrate-token-hashes.ts
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to packages/db/src/schema/**/*.{ts,tsx} : Database schema changes must be made in `packages/db/src/schema/` and then `pnpm db:generate` must be run to create migrations

Applied to files:

  • packages/db/src/schema/auth.ts
  • scripts/migrate-token-hashes.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to **/*.{ts,tsx} : Keep commits and diffs minimal and focused on specific changes

Applied to files:

  • .gitignore
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to packages/db/src/schema.ts : Database schema entry point is at `packages/db/src/schema.ts`; migrations emit to `packages/db/drizzle/`

Applied to files:

  • scripts/migrate-token-hashes.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to packages/db/drizzle/*.ts : Database migrations should be generated into `packages/db/drizzle/` directory using `pnpm db:generate` command

Applied to files:

  • scripts/migrate-token-hashes.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to **/*.{ts,tsx} : Always use the Drizzle client and database exports from `pagespace/db` (e.g., `import { db, pages } from 'pagespace/db'`) for all database access

Applied to files:

  • scripts/migrate-token-hashes.ts
🧬 Code graph analysis (2)
packages/db/src/schema/auth.ts (1)
packages/db/src/index.ts (1)
  • sql (8-8)
scripts/migrate-token-hashes.ts (3)
packages/lib/src/__tests__/security-test-utils.ts (1)
  • hashToken (276-278)
packages/db/src/index.ts (1)
  • db (20-20)
packages/db/src/schema/auth.ts (2)
  • refreshTokens (35-56)
  • mcpTokens (100-118)
🪛 Checkov (3.2.334)
.github/workflows/load-test.yml

[medium] 70-71: Basic Auth Credentials

(CKV_SECRET_4)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Lint & TypeScript Check
  • GitHub Check: Unit Tests
🔇 Additional comments (23)
docker-compose.dev.yml (1)

19-22: LGTM!

The redis-sessions service network configuration mirrors the existing postgres and redis patterns, enabling port publishing for development access.

docker-compose.security.yml (1)

1-24: Verify Redis security configuration in production.

The redis-sessions service uses appropriate settings for session storage and rate limiting:

  • volatile-ttl eviction policy suits TTL-based data
  • appendonly persistence protects against data loss
  • Healthcheck ensures service readiness

However, ensure that REDIS_PASSWORD is set to a strong value in production deployments, as the default pagespace_redis is only suitable for development.

docker-compose.yml (4)

68-69: LGTM!

Adding redis-sessions as a health-checked dependency ensures the session store is ready before the web service starts, preventing startup race conditions.


79-80: Good separation of Redis databases for different concerns.

Using separate Redis databases (DB 0 for sessions/JTI, DB 1 for rate limiting) provides logical isolation and simplifies debugging. The health-based orchestration ensures availability before dependent services start.


171-172: LGTM!

Consistent dependency pattern with the web service ensures realtime service waits for redis-sessions availability.


179-180: Configuration aligns with web service pattern.

The realtime service correctly uses the same Redis session and rate-limit URLs as the web service, enabling shared state across both services.

packages/db/drizzle/meta/_journal.json (1)

236-242: LGTM!

The migration journal entry is correctly formatted and properly extends the migration history.

packages/db/drizzle/0033_furry_vampiro.sql (3)

1-4: LGTM!

The tokenHash and tokenPrefix columns are correctly added as nullable fields, which enables backward compatibility with existing tokens that haven't been migrated yet.


8-9: LGTM!

The partial unique indexes correctly enforce uniqueness on tokenHash only when it's not NULL, allowing unmigrated tokens to coexist during the migration period.


5-7: Verify that activity_logs changes are intentional.

The migration includes activity_logs columns (previousLogHash, logHash, chainSeed) and an index on logHash, but these changes are not mentioned in the PR objectives or summary. The PR focuses on JTI, rate limiting, and timing-safe comparisons for token security.

Also applies to: 10-10

packages/db/src/schema/auth.ts (4)

39-40: LGTM!

The tokenHash and tokenPrefix columns are correctly defined as optional fields, supporting backward compatibility during the migration period.


52-54: LGTM!

The partial unique index correctly enforces uniqueness on tokenHash only for non-NULL values, which is essential for the gradual token migration strategy.


104-105: LGTM!

Consistent with the refresh_tokens table, these columns are correctly defined as optional to support the migration period.


114-116: LGTM!

The partial unique index implementation is consistent with the refresh_tokens table and correctly handles the migration period.

scripts/migrate-token-hashes.ts (2)

21-27: LGTM!

The hash and prefix extraction functions are correctly implemented using SHA-256 and a 12-character prefix.


153-182: LGTM!

The main function properly handles CLI arguments, orchestrates migrations, provides clear output, and includes appropriate error handling.

.gitignore (1)

135-137: LGTM!

The ignore patterns correctly exclude k6 load test artifacts and align with the new load-test workflow.

.github/workflows/load-test.yml (5)

1-19: LGTM!

The workflow trigger configuration and inputs are well-structured for both scheduled and on-demand load testing.


50-67: LGTM!

The setup steps follow best practices for pnpm-based monorepo CI.


68-74: LGTM!

The hardcoded credentials are appropriate for the CI test environment. The static analysis warning about basic auth credentials is a false positive—these are standard test credentials for an ephemeral CI database.


98-104: LGTM!

The results upload step correctly uses if: always() to capture artifacts even on test failure, and the 30-day retention is appropriate for load test history.


26-48: Single Redis service is correctly configured for load tests.

Both session and rate-limit Redis clients fall back to REDIS_URL in non-production environments. The load test workflow's single Redis service is sufficient—separate REDIS_SESSION_URL and REDIS_RATE_LIMIT_URL are only required in production.

packages/lib/src/security/security-redis.ts (1)

124-124: LGTM! Consistent client routing.

All security operations have been correctly routed to their respective specialized Redis clients:

  • JTI and session operations → session client
  • Rate limiting operations → rate limit client

The separation of concerns is well-implemented.

Also applies to: 142-142, 166-166, 239-239, 280-280, 300-300, 317-317, 328-328, 345-345

Comment thread .env.example Outdated
Comment thread .github/workflows/load-test.yml
Comment thread packages/lib/src/security/security-redis.ts
Comment thread scripts/migrate-token-hashes.ts Outdated
Comment thread scripts/migrate-token-hashes.ts Outdated
Comment thread scripts/migrate-token-hashes.ts Outdated
- Add comprehensive k6 auth baseline test with proper CSRF token handling
- Create seed-loadtest-user.ts script for test user provisioning
- Update plan.md with P0-T2, P0-T3, P0-T4 completion status

Baseline metrics captured:
- CSRF fetch: p95 ~9ms
- Login: p95 ~978ms (bcrypt by design)
- Token validation: p95 ~17ms
- Token refresh: p95 ~9ms

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 0

🧹 Nitpick comments (12)
plan.md (6)

2090-2122: CSP nonce implementation scope unclear—may be incomplete for Phase 4-T1.

Lines 2090-2110 describe CSP nonce generation and x-nonce header exposure via securityHeaders middleware. Line 2122 adds a note that apps/web/src/app/layout.tsx must read the nonce and pass it to Script components, but:

  1. Status inconsistency: P4-T1 (Content Security Policy) is not marked COMPLETED, yet nonce implementation details are included in the plan.
  2. Scope question: Is the nonce support in layout.tsx part of this PR (#167, Phase 1) or a separate future task?
  3. Verification gap: The plan mentions layout.tsx changes but provides no implementation example for how nonce is passed to Script components.

Clarify whether nonce support is in-scope for this PR or deferred to Phase 4. If in-scope, add implementation guidance for layout.tsx.


2527-2670: Test coverage matrix and metrics are aspirational—clarify what's baseline vs. target.

The test coverage matrix (lines 2529-2541) and success metrics (lines 2661-2669) describe end-state coverage across all five phases. However, the PR objectives state "initial counts reported for JTI (20), secure-compare (28), distributed rate-limit (22); total auth tests reported as 108 initially, later 194 after remediation."

Recommendation: Add a subsection distinguishing Phase 1 test coverage (actual, from this PR) vs. overall project target (100% across all phases). Example:

Phase 1 Test Coverage (PR #167):
- JTI: 20 tests ✅
- Secure-compare: 28 tests ✅
- Distributed rate-limit: 22 tests ✅
- Auth routes: 194 total (after remediation) ✅

Full Project Target (Phases 1-5): 100% across all categories

This makes it clear what has been delivered vs. what remains.


53-55: New security module files are correctly listed—ensure all exports are included.

Lines 53-55 reference three new files in the security module:

  • packages/lib/src/security/security-redis.ts
  • packages/lib/src/security/distributed-rate-limit.ts
  • packages/lib/src/security/index.ts

Per the learnings and best practices, verify that packages/lib/src/security/index.ts exports all public APIs for:

  • Distributed rate limiting functions (e.g., checkDistributedRateLimit, resetDistributedRateLimit)
  • Rate limit configuration constants (e.g., DISTRIBUTED_RATE_LIMITS)
  • Security Redis client utilities

This enables imports like import { ... } from '@pagespace/lib/security' throughout the codebase, as referenced in the enriched summary and auth route implementations.


1-41: Plan document needs post-implementation update to reflect remediation insights.

The plan is thorough and well-structured, but does not reference the three remediation rounds documented in PR comments:

  1. Round 1 (commit e6dbf5c): Fixed JTI existence checks, added OAuth rate limiting, corrected headers, parallelized resets.
  2. Round 2 (commit d41c172): Added failure logging, OAuth environment validation, test mock fixes.
  3. Round 3 (commit 11fe272): Centralized IP extraction via getClientIP(), standardized rate-limit key naming.

Recommendation: After this PR merges, add a "Lessons Learned & Remediation Notes" section documenting:

  • Issues discovered during implementation (e.g., X-RateLimit-Remaining header staleness)
  • How fixes deviated from the initial plan
  • Guidance for future phases

This makes the plan a living document that captures both initial design and real-world learnings. Examples:

### Lessons from P1 Implementation (PR #167)

#### IP Extraction Standardization
- **Issue:** IP extraction logic duplicated across 12 routes
- **Solution:** Centralized `getClientIP(request)` utility in auth-helpers.ts
- **Lesson:** For P2-P5, identify cross-cutting concerns early to avoid duplication

#### Promise.allSettled for Resilience
- **Issue:** Rate-limit reset failures caused 500 errors on successful auth
- **Solution:** Changed Promise.all → Promise.allSettled with logging
- **Lesson:** Always use allSettled for non-critical cleanup operations

297-297: Minor documentation: Capitalize "GitHub" per official branding.

Lines 297 and 387 reference .github/workflows/ paths with context mentioning GitHub. The official name is "GitHub" (not "github"). While this is a minor style issue, consistent capitalization improves professionalism. Consider:

- Created `.github/workflows/security.yml` CI workflow  
  ↓
- Created `.github/workflows/security.yml` GitHub workflow

Also applies to: 387-387


388-388: Remove bare URL to comply with markdown standards.

Line 388 flags a bare URL. If there is a link to external documentation or a reference, wrap it in markdown link syntax:

<!-- BAD -->
See https://example.com/docs

<!-- GOOD -->
See [documentation](https://example.com/docs)

If the URL is not needed, remove it. This improves markdown linting compliance.

scripts/seed-loadtest-user.ts (4)

8-19: Consider using URL parsing for safer DATABASE_URL manipulation.

The string replacements work for common Docker configurations but could match unintended parts if the database name, username, or password contains "postgres". Using the URL class would be more robust.

🔎 Suggested safer approach
-// Fix DATABASE_URL for local execution (Docker uses "postgres" hostname, local needs "localhost")
-if (process.env.DATABASE_URL) {
-  const originalUrl = process.env.DATABASE_URL;
-  // Replace Docker hostname with localhost for local script execution
-  process.env.DATABASE_URL = originalUrl
-    .replace('://postgres:', '://localhost:')
-    .replace('@postgres/', '@localhost/')
-    .replace('@postgres:', '@localhost:');
-
-  if (originalUrl !== process.env.DATABASE_URL) {
-    console.log('Note: Adjusted DATABASE_URL for local execution (postgres -> localhost)\n');
-  }
-}
+// Fix DATABASE_URL for local execution (Docker uses "postgres" hostname, local needs "localhost")
+if (process.env.DATABASE_URL) {
+  const originalUrl = process.env.DATABASE_URL;
+  try {
+    const url = new URL(originalUrl);
+    if (url.hostname === 'postgres') {
+      url.hostname = 'localhost';
+      process.env.DATABASE_URL = url.toString();
+      console.log('Note: Adjusted DATABASE_URL for local execution (postgres -> localhost)\n');
+    }
+  } catch {
+    // If URL parsing fails, leave DATABASE_URL unchanged
+  }
+}

28-29: Simplify the usage command.

The path node_modules/.pnpm/node_modules/.bin/tsx is pnpm-specific and fragile. pnpm exec tsx or npx tsx would be cleaner.

🔎 Suggested fix
  * Usage:
  *   # From project root:
-  *   node_modules/.pnpm/node_modules/.bin/tsx scripts/seed-loadtest-user.ts
+  *   pnpm exec tsx scripts/seed-loadtest-user.ts

61-65: Move production check before database imports for faster failure.

The production guard executes after loading database modules. Moving it before dynamic imports would fail faster and avoid unnecessary module loading in production.

🔎 Suggested structure
 async function main() {
+  // Check if running in production - fail fast before loading dependencies
+  if (process.env.NODE_ENV === 'production') {
+    console.error('ERROR: Cannot seed load test user in production environment!');
+    process.exit(1);
+  }
+
   const { db, users } = await import('@pagespace/db');
   const { eq } = await import('drizzle-orm');
   const bcrypt = await import('bcryptjs');
   const { createId } = await import('@paralleldrive/cuid2');
   
   // ... rest of the code ...
   
-  // Check if running in production
-  if (process.env.NODE_ENV === 'production') {
-    console.error('ERROR: Cannot seed load test user in production environment!');
-    process.exit(1);
-  }

106-110: Consider explicit database connection cleanup.

Using process.exit() without closing the database connection may leave connections hanging briefly. While this is typically fine for one-off scripts, explicit cleanup is better practice.

🔎 Example cleanup approach

If your Drizzle setup exposes a connection pool or client, you could add:

// After successful insert or in finally block
// await db.$client.end(); // or equivalent for your db driver
process.exit(0);
tests/load/auth-baseline.k6.js (2)

50-58: Consider tightening the error rate threshold or splitting metrics.

The 30% error rate threshold is quite permissive. While the comment mentions "refresh token consumption expected," this high threshold could mask legitimate auth failures. Consider:

  1. Tracking refresh failures separately from auth errors
  2. Using a lower threshold (e.g., 10%) for non-refresh operations

This would provide better signal when actual login/CSRF issues occur.


364-367: Ensure the results directory exists before test execution.

The output path tests/load/results/baseline-latest.json assumes the results directory exists. k6 will fail to write the summary if the directory is missing.

Consider adding a note to the usage documentation or creating the directory in CI/setup:

 * Usage:
 *   # Run baseline test
+*   mkdir -p tests/load/results  # Ensure output directory exists
 *   k6 run tests/load/auth-baseline.k6.js
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0ffde0a and 0156400.

📒 Files selected for processing (3)
  • plan.md
  • scripts/seed-loadtest-user.ts
  • tests/load/auth-baseline.k6.js
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Use camelCase for variable and function names
Use UPPER_SNAKE_CASE for constants
Use PascalCase for type and enum names
Use kebab-case for filenames, except React hooks (camelCase with use prefix), Zustand stores (camelCase with use prefix), and React components (PascalCase)
Lint with Next/ESLint as configured in apps/web/eslint.config.mjs
Message content should always use the message parts structure with { parts: [{ type: 'text', text: '...' }] }
Use centralized permission functions from @pagespace/lib/permissions (e.g., getUserAccessLevel, canUserEditPage) instead of implementing permission logic locally
Always use Drizzle client from @pagespace/db package for database access
Use ESM modules throughout the codebase

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Write code that is explicit over implicit and self-documenting

Files:

  • scripts/seed-loadtest-user.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: React hook files should use camelCase matching the exported hook name (e.g., useAuth.ts)
Zustand store files should use camelCase with use prefix (e.g., useAuthStore.ts)

Files:

  • scripts/seed-loadtest-user.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • scripts/seed-loadtest-user.ts
  • tests/load/auth-baseline.k6.js
🧠 Learnings (17)
📓 Common learnings
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Tech stack: Next.js 15 App Router + TypeScript + Tailwind + shadcn/ui (frontend), PostgreSQL + Drizzle ORM (database), Ollama + Vercel AI SDK + OpenRouter + Google AI SDK (AI), custom JWT auth, local filesystem storage, Socket.IO for real-time, Docker deployment
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to packages/db/src/schema.ts : Maintain the Drizzle ORM database schema in `packages/db/src/schema.ts` as the single entry point for schema definitions

Applied to files:

  • scripts/seed-loadtest-user.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to **/*.{ts,tsx} : Always use the Drizzle client and database exports from `pagespace/db` (e.g., `import { db, pages } from 'pagespace/db'`) for all database access

Applied to files:

  • scripts/seed-loadtest-user.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to packages/lib/**/*.test.ts : Write unit tests for shared utilities in `packages/lib` with test files named `*.test.ts` alongside source or in `__tests__/` directory

Applied to files:

  • plan.md
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to packages/db/src/schema.ts : Update database schema in `packages/db/src/schema.ts` and generate migrations with `pnpm db:generate`

Applied to files:

  • plan.md
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to packages/db/src/schema/**/*.{ts,tsx} : Database schema changes must be made in `packages/db/src/schema/` and then `pnpm db:generate` must be run to create migrations

Applied to files:

  • plan.md
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to **/__tests__/**/*.test.ts : Unit tests should be placed next to source files or in `__tests__/` directories with `*.test.ts` extension. Add a `test` script to the package and run with `pnpm --filter <pkg> test`

Applied to files:

  • plan.md
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to packages/db/drizzle/*.ts : Database migrations should be generated into `packages/db/drizzle/` directory using `pnpm db:generate` command

Applied to files:

  • plan.md
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/components/**/*.{ts,tsx} : Use SWR for server state and caching with proper configuration including `revalidateOnFocus: false` for editing protection

Applied to files:

  • plan.md
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/**/*.{ts,tsx} : Use Next.js 15 App Router and TypeScript for all routes and components

Applied to files:

  • plan.md
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/web/{app,components,lib,src}/**/*.{ts,tsx,js,jsx} : Code must be formatted with Prettier and linted with Next/ESLint as configured in `apps/web/eslint.config.mjs`

Applied to files:

  • plan.md
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*.{ts,tsx} : Lint with Next/ESLint as configured in `apps/web/eslint.config.mjs`

Applied to files:

  • plan.md
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*.tsx : Use SWR for server state management and caching

Applied to files:

  • plan.md
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*auth*.{ts,tsx} : Use bcryptjs for password hashing

Applied to files:

  • plan.md
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/components/**/*.{ts,tsx} : Use Zustand for client-side state management as the primary state solution

Applied to files:

  • plan.md
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/components/**/*.{ts,tsx} : Use TipTap rich text editor with markdown support for document editing

Applied to files:

  • plan.md
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/web/components/**/*.{ts,tsx} : React component files must use PascalCase naming (e.g., `MyComponent.tsx`)

Applied to files:

  • plan.md
🪛 LanguageTool
plan.md

[uncategorized] ~297-~297: The official name of this software platform is spelled with a capital “H”.
Context: ...y-fixtures.tswith test data - Created.github/workflows/security.yml` CI workflow - A...

(GITHUB)


[uncategorized] ~387-~387: The official name of this software platform is spelled with a capital “H”.
Context: ...andling and cookie management - Created .github/workflows/load-test.yml CI workflow (w...

(GITHUB)

🪛 markdownlint-cli2 (0.18.1)
plan.md

388-388: Bare URL used

(MD034, no-bare-urls)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Unit Tests
🔇 Additional comments (11)
plan.md (2)

44-222: Phase 0 completion status needs reconciliation with PR scope.

The plan marks all Phase 0 tasks (P0-T1 through P0-T4) as ✅ COMPLETED with specific test counts and implementation notes. However, the PR objectives describe this PR (#167) as implementing only Phase 1 tasks (P1-T1 through P1-T6). The PR comments reference three remediation commits (e6dbf5c, d41c172, 11fe272) for P1 issues, not Phase 0 completion.

Consider clarifying in the plan whether Phase 0 represents:

  1. Completed prerequisite work (in prior commits/PRs) that Phase 1 depends on, or
  2. Aspirational planning for future implementation.

If Phase 0 is complete, add a note referencing the commits/PR numbers. If aspirational, adjust the status marks from ✅ to ⏳ or remove the detailed implementation notes to avoid confusion.


810-885: Timing-safe comparison validation addition is critical—verify completeness.

Lines 2015-2025 add important hex-format validation before timingSafeEqual, preventing potential DoS from malformed signatures. This is a defensive hardening that goes beyond the base secureCompare utility. Ensure:

  1. ✅ The verifyBroadcastSignature function in apps/realtime/src/broadcast/signature.ts includes the hex validation check (lines 2017-2019).
  2. ✅ Tests in apps/realtime/src/__tests__/broadcast-security.test.ts cover malformed signature rejection.
  3. ✅ This fix is mentioned in the Round 2 or Round 3 remediation commits referenced in PR comments.
scripts/seed-loadtest-user.ts (1)

67-96: LGTM!

Good idempotent behavior with the existing user check. User creation includes all necessary fields, and the bcrypt cost factor of 12 is appropriate.

tests/load/auth-baseline.k6.js (8)

34-39: LGTM!

The custom metrics are well-defined and cover all the key latency points (CSRF, login, refresh, token validation) plus an error rate metric for overall auth health monitoring.


70-85: LGTM!

The setup function properly validates server availability before test execution with an appropriate timeout, and the permissive 404 check is reasonable since /api/health may not exist in all deployments.


98-134: LGTM!

The CSRF token fetch is well-implemented with:

  • Proper latency tracking
  • Manual cookie handling for path-restricted cookies (documented why)
  • Early termination on failure to avoid cascading errors
  • Clear failure logging

The fail-fast pattern at lines 136-140 is a good approach to prevent meaningless downstream requests.


166-180: Cookie parsing is functional but fragile.

The manual cookie parsing handles the common cases correctly (array vs string, values with =). However, it doesn't account for URL-encoded values or whitespace around the =. For a load test this is acceptable, but be aware that unusual cookie values could cause silent failures.

The comment explaining why manual parsing is needed (line 167) is helpful.


145-209: LGTM!

The login flow is well-structured with:

  • CSRF token sent via header (aligns with production implementation)
  • Response validation checking for user id (appropriate for cookie-based auth)
  • Comprehensive error handling with specific messages for 403/401/429 status codes
  • Flow control via isLoggedIn flag to prevent downstream test pollution

219-253: LGTM!

Testing against /api/drives as a real protected endpoint provides meaningful token validation coverage. The flexible response check at line 240 (Array.isArray(body) || Array.isArray(body.drives)) handles potential response format variations gracefully.


257-301: LGTM!

The refresh flow correctly tests the cookie-based token refresh mechanism:

  • Validates both the response message and that new cookies are set
  • Rate limit handling at line 293-294 provides visibility into distributed rate limiting behavior
  • The null body with Content-Type: application/json is acceptable for k6

373-416: LGTM!

The text summary is well-formatted with clear section headers and defensive null handling via optional chaining. The threshold pass/fail indicator at lines 407-409 provides quick feedback on test success.

CI Fixes:
- Fix apps/web/src/test/setup.ts to not override CI DATABASE_URL
  (was unconditionally setting test:test, breaking postgres:postgres CI)

PR Review Feedback:
- Add "Lessons Learned" section to plan.md with P0-P1 insights
- Add CSP nonce scope clarification note
- Update seed-loadtest-user.ts with URL class parsing
- Refactor migrate-token-hashes.ts to use generic function
- Add tryGetSessionRedisClient for graceful Redis fallback
- Update load-test workflow to use docker-based k6
- Fix .env.example to use placeholders instead of shell vars
- Add separate refresh_error_rate metric in k6 test

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @.github/workflows/load-test.yml:
- Around line 76-87: The workflow step "Start web server" uses a fixed sleep
(sleep 10) which is brittle; replace it with a retry health check loop in the
same run block that repeatedly polls a health endpoint (e.g., /api/health or /)
on localhost until it returns HTTP 200 or a timeout is reached, failing the job
if the server never becomes healthy; ensure the loop uses a short delay between
attempts, a max attempt count or overall timeout, and exits successfully only
after a 200 response so the subsequent k6 load test runs against a ready server.

In @scripts/migrate-token-hashes.ts:
- Around line 15-17: The batchSize parsing using parseInt on args
(args.find(...)?split('=')[1]) can yield NaN for invalid input; update the
batchSize assignment to parse with radix 10, validate the result is a positive
integer (e.g., const parsed = parseInt(..., 10); if (!Number.isInteger(parsed)
|| parsed <= 0) handle invalid input), and either fall back to the default 1000
or exit with a clear error; reference the batchSize variable, the args array and
the '--batch-size=' token when implementing the validation.
🧹 Nitpick comments (5)
.env.example (1)

110-120: Standardize Redis password placeholder naming for consistency.

The new Redis URLs use REDIS_PASSWORD_PLACEHOLDER (lines 116, 119), but the existing REDIS_PASSWORD and REDIS_URL entries use your_redis_password_here (lines 110–111). To maintain consistency and clarity across all Redis configuration, adopt the same placeholder pattern throughout.

Additionally, clarify the comment on line 115 to indicate that users should replace the placeholder with the same password value as REDIS_PASSWORD (line 110) to avoid connection failures when both databases are accessed via the same Redis instance.

♻️ Proposed fix: Standardize placeholder naming and clarify instructions
 REDIS_PASSWORD=your_redis_password_here
 REDIS_URL=redis://:your_redis_password_here@redis:6379

 # Redis Cluster Configuration for Security Features
 # Session storage - Database 0 (JTI tracking, session data)
-# NOTE: Replace REDIS_PASSWORD_PLACEHOLDER with your REDIS_PASSWORD when copying to .env.
-REDIS_SESSION_URL=redis://:REDIS_PASSWORD_PLACEHOLDER@redis-sessions:6379/0
+# NOTE: Replace your_redis_password_here with the same password as REDIS_PASSWORD when copying to .env.
+REDIS_SESSION_URL=redis://:your_redis_password_here@redis-sessions:6379/0

 # Rate limiting - Database 1 (distributed rate limit counters)
-REDIS_RATE_LIMIT_URL=redis://:REDIS_PASSWORD_PLACEHOLDER@redis-sessions:6379/1
+REDIS_RATE_LIMIT_URL=redis://:your_redis_password_here@redis-sessions:6379/1

This ensures all Redis credentials follow the same naming convention and makes it clearer that the same password from line 110 should be used across all three URLs.

scripts/migrate-token-hashes.ts (1)

74-79: Consider batch update optimization for large datasets.

The current implementation updates rows individually within the transaction loop. While this is safe and correct for a one-time migration script, if you encounter performance issues with very large token tables, you could explore Drizzle's batch update capabilities or PostgreSQL's UPDATE FROM with unnested arrays for better throughput.

tests/load/auth-baseline.k6.js (2)

78-81: Consider clarifying health check acceptance criteria.

The health check treats both 200 and 404 as acceptable (no warning), but 404 typically means "endpoint not found." While this may be intentional (to handle servers without a /api/health endpoint), adding a brief comment would clarify the intent.

📝 Suggested clarification
   // Test that we can reach the server
   const healthCheck = http.get(`${BASE_URL}/api/health`, { timeout: '5s' });
+  // 404 is acceptable - server is reachable but may not have a health endpoint
   if (healthCheck.status !== 200 && healthCheck.status !== 404) {
     console.warn(`Health check returned ${healthCheck.status} - server may not be running`);
   }

114-121: Empty catch blocks hide parse errors.

The empty catch block discards the actual error, making debugging harder if the endpoint returns malformed JSON. While the outer check handles the failure, logging the parse error would aid troubleshooting.

📝 Suggested improvement
       'csrf returns token': (r) => {
         try {
           const body = JSON.parse(r.body);
           loginCsrfToken = body.csrfToken;
           return !!loginCsrfToken;
-        } catch {
+        } catch (e) {
+          console.log(`CSRF parse error: ${e.message}`);
           return false;
         }
       },

This same pattern applies to the other empty catch blocks at lines 191-193, 244-246, and 284-286.

packages/lib/src/security/security-redis.ts (1)

21-86: LGTM! Past review concerns properly addressed.

The graceful degradation issue raised in the previous review has been correctly resolved:

  • JTI operations (recordJTI, isJTIRevoked, revokeJTI) now use tryGetSessionRedisClient(), which re-throws in production but returns null in dev/test for graceful degradation.
  • Rate limiting and session data operations intentionally use throwing getters (getRateLimitRedisClient(), getSessionRedisClient()), which is appropriate for fail-fast behavior on core features.

The architecture properly separates concerns with dedicated Redis clients for sessions (JTI tracking, session data) and rate limiting.

Optional: Add shutdown function for graceful cleanup

Consider adding a cleanup function to close Redis connections on application shutdown:

/**
 * Shutdown Redis clients gracefully
 * Call this during application shutdown
 */
export async function shutdownSecurityRedis(): Promise<void> {
  const promises: Promise<void>[] = [];
  
  if (sessionRedisClient) {
    promises.push(sessionRedisClient.quit());
    sessionRedisClient = null;
  }
  
  if (rateLimitRedisClient) {
    promises.push(rateLimitRedisClient.quit());
    rateLimitRedisClient = null;
  }
  
  await Promise.allSettled(promises);
}

This is a nice-to-have for clean shutdown but not critical.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0156400 and fbefd20.

📒 Files selected for processing (8)
  • .env.example
  • .github/workflows/load-test.yml
  • apps/web/src/test/setup.ts
  • packages/lib/src/security/security-redis.ts
  • plan.md
  • scripts/migrate-token-hashes.ts
  • scripts/seed-loadtest-user.ts
  • tests/load/auth-baseline.k6.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • scripts/seed-loadtest-user.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Use camelCase for variable and function names
Use UPPER_SNAKE_CASE for constants
Use PascalCase for type and enum names
Use kebab-case for filenames, except React hooks (camelCase with use prefix), Zustand stores (camelCase with use prefix), and React components (PascalCase)
Lint with Next/ESLint as configured in apps/web/eslint.config.mjs
Message content should always use the message parts structure with { parts: [{ type: 'text', text: '...' }] }
Use centralized permission functions from @pagespace/lib/permissions (e.g., getUserAccessLevel, canUserEditPage) instead of implementing permission logic locally
Always use Drizzle client from @pagespace/db package for database access
Use ESM modules throughout the codebase

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Write code that is explicit over implicit and self-documenting

Files:

  • apps/web/src/test/setup.ts
  • scripts/migrate-token-hashes.ts
  • packages/lib/src/security/security-redis.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: React hook files should use camelCase matching the exported hook name (e.g., useAuth.ts)
Zustand store files should use camelCase with use prefix (e.g., useAuthStore.ts)

Files:

  • apps/web/src/test/setup.ts
  • scripts/migrate-token-hashes.ts
  • packages/lib/src/security/security-redis.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • apps/web/src/test/setup.ts
  • scripts/migrate-token-hashes.ts
  • packages/lib/src/security/security-redis.ts
  • tests/load/auth-baseline.k6.js
apps/web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/src/**/*.{ts,tsx}: Use message parts structure for message content: { parts: [{ type: 'text', text: '...' }] }
For database access, always use Drizzle client from @pagespace/db: import { db, pages } from '@pagespace/db';
Use centralized Drizzle ORM with PostgreSQL for all database operations - no direct SQL or other ORMs
Use Socket.IO for real-time collaboration features - imported from the realtime service at port 3001
Use Vercel AI SDK with async/await for all AI operations and streaming
Use Next.js 15 App Router and TypeScript for all routes and components

Files:

  • apps/web/src/test/setup.ts
.env*

📄 CodeRabbit inference engine (AGENTS.md)

Never commit secrets to version control; use .env.example for base config and .env for runtime values

Files:

  • .env.example
🧠 Learnings (19)
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to **/*.{ts,tsx} : Keep commits and diffs minimal and focused on specific changes

Applied to files:

  • apps/web/src/test/setup.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to .env* : Always include critical environment variables in `.env.example`: `DATABASE_URL`, encryption keys, `WEB_APP_URL`, `NEXT_PUBLIC_*` variables, and service ports; never commit actual secrets

Applied to files:

  • apps/web/src/test/setup.ts
  • .env.example
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to packages/db/src/schema.ts : Update database schema in `packages/db/src/schema.ts` and generate migrations with `pnpm db:generate`

Applied to files:

  • scripts/migrate-token-hashes.ts
  • plan.md
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to packages/db/src/schema.ts : Database schema entry point is at `packages/db/src/schema.ts`; migrations emit to `packages/db/drizzle/`

Applied to files:

  • scripts/migrate-token-hashes.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to packages/db/drizzle/*.ts : Database migrations should be generated into `packages/db/drizzle/` directory using `pnpm db:generate` command

Applied to files:

  • scripts/migrate-token-hashes.ts
  • plan.md
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to packages/db/src/schema/**/*.{ts,tsx} : Database schema changes must be made in `packages/db/src/schema/` and then `pnpm db:generate` must be run to create migrations

Applied to files:

  • scripts/migrate-token-hashes.ts
  • plan.md
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: CRITICAL: Never manually create or edit SQL migration files in `packages/db/drizzle/` - always use `pnpm db:generate` to auto-generate migrations from schema changes

Applied to files:

  • scripts/migrate-token-hashes.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to **/*.{ts,tsx} : Always use the Drizzle client and database exports from `pagespace/db` (e.g., `import { db, pages } from 'pagespace/db'`) for all database access

Applied to files:

  • scripts/migrate-token-hashes.ts
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to packages/db/{src/schema.ts,drizzle/**/*.ts} : Database schema must be defined in `packages/db/src/schema.ts` and migrations must be emitted to `packages/db/drizzle/`

Applied to files:

  • scripts/migrate-token-hashes.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to .env* : Never commit secrets to version control; use `.env.example` for base config and `.env` for runtime values

Applied to files:

  • .env.example
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Never commit secrets; base configuration should be in `.env.example` with runtime values in `.env`

Applied to files:

  • .env.example
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Never commit secrets to version control. Base configuration in `.env.example`; runtime configuration in `.env`

Applied to files:

  • .env.example
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to packages/lib/**/*.test.ts : Write unit tests for shared utilities in `packages/lib` with test files named `*.test.ts` alongside source or in `__tests__/` directory

Applied to files:

  • plan.md
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to **/__tests__/**/*.test.ts : Unit tests should be placed next to source files or in `__tests__/` directories with `*.test.ts` extension. Add a `test` script to the package and run with `pnpm --filter <pkg> test`

Applied to files:

  • plan.md
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/web/{app,components,lib,src}/**/*.{ts,tsx,js,jsx} : Code must be formatted with Prettier and linted with Next/ESLint as configured in `apps/web/eslint.config.mjs`

Applied to files:

  • plan.md
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/**/*.{ts,tsx} : Use Next.js 15 App Router and TypeScript for all routes and components

Applied to files:

  • plan.md
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*.{ts,tsx} : Lint with Next/ESLint as configured in `apps/web/eslint.config.mjs`

Applied to files:

  • plan.md
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/components/**/*.{ts,tsx} : Use SWR for server state and caching with proper configuration including `revalidateOnFocus: false` for editing protection

Applied to files:

  • plan.md
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to **/*.{ts,tsx} : Format code with Prettier and lint with Next/ESLint (lint config at `apps/web/eslint.config.mjs`)

Applied to files:

  • plan.md
🪛 Checkov (3.2.334)
.github/workflows/load-test.yml

[medium] 70-71: Basic Auth Credentials

(CKV_SECRET_4)

🪛 LanguageTool
plan.md

[uncategorized] ~118-~118: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...curity/index.tsre-exports distributed rate limiting APIs and security Redis utilities for...

(EN_COMPOUND_ADJECTIVE_INTERNAL)


[uncategorized] ~319-~319: The official name of this software platform is spelled with a capital “H”.
Context: ...y-fixtures.tswith test data - Created.github/workflows/security.yml` GitHub workflow...

(GITHUB)


[uncategorized] ~409-~409: The official name of this software platform is spelled with a capital “H”.
Context: ...andling and cookie management - Created .github/workflows/load-test.yml GitHub workflo...

(GITHUB)

🪛 markdownlint-cli2 (0.18.1)
plan.md

410-410: Bare URL used

(MD034, no-bare-urls)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Unit Tests
🔇 Additional comments (10)
.github/workflows/load-test.yml (2)

1-16: LGTM!

The workflow triggers are well-configured with a weekly schedule and manual dispatch option. The configurable VUs and duration parameters provide good flexibility for different load testing scenarios.


89-101: LGTM! Addresses the past review concern.

The current implementation uses the official grafana/k6 Docker image directly instead of the archived GitHub Action, which resolves the concern raised in the previous review. Running k6 via Docker is a recommended approach that ensures access to updated k6 versions without depending on an archived GitHub Action.

scripts/migrate-token-hashes.ts (3)

21-27: LGTM - Helper functions are well-implemented.

The use of SHA-256 for token hashing is appropriate, and the prefix extraction handles short tokens gracefully by returning the entire token if it's less than 12 characters.


31-93: Excellent refactoring - previous issues resolved.

The generic migrateTokenTable function successfully addresses both the SQL injection vulnerability (now uses parameterized updates via Drizzle's query builder) and the code duplication issue (single function handles both tables).

The transaction-based batch processing with progress reporting is well-implemented and safe.


95-132: LGTM - Well-structured main function.

The main function has good error handling, clear summary output, and helpful next steps. The explicit process.exit() calls ensure proper exit codes.

tests/load/auth-baseline.k6.js (4)

148-212: LGTM - Login flow with robust error handling.

Good error differentiation by status code (403 for CSRF, 401 for auth, 429 for rate limit) aids debugging. The manual cookie parsing at lines 171-183 properly handles edge cases like values containing =.


222-256: LGTM - Protected endpoint test.

Using /api/drives as the protected endpoint is a practical approach that tests real auth cookie validation. The flexible response check handles both body as array and body.drives array shapes.


260-304: LGTM - Token refresh with separate error tracking.

Good decision to use a separate refreshErrorRate metric since refresh operations have different expected failure rates (token consumption across VUs). The cookie validation check is a reasonable approximation for load testing.


320-372: LGTM - Comprehensive summary reporting.

The summary structure properly captures all new metrics (CSRF, refresh), includes sample counts for each metric, and correctly aggregates threshold pass/fail status. The JSON output at tests/load/results/baseline-latest.json provides a machine-readable format for trend analysis.

apps/web/src/test/setup.ts (1)

24-31: LGTM! Good practice for CI compatibility.

The conditional assignment pattern (process.env.X = process.env.X || 'default') properly allows CI environments to override test configuration while providing sensible defaults. All critical environment variables are covered.

Comment thread scripts/migrate-token-hashes.ts Outdated
2witstudios and others added 2 commits January 7, 2026 10:37
The security-redis module requires separate Redis URLs for session
and rate limit databases. Add these to test.yml workflow.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When REDIS_URL is provided (CI), automatically derive REDIS_SESSION_URL
and REDIS_RATE_LIMIT_URL by adding database indices (/0 and /1).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @packages/lib/src/test/setup.ts:
- Around line 44-49: The current logic in setup.ts uses
process.env.REDIS_URL.replace(/\/\d*$/, '/0') || ... which fails for URLs
without a trailing "/<db>" because replace returns the original string; update
the derivation to detect whether REDIS_URL already ends with a "/<digits>"
(e.g., use RegExp.test on process.env.REDIS_URL or string.endsWith after
matching) and if it does, replace that suffix with "/0" (for REDIS_SESSION_URL)
or "/1" (for REDIS_RATE_LIMIT_URL), otherwise append "/0" or "/1"; change the
assignments for REDIS_SESSION_URL and REDIS_RATE_LIMIT_URL accordingly so both
cases (with and without existing DB suffix) produce the correct URL.
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dd9dbfe and 392436f.

📒 Files selected for processing (1)
  • packages/lib/src/test/setup.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Use camelCase for variable and function names
Use UPPER_SNAKE_CASE for constants
Use PascalCase for type and enum names
Use kebab-case for filenames, except React hooks (camelCase with use prefix), Zustand stores (camelCase with use prefix), and React components (PascalCase)
Lint with Next/ESLint as configured in apps/web/eslint.config.mjs
Message content should always use the message parts structure with { parts: [{ type: 'text', text: '...' }] }
Use centralized permission functions from @pagespace/lib/permissions (e.g., getUserAccessLevel, canUserEditPage) instead of implementing permission logic locally
Always use Drizzle client from @pagespace/db package for database access
Use ESM modules throughout the codebase

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Write code that is explicit over implicit and self-documenting

Files:

  • packages/lib/src/test/setup.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: React hook files should use camelCase matching the exported hook name (e.g., useAuth.ts)
Zustand store files should use camelCase with use prefix (e.g., useAuthStore.ts)

Files:

  • packages/lib/src/test/setup.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • packages/lib/src/test/setup.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Lint & TypeScript Check
  • GitHub Check: Unit Tests

Comment thread packages/lib/src/test/setup.ts
2witstudios and others added 2 commits January 7, 2026 11:03
Turborepo requires environment variables to be explicitly listed in
globalEnv to pass them through to tasks. Add REDIS_URL, REDIS_SESSION_URL,
and REDIS_RATE_LIMIT_URL for security-redis tests.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The security-redis module uses module-level cached Redis clients from
getSessionRedisClient() and getRateLimitRedisClient(). The tests were
mocking shared-redis but those functions create their own Redis instances.

Fixed by:
- Mocking ioredis directly so new Redis() returns our mock
- Using shared state (sharedMockStore, sharedSortedSets) since the
  module caches clients and all tests use the same mock instance
- Clearing state in beforeEach for test isolation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/lib/src/security/__tests__/security-redis.test.ts (2)

105-144: Mock setup order is correct; consider improving type safety.

The mocks are correctly defined before importing the module under test, which is essential for testing module-level cached clients. However, multiple as never casts are used when mocking getSharedRedisClient (lines 163, 181, 190, 215, 490), which bypasses type checking.

♻️ Consider using a properly typed mock

Instead of as never, consider creating a mock type that satisfies the Redis interface or using vi.mocked() with proper generic types:

-vi.mocked(getSharedRedisClient).mockResolvedValue(mockRedis as never);
+vi.mocked(getSharedRedisClient).mockResolvedValue(mockRedis as unknown as Redis);

Or define a type-safe mock that implements the Redis interface used by the tests.


299-305: Strengthen TTL verification test.

The test "preserves original TTL on revocation" only verifies that setex was called twice but doesn't confirm the TTL value is preserved. Consider verifying the actual TTL passed to the second setex call matches the remaining TTL from the first.

♻️ Proposed improvement
 it('preserves original TTL on revocation', async () => {
   await recordJTI('ttl-test', 'user-1', 300);
   await revokeJTI('ttl-test', 'test');

-  // setex should be called twice - once for record, once for revoke
   expect(mockRedis.setex).toHaveBeenCalledTimes(2);
+  // Verify both calls used the same TTL (300 seconds)
+  const firstCall = mockRedis.setex.mock.calls[0];
+  const secondCall = mockRedis.setex.mock.calls[1];
+  expect(firstCall[1]).toBe(300);
+  expect(secondCall[1]).toBe(300);
 });
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e2d79d9 and dba4cf1.

📒 Files selected for processing (1)
  • packages/lib/src/security/__tests__/security-redis.test.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Use camelCase for variable and function names
Use UPPER_SNAKE_CASE for constants
Use PascalCase for type and enum names
Use kebab-case for filenames, except React hooks (camelCase with use prefix), Zustand stores (camelCase with use prefix), and React components (PascalCase)
Lint with Next/ESLint as configured in apps/web/eslint.config.mjs
Message content should always use the message parts structure with { parts: [{ type: 'text', text: '...' }] }
Use centralized permission functions from @pagespace/lib/permissions (e.g., getUserAccessLevel, canUserEditPage) instead of implementing permission logic locally
Always use Drizzle client from @pagespace/db package for database access
Use ESM modules throughout the codebase

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Write code that is explicit over implicit and self-documenting

Files:

  • packages/lib/src/security/__tests__/security-redis.test.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: React hook files should use camelCase matching the exported hook name (e.g., useAuth.ts)
Zustand store files should use camelCase with use prefix (e.g., useAuthStore.ts)

Files:

  • packages/lib/src/security/__tests__/security-redis.test.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • packages/lib/src/security/__tests__/security-redis.test.ts
🧬 Code graph analysis (1)
packages/lib/src/security/__tests__/security-redis.test.ts (1)
packages/lib/src/services/shared-redis.ts (2)
  • getSharedRedisClient (25-39)
  • isSharedRedisAvailable (44-46)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Static Security Analysis
  • GitHub Check: Unit Tests
🔇 Additional comments (5)
packages/lib/src/security/__tests__/security-redis.test.ts (5)

3-108: LGTM! Shared mock architecture is well-designed for testing cached Redis clients.

The use of a single shared mock instance with persistent stores (sharedMockStore, sharedSortedSets) correctly addresses the challenge of testing module-level cached Redis clients. The mock implementation comprehensively covers Redis operations (setex, get, ttl, del, pipeline with sorted sets, etc.) and properly simulates expiry behavior.

The _store and _sortedSets accessors allow test inspection of internal state, which is acceptable in unit tests but bypasses the public API.


146-171: LGTM! Test lifecycle properly ensures isolation.

The lifecycle hooks correctly handle test isolation:

  • beforeAll sets Redis URLs once before module initialization
  • beforeEach clears both shared stores and mock call history
  • afterEach restores environment variables

This approach prevents test interdependence while supporting the shared mock architecture.


334-434: LGTM! Comprehensive rate limiting test coverage.

The rate limiting tests thoroughly verify:

  • Allow/block behavior based on limits
  • Sliding window algorithm using Redis sorted sets
  • Time-based reset functionality
  • Key isolation between different rate limit contexts
  • Status checks that don't increment counters
  • Reset operations

436-477: LGTM! Session operation tests cover key scenarios.

The session tests verify:

  • Storage with expiry
  • Retrieval of valid data
  • Graceful handling of nonexistent and corrupted data
  • Deletion operations

Direct _store access on line 463 is used to inject corrupted data for error path testing, which is appropriate.


479-505: LGTM! Health check tests verify availability and latency.

The health check tests appropriately verify:

  • Success path with availability and latency metrics
  • Failure path with error information
  • Latency measurement accuracy

2witstudios and others added 2 commits January 7, 2026 11:46
The previous logic used replace().|| fallback which never triggered
since String.replace always returns a string. Now correctly strips
any existing database suffix before appending /0 or /1.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
SECURITY CRITICAL: When Redis is unavailable in production, security
operations now fail-closed instead of silently degrading:

- isJTIRevoked: Returns true (revoked) when Redis unavailable
- recordJTI: Throws in production when Redis unavailable
- revokeJTI: Throws in production when Redis unavailable
- checkDistributedRateLimit: Denies requests in production when Redis down

Also adds:
- ioredis reconnection config with exponential backoff
- Separate tryGetRateLimitRedisClient for consistent client usage
- Connection status checks to handle broken Redis connections
- Updated tests for fail-closed behavior

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @packages/lib/src/security/security-redis.ts:
- Around line 96-107: getRateLimitRedisClient has the same TypeScript type error
when checking rateLimitRedisClient?.status === 'ready'; update the check the
same way you fixed getSessionRedisClient: narrow or cast the client type before
reading .status (e.g., assert rateLimitRedisClient as Redis or check using a
type guard) and ensure the 'ready' comparison uses the correct Redis status
union so TypeScript is satisfied; adjust the early return and the subsequent
broken-connection block to use the corrected typed check on
rateLimitRedisClient.status.
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dba4cf1 and 708a14a.

📒 Files selected for processing (4)
  • packages/lib/src/security/__tests__/distributed-rate-limit.test.ts
  • packages/lib/src/security/distributed-rate-limit.ts
  • packages/lib/src/security/security-redis.ts
  • packages/lib/src/test/setup.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Use camelCase for variable and function names
Use UPPER_SNAKE_CASE for constants
Use PascalCase for type and enum names
Use kebab-case for filenames, except React hooks (camelCase with use prefix), Zustand stores (camelCase with use prefix), and React components (PascalCase)
Lint with Next/ESLint as configured in apps/web/eslint.config.mjs
Message content should always use the message parts structure with { parts: [{ type: 'text', text: '...' }] }
Use centralized permission functions from @pagespace/lib/permissions (e.g., getUserAccessLevel, canUserEditPage) instead of implementing permission logic locally
Always use Drizzle client from @pagespace/db package for database access
Use ESM modules throughout the codebase

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Write code that is explicit over implicit and self-documenting

Files:

  • packages/lib/src/security/__tests__/distributed-rate-limit.test.ts
  • packages/lib/src/test/setup.ts
  • packages/lib/src/security/security-redis.ts
  • packages/lib/src/security/distributed-rate-limit.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: React hook files should use camelCase matching the exported hook name (e.g., useAuth.ts)
Zustand store files should use camelCase with use prefix (e.g., useAuthStore.ts)

Files:

  • packages/lib/src/security/__tests__/distributed-rate-limit.test.ts
  • packages/lib/src/test/setup.ts
  • packages/lib/src/security/security-redis.ts
  • packages/lib/src/security/distributed-rate-limit.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • packages/lib/src/security/__tests__/distributed-rate-limit.test.ts
  • packages/lib/src/test/setup.ts
  • packages/lib/src/security/security-redis.ts
  • packages/lib/src/security/distributed-rate-limit.ts
🧬 Code graph analysis (1)
packages/lib/src/security/distributed-rate-limit.ts (2)
packages/lib/src/security/security-redis.ts (1)
  • tryGetRateLimitRedisClient (165-174)
packages/lib/src/logging/logger-config.ts (1)
  • loggers (8-18)
🪛 GitHub Actions: Security Tests
packages/lib/src/security/security-redis.ts

[error] 54-54: TypeScript error TS2367: This comparison appears to be unintentional because the types '"end" | "connect" | "wait" | "reconnecting" | "connecting" | "close"' and '"ready"' have no overlap.

🪛 GitHub Actions: Test Suite
packages/lib/src/security/security-redis.ts

[error] 54-54: TS2367: This comparison appears to be unintentional because the types '"end" | "connect" | "wait" | "reconnecting" | "connecting" | "close"' and '"ready"' have no overlap.

🔇 Additional comments (11)
packages/lib/src/security/__tests__/distributed-rate-limit.test.ts (3)

1-9: LGTM - Mock setup properly updated for new rate limit Redis client.

The mock correctly reflects the API change from tryGetSecurityRedisClient to tryGetRateLimitRedisClient, aligning with the security-redis module refactoring.


175-225: Thorough coverage of fail-closed behavior in production.

These tests correctly verify that when Redis is unavailable or errors occur in production, the system fails closed with allowed: false and retryAfter: 60, matching the security requirements. The logging assertions ensure observability of these critical failure modes.


428-463: In-memory fallback tests properly scoped to development.

The describe block clearly indicates these tests apply to development only, and the beforeEach correctly sets NODE_ENV = 'development'. The isolation and expiry tests validate the in-memory fallback behavior.

packages/lib/src/security/security-redis.ts (4)

25-44: Solid Redis client options with appropriate reconnection strategy.

The exponential backoff (capped at 3 seconds), 10 retry limit, and targeted error reconnection provide good resilience without excessive retry storms.


219-246: Proper fail-closed implementation for JTI verification.

isJTIRevoked correctly returns true (treat as revoked) when Redis is unavailable or data is corrupted. This fail-closed approach is the right security posture for token validation.


161-174: Appropriate production vs development behavior for rate limit Redis client.

The tryGetRateLimitRedisClient correctly re-throws in production (fail-fast) while returning null in non-production for graceful degradation. This aligns with the distributed rate limiting module's expectations.


50-61: The review comment is incorrect. The ioredis library (version ^5.8.0 as configured) explicitly supports 'ready' as a valid status value. The documented status values are: "wait", "connecting", "connect", "ready", "reconnecting", "close", and "end". The code correctly uses 'ready' to check for an active connection, and there is no TS2367 type error. No changes are needed.

Likely an incorrect or invalid review comment.

packages/lib/src/security/distributed-rate-limit.ts (3)

203-268: Well-structured fail-closed rate limiting with environment-aware fallback.

The implementation correctly:

  1. Uses tryGetRateLimitRedisClient for the dedicated rate limit Redis connection
  2. Falls back to in-memory only in development
  3. Fails closed in production with a 60-second retry window
  4. Logs appropriately at error level for production failures

This is the correct security posture for distributed rate limiting.


349-354: OAUTH_VERIFY configuration matches REFRESH limits.

The configuration (10 attempts / 5 minutes) is appropriate for OAuth verification flows and consistent with the REFRESH token limits.


273-287: Robust reset implementation with dual-store cleanup.

Resetting both Redis and in-memory ensures consistent state regardless of which backend was used. The graceful error handling prevents reset failures from disrupting authentication flows.

packages/lib/src/test/setup.ts (1)

44-53: LGTM - Redis URL derivation correctly fixed.

The fix properly addresses the previous review comment by:

  1. Stripping any existing database suffix with replace(/\/\d+$/, '')
  2. Then unconditionally appending the desired database (/0 or /1)

This ensures correct behavior whether the original URL has a database suffix or not.

Comment thread packages/lib/src/security/security-redis.ts
Cast status to string to handle version differences in ioredis types
where 'ready' vs 'connect' varies between versions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/lib/src/security/security-redis.ts (1)

52-55: Consider narrowing the connection state check.

The cast to string works around TypeScript's strict union types but loses compile-time safety. While this handles ioredis version differences ('ready' vs 'connect'), transient states like 'connecting' or 'reconnecting' will currently cause a premature client reset (line 58).

♻️ Proposed refinement
-  const sessionStatus = sessionRedisClient?.status as string;
-  if (sessionStatus === 'ready' || sessionStatus === 'connect') {
+  const sessionStatus = sessionRedisClient?.status;
+  if (sessionStatus && ['ready', 'connect', 'connecting', 'reconnecting'].includes(sessionStatus)) {
     return sessionRedisClient!;
   }

Then update the reset check to exclude transient states:

-  if (sessionRedisClient && sessionStatus !== 'ready' && sessionStatus !== 'connect') {
+  const isHealthy = sessionStatus && ['ready', 'connect', 'connecting', 'reconnecting'].includes(sessionStatus);
+  if (sessionRedisClient && !isHealthy) {
     try {
       await sessionRedisClient.quit();

This avoids resetting during normal connection establishment and provides a whitelist of acceptable states.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 708a14a and 12d2952.

📒 Files selected for processing (1)
  • packages/lib/src/security/security-redis.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Use camelCase for variable and function names
Use UPPER_SNAKE_CASE for constants
Use PascalCase for type and enum names
Use kebab-case for filenames, except React hooks (camelCase with use prefix), Zustand stores (camelCase with use prefix), and React components (PascalCase)
Lint with Next/ESLint as configured in apps/web/eslint.config.mjs
Message content should always use the message parts structure with { parts: [{ type: 'text', text: '...' }] }
Use centralized permission functions from @pagespace/lib/permissions (e.g., getUserAccessLevel, canUserEditPage) instead of implementing permission logic locally
Always use Drizzle client from @pagespace/db package for database access
Use ESM modules throughout the codebase

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Write code that is explicit over implicit and self-documenting

Files:

  • packages/lib/src/security/security-redis.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: React hook files should use camelCase matching the exported hook name (e.g., useAuth.ts)
Zustand store files should use camelCase with use prefix (e.g., useAuthStore.ts)

Files:

  • packages/lib/src/security/security-redis.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • packages/lib/src/security/security-redis.ts
🧬 Code graph analysis (1)
packages/lib/src/security/security-redis.ts (3)
apps/processor/src/logger.ts (1)
  • error (57-63)
packages/lib/src/security/index.ts (3)
  • recordJTI (19-19)
  • isJTIRevoked (20-20)
  • revokeJTI (21-21)
packages/lib/src/logging/logger-config.ts (1)
  • loggers (8-18)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Unit Tests
  • GitHub Check: Lint & TypeScript Check
🔇 Additional comments (3)
packages/lib/src/security/security-redis.ts (3)

338-407: Verify fail-fast behavior for rate limiting operations is intentional.

Rate limiting operations call getRateLimitRedisClient() directly, which throws when Redis is unavailable in any environment (including development). This differs from JTI operations, which use tryGetSessionRedisClient() and gracefully degrade in non-production environments.

While fail-fast for a security feature like rate limiting may be intentional, this creates an inconsistent developer experience: JTI operations continue with warnings in development, but rate limiting operations fail hard.

Confirm whether this is the intended design, or whether rate limiting should also gracefully degrade in development.

If graceful degradation is desired in development:

♻️ Option to align with JTI pattern

Update these functions to use tryGetRateLimitRedisClient() and handle null:

export async function checkRateLimit(
  key: string,
  limit: number,
  windowMs: number
): Promise<RateLimitResult> {
  const redis = await tryGetRateLimitRedisClient();
  if (!redis) {
    if (process.env.NODE_ENV === 'production') {
      throw new Error('Rate limiting unavailable in production');
    }
    // Graceful degradation: allow request but log
    loggers.api.warn('Rate limiting skipped: Redis unavailable', { key });
    return {
      allowed: true,
      remaining: limit,
      resetAt: new Date(Date.now() + windowMs),
      totalCount: 0,
    };
  }
  // ... rest of implementation
}

Apply similar patterns to getRateLimitStatus and resetRateLimit.


195-304: JTI operations correctly implement fail-closed security with graceful degradation.

The JTI functions properly use tryGetSessionRedisClient() and handle unavailability with appropriate security semantics:

  • recordJTI: Throws in production, logs warning in dev
  • isJTIRevoked: Fails closed (returns true) when Redis unavailable, treating unknown tokens as revoked
  • revokeJTI: Throws in production, returns false with warning in dev

This addresses the concerns from past review comments about graceful degradation for separate Redis clients.


85-94: Try wrappers correctly implement environment-aware error handling.

Both tryGetSessionRedisClient() and tryGetRateLimitRedisClient() properly re-throw in production while returning null in development, enabling fail-closed production behavior with graceful development degradation.

Also applies to: 173-182

initializeDistributedRateLimiting now throws in production when Redis
is unavailable instead of returning an error object. This ensures the
application fails to start rather than silently degrading to in-memory
rate limiting.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
packages/lib/src/security/distributed-rate-limit.ts (1)

292-318: Address inconsistent fail-closed behavior in status check function.

getDistributedRateLimitStatus silently falls back to in-memory state when Redis is unavailable in all environments (lines 313-315), while checkDistributedRateLimit explicitly fails closed in production (lines 254-264).

In a distributed production deployment with Redis down, this status check returns potentially stale or incorrect in-memory state instead of failing or returning a conservative response. This inconsistency is undocumented and could lead to incorrect rate-limit status being reported.

Decide whether the soft-fail behavior is intentional for non-mutating status checks, or align it with the production fail-closed pattern used for checkDistributedRateLimit. If intentional, add a comment explaining why status checks differ from blocking operations.

🤖 Fix all issues with AI agents
In @packages/lib/src/security/distributed-rate-limit.ts:
- Around line 254-264: The fail-closed branch currently returns a hardcoded
retryAfter and unsafely trims the identifier; change it to compute retryAfter
from the actual rate-limit window used for this request (e.g., the configured
windowSeconds / windowMs for the relevant rule—LOGIN/SIGNUP/OAUTH_VERIFY—in the
same scope as the rate-limit check) so clients get a correct wait time, and
replace the unsafe identifier.substring(0, 20) with a safe truncation that won’t
throw on short/undefined identifiers (e.g., coalesce to an empty string then
slice/truncate to 20 chars or use Math.min on length) so logging never throws;
update the object returned in the fail-closed path to use the computed
retryAfter and the safe-truncated identifier.
🧹 Nitpick comments (2)
packages/lib/src/security/distributed-rate-limit.ts (2)

273-287: Consider elevating reset failure logging or propagating errors.

resetDistributedRateLimit catches Redis errors and logs them at debug level (line 281), but always succeeds silently. If the Redis reset fails, the caller has no indication, and subsequent requests will still be rate-limited even after a successful authentication.

While making this throw would complicate callers, consider either:

  1. Logging failures at warn or error level (not debug) so they're visible in production
  2. Returning a boolean success indicator so callers can handle failures

Based on learnings, this aligns with the PR's focus on fail-closed semantics and observability.

♻️ Option 1: Elevate log level
  } catch (error) {
-   loggers.api.debug('Redis rate limit reset failed', {
+   loggers.api.warn('Redis rate limit reset failed - rate limit may persist', {
      error: error instanceof Error ? error.message : String(error),
    });
  }

349-354: Consider deduplicating or documenting identical rate limit configs.

OAUTH_VERIFY (lines 349-354) is identical to REFRESH (lines 343-348). If these limits should always remain synchronized, consider deduplicating:

REFRESH: {
  maxAttempts: 10,
  windowMs: 5 * 60 * 1000,
  blockDurationMs: 5 * 60 * 1000,
  progressiveDelay: false,
} as const,
OAUTH_VERIFY: {
  maxAttempts: 10,
  windowMs: 5 * 60 * 1000,
  blockDurationMs: 5 * 60 * 1000,
  progressiveDelay: false,
} as const,

If they're intentionally separate and may diverge in the future, the current approach is fine—just worth noting.

♻️ Possible deduplication approach
+ const STANDARD_SHORT_WINDOW = {
+   maxAttempts: 10,
+   windowMs: 5 * 60 * 1000,
+   blockDurationMs: 5 * 60 * 1000,
+   progressiveDelay: false,
+ } as const;
+
  REFRESH: {
-   maxAttempts: 10,
-   windowMs: 5 * 60 * 1000,
-   blockDurationMs: 5 * 60 * 1000,
-   progressiveDelay: false,
+   ...STANDARD_SHORT_WINDOW,
  },
  OAUTH_VERIFY: {
-   maxAttempts: 10,
-   windowMs: 5 * 60 * 1000,
-   blockDurationMs: 5 * 60 * 1000,
-   progressiveDelay: false,
+   ...STANDARD_SHORT_WINDOW,
  },
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 12d2952 and 5607d05.

📒 Files selected for processing (2)
  • packages/lib/src/security/__tests__/distributed-rate-limit.test.ts
  • packages/lib/src/security/distributed-rate-limit.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Use camelCase for variable and function names
Use UPPER_SNAKE_CASE for constants
Use PascalCase for type and enum names
Use kebab-case for filenames, except React hooks (camelCase with use prefix), Zustand stores (camelCase with use prefix), and React components (PascalCase)
Lint with Next/ESLint as configured in apps/web/eslint.config.mjs
Message content should always use the message parts structure with { parts: [{ type: 'text', text: '...' }] }
Use centralized permission functions from @pagespace/lib/permissions (e.g., getUserAccessLevel, canUserEditPage) instead of implementing permission logic locally
Always use Drizzle client from @pagespace/db package for database access
Use ESM modules throughout the codebase

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Write code that is explicit over implicit and self-documenting

Files:

  • packages/lib/src/security/__tests__/distributed-rate-limit.test.ts
  • packages/lib/src/security/distributed-rate-limit.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: React hook files should use camelCase matching the exported hook name (e.g., useAuth.ts)
Zustand store files should use camelCase with use prefix (e.g., useAuthStore.ts)

Files:

  • packages/lib/src/security/__tests__/distributed-rate-limit.test.ts
  • packages/lib/src/security/distributed-rate-limit.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • packages/lib/src/security/__tests__/distributed-rate-limit.test.ts
  • packages/lib/src/security/distributed-rate-limit.ts
🧬 Code graph analysis (2)
packages/lib/src/security/__tests__/distributed-rate-limit.test.ts (3)
packages/lib/src/security/security-redis.ts (1)
  • tryGetRateLimitRedisClient (173-182)
packages/lib/src/security/distributed-rate-limit.ts (3)
  • checkDistributedRateLimit (203-268)
  • resetDistributedRateLimit (273-287)
  • initializeDistributedRateLimiting (383-415)
packages/lib/src/security/index.ts (3)
  • checkDistributedRateLimit (31-31)
  • resetDistributedRateLimit (32-32)
  • initializeDistributedRateLimiting (34-34)
packages/lib/src/security/distributed-rate-limit.ts (3)
packages/lib/src/security/security-redis.ts (1)
  • tryGetRateLimitRedisClient (173-182)
packages/lib/src/logging/logger-config.ts (1)
  • loggers (8-18)
apps/processor/src/logger.ts (1)
  • error (57-63)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Static Security Analysis
  • GitHub Check: Unit Tests
🔇 Additional comments (9)
packages/lib/src/security/__tests__/distributed-rate-limit.test.ts (7)

4-9: LGTM!

The mock structure correctly reflects the updated public API (tryGetRateLimitRedisClient), and the imports are properly aligned with the mock definitions.

Also applies to: 32-37


57-60: LGTM!

The mock setup correctly simulates Redis availability, and the subsequent tests thoroughly cover the Redis-backed rate limiting behavior including progressive delay logic.


149-189: LGTM!

The tests properly cover both development (in-memory fallback) and production (fail-closed denial) behaviors when Redis is unavailable. The explicit NODE_ENV settings ensure proper test isolation, and the assertions correctly match the implementation's fail-closed semantics.


207-225: LGTM!

Excellent addition testing the production fail-closed behavior when Redis is available but the rate limit check fails. The test correctly verifies both the warning log (from the catch block) and the subsequent error log (from the fail-closed path), along with the expected denial response.


324-339: LGTM!

The fail-fast initialization tests correctly verify that production environments fail immediately when Redis is unavailable or when the ping check fails. This aligns with the fail-closed security model and prevents silent degradation in production.


391-422: LGTM!

The shutdown tests correctly set up the development environment to test in-memory state clearing. The idempotency test ensures safe repeated calls.


424-428: LGTM!

Good clarification by adding "(development only)" to the describe block name. The explicit NODE_ENV = 'development' setup makes the test intent clear and ensures proper isolation of the in-memory fallback tests.

packages/lib/src/security/distributed-rate-limit.ts (2)

16-21: LGTM! Consistent Redis client access pattern.

The migration to tryGetRateLimitRedisClient aligns with the fail-closed production semantics and centralizes rate-limit Redis client access.


383-414: LGTM! Fail-fast initialization in production.

The fail-fast behavior (throwing in production when Redis is unavailable) aligns with the PR objectives and ensures the application doesn't start in a degraded state. Development retains the in-memory fallback with appropriate warnings.

Comment thread packages/lib/src/security/distributed-rate-limit.ts
- Fix hardcoded retryAfter in fail-closed path: now computed from config.windowMs
- Fix unsafe identifier.substring: use safe String().slice() pattern
- Add fail-closed behavior to getDistributedRateLimitStatus in production
- Fix Redis URL derivation for URLs without trailing slash
- Add batch size validation in migrate-token-hashes.ts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 0

🧹 Nitpick comments (3)
scripts/migrate-token-hashes.ts (1)

81-86: Consider batch update optimization for large datasets.

The current implementation issues individual UPDATE statements for each token in the batch. While this works correctly within the transaction, it may be less efficient for large datasets. Consider exploring bulk update patterns if Drizzle supports updating multiple rows with different values in a single query.

Note: For a one-time migration script, the current approach is acceptable and prioritizes code clarity over performance optimization.

packages/lib/src/test/setup.ts (1)

42-54: Robust Redis URL derivation logic.

The regex pattern /\/\d*$/ correctly handles various Redis URL formats:

  • redis://host:6379 → redis://host:6379/0
  • redis://host:6379/ → redis://host:6379/0
  • redis://host:6379/0 → redis://host:6379/0
  • redis://host:6379/15 → redis://host:6379/0

This ensures session (database 0) and rate-limit (database 1) clients use separate Redis databases regardless of the input format.

♻️ Optional refactor to reduce duplication

The same regex logic is used on lines 47 and 52. You could extract it to reduce duplication:

+// Helper to strip database suffix from Redis URL
+function getBaseRedisUrl(url: string): string {
+  return url.replace(/\/\d*$/, '');
+}
+
 // Security Redis URLs - required for security-redis tests
 // These use separate databases on the same Redis instance
 if (process.env.REDIS_URL && !process.env.REDIS_SESSION_URL) {
-  // Strip any existing database suffix (e.g., /0, /15) or trailing slash, then append /0
-  // Handles: redis://host:6379, redis://host:6379/, redis://host:6379/0, redis://host:6379/15
-  const baseUrl = process.env.REDIS_URL.replace(/\/\d*$/, '')
+  const baseUrl = getBaseRedisUrl(process.env.REDIS_URL);
   process.env.REDIS_SESSION_URL = `${baseUrl}/0`
 }
 if (process.env.REDIS_URL && !process.env.REDIS_RATE_LIMIT_URL) {
-  // Strip any existing database suffix or trailing slash, then append /1
-  const baseUrl = process.env.REDIS_URL.replace(/\/\d*$/, '')
+  const baseUrl = getBaseRedisUrl(process.env.REDIS_URL);
   process.env.REDIS_RATE_LIMIT_URL = `${baseUrl}/1`
 }
packages/lib/src/security/distributed-rate-limit.ts (1)

254-269: Solid fail-closed implementation with safe identifier handling.

The production fail-closed logic properly:

  • Uses String(identifier ?? '').slice(0, 20) to safely truncate identifiers without throwing on undefined/null
  • Computes retryAfter from the actual rate-limit window (config.windowMs)
  • Logs explicit denial with truncated identifier for security
  • Returns blocked status with 0 remaining attempts
♻️ Optional: Minor refinement for ellipsis logic

The ellipsis check on line 262 could be more precise:

-    const safeId = String(identifier ?? '').slice(0, 20);
+    const originalId = String(identifier ?? '');
+    const safeId = originalId.slice(0, 20);
     // Compute retryAfter from the actual rate-limit window for this request
     const retryAfterSeconds = Math.ceil(config.windowMs / 1000);

     loggers.api.error('Redis unavailable in production - DENYING request (fail-closed)', {
-      identifier: safeId.length >= 20 ? `${safeId}...` : safeId,
+      identifier: originalId.length > 20 ? `${safeId}...` : safeId,
     });

This prevents adding ... when the original identifier is exactly 20 characters.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5607d05 and ce418ad.

📒 Files selected for processing (4)
  • packages/lib/src/security/__tests__/distributed-rate-limit.test.ts
  • packages/lib/src/security/distributed-rate-limit.ts
  • packages/lib/src/test/setup.ts
  • scripts/migrate-token-hashes.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Use camelCase for variable and function names
Use UPPER_SNAKE_CASE for constants
Use PascalCase for type and enum names
Use kebab-case for filenames, except React hooks (camelCase with use prefix), Zustand stores (camelCase with use prefix), and React components (PascalCase)
Lint with Next/ESLint as configured in apps/web/eslint.config.mjs
Message content should always use the message parts structure with { parts: [{ type: 'text', text: '...' }] }
Use centralized permission functions from @pagespace/lib/permissions (e.g., getUserAccessLevel, canUserEditPage) instead of implementing permission logic locally
Always use Drizzle client from @pagespace/db package for database access
Use ESM modules throughout the codebase

**/*.{ts,tsx}: Never use any types - always use proper TypeScript types
Write code that is explicit over implicit and self-documenting

Files:

  • packages/lib/src/test/setup.ts
  • packages/lib/src/security/__tests__/distributed-rate-limit.test.ts
  • scripts/migrate-token-hashes.ts
  • packages/lib/src/security/distributed-rate-limit.ts
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: React hook files should use camelCase matching the exported hook name (e.g., useAuth.ts)
Zustand store files should use camelCase with use prefix (e.g., useAuthStore.ts)

Files:

  • packages/lib/src/test/setup.ts
  • packages/lib/src/security/__tests__/distributed-rate-limit.test.ts
  • scripts/migrate-token-hashes.ts
  • packages/lib/src/security/distributed-rate-limit.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • packages/lib/src/test/setup.ts
  • packages/lib/src/security/__tests__/distributed-rate-limit.test.ts
  • scripts/migrate-token-hashes.ts
  • packages/lib/src/security/distributed-rate-limit.ts
🧠 Learnings (5)
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to packages/db/src/schema.ts : Update database schema in `packages/db/src/schema.ts` and generate migrations with `pnpm db:generate`

Applied to files:

  • scripts/migrate-token-hashes.ts
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to packages/db/src/schema.ts : Database schema entry point is at `packages/db/src/schema.ts`; migrations emit to `packages/db/drizzle/`

Applied to files:

  • scripts/migrate-token-hashes.ts
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to packages/db/drizzle/*.ts : Database migrations should be generated into `packages/db/drizzle/` directory using `pnpm db:generate` command

Applied to files:

  • scripts/migrate-token-hashes.ts
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to packages/db/src/schema/**/*.{ts,tsx} : Database schema changes must be made in `packages/db/src/schema/` and then `pnpm db:generate` must be run to create migrations

Applied to files:

  • scripts/migrate-token-hashes.ts
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: CRITICAL: Never manually create or edit SQL migration files in `packages/db/drizzle/` - always use `pnpm db:generate` to auto-generate migrations from schema changes

Applied to files:

  • scripts/migrate-token-hashes.ts
🧬 Code graph analysis (2)
packages/lib/src/security/__tests__/distributed-rate-limit.test.ts (3)
packages/lib/src/security/security-redis.ts (1)
  • tryGetRateLimitRedisClient (173-182)
packages/lib/src/security/distributed-rate-limit.ts (4)
  • checkDistributedRateLimit (203-273)
  • resetDistributedRateLimit (278-292)
  • getDistributedRateLimitStatus (299-336)
  • initializeDistributedRateLimiting (401-433)
packages/lib/src/security/index.ts (4)
  • checkDistributedRateLimit (31-31)
  • resetDistributedRateLimit (32-32)
  • getDistributedRateLimitStatus (33-33)
  • initializeDistributedRateLimiting (34-34)
packages/lib/src/security/distributed-rate-limit.ts (3)
packages/lib/src/security/security-redis.ts (1)
  • tryGetRateLimitRedisClient (173-182)
packages/lib/src/logging/logger-config.ts (1)
  • loggers (8-18)
packages/lib/src/security/index.ts (2)
  • getDistributedRateLimitStatus (33-33)
  • RateLimitConfig (37-37)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Lint & TypeScript Check
  • GitHub Check: Unit Tests
🔇 Additional comments (9)
scripts/migrate-token-hashes.ts (2)

11-26: Well-structured migration script with good safety features.

The implementation demonstrates strong practices for a migration utility:

  • Robust argument parsing with batch size validation
  • Transaction-based batch processing ensures atomicity
  • Progress reporting provides visibility during long-running migrations
  • Dry-run mode enables safe testing before execution
  • Proper error handling with appropriate exit codes
  • Generic migrateTokenTable function supports multiple table types

Also applies to: 38-100, 102-139


32-34: Apply the safe String().slice() fix as stated in PR objectives.

The PR objectives mention that one of the fixes was to "replace unsafe identifier.substring with safe String().slice()", but this function still uses substring. This creates a potential robustness issue if the token parameter has unexpected prototype behavior.

🔒 Apply the documented security fix
 function getTokenPrefix(token: string): string {
-  return token.substring(0, 12);
+  return String(token).slice(0, 12);
 }

Likely an incorrect or invalid review comment.

packages/lib/src/security/__tests__/distributed-rate-limit.test.ts (3)

8-8: Excellent test coverage for Redis client specialization and fail-closed behavior.

All mock and import updates consistently use tryGetRateLimitRedisClient throughout the test suite, properly aligning with the rate-limit Redis client separation. The tests comprehensively cover:

  • Redis availability scenarios
  • Development fallback to in-memory rate limiting
  • Production fail-closed behavior (denying requests when Redis unavailable)
  • Error handling in both environments
  • In-memory fallback isolation

Also applies to: 36-36, 59-59, 151-151, 194-194, 231-231, 239-239, 247-247, 261-261, 277-277, 294-294, 304-304, 317-317, 328-328, 338-338, 348-348, 356-356, 406-406


154-188: Well-structured environment-specific test coverage.

The tests properly differentiate between development and production behavior:

  • Development mode falls back to in-memory rate limiting when Redis is unavailable
  • Production mode enforces fail-closed semantics, denying requests with explicit logging

This ensures the security posture is maintained in production while allowing graceful degradation in development.


207-225: Thorough error handling test for production fail-closed semantics.

This test correctly verifies that when Redis throws an error in production (not just unavailable), the system:

  1. Logs the warning about Redis failure
  2. Falls through to fail-closed logic
  3. Denies the request with appropriate logging

The dual-assertion approach (checking both the warning about Redis failure and the error about denying the request) ensures the complete error path is validated.

packages/lib/src/security/distributed-rate-limit.ts (4)

207-208: Correct migration to rate-limit Redis client.

All Redis client retrievals consistently use tryGetRateLimitRedisClient(), properly separating rate-limiting flows to the dedicated Redis client (using REDIS_RATE_LIMIT_URL). This aligns with the broader security infrastructure refactoring.

Also applies to: 281-281, 304-304, 406-406


296-336: Proper fail-closed semantics in status retrieval.

The updated getDistributedRateLimitStatus correctly implements fail-closed behavior in production when Redis is unavailable, preventing stale in-memory status from being returned in distributed deployments. Development mode retains the in-memory fallback for single-instance testing.

The comment on lines 296-297 clearly explains the rationale for this approach.


367-372: Verify OAUTH_VERIFY rate limit configuration.

The new OAUTH_VERIFY configuration has identical values to REFRESH:

  • maxAttempts: 10
  • windowMs: 5 minutes
  • blockDurationMs: 5 minutes
  • progressiveDelay: false

This seems reasonable given that OAuth token exchanges and refresh operations have similar security profiles. However, please confirm this is intentional and aligns with your security requirements for OAuth flows.


401-432: Robust initialization with production fail-fast.

The initialization logic properly:

  • Retrieves the rate-limit Redis client via tryGetRateLimitRedisClient()
  • Validates connection with a ping
  • Throws in production when Redis is unavailable or unreachable (fail-fast at startup)
  • Falls back to in-memory with appropriate warnings in development

This ensures production deployments fail early if Redis is misconfigured rather than silently degrading to potentially unsafe in-memory fallback.

- Fix setup-node@v6 -> @v4 in all workflow files (v6 doesn't exist)
- Check P0-T1 acceptance criteria boxes in plan.md
- Fix runbook path reference to match actual file

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @.github/workflows/build-desktop.yml:
- Line 28: Replace all occurrences of actions/setup-node@v4 with
actions/setup-node@v6 in the workflow; specifically update the three uses
entries currently set to "uses: actions/setup-node@v4" (the occurrences around
the build jobs) so they read "uses: actions/setup-node@v6" to pick up the v6.x
setup-node action.

In @.github/workflows/load-test.yml:
- Around line 89-101: The k6 Docker container cannot reach the web server at
http://localhost:3000 because Docker’s default bridge network isolates the
container from the GitHub Actions runner; update the Docker invocation in the
"Run k6 load test" step (the docker run command that launches grafana/k6 to run
/scripts/tests/load/auth-baseline.k6.js) to use host networking by adding the
--network host flag (so the container shares the runner’s network and can reach
BASE_URL), and ensure BASE_URL remains set to http://localhost:3000 in the env
block for the step.
- Around line 76-87: Replace the fixed "sleep 10" in the "Start web server"
step: after running "pnpm --filter web build" and backgrounding "pnpm --filter
web start &", implement a health-check loop that polls a known endpoint (e.g.
/api/health) with curl or wget until it returns a healthy status (200) or a
configurable timeout is reached; if the check never succeeds, fail the job with
a clear error message and non-zero exit code so the load test doesn't proceed on
a dead or slow server.
🧹 Nitpick comments (2)
plan.md (1)

44-63: Document remediation approach in lessons learned.

The "Lessons Learned" section captures three important remediation rounds (IP extraction standardization, Promise.allSettled for resilience, X-RateLimit header accuracy). These are valuable for future phases. However, consider adding a brief note on why these patterns were missed in initial implementation—would help avoid similar issues in Phase 2-5.

.github/workflows/test.yml (1)

48-48: Consider adding a comment explaining the actions/setup-node downgrade.

Version 4 is still actively maintained and not deprecated. However, since v6 introduced behavioral changes (stricter package manager caching, removal of always-auth input), it would be helpful to document in the workflow why v4 was chosen instead of v6 for future maintainers.

Also applies to: 94-94

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ce418ad and 8afd395.

📒 Files selected for processing (5)
  • .github/workflows/build-desktop.yml
  • .github/workflows/load-test.yml
  • .github/workflows/security.yml
  • .github/workflows/test.yml
  • plan.md
🧰 Additional context used
🧠 Learnings (15)
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to packages/lib/**/*.test.ts : Write unit tests for shared utilities in `packages/lib` with test files named `*.test.ts` alongside source or in `__tests__/` directory

Applied to files:

  • .github/workflows/security.yml
  • plan.md
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to apps/processor/**/*.test.ts : Write unit tests for the processor service with test files named `*.test.ts` alongside source or in `__tests__/` directory

Applied to files:

  • .github/workflows/security.yml
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to **/__tests__/**/*.test.ts : Unit tests should be placed next to source files or in `__tests__/` directories with `*.test.ts` extension. Add a `test` script to the package and run with `pnpm --filter <pkg> test`

Applied to files:

  • .github/workflows/security.yml
  • plan.md
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: When adding tests, prefer unit tests for `packages/lib` and `apps/processor` with test files in `__tests__/` directory or colocated with source

Applied to files:

  • .github/workflows/security.yml
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : For permission logic, use centralized functions from `pagespace/lib/permissions`: `getUserAccessLevel()`, `canUserEditPage()`

Applied to files:

  • .github/workflows/security.yml
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to app/**/*.{ts,tsx} : Import and use `getUserAccessLevel()` and `canUserEditPage()` from `pagespace/lib/permissions` for centralized permission logic

Applied to files:

  • .github/workflows/security.yml
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*.{ts,tsx} : Lint with Next/ESLint as configured in `apps/web/eslint.config.mjs`

Applied to files:

  • .github/workflows/security.yml
  • plan.md
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to packages/db/src/schema.ts : Update database schema in `packages/db/src/schema.ts` and generate migrations with `pnpm db:generate`

Applied to files:

  • plan.md
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to packages/db/src/schema/**/*.{ts,tsx} : Database schema changes must be made in `packages/db/src/schema/` and then `pnpm db:generate` must be run to create migrations

Applied to files:

  • plan.md
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to packages/db/drizzle/*.ts : Database migrations should be generated into `packages/db/drizzle/` directory using `pnpm db:generate` command

Applied to files:

  • plan.md
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: CRITICAL: Never manually create or edit SQL migration files in `packages/db/drizzle/` - always use `pnpm db:generate` to auto-generate migrations from schema changes

Applied to files:

  • plan.md
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/**/*.{ts,tsx} : Use Next.js 15 App Router and TypeScript for all routes and components

Applied to files:

  • plan.md
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to apps/web/{app,components,lib,src}/**/*.{ts,tsx,js,jsx} : Code must be formatted with Prettier and linted with Next/ESLint as configured in `apps/web/eslint.config.mjs`

Applied to files:

  • plan.md
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/components/**/*.{ts,tsx} : Use SWR for server state and caching with proper configuration including `revalidateOnFocus: false` for editing protection

Applied to files:

  • plan.md
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to **/*.{ts,tsx} : Format code with Prettier and lint with Next/ESLint (lint config at `apps/web/eslint.config.mjs`)

Applied to files:

  • plan.md
🪛 Checkov (3.2.334)
.github/workflows/load-test.yml

[medium] 70-71: Basic Auth Credentials

(CKV_SECRET_4)

🪛 LanguageTool
plan.md

[uncategorized] ~118-~118: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...curity/index.tsre-exports distributed rate limiting APIs and security Redis utilities for...

(EN_COMPOUND_ADJECTIVE_INTERNAL)


[uncategorized] ~319-~319: The official name of this software platform is spelled with a capital “H”.
Context: ...y-fixtures.tswith test data - Created.github/workflows/security.yml` GitHub workflow...

(GITHUB)


[uncategorized] ~409-~409: The official name of this software platform is spelled with a capital “H”.
Context: ...andling and cookie management - Created .github/workflows/load-test.yml GitHub workflo...

(GITHUB)

🪛 markdownlint-cli2 (0.18.1)
plan.md

410-410: Bare URL used

(MD034, no-bare-urls)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Unit Tests
  • GitHub Check: Static Security Analysis
🔇 Additional comments (11)
plan.md (7)

2051-2055: Hex format validation helps prevent timing attacks.

The addition of format validation before timingSafeEqual is a solid defensive measure. However, this appears in the Phase 3 planning section, while the learnings indicate broadcast signature verification was already addressed in PR #167. Clarify whether this implementation is part of the current PR or a planned improvement.


2118-2120: Clarify CSP implementation scope for PR #167.

This section is marked "PLANNED (Phase 4; not part of PR #167)," but the AI summary states "CSP nonce wiring groundwork introduced; security-headers.ts updated with nonce generation and propagation notes for future Phase 4." This creates ambiguity about whether nonce groundwork was partially implemented now or is deferred entirely.

Verify the actual scope: Did PR #167 add CSP groundwork (nonce generation skeleton for Phase 4), or is the entire CSP feature deferred to Phase 4?


124-244: P0-T1.1 remediation is well-documented with concrete fixes.

The sub-task clearly outlines four issues found post-P0-T1 (JTI logging, memory leak, cleanup cutoff, integration tests) and provides code diffs for each. The cleanup interval fix (storing cleanupIntervalId and providing shutdownRateLimiting()) is solid. Test count of 76 (67 unit + 9 integration) provides confidence in coverage.


331-381: Migration strategy is thorough but relies on assumptions about DB performance.

The P0-T3 section documents a phased token hashing migration with rollback safeguards:

  1. Add nullable columns
  2. Batch compute hashes (100K+ tokens efficiently assumed)
  3. Make columns non-null
  4. Monitor & drop plaintext column

This is sound, but the note "Migration script handles 100K+ tokens efficiently" and "batch processing (1000/batch)" should be verified with actual load testing. Include expected migration time in production environment as acceptance criteria.

Confirm that the batch migration script was tested with realistic token volumes and provide expected migration duration for production rollout.


583-610: P1-T3 schema updates are clear but partially implemented per learnings.

The schema documentation for tokenHash and tokenPrefix is good. However, the implementation notes state columns are "nullable initially" (lines 590, 602-603 in comments), suggesting a phased rollout. Ensure migration tooling and backward-compatibility code are documented for the transition period where old plaintext tokens coexist with new hashed tokens.

Verify that the dual-storage mode (plaintext + hash) during transition is fully documented and that code paths handle both correctly during the migration window.


2589-2614: Phase 1 test coverage table provides good transparency.

The coverage matrix (lines 2600-2612) shows target coverage of 100% across critical categories (JWT/Auth, Rate Limit, Race Conditions, SSRF, etc.) with an estimated 200 new tests. This aligns well with the reported 194 auth tests passing in PR #167 and motivates Phase 2-5 work.


2166-2184: Nonce wiring example is correct but needs clarification on Phase 4 scope.

The code example shows proper Next.js 15 App Router nonce integration: reading from headers, passing to Script components. The comment notes "inline hydration scripts" require nonce—this is accurate and important. However, link this example to the Phase 4 scope clarification requested in lines 2118-2120.

If CSP/nonce is Part of PR #167, update line 2118 to reflect "IN PROGRESS" instead of "PLANNED". If purely Phase 4, confirm this example is included in Phase 4 task definition only.

.github/workflows/load-test.yml (1)

76-87: Verify that Redis connection configuration is complete.

The PR context indicates that test infrastructure now provides REDIS_SESSION_URL and REDIS_RATE_LIMIT_URL explicitly derived from REDIS_URL. This workflow only sets REDIS_URL.

If the application automatically derives the session and rate-limit URLs from REDIS_URL, this is fine. Otherwise, the distributed rate limiting and session features tested by this load test may fail to connect to Redis.

Please confirm whether the web server requires explicit REDIS_SESSION_URL and REDIS_RATE_LIMIT_URL environment variables, or if it derives them automatically from REDIS_URL. If explicit values are required, add them to this step:

       - name: Start web server
         env:
           DATABASE_URL: postgresql://postgres:postgres@localhost:5432/pagespace_test
           REDIS_URL: redis://localhost:6379
+          REDIS_SESSION_URL: redis://localhost:6379
+          REDIS_RATE_LIMIT_URL: redis://localhost:6379
           JWT_SECRET: test-secret-key-minimum-32-characters-long-for-ci
           JWT_ISSUER: pagespace-test
           JWT_AUDIENCE: pagespace-test-users
           NODE_ENV: test
.github/workflows/test.yml (1)

75-77: This is an intentional backward-compatible fallback pattern, not redundancy.

REDIS_SESSION_URL and REDIS_RATE_LIMIT_URL are designed to fall back to REDIS_URL if not provided (as seen in security-redis.ts lines 67 and 117). This allows flexibility:

  • Use a single REDIS_URL for backward compatibility, with auto-derived databases in tests
  • Or provide explicit REDIS_SESSION_URL and REDIS_RATE_LIMIT_URL for better isolation in production

REDIS_URL itself doesn't specify a database suffix (unlike the explicit /0 and /1 on the session and rate limit URLs), making it a truly generic fallback used across multiple services (shared-redis.ts, rate-limit-cache.ts). The separation of rate limiting to database 1 is good practice.

.github/workflows/security.yml (2)

9-10: LGTM! Appropriate CI trigger expansion.

Adding the security test utilities and fixtures to the workflow triggers ensures that CI runs when test infrastructure changes, which is essential for maintaining test reliability.

Also applies to: 20-21


67-67: Consider documenting why actions/setup-node is standardized on v4 instead of the latest v6.

All workflows in the repository consistently use v4, and it's stable and compatible with Node 20. However, v6.1.0 (released December 3, 2025) is now available and provides the latest features and security updates. If this is an intentional standardization choice, add a comment in the workflow explaining the reasoning—whether it's for reproducibility, compatibility concerns, or another consideration.

Also applies to: 101-101, 147-147

Comment thread .github/workflows/build-desktop.yml Outdated
Comment thread .github/workflows/load-test.yml Outdated
v6.1.0 was released Dec 2024 - previous change was based on
incorrect information.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @.github/workflows/load-test.yml:
- Around line 76-83: The workflow step "Start web server" currently only sets
REDIS_URL but the app now expects separate Redis connections; add
REDIS_SESSION_URL and REDIS_RATE_LIMIT_URL environment variables (e.g.,
redis://localhost:6379/0 and redis://localhost:6379/1) alongside or in place of
REDIS_URL so the startup uses the separate session and rate-limit Redis
endpoints referenced by the app (REDIS_SESSION_URL and REDIS_RATE_LIMIT_URL).
- Around line 89-101: In the "Run k6 load test" step of load-test.yml where the
workflow runs docker run with env vars K6_VUS, K6_DURATION and BASE_URL, update
the docker invocation to allow the container to reach the runner's localhost by
adding the host network flag (--network host) to the docker run command; ensure
the modified docker run still includes the same -v and -e arguments and the
grafana/k6 run invocation for /scripts/tests/load/auth-baseline.k6.js.
- Around line 84-87: Replace the brittle "sleep 10" after launching the
background server (the pnpm --filter web start & step) with a retry readiness
check that polls a health endpoint (e.g., GET /health or /_health) or a TCP port
check for localhost:3000, retrying with a short delay and failing after a
timeout; update the workflow segment that currently runs "pnpm --filter web
build" and "pnpm --filter web start &" to include this loop so the job only
proceeds when the server responds successfully or exits with an error if it
never becomes ready.
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8afd395 and 92399ce.

📒 Files selected for processing (3)
  • .github/workflows/load-test.yml
  • .github/workflows/security.yml
  • .github/workflows/test.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/test.yml
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to **/*.{ts,tsx} : Keep commits and diffs minimal and focused on specific changes

Applied to files:

  • .github/workflows/security.yml
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to packages/lib/**/*.test.ts : Write unit tests for shared utilities in `packages/lib` with test files named `*.test.ts` alongside source or in `__tests__/` directory

Applied to files:

  • .github/workflows/security.yml
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to apps/processor/**/*.test.ts : Write unit tests for the processor service with test files named `*.test.ts` alongside source or in `__tests__/` directory

Applied to files:

  • .github/workflows/security.yml
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: Applies to **/__tests__/**/*.test.ts : Unit tests should be placed next to source files or in `__tests__/` directories with `*.test.ts` extension. Add a `test` script to the package and run with `pnpm --filter <pkg> test`

Applied to files:

  • .github/workflows/security.yml
📚 Learning: 2025-12-14T14:54:38.009Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:38.009Z
Learning: When adding tests, prefer unit tests for `packages/lib` and `apps/processor` with test files in `__tests__/` directory or colocated with source

Applied to files:

  • .github/workflows/security.yml
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : For permission logic, use centralized functions from `pagespace/lib/permissions`: `getUserAccessLevel()`, `canUserEditPage()`

Applied to files:

  • .github/workflows/security.yml
📚 Learning: 2025-12-14T14:54:45.713Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T14:54:45.713Z
Learning: Applies to app/**/*.{ts,tsx} : Import and use `getUserAccessLevel()` and `canUserEditPage()` from `pagespace/lib/permissions` for centralized permission logic

Applied to files:

  • .github/workflows/security.yml
📚 Learning: 2025-12-22T20:04:40.910Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-22T20:04:40.910Z
Learning: Applies to **/*.{ts,tsx} : Lint with Next/ESLint as configured in `apps/web/eslint.config.mjs`

Applied to files:

  • .github/workflows/security.yml
🪛 Checkov (3.2.334)
.github/workflows/load-test.yml

[medium] 70-71: Basic Auth Credentials

(CKV_SECRET_4)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Unit Tests
🔇 Additional comments (3)
.github/workflows/security.yml (1)

9-10: LGTM! Test utility paths correctly added to CI triggers.

The additions ensure the security workflow runs when test utilities or fixtures change, maintaining test reliability. The symmetric application to both push and pull_request triggers is consistent and appropriate.

Also applies to: 20-21

.github/workflows/load-test.yml (2)

68-74: Static analysis false positive: test credentials are acceptable.

The static analysis tool flagged the hardcoded credentials in the DATABASE_URL (Line 70), but this is a false positive. Using postgres:postgres for an ephemeral PostgreSQL service in CI is standard practice and poses no security risk since the database only exists during the workflow run and is not exposed externally.


95-101: All concerns raised have been verified and addressed:

  1. Script exists — tests/load/auth-baseline.k6.js is present in the repository.
  2. Output configured — The k6 script contains a handleSummary function that outputs results to tests/load/results/baseline-latest.json.
  3. Results captured — The workflow's "Upload load test results" step explicitly uploads the tests/load/results/ directory as an artifact with 30-day retention.

The workflow is correctly configured and no changes are needed.

Likely an incorrect or invalid review comment.

Comment on lines +76 to +83
- name: Start web server
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/pagespace_test
REDIS_URL: redis://localhost:6379
JWT_SECRET: test-secret-key-minimum-32-characters-long-for-ci
JWT_ISSUER: pagespace-test
JWT_AUDIENCE: pagespace-test-users
NODE_ENV: test

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.

⚠️ Potential issue | 🟠 Major

Add separate Redis URLs for sessions and rate limiting.

The PR introduces distributed rate limiting with separate Redis instances (REDIS_SESSION_URL and REDIS_RATE_LIMIT_URL), but this workflow only configures a single REDIS_URL. This inconsistency may cause the web server to fail during startup or runtime if the application now expects separate Redis connections.

🔧 Proposed fix
       - name: Start web server
         env:
           DATABASE_URL: postgresql://postgres:postgres@localhost:5432/pagespace_test
-          REDIS_URL: redis://localhost:6379
+          REDIS_SESSION_URL: redis://localhost:6379/0
+          REDIS_RATE_LIMIT_URL: redis://localhost:6379/1
           JWT_SECRET: test-secret-key-minimum-32-characters-long-for-ci
           JWT_ISSUER: pagespace-test
           JWT_AUDIENCE: pagespace-test-users
           NODE_ENV: test

Note: Using different Redis databases (/0 and /1) on the same instance is acceptable for testing, though production should use separate instances as indicated in the PR context.

📝 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
- name: Start web server
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/pagespace_test
REDIS_URL: redis://localhost:6379
JWT_SECRET: test-secret-key-minimum-32-characters-long-for-ci
JWT_ISSUER: pagespace-test
JWT_AUDIENCE: pagespace-test-users
NODE_ENV: test
- name: Start web server
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/pagespace_test
REDIS_SESSION_URL: redis://localhost:6379/0
REDIS_RATE_LIMIT_URL: redis://localhost:6379/1
JWT_SECRET: test-secret-key-minimum-32-characters-long-for-ci
JWT_ISSUER: pagespace-test
JWT_AUDIENCE: pagespace-test-users
NODE_ENV: test
🤖 Prompt for AI Agents
In @.github/workflows/load-test.yml around lines 76 - 83, The workflow step
"Start web server" currently only sets REDIS_URL but the app now expects
separate Redis connections; add REDIS_SESSION_URL and REDIS_RATE_LIMIT_URL
environment variables (e.g., redis://localhost:6379/0 and
redis://localhost:6379/1) alongside or in place of REDIS_URL so the startup uses
the separate session and rate-limit Redis endpoints referenced by the app
(REDIS_SESSION_URL and REDIS_RATE_LIMIT_URL).

Comment thread .github/workflows/load-test.yml Outdated
Comment thread .github/workflows/load-test.yml
- Replace fixed sleep with health check loop polling /api/health
- Add --network host to k6 docker container for localhost access
- Fail fast with clear error if server doesn't start within 60s

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant