Skip to content

Conversation

@2witstudios
Copy link
Owner

@2witstudios 2witstudios commented Jan 15, 2026

  • Add filePages lookup for page-bound tokens instead of just checking
    drive membership
  • Make isResourceAccessAllowed async to support database query
  • Update plan.md Phase 2 acceptance criteria to reflect completion status

Summary by CodeRabbit

  • Bug Fixes

    • Access checks made asynchronous and improved page-binding validation so file access reflects actual page links; logging adjusted for failure reasons.
  • Tests

    • Added/expanded tests and mocks for file–page linking and access-control flows; updated encryption test to better simulate corrupted auth tags; improved test setup/teardown to avoid global cleanup races.
  • Chores

    • Updated project plan/documentation with task statuses, notes, and test scaffolding.

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

- Add filePages lookup for page-bound tokens instead of just checking
  drive membership
- Make isResourceAccessAllowed async to support database query
- Update plan.md Phase 2 acceptance criteria to reflect completion status
@chatgpt-codex-connector
Copy link

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 15, 2026

📝 Walkthrough

Walkthrough

Converts resource-binding checks in the enforced file repository from synchronous to asynchronous, adds DB-backed validation for page-type bindings via the filePages relation, and updates tests and plan documentation to reflect the changed access checks and test scaffolding.

Changes

Cohort / File(s) Summary
Enforced file repository
packages/lib/src/repositories/enforced-file-repository.ts
Converts isResourceAccessAllowed checks to async/await across getFile, getFileForUpdate, and update flows. Imports filePages and and helpers and implements DB validation for binding.type === 'page' by querying filePages to confirm file↔page linkage. Adjusts logging to reflect async checks and explicit resource-binding failure reasons.
Unit tests / DB mock (enforced file repo)
packages/lib/src/repositories/__tests__/enforced-file-repository.test.ts
Adds db.query.filePages.findFirst mock, exposes db.filePages mock shape and db.and helper. Expands tests to cover file-page link presence/absence and adjusts expectations for access checks and null-masking behavior.
Tests — notifications
packages/lib/src/__tests__/notifications.test.ts
Adds eq import from @pagespace/db. Changes test setup/teardown to per-test creation with targeted deletions using eq instead of global cleanup; adds testPage creation tied to the drive.
Tests — encryption utils
packages/lib/src/__tests__/encryption-utils.test.ts
Modifies how the auth tag is corrupted in the test (invert each hex digit) to produce a decryption failure case.
Project plan / docs
plan.md
Updates Phase 2 migration and Phase 3+ documentation: marks multiple P2 tasks as completed or skipped, expands acceptance criteria, and records test scaffolding and implementation notes. (Documentation-only changes.)
Manifest
package.json
Minor manifest lines changed (+13/-8).

Sequence Diagram(s)

(Skipped — changes are focused and do not introduce a new multi-component flow requiring visualization.)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 Async hops through binding checks so neat,
filePages confirm where file and page shall meet,
Await we must before access is complete,
Tests adapt their rhythms to a new beat,
A rabbit twirls and thumps its tiny feet.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: implementing stricter page binding validation for files, which is the core technical objective reflected in the enforced-file-repository changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing touches
  • 📝 Generate docstrings


📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ac1a79 and d47d55f.

📒 Files selected for processing (3)
  • packages/lib/src/__tests__/encryption-utils.test.ts
  • packages/lib/src/__tests__/notifications.test.ts
  • packages/lib/src/repositories/__tests__/enforced-file-repository.test.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

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

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

Files:

  • packages/lib/src/__tests__/encryption-utils.test.ts
  • packages/lib/src/repositories/__tests__/enforced-file-repository.test.ts
  • packages/lib/src/__tests__/notifications.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__/encryption-utils.test.ts
  • packages/lib/src/repositories/__tests__/enforced-file-repository.test.ts
  • packages/lib/src/__tests__/notifications.test.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • packages/lib/src/__tests__/encryption-utils.test.ts
  • packages/lib/src/repositories/__tests__/enforced-file-repository.test.ts
  • packages/lib/src/__tests__/notifications.test.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: 2witstudios
Repo: 2witstudios/PageSpace PR: 199
File: packages/lib/src/repositories/enforced-file-repository.ts:214-218
Timestamp: 2026-01-14T04:56:49.682Z
Learning: Page-bound tokens in PageSpace are primarily used for editor operations and already have page-level access verified through getUserAccessLevel(), which mitigates the security impact of the current permissive page binding check in EnforcedFileRepository.
📚 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__/encryption-utils.test.ts
🧬 Code graph analysis (1)
packages/lib/src/repositories/__tests__/enforced-file-repository.test.ts (1)
packages/db/src/index.ts (1)
  • db (20-20)
⏰ 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 (10)
packages/lib/src/repositories/__tests__/enforced-file-repository.test.ts (6)

28-31: LGTM!

The filePages query mock correctly addresses the reported test failure. The structure matches the Drizzle query pattern expected by the implementation's db.query.filePages.findFirst() call.


41-43: LGTM!

The export mocks follow the established pattern for Drizzle table/operator mocking. The and mock appropriately captures its arguments for potential assertion verification.


322-329: LGTM!

The mock correctly models a file-page linkage record with all required properties (linkedBy, linkedAt, linkSource). The test appropriately validates that page-bound tokens can access files linked to their bound page.


351-352: LGTM!

The undefined return correctly models the absence of a file-page link. The comment clearly documents the test intent for the cross-drive security scenario.


371-372: LGTM!

Good security test coverage - verifying that drive permissions are not checked when the resource binding check fails early. This ensures the implementation follows the principle of failing fast on authorization.


393-394: LGTM!

This test covers an important edge case where a page-bound token lacks the driveId claim. The null-masking behavior prevents information disclosure about file existence.

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

124-132: LGTM! Deterministic auth tag corruption.

The hex digit inversion approach (15 - val) reliably produces a different valid auth tag since each digit maps to its complement (0↔15, 1↔14, etc.). This eliminates flakiness compared to bit-flipping a single byte which could theoretically produce edge cases.

packages/lib/src/__tests__/notifications.test.ts (3)

10-10: LGTM!

The eq import is correctly added for the targeted cleanup queries in afterEach.


22-34: LGTM!

The setup changes are well-documented. Using unique factory-generated IDs per test run instead of deleting all users avoids race conditions in parallel test execution. The testPage fixture properly supports the page-reference notification tests.


36-44: The cascade delete configuration is properly set up—cleanup approach is sound.

The afterEach cleanup correctly relies on cascade deletes across all related tables:

  • Deleting the user cascades to notifications, drives (via ownerId), chatMessages, storage events, and favorites
  • Cascade also flows through drives → pages → all page-related data (chatMessages, pageTags, mentions, favorites)

The optional chaining defensively handles edge cases where beforeEach might fail, and the targeted cleanup ensures test isolation without interfering with other test files.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/lib/src/repositories/enforced-file-repository.ts`:
- Around line 215-223: The const filePageLink declared in the switch case 'page'
can leak into other cases; wrap the case body in its own block (add { ... }
around the existing case 'page' statements) so filePageLink (and any other local
const/let) is scoped to that case; keep the existing logic using
db.query.filePages.findFirst with where: and(eq(filePages.fileId, file.id),
eq(filePages.pageId, binding.id)) and return filePageLink !== undefined inside
the new block.
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ce6b7e4 and 86bca2e.

📒 Files selected for processing (2)
  • packages/lib/src/repositories/enforced-file-repository.ts
  • plan.md
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

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

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

Files:

  • packages/lib/src/repositories/enforced-file-repository.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/repositories/enforced-file-repository.ts
**/*.{ts,tsx,js,jsx,json}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier

Files:

  • packages/lib/src/repositories/enforced-file-repository.ts
🧠 Learnings (5)
📓 Common learnings
Learnt from: 2witstudios
Repo: 2witstudios/PageSpace PR: 199
File: packages/lib/src/repositories/enforced-file-repository.ts:214-218
Timestamp: 2026-01-14T04:56:49.682Z
Learning: Page-bound tokens in PageSpace are primarily used for editor operations and already have page-level access verified through getUserAccessLevel(), which mitigates the security impact of the current permissive page binding check in EnforcedFileRepository.
📚 Learning: 2026-01-14T04:56:49.682Z
Learnt from: 2witstudios
Repo: 2witstudios/PageSpace PR: 199
File: packages/lib/src/repositories/enforced-file-repository.ts:214-218
Timestamp: 2026-01-14T04:56:49.682Z
Learning: Page-bound tokens in PageSpace are primarily used for editor operations and already have page-level access verified through getUserAccessLevel(), which mitigates the security impact of the current permissive page binding check in EnforcedFileRepository.

Applied to files:

  • packages/lib/src/repositories/enforced-file-repository.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/repositories/enforced-file-repository.ts
📚 Learning: 2025-12-23T18:49:41.966Z
Learnt from: CR
Repo: 2witstudios/PageSpace PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-23T18:49:41.966Z
Learning: Applies to apps/web/src/**/*.{ts,tsx} : For database access, always use Drizzle client from `pagespace/db`: `import { db, pages } from 'pagespace/db';`

Applied to files:

  • packages/lib/src/repositories/enforced-file-repository.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:

  • packages/lib/src/repositories/enforced-file-repository.ts
🧬 Code graph analysis (1)
packages/lib/src/repositories/enforced-file-repository.ts (2)
packages/db/src/index.ts (3)
  • db (20-20)
  • and (8-8)
  • eq (8-8)
packages/db/src/schema/storage.ts (1)
  • filePages (23-38)
🪛 Biome (2.1.2)
packages/lib/src/repositories/enforced-file-repository.ts

[error] 217-222: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

⏰ 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 (5)
packages/lib/src/repositories/enforced-file-repository.ts (3)

10-10: LGTM!

The import correctly adds filePages, eq, and and from @pagespace/db, which are necessary for the new page binding validation query.


103-112: LGTM!

The async resource binding check is properly awaited, and the existing security pattern of returning null (rather than throwing) to prevent file enumeration is preserved.


250-259: LGTM!

The async resource binding check in getFileForUpdate correctly mirrors the pattern in getFile, maintaining consistent security behavior across both code paths.

plan.md (2)

1093-1099: LGTM!

The P2-T1 acceptance criteria correctly reflects the completed sessions schema deployment. Status update is consistent with the broader Phase 2 completion noted in the document.


1751-1763: LGTM!

The P2-T7 acceptance criteria and status are consistent with the code changes. The stricter page binding validation via filePages lookup directly supports the "Resource binding enforced" criterion.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines 215 to +223
case 'page':
// Page binding requires checking file-page associations
// For now, we allow if the file is in the same drive
// TODO: Add filePages lookup for stricter validation
return this.ctx.driveId !== undefined && this.ctx.driveId === file.driveId;
// Page binding: file must be linked to the bound page via filePages table
const filePageLink = await db.query.filePages.findFirst({
where: and(
eq(filePages.fileId, file.id),
eq(filePages.pageId, binding.id)
),
});
return filePageLink !== undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Wrap switch case declaration in a block scope.

The const filePageLink declaration inside the switch case can leak to other cases, which Biome correctly flags. Wrap the case body in a block to restrict scope.

🔧 Proposed fix
       case 'page':
-        // Page binding: file must be linked to the bound page via filePages table
-        const filePageLink = await db.query.filePages.findFirst({
-          where: and(
-            eq(filePages.fileId, file.id),
-            eq(filePages.pageId, binding.id)
-          ),
-        });
-        return filePageLink !== undefined;
+        {
+          // Page binding: file must be linked to the bound page via filePages table
+          const filePageLink = await db.query.filePages.findFirst({
+            where: and(
+              eq(filePages.fileId, file.id),
+              eq(filePages.pageId, binding.id)
+            ),
+          });
+          return filePageLink !== undefined;
+        }
📝 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
case 'page':
// Page binding requires checking file-page associations
// For now, we allow if the file is in the same drive
// TODO: Add filePages lookup for stricter validation
return this.ctx.driveId !== undefined && this.ctx.driveId === file.driveId;
// Page binding: file must be linked to the bound page via filePages table
const filePageLink = await db.query.filePages.findFirst({
where: and(
eq(filePages.fileId, file.id),
eq(filePages.pageId, binding.id)
),
});
return filePageLink !== undefined;
case 'page':
{
// Page binding: file must be linked to the bound page via filePages table
const filePageLink = await db.query.filePages.findFirst({
where: and(
eq(filePages.fileId, file.id),
eq(filePages.pageId, binding.id)
),
});
return filePageLink !== undefined;
}
🧰 Tools
🪛 Biome (2.1.2)

[error] 217-222: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

🤖 Prompt for AI Agents
In `@packages/lib/src/repositories/enforced-file-repository.ts` around lines 215 -
223, The const filePageLink declared in the switch case 'page' can leak into
other cases; wrap the case body in its own block (add { ... } around the
existing case 'page' statements) so filePageLink (and any other local const/let)
is scoped to that case; keep the existing logic using
db.query.filePages.findFirst with where: and(eq(filePages.fileId, file.id),
eq(filePages.pageId, binding.id)) and return filePageLink !== undefined inside
the new block.

@2witstudios
Copy link
Owner Author

Code review

Found 1 issue:

  1. Missing mock for db.query.filePages causes test failures - The implementation uses db.query.filePages.findFirst() at line 217, but the test mock (lines 22-39) only mocks db.query.files, not db.query.filePages. When tests with resourceType: 'page' hit the case 'page': branch, db.query.filePages is undefined, causing TypeError: Cannot read properties of undefined (reading 'findFirst').

// Mock @pagespace/db
vi.mock('@pagespace/db', () => ({
db: {
query: {
files: {
findFirst: vi.fn(),
},
},
update: vi.fn(() => ({
set: vi.fn(() => ({
where: vi.fn(() => ({
returning: vi.fn(),
})),
})),
})),
},
files: { id: 'id' },
eq: vi.fn((a, b) => ({ type: 'eq', a, b })),
}));

Fix: Add filePages to the mock:

db: {
  query: {
    files: { findFirst: vi.fn() },
    filePages: { findFirst: vi.fn() },  // Add this
  },
}

And mock db.query.filePages.findFirst in the page-bound tests to return appropriate values (a file-page link object for allowed access, undefined for denied access).

🤖 Generated with Claude Code

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

2witstudios and others added 2 commits January 15, 2026 08:30
The test mock was missing db.query.filePages, causing TypeError when
tests hit the 'case page' branch in isResourceAccessAllowed(). Added:
- filePages to db.query mock
- filePages and 'and' to mock exports
- Proper filePages.findFirst mocking in page-bound test cases

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add missing filePages mock properties (linkedBy, linkedAt, linkSource)
- Fix flaky encryption auth tag test by inverting all hex digits instead of just first byte
- Fix flaky notification tests by using targeted cleanup instead of deleting all users

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@2witstudios 2witstudios merged commit 4b5dd39 into master Jan 15, 2026
3 checks passed
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.

3 participants