Skip to content

Tech debt & feature gaps: auth, persistence, cleanup, docs - #508

Merged
2witstudios merged 2 commits into
masterfrom
techdebt/features
Feb 9, 2026
Merged

2witstudios merged 2 commits into
masterfrom
techdebt/features

Conversation

@2witstudios

@2witstudios 2witstudios commented Feb 9, 2026 •

Copy link
Copy Markdown
Owner

Summary

Fixes 7 open issues covering auth policy gaps, dead code, missing persistence, cache hardening, and security documentation.

Fixes #437, Fixes #433, Fixes #430, Fixes #432, Fixes #438, Fixes #424, Fixes #423

Test plan

  • pnpm typecheck — all packages pass (web TS6053 errors are pre-existing missing .next/types)
  • pnpm --filter web lint — zero warnings or errors
  • pnpm vitest run — 208/209 files pass (1 pre-existing DB connection failure in admin-role-version.test.ts)
  • New tests: calendar auth (7), configOverrides (2), conversation PATCH (3), drive access tracking fixtures (2)
  • Manual: edit calendar event as drive admin/owner — verify 200
  • Manual: PATCH page-agent conversation title — verify persistence across reload
  • Manual: switch drives in DriveSwitcher — verify Recent section reorders
  • pnpm db:generate for Tech debt: Reconcile drive invitation model with auto-accepted member adds #432 migration after snapshot collision is resolved on master

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Conversation titles now persist and save properly.
    • Recent drives list now sorted by last accessed time.
    • Drive administrators can now edit events in their drives.
    • Direct-add model for drive members replaces invitation system.
  • Improvements

    • Permission validation now bypasses cache for critical operations to ensure up-to-date access checks.

, #430, #432, #438, #424, #423)

Fixes seven open issues covering auth policy, dead code removal, missing
persistence, cache hardening, and security documentation.

- #437: Calendar event edit now allows drive admins/owners, not just creator
- #433: Integration configOverrides.rateLimit passed to rate limiter
- #430: Page-agent conversation PATCH persists title via upsert
- #432: Remove unused driveInvitations table and retention cleanup
- #438: DriveSwitcher sorts recent drives by real lastAccessedAt
- #424: Document permission cache TTL, audit and fix bypassCache on mutations
- #423: Document desktop MCP trust model exception to zero-trust

Fixes #437, Fixes #433, Fixes #430, Fixes #432, Fixes #438, Fixes #424, Fixes #423

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

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

coderabbitai Bot commented Feb 9, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@2witstudios has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 minutes and 45 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

This PR addresses five linked technical debts: (1) implements persistent title updates for page-agent conversations via a new upsert repository method, (2) adds drive-admin authorization to calendar event editing with comprehensive tests, (3) extends permission-check functions with a bypassCache option for sensitive mutations, (4) introduces drive last-access tracking with a new endpoint and updates drive-switcher ordering, and (5) removes obsolete driveInvitations schema and related retention logic, updating docs to reflect direct-add membership model. Additionally, it adds connection-level rate-limit overrides for integrations and comprehensive security documentation for permission caching and desktop MCP trust model.

Changes

Cohort / File(s) Summary
Conversation Title Persistence
apps/web/src/app/api/ai/page-agents/.../conversations/.../route.ts, apps/web/src/app/api/ai/page-agents/.../conversations/.../__tests__/route.test.ts, apps/web/src/lib/repositories/conversation-repository.ts
Adds upsertConversationTitle method to persist conversation titles; PATCH endpoint now saves to database and returns persisted ID and title instead of placeholders; tests updated and new test added for upsert verification.
Calendar Event Edit Authorization
apps/web/src/app/api/calendar/events/[eventId]/__tests__/can-edit-event.test.ts, apps/web/src/app/api/calendar/events/[eventId]/route.ts
Implements drive-admin/owner authorization for event editing alongside creator-only access; adds comprehensive contract tests covering creator, drive admin, drive owner, and non-member scenarios; updates error messages to be role-agnostic.
Permission Cache Bypass Enhancement
packages/lib/src/permissions/permissions-cached.ts, apps/web/src/app/api/channels/[pageId]/*, apps/web/src/app/api/debug/chat-messages/*, apps/web/src/app/api/pages/*/route.ts, apps/web/src/app/api/trash/[pageId]/route.ts, apps/web/src/services/api/*.ts
Adds optional bypassCache: true parameter to permission-check functions (canUserEditPage, canUserDeletePage, etc.); applied to sensitive mutations across multiple routes; enables fresh authorization evaluation for write operations.
Drive Last-Access Tracking
apps/web/src/components/layout/navbar/DriveSwitcher.tsx, apps/web/src/app/api/drives/[driveId]/access/route.ts, apps/web/src/app/api/drives/[driveId]/__tests__/route.test.ts, apps/web/src/app/api/drives/__tests__/route.test.ts, packages/lib/src/services/drive-service.ts, packages/lib/src/types.ts
Adds lastAccessedAt field to Drive type and DriveWithAccess; new POST /drives/[driveId]/access endpoint to track access; DriveSwitcher orders recent drives by last access time (top 5); service method updateDriveLastAccessed updates timestamp in driveMembers table; fixtures updated to include field.
Drive Invitations Removal
packages/db/src/schema/members.ts, packages/lib/src/compliance/retention/retention-engine.ts, packages/lib/src/compliance/retention/retention-engine.test.ts, docs/2.0-architecture/2.2-backend/database.md
Removes obsolete driveInvitations table, InvitationStatus enum, and driveInvitationsRelations from schema; removes cleanupExpiredDriveInvitations function from retention engine; eliminates related test and reduces cleanup function count.
Integrations Rate-Limit Configuration
packages/lib/src/integrations/saga/execute-tool.ts, packages/lib/src/integrations/saga/execute-tool.test.ts
Adds optional configOverrides field to ConnectionWithProvider to carry per-connection rate-limit settings; rate-limit calculation now reads from overrides if present; tests added to validate override propagation.
Security & Architecture Documentation
docs/security/desktop-mcp-trust-model.md, docs/security/permission-cache-threat-model.md, docs/2.0-architecture/2.2-backend/permissions.md, docs/security/zero-trust-architecture.md, docs/features/local-mcp-servers.md
New comprehensive threat-model documents for permission cache (L1/L2 tiers, TTL, invalidation strategy) and desktop MCP trust model (security boundaries, defensive measures, user responsibilities); updated permissions architecture with caching strategy details; added zero-trust exception for desktop MCP; added reference link in MCP security warnings.

Sequence Diagram(s)

sequenceDiagram
    participant UI as DriveSwitcher Component
    participant API as POST /drives/[driveId]/access
    participant Service as Drive Service
    participant DB as Database<br/>(driveMembers)
    participant Response as JSON Response

    UI->>API: POST /api/drives/{driveId}/access<br/>(authenticated, CSRF protected)
    activate API
    API->>API: Extract driveId from route params
    API->>API: Authenticate request<br/>(session or MCP)
    alt Auth Successful
        API->>Service: updateDriveLastAccessed<br/>(userId, driveId)
        activate Service
        Service->>DB: UPDATE driveMembers<br/>SET lastAccessedAt = NOW()<br/>WHERE userId=? AND driveId=?
        activate DB
        DB-->>Service: Confirmation
        deactivate DB
        Service-->>API: Void (Promise)
        deactivate Service
        API->>Response: { success: true }
    else Auth Failed
        API->>Response: { error, statusCode }
    else Error Occurs
        API->>Response: { error: 'An error occurred',<br/>statusCode: 500 }
    end
    deactivate API
    Response-->>UI: JSON Response
    Note over UI: Fire-and-forget:<br/>silently ignore errors
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 The burrows run deep with fresh access trails,
Conversations now persist through data's veils,
Cache bypasses guard the sensitive gates,
While old invitations fade to deprecated states!
Admins can edit calendars true,
Our warren of logic, rebuilt anew! 🌟

🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title broadly references multiple tech debt and feature areas but lacks specificity about the main change, using vague phrasing that doesn't clearly convey the primary scope. Consider a more specific title focusing on the primary change, such as 'Add cache-bypass hardening, finalize event auth, and document desktop MCP trust model' or 'Consolidate auth policies, permission cache bypass, and security documentation'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed All seven linked issues (#437, #433, #430, #432, #438, #424, #423) are addressed with corresponding code changes, tests, and documentation updates that fulfill their stated acceptance criteria.
Out of Scope Changes check ✅ Passed All changes directly address the seven linked issues; no unrelated modifications detected in the changeset.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch techdebt/features

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (2)
apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/route.ts (1)

45-65: ⚠️ Potential issue | 🟠 Major

Missing input validation for title.

title is destructured from the request body without any validation. If the client sends undefined, null, a non-string value, or an excessively long string, it flows straight into the DB upsert. Since this is the endpoint's core purpose, title should be validated as a required, non-empty string with a reasonable length cap.

Proposed fix
     // Parse request body
     const body = await request.json();
     const { title } = body;
 
+    if (typeof title !== 'string' || title.trim().length === 0) {
+      return NextResponse.json(
+        { error: 'Title is required and must be a non-empty string' },
+        { status: 400 }
+      );
+    }
+
+    if (title.length > 255) {
+      return NextResponse.json(
+        { error: 'Title must be 255 characters or fewer' },
+        { status: 400 }
+      );
+    }
+
     // Validate that the conversation exists (has at least one active message)

You'll also want to add corresponding test cases for bad/missing title in the test file.

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

138-148: ⚠️ Potential issue | 🟠 Major

Owned drives will always have null lastAccessedAt — access tracking fails silently because owners lack driveMembers rows.

driveLastAccessed is populated solely from driveMembers rows (lines 85-110). When a user owns a drive and calls the access-tracking POST endpoint at /api/drives/[driveId]/access, updateDriveLastAccessed attempts to UPDATE driveMembers where userId and driveId match (lines 386-392). Since the drive owner is never added to driveMembers during drive creation (line 169-179 only inserts into the drives table), this UPDATE affects 0 rows and silently fails. Consequently, driveLastAccessed.get(drive.id) remains undefined, defaulting to null (line 142), and owned drives never appear in the "Recent" section based on recency.

To fix this, either:

  • Add the owner to driveMembers automatically when creating a drive, or
  • Update updateDriveLastAccessed to handle owned drives separately (using the drives.ownerId field)
🤖 Fix all issues with AI agents
In `@docs/2.0-architecture/2.2-backend/permissions.md`:
- Around line 276-277: Update this section to use the same function names as
Section 5: replace any occurrences of grantPagePermission and
revokePagePermission with grantPagePermissions and revokePagePermissions
(including prose, examples, and any referenced invalidation function names) so
the docs consistently reference grantPagePermissions / revokePagePermissions
everywhere; ensure accompanying sentences that mention “invalidation functions”
call out the pluralized names too.

In `@docs/security/permission-cache-threat-model.md`:
- Line 62: The sentence currently says "Sub-millisecond" while also stating a
1–5ms range; update the text so they match by either changing the phrase
"Sub-millisecond" to "few milliseconds" (or "1–5 milliseconds") or by adjusting
the numeric range to be <1ms; specifically edit the line containing the quoted
phrase "Timing differences are sub-millisecond" and the adjacent numeric range
"1-5ms" so both use consistent wording (e.g., "few milliseconds (≈1–5 ms)") for
clarity.
🧹 Nitpick comments (7)
docs/security/desktop-mcp-trust-model.md (1)

36-36: Tighten phrasing.

“Outside of” is redundant here—“outside” reads cleaner.

✏️ Proposed tweak
- Modify PageSpace configuration outside of their own tool responses -- the MCP manager controls config read/write
+ Modify PageSpace configuration outside their own tool responses -- the MCP manager controls config read/write
docs/security/zero-trust-architecture.md (1)

1470-1472: Vary repeated sentence starts.

Three consecutive bullets start with “MCP servers do…”. Consider rephrasing one or two for readability.

packages/lib/src/compliance/retention/retention-engine.test.ts (1)

1-5: Minor: consider consolidating the three separate @pagespace/db imports.

Lines 2–5 have three separate import statements from @pagespace/db. This is pre-existing, but since line 3 was touched in this change, it could be a good opportunity to merge them.

-import { db, users, sessions, socketTokens, verificationTokens, emailUnsubscribeTokens, pageVersions, driveBackups, aiUsageLogs } from '@pagespace/db';
-import { pagePermissions } from '@pagespace/db';
-import { pulseSummaries } from '@pagespace/db';
-import { drives, pages } from '@pagespace/db';
+import { db, users, sessions, socketTokens, verificationTokens, emailUnsubscribeTokens, pageVersions, driveBackups, aiUsageLogs, pagePermissions, pulseSummaries, drives, pages } from '@pagespace/db';
apps/web/src/components/layout/navbar/DriveSwitcher.tsx (1)

110-111: Local store not updated after tracking access — stale recency until next fetch.

The fire-and-forget POST updates the server, but the local drives array in the Zustand store still holds the old lastAccessedAt. If the user re-opens the switcher without a drives refetch, the "Recent" ordering won't reflect the access they just made. Consider optimistically updating the drive's lastAccessedAt in the store (e.g., via useDriveStore.getState()) after the POST, so the dropdown is immediately consistent.

apps/web/src/app/api/drives/[driveId]/access/route.ts (1)

18-19: Consider logging the caught error for observability.

The catch block discards error details. Since this is a new endpoint, adding a log line would help diagnose issues in production without changing the 500 response.

🔧 Suggested improvement
-  } catch {
+  } catch (error) {
+    console.error('Failed to update drive access time:', error);
     return NextResponse.json({ error: 'Failed to update access time' }, { status: 500 });
   }
packages/lib/src/services/drive-service.ts (2)

37-37: Date | string | null is a loose type for a service-layer interface.

The service layer (backed by Drizzle) should consistently return Date | null. The string variant leaks serialization concerns into the domain type. The client-facing Drive type in types.ts correctly uses string | null for the API boundary.

♻️ Tighten the service-layer type
-  lastAccessedAt: Date | string | null;
+  lastAccessedAt: Date | null;

310-317: getDriveWithAccess always returns lastAccessedAt: null — actual value not fetched.

Unlike listAccessibleDrives which queries driveMembers.lastAccessedAt, this function hardcodes null. If this endpoint is ever used where lastAccessedAt matters, the value will be silently missing. If this is intentional (only the list view needs it), a comment would clarify the design choice.

Comment thread docs/2.0-architecture/2.2-backend/permissions.md
Comment thread docs/security/permission-cache-threat-model.md Outdated
- Add title validation (type, empty, length) to conversation PATCH with 3 tests
- Fix drive owner access tracking: upsert driveMembers row for owners
- Optimistically update local store on drive switch for immediate recency
- Log errors in drive access endpoint for observability
- Fix function name mismatch in permissions docs (plural → singular)
- Fix timing claim inconsistency in permission cache threat model
- Tighten DriveWithAccess.lastAccessedAt to Date | null (service layer)
- Consolidate split @pagespace/db imports in retention engine
- Vary sentence starts in zero-trust MCP exception section
- Fix "outside of" → "outside" in MCP trust model doc
- Add clarifying comment on getDriveWithAccess hardcoded lastAccessedAt

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

Copy link
Copy Markdown
Owner Author

Review Feedback Addressed — de56b25

All actionable comments and nitpicks from the CodeRabbit review have been addressed in commit de56b25. Here's the summary:

Major Issues (2/2 resolved)

1. Missing title validation (conversation PATCH)
Added type check (typeof title !== 'string'), empty-string check (.trim().length === 0), and length cap (255 chars) before the DB upsert. Three new test cases added covering missing title, whitespace-only title, and over-length title.

  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/route.ts
  • apps/web/src/app/api/ai/page-agents/[agentId]/conversations/[conversationId]/__tests__/route.test.ts

2. Owned drives lack driveMembers rows
updateDriveLastAccessed now tries the UPDATE first; if 0 rows affected, verifies the user is the drive owner before inserting an OWNER membership row via upsert. This prevents both the silent no-op for owners and the privilege escalation risk of blindly inserting.

  • packages/lib/src/services/drive-service.ts

Minor Issues (2/2 resolved)

3. Doc function name mismatch — Fixed Section 5 to use singular grantPagePermission/revokePagePermission (matching actual exports).

4. Timing claim inconsistency — Changed "sub-millisecond" to "a few milliseconds" to match the 1-5 ms range.

Nitpicks (7/7 addressed)

# Issue Fix
5 "outside of" phrasing Changed to "outside"
6 Repeated "MCP servers do..." Varied sentence structure
7 Split @pagespace/db imports Consolidated into single imports in both source and test
8 Local store not updated after tracking Added optimistic updateDrive() call before fire-and-forget POST
9 Error not logged in access route Added loggers.api.error() with error details
10 Date | string | null type too loose Tightened to Date | null in service layer
11 Hardcoded lastAccessedAt: null unexplained Added clarifying comment

Verification

  • 331 unit tests passing (up from 328 — 3 new validation tests)
  • pnpm typecheck (full build): all 10 tasks successful
  • All CI checks were green before this push

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