Skip to content

feat(security): Phase 0 - Security infrastructure foundation - #160

Merged
2witstudios merged 10 commits into
masterfrom
feat/security-infrastructure
Jan 6, 2026
Merged

2witstudios merged 10 commits into
masterfrom
feat/security-infrastructure

Conversation

@2witstudios

@2witstudios 2witstudios commented Jan 5, 2026 •

Copy link
Copy Markdown
Owner

Summary

Phase 0 of the PageSpace Cloud Security Hardening project establishes the foundational infrastructure for enterprise-grade security:

  • Redis-based JTI tracking for instant token revocation with fail-closed pattern
  • Distributed rate limiting using sliding window algorithm (Redis sorted sets)
  • Security test utilities for comprehensive vulnerability testing
  • Token hashing migration strategy with detailed runbook
  • Auth performance baseline scripts (k6) for regression detection

Changes

P0-T1: Redis Security Infrastructure

  • packages/lib/src/security/security-redis.ts - JTI ops, rate limiting, sessions
  • packages/lib/src/security/distributed-rate-limit.ts - Rate limiter with fallback
  • Production enforcement (Redis required in prod, fail-closed pattern)

P0-T1.1: Foundation Hardening

  • JTI logging uses [REDACTED] for security
  • shutdownRateLimiting() prevents memory leaks
  • Integration tests with real Redis (skip gracefully when unavailable)

P0-T2: Security Test Infrastructure

  • Malicious input generators (SQL injection, XSS, path traversal, SSRF, etc.)
  • Race condition testing helpers
  • JWT manipulation utilities for testing
  • GitHub Actions workflow for security CI

P0-T3: Token Hashing Migration Strategy

  • 6-phase migration runbook with rollback procedures
  • Verification script for migration validation
  • Risk assessment and mitigation strategies

P0-T4: Auth Performance Baseline

  • k6 load test for auth endpoints
  • Custom metrics (loginLatency, refreshLatency, tokenValidationLatency)
  • Performance thresholds (p95 < 500ms)

Test plan

  • All 102 tests passing (76 security + 26 utility tests)
  • Integration tests skip gracefully when Redis unavailable
  • Code review completed (96/100 score)
  • Manual verification of Redis connection in staging
  • Run k6 baseline before Phase 1 implementation

Commits (5)

  1. feat(security): add Redis-based JTI tracking and rate limiting
  2. fix(security): harden foundation - redaction, shutdown, integration tests
  3. test(security): add security test utilities and fixtures
  4. docs(security): add token hashing migration strategy
  5. test(security): add auth performance baseline scripts

Related

This PR implements Phase 0 of the security hardening master plan. Subsequent phases will build on this foundation:

  • Phase 1: Critical security fixes (JTI enforcement, token hashing)
  • Phase 2: Zero-trust token architecture
  • Phase 3: Distributed security services
  • Phase 4: Defense in depth
  • Phase 5: Monitoring & incident response

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Distributed rate limiting and Redis-backed JTI/session support with in-memory fallback; added cron secret for scheduled tasks.
  • Documentation

    • Token-hashing migration runbook, master security hardening plan, and expanded Redis production guidance including availability and fallback behavior.
  • Infrastructure

    • New security CI workflow, Redis test service for integration testing, and Dependabot config for GitHub Actions updates.
  • Tests

    • Extensive unit, integration, and load tests plus security test utilities and fixtures; verification script for token migration.

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

@coderabbitai

coderabbitai Bot commented Jan 5, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a Redis-backed security subsystem (JTI tracking, sessions, sliding-window distributed rate limiter with in-memory fallback), extensive tests/fixtures and test Redis service, CI security workflow, token-hash migration runbook and verifier, a k6 auth load test, and .env / cron configuration updates.

Changes

Cohort / File(s) Change summary & attention areas
Config / Compose
\.env.example, docker-compose.test.yml
Expanded Redis production guidance and placeholders (REDIS_URL, REDIS_PASSWORD), added CRON_SECRET, and added redis-test service (redis:7.4-alpine, port 6380, healthcheck, memory cap). Review HA guidance, fallback behavior when Redis is unavailable, and local test port mapping.
CI / Security Workflow
.github/workflows/security.yml
New Security Tests workflow with jobs for security-tests (Postgres+Redis services + Vitest), dependency-audit (pnpm audit), secret-scanning (TruffleHog), and static-analysis (tsc + ESLint). Check service startup timing, secrets used in workflow, and resource limits.
Security core & API surface
packages/lib/src/security/security-redis.ts, packages/lib/src/security/distributed-rate-limit.ts, packages/lib/src/security/index.ts
New Redis utilities (JTI record/revoke, rate limiting, sessions, health checks) and a Redis-backed sliding-window rate limiter with an in-memory fallback, plus initialize/shutdown APIs and exported types. Inspect TTL handling, pipeline atomicity, fail‑closed semantics in production, concurrency, cleanup, and shutdown behavior.
Tests & Test Utilities
packages/lib/src/__tests__/*, packages/lib/src/__tests__/test-fixtures/security-fixtures.ts, packages/lib/src/__tests__/security-test-utils.ts, packages/lib/src/__tests__/security-test-utils.test.ts
New comprehensive test fixtures and utilities (malicious inputs, JWT helpers, timing/race helpers, token hashing helpers) and unit tests. Verify mock fidelity, determinism, and coverage of edge cases.
Integration & Unit Tests (Redis)
packages/lib/src/security/__tests__/*
New unit and integration tests for security-redis and distributed-rate-limit, including a real-Redis integration that skips when Redis unavailable. Ensure gating logic, cleanup of sec:test:* keys, and correctness of mocked pipeline behavior.
Migration & Runbooks
docs/security/token-hashing-migration.md, scripts/verify-token-migration.ts, plan.md
Adds token-hash migration runbook, verification script (exit codes 0/1/2), and a master security plan. Review SQL statements, verification heuristics, rollback criteria, and operational checklist.
Load testing
tests/load/auth-baseline.k6.js
New k6 auth baseline script with staged load, thresholds, and login/refresh/profile flows. Validate environment variables, thresholds, and token lifecycle assumptions.
Repo tooling
.github/dependabot.yml
Adds Dependabot config for weekly GitHub Actions updates.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant App as Application
  participant Redis
  participant Mem as "In-Memory Store"

  rect rgb(240,250,235)
    Note over App,Redis: Distributed rate-limit primary flow
    Client->>App: Request with identifier
    App->>Redis: Pipeline(ZREMRANGEBYSCORE, ZADD, ZCARD, PEXPIRE)
    alt Redis available & pipeline succeeds
      Redis-->>App: Pipeline results (count, expiry)
      App-->>Client: Allow / Reject (may apply progressive delay)
    else Redis error or unavailable
      App->>Mem: In-memory sliding-window check
      Mem-->>App: Count & expiry
      App-->>Client: Allow / Reject (in-memory fallback)
    end
  end

  par async
    App->>App: Record metrics / logs
    App->>Redis: Async cleanup/expiry (if available)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

I'm a rabbit in a server den 🐇,
I hopped through keys and piped a plan,
Redis hums or memory stays,
JTIs logged and limits blaze,
Cron secret tucked — secure again.

Pre-merge checks

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and accurately summarizes the main objective of the changeset: establishing Phase 0 of the security infrastructure foundation as part of a broader security hardening initiative.
Docstring Coverage ✅ Passed Docstring coverage is 86.36% which is sufficient. The required threshold is 80.00%.

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2cf0898 and 987ae86.

📒 Files selected for processing (2)
  • .github/dependabot.yml
  • .github/workflows/security.yml
🧰 Additional context used
🪛 Checkov (3.2.334)
.github/workflows/security.yml

[medium] 76-77: 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: Static Security Analysis
  • GitHub Check: Unit Tests
🔇 Additional comments (4)
.github/dependabot.yml (1)

1-8: LGTM! Dependabot configuration aligns with security best practices.

This configuration enables weekly automated updates for GitHub Actions, which complements the pinned TruffleHog action in the security workflow and ensures timely security patches without relying on mutable branch references.

.github/workflows/security.yml (3)

1-85: Security test infrastructure looks solid.

The workflow triggers appropriately on security/auth path changes and provides comprehensive test coverage with Postgres and Redis services. The health checks and test environment configuration are well-structured.

Note: The static analysis warning about credentials in lines 76-77 is a false positive—these are ephemeral test credentials for local CI services, not production secrets.


87-114: LGTM! Dependency audit strategy is pragmatic.

The tiered approach (report high-severity issues, fail on critical) provides good visibility without excessive build failures.


157-161: The current configuration is correct. While the security tests properly filter for @pagespace/lib, the lint step correctly targets web because @pagespace/lib does not have a lint script. The workflow triggers on changes to both packages/lib/src/security/**, packages/lib/src/auth/**, and apps/web/src/lib/auth/**, making it appropriate to lint the web package which is configured with next lint.

If broader static analysis coverage for lib security code is desired, add a lint script to packages/lib/package.json or configure a shared root-level linting approach rather than changing the current filter.

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 808ca16ee1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +258 to +262
loggers.api.error('Redis unavailable in production - rate limiting may be inconsistent');
// Still use in-memory as last resort to prevent complete auth bypass
}

return inMemoryCheckRateLimit(identifier, config);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Fail-closed requirement ignored when Redis is down

When NODE_ENV is production and Redis is unavailable, the rate limiter only logs an error and then falls through to inMemoryCheckRateLimit. That means distributed limits silently revert to per-instance memory limits instead of denying requests, so an outage or misconfiguration of Redis in production lets clients bypass global rate limiting by hitting different instances, contrary to the stated fail-closed requirement. This only occurs when Redis is unreachable in production.

Useful? React with 👍 / 👎.

@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 @docs/security/token-hashing-migration.md:
- Around line 74-98: The migration script in migrateRefreshTokens is missing the
eq import from drizzle-orm and performs one-row updates which will be slow for
large tables; import eq, then change the loop to process tokens in batches
(define BATCH_SIZE, slice tokens into batches) and perform each batch inside a
db.transaction using tx.update on refreshTokens with .set({ tokenHash:
hashToken(token.token), tokenPrefix: token.token.substring(0,12) }) and
.where(eq(refreshTokens.id, token.id)) to reduce round-trips and improve
throughput while preserving the existing hashToken logic.
- Around line 243-249: The Drizzle ORM condition uses an invalid `.not()` on the
isNull expression; update the queries that count rows with/without token hashes
to use the proper isNotNull condition. Replace the
`.where(isNull(refreshTokens.tokenHash).not())` usage with
`.where(isNotNull(refreshTokens.tokenHash))` in the refresh token counts (the
queries that define refreshWithHash and refreshWithoutHash) so the conditions
use isNotNull and isNull respectively while keeping the same
db.select/from(refreshTokens) structure.

In @packages/lib/src/__tests__/security-test-utils.ts:
- Around line 200-215: tamperJWTClaim currently parses the JWT payload without
catching JSON errors; wrap the payload JSON.parse in a try/catch (matching the
pattern used in extractJWTClaims/extractJWTHeader) and when parsing fails throw
a descriptive Error (e.g., "Invalid JWT payload: failed to parse JSON") so
callers get a consistent, clear error; adjust the code inside tamperJWTClaim
around the JSON.parse/Buffer decode and ensure the thrown error includes context
and the original exception message.
♻️ Duplicate comments (1)
packages/lib/src/security/distributed-rate-limit.ts (1)

256-262: Production fail-closed behavior not enforced.

This concern was previously raised: when Redis is unavailable in production, the code logs an error but still falls through to in-memory rate limiting. This allows distributed limits to be bypassed by hitting different instances.

The current behavior is documented as intentional ("last resort to prevent complete auth bypass"), which is a reasonable tradeoff. However, the security posture should be clearly documented in operational runbooks.

🧹 Nitpick comments (11)
packages/lib/src/security/__tests__/distributed-rate-limit.test.ts (1)

422-434: Time-based test may be flaky under CI load.

The 50ms window with 100ms wait has a 2x margin, which should generally be sufficient. However, under heavy CI load, timing tests can occasionally fail.

Consider increasing the margin if flakiness is observed:

🔎 Suggested adjustment if flakiness occurs
     it('resets window after expiry', async () => {
-      const config: RateLimitConfig = { maxAttempts: 1, windowMs: 50 };
+      const config: RateLimitConfig = { maxAttempts: 1, windowMs: 100 };

       await checkDistributedRateLimit('expiry-test', config);
       const blocked = await checkDistributedRateLimit('expiry-test', config);
       expect(blocked.allowed).toBe(false);

       // Wait for window to expire
-      await new Promise(resolve => setTimeout(resolve, 100));
+      await new Promise(resolve => setTimeout(resolve, 200));

       const afterExpiry = await checkDistributedRateLimit('expiry-test', config);
       expect(afterExpiry.allowed).toBe(true);
     });
docs/security/token-hashing-migration.md (2)

157-177: Add language specifier to the code block.

Per static analysis (MD040), the expected output code block should have a language specifier for proper formatting.

🔎 Suggested fix
-```
+```text
 Token Migration Verification
 ============================
 Refresh Tokens:
   Total: 1,234

332-332: Minor: Use proper heading or plain text instead of bold emphasis.

Per markdown lint (MD036), bold emphasis shouldn't be used as a heading substitute.

🔎 Suggested fix
-**Total: ~1 week**
+### Total Duration: ~1 week

Or simply:

-**Total: ~1 week**
+Total: approximately 1 week
.github/workflows/security.yml (1)

126-132: Pin TruffleHog to a specific stable version instead of @main.

Using @main could introduce unexpected breaking changes. Pin to the latest stable release (v3.92.4) for consistency.

🔎 Suggested fix
       - name: TruffleHog Secret Scan
-        uses: trufflesecurity/trufflehog@main
+        uses: trufflesecurity/trufflehog@v3.92.4
         with:
           path: ./
           base: ${{ github.event.repository.default_branch }}
           head: HEAD
           extra_args: --only-verified
scripts/verify-token-migration.ts (1)

34-42: SQL injection risk with sql.raw() and string interpolation.

While this script is intended for internal use, passing tableName and hashColumn directly into sql.raw() creates a SQL injection vector if inputs are ever sourced externally. Since this is a developer-run script with hardcoded callers, the practical risk is low.

Consider using Drizzle's parameterized approach or at minimum adding input validation:

🔎 Proposed safeguard
 async function countTokens(
   tableName: string,
   hashColumn: string
 ): Promise<VerificationResult> {
+  // Validate table/column names to prevent injection
+  const validTables = ['refresh_tokens', 'mcp_tokens'];
+  const validColumns = ['token_hash'];
+  if (!validTables.includes(tableName) || !validColumns.includes(hashColumn)) {
+    throw new Error(`Invalid table or column name: ${tableName}.${hashColumn}`);
+  }
+
   try {
     // Count total tokens
     const totalResult = await db.execute(
packages/lib/src/security/__tests__/security-redis.integration.test.ts (2)

59-127: Consider testing the actual module APIs alongside low-level Redis operations.

These tests directly exercise Redis commands (setex, zadd, zcard, etc.) rather than the security-redis.ts module APIs (recordJTI, isJTIRevoked, checkRateLimit). While validating Redis behavior is valuable, adding tests that call the actual module functions would provide better integration coverage and catch any abstraction bugs.

Example addition:

// In addition to low-level tests, verify the module API
it('recordJTI and isJTIRevoked work end-to-end', async () => {
  if (!redis) return;
  // Import and call the actual security-redis functions
  // to verify the full integration path
});

60-64: Consider using Vitest's it.skipIf for cleaner conditional skipping.

The manual if (!redis) { return; } pattern works but is verbose. Vitest provides it.skipIf() for this use case:

it.skipIf(!isRedisAvailable)('stores and retrieves JTI data with TTL', async () => {
  // Test body without manual check
});

This is a minor stylistic preference—the current approach is functional.

tests/load/auth-baseline.k6.js (1)

59-61: Module-level token state may not behave as expected across k6 VUs.

In k6, each VU (virtual user) runs in isolation, so module-level variables like accessToken and refreshToken are not shared between VUs. However, within a single VU's iteration, this works correctly. If the intent is for each VU to maintain its own session, this is fine.

Consider adding a comment clarifying the intended behavior:

// Per-VU token state (not shared across VUs, each VU authenticates independently)
let accessToken = null;
let refreshToken = null;
plan.md (1)

33-40: Add language specifiers to fenced code blocks for syntax highlighting.

Static analysis flagged several code blocks without language specifiers. Adding them improves readability in rendered markdown:

-```
+```text
 Phase 0: Infrastructure & Preparation
 Phase 1: Critical Security Foundation

Similar fixes apply to lines 2456, 2501, and 2532.

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

170-217: Interface inconsistency between rate limit modules.

The RateLimitResult interface here (lines 170-175) includes totalCount and resetAt: Date, while distributed-rate-limit.ts defines its own RateLimitResult with retryAfter?: number and attemptsRemaining?: number. This creates potential confusion.

Consider consolidating to a single shared interface or renaming one to clarify the distinction (e.g., RedisRateLimitResult vs DistributedRateLimitResult).

packages/lib/src/__tests__/security-test-utils.ts (1)

111-133: Consider TypeScript definite assignment assertion.

The barrier pattern correctly maximizes concurrency for race condition testing. However, TypeScript's strict mode may flag releaseBarrier as potentially undefined at line 130.

🔎 Proposed fix for stricter TypeScript compliance
 export async function racingRequests<T>(
   fn: () => Promise<T>,
   count: number = 10
 ): Promise<T[]> {
   // Create a barrier that all promises wait on
-  let releaseBarrier: () => void;
+  let releaseBarrier!: () => void;
   const barrier = new Promise<void>((resolve) => {
     releaseBarrier = resolve;
   });
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9e69782 and 0203821.

📒 Files selected for processing (16)
  • .env.example
  • .github/workflows/security.yml
  • docker-compose.test.yml
  • docs/security/token-hashing-migration.md
  • packages/lib/src/__tests__/security-test-utils.test.ts
  • packages/lib/src/__tests__/security-test-utils.ts
  • packages/lib/src/__tests__/test-fixtures/security-fixtures.ts
  • packages/lib/src/security/__tests__/distributed-rate-limit.test.ts
  • packages/lib/src/security/__tests__/security-redis.integration.test.ts
  • packages/lib/src/security/__tests__/security-redis.test.ts
  • packages/lib/src/security/distributed-rate-limit.ts
  • packages/lib/src/security/index.ts
  • packages/lib/src/security/security-redis.ts
  • plan.md
  • scripts/verify-token-migration.ts
  • tests/load/auth-baseline.k6.js
🧰 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:

  • packages/lib/src/__tests__/security-test-utils.test.ts
  • packages/lib/src/security/__tests__/security-redis.test.ts
  • packages/lib/src/security/__tests__/security-redis.integration.test.ts
  • packages/lib/src/security/__tests__/distributed-rate-limit.test.ts
  • packages/lib/src/security/distributed-rate-limit.ts
  • scripts/verify-token-migration.ts
  • packages/lib/src/__tests__/test-fixtures/security-fixtures.ts
  • packages/lib/src/security/security-redis.ts
  • packages/lib/src/__tests__/security-test-utils.ts
  • packages/lib/src/security/index.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/__tests__/security-test-utils.test.ts
  • packages/lib/src/security/__tests__/security-redis.test.ts
  • packages/lib/src/security/__tests__/security-redis.integration.test.ts
  • packages/lib/src/security/__tests__/distributed-rate-limit.test.ts
  • packages/lib/src/security/distributed-rate-limit.ts
  • scripts/verify-token-migration.ts
  • packages/lib/src/__tests__/test-fixtures/security-fixtures.ts
  • packages/lib/src/security/security-redis.ts
  • packages/lib/src/__tests__/security-test-utils.ts
  • packages/lib/src/security/index.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • packages/lib/src/__tests__/security-test-utils.test.ts
  • packages/lib/src/security/__tests__/security-redis.test.ts
  • packages/lib/src/security/__tests__/security-redis.integration.test.ts
  • tests/load/auth-baseline.k6.js
  • packages/lib/src/security/__tests__/distributed-rate-limit.test.ts
  • packages/lib/src/security/distributed-rate-limit.ts
  • scripts/verify-token-migration.ts
  • packages/lib/src/__tests__/test-fixtures/security-fixtures.ts
  • packages/lib/src/security/security-redis.ts
  • packages/lib/src/__tests__/security-test-utils.ts
  • packages/lib/src/security/index.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 (6)
📚 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/__tests__/security-test-utils.test.ts
  • packages/lib/src/security/__tests__/security-redis.test.ts
  • packages/lib/src/__tests__/test-fixtures/security-fixtures.ts
  • packages/lib/src/__tests__/security-test-utils.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/verify-token-migration.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 : Update database schema in `packages/db/src/schema.ts` and generate migrations with `pnpm db:generate`

Applied to files:

  • scripts/verify-token-migration.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 **/__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/src/__tests__/test-fixtures/security-fixtures.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:

  • packages/lib/src/security/index.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:

  • packages/lib/src/security/index.ts
🧬 Code graph analysis (4)
packages/lib/src/__tests__/security-test-utils.test.ts (1)
packages/lib/src/__tests__/security-test-utils.ts (12)
  • getMaliciousInputs (18-88)
  • getAllMaliciousInputs (93-96)
  • racingRequests (111-133)
  • sequentialRace (141-156)
  • extractJWTClaims (166-178)
  • extractJWTHeader (183-195)
  • tamperJWTClaim (200-215)
  • measureExecutionTime (225-232)
  • generateTestToken (269-271)
  • hashToken (276-278)
  • createMockHeaders (287-295)
  • createMockRequest (300-313)
packages/lib/src/security/__tests__/security-redis.test.ts (4)
packages/lib/src/services/shared-redis.ts (2)
  • getSharedRedisClient (25-39)
  • isSharedRedisAvailable (44-46)
packages/lib/src/security/index.ts (11)
  • getSecurityRedisClient (14-14)
  • isSecurityRedisAvailable (15-15)
  • tryGetSecurityRedisClient (16-16)
  • recordJTI (19-19)
  • isJTIRevoked (20-20)
  • revokeJTI (21-21)
  • revokeAllUserJTIs (22-22)
  • setSessionData (24-24)
  • getSessionData (25-25)
  • deleteSessionData (26-26)
  • checkSecurityRedisHealth (17-17)
packages/lib/src/security/security-redis.ts (14)
  • getSecurityRedisClient (25-36)
  • isSecurityRedisAvailable (42-44)
  • tryGetSecurityRedisClient (50-56)
  • recordJTI (66-80)
  • isJTIRevoked (88-106)
  • revokeJTI (112-147)
  • revokeAllUserJTIs (154-164)
  • checkRateLimit (181-217)
  • getRateLimitStatus (222-241)
  • resetRateLimit (246-250)
  • setSessionData (259-267)
  • getSessionData (272-286)
  • deleteSessionData (291-295)
  • checkSecurityRedisHealth (305-327)
packages/lib/src/logging/logger-config.ts (1)
  • loggers (8-18)
scripts/verify-token-migration.ts (1)
packages/db/src/index.ts (2)
  • db (20-20)
  • sql (8-8)
packages/lib/src/security/security-redis.ts (7)
packages/lib/src/security/index.ts (8)
  • getSecurityRedisClient (14-14)
  • isSecurityRedisAvailable (15-15)
  • tryGetSecurityRedisClient (16-16)
  • recordJTI (19-19)
  • isJTIRevoked (20-20)
  • revokeJTI (21-21)
  • revokeAllUserJTIs (22-22)
  • RateLimitResult (38-38)
packages/lib/src/services/shared-redis.ts (2)
  • getSharedRedisClient (25-39)
  • isSharedRedisAvailable (44-46)
packages/lib/src/logging/logger-config.ts (1)
  • loggers (8-18)
packages/lib/src/security/distributed-rate-limit.ts (1)
  • RateLimitResult (36-40)
scripts/check-fetch-auth.js (1)
  • results (56-62)
packages/db/src/index.ts (1)
  • count (8-8)
apps/processor/src/logger.ts (1)
  • error (57-63)
🪛 Checkov (3.2.334)
.github/workflows/security.yml

[medium] 76-77: Basic Auth Credentials

(CKV_SECRET_4)

🪛 LanguageTool
plan.md

[uncategorized] ~748-~748: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...``` Acceptance Criteria: - [ ] All rate limiting uses Redis - [ ] Production startup fai...

(EN_COMPOUND_ADJECTIVE_INTERNAL)


[uncategorized] ~2574-~2574: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...tokens hashed at rest - [ ] Distributed rate limiting active --- ## Rollout Strategy 1. **...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

🪛 markdownlint-cli2 (0.18.1)
plan.md

33-33: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


112-112: Strong style
Expected: asterisk; Actual: underscore

(MD050, strong-style)


112-112: Strong style
Expected: asterisk; Actual: underscore

(MD050, strong-style)


119-119: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


128-128: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


170-170: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


2448-2448: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


2456-2456: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


2501-2501: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


2532-2532: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

docs/security/token-hashing-migration.md

158-158: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


332-332: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

⏰ 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 (35)
.env.example (1)

98-109: Clear and helpful documentation for Redis security requirements.

The comments accurately document the fail-closed behavior for JTI checks and the security implications of Redis unavailability. This aligns well with the PR's security-first approach.

packages/lib/src/security/__tests__/distributed-rate-limit.test.ts (3)

1-51: LGTM!

The mock setup follows the correct vitest pattern with vi.mock() calls before module imports. The environment variable backup/restore pattern ensures test isolation.


52-200: Thorough test coverage for rate limit checking.

The tests properly cover Redis available/unavailable scenarios, progressive delay behavior, and graceful fallback. The production logging verification (lines 174-183) ensures proper observability.


331-367: Good validation of rate limit configurations.

Testing the DISTRIBUTED_RATE_LIMITS constants ensures configuration values aren't accidentally changed. This serves as a form of documentation and regression prevention.

docker-compose.test.yml (1)

22-34: Well-configured Redis test service.

The configuration is appropriate for testing: isolated port (6380), memory-limited, ephemeral tmpfs storage, and proper healthcheck. The allkeys-lru policy ensures graceful behavior when memory is exhausted during tests.

packages/lib/src/__tests__/security-test-utils.test.ts (4)

1-70: LGTM!

The malicious input tests properly verify that all attack vector categories are present and contain expected patterns (SQL injection quotes, XSS script tags, path traversal sequences). Based on learnings, test file placement in __tests__/ directory is correct.


72-110: Good demonstration of concurrency helper behavior.

The racingRequests test cleverly uses setImmediate to force race conditions, demonstrating the utility's purpose. The comment on line 85 appropriately documents expected non-deterministic behavior.


112-166: Well-designed JWT manipulation tests.

The tests properly verify claim extraction and tampering behavior. Importantly, line 159-164 confirms that tamperJWTClaim preserves the original signature, which is essential for testing that applications correctly reject tampered tokens.


168-259: LGTM!

The utility tests cover essential behaviors: timing measurement precision, token uniqueness, hash consistency, and request mocking flexibility. The hex format validation (line 218) correctly expects 64 characters for SHA-256 output.

.github/workflows/security.yml (3)

3-23: Well-structured trigger configuration with appropriate path filters.

The path filters ensure the security workflow only runs when security-relevant code changes, reducing unnecessary CI load. Including the workflow file itself in paths is a good practice for testing workflow changes.


109-114: Good tiered approach for dependency auditing.

The two-stage audit (warn on high, fail on critical) balances security with practicality, allowing teams to track high-severity issues without blocking all PRs.


74-85: Test credentials in CI environment are acceptable.

The static analysis hint (CKV_SECRET_4) flagged test:test credentials, but these are intentionally non-sensitive CI-only values. This is a false positive—test database credentials in workflow files are standard practice.

docs/security/token-hashing-migration.md (1)

309-319: Excellent inclusion of timing attack defense.

Documenting the timingSafeEqual usage for hash comparison is crucial. This prevents attackers from inferring hash values through response time analysis.

packages/lib/src/security/__tests__/security-redis.test.ts (6)

40-136: Excellent mock Redis implementation for testing.

The createMockRedis function provides a sophisticated simulation of Redis behavior, including string operations with TTL, sorted set operations via pipeline, and proper sliding window support. The filter logic in zremrangebyscore (line 103) correctly removes elements within the score range.


153-199: Good coverage of Redis availability scenarios.

Tests properly verify the fail-closed behavior in production (line 163-165) and the graceful null return from tryGetSecurityRedisClient (line 194-198).


268-277: Critical security verification: JTI redaction in logs.

This test ensures that sensitive JTI values are logged as [REDACTED], preventing credential leakage to log aggregation systems. This aligns with the PR objective of hardened logging.


237-253: Proper fail-closed behavior verification.

Tests correctly verify that non-existent JTIs (line 237-240) and corrupted data (line 249-253) both return true (revoked), implementing the fail-closed security pattern that rejects tokens when in doubt.


314-414: Thorough rate limiting test coverage.

Tests properly verify the sliding window algorithm, key isolation between different identifiers (lines 352-365), and correct Redis key formatting (line 411). The status check test (lines 369-380) confirms reads don't increment counters.


416-485: LGTM!

Session operations and health check tests provide good coverage of Redis-backed session storage and observability. The corrupted data test (lines 442-446) verifies graceful handling of invalid JSON.

scripts/verify-token-migration.ts (1)

107-170: LGTM!

The main() function has clear logic, well-defined exit codes, and informative console output for migration status. The error handling with distinct exit codes (0, 1, 2) provides good CI integration capabilities.

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

129-225: LGTM!

The rate limiting tests thoroughly validate the sliding window algorithm, key independence, and expired entry cleanup. The test scenarios cover the essential Redis sorted set operations used for distributed rate limiting.

tests/load/auth-baseline.k6.js (1)

224-264: LGTM!

The handleSummary function provides good structured output with metrics aggregation and threshold validation. The textSummary helper produces clear, readable console output. The threshold pass detection logic correctly filters and validates all metric thresholds.

Note: The hardcoded output path tests/load/results/baseline-latest.json may cause issues if running multiple tests in parallel. Consider parameterizing with a timestamp or run ID for CI environments.

plan.md (1)

1-41: LGTM!

This master plan document provides an excellent, actionable roadmap for the security hardening project. The phased approach with clear dependencies, acceptance criteria, and risk assessment demonstrates thorough planning.

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

53-91: LGTM!

The cleanup interval implementation correctly addresses memory leak concerns:

  • Stores interval ID for cancellation
  • Uses 2-hour cutoff matching longest window + buffer
  • shutdownRateLimiting() clears both interval and Map data
  • Auto-starts only when setInterval is available (SSR-safe)

204-263: LGTM!

The checkDistributedRateLimit function correctly implements:

  • Redis-first with graceful fallback
  • Progressive delay using exponential backoff (capped at 30 minutes)
  • One-time logging to avoid log spam
  • Proper error handling with fallback to in-memory

319-362: LGTM!

The predefined rate limit configurations are well-suited for their respective use cases:

  • LOGIN: Conservative limits with progressive delay for brute-force protection
  • SIGNUP/PASSWORD_RESET: Hourly windows for account enumeration prevention
  • SERVICE_TOKEN: High limits for internal service-to-service communication
packages/lib/src/__tests__/test-fixtures/security-fixtures.ts (1)

1-293: LGTM!

Excellent comprehensive test fixtures covering all security testing scenarios:

  • Users with various states (admin, regular, suspended, deleted)
  • Multi-tenant data with proper cross-references
  • Token fixtures for valid, expired, and revoked states
  • Rate limit scenarios matching the predefined configurations
  • IP address fixtures including special cases (metadata, link-local, private ranges)

The inter-fixture references (e.g., testUsers.regularUser.id) ensure consistency across tests. Well-organized with clear section separators following the codebase patterns.

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

25-56: LGTM!

The three client access patterns provide appropriate flexibility:

  • getSecurityRedisClient(): Throws for strict production requirements
  • isSecurityRedisAvailable(): Quick availability check
  • tryGetSecurityRedisClient(): Non-throwing for graceful degradation

The production enforcement at line 30 ensures Redis availability in production environments.


62-164: LGTM!

The JTI operations implement correct security patterns:

  • Fail-closed: Missing or corrupted JTIs are treated as revoked (lines 94-105)
  • TTL preservation: Revocation maintains original TTL for consistent cleanup
  • Redacted logging: JTI values logged as [REDACTED] (line 145) per security guidelines
  • tokenVersion authority: revokeAllUserJTIs correctly documents that database tokenVersion is the authoritative source

256-327: LGTM!

Session operations and health check are well-implemented:

  • Session CRUD with proper JSON serialization/deserialization
  • Defensive null returns on parse errors
  • Health check provides latency metrics for monitoring
  • Error messages captured for diagnostics
packages/lib/src/__tests__/security-test-utils.ts (4)

18-96: Comprehensive malicious input coverage.

The malicious input generators provide excellent coverage across SQL injection, XSS, path traversal, SSRF, command injection, LDAP injection, header injection, and null-byte attacks. The organization by category makes them easy to use in targeted tests.


225-260: Excellent timing analysis implementation.

The use of process.hrtime.bigint() provides nanosecond precision suitable for detecting timing leaks. The statistical analysis (mean, standard deviation, min/max) across iterations is mathematically sound and will effectively identify timing discrepancies in security-sensitive operations.


269-278: LGTM!

Token generation uses appropriate cryptographic primitives: 32 bytes of randomness via randomBytes and SHA-256 hashing matching production behavior.


287-313: Well-structured request mocking utilities.

The mock request helpers correctly use documentation IP ranges (203.0.113.1 from TEST-NET-3) and properly handle optional body serialization. The composition of createMockRequest calling createMockHeaders promotes code reuse.

packages/lib/src/security/index.ts (1)

1-39: Clean barrel export structure.

The security module index properly organizes re-exports into logical groups (Redis operations, JTI operations, session operations, rate limiting) with clear comments. This provides a clean public API surface for consumers.

Comment on lines +74 to +98
// scripts/migrate-token-hashes.ts
import { db, refreshTokens, mcpTokens } from '@pagespace/db';
import { createHash } from 'crypto';
import { isNull } from 'drizzle-orm';

function hashToken(token: string): string {
return createHash('sha256').update(token).digest('hex');
}

async function migrateRefreshTokens() {
const tokens = await db.select()
.from(refreshTokens)
.where(isNull(refreshTokens.tokenHash));

console.log(`Found ${tokens.length} refresh tokens to migrate`);

for (const token of tokens) {
await db.update(refreshTokens)
.set({
tokenHash: hashToken(token.token),
tokenPrefix: token.token.substring(0, 12),
})
.where(eq(refreshTokens.id, token.id));
}
}

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 | 🟡 Minor

Migration script missing import and could benefit from batching.

The script example is missing the eq import from drizzle-orm. Additionally, single-row updates may be slow for large tables.

🔎 Suggested improvements
 // scripts/migrate-token-hashes.ts
 import { db, refreshTokens, mcpTokens } from '@pagespace/db';
 import { createHash } from 'crypto';
-import { isNull } from 'drizzle-orm';
+import { isNull, eq } from 'drizzle-orm';

 function hashToken(token: string): string {
   return createHash('sha256').update(token).digest('hex');
 }

For large tables, consider batching:

// Process in batches of 1000
const BATCH_SIZE = 1000;
for (let offset = 0; offset < tokens.length; offset += BATCH_SIZE) {
  const batch = tokens.slice(offset, offset + BATCH_SIZE);
  await db.transaction(async (tx) => {
    for (const token of batch) {
      await tx.update(refreshTokens)
        .set({ /* ... */ })
        .where(eq(refreshTokens.id, token.id));
    }
  });
}
📝 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
// scripts/migrate-token-hashes.ts
import { db, refreshTokens, mcpTokens } from '@pagespace/db';
import { createHash } from 'crypto';
import { isNull } from 'drizzle-orm';
function hashToken(token: string): string {
return createHash('sha256').update(token).digest('hex');
}
async function migrateRefreshTokens() {
const tokens = await db.select()
.from(refreshTokens)
.where(isNull(refreshTokens.tokenHash));
console.log(`Found ${tokens.length} refresh tokens to migrate`);
for (const token of tokens) {
await db.update(refreshTokens)
.set({
tokenHash: hashToken(token.token),
tokenPrefix: token.token.substring(0, 12),
})
.where(eq(refreshTokens.id, token.id));
}
}
// scripts/migrate-token-hashes.ts
import { db, refreshTokens, mcpTokens } from '@pagespace/db';
import { createHash } from 'crypto';
import { isNull, eq } from 'drizzle-orm';
function hashToken(token: string): string {
return createHash('sha256').update(token).digest('hex');
}
async function migrateRefreshTokens() {
const tokens = await db.select()
.from(refreshTokens)
.where(isNull(refreshTokens.tokenHash));
console.log(`Found ${tokens.length} refresh tokens to migrate`);
for (const token of tokens) {
await db.update(refreshTokens)
.set({
tokenHash: hashToken(token.token),
tokenPrefix: token.token.substring(0, 12),
})
.where(eq(refreshTokens.id, token.id));
}
}
🤖 Prompt for AI Agents
In @docs/security/token-hashing-migration.md around lines 74-98, The migration
script in migrateRefreshTokens is missing the eq import from drizzle-orm and
performs one-row updates which will be slow for large tables; import eq, then
change the loop to process tokens in batches (define BATCH_SIZE, slice tokens
into batches) and perform each batch inside a db.transaction using tx.update on
refreshTokens with .set({ tokenHash: hashToken(token.token), tokenPrefix:
token.token.substring(0,12) }) and .where(eq(refreshTokens.id, token.id)) to
reduce round-trips and improve throughput while preserving the existing
hashToken logic.

Comment on lines +243 to +249
const [refreshTotal] = await db.select({ count: count() }).from(refreshTokens);
const [refreshWithHash] = await db.select({ count: count() })
.from(refreshTokens)
.where(isNull(refreshTokens.tokenHash).not());
const [refreshWithoutHash] = await db.select({ count: count() })
.from(refreshTokens)
.where(isNull(refreshTokens.tokenHash));

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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, find and read the actual file to confirm the syntax
find . -name "token-hashing-migration.md" -type f

Repository: 2witstudios/PageSpace

Length of output: 107


🌐 Web query:

Drizzle ORM isNull().not() vs isNotNull() syntax

💡 Result:

Short answer: Drizzle provides isNull(column) and isNotNull(column) functions. There is no documented isNull().not() chain — to invert you use the not(...) helper (e.g. not(isNull(col))) or simply use isNotNull(col). [1][2]

Examples:

  • isNotNull(users.name) // WHERE "users"."name" IS NOT NULL. [1]
  • not(isNull(users.name)) // equivalent; or not(eq(...)) shown for other conditions. [2]

Sources:
[1] Drizzle operators (isNull / isNotNull).
[2] Drizzle select docs (using not(...) to invert conditions).


🏁 Script executed:

# Read the specific lines mentioned in the review comment
sed -n '243,249p' ./docs/security/token-hashing-migration.md

Repository: 2witstudios/PageSpace

Length of output: 423


🏁 Script executed:

# Check context around those lines to understand the code better
sed -n '240,255p' ./docs/security/token-hashing-migration.md

Repository: 2witstudios/PageSpace

Length of output: 702


Fix Drizzle ORM syntax for NOT NULL check.

Line 246 uses .where(isNull(refreshTokens.tokenHash).not()), which is not valid Drizzle ORM syntax. The .not() method does not exist on condition objects. Use isNotNull(refreshTokens.tokenHash) instead.

Suggested fix
  const [refreshWithHash] = await db.select({ count: count() })
    .from(refreshTokens)
-   .where(isNull(refreshTokens.tokenHash).not());
+   .where(isNotNull(refreshTokens.tokenHash));
📝 Committable suggestion

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

Suggested change
const [refreshTotal] = await db.select({ count: count() }).from(refreshTokens);
const [refreshWithHash] = await db.select({ count: count() })
.from(refreshTokens)
.where(isNull(refreshTokens.tokenHash).not());
const [refreshWithoutHash] = await db.select({ count: count() })
.from(refreshTokens)
.where(isNull(refreshTokens.tokenHash));
const [refreshTotal] = await db.select({ count: count() }).from(refreshTokens);
const [refreshWithHash] = await db.select({ count: count() })
.from(refreshTokens)
.where(isNotNull(refreshTokens.tokenHash));
const [refreshWithoutHash] = await db.select({ count: count() })
.from(refreshTokens)
.where(isNull(refreshTokens.tokenHash));
🤖 Prompt for AI Agents
In @docs/security/token-hashing-migration.md around lines 243-249, The Drizzle
ORM condition uses an invalid `.not()` on the isNull expression; update the
queries that count rows with/without token hashes to use the proper isNotNull
condition. Replace the `.where(isNull(refreshTokens.tokenHash).not())` usage
with `.where(isNotNull(refreshTokens.tokenHash))` in the refresh token counts
(the queries that define refreshWithHash and refreshWithoutHash) so the
conditions use isNotNull and isNull respectively while keeping the same
db.select/from(refreshTokens) structure.

Comment on lines +200 to +215
export function tamperJWTClaim(
token: string,
claim: string,
value: unknown
): string {
const parts = token.split('.');
if (parts.length !== 3) {
throw new Error('Invalid JWT format');
}

const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
payload[claim] = value;

const tamperedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url');
return `${parts[0]}.${tamperedPayload}.${parts[2]}`;
}

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 | 🟡 Minor

Add error handling for JSON parsing consistency.

The tamperJWTClaim function parses JSON at line 210 without error handling, unlike extractJWTClaims and extractJWTHeader. If parsing fails, it will throw a raw error instead of a descriptive message.

🔎 Proposed fix for consistent error handling
 export function tamperJWTClaim(
   token: string,
   claim: string,
   value: unknown
 ): string {
   const parts = token.split('.');
   if (parts.length !== 3) {
     throw new Error('Invalid JWT format');
   }
 
-  const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
+  try {
+    const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
-  payload[claim] = value;
+    payload[claim] = value;
 
-  const tamperedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url');
-  return `${parts[0]}.${tamperedPayload}.${parts[2]}`;
+    const tamperedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url');
+    return `${parts[0]}.${tamperedPayload}.${parts[2]}`;
+  } catch {
+    throw new Error('Failed to parse JWT payload');
+  }
 }
🤖 Prompt for AI Agents
In @packages/lib/src/__tests__/security-test-utils.ts around lines 200-215,
tamperJWTClaim currently parses the JWT payload without catching JSON errors;
wrap the payload JSON.parse in a try/catch (matching the pattern used in
extractJWTClaims/extractJWTHeader) and when parsing fails throw a descriptive
Error (e.g., "Invalid JWT payload: failed to parse JSON") so callers get a
consistent, clear error; adjust the code inside tamperJWTClaim around the
JSON.parse/Buffer decode and ensure the thrown error includes context and the
original exception message.

@2witstudios 2witstudios mentioned this pull request Jan 5, 2026
4 tasks done
2witstudios and others added 6 commits January 5, 2026 17:31
- Add SecurityRedis module for JTI tracking with revocation support
- Implement distributed rate limiter using Redis sorted sets (sliding window)
- Create in-memory fallback for development/testing environments
- Add comprehensive unit tests (67 tests passing)
- Update .env.example with security Redis documentation

Features:
- JTI recording, revocation, and fail-closed validation
- Sliding window rate limiting with configurable limits
- Session data storage with TTL
- Health check endpoint
- Production enforcement (Redis required)

Part of Phase 0: Security infrastructure foundation

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

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

- Fix JTI logging to use [REDACTED] pattern for security
- Add shutdownRateLimiting() export to prevent memory leaks
- Change cleanup cutoff from 24h to 2h (matches longest window + buffer)
- Add redis-test service to docker-compose.test.yml on port 6380
- Create integration test file with 9 tests (skip gracefully when Redis unavailable)

Part of Phase 0: Security infrastructure foundation

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add security-test-utils.ts with malicious input generators
- Add race condition testing helpers (racingRequests, sequentialRace)
- Add JWT manipulation utilities for testing (extractClaims, tamper)
- Add timing attack measurement helpers
- Add mock request/headers factories
- Create comprehensive test fixtures for security scenarios
- Add GitHub Actions workflow for security tests

Test utilities include:
- SQL injection, XSS, path traversal, SSRF test cases
- LDAP injection, header injection, null byte test cases
- Token generation and hashing helpers

CI workflow includes:
- Security test suite with Redis/Postgres
- Dependency audit (high/critical levels)
- Secret scanning with TruffleHog
- Static analysis with TypeScript/ESLint

Part of Phase 0: Security infrastructure foundation

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add comprehensive migration runbook for token hashing
- Create verify-token-migration.ts script for validation
- Document 6-phase migration plan with rollback procedures
- Include risk assessment and mitigation strategies
- Add SQL examples for schema changes
- Document timing-safe comparison requirements

Migration phases:
1. Schema migration (add hash columns)
2. Backfill existing tokens
3. Deploy dual-mode code
4. Verification
5. Monitoring (24-48 hours)
6. Remove plaintext columns

Part of Phase 0: Security infrastructure foundation

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add k6 load test script for auth endpoints
- Configure custom metrics (loginLatency, refreshLatency, tokenValidationLatency)
- Set up performance thresholds (p95 < 500ms login, < 200ms refresh)
- Create test stages (ramp up, hold, spike, ramp down)
- Add JSON result export and custom text summary
- Create results directory for baseline captures

Test scenarios:
- Login flow with token extraction
- Token validation against protected endpoints
- Token refresh with new token capture
- Error rate tracking

Usage: k6 run tests/load/auth-baseline.k6.js

Part of Phase 0: Security infrastructure foundation

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive project plan covering all 5 phases of security hardening:
- Phase 0: Infrastructure & Preparation (this PR)
- Phase 1: Critical Security Foundation
- Phase 2: Zero-Trust Token Architecture
- Phase 3: Distributed Security Services
- Phase 4: Defense in Depth
- Phase 5: Monitoring & Incident Response

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@2witstudios
2witstudios force-pushed the feat/security-infrastructure branch from 0203821 to 2a658b5 Compare January 5, 2026 23:31

@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 @.env.example:
- Around line 110-111: Replace the hardcoded Redis credentials with clear
placeholder values: change REDIS_PASSWORD from "pagespace_redis" to a
descriptive placeholder like
"your_redis_password_here_generate_a_secure_random_string" and update REDIS_URL
to reference that placeholder (e.g.,
"redis://:your_redis_password_here_generate_a_secure_random_string@redis:6379"
or a neutral host/port placeholder). Ensure both REDIS_PASSWORD and REDIS_URL
use the same placeholder pattern consistent with other secrets in the file so
contributors must replace them with real credentials.

In @tests/load/auth-baseline.k6.js:
- Around line 260-263: The JSON output path
'tests/load/results/baseline-latest.json' returned by the script may fail if the
containing directory doesn't exist; ensure the directory is created before
writing by adding a CI/pre-run step (e.g., run mkdir -p for the
tests/load/results directory) or add a clear prerequisite comment at the top of
this test file so callers create that directory before executing the script.
- Around line 115-120: The test currently logs the full loginResponse.body on
failure which may leak sensitive data; update the failure branch (where
loginSuccess is false and authErrorRate.add is called) to avoid printing the
full body—log only the status code and a sanitized message or selected
non-sensitive fields (e.g., error code or truncated message) from loginResponse,
or omit the body entirely; modify the console.log invocation that references
loginResponse.body to instead reference loginResponse.status and a
sanitized/short message derived from loginResponse (or remove body logging) and
keep authErrorRate handling unchanged.
🧹 Nitpick comments (6)
tests/load/auth-baseline.k6.js (1)

269-269: Unused options parameter.

The options parameter (with indent and enableColors) is passed but never used. Consider either implementing color support or removing the unused parameter to avoid confusion.

🔎 Proposed fix (remove unused parameter)
-function textSummary(data, options) {
+function textSummary(data) {

And update the call site at line 261:

-    'stdout': textSummary(data, { indent: ' ', enableColors: true }),
+    'stdout': textSummary(data),
docs/security/token-hashing-migration.md (2)

158-177: Add language identifier to fenced code block.

The expected output block should specify a language for syntax highlighting.

🔎 Suggested fix
-```
+```text
 Token Migration Verification
 ============================

332-333: Consider using proper heading instead of bold text.

Line 332 uses emphasis for what appears to be a heading.

🔎 Suggested fix
-**Total: ~1 week**
+### Total Duration
+
+~1 week
packages/lib/src/__tests__/security-test-utils.test.ts (1)

35-38: Unused loop variable category.

The category variable is declared but never used in the loop body. This may trigger linting warnings.

🔎 Proposed fix
-      for (const [category, cases] of Object.entries(inputs)) {
+      for (const cases of Object.values(inputs)) {
         expect(cases.length).toBeGreaterThan(0);
         expect(Array.isArray(cases)).toBe(true);
       }
packages/lib/src/security/distributed-rate-limit.ts (1)

29-40: Different RateLimitResult interface definition.

This file defines RateLimitResult with { allowed, retryAfter?, attemptsRemaining? } while security-redis.ts (lines 170-175) defines it with { allowed, remaining, resetAt, totalCount }. Consider consolidating to avoid confusion and ensure type compatibility across the module.

🔎 Consider using the security-redis.ts interface or creating a shared type
+// Consider importing from security-redis.ts or creating a shared types file
+import type { RateLimitResult as RedisRateLimitResult } from './security-redis';
+
+// Then adapt the interface or rename to avoid collision
 export interface RateLimitResult {
   allowed: boolean;
   retryAfter?: number;
   attemptsRemaining?: number;
 }
plan.md (1)

33-40: Add language specifier to code block.

Static analysis correctly identified that this code block lacks a language specifier. Adding one improves rendering and syntax highlighting.

🔎 Proposed fix
-```
+```text
 Phase 0: Infrastructure & Preparation
 Phase 1: Critical Security Foundation
 Phase 2: Zero-Trust Token Architecture
 Phase 3: Distributed Security Services
 Phase 4: Defense in Depth
 Phase 5: Monitoring & Incident Response
 ```
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0203821 and 2a658b5.

📒 Files selected for processing (16)
  • .env.example
  • .github/workflows/security.yml
  • docker-compose.test.yml
  • docs/security/token-hashing-migration.md
  • packages/lib/src/__tests__/security-test-utils.test.ts
  • packages/lib/src/__tests__/security-test-utils.ts
  • packages/lib/src/__tests__/test-fixtures/security-fixtures.ts
  • packages/lib/src/security/__tests__/distributed-rate-limit.test.ts
  • packages/lib/src/security/__tests__/security-redis.integration.test.ts
  • packages/lib/src/security/__tests__/security-redis.test.ts
  • packages/lib/src/security/distributed-rate-limit.ts
  • packages/lib/src/security/index.ts
  • packages/lib/src/security/security-redis.ts
  • plan.md
  • scripts/verify-token-migration.ts
  • tests/load/auth-baseline.k6.js
✅ Files skipped from review due to trivial changes (1)
  • packages/lib/src/security/tests/security-redis.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • scripts/verify-token-migration.ts
  • packages/lib/src/security/index.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:

  • packages/lib/src/security/__tests__/security-redis.integration.test.ts
  • packages/lib/src/security/distributed-rate-limit.ts
  • packages/lib/src/__tests__/test-fixtures/security-fixtures.ts
  • packages/lib/src/security/security-redis.ts
  • packages/lib/src/security/__tests__/distributed-rate-limit.test.ts
  • packages/lib/src/__tests__/security-test-utils.ts
  • packages/lib/src/__tests__/security-test-utils.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.integration.test.ts
  • packages/lib/src/security/distributed-rate-limit.ts
  • packages/lib/src/__tests__/test-fixtures/security-fixtures.ts
  • packages/lib/src/security/security-redis.ts
  • packages/lib/src/security/__tests__/distributed-rate-limit.test.ts
  • packages/lib/src/__tests__/security-test-utils.ts
  • packages/lib/src/__tests__/security-test-utils.test.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • packages/lib/src/security/__tests__/security-redis.integration.test.ts
  • packages/lib/src/security/distributed-rate-limit.ts
  • tests/load/auth-baseline.k6.js
  • packages/lib/src/__tests__/test-fixtures/security-fixtures.ts
  • packages/lib/src/security/security-redis.ts
  • packages/lib/src/security/__tests__/distributed-rate-limit.test.ts
  • packages/lib/src/__tests__/security-test-utils.ts
  • packages/lib/src/__tests__/security-test-utils.test.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 (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 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/__tests__/test-fixtures/security-fixtures.ts
  • packages/lib/src/__tests__/security-test-utils.ts
  • packages/lib/src/__tests__/security-test-utils.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 **/__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/src/__tests__/test-fixtures/security-fixtures.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:

  • docs/security/token-hashing-migration.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:

  • docs/security/token-hashing-migration.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:

  • docs/security/token-hashing-migration.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:

  • docs/security/token-hashing-migration.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/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:

  • docs/security/token-hashing-migration.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/**/*.ts : Always use Drizzle client and queries from `pagespace/db` for database access instead of direct queries

Applied to files:

  • docs/security/token-hashing-migration.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:

  • docs/security/token-hashing-migration.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} : Always use Drizzle client from `pagespace/db` package for database access

Applied to files:

  • docs/security/token-hashing-migration.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} : For database access, always use Drizzle client from `pagespace/db`: `import { db, pages } from 'pagespace/db';`

Applied to files:

  • docs/security/token-hashing-migration.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 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:

  • docs/security/token-hashing-migration.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 .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:

  • .env.example
🧬 Code graph analysis (4)
packages/lib/src/security/distributed-rate-limit.ts (3)
packages/lib/src/security/index.ts (9)
  • RateLimitConfig (37-37)
  • RateLimitResult (38-38)
  • shutdownRateLimiting (35-35)
  • checkDistributedRateLimit (31-31)
  • tryGetSecurityRedisClient (16-16)
  • resetDistributedRateLimit (32-32)
  • getDistributedRateLimitStatus (33-33)
  • DISTRIBUTED_RATE_LIMITS (36-36)
  • initializeDistributedRateLimiting (34-34)
packages/lib/src/security/security-redis.ts (2)
  • RateLimitResult (170-175)
  • tryGetSecurityRedisClient (50-56)
packages/lib/src/logging/logger-config.ts (1)
  • loggers (8-18)
packages/lib/src/security/security-redis.ts (3)
packages/lib/src/services/shared-redis.ts (2)
  • getSharedRedisClient (25-39)
  • isSharedRedisAvailable (44-46)
packages/lib/src/logging/logger-config.ts (1)
  • loggers (8-18)
packages/lib/src/security/distributed-rate-limit.ts (1)
  • RateLimitResult (36-40)
packages/lib/src/security/__tests__/distributed-rate-limit.test.ts (4)
packages/lib/src/security/distributed-rate-limit.ts (7)
  • RateLimitConfig (29-34)
  • checkDistributedRateLimit (204-263)
  • resetDistributedRateLimit (268-282)
  • getDistributedRateLimitStatus (287-313)
  • initializeDistributedRateLimiting (372-405)
  • DISTRIBUTED_RATE_LIMITS (319-362)
  • shutdownRateLimiting (80-86)
packages/lib/src/security/index.ts (8)
  • RateLimitConfig (37-37)
  • tryGetSecurityRedisClient (16-16)
  • checkDistributedRateLimit (31-31)
  • resetDistributedRateLimit (32-32)
  • getDistributedRateLimitStatus (33-33)
  • initializeDistributedRateLimiting (34-34)
  • DISTRIBUTED_RATE_LIMITS (36-36)
  • shutdownRateLimiting (35-35)
packages/lib/src/security/security-redis.ts (1)
  • tryGetSecurityRedisClient (50-56)
packages/lib/src/logging/logger-config.ts (1)
  • loggers (8-18)
packages/lib/src/__tests__/security-test-utils.test.ts (1)
packages/lib/src/__tests__/security-test-utils.ts (12)
  • getMaliciousInputs (18-88)
  • getAllMaliciousInputs (93-96)
  • racingRequests (111-133)
  • sequentialRace (141-156)
  • extractJWTClaims (166-178)
  • extractJWTHeader (183-195)
  • tamperJWTClaim (200-215)
  • measureExecutionTime (225-232)
  • generateTestToken (269-271)
  • hashToken (276-278)
  • createMockHeaders (287-295)
  • createMockRequest (300-313)
🪛 Checkov (3.2.334)
.github/workflows/security.yml

[medium] 76-77: Basic Auth Credentials

(CKV_SECRET_4)

🪛 LanguageTool
plan.md

[uncategorized] ~748-~748: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...``` Acceptance Criteria: - [ ] All rate limiting uses Redis - [ ] Production startup fai...

(EN_COMPOUND_ADJECTIVE_INTERNAL)


[uncategorized] ~2574-~2574: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...tokens hashed at rest - [ ] Distributed rate limiting active --- ## Rollout Strategy 1. **...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

🪛 markdownlint-cli2 (0.18.1)
plan.md

33-33: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


112-112: Strong style
Expected: asterisk; Actual: underscore

(MD050, strong-style)


112-112: Strong style
Expected: asterisk; Actual: underscore

(MD050, strong-style)


119-119: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


128-128: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


170-170: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


2448-2448: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


2456-2456: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


2501-2501: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


2532-2532: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

docs/security/token-hashing-migration.md

158-158: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


332-332: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

⏰ 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 (33)
tests/load/auth-baseline.k6.js (5)

29-33: Well-structured custom metrics for detailed performance analysis.

The custom metrics (Trend for latencies, Rate for errors) align well with k6 best practices and will provide granular visibility into authentication flow performance.


36-52: Thresholds configuration looks appropriate for baseline testing.

The staged load profile provides good coverage from low (5 VUs) to spike (20 VUs) conditions. The p95 thresholds align with the PR objectives. The 50ms threshold for token validation is aggressive but reasonable for a protected endpoint that only validates a JWT.


59-61: Module-level token state is correct for k6's VU isolation model.

Each k6 VU runs in its own JavaScript VM, so accessToken and refreshToken are correctly isolated per VU. This accurately simulates separate user sessions.


66-75: Setup function correctly passes configuration to VUs.

The setup function appropriately returns test configuration that is shared with all VUs via the data parameter.


125-211: Token validation and refresh flows are well-implemented.

The test correctly:

  • Guards protected operations with token checks
  • Handles token rotation on refresh
  • Includes realistic think times between operations
  • Tracks error rates consistently
docker-compose.test.yml (1)

22-34: LGTM! Well-configured Redis test service.

The Redis test service configuration is appropriate:

  • Specific version pinning (7.4-alpine)
  • Non-conflicting port (6380)
  • Health checks for service readiness
  • Memory limits suitable for testing
  • Ephemeral data via tmpfs
.github/workflows/security.yml (1)

1-160: LGTM! Comprehensive security CI workflow.

The workflow provides thorough security validation:

  • Parallel jobs for efficiency (security-tests, dependency-audit, secret-scanning, static-analysis)
  • Proper service configuration for Redis and PostgreSQL
  • Appropriate path filters to trigger on security-relevant changes
  • Progressive audit levels (high warnings continue, critical fails)

The static analysis hint about credentials on lines 76-77 is a false positive—these are test credentials for the CI environment and are appropriate.

packages/lib/src/__tests__/test-fixtures/security-fixtures.ts (1)

1-293: LGTM! Comprehensive and well-organized test fixtures.

The security test fixtures are thorough and cover a wide range of scenarios:

  • Multiple user states (admin, regular, suspended, deleted)
  • Multi-tenant test data with proper relationships
  • Various token states and types (valid, expired, revoked, user/service/MCP sessions)
  • Edge cases for rate limiting, IPs, and JTIs
  • Proper TypeScript typing with as const for literal types

The fixtures provide excellent coverage for security testing scenarios.

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

1-436: LGTM! Comprehensive and well-structured test suite.

The distributed rate limiting tests provide excellent coverage:

  • Both Redis and in-memory fallback paths
  • Error handling and graceful degradation
  • Progressive delay logic with proper cap enforcement
  • Environment-specific behavior (development vs production)
  • Edge cases including window expiry and identifier isolation
  • Proper mocking and cleanup

The test structure follows best practices with clear describe blocks and focused test cases.

packages/lib/src/__tests__/security-test-utils.test.ts (2)

1-16: LGTM! Well-structured test file with comprehensive coverage.

The test file properly validates all exported utilities from security-test-utils.ts. Test organization follows best practices with descriptive describe blocks and focused it statements. Based on learnings, the file is correctly placed in the __tests__/ directory with the *.test.ts naming convention.


72-92: Good race condition test setup.

The test correctly demonstrates that racingRequests creates concurrent execution where race conditions can occur (line 85 comment acknowledges non-unique results due to races). The default count verification (line 88-91) is also appropriate.

packages/lib/src/security/__tests__/security-redis.integration.test.ts (4)

1-36: LGTM! Well-designed integration test with graceful degradation.

The test file correctly implements the skip-when-unavailable pattern, making it suitable for CI environments where Redis may not always be present. The connection timeout and retry settings (lines 25-27) are appropriate for test environments.


38-57: Test cleanup is appropriate for integration tests.

Using redis.keys() with a test-specific prefix (sec:test:*) is acceptable for integration test cleanup. The beforeEach cleanup ensures test isolation.


59-127: Good JTI operation coverage.

The tests properly validate TTL behavior and revocation semantics. The non-null assertions (lines 80, 119) are safe given the preceding truthy checks.


129-172: Thorough sliding window rate limiting tests.

The test correctly validates the sliding window algorithm including the boundary case where the 6th request exceeds the limit (lines 158-171). The pipeline result extraction with type assertion (line 169) is appropriate given the known structure.

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

60-91: Good memory leak prevention with shutdown hook.

The shutdownRateLimiting() function properly clears the interval and in-memory data. The auto-start check (line 89) handles non-browser environments correctly. The 2-hour cleanup cutoff aligns well with the longest rate limit window (1 hour for SIGNUP/PASSWORD_RESET) plus buffer.


319-362: Well-defined rate limit configurations.

The predefined configurations cover appropriate use cases with sensible defaults. The progressive delay for LOGIN (line 324) helps prevent brute-force attacks, while SERVICE_TOKEN's higher limit (1000/minute) accommodates M2M communication patterns.


372-404: Initialization allows production startup without Redis.

The initializeDistributedRateLimiting function (lines 386-390) returns { mode: 'memory', error } in production when Redis is unavailable instead of throwing. This matches the fallback behavior in checkDistributedRateLimit but may warrant explicit documentation about the degraded security posture during startup.

Verify that application startup logic appropriately handles this degraded state, potentially alerting operations teams when Redis is unavailable in production.

plan.md (2)

1-8: Comprehensive security planning document.

This master plan effectively consolidates security requirements into an actionable roadmap. The phased approach with clear dependencies (lines 2532-2552) enables systematic implementation.


90-97: Implementation status tracking is clear and helpful.

The completion status markers (✅ COMPLETED) with dates and implementation notes provide good visibility into project progress. The test count updates (76 tests total) help track coverage growth.

packages/lib/src/__tests__/security-test-utils.ts (5)

1-16: Well-documented utility module.

The module documentation clearly explains the purpose of each utility category. The import of crypto primitives (line 8) is appropriate for security testing utilities.


17-88: Comprehensive malicious input coverage.

The input generators cover essential attack vectors including:

  • SQL injection with time-based blind injection (line 27: WAITFOR DELAY)
  • XSS with various payload types (event handlers, script tags, JavaScript URIs)
  • Path traversal with encoding bypasses (double encoding, null bytes)
  • SSRF targeting cloud metadata endpoints (AWS, GCP, Azure patterns)
  • LDAP injection patterns

These are appropriate for validating input sanitization across the codebase.


111-133: Good barrier pattern for race condition testing.

The racingRequests implementation correctly uses a barrier to ensure all requests start simultaneously (line 130 uses setImmediate to release). This maximizes the chance of triggering race conditions in tests.


225-260: Good timing analysis utilities for security testing.

The measureExecutionTime uses process.hrtime.bigint() for nanosecond precision (lines 228-231), which is essential for detecting timing leaks. The timingAnalysis function provides statistical measures useful for identifying timing attack vulnerabilities.


269-278: Token generation matches production security requirements.

Using randomBytes(32) provides 256 bits of entropy, and hashToken uses SHA-256 consistent with the production token hashing strategy documented in the migration plan.

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

1-18: Well-documented module with clear namespace strategy.

The module documentation clearly explains the key prefix scheme (sec:jti:, sec:rate:, sec:session:) which aids debugging and prevents key collisions with other Redis usage.


25-36: Appropriate production enforcement.

The getSecurityRedisClient function correctly enforces Redis availability in production (line 29-31). The differentiated error messages help distinguish between production requirements and general unavailability.


88-106: Correct fail-closed implementation for JTI validation.

The isJTIRevoked function properly implements fail-closed security:

  • Missing JTI (line 94-96) → treated as revoked
  • Corrupted data (line 102-104) → treated as revoked
  • Only explicit status: 'valid' allows the token

This ensures that Redis failures or data corruption don't inadvertently allow unauthorized access.


145-145: JTI properly redacted in logs.

The JTI is logged as [REDACTED] (line 145), consistent with codebase logging patterns and preventing sensitive token identifiers from appearing in logs.


170-175: Note: Different RateLimitResult interface.

This interface defines { allowed, remaining, resetAt, totalCount } while distributed-rate-limit.ts defines { allowed, retryAfter?, attemptsRemaining? }. The differing shapes may cause confusion when these modules are used together. Consider consolidating or clearly documenting the distinction.

Verify that consumers of these interfaces handle the different shapes appropriately, particularly when distributed-rate-limit.ts adapts the Redis result at lines 240-246.


191-216: Correct sliding window implementation with atomic pipeline.

The rate limiting uses an atomic Redis pipeline:

  1. Remove expired entries (line 195)
  2. Add current request with unique ID (line 198)
  3. Count entries (line 201)
  4. Set key expiry (line 204)

The random suffix in the member value (${now}-${Math.random().toString(36).slice(2)}) ensures uniqueness even for same-millisecond requests.


269-286: Good error handling in session data retrieval.

The getSessionData function correctly handles JSON parse failures (lines 281-284) by returning null rather than throwing, which allows graceful handling of corrupted session data.


305-327: Useful health check with latency measurement.

The checkSecurityRedisHealth function provides actionable information including availability status, latency measurement, and error details. This is valuable for operational monitoring and debugging Redis connectivity issues.

Comment thread .env.example Outdated
Comment thread tests/load/auth-baseline.k6.js
Comment thread tests/load/auth-baseline.k6.js
- Use placeholder credentials in .env.example for Redis
- Remove unused options parameter from textSummary in k6 test
- Sanitize login failure logs to avoid exposing sensitive data
- Add prerequisite note about output directory creation
- Add language identifiers to code blocks in docs
- Use proper heading instead of bold text in migration doc
- Prefix unused loop variable with underscore in 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: 0

🧹 Nitpick comments (4)
packages/lib/src/__tests__/security-test-utils.test.ts (2)

41-58: Consider testing patterns for all input categories.

The tests validate patterns for only 3 of 8 categories (SQL injection, XSS, path traversal). Consider adding similar pattern checks for the remaining categories (SSRF, command injection, LDAP injection, header injection, null byte) to ensure comprehensive validation of malicious input generators.

🔎 Example additional pattern tests
+    it('SSRF inputs contain URL patterns', () => {
+      const inputs = getMaliciousInputs();
+
+      expect(inputs.ssrf).toContainEqual(expect.stringContaining('://'));
+    });
+
+    it('command injection inputs contain shell metacharacters', () => {
+      const inputs = getMaliciousInputs();
+
+      expect(inputs.commandInjection).toContainEqual(expect.stringMatching(/[;&|`$]/));
+    });
+
+    it('null byte inputs contain null characters', () => {
+      const inputs = getMaliciousInputs();
+
+      expect(inputs.nullByte).toContainEqual(expect.stringContaining('%00'));
+    });

73-86: Strengthen race condition validation.

The test demonstrates concurrent execution but doesn't validate race condition behavior. The comment on line 85 acknowledges this but no assertion verifies it. Consider asserting that counter is less than the expected value (5) or that results contain duplicate values, which would confirm actual race conditions occurred.

🔎 Proposed improvement
     const results = await racingRequests(increment, 5);

     expect(results.length).toBe(5);
-    // Due to race conditions, not all results will be unique
+    // Due to race conditions, final counter should be less than count
+    expect(counter).toBeLessThan(5);
+    // Or verify results contain duplicates
+    const uniqueResults = new Set(results);
+    expect(uniqueResults.size).toBeLessThan(results.length);
plan.md (2)

2456-2532: Add language specifiers to code blocks for clarity.

Several code blocks lack language identifiers, which affects readability and syntax highlighting in documentation:

  • Line 2456: files block (should be text or omitted, but context unclear)
  • Line 2501: files block (should be text or omitted)
  • Line 2532: graph or mermaid block (dependency graph)

While not a blocker, adding language specs improves documentation quality:

 ### New Files to Create

-```
+```text
 packages/lib/src/
 ├── auth/

And for the dependency graph:

 ## Dependency Graph

-```
+```mermaid
 P0-T1 (Redis) ←─┬─ P1-T1 (JTI) ←── P1-T2 (User Validation)

119-170: Prefer headings over emphasis for section organization.

Several subsections use bold text (**...**) instead of proper Markdown headings (##), which impacts document structure and navigation:

  • Line 119: **Fix 1: JTI Logging** → should be ## Fix 1: JTI Logging
  • Line 128: **Fix 2: Memory Leak** → should be ## Fix 2: Memory Leak
  • Line 170: **Fix 3: Add Redis to docker-compose.test.yml** → should be ## Fix 3: Add Redis to docker-compose.test.yml

Same pattern at line 2448 and throughout the document.

Using proper heading hierarchy makes the document more navigable (table of contents, anchors, screen readers) and aligns with Markdown best practices.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2a658b5 and 1e9feab.

📒 Files selected for processing (5)
  • .env.example
  • docs/security/token-hashing-migration.md
  • packages/lib/src/__tests__/security-test-utils.test.ts
  • plan.md
  • tests/load/auth-baseline.k6.js
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • tests/load/auth-baseline.k6.js
  • packages/lib/src/__tests__/security-test-utils.test.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
**/*.{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/__tests__/security-test-utils.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/__tests__/security-test-utils.test.ts
🧠 Learnings (15)
📚 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:

  • docs/security/token-hashing-migration.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} : 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:

  • docs/security/token-hashing-migration.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:

  • docs/security/token-hashing-migration.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:

  • docs/security/token-hashing-migration.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/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:

  • docs/security/token-hashing-migration.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:

  • docs/security/token-hashing-migration.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/**/*.ts : Always use Drizzle client and queries from `pagespace/db` for database access instead of direct queries

Applied to files:

  • docs/security/token-hashing-migration.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} : Always use Drizzle client from `pagespace/db` package for database access

Applied to files:

  • docs/security/token-hashing-migration.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 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:

  • docs/security/token-hashing-migration.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} : For database access, always use Drizzle client from `pagespace/db`: `import { db, pages } from 'pagespace/db';`

Applied to files:

  • docs/security/token-hashing-migration.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 .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:

  • .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-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: 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/__tests__/security-test-utils.test.ts
🧬 Code graph analysis (1)
packages/lib/src/__tests__/security-test-utils.test.ts (1)
packages/lib/src/__tests__/security-test-utils.ts (12)
  • getMaliciousInputs (18-88)
  • getAllMaliciousInputs (93-96)
  • racingRequests (111-133)
  • sequentialRace (141-156)
  • extractJWTClaims (166-178)
  • extractJWTHeader (183-195)
  • tamperJWTClaim (200-215)
  • measureExecutionTime (225-232)
  • generateTestToken (269-271)
  • hashToken (276-278)
  • createMockHeaders (287-295)
  • createMockRequest (300-313)
🪛 LanguageTool
plan.md

[uncategorized] ~748-~748: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...``` Acceptance Criteria: - [ ] All rate limiting uses Redis - [ ] Production startup fai...

(EN_COMPOUND_ADJECTIVE_INTERNAL)


[uncategorized] ~2574-~2574: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...tokens hashed at rest - [ ] Distributed rate limiting active --- ## Rollout Strategy 1. **...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

🪛 markdownlint-cli2 (0.18.1)
plan.md

112-112: Strong style
Expected: asterisk; Actual: underscore

(MD050, strong-style)


112-112: Strong style
Expected: asterisk; Actual: underscore

(MD050, strong-style)


119-119: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


128-128: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


170-170: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


2448-2448: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


2456-2456: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


2501-2501: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


2532-2532: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ 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 (8)
.env.example (1)

98-115: ✅ Previous security concern resolved: Redis credentials are now placeholders.

The hardcoded Redis credentials flagged in the previous review have been replaced with clear placeholder values (your_redis_password_here). Both REDIS_PASSWORD and REDIS_URL now use consistent placeholder patterns, forcing developers to explicitly set real credentials rather than accidentally deploying with known defaults.

The expanded comments (lines 98–109) effectively document production requirements and security implications when Redis is unavailable. The new CRON_SECRET variable (lines 113–115) also follows the established placeholder pattern.

packages/lib/src/__tests__/security-test-utils.test.ts (1)

1-260: Excellent test coverage with well-structured test suite.

This test file provides comprehensive validation of all security test utilities with clear test descriptions and proper error case handling. The tests follow TypeScript best practices and Vitest conventions effectively.

plan.md (1)

1-100: Excellent comprehensive security hardening roadmap.

The master plan is well-structured, detailed, and aligns perfectly with the PR objectives. It successfully consolidates four planning documents into an actionable implementation roadmap with:

  • Clear phase breakdown (0-5) with specific, measurable tasks
  • Dependency graph showing implementation order
  • Risk assessment and success metrics
  • Concrete code examples and test specifications
  • Integration with actual implementations in this PR (security-redis, distributed-rate-limit, test utilities)

The Phase 0 completion status is appropriate given that this PR includes the foundational infrastructure work (Redis-based JTI tracking, distributed rate limiting, security test infrastructure, and migration planning).

tests/load/auth-baseline.k6.js (5)

19-21: Documentation addresses directory creation prerequisite.

The prerequisite section clearly documents the need to create the output directory before running the test, directly addressing the previous concern about the directory potentially not existing.


29-37: LGTM: Clean metric definitions.

The custom metrics provide valuable granular tracking for login, refresh, and token validation flows. Using k6's Trend and Rate metrics is appropriate for latency and error rate measurements.


40-56: LGTM: Well-structured load profile and thresholds.

The staged load pattern provides a realistic baseline scenario. The thresholds are appropriate starting points, and line 49's comment correctly notes they should be adjusted based on actual baseline results.


119-124: Sanitized logging resolves previous security concern.

Line 121 now logs only the status code, removing the sensitive response body from logs. This directly addresses the previous review comment about potential exposure of sensitive error details or tokens.


228-268: LGTM: Comprehensive summary generation.

The handleSummary function produces both JSON and text outputs with proper error handling via optional chaining. The output path at line 266 aligns with the documented prerequisite at lines 19-21.

- Add pattern tests for all malicious input categories (SSRF, command
  injection, LDAP injection, header injection, null byte)
- Strengthen race condition test with proper assertions verifying
  that counter < expected and results contain duplicates
- Add language specifiers to plan.md code blocks (file trees, graph)
- Convert Fix 1-4 bold sections to proper headings in plan.md

🤖 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 (5)
plan.md (5)

101-221: Clarify task ordering: P0-T1.1 is presented as a new prerequisite but mid-phase.

P0-T1.1 (Foundation Hardening) is labeled "NEW - Pre-requisite for P0-T2" at line 101, yet it's embedded within the Phase 0 sequence after P0-T1 and before P0-T2. The narrative flow suggests these were originally sequential, then P0-T1.1 was inserted as a retrofit to address code-review findings. To improve clarity for future readers:

  • Either move P0-T1.1 immediately after P0-T1 and before P0-T2 with explicit "Inserted pre-requisite" language, or
  • Restructure P0 into P0-T1, P0-T1-Hotfix (or similar), P0-T2, etc., to signal the dependency clearly without the "[NEW]" label.

The current presentation may confuse readers about whether P0-T1 and P0-T1.1 are truly separate or a single task with follow-up fixes.


50-50: Distributed rate limiting appears in multiple phases—clarify task boundary and avoid duplication.

Rate limiting is introduced in:

  • P0-T1 (lines 50): Redis cluster setup mentions "rate limiting" as one of three use cases.
  • P0-T1.1 (lines 110-112, 129-170): Addresses memory-leak and cleanup-interval fixes for in-memory rate limiting.
  • P1-T5 (lines 662-760): Describes implementation of distributed rate limiting with Redis and in-memory fallback.

This suggests that P0-T1.1 fixes are interim patches (in-memory rate limiting), while P1-T5 is the final distributed Redis-based system. The plan should explicitly state this progression—e.g., "P0-T1.1 hardens temporary in-memory rate limiting; P1-T5 replaces with permanent distributed Redis-backed solution"—to avoid confusion about whether these are parallel efforts or sequential phases of the same feature.

Also applies to: 110-112, 662-760


752-752: Minor Markdown style issue: inconsistent strong/emphasis formatting.

Static analysis flags suggest possible strong-style consistency and compound-adjective hyphenation issues in the acceptance criteria sections around lines 752 and 2578. For example:

  • Line 752 (P1-T5 acceptance criteria): "[ ] Production startup fails without Redis" – verify this matches the codebase's emphasis/strong style convention (asterisks ** vs underscores __).
  • Line 2578 (Success Metrics): "[ ] tokens hashed at rest" – ensure consistency with other bullet-point formatting.

These are minor, but maintaining consistent Markdown style improves readability for all output formats.

Also applies to: 2578-2578


2437-2453: Test Coverage Matrix target is aspirational—consider adding explicit baseline and interim milestones.

The Test Coverage Matrix (lines 2437-2451) shows current coverage (0–95%) and target (100%), but lacks interim checkpoints. For example, "Race Conditions" shows 0% → 100%, but no Phase 1 or Phase 2 target. Given the 6-phase rollout (lines 2584–2596), consider adding a third column or footnotes specifying coverage expectations after each phase (e.g., "Race Conditions: 0% → 30% by P1 → 100% by P3"). This helps stakeholders understand progress and prevents surprise gaps later.


2584-2596: Rollout timeline assumes parallel work in later phases—clarify team capacity and critical paths.

The Rollout Strategy spans 13+ weeks with 5 phases. The Dependency Graph (lines 2534–2556) shows that Phase 2 depends on Phase 1, and Phase 4 depends on Phase 2, but the schedule doesn't explicitly account for this sequencing or parallel streams. For example:

  • Weeks 3–4 (Phase 1) could be compressed if some tasks are parallelizable (e.g., P1-T4 Load Testing and P1-T6 Timing-Safe Comparisons appear independent).
  • Weeks 9–10 (Phase 3) depends on Phase 2 completion; ensure Phase 2 critical path is clear.

Consider adding a Gantt or critical-path note to the Rollout Strategy so implementation teams understand which tasks can overlap and which are strictly sequential.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1e9feab and bd34f10.

📒 Files selected for processing (2)
  • packages/lib/src/__tests__/security-test-utils.test.ts
  • plan.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/lib/src/tests/security-test-utils.test.ts
🧰 Additional context used
🪛 LanguageTool
plan.md

[uncategorized] ~752-~752: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...``` Acceptance Criteria: - [ ] All rate limiting uses Redis - [ ] Production startup fai...

(EN_COMPOUND_ADJECTIVE_INTERNAL)


[uncategorized] ~2578-~2578: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...tokens hashed at rest - [ ] Distributed rate limiting active --- ## Rollout Strategy 1. **...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

🪛 markdownlint-cli2 (0.18.1)
plan.md

112-112: Strong style
Expected: asterisk; Actual: underscore

(MD050, strong-style)


112-112: Strong style
Expected: asterisk; Actual: underscore

(MD050, strong-style)


2452-2452: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

⏰ 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)
plan.md (3)

5-5: Clarify document status—contradiction between "Planning Phase" and completed Phase 0.

Line 5 indicates "Status: Planning Phase," but line 90 marks P0-T1 as "✅ COMPLETED (2026-01-05)," and line 212 marks P0-T1.1 as "✅ COMPLETED (2026-01-05)." This signals ambiguity about whether the plan itself is in progress or post-facto documentation of completed work. Consider updating the header status to reflect the actual state, e.g., "Status: Phase 0 Complete; Phase 1 In Progress" or "Status: Executed Plan with Retrospective Documentation."

Also applies to: 90-90


1727-1782: Verify transaction isolation level syntax for Drizzle ORM.

Line 1780 specifies isolationLevel: 'serializable' as a transaction option. Confirm that Drizzle ORM's transaction API supports this parameter and syntax. If the actual API differs (e.g., requires { isolation: 'serializable' } or must be set at connection level), update the code example to match the library's current version.


200-221: Implementation Notes contradict task narrative—clarify P0-T1.1 status and scope.

The "Implementation Notes" section (lines 214–221) states fixes have been applied (e.g., "Fixed JTI logging in security-redis.ts:145," "Added shutdownRateLimiting() export") and tests are passing (76 tests total). However, the task narrative above (lines 102–170) presents P0-T1.1 as prospective fixes to be made ("Fix 1, Fix 2," etc., using imperative language).

This suggests P0-T1.1 has already been executed, so the "Implementation" section should use past tense ("implemented," "deployed") and include verification steps (e.g., "All 76 tests verified passing in [commit hash or build number]") to clarify that these changes are in the codebase. Alternatively, if the notes are future-looking, the status should be marked "IN PROGRESS" rather than "✅ COMPLETED."

@2witstudios

Copy link
Copy Markdown
Owner Author

Code review

Found 1 issue:

  1. Security workflow calls missing lint script - @pagespace/lib package does not define a lint script in package.json (only build, typecheck, test, email:dev), so the workflow step will fail with "Missing script: lint"

https://github.com/2witstudios/PageSpace/blob/7f8c1fcba20571734381afc63067169b62fa3552/.github/workflows/security.yml#L159-L161

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@pagespace/lib doesn't define a lint script, causing the workflow to
fail with "Missing script: lint". Changed to target web which has
ESLint configured.

🤖 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 @.github/workflows/security.yml:
- Around line 126-132: The workflow step "TruffleHog Secret Scan" currently uses
the mutable reference trufflesecurity/trufflehog@main; change the uses value to
a specific release tag or full commit SHA (e.g.,
trufflesecurity/trufflehog@v3.91.2 or trufflesecurity/trufflehog@<full-sha>) and
add a short comment noting the chosen tag/sha and reason for pinning; also
enable automated updates (Dependabot/Renovate) for actions so the pinned
reference can be updated regularly.
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd34f10 and 2cf0898.

📒 Files selected for processing (1)
  • .github/workflows/security.yml
🧰 Additional context used
🪛 Checkov (3.2.334)
.github/workflows/security.yml

[medium] 76-77: 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: Static Security Analysis
  • GitHub Check: Unit Tests
🔇 Additional comments (4)
.github/workflows/security.yml (4)

3-23: Well-scoped path filters.

The trigger configuration correctly limits workflow runs to security-relevant paths and includes the workflow file itself for testing changes. The combination of push, pull_request, and manual dispatch provides good flexibility.


30-85: Service configuration and test environment look correct.

The Postgres and Redis services are properly configured with health checks to ensure availability before tests run. Regarding the static analysis hint (CKV_SECRET_4) about credentials in DATABASE_URL: these are test credentials for ephemeral CI containers, not production secrets—this is standard practice for CI environments.


109-114: Two-tier audit approach is reasonable.

The strategy of reporting high-severity vulnerabilities while only failing on critical ones provides a good balance between visibility and workflow stability. The inline comment clearly documents this intent.


156-160: Lint target correctly targets the web package.

The fix to target web instead of @pagespace/lib (which lacks a lint script) addresses the reviewer's concern. The combination of TypeScript type checking and ESLint provides good static analysis coverage.

Comment thread .github/workflows/security.yml
- Pin trufflehog@main to v3.92.4 for reproducible builds
- Add dependabot.yml for automated weekly GitHub Actions updates

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2witstudios added a commit that referenced this pull request Jun 9, 2026
…rd-hash CWE

CodeQL's js/insufficient-password-hash fires on the createHash calls; it was
already reviewed and dismissed for the prior SHA-256 form (alerts #160/#161).
Document inline why it is a false positive — this is a deterministic constant-
time comparison of high-entropy secrets (API keys, HMAC signatures, device
tokens), not at-rest storage of low-entropy passwords, so a slow salted KDF is
inapplicable (non-deterministic → cannot compare digests for equality). Prevents
future "fixes" that would reintroduce the hangman timing risk.

Co-Authored-By: Claude <noreply@anthropic.com>
2witstudios added a commit that referenced this pull request Jun 9, 2026
* fix(security): timing-safe SERVICE_API_SECRET comparison

Finding M4: the global-prompt service endpoint validated the inbound
x-service-secret with a raw `!==` against process.env.SERVICE_API_SECRET.
That secret is an unhashed shared secret, and a successful match lets the
caller pass an arbitrary x-service-user-id and receive that user's COMPLETE
global-prompt context window — so a timing side-channel on the compare leaks
a full-impersonation credential one byte at a time.

Swap the raw comparison for the existing pure `secureCompare` (SHA-256 +
timingSafeEqual) from @pagespace/lib/auth/secure-compare, and treat a present
x-service-secret header (even empty) as an explicit service-auth attempt that
fails closed when SERVICE_API_SECRET is unset/empty (an empty env secret can no
longer silently fall through to the admin path).

Tests (strict TDD, RED-first):
- packages/lib secure-compare unit tests: equal->true, differing->false,
  prefix/length->false, empty/undefined/non-string->false, case sensitivity.
- global-prompt GET auth-gate tests: 403 on mismatch, 403 fail-closed on
  unset/empty secret, x-service-user-id path reachable only after the compare
  passes (400 on missing user id only when secret matches), and fall-through to
  withAdminAuth when no header is present.

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

* fix(security): use SHA3-256 in secureCompare (repo token-hash convention)

Per the project rule (aidd-timing-safe-compare) and the at-rest convention in
token-utils.ts (`hashToken` = SHA3-256) and services/sandbox/session-key.ts,
auth/secret comparisons must hash with SHA3-256 before comparing. secureCompare
was hashing with SHA-256, which is off-pattern.

Switch the internal digest from sha256 -> sha3-256. secureCompare hashes BOTH
inputs with the same algorithm and compares ephemeral digests, so equality
semantics are unchanged for all ~20 callers (the digest is never persisted or
cross-compared) — this is a behavior-preserving conformance change. Added a
comment documenting why it must stay a hash-then-compare (anti-"fix"-to-raw
note). The global-prompt SERVICE_API_SECRET gate inherits SHA3-256.

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

* test(security): update canonical secureCompare test for SHA3-256, drop duplicate

The canonical secureCompare unit suite already lives at
packages/lib/src/__tests__/secure-compare.test.ts and is comprehensive
(equality, length-safety, null/undefined/non-string, unicode, device tokens,
hash-algorithm assertion). Update its SHA-256 references/spy to SHA3-256 to
match the implementation, and remove the redundant parallel test file I had
added under src/auth/__tests__/ — one canonical location, per repo convention.

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

* docs(security): explain why secureCompare's fast hash is not a password-hash CWE

CodeQL's js/insufficient-password-hash fires on the createHash calls; it was
already reviewed and dismissed for the prior SHA-256 form (alerts #160/#161).
Document inline why it is a false positive — this is a deterministic constant-
time comparison of high-entropy secrets (API keys, HMAC signatures, device
tokens), not at-rest storage of low-entropy passwords, so a slow salted KDF is
inapplicable (non-deterministic → cannot compare digests for equality). Prevents
future "fixes" that would reintroduce the hangman timing risk.

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

---------

Co-authored-by: Claude <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