Skip to content

Upload files - #2

Merged
2witstudios merged 10 commits into
masterfrom
upload-files
Sep 18, 2025
Merged

2witstudios merged 10 commits into
masterfrom
upload-files

Conversation

@2witstudios

Copy link
Copy Markdown
Owner

No description provided.

@2witstudios
2witstudios merged commit 2de910c into master Sep 18, 2025
@2witstudios
2witstudios deleted the upload-files branch September 18, 2025 15:17
2witstudios added a commit that referenced this pull request Dec 22, 2025
- Use Drizzle count() instead of .length for efficient queries
- Add operation validation against known valid operations
- Fix unsafe null type assertion in RollbackPreview
- Re-export RollbackContext type for API route usage
- Wrap AI undo mutations in database transaction
- Use switch statement for context determination

Addresses PR #118 review issues #2, #16, #18, #19, #23

🤖 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 Dec 23, 2025
Fixes Major issue #2 from PR #118 review:
- Move rollbackFromActivityId from metadata to top-level field
- Move contentFormat from metadata to top-level field
- Add rollback source snapshot fields to ActivityLogInput interface
- Update logRollbackActivity to accept and pass snapshot fields

Fields are now stored in dedicated database columns instead of being
nested in the metadata JSONB, enabling proper indexing and querying.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai coderabbitai Bot mentioned this pull request Dec 23, 2025
5 tasks
2witstudios added a commit that referenced this pull request Dec 23, 2025
* feat: add version history and rollback functionality

Implement comprehensive version history browsing and rollback capabilities
for PageSpace, allowing users to restore resources to previous states.

## Schema Changes
- Add 'rollback' operation to activity_operation enum
- Add rollbackFromActivityId and contentFormat fields to activity_logs
- Create retention_policies table for plan-based history retention

## Core Features
- RBAC-based rollback permissions (edit access = rollback access)
- Resource-specific rollback handlers (pages, drives, agents, etc.)
- Plan-based retention limits (7/30/90/unlimited days)
- Rollback creates new activity entry (history never erased)

## API Endpoints
- GET /api/activities/[activityId] - Single activity with rollback eligibility
- POST /api/activities/[activityId]/rollback - Execute rollback
- GET /api/pages/[pageId]/history - Page version history
- GET /api/drives/[driveId]/history - Drive version history (admin)

## UI Components
- VersionHistoryPanel - Slide-out panel with timeline and filters
- VersionHistoryItem - Activity item with "Restore" action
- RollbackConfirmDialog - Confirmation modal with warnings

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

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

* fix: complete rollback handler implementations and fix lint errors

- Fully implement rollbackPermissionChange for grant/revoke/update operations
- Fully implement rollbackMemberChange for add/remove/role change operations
- Fully implement rollbackRoleChange for create/delete/update operations
- Fix lint errors: remove unused imports, fix useEffect dependencies
- Add package exports for @pagespace/lib/permissions and @pagespace/lib/monitoring
- Fix useToast import path to use correct hook location

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

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

* feat: add undo AI changes feature for conversations

Add ability to undo AI changes from a specific message point:
- Preview what will be affected before undoing
- Two modes: revert conversation only OR revert with all tool changes
- Activity logging for audit compliance

New files:
- ai-undo-service.ts: preview and execute functions
- UndoAiChangesDialog.tsx: confirmation dialog with mode selection
- /api/ai/chat/messages/[messageId]/undo: GET preview, POST execute

Changes:
- Add conversation_undo operations to activity schema
- Add message rollback handler to rollback-service
- Wire undo button to MessageActionButtons and MessageRenderer

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

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

* fix: add missing radio-group UI component

Required for UndoAiChangesDialog mode selection.

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

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

* fix: add @radix-ui/react-radio-group dependency

Required for radio-group UI component used in UndoAiChangesDialog.

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

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

* fix: restore correct @electron/node-gyp resolution in lockfile

The previous lockfile had a malformed git SSH URL that broke CI:
- Before: git+https://git@github.com:electron/node-gyp.git (broken)
- After: https://codeload.github.com/electron/node-gyp/tar.gz/... (works)

Restored lockfile and re-ran pnpm install to properly resolve dependencies.

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

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

* fix: add critical rules to CLAUDE.md and fix retention tier order

- Add CRITICAL rules for package manager (always pnpm, never npm)
- Add CRITICAL rules for database migrations (never manually edit)
- Fix retention days: founder=90, business=unlimited (was swapped)

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

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

* fix(db): add missing default and rollback index

- Add DEFAULT now() for retention_policies.updatedAt column
- Add index on activity_logs.rollbackFromActivityId for queries
- Create migration 0027_fix_retention_updated_at.sql

Addresses PR #118 review issues #10, #20

* fix(lib/permissions): add block scoping and eligibility helper

- Wrap switch case declarations in blocks to prevent variable leakage
- Add isActivityEligibleForRollback() helper for DRY rollback checks
- Export helper for use in history API routes

Addresses PR #118 review issues #5, #13

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

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

* fix(lib/monitoring): add rollback fields to ActivityLogInput

- Add contentFormat field for content type tracking
- Add rollbackFromActivityId for rollback chain references
- Pass fields as top-level properties to logActivity

Addresses PR #118 review issue #3

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

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

* fix(api): add authorization and resourceType filtering

- Add permission check before returning activity details
- Implement resourceType query parameter filtering in history routes
- Use shared isActivityEligibleForRollback helper
- Add case-insensitive resource type matching

Addresses PR #118 review issues #6, #7, #13

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

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

* fix(services): improve type safety and add transactions

- Use Drizzle count() instead of .length for efficient queries
- Add operation validation against known valid operations
- Fix unsafe null type assertion in RollbackPreview
- Re-export RollbackContext type for API route usage
- Wrap AI undo mutations in database transaction
- Use switch statement for context determination

Addresses PR #118 review issues #2, #16, #18, #19, #23

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

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

* fix(ui): add CSRF protection and improve error handling

- Use post() with CSRF token for rollback requests
- Add error toast for failed preview fetch
- Handle non-JSON error responses gracefully
- Fix non-existent postWithAuth import to use post
- Remove re-throw after toast notification
- Fix redundant ternary for buttonSize

Addresses PR #118 review issues #1, #8, #11, #17, #22, NEW

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

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

* fix(db): address schema issues from PR #118 review

Critical & Major fixes:
- Add missing .defaultNow() to retention_policies.updatedAt
  (fixes schema/migration mismatch that would cause insert failures)
- Add contentFormatEnum for type-safe content format validation
  (prevents invalid values during rollback parsing)
- Add CHECK constraint: retentionDays >= -1 (where -1 = unlimited)
- Add rollback source snapshot fields for audit trail preservation:
  - rollbackSourceOperation: captures source activity type
  - rollbackSourceTimestamp: captures when source change occurred
  - rollbackSourceTitle: captures resource title at time of change

These denormalized fields survive retention policy deletion, ensuring
complete audit trails even when source activities are purged.

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

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

* fix(lib): move rollback fields to top-level in activity logger

Fixes Major issue #2 from PR #118 review:
- Move rollbackFromActivityId from metadata to top-level field
- Move contentFormat from metadata to top-level field
- Add rollback source snapshot fields to ActivityLogInput interface
- Update logRollbackActivity to accept and pass snapshot fields

Fields are now stored in dedicated database columns instead of being
nested in the metadata JSONB, enabling proper indexing and querying.

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

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

* fix(services): add transaction support to rollback service

Enables atomic rollback operations (Issue #7 from PR #118 review):
- Add optional transaction parameter to executeRollback()
- Update all internal rollback functions to accept database parameter
- Pass rollback source snapshot fields to logRollbackActivity

When a transaction is provided, all database operations use it instead
of the default db connection, enabling atomic rollback + message
deletion in AI undo operations.

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

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

* fix(services): improve atomicity and context logic in AI undo

Fixes Issues #5 and #7 from PR #118 review:

Context determination (#5):
- Align executeAiUndo context logic with previewAiUndo
- Use 'ai_tool' context for pages (all activities here are AI-generated)
- Simplifies from switch statement to direct assignment with drive check

Transaction atomicity (#7):
- Wrap rollbacks AND message deletion in single transaction
- Pass transaction to executeRollback for atomic operations
- If any operation fails, entire undo is rolled back

This ensures users don't end up in inconsistent states where messages
are deleted but only some changes were reverted.

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

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

* fix(api): use Zod schema for undo route validation

Fixes Issue #6 from PR #118 review:
- Replace manual mode validation with Zod schema
- Aligns with codebase patterns (see rollback route, history route)
- Provides type-safe body parsing with proper TypeScript inference

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

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

* fix: address CodeRabbit review feedback

- Add CHECK constraint to retentionPolicies schema definition
  (aligns Drizzle schema with existing migration constraint)
- Change AI undo to all-or-nothing transaction semantics
  (any rollback failure aborts entire operation)

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

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

* fix(db): add subscriptionTier enum for retention policies

- Create subscription_tier pgEnum with 'free', 'pro', 'business', 'founder'
- Convert retentionPolicies.subscriptionTier from text to enum
- Adds DB-level validation to prevent invalid tier values

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

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

* fix: address final CodeRabbit review comments

- Add comments explaining rollbackFromActivityId intentionally lacks FK
  (allows provenance to survive source activity deletion for audit)
- Add TODO note for contentSnapshot storage considerations
- Optimize message fetching: include createdAt in AiUndoPreview to
  avoid double database query

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

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

* fix(db): add USING clause for text-to-enum cast

PostgreSQL requires explicit cast when converting text column to enum type.

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

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

* fix(ai): support undo for global assistant messages by checking both messages tables and updating permission logic

* fix(ai): resolve lint errors in undo route and tests

* test: add contract tests for version history rollback

- Add route tests for /api/activities/[activityId]/rollback (12 tests)
- Add route tests for /api/pages/[pageId]/history (21 tests)
- Add service tests for ai-undo-service (16 tests) with @scaffold label
- Add service tests for rollback-service (28 tests) with @scaffold label
- Add permission tests for rollback-permissions (69 tests)
- Fix undo route test: remove duplicate test expecting wrong status
- Add fake timers to history route tests for deterministic dates

Per rubric v2: service tests use @scaffold labels for ORM chain mocks
pending repository seam refactoring.

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

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

* feat(ui): add rollback capability to activity views

Add "Restore this version" action to activity items in:
- ActivityDashboard (middle panel for /dashboard/activity and drive activity)
- SidebarActivityTab (right sidebar context-aware activity feed)

Changes:
- ActivityItem: Add hover dropdown menu with rollback action and confirmation dialog
- ActivityTimeline: Pass context and onRollback handler to items
- ActivityDashboard: Handle rollback with proper context mapping (user→user_dashboard, drive→drive)
- SidebarActivityTab: Add rollback UI with context-aware scoping (page/drive/user_dashboard)

Rollback is properly scoped by context to prevent unintended changes.

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

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

* test: add activity logger tests for rollback and undo operations

Add compliance tests for:
- logRollbackActivity: validates rollback operation logging with source activity reference
- logConversationUndo: validates conversation undo logging for both modes

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

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

* fix(rollback): support create operation rollback and global messages

- Add 'create' as rollbackable operation (trash resource to undo creation)
- Support both global messages and page chat messages in rollback
- Fix AI undo timing to include tool calls from preceding message
- Handle message create rollback by deactivating the message

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

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

* fix: address PR #118 code review feedback

- Fix build failure: replace dynamic db.query[] bracket notation with
  explicit conditional to resolve TypeScript union type error
- Extract checkUndoPermissions() helper to eliminate 31 lines of
  duplicated permission logic between GET and POST handlers
- Add existingPreview parameter to executeAiUndo() to avoid redundant
  preview computation (was being called twice per request)
- Simplify context determination: remove redundant isAiGenerated check
  since query already filters for it
- Update tests to match new 4-parameter executeAiUndo signature
- Add clarifying comment explaining partial failure tests document
  defensive handling (actual impl uses all-or-nothing transaction)

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

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

* fix: resolve header loading and rendering issues across all page types

* style: add truncation for page titles and breadcrumb items

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2witstudios added a commit that referenced this pull request Dec 23, 2025
* feat: add version history and rollback functionality

Implement comprehensive version history browsing and rollback capabilities
for PageSpace, allowing users to restore resources to previous states.

## Schema Changes
- Add 'rollback' operation to activity_operation enum
- Add rollbackFromActivityId and contentFormat fields to activity_logs
- Create retention_policies table for plan-based history retention

## Core Features
- RBAC-based rollback permissions (edit access = rollback access)
- Resource-specific rollback handlers (pages, drives, agents, etc.)
- Plan-based retention limits (7/30/90/unlimited days)
- Rollback creates new activity entry (history never erased)

## API Endpoints
- GET /api/activities/[activityId] - Single activity with rollback eligibility
- POST /api/activities/[activityId]/rollback - Execute rollback
- GET /api/pages/[pageId]/history - Page version history
- GET /api/drives/[driveId]/history - Drive version history (admin)

## UI Components
- VersionHistoryPanel - Slide-out panel with timeline and filters
- VersionHistoryItem - Activity item with "Restore" action
- RollbackConfirmDialog - Confirmation modal with warnings

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

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

* fix: complete rollback handler implementations and fix lint errors

- Fully implement rollbackPermissionChange for grant/revoke/update operations
- Fully implement rollbackMemberChange for add/remove/role change operations
- Fully implement rollbackRoleChange for create/delete/update operations
- Fix lint errors: remove unused imports, fix useEffect dependencies
- Add package exports for @pagespace/lib/permissions and @pagespace/lib/monitoring
- Fix useToast import path to use correct hook location

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

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

* feat: add undo AI changes feature for conversations

Add ability to undo AI changes from a specific message point:
- Preview what will be affected before undoing
- Two modes: revert conversation only OR revert with all tool changes
- Activity logging for audit compliance

New files:
- ai-undo-service.ts: preview and execute functions
- UndoAiChangesDialog.tsx: confirmation dialog with mode selection
- /api/ai/chat/messages/[messageId]/undo: GET preview, POST execute

Changes:
- Add conversation_undo operations to activity schema
- Add message rollback handler to rollback-service
- Wire undo button to MessageActionButtons and MessageRenderer

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

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

* fix: add missing radio-group UI component

Required for UndoAiChangesDialog mode selection.

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

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

* fix: add @radix-ui/react-radio-group dependency

Required for radio-group UI component used in UndoAiChangesDialog.

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

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

* fix: restore correct @electron/node-gyp resolution in lockfile

The previous lockfile had a malformed git SSH URL that broke CI:
- Before: git+https://git@github.com:electron/node-gyp.git (broken)
- After: https://codeload.github.com/electron/node-gyp/tar.gz/... (works)

Restored lockfile and re-ran pnpm install to properly resolve dependencies.

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

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

* fix: add critical rules to CLAUDE.md and fix retention tier order

- Add CRITICAL rules for package manager (always pnpm, never npm)
- Add CRITICAL rules for database migrations (never manually edit)
- Fix retention days: founder=90, business=unlimited (was swapped)

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

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

* fix(db): add missing default and rollback index

- Add DEFAULT now() for retention_policies.updatedAt column
- Add index on activity_logs.rollbackFromActivityId for queries
- Create migration 0027_fix_retention_updated_at.sql

Addresses PR #118 review issues #10, #20

* fix(lib/permissions): add block scoping and eligibility helper

- Wrap switch case declarations in blocks to prevent variable leakage
- Add isActivityEligibleForRollback() helper for DRY rollback checks
- Export helper for use in history API routes

Addresses PR #118 review issues #5, #13

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

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

* fix(lib/monitoring): add rollback fields to ActivityLogInput

- Add contentFormat field for content type tracking
- Add rollbackFromActivityId for rollback chain references
- Pass fields as top-level properties to logActivity

Addresses PR #118 review issue #3

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

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

* fix(api): add authorization and resourceType filtering

- Add permission check before returning activity details
- Implement resourceType query parameter filtering in history routes
- Use shared isActivityEligibleForRollback helper
- Add case-insensitive resource type matching

Addresses PR #118 review issues #6, #7, #13

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

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

* fix(services): improve type safety and add transactions

- Use Drizzle count() instead of .length for efficient queries
- Add operation validation against known valid operations
- Fix unsafe null type assertion in RollbackPreview
- Re-export RollbackContext type for API route usage
- Wrap AI undo mutations in database transaction
- Use switch statement for context determination

Addresses PR #118 review issues #2, #16, #18, #19, #23

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

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

* fix(ui): add CSRF protection and improve error handling

- Use post() with CSRF token for rollback requests
- Add error toast for failed preview fetch
- Handle non-JSON error responses gracefully
- Fix non-existent postWithAuth import to use post
- Remove re-throw after toast notification
- Fix redundant ternary for buttonSize

Addresses PR #118 review issues #1, #8, #11, #17, #22, NEW

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

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

* fix(db): address schema issues from PR #118 review

Critical & Major fixes:
- Add missing .defaultNow() to retention_policies.updatedAt
  (fixes schema/migration mismatch that would cause insert failures)
- Add contentFormatEnum for type-safe content format validation
  (prevents invalid values during rollback parsing)
- Add CHECK constraint: retentionDays >= -1 (where -1 = unlimited)
- Add rollback source snapshot fields for audit trail preservation:
  - rollbackSourceOperation: captures source activity type
  - rollbackSourceTimestamp: captures when source change occurred
  - rollbackSourceTitle: captures resource title at time of change

These denormalized fields survive retention policy deletion, ensuring
complete audit trails even when source activities are purged.

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

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

* fix(lib): move rollback fields to top-level in activity logger

Fixes Major issue #2 from PR #118 review:
- Move rollbackFromActivityId from metadata to top-level field
- Move contentFormat from metadata to top-level field
- Add rollback source snapshot fields to ActivityLogInput interface
- Update logRollbackActivity to accept and pass snapshot fields

Fields are now stored in dedicated database columns instead of being
nested in the metadata JSONB, enabling proper indexing and querying.

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

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

* fix(services): add transaction support to rollback service

Enables atomic rollback operations (Issue #7 from PR #118 review):
- Add optional transaction parameter to executeRollback()
- Update all internal rollback functions to accept database parameter
- Pass rollback source snapshot fields to logRollbackActivity

When a transaction is provided, all database operations use it instead
of the default db connection, enabling atomic rollback + message
deletion in AI undo operations.

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

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

* fix(services): improve atomicity and context logic in AI undo

Fixes Issues #5 and #7 from PR #118 review:

Context determination (#5):
- Align executeAiUndo context logic with previewAiUndo
- Use 'ai_tool' context for pages (all activities here are AI-generated)
- Simplifies from switch statement to direct assignment with drive check

Transaction atomicity (#7):
- Wrap rollbacks AND message deletion in single transaction
- Pass transaction to executeRollback for atomic operations
- If any operation fails, entire undo is rolled back

This ensures users don't end up in inconsistent states where messages
are deleted but only some changes were reverted.

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

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

* fix(api): use Zod schema for undo route validation

Fixes Issue #6 from PR #118 review:
- Replace manual mode validation with Zod schema
- Aligns with codebase patterns (see rollback route, history route)
- Provides type-safe body parsing with proper TypeScript inference

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

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

* fix: address CodeRabbit review feedback

- Add CHECK constraint to retentionPolicies schema definition
  (aligns Drizzle schema with existing migration constraint)
- Change AI undo to all-or-nothing transaction semantics
  (any rollback failure aborts entire operation)

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

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

* fix(db): add subscriptionTier enum for retention policies

- Create subscription_tier pgEnum with 'free', 'pro', 'business', 'founder'
- Convert retentionPolicies.subscriptionTier from text to enum
- Adds DB-level validation to prevent invalid tier values

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

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

* fix: address final CodeRabbit review comments

- Add comments explaining rollbackFromActivityId intentionally lacks FK
  (allows provenance to survive source activity deletion for audit)
- Add TODO note for contentSnapshot storage considerations
- Optimize message fetching: include createdAt in AiUndoPreview to
  avoid double database query

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

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

* fix(db): add USING clause for text-to-enum cast

PostgreSQL requires explicit cast when converting text column to enum type.

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

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

* fix(ai): support undo for global assistant messages by checking both messages tables and updating permission logic

* fix(ai): resolve lint errors in undo route and tests

* test: add contract tests for version history rollback

- Add route tests for /api/activities/[activityId]/rollback (12 tests)
- Add route tests for /api/pages/[pageId]/history (21 tests)
- Add service tests for ai-undo-service (16 tests) with @scaffold label
- Add service tests for rollback-service (28 tests) with @scaffold label
- Add permission tests for rollback-permissions (69 tests)
- Fix undo route test: remove duplicate test expecting wrong status
- Add fake timers to history route tests for deterministic dates

Per rubric v2: service tests use @scaffold labels for ORM chain mocks
pending repository seam refactoring.

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

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

* feat(ui): add rollback capability to activity views

Add "Restore this version" action to activity items in:
- ActivityDashboard (middle panel for /dashboard/activity and drive activity)
- SidebarActivityTab (right sidebar context-aware activity feed)

Changes:
- ActivityItem: Add hover dropdown menu with rollback action and confirmation dialog
- ActivityTimeline: Pass context and onRollback handler to items
- ActivityDashboard: Handle rollback with proper context mapping (user→user_dashboard, drive→drive)
- SidebarActivityTab: Add rollback UI with context-aware scoping (page/drive/user_dashboard)

Rollback is properly scoped by context to prevent unintended changes.

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

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

* test: add activity logger tests for rollback and undo operations

Add compliance tests for:
- logRollbackActivity: validates rollback operation logging with source activity reference
- logConversationUndo: validates conversation undo logging for both modes

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

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

* fix(rollback): support create operation rollback and global messages

- Add 'create' as rollbackable operation (trash resource to undo creation)
- Support both global messages and page chat messages in rollback
- Fix AI undo timing to include tool calls from preceding message
- Handle message create rollback by deactivating the message

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

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

* fix: address PR #118 code review feedback

- Fix build failure: replace dynamic db.query[] bracket notation with
  explicit conditional to resolve TypeScript union type error
- Extract checkUndoPermissions() helper to eliminate 31 lines of
  duplicated permission logic between GET and POST handlers
- Add existingPreview parameter to executeAiUndo() to avoid redundant
  preview computation (was being called twice per request)
- Simplify context determination: remove redundant isAiGenerated check
  since query already filters for it
- Update tests to match new 4-parameter executeAiUndo signature
- Add clarifying comment explaining partial failure tests document
  defensive handling (actual impl uses all-or-nothing transaction)

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

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

* test: update rollback-permissions tests for create operation

Update test expectations to match the new behavior where 'create'
is a rollbackable operation (rolling back a create = trashing the resource).

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

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

* fix: reset undo counters on transaction failure

When executeAiUndo catches an error, the transaction has been rolled
back so no changes were committed. Reset messagesDeleted and
activitiesRolledBack to 0 to accurately reflect this.

Also simplify the route handler to always return 500 on failure since
partial success (207) is now unreachable with all-or-nothing semantics.

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

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

* fix: remove unused params variable in OptimizedViewHeader

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

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

* fix(web): refine AI undo error message to reflect atomic transaction semantics

* fix(tests): correct hook test expectations for SWR behavior

- useBreadcrumbs: Fix isLoading expectation for null pageId
  When pageId is null, isLoading correctly returns false (nothing to load)

- usePermissions: Fix SWR mock to trigger onSuccess callback
  isPaused requires hasLoadedRef.current=true, which is set by onSuccess

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

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

* fix: handle all failure cases in AI undo route

- Remove `&& result.errors.length > 0` condition so any success:false
  triggers 500 response, not just failures with non-empty errors
- Add test for empty errors array edge case
- Enhance usePermissions test to properly validate after onSuccess

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

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

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2witstudios added a commit that referenced this pull request Dec 24, 2025
* feat: add version history and rollback functionality

Implement comprehensive version history browsing and rollback capabilities
for PageSpace, allowing users to restore resources to previous states.

## Schema Changes
- Add 'rollback' operation to activity_operation enum
- Add rollbackFromActivityId and contentFormat fields to activity_logs
- Create retention_policies table for plan-based history retention

## Core Features
- RBAC-based rollback permissions (edit access = rollback access)
- Resource-specific rollback handlers (pages, drives, agents, etc.)
- Plan-based retention limits (7/30/90/unlimited days)
- Rollback creates new activity entry (history never erased)

## API Endpoints
- GET /api/activities/[activityId] - Single activity with rollback eligibility
- POST /api/activities/[activityId]/rollback - Execute rollback
- GET /api/pages/[pageId]/history - Page version history
- GET /api/drives/[driveId]/history - Drive version history (admin)

## UI Components
- VersionHistoryPanel - Slide-out panel with timeline and filters
- VersionHistoryItem - Activity item with "Restore" action
- RollbackConfirmDialog - Confirmation modal with warnings

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

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

* fix: complete rollback handler implementations and fix lint errors

- Fully implement rollbackPermissionChange for grant/revoke/update operations
- Fully implement rollbackMemberChange for add/remove/role change operations
- Fully implement rollbackRoleChange for create/delete/update operations
- Fix lint errors: remove unused imports, fix useEffect dependencies
- Add package exports for @pagespace/lib/permissions and @pagespace/lib/monitoring
- Fix useToast import path to use correct hook location

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

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

* feat: add undo AI changes feature for conversations

Add ability to undo AI changes from a specific message point:
- Preview what will be affected before undoing
- Two modes: revert conversation only OR revert with all tool changes
- Activity logging for audit compliance

New files:
- ai-undo-service.ts: preview and execute functions
- UndoAiChangesDialog.tsx: confirmation dialog with mode selection
- /api/ai/chat/messages/[messageId]/undo: GET preview, POST execute

Changes:
- Add conversation_undo operations to activity schema
- Add message rollback handler to rollback-service
- Wire undo button to MessageActionButtons and MessageRenderer

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

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

* fix: add missing radio-group UI component

Required for UndoAiChangesDialog mode selection.

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

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

* fix: add @radix-ui/react-radio-group dependency

Required for radio-group UI component used in UndoAiChangesDialog.

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

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

* fix: restore correct @electron/node-gyp resolution in lockfile

The previous lockfile had a malformed git SSH URL that broke CI:
- Before: git+https://git@github.com:electron/node-gyp.git (broken)
- After: https://codeload.github.com/electron/node-gyp/tar.gz/... (works)

Restored lockfile and re-ran pnpm install to properly resolve dependencies.

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

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

* fix: add critical rules to CLAUDE.md and fix retention tier order

- Add CRITICAL rules for package manager (always pnpm, never npm)
- Add CRITICAL rules for database migrations (never manually edit)
- Fix retention days: founder=90, business=unlimited (was swapped)

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

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

* fix(db): add missing default and rollback index

- Add DEFAULT now() for retention_policies.updatedAt column
- Add index on activity_logs.rollbackFromActivityId for queries
- Create migration 0027_fix_retention_updated_at.sql

Addresses PR #118 review issues #10, #20

* fix(lib/permissions): add block scoping and eligibility helper

- Wrap switch case declarations in blocks to prevent variable leakage
- Add isActivityEligibleForRollback() helper for DRY rollback checks
- Export helper for use in history API routes

Addresses PR #118 review issues #5, #13

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

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

* fix(lib/monitoring): add rollback fields to ActivityLogInput

- Add contentFormat field for content type tracking
- Add rollbackFromActivityId for rollback chain references
- Pass fields as top-level properties to logActivity

Addresses PR #118 review issue #3

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

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

* fix(api): add authorization and resourceType filtering

- Add permission check before returning activity details
- Implement resourceType query parameter filtering in history routes
- Use shared isActivityEligibleForRollback helper
- Add case-insensitive resource type matching

Addresses PR #118 review issues #6, #7, #13

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

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

* fix(services): improve type safety and add transactions

- Use Drizzle count() instead of .length for efficient queries
- Add operation validation against known valid operations
- Fix unsafe null type assertion in RollbackPreview
- Re-export RollbackContext type for API route usage
- Wrap AI undo mutations in database transaction
- Use switch statement for context determination

Addresses PR #118 review issues #2, #16, #18, #19, #23

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

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

* fix(ui): add CSRF protection and improve error handling

- Use post() with CSRF token for rollback requests
- Add error toast for failed preview fetch
- Handle non-JSON error responses gracefully
- Fix non-existent postWithAuth import to use post
- Remove re-throw after toast notification
- Fix redundant ternary for buttonSize

Addresses PR #118 review issues #1, #8, #11, #17, #22, NEW

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

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

* fix(db): address schema issues from PR #118 review

Critical & Major fixes:
- Add missing .defaultNow() to retention_policies.updatedAt
  (fixes schema/migration mismatch that would cause insert failures)
- Add contentFormatEnum for type-safe content format validation
  (prevents invalid values during rollback parsing)
- Add CHECK constraint: retentionDays >= -1 (where -1 = unlimited)
- Add rollback source snapshot fields for audit trail preservation:
  - rollbackSourceOperation: captures source activity type
  - rollbackSourceTimestamp: captures when source change occurred
  - rollbackSourceTitle: captures resource title at time of change

These denormalized fields survive retention policy deletion, ensuring
complete audit trails even when source activities are purged.

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

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

* fix(lib): move rollback fields to top-level in activity logger

Fixes Major issue #2 from PR #118 review:
- Move rollbackFromActivityId from metadata to top-level field
- Move contentFormat from metadata to top-level field
- Add rollback source snapshot fields to ActivityLogInput interface
- Update logRollbackActivity to accept and pass snapshot fields

Fields are now stored in dedicated database columns instead of being
nested in the metadata JSONB, enabling proper indexing and querying.

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

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

* fix(services): add transaction support to rollback service

Enables atomic rollback operations (Issue #7 from PR #118 review):
- Add optional transaction parameter to executeRollback()
- Update all internal rollback functions to accept database parameter
- Pass rollback source snapshot fields to logRollbackActivity

When a transaction is provided, all database operations use it instead
of the default db connection, enabling atomic rollback + message
deletion in AI undo operations.

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

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

* fix(services): improve atomicity and context logic in AI undo

Fixes Issues #5 and #7 from PR #118 review:

Context determination (#5):
- Align executeAiUndo context logic with previewAiUndo
- Use 'ai_tool' context for pages (all activities here are AI-generated)
- Simplifies from switch statement to direct assignment with drive check

Transaction atomicity (#7):
- Wrap rollbacks AND message deletion in single transaction
- Pass transaction to executeRollback for atomic operations
- If any operation fails, entire undo is rolled back

This ensures users don't end up in inconsistent states where messages
are deleted but only some changes were reverted.

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

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

* fix(api): use Zod schema for undo route validation

Fixes Issue #6 from PR #118 review:
- Replace manual mode validation with Zod schema
- Aligns with codebase patterns (see rollback route, history route)
- Provides type-safe body parsing with proper TypeScript inference

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

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

* fix: address CodeRabbit review feedback

- Add CHECK constraint to retentionPolicies schema definition
  (aligns Drizzle schema with existing migration constraint)
- Change AI undo to all-or-nothing transaction semantics
  (any rollback failure aborts entire operation)

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

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

* fix(db): add subscriptionTier enum for retention policies

- Create subscription_tier pgEnum with 'free', 'pro', 'business', 'founder'
- Convert retentionPolicies.subscriptionTier from text to enum
- Adds DB-level validation to prevent invalid tier values

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

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

* fix: address final CodeRabbit review comments

- Add comments explaining rollbackFromActivityId intentionally lacks FK
  (allows provenance to survive source activity deletion for audit)
- Add TODO note for contentSnapshot storage considerations
- Optimize message fetching: include createdAt in AiUndoPreview to
  avoid double database query

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

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

* fix(db): add USING clause for text-to-enum cast

PostgreSQL requires explicit cast when converting text column to enum type.

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

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

* fix(ai): support undo for global assistant messages by checking both messages tables and updating permission logic

* fix(ai): resolve lint errors in undo route and tests

* test: add contract tests for version history rollback

- Add route tests for /api/activities/[activityId]/rollback (12 tests)
- Add route tests for /api/pages/[pageId]/history (21 tests)
- Add service tests for ai-undo-service (16 tests) with @scaffold label
- Add service tests for rollback-service (28 tests) with @scaffold label
- Add permission tests for rollback-permissions (69 tests)
- Fix undo route test: remove duplicate test expecting wrong status
- Add fake timers to history route tests for deterministic dates

Per rubric v2: service tests use @scaffold labels for ORM chain mocks
pending repository seam refactoring.

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

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

* feat(ui): add rollback capability to activity views

Add "Restore this version" action to activity items in:
- ActivityDashboard (middle panel for /dashboard/activity and drive activity)
- SidebarActivityTab (right sidebar context-aware activity feed)

Changes:
- ActivityItem: Add hover dropdown menu with rollback action and confirmation dialog
- ActivityTimeline: Pass context and onRollback handler to items
- ActivityDashboard: Handle rollback with proper context mapping (user→user_dashboard, drive→drive)
- SidebarActivityTab: Add rollback UI with context-aware scoping (page/drive/user_dashboard)

Rollback is properly scoped by context to prevent unintended changes.

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

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

* test: add activity logger tests for rollback and undo operations

Add compliance tests for:
- logRollbackActivity: validates rollback operation logging with source activity reference
- logConversationUndo: validates conversation undo logging for both modes

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

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

* fix(rollback): support create operation rollback and global messages

- Add 'create' as rollbackable operation (trash resource to undo creation)
- Support both global messages and page chat messages in rollback
- Fix AI undo timing to include tool calls from preceding message
- Handle message create rollback by deactivating the message

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

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

* fix: address PR #118 code review feedback

- Fix build failure: replace dynamic db.query[] bracket notation with
  explicit conditional to resolve TypeScript union type error
- Extract checkUndoPermissions() helper to eliminate 31 lines of
  duplicated permission logic between GET and POST handlers
- Add existingPreview parameter to executeAiUndo() to avoid redundant
  preview computation (was being called twice per request)
- Simplify context determination: remove redundant isAiGenerated check
  since query already filters for it
- Update tests to match new 4-parameter executeAiUndo signature
- Add clarifying comment explaining partial failure tests document
  defensive handling (actual impl uses all-or-nothing transaction)

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

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

* test: update rollback-permissions tests for create operation

Update test expectations to match the new behavior where 'create'
is a rollbackable operation (rolling back a create = trashing the resource).

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

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

* fix: reset undo counters on transaction failure

When executeAiUndo catches an error, the transaction has been rolled
back so no changes were committed. Reset messagesDeleted and
activitiesRolledBack to 0 to accurately reflect this.

Also simplify the route handler to always return 500 on failure since
partial success (207) is now unreachable with all-or-nothing semantics.

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

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

* fix: remove unused params variable in OptimizedViewHeader

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

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

* fix(web): refine AI undo error message to reflect atomic transaction semantics

* fix(tests): correct hook test expectations for SWR behavior

- useBreadcrumbs: Fix isLoading expectation for null pageId
  When pageId is null, isLoading correctly returns false (nothing to load)

- usePermissions: Fix SWR mock to trigger onSuccess callback
  isPaused requires hasLoadedRef.current=true, which is set by onSuccess

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

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

* fix: handle all failure cases in AI undo route

- Remove `&& result.errors.length > 0` condition so any success:false
  triggers 500 response, not just failures with non-empty errors
- Add test for empty errors array edge case
- Enhance usePermissions test to properly validate after onSuccess

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

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

* fix: resolve AI chat undo feature not working

The undo feature was always showing "Failed to undo changes" due to incorrect
usage of the post() helper from auth-fetch. The post() function:
- Returns parsed JSON on success (not a Response object)
- Throws an error on non-2xx responses

Components were incorrectly checking res.ok (undefined on parsed JSON) and
calling res.json() on already-parsed objects, causing the error branch to
always trigger.

Changes:
- Fix UndoAiChangesDialog to properly use post() - await and catch errors
- Fix ActivityDashboard, SidebarActivityTab, VersionHistoryPanel (same pattern)
- Update page-write-tools activity logging to pass previousValues for rollback
  support (replace_lines, rename_page, trash/restore, move_page, edit_sheet_cells
  now store original state for proper undo)

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

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

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2witstudios added a commit that referenced this pull request Jan 29, 2026
This PR implements the security hardening plan priorities 1-4:

**P1: User Account Suspension Check**
- Add suspendedAt/suspendedReason fields to users schema
- Update user-validator.ts to check suspension status
- Update session-service.ts to auto-revoke sessions for suspended users

**P2: WebSocket Origin Validation Blocking**
- Change validateAndLogWebSocketOrigin to return boolean and reject invalid origins
- Fail closed in production when no allowed origins configured
- Add proper logging for rejected connections

**P3: SSRF DNS Rebinding Mitigation**
- Add buildIPDirectURL helper to connect directly to validated IPs
- Modify safeFetch to use resolved IP with Host header preservation
- Prevents TOCTOU attacks where DNS rebinds between validation and fetch

**P4: Service Token Audit Logging**
- Add structured security logging to processor auth middleware
- Log successful validations, failures, and scope assertion failures
- Include request context (IP, endpoint, user agent) in all logs

**P5: Legacy Cleanup**
- Remove SERVICE_JWT_SECRET from .env.example and docker-compose.yml
- Mark vulnerabilities #1, #2, #11 as RESOLVED in security docs
- Update zero-trust-architecture.md to reflect opaque token migration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2witstudios added a commit that referenced this pull request Jan 29, 2026
* feat(security): implement P1-P4 security hardening fixes

This PR implements the security hardening plan priorities 1-4:

**P1: User Account Suspension Check**
- Add suspendedAt/suspendedReason fields to users schema
- Update user-validator.ts to check suspension status
- Update session-service.ts to auto-revoke sessions for suspended users

**P2: WebSocket Origin Validation Blocking**
- Change validateAndLogWebSocketOrigin to return boolean and reject invalid origins
- Fail closed in production when no allowed origins configured
- Add proper logging for rejected connections

**P3: SSRF DNS Rebinding Mitigation**
- Add buildIPDirectURL helper to connect directly to validated IPs
- Modify safeFetch to use resolved IP with Host header preservation
- Prevents TOCTOU attacks where DNS rebinds between validation and fetch

**P4: Service Token Audit Logging**
- Add structured security logging to processor auth middleware
- Log successful validations, failures, and scope assertion failures
- Include request context (IP, endpoint, user agent) in all logs

**P5: Legacy Cleanup**
- Remove SERVICE_JWT_SECRET from .env.example and docker-compose.yml
- Mark vulnerabilities #1, #2, #11 as RESOLVED in security docs
- Update zero-trust-architecture.md to reflect opaque token migration

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

* fix: address CI failures and code review comments

- Fix logger import path: @pagespace/lib/logger-config -> @pagespace/lib/logging/logger-config
- Fix SSRF DNS rebinding for HTTPS: Skip IP-direct URLs for HTTPS because TLS/SNI requires hostname
- Add safer error type casting in auth middleware (error instanceof Error check)

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

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2witstudios added a commit that referenced this pull request Feb 9, 2026
- Reorder copy-webpack-plugin alphabetically in devDependencies and
  regenerate pnpm-lock.yaml (CodeRabbit #1)
- Add runtime typeof guard in getDefaultContent return (CodeRabbit #2)
- Fix WebSocketMessage cast via unknown for ToolExecutionRequest
- Add index signatures to PdfExtractionMetadata/VisionExtractionMetadata
- Guard sheet content validation with typeof string check
- Cast logger-database category field to string | undefined
- Wrap ZodError/Error/MetricsSummary at logger call sites instead of
  passing incompatible types to LogInput (Record<string, unknown>)
- Remove unused ToolUIPart import from confirmation.tsx
- Include root types/ directory in lib, processor, and web tsconfigs
  so pdf-parse-debugging-disabled ambient module resolves in all builds
- Spread PDFInfo to satisfy Record<string, unknown> constraint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2witstudios added a commit that referenced this pull request Feb 9, 2026
* Remove unused getPageContentForAI function

Dead code — only consumed by its own test file. The AI system uses
read_page tool to access page content directly from the database.

Fixes #439

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

* Replace any types in logging with LogInput/LogContext/HttpMethod

Introduce JsonValue, LogMetadata (strict), LogInput (caller-facing),
and HttpMethod types. Replace all `any` in logger.ts, logger-browser.ts,
logger-config.ts, logger-database.ts, and activity-tracker.ts.
Type request params with LoggableRequest union (Next.js + Express).
sanitizeData now uses unknown throughout.

Fixes #443

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

* Type page content as string | null, fix validator and config types

Page.content: any → string | null (all page content stored as text).
Page.fileMetadata: Record<string, any> → Record<string, JsonValue>.
Validator data params: any → Record<string, unknown>.
PageTypeConfig.defaultContent: () => any → () => string | Record<string, unknown>.
storage-limits existingTx: any → Drizzle transaction type.

Fixes #444

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

* Type file processing: extraction metadata, MIME checks, pdf-parse, content-store

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

* Bundle PDF.js worker locally instead of loading from unpkg CDN

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

* Type desktop IPC: error-utils helpers, catch unknown, WebSocketMessage

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

* Remove @ts-expect-error suppressions with ExtendedToolState, type PDFViewer imports

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

* Fix review feedback: lockfile sync, type narrowing, build errors

- Reorder copy-webpack-plugin alphabetically in devDependencies and
  regenerate pnpm-lock.yaml (CodeRabbit #1)
- Add runtime typeof guard in getDefaultContent return (CodeRabbit #2)
- Fix WebSocketMessage cast via unknown for ToolExecutionRequest
- Add index signatures to PdfExtractionMetadata/VisionExtractionMetadata
- Guard sheet content validation with typeof string check
- Cast logger-database category field to string | undefined
- Wrap ZodError/Error/MetricsSummary at logger call sites instead of
  passing incompatible types to LogInput (Record<string, unknown>)
- Remove unused ToolUIPart import from confirmation.tsx
- Include root types/ directory in lib, processor, and web tsconfigs
  so pdf-parse-debugging-disabled ambient module resolves in all builds
- Spread PDFInfo to satisfy Record<string, unknown> constraint

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2witstudios added a commit that referenced this pull request Apr 13, 2026
- Switch from audit() to auditRequest(request, event) so x-real-ip and
  user-agent are extracted consistently via the shared helper (matches
  review suggestion #1 — removes hand-rolled header parsing).
- Drop fingerprint from auth.session.created audit details. The
  fingerprint is an SHA-256 pseudonym of IP+UA that lives inside the
  tamper-evident audit hash chain (review comment #2: persisting it
  would resist GDPR erasure). Truncated 16-char prefix was not useful
  for verification anyway.
- Strengthen the "should not leak sensitive data" test: it was only
  spying on console, and since audit is mocked to a no-op, any
  regression piping a raw bearer token into audit({ details }) would
  have slipped through. Now asserts against the serialized audit
  payloads in addition to console.
- Parameterize the "normal close" test over [1000, 1001] via it.each
  instead of only invoking with 1000.
- Add an assertion that auth.session.created details does NOT contain
  a fingerprint key, locking in the privacy fix.

Tests: 20/20 pass (added it.each parameterization → +1 case).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2witstudios added a commit that referenced this pull request Apr 13, 2026
* feat(security): audit mcp-ws route via unified audit() pipeline

Migrates the WebSocket MCP bridge from the legacy dual-write logging
(logSecurityEvent + securityAudit) to the single audit() function, and
fixes several semantic issues surfaced in review.

Key changes:
- Unified all audit calls through audit() from @pagespace/lib/server
- Normal close codes (1000/1001) no longer audit — they are routine
  lifecycle events, not security signals, and were polluting session
  revocation forensics. Abnormal closes are audited as anomalies.
- Propagate clientIp (ipAddress) to every in-handler audit call so
  IP-filtered forensic queries capture message/close/error events.
- Drop audit on ws_tool_execution_result and ws_fetch_response_start —
  these are high-volume protocol acks, not security events.
- Lower ws_fetch_response_error riskScore to 0.1 (network noise).
- Lower DB-error and generic WebSocket-error riskScore to 0.3 (these
  are infrastructure failures, not elevated security risk).
- Fix pre-existing double cancelUserRequests() in close handler: the
  guarded path already handled active-connection cleanup; the second
  cached-flag path was redundant and missing isFetchBridgeInitialized.
- Remove dead logSecurityEvent export from ws-security.ts — the function
  now lives in the centralized audit pipeline.

Tests: 19 mcp-ws security tests pass (was 17), including new coverage
for normal-close-skip, abnormal-close-audit, and single-cancel behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(security): propagate userAgent + resourceType on mcp-ws audit events

Improves forensic queryability of WebSocket audit events:
- Add userAgent to every in-route audit() call (previously only ipAddress)
- Add resourceType: 'mcp_websocket' at top level on every call so audit
  queries can filter by resource type (already set on auth.session.created)
- Add x-real-ip fallback to clientIp extraction to match auditRequest() helper
- Move 'mcp_websocket' off details.component (non-standard, not indexed) to
  the typed AuditEvent.resourceType field

Tests updated to lock in the new shape (resourceType + userAgent) on the
representative authz.access.denied and auth.session.created assertions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(security): address CodeRabbit review on mcp-ws audit migration

- Switch from audit() to auditRequest(request, event) so x-real-ip and
  user-agent are extracted consistently via the shared helper (matches
  review suggestion #1 — removes hand-rolled header parsing).
- Drop fingerprint from auth.session.created audit details. The
  fingerprint is an SHA-256 pseudonym of IP+UA that lives inside the
  tamper-evident audit hash chain (review comment #2: persisting it
  would resist GDPR erasure). Truncated 16-char prefix was not useful
  for verification anyway.
- Strengthen the "should not leak sensitive data" test: it was only
  spying on console, and since audit is mocked to a no-op, any
  regression piping a raw bearer token into audit({ details }) would
  have slipped through. Now asserts against the serialized audit
  payloads in addition to console.
- Parameterize the "normal close" test over [1000, 1001] via it.each
  instead of only invoking with 1000.
- Add an assertion that auth.session.created details does NOT contain
  a fingerprint key, locking in the privacy fix.

Tests: 20/20 pass (added it.each parameterization → +1 case).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2witstudios added a commit that referenced this pull request May 4, 2026
…adcast

Addresses 3 Codex P1/P2 review comments.

P1 #1: PATCH /events/[id] now wraps event update + agent-trigger upsert in a single db.transaction so a failed trigger upsert rolls back the event update too — no more partial-write where the title moved but the trigger didn't. update_calendar_event AI tool gets the same atomic shape. New helper upsertCalendarTriggerWorkflowInTx runs against the caller's tx; the standalone upsertCalendarTriggerWorkflow continues to open its own tx for PUT /triggers.

P1 #2: PUT /events/[id]/triggers and PATCH /events/[id] now reject agent-trigger upserts on recurring events, mirroring POST /events. Cron poller fires one-shot occurrences, so attaching one trigger to a recurring event silently misfired on every occurrence past the first.

P2 #3: PUT/DELETE /triggers broadcast now includes the event's attendee user IDs so personal-calendar clients (which only join their own user channel) get immediate trigger-state updates instead of waiting for periodic polling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2witstudios added a commit that referenced this pull request May 4, 2026
* feat(calendar-triggers): validation + upsert + remove

Adds validateCalendarAgentTrigger, upsertCalendarTriggerWorkflow, and removeCalendarTrigger to mirror the task-trigger-helpers surface so the REST + AI tool calendar paths share one validator and the new triggers endpoints have a clean upsert/delete primitive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(calendar): wire agent trigger context on POST

POST /api/calendar/events now accepts agentTrigger.instructionPageId and contextPageIds and forwards them to createCalendarTriggerWorkflow. Routes through validateCalendarAgentTrigger so the REST path enforces the same drive/agent checks as the AI tool. Loosens prompt to optional when an instructionPageId is set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(calendar): PATCH accepts agentTrigger upsert+remove

PATCH /api/calendar/events/[eventId] now accepts agentTrigger with three semantics: undefined leaves any existing trigger alone, null drops the trigger, an object upserts via upsertCalendarTriggerWorkflow. Personal events (no driveId) reject upserts because the executor needs a drive context to resolve agent/instruction/context pages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(calendar): /events/:id/triggers REST endpoints

GET returns the event's trigger row joined with the latest workflow_runs status (lastFiredAt / lastFireError / lastRunStatus). PUT upserts via upsertCalendarTriggerWorkflow. DELETE removes via removeCalendarTrigger. Permission check mirrors PATCH on the event: creator or drive owner/admin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(triggers): extract AgentTriggerSection

Pulls the agent / prompt / instruction-page / context-pages form out of TaskAgentTriggersDialog into a shared presentational component so the new EventAgentTriggerDialog can reuse the same UX. Behavior unchanged for tasks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(calendar-ui): EventAgentTriggerDialog

New dialog mirrors TaskAgentTriggersDialog UX for calendar events: toggle, agent select, prompt, instruction page, context pages (max 10), last-run status, save / remove. Hooks the new /api/calendar/events/[eventId]/triggers endpoints. Pauses SWR via useEditingSession so a remote calendar broadcast cannot clobber unsaved typing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(calendar-ui): linked page + agent trigger button

EventModal now exposes the existing calendarEvents.pageId column as a 'Linked page' picker (drive context, both create and edit) so events can carry a doc the same way task items do via taskItems.pageId. Replaces the inline agent toggle with a 'Configure agent trigger' button that opens EventAgentTriggerDialog on saved drive events. Drops the !isEditing constraint that previously blocked managing triggers on existing events.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(calendar): drop unused imports flagged by lint

CI lint surfaced three unused symbols left over from the inline-validation removal: pages from route.ts (validation moved into validateCalendarAgentTrigger), and mockSelectFrom / mockUpdateWhere from the trigger-helpers test. The test mocks are still wired into the chain (.from / .where) but no longer need to be re-exported from the hoist block. Unblocks Lint, Unit Tests, and Static Security CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(calendar): agentTrigger on update AI tool + safer PATCH

PATCH /api/calendar/events/[eventId] now pre-validates agentTrigger before opening the event update tx so a bad payload (off-drive agent / context page, missing prompt-or-instruction) returns 400 without dirtying event state. update_calendar_event AI tool gains an agentTrigger field with the same three-state semantics (undefined no-op, null remove, object upsert) as the HTTP PATCH, closing the AI-vs-HTTP parity gap. Three new tests cover the AI path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(calendar): atomic event+trigger tx, recurring guard, attendee broadcast

Addresses 3 Codex P1/P2 review comments.

P1 #1: PATCH /events/[id] now wraps event update + agent-trigger upsert in a single db.transaction so a failed trigger upsert rolls back the event update too — no more partial-write where the title moved but the trigger didn't. update_calendar_event AI tool gets the same atomic shape. New helper upsertCalendarTriggerWorkflowInTx runs against the caller's tx; the standalone upsertCalendarTriggerWorkflow continues to open its own tx for PUT /triggers.

P1 #2: PUT /events/[id]/triggers and PATCH /events/[id] now reject agent-trigger upserts on recurring events, mirroring POST /events. Cron poller fires one-shot occurrences, so attaching one trigger to a recurring event silently misfired on every occurrence past the first.

P2 #3: PUT/DELETE /triggers broadcast now includes the event's attendee user IDs so personal-calendar clients (which only join their own user channel) get immediate trigger-state updates instead of waiting for periodic polling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(calendar): cover validator-throws PATCH path

New test confirms PATCH /events/[id] returns 400 cleanly when validateCalendarAgentTrigger throws (e.g. off-drive agent), without ever opening the event update transaction. Pins the partial-write protection added in the previous commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2witstudios added a commit that referenced this pull request Jul 18, 2026
* fix(toast): replace console.log stub with sonner

Migrate all 14 useToast consumers (member management, roles, drive AI settings, drive deletion, invites, version-history/activity rollback) to sonner toast.success/toast.error, restoring user-facing feedback that had been silently console.log-only since 2025-12-22.

Update DriveMembers.test.tsx and invite/page.test.tsx to mock sonner instead of the stub; delete apps/web/src/hooks/useToast.ts entirely.

From the 2026-07-17 solo-tells audit (cheap-fix #2).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YYurihmWZ3E5yrmz7W4tEt

* docs(changelog): note toast-notification fix

Add CHANGELOG.md entry for the useToast->sonner migration and record the self-review findings at tasks/reviews/pu-sonner-toast.md (0 blockers, 0 majors).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YYurihmWZ3E5yrmz7W4tEt

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2witstudios added a commit that referenced this pull request Jul 29, 2026
…ty, error UX, reconcile resilience

Six findings from the full-branch review. (#1 SSRF, #2 storage metering and #7
concurrency quota were already fixed in the two prior commits; #3's cron suite
now collects and passes 5 tests, so it was fixed by a later phase than the
review sampled.)

#4/#15 — prompt regression on default config. `buildAgentAwarenessPrompt`
unconditionally told the model to delegate with `spawn_session`, but session
tools only exist when CODE_EXECUTION_ENABLED is on (default off), so the
assistant confidently called a tool it did not have and delegation silently
broke. The delegation sentence is now gated on a `canDelegate` flag that both
call sites derive from `isCodeExecutionEnabled()`; without it the section still
lists the agents, it just stops naming a tool that isn't there.

#13/#14 — git argv safety. `git_add` spliced paths straight into argv with no
`--`, unlike its siblings `git status`/`git diff`: a path named `-p` was read as
a flag (interactive add). It now separates, and only when there are paths, so no
bare trailing `--`. `git_reset.ref` and `git_remote_add.name` gained the
`validateFlagSafe` guard that structurally identical sibling fields already
apply.

#10/#11 — infinite spinner on a failed agent load. `AgentView` guarded on
`agentLoading || !agent`, so once SWR gave up retrying, `isLoading` went false,
`agent` stayed null, and the user watched a spinner that would never resolve
with no error text and no escape but a reload. Loading and failure are now
distinct states: the failure surfaces the server's own message and a Try again
button, backed by a new `retry` from `useResolvedAgent`.

#6 — one failing candidate query no longer parks the other pass. The orphan
reconcile listed the reclaim outbox and the teardown-intent rows under
`Promise.all`, so either failing dropped both. Now `allSettled` with per-source
error logs: a degraded query costs its own candidates, not every reclaim, and
those are billing VMs nobody is using.

#8 — the leak signal is no longer silent. When a confirmed-unreferenced Sprite
fails BOTH its kill and its reclaim-outbox insert, nothing in the system knows
that VM exists — no row points at it, so no trigger and no cross-check will find
it. That path swallowed its error, making the one path built to catch a
permanently leaked VM the one path with no signal. It now logs loudly with the
sandbox id and both failure reasons.

#5 — the destructive teardown binding gets tests. 10 cases over `killSprite`
(confirmed kill, replaced-name-as-success, genuine failure, unpinned instance),
`markSessionTornDown` (CAS win/loss), and `listOrphanCandidates` (both sources,
each single-source failure, cap + backlog).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017V3eqwRX5cFy3Tdu2Syoro
2witstudios added a commit that referenced this pull request Aug 9, 2026
Fix the two HIGH defects (#1, #2) and the MEDIUM-HIGH (#3) from
`.pu-reports/pu-rev-phase1.md`. All are latent — nothing writes these
tables yet — and each would be a hand-written DELETE against production
later.

Finding #1 — `applyNodeWrite` prescribed drop-then-put. The
composite self-FK is ON DELETE cascade, so dropping a container took
its whole subtree with it. The collapse path drops a split whose
children are being reparented, not deleted, and those children are
only in `put`. Drop-first cascades them away and `put` cannot
resurrect them. Fixed: put before drop, with the cascade named in the
docblock so nobody tidies it back.

Finding #2 — `create` accepted an empty `nodeId` and a blank
`targetId`. Postgres stores both (text NOT NULL is satisfied by
''), and the read then rejects the whole set rather than filtering,
so the workspace becomes permanently unreadable. Fixed: `create`
refuses both with typed codes; `validateTree` refuses them too (the
gate every write path runs, especially `put(nodes[])`); the false
comment in `bind` is corrected; the untested compensating guard in
`open` is removed, with a new test for the split path it covered.

Finding #3 — `validateTree` skipped the finiteness sweep for parked
panes, so a NaN/Infinity share on a detached pane passed. Fixed:
the sweep is hoisted out of the group loop entirely, ahead of the
per-container fraction rules, with a comment that parking does not
make a share a number.

Also fixed: finding #6 (the byte-identical round-trip claim is
false; corrected to structural identity, with the change-test hazard
named), #10 (the FK's truncated live name is recorded), #11
(`put`'s ordering guarantee is stated), and #12 (`validateTree`'s
cast is removed, using the idiom `descendantsOf` already follows).

Report with mutation table and the deferrable-FK argument (no,
tested against PostgreSQL 17.5):
`.pu-reports/pu-fix-review.md`

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4EZg67rRbxgutMEFi2UZJ
2witstudios added a commit that referenced this pull request Sep 9, 2026
…use marker is the machine's signed ack, never the send (Codex P1 #2, review round 1)

In-flight and acked are tracked separately on the socket; the ack marker is set
only from the verified pause_result, failure clears the in-flight marker, and
the heartbeat resends while the row is paused and unacked. Mutants: mark on
send, and keep the in-flight marker on failure — both red.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V6iGLmPYL3w565nDECjM5T
2witstudios added a commit that referenced this pull request Sep 16, 2026
… egress proxy to R13, diagram, score and probe 3; DNS bypass bounded to pinned hosts; principal header conditional on probe #2; present-Origin wording; five mechanisms; corrected line citations

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JQD1y1eGrFVHKPXwRWAHq6
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