Skip to content

Codex/add sheets page type with calculations - #10

Merged
2witstudios merged 2 commits into
masterfrom
codex/add-sheets-page-type-with-calculations
Sep 24, 2025
Merged

2witstudios merged 2 commits into
masterfrom
codex/add-sheets-page-type-with-calculations

Conversation

@2witstudios

Copy link
Copy Markdown
Owner

No description provided.

@2witstudios
2witstudios merged commit db19d19 into master Sep 24, 2025
@2witstudios
2witstudios deleted the codex/add-sheets-page-type-with-calculations branch October 17, 2025 19:57
2witstudios added a commit that referenced this pull request Dec 22, 2025
- 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
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 Jun 25, 2026
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014jffCMhRbzGjj9JyYavjYn
2witstudios added a commit that referenced this pull request Jun 25, 2026
…956 #969 #971) (#1715)

* docs(security): PII encryption design gate + GDPR epic plan (#965)

Phase-0 hard gate: per-column deterministic-blind-index vs random AES-256-GCM
decisions grounded in actual query usage. Content scoped out (substring search).

Refs #965 #966 #973 #956 #969 #971

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014jffCMhRbzGjj9JyYavjYn

* feat(encryption): blind-index + rollout-safe field-crypto pure core (#965)

Phase 1 of GDPR encryption epic. Deterministic HMAC blind index (domain-
separated derived key) for equality-searchable PII; encryptField/decryptField
wrappers tolerate legacy plaintext mid-backfill and never double-encrypt.
IPv6-safe ciphertext detection. 19 unit tests, TDD RED->GREEN.

Refs #965

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014jffCMhRbzGjj9JyYavjYn

* feat(security): emit HSTS for all HTTPS responses, not just NODE_ENV=production (#969)

Phase 5 of GDPR encryption epic. Closes transit-encryption gap where staging/
tenant/preview HTTPS envs shipped no HSTS. Pure shouldEmitHsts + isSecureRequest
(honors x-forwarded-proto); HSTS now scheme-driven, dev http unaffected, prod
back-compat preserved. Documents internal-traffic + mobile cert-pinning posture.
9 new tests, existing 50 still green. TDD RED->GREEN.

Refs #969

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014jffCMhRbzGjj9JyYavjYn

* feat(processor): envelope-encrypt file storage at rest + ephemeral extracted text (#966 #973)

Phase 3+4 of GDPR encryption epic.
- AES-256-GCM envelope codec (per-object derived key, magic-prefixed, fail-closed)
- ContentStore encrypts originals/binary cache behind FILE_ENCRYPTION_ENABLED
  (default OFF: presigned-URL browser delivery would break); server-side text
  caches (extracted-text/ocr-text) always encrypted when ENCRYPTION_KEY set.
  Reads transparently decrypt envelopes; hash stays over plaintext.
- text-extractor: PII extracted text persisted to cache ONLY when encryptable;
  otherwise plaintext write skipped (text still returned for DB-backed search).
  Pure cleanExtractedText + shouldPersistExtractedText.
- Docs: file-encryption-at-rest design + presigned-delivery constraint.
49 new tests; pre-existing magika model-load failures unrelated. TDD RED->GREEN.

Refs #966 #973

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014jffCMhRbzGjj9JyYavjYn

* feat(search): runtime PII enforcement for search audit details (#971)

Phase 6 of GDPR encryption epic. Replaces static-only source check with a pure
whitelist builder (buildSearchAuditDetails) wired into all 3 search routes; it
is structurally incapable of emitting the user query into the tamper-evident,
non-erasable audit chain. Adds runtime guard auditDetailsContainText + tests.

Refs #971

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014jffCMhRbzGjj9JyYavjYn

* feat(security): pure encrypted-backup pipeline spec + tests (#956)

Phase 6 of GDPR encryption epic. Canonical, unit-testable definition of the
encrypted pg_dump backup pipeline (pg_dump | openssl aes-256 | aws s3 cp -)
mirrored by PageSpace-Deploy backup.sh. Rejects non-.enc keys to prevent
plaintext-at-rest regressions; key sourced from env, never a literal.

Refs #956

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014jffCMhRbzGjj9JyYavjYn

* feat(encryption): user PII encryption edge, schema + idempotent backfill (#965)

Phase 2 of GDPR encryption epic. Additive + unit-tested foundation:
- migration 0171: users.emailBidx (unique) + security_audit_log.ip_bidx (fwd-only)
- user-crypto edge: encryptUserPii/decryptUserPii/emailLookupBidx + getPiiIndexKey;
  proves email blind-index lookup path by test
- idempotent/resumable/dry-run backfill (pure planner + runner) for email+name
- lib package.json exports for the new encryption subpaths
Live auth call-site cutover documented as the gated, separately-tested rollout
(passwordless-auth hot path can't be integration-tested in a worktree); columns
are inert until then, so this lands safely. 24 new tests, db+lib typecheck green.

Refs #965

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014jffCMhRbzGjj9JyYavjYn

* docs(tasks): mark GDPR encryption epic in-review (PR #1715, Deploy #10)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014jffCMhRbzGjj9JyYavjYn

* test(auth): add emailBidx to me.test.ts user fixture (typecheck fix)

The new nullable users.emailBidx column is a required property on the inferred
User type; the me.test.ts fixture literal needed it. Fixes web#typecheck CI.

Refs #965

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014jffCMhRbzGjj9JyYavjYn

* fix(security): address Codex + CodeQL review on GDPR encryption PR

- audit IP (P2): wire forward-only AES-GCM encryption + ipBidx in logEvent
  (ipAddress is hash-excluded → chain-safe); queryAuditEvents filters by blind
  index OR legacy plaintext and decrypts for display. Claim now true.
- backfill (P1): hard-gate live runs — default dry-run, refuse --apply unless
  PII_ENCRYPTION_CUTOVER_DEPLOYED=true, so it cannot break plaintext-email auth
  by running before the encryption-aware app ships.
- streamOriginalToFile (P2): decrypt envelopes whenever a key exists, not only
  when FILE_ENCRYPTION_ENABLED is on, so flag-off-after-encrypt reads don't
  stream raw PSE1 bytes to disk.
- HSTS (P2): pass isSecureRequest to the origin-validation 403 error path.
- CodeQL TOCTOU: saveOriginalFromFile derives size from the single read in the
  encrypt branch (no stat→readFile race).
New pure helpers + tests (audit-ip-crypto, resolveBackfillMode, flag-off decrypt).
lib 5646 green; lib/db/processor/web typecheck clean.

Refs #965 #966 #969

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014jffCMhRbzGjj9JyYavjYn

* test(encryption): cover env-edge helpers (getPiiIndexKey, resolveEncryptionConfigFromEnv)

Guards against a coverage-threshold regression on the new env-bound edges.

Refs #965 #966

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014jffCMhRbzGjj9JyYavjYn

* fix(audit): make per-row IP decryption resilient in queryAuditEvents

One undecryptable row (corruption / key rotation) no longer fails the entire
forensic query — it falls back to the stored value for that row only.

Refs #965

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014jffCMhRbzGjj9JyYavjYn

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2witstudios added a commit that referenced this pull request Jul 3, 2026
…429 handling (#1763)

Two P2 findings from the automated review on PR #1756 (merged 69ecab2),
verified against source and both real:

1. Sec 1.4 mischaracterized the device-token grace window as delivering the
   replacement token to a concurrent caller. Verified against
   auth-transactions.ts:147-174 and refresh/route.ts:135-137: the grace
   branch returns only replacement metadata, never the new token value — a
   caller whose first response was genuinely lost has no recovery path.
   Sec 3.4 now specifies an explicit ephemeral grace-cache design (30s TTL,
   keyed by old-token hash) so the OAuth flow actually recovers a dropped
   rotation response instead of inheriting that gap, plus a distinct
   grace_cache_miss outcome (infra gap, not theft — no family revocation).

2. classifyRefreshFailure() treated all 4xx as purge-and-reauth, which would
   delete a valid stored credential on a 429 rate-limit response from the
   /token endpoint (F11). 429 now routes through the retryable path with
   the rest of F4, honoring Retry-After.

Updates flow through Sec 3.2/3.4, the F2a/F4/F5 fail-closed ledger rows,
the decideRefresh/classifyRefreshFailure signatures in Sec 6, and the
Sec 7 testable assertions (#3, #10).


Claude-Session: https://claude.ai/code/session_019ovsuAwugHKEdMbd2wyQAk

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2witstudios added a commit that referenced this pull request Jul 4, 2026
…ope, task trigger echo (#1837) (#1846)

* fix(w7): live-test triage — consult conversation listing, calendar scope, task trigger echo (#1837)

W7 of the SDK/CLI/OAuth epic post-review remediation plan. Fixes the hard
failure and two epic-surface findings from the 70-tool live MCP battery in
#1837; the remaining 7 findings are traced with evidence and deferred (see
PR description).

- #1 (hard fail): ask_agent-created conversations were invisible to
  list_conversations. Neither the consult route nor the internal ask_agent
  tool ever wrote a `conversations` row — only chat_messages — so the
  listing query's ownership join (`conv.userId = userId OR conv.isShared`)
  never matched and the conversation silently dropped out of every listing.
  Fixed both call sites to eagerly createConversation, mirroring the
  existing pattern in apps/web/src/app/api/ai/chat/route.ts.

- #2: create_calendar_event with no driveId (a personal event) 403'd with
  "Scoped tokens cannot create new drives". checkMCPCreateScope treats a
  null targetDriveId as "creating a brand-new drive" (its real caller is
  POST /api/drives) — the events route reused it for "no drive" too. Now
  only enforced when a driveId is actually supplied.

- #10 (partial): update_task/create_task with an inline agentTrigger
  created the trigger but the response was indistinguishable from a no-op.
  createTaskTriggerWorkflow now returns the trigger identity, surfaced as
  `agentTrigger` in both routes' responses and in the SDK's tasks output
  schema.

Every fix ships with a RED→GREEN test.

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

* fix(security): don't let a supplied conversationId claim ownership of another user's conversation

Codex P2 finding on #1846: the finding #1 fix eagerly created a
`conversations` row for any conversationId the caller supplied, including
one that already had real chat_messages authored by a DIFFERENT user (the
exact "legacy conversation, no conversations row yet" backfill case the
fix targets). Any caller who learned that ID could claim
`conversations.userId` for themselves, and ownership-gated actions
elsewhere (e.g. the conversation DELETE handler's `isOwner` check) trust
that field — letting them delete a thread they never participated in.

Fixed in both the consult route and the internal ask_agent tool: before
backfilling ownership, check whether any existing message in that
conversation was authored by a different user. Only create/claim the row
when there's no conflicting owner (empty history, or all prior messages
already belong to the caller).

RED→GREEN test added for both call sites, verified against the
pre-fix code to reproduce the hijack, then against the fix.

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

* refactor: centralize conversation-ownership guard, dedupe agentTrigger response shaping

Proactive review pass (8 finder angles) surfaced that the Codex P2 fix
(previous commit) duplicated its ownership-conflict check identically across
consult/route.ts and agent-communication-tools.ts, and — more importantly —
that the exact same hijack pattern (unconditional createConversation with a
caller-supplied conversationId) still exists, unfixed, in
apps/web/src/app/api/ai/chat/route.ts and
apps/web/src/app/api/v1/chat/completions/route.ts, which this PR's own code
comments cite as the pattern being mirrored.

Root-caused by moving the guard into conversationRepository.createConversation
itself: a cheap indexed lookup short-circuits once the conversations row
exists (the common case), and only a brand-new/legacy row triggers the
ownership-conflict check against existing chat_messages. Every caller —
including the two pre-existing routes above, which this commit does not
touch — now gets the guarantee automatically instead of requiring each call
site to remember a bespoke check. consult/route.ts and
agent-communication-tools.ts go back to calling createConversation
unconditionally.

Also dedupes the agentTrigger response-shaping in the task create/update
routes: TaskTriggerWorkflowResult already matches the response shape
field-for-field, so both routes now just do `agentTrigger: result` instead
of reconstructing the object, and import the exported type instead of
re-deriving it via Awaited<ReturnType<...>>.

Ownership-conflict tests moved from the two call-site test files (which now
mock the repository and have nothing conflict-specific to assert) to a new
suite directly against conversationRepository.createConversation — verified
RED against the pre-fix repository (both the short-circuit and the hijack
guard failed), GREEN against the fix.

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

* fix(security): let a scoped token read/edit/delete the personal event it created

Codex P2 (2nd round) on #1846: allowing a scoped MCP/OAuth token to create a
personal (driveless) calendar event (previous commit's finding #2 fix) is a
dead end without this — canAccessEvent/canEditEvent both explicitly denied
ANY scoped token ANY access to a driveless event, and the GET listing route
excluded personal events entirely for scoped auth. A scoped token could get
a 201 creating a personal event, then never list, read, update, or delete
it again.

Fixed by reordering the creator-identity check ahead of the "no identity
power over driveless events" denial in canAccessEvent and canEditEvent
(apps/web/src/app/api/calendar/events/[eventId]/route.ts) — a scoped token
still acts on behalf of its owning user, so it retains full access to a
driveless event it created itself, but still has zero power over a
different user's personal event (that denial still applies to non-creators).

The listing route (apps/web/src/app/api/calendar/events/route.ts) needed
two matching changes: the personal-events inclusion condition is already
scoped to `createdById = userId` so the blanket `!isScopedMCPAuth` exclusion
was unnecessary and is removed; the final scoped-token cap filter now keeps
a driveless event through when the caller created it, while still capping
out driveless events reachable only via the (identity-derived, not
drive-scoped) attendee branch.

RED→GREEN tests added for all three surfaces (GET single event, PATCH/DELETE
via canEditEvent, GET listing) — each verified failing against the pre-fix
ordering/filter, then passing against the fix.

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

---------

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