Skip to content

Conversation

@maceip
Copy link
Contributor

@maceip maceip commented Jul 11, 2025

Summary

This PR establishes the CI/CD infrastructure with GitHub Actions workflows for automated testing and building on pull requests and pushes.

Changes

  • Added GitHub Actions workflow for pull request validation
  • Added workflow for push events to main branch
  • Configured automated build and test processes
  • Set up proper Node.js and pnpm caching for faster builds

Commits

  • fb36d02 feat: Core infrastructure and CI/CD improvements

Summary by CodeRabbit

  • New Features

    • Introduced a robust user profile system with encrypted storage for passwords, bookmarks, history, and settings.
    • Added comprehensive update management, including scheduling, user activity detection, rollback, and notifications.
    • Implemented a centralized app configuration manager and enhanced analytics tracking.
    • Enabled persistent download tracking and management.
    • Provided utilities for debouncing, throttling, and performance monitoring.
    • Added detailed metadata structures for navigation, search, and suggestions.
  • Documentation

    • Added extensive markdown documentation for commands, planning, and pull request templates.
  • Chores

    • Set up GitHub Actions workflows for CI, linting, testing, and Slack notifications.
    • Introduced configuration files for command permissions and process automation.

- Add GitHub Actions workflows for CI/CD
- Add Claude integration for AI-powered development
- Implement user profile store with multi-profile support
- Add encryption service for secure data storage
- Refactor update service with improved scheduling and rollback
- Add user analytics service for cohort tracking
- Add performance monitoring utilities
- Update store infrastructure with initialization lifecycle

This is the foundational layer (1/6) from PR #45 that other features depend on.

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

Co-Authored-By: Claude <[email protected]>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jul 11, 2025

Walkthrough

This update introduces a wide-ranging set of new features, modules, and infrastructure for an Electron application and its development workflow. Major additions include a robust user profile store, update management services, analytics tracking, performance monitoring, new utility modules, and comprehensive workflow documentation and configuration for CI, PRs, and codebase research. The changes also add several new TypeScript interfaces and classes to support these features.

Changes

Files / Areas Change Summary
.claude/commands/commit.md, .claude/commands/learn.md, .claude/commands/plan.md,
.claude/commands/pr.md
Added detailed markdown documentation specifying structured workflows for commit creation, codebase research, implementation planning, and PR description generation.
.claude/settings.local.json Added a JSON config file defining allowed shell/web commands and patterns for command execution.
.claude/shared/pr_template.md Added a pull request template guiding contributors with structured sections and checklist.
.github/workflows/pr.yml, .github/workflows/push.yml, .github/workflows/slack-notifications.yml Added GitHub Actions workflows for PR and push events, including linting, testing, and Slack notifications via a reusable workflow.
apps/electron-app/src/main/config/app-config.ts Introduced centralized application configuration management with environment overrides and a singleton interface.
apps/electron-app/src/main/ipc/index.ts Registered new IPC handlers for user profile, password, autofill, top sites, and downloads services.
apps/electron-app/src/main/services/encryption-service.ts Added a singleton encryption service supporting Electron safeStorage and AES-256-CBC fallback, with migration and detection utilities.
apps/electron-app/src/main/services/update/activity-detector.ts,
update-notifier.ts,
update-rollback.ts,
update-scheduler.ts,
update-service.ts,
index.ts
Introduced a modular update management system: user activity detection, update notifications, rollback, scheduling, and the main update service with IPC integration and periodic tasks. Centralized exports in an index file.
apps/electron-app/src/main/services/user-analytics.ts Added a user analytics service for persistent user identification, cohort analysis, usage tracking, Sentry/Umami integration, and performance monitoring.
apps/electron-app/src/main/store/index.ts,
store.ts
Extended the main store with initialization lifecycle management, status querying, and cleanup.
apps/electron-app/src/main/store/profile-actions.ts Added high-level API functions for managing user profiles and associated data, abstracting store access.
apps/electron-app/src/main/store/types.ts Introduced a DownloadItem interface and added a downloads array to AppState. Removed obsolete imports/properties.
apps/electron-app/src/main/store/user-profile-store.ts Implemented a comprehensive Zustand-based user profile store with encrypted secure settings, navigation/download/bookmark/password/autofill management, import/export, session handling, and persistence.
apps/electron-app/src/main/utils/debounce.ts Added a robust debounce manager class and standalone debounce/throttle functions for rate-limiting function calls.
apps/electron-app/src/main/utils/performanceMonitor.ts Introduced a main process performance monitor for tracking and logging view update and chat resize timings.
apps/electron-app/src/types/metadata.ts Added a suite of TypeScript interfaces and type guards for suggestion/navigation metadata, with runtime validation helpers.

Sequence Diagram(s)

sequenceDiagram
    participant Renderer
    participant MainProcess
    participant UpdateService
    participant UpdateNotifier
    participant ActivityDetector
    participant UpdateScheduler
    participant UpdateRollback

    Renderer->>MainProcess: IPC - Check for updates
    MainProcess->>UpdateService: checkForUpdates()
    UpdateService->>UpdateNotifier: showUpdateNotification()
    Note right of UpdateService: Tracks download progress, schedules, and user activity
    UpdateService->>ActivityDetector: isUserInactive()
    alt User is inactive
        UpdateService->>UpdateScheduler: scheduleUpdate()
        UpdateScheduler-->>UpdateService: Confirmation
        UpdateService->>UpdateNotifier: showScheduledUpdateNotification()
    else User is active
        UpdateService->>UpdateNotifier: showUpdateReadyNotification()
    end
    UpdateService->>UpdateRollback: rollbackToVersion(version)
    UpdateRollback-->>UpdateService: Rollback result
    UpdateService->>Renderer: IPC - Update result/progress
Loading
sequenceDiagram
    participant App
    participant UserAnalyticsService
    participant Sentry
    participant Umami

    App->>UserAnalyticsService: initialize()
    App->>UserAnalyticsService: startFeatureTimer(feature)
    App->>UserAnalyticsService: endFeatureTimer(feature)
    UserAnalyticsService->>Sentry: Record breadcrumb/event
    UserAnalyticsService->>Umami: Track event
    App->>UserAnalyticsService: trackNavigation(event, data)
    App->>UserAnalyticsService: trackSessionEnd()
    UserAnalyticsService->>Sentry: Session stats/cohort tagging
    UserAnalyticsService->>Umami: Usage stats
Loading
sequenceDiagram
    participant App
    participant UserProfileStore
    participant EncryptionService
    participant Disk

    App->>UserProfileStore: initialize()
    UserProfileStore->>Disk: Load profiles JSON
    UserProfileStore->>EncryptionService: decrypt(secureSettings)
    App->>UserProfileStore: visitPage(url, title)
    App->>UserProfileStore: getPasswords()
    UserProfileStore->>EncryptionService: decrypt(passwords)
    App->>UserProfileStore: setSetting(key, value)
    UserProfileStore->>Disk: Save profiles JSON (debounced)
Loading

Poem

A flurry of changes, a hop and a leap,
New features and configs, no code left asleep!
Profiles and updates, analytics galore,
With debounce and metadata, performance we score.
CI runs swiftly, Slack gets the news,
This rabbit’s delighted—so much to peruse! 🐇✨
✨ Finishing Touches
  • 📝 Generate Docstrings

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

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@claude
Copy link

claude bot commented Jul 11, 2025

Claude finished @maceip's task —— View job


PR Review Complete

Review Checklist:

  • Analyze GitHub Actions workflows for security and best practices
  • Review new Electron app services (encryption, updates, analytics)
  • Evaluate store management implementation
  • Check for security vulnerabilities and concerns
  • Assess code quality and structure
  • Provide comprehensive feedback with recommendations

🔒 Security Concerns

Critical:

  • Hardcoded GitHub repo path: In .github/workflows/slack-notifications.yml:223, the GitHub API URL contains a placeholder (your-org/your-repo). This will cause runtime failures and should be updated to the actual repository path.
  • Encryption key fallback: The encryption service falls back to predictable keys based on environment variables. Consider using a more robust key derivation strategy.
  • Analytics data collection: The user analytics service collects extensive user data including usage patterns, session durations, and feature usage. Ensure this complies with privacy regulations.

Moderate:

  • Unencrypted local storage: User profile data, bookmarks, and passwords are stored locally with optional encryption. Consider making encryption mandatory for sensitive data.
  • Network requests: Update service fetches release notes from GitHub API without authentication, which could lead to rate limiting.

🏗️ Code Quality & Architecture

Strengths:

  • Well-structured service classes with clear separation of concerns
  • Proper error handling and logging throughout
  • Good use of TypeScript interfaces and type safety
  • Comprehensive debounce utility with proper cleanup
  • Singleton pattern appropriately used for services

Areas for Improvement:

  • Large file sizes: Some files exceed 600+ lines (e.g., user-analytics.ts, user-profile-store.ts). Consider breaking these into smaller, more focused modules.
  • Complex state management: The user profile store handles multiple concerns (navigation, downloads, passwords, bookmarks). Consider splitting into domain-specific stores.
  • Magic numbers: Several hardcoded values (timeouts, intervals, thresholds) should be moved to configuration.

🚀 Performance Considerations

Potential Issues:

  • Memory leaks: The debounce manager maintains static maps that could grow unbounded. Consider implementing automatic cleanup based on age.
  • Frequent file I/O: User analytics service writes to disk frequently. Consider batching writes or using in-memory caching.
  • Synchronous operations: Some encryption operations are synchronous, which could block the main thread.

Recommendations:

  • Implement periodic cleanup for debounce manager
  • Add memory usage monitoring alerts
  • Consider using Worker threads for CPU-intensive operations

🔧 GitHub Actions Workflow Review

Security:

  • Using pinned action versions (good practice)
  • Appropriate use of secrets for Slack notifications
  • Proper permissions scoping for PR workflow

Improvements:

  • Add dependency caching for faster builds
  • Consider adding security scanning (CodeQL, dependency scanning)
  • Add workflow dispatch triggers for manual runs

📊 Test Coverage

Missing Areas:

  • No visible test files for the new services
  • Complex business logic in analytics and encryption services lacks test coverage
  • Update service has multiple edge cases that should be tested

Recommendations:

  • Add unit tests for encryption service
  • Add integration tests for update workflow
  • Test error scenarios and edge cases

🐛 Potential Bugs

  1. Update service race condition: Line 372-374 in update-service.ts could cause race conditions when multiple updates are scheduled simultaneously.

  2. File path handling: Several services construct file paths without proper validation, which could lead to path traversal issues.

  3. Async/await inconsistency: Some promises are not properly awaited in the analytics service.

💡 Recommendations

High Priority:

  1. Fix the hardcoded GitHub repo path in workflows
  2. Add comprehensive error handling for file operations
  3. Implement proper cleanup for static managers
  4. Add unit tests for critical services

Medium Priority:

  1. Break down large files into smaller modules
  2. Add configuration management for magic numbers
  3. Implement rate limiting for external API calls
  4. Add performance monitoring for critical operations

Low Priority:

  1. Add JSDoc comments for public APIs
  2. Consider using a state management library like Redux Toolkit
  3. Add logging levels configuration
  4. Implement graceful shutdown handlers

🎯 Overall Assessment

This PR introduces significant infrastructure improvements with well-architected services. The code quality is generally good with proper TypeScript usage and error handling. However, there are security concerns that need immediate attention, particularly around hardcoded values and data privacy considerations.

Recommendation: Request changes to address the critical security issues before merging. The architecture is solid but needs refinement in key areas.


@michiosw
Copy link
Contributor

@maceip Can you clarify why there are 26k lines of changes in this PR for CI/CD? I noticed the CI/CD scripts already exist in main and seem duplicated here. Since the PR title is '[feat: Add CI/CD workflows and build infrastructure improvements]', should I assume most of these changes are infrastructure-related?

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 24

🔭 Outside diff range comments (1)
apps/electron-app/src/main/store/user-profile-store.ts (1)

1-2175: Consider breaking down this large store into smaller, focused modules.

This 2175-line file handles multiple responsibilities that could be separated:

  • Profile management
  • Navigation history
  • Download management
  • Import/export operations
  • Session management
  • Encryption operations

Consider splitting into:

  1. user-profile-store.ts - Core profile operations
  2. profile-navigation-store.ts - Navigation history
  3. profile-downloads-store.ts - Download management
  4. profile-import-export.ts - Import/export utilities
  5. profile-session-manager.ts - Session management

This would improve maintainability, testability, and code organization.

🧹 Nitpick comments (22)
.claude/settings.local.json (1)

31-34: Remove duplicate permission entries.

There are duplicate entries for pnpm build and git commit commands.

   "Bash(pnpm build:*)",
-  "Bash(pnpm build:*)",
-  "Bash(git commit:*)",
   "Bash(pnpm build:*)",
apps/electron-app/src/main/store/types.ts (1)

10-10: Consider timeline for moving to shared-types.

The comment indicates this is a temporary local definition. Consider creating an issue to track when this should be moved to shared-types to avoid it being forgotten.

Do you want me to help create a tracking issue for moving DownloadItem to shared-types?

.claude/commands/commit.md (1)

5-5: Fix markdown linting issues with trailing punctuation in headings.

The markdownlint tool flagged trailing colons in headings which should be removed for better markdown compliance.

-## Process:
+## Process

-## Important:
+## Important

-## Remember:
+## Remember

Also applies to: 29-29, 37-37

apps/electron-app/src/main/utils/performanceMonitor.ts (1)

54-54: Use more precise frame timing constant.

The hardcoded 16.67 should be defined as a constant for better maintainability and clarity.

+  private static readonly FRAME_TIME_60FPS = 1000 / 60; // 16.67ms
+
   // Log if update is slow
-  if (duration > 16.67) {
+  if (duration > MainProcessPerformanceMonitor.FRAME_TIME_60FPS) {
     // More than 1 frame at 60fps
apps/electron-app/src/main/services/update/activity-detector.ts (1)

125-133: Improve confidence score calculation for better suggestions.

The confidence calculation 1 - activity can result in very high confidence scores for hours with minimal activity, which might not be ideal for update scheduling.

Consider a more nuanced confidence scoring:

   inactiveHours.forEach(({ hour, activity }) => {
     const time = `${hour.toString().padStart(2, "0")}:00`;
-    const confidence = 1 - activity; // Higher confidence for lower activity
+    // Scale confidence based on activity level and add minimum threshold
+    const confidence = Math.max(0.1, Math.min(0.9, (1 - activity) * 0.8));

     suggestions.push({
       time,
       confidence,
       reason: `Low activity period (${(activity * 100).toFixed(1)}% of peak)`,
     });
   });
apps/electron-app/src/main/store/store.ts (2)

56-59: Consider encapsulating initialization state within the store object.

The module-level state variables could create issues if multiple store instances are needed in the future or during testing.

Consider moving the initialization state into the store interface:

-// Store initialization state
-let isInitialized = false;
-let initializationPromise: Promise<void> | null = null;
-let lastError: Error | null = null;
+interface StoreState {
+  isInitialized: boolean;
+  initializationPromise: Promise<void> | null;
+  lastError: Error | null;
+}
+
+const storeState: StoreState = {
+  isInitialized: false,
+  initializationPromise: null,
+  lastError: null,
+};

This would make the store more testable and prevent potential issues with shared state.


130-137: Consider preserving data during cleanup.

The cleanup method completely resets the store to initial state, which might not be desired in all scenarios. Consider whether this should be optional or if there should be a separate reset method.

 /**
- * Clean up the store state
+ * Clean up the store state and optionally reset data
  */
-const cleanup = (): void => {
+const cleanup = (resetData: boolean = true): void => {
   isInitialized = false;
   initializationPromise = null;
   lastError = null;

-  // Reset to initial state
-  zustandStore.setState(initialState, true);
+  if (resetData) {
+    // Reset to initial state
+    zustandStore.setState(initialState, true);
+  }
 };
.claude/commands/pr.md (2)

5-5: Fix markdown formatting: remove trailing punctuation from heading.

The heading has trailing punctuation which violates markdown linting rules.

-## Steps to follow:
+## Steps to follow

63-63: Fix markdown formatting: remove trailing punctuation from heading.

The heading has trailing punctuation which violates markdown linting rules.

-## Important notes:
+## Important notes
apps/electron-app/src/main/services/update/update-scheduler.ts (1)

129-131: Improve ID generation for better uniqueness and security.

The current ID generation using Math.random() could potentially create collisions and is not cryptographically secure.

Use Node.js crypto module for better uniqueness:

+import { randomBytes } from "crypto";

   private generateId(): string {
-    return `update_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+    return `update_${Date.now()}_${randomBytes(6).toString('hex')}`;
   }
.claude/commands/learn.md (4)

9-11: Add language specifier to code block.

-```
+```text
I'm ready to research the codebase. Please provide your research question or area of interest, and I'll analyze it thoroughly by exploring relevant components and connections.

41-49: Add language specifier to code block.

-     ```
+     ```text
     Research [specific component/pattern] in [directory/module]:

70-122: Add language specifier to markdown code block.

-     ```markdown
+     ```markdown
     ---

Note: The language specifier is already present, but ensure all other code blocks in lines 17, 59, 83, 118, 129, 139, and 153 have appropriate language specifiers added.


15-15: Remove redundant phrasing.

The phrase "steps to follow after" is redundant.

-## Steps to follow after receiving the research query:
+## Steps after receiving the research query:
apps/electron-app/src/main/utils/debounce.ts (1)

8-168: Consider refactoring static-only class to module functions.

The DebounceManager class contains only static members. This pattern can be confusing and goes against TypeScript best practices. Consider refactoring to module-level functions with a shared state object.

// Instead of a static-only class, use module-level state and functions
const debounceState = {
  timers: new Map<string, NodeJS.Timeout>(),
  callbacks: new Map<string, (...args: any[]) => any>(),
};

export function createManagedDebounce<T extends (...args: any[]) => any>(
  key: string,
  fn: T,
  delay: number = 300,
): (...args: Parameters<T>) => void {
  return (...args: Parameters<T>) => {
    clearDebounceTimer(key);
    debounceState.callbacks.set(key, () => fn(...args));
    
    const timer = setTimeout(() => {
      try {
        const callback = debounceState.callbacks.get(key);
        if (callback) {
          callback();
        }
      } catch (error) {
        logger.error(`Debounced function error for key '${key}':`, error);
      } finally {
        debounceState.timers.delete(key);
        debounceState.callbacks.delete(key);
      }
    }, delay);
    
    debounceState.timers.set(key, timer);
  };
}

export function cancelDebounce(key: string): boolean {
  return clearDebounceTimer(key);
}

// ... export other functions similarly

This approach:

  • Avoids the confusion of this in static context
  • Is more tree-shakeable
  • Follows functional programming patterns
  • Is easier to test
apps/electron-app/src/main/config/app-config.ts (2)

360-382: Enhance type safety in deepMerge method.

The deepMerge method uses any types which reduces type safety. Consider using generic types for better type preservation.

- private deepMerge(...objects: any[]): any {
-   const result: any = {};
+ private deepMerge<T extends Record<string, any>>(...objects: Partial<T>[]): T {
+   const result = {} as T;

    for (const obj of objects) {
      if (obj && typeof obj === "object") {
        for (const key in obj) {
          if (Object.prototype.hasOwnProperty.call(obj, key)) {
+           const objKey = key as keyof T;
            if (
-             typeof obj[key] === "object" &&
-             !Array.isArray(obj[key]) &&
-             obj[key] !== null
+             typeof obj[objKey] === "object" &&
+             !Array.isArray(obj[objKey]) &&
+             obj[objKey] !== null
            ) {
-             result[key] = this.deepMerge(result[key] || {}, obj[key]);
+             result[objKey] = this.deepMerge(
+               (result[objKey] || {}) as any,
+               obj[objKey] as any
+             ) as T[keyof T];
            } else {
-             result[key] = obj[key];
+             result[objKey] = obj[objKey] as T[keyof T];
            }
          }
        }
      }
    }

    return result;
  }

433-455: Add return type annotations to getter methods.

The configuration getter methods are missing return type annotations.

- public getPerformanceConfig() {
+ public getPerformanceConfig(): AppConfig['performance'] {
    return this.config.performance;
  }

- public getNetworkConfig() {
+ public getNetworkConfig(): AppConfig['network'] {
    return this.config.network;
  }

- public getUIConfig() {
+ public getUIConfig(): AppConfig['ui'] {
    return this.config.ui;
  }

- public getWorkersConfig() {
+ public getWorkersConfig(): AppConfig['workers'] {
    return this.config.workers;
  }

- public getDevelopmentConfig() {
+ public getDevelopmentConfig(): AppConfig['development'] {
    return this.config.development;
  }

- public getSecurityConfig() {
+ public getSecurityConfig(): AppConfig['security'] {
    return this.config.security;
  }
apps/electron-app/src/main/services/encryption-service.ts (1)

212-222: Improve encryption detection heuristic

The current patterns are too broad and may produce false positives. Consider checking for the specific format used by your encryption methods.

Since your fallback encryption uses the format {iv}:{encrypted}, check for this pattern:

 public isEncrypted(data: string): boolean {
   if (!data) {
     return false;
   }

-  // Simple heuristic: encrypted data is typically base64 or hex
-  const base64Pattern = /^[A-Za-z0-9+/]*={0,2}$/;
-  const hexPattern = /^[0-9a-fA-F]+$/;
-
-  return base64Pattern.test(data) || hexPattern.test(data);
+  // Check for safeStorage base64 format (min length for encrypted data)
+  const base64Pattern = /^[A-Za-z0-9+/]{20,}={0,2}$/;
+  
+  // Check for fallback format: {32 hex chars for IV}:{hex encrypted data}
+  const fallbackPattern = /^[0-9a-fA-F]{32}:[0-9a-fA-F]+$/;
+
+  return base64Pattern.test(data) || fallbackPattern.test(data);
 }
.claude/commands/plan.md (1)

17-17: Add language specifiers to fenced code blocks

Multiple code blocks are missing language specifiers, which affects syntax highlighting and readability.

Add appropriate language specifiers to all code blocks. For example:

  • Lines 17, 83, 153, 177, 297, 314, 458: Use ```text
  • Lines 47, 59, 118, 129, 139: Use ```text or ```bash depending on content

Example:

-```
+```text
 I'll help you create a detailed implementation plan. Let me start by understanding what we're building.

Also applies to: 47-47, 59-59, 83-83, 118-118, 129-129, 139-139, 153-153, 177-177, 297-297, 314-314, 458-458

apps/electron-app/src/main/services/update/update-notifier.ts (1)

23-25: Remove unnecessary constructor

The constructor doesn't perform any initialization and can be removed.

Remove the empty constructor:

 export class UpdateNotifier {
   private isInitialized = false;
   private notificationCallbacks: Map<string, () => void> = new Map();

-  constructor() {
-    // Initialize with default values
-  }
-
   public async initialize(): Promise<void> {
apps/electron-app/src/main/services/user-analytics.ts (1)

483-642: Consider extracting common breadcrumb logic

The monitorPerformance and monitorPerformanceSync methods have very similar breadcrumb creation logic that could be extracted to reduce duplication.

Extract common breadcrumb creation:

private createPerformanceBreadcrumb(
  category: string,
  operationName: string,
  operationId: string,
  context?: any,
  additionalData?: any
): void {
  Sentry.addBreadcrumb({
    category,
    message: `${category.includes('start') ? 'Started' : category.includes('success') ? 'Completed' : 'Failed'} ${operationName}`,
    level: category.includes('error') ? 'error' : 'info',
    data: {
      operationName,
      operationId,
      context,
      ...additionalData,
    },
  });
}

Then use it in both methods to reduce duplication.

apps/electron-app/src/types/metadata.ts (1)

276-310: Consider improving type safety in validation functions.

While the validation logic is correct, consider using the same pattern as suggested for the type guards to avoid as any casts.

   validateNavigationHistory(data: unknown): data is NavigationHistoryMetadata {
     if (typeof data !== "object" || data === null) return false;
 
-    const obj = data as any;
+    const obj = data as Record<string, unknown>;
     return (
       typeof obj.url === "string" &&
       typeof obj.visitCount === "number" &&
       typeof obj.lastVisit === "number" &&
-      obj.visitCount >= 0 &&
-      obj.lastVisit > 0
+      Number(obj.visitCount) >= 0 &&
+      Number(obj.lastVisit) > 0
     );
   },
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a75f9fd and 3780870.

📒 Files selected for processing (27)
  • .claude/commands/commit.md (1 hunks)
  • .claude/commands/learn.md (1 hunks)
  • .claude/commands/plan.md (1 hunks)
  • .claude/commands/pr.md (1 hunks)
  • .claude/settings.local.json (1 hunks)
  • .claude/shared/pr_template.md (1 hunks)
  • .github/workflows/pr.yml (1 hunks)
  • .github/workflows/push.yml (1 hunks)
  • .github/workflows/slack-notifications.yml (1 hunks)
  • apps/electron-app/src/main/config/app-config.ts (1 hunks)
  • apps/electron-app/src/main/ipc/index.ts (3 hunks)
  • apps/electron-app/src/main/services/encryption-service.ts (1 hunks)
  • apps/electron-app/src/main/services/update/activity-detector.ts (1 hunks)
  • apps/electron-app/src/main/services/update/index.ts (1 hunks)
  • apps/electron-app/src/main/services/update/update-notifier.ts (1 hunks)
  • apps/electron-app/src/main/services/update/update-rollback.ts (1 hunks)
  • apps/electron-app/src/main/services/update/update-scheduler.ts (1 hunks)
  • apps/electron-app/src/main/services/update/update-service.ts (1 hunks)
  • apps/electron-app/src/main/services/user-analytics.ts (1 hunks)
  • apps/electron-app/src/main/store/index.ts (2 hunks)
  • apps/electron-app/src/main/store/profile-actions.ts (1 hunks)
  • apps/electron-app/src/main/store/store.ts (3 hunks)
  • apps/electron-app/src/main/store/types.ts (2 hunks)
  • apps/electron-app/src/main/store/user-profile-store.ts (1 hunks)
  • apps/electron-app/src/main/utils/debounce.ts (1 hunks)
  • apps/electron-app/src/main/utils/performanceMonitor.ts (1 hunks)
  • apps/electron-app/src/types/metadata.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (15)
📓 Common learnings
Learnt from: maceip
PR: co-browser/vibe#38
File: apps/electron-app/src/main/services/profile-service.ts:131-135
Timestamp: 2025-07-01T18:24:21.101Z
Learning: In the ProfileService implementation (apps/electron-app/src/main/services/profile-service.ts), the team prefers to keep the updateProfile method simple without filtering out immutable fields like `id` and `createdAt` from updates, since these fields are not currently being used elsewhere in the codebase. They prioritize code simplicity over theoretical protection against field overwriting.
Learnt from: maceip
PR: co-browser/vibe#37
File: apps/electron-app/src/main/store/desktop-store.ts:0-0
Timestamp: 2025-07-01T18:22:27.574Z
Learning: In the desktop store implementation (apps/electron-app/src/main/store/desktop-store.ts), the team takes a performance-data-driven approach to optimization decisions. They prefer to keep the current implementation (including double encryption scenarios) until they can properly evaluate performance impact, rather than making premature optimizations.
apps/electron-app/src/main/ipc/index.ts (2)
Learnt from: maceip
PR: co-browser/vibe#38
File: apps/electron-app/src/main/services/profile-service.ts:131-135
Timestamp: 2025-07-01T18:24:21.101Z
Learning: In the ProfileService implementation (apps/electron-app/src/main/services/profile-service.ts), the team prefers to keep the updateProfile method simple without filtering out immutable fields like `id` and `createdAt` from updates, since these fields are not currently being used elsewhere in the codebase. They prioritize code simplicity over theoretical protection against field overwriting.
Learnt from: maceip
PR: co-browser/vibe#37
File: apps/electron-app/src/main/store/desktop-store.ts:0-0
Timestamp: 2025-07-01T18:22:27.574Z
Learning: In the desktop store implementation (apps/electron-app/src/main/store/desktop-store.ts), the team takes a performance-data-driven approach to optimization decisions. They prefer to keep the current implementation (including double encryption scenarios) until they can properly evaluate performance impact, rather than making premature optimizations.
apps/electron-app/src/main/utils/performanceMonitor.ts (1)
Learnt from: maceip
PR: co-browser/vibe#37
File: apps/electron-app/src/main/store/desktop-store.ts:0-0
Timestamp: 2025-07-01T18:22:27.574Z
Learning: In the desktop store implementation (apps/electron-app/src/main/store/desktop-store.ts), the team takes a performance-data-driven approach to optimization decisions. They prefer to keep the current implementation (including double encryption scenarios) until they can properly evaluate performance impact, rather than making premature optimizations.
apps/electron-app/src/main/store/index.ts (3)
Learnt from: maceip
PR: co-browser/vibe#37
File: apps/electron-app/src/main/store/desktop-store.ts:0-0
Timestamp: 2025-07-01T18:22:27.574Z
Learning: In the desktop store implementation (apps/electron-app/src/main/store/desktop-store.ts), the team takes a performance-data-driven approach to optimization decisions. They prefer to keep the current implementation (including double encryption scenarios) until they can properly evaluate performance impact, rather than making premature optimizations.
Learnt from: maceip
PR: co-browser/vibe#37
File: apps/electron-app/src/main/store/desktop-store.ts:254-261
Timestamp: 2025-07-01T15:07:58.879Z
Learning: In the desktop store implementation (apps/electron-app/src/main/store/desktop-store.ts), the team prefers to maintain key-level encryption even when it results in double encryption scenarios like in the UserDataRecover function. They want to keep the consistent encryption handling approach rather than optimizing for performance by storing some data unencrypted.
Learnt from: maceip
PR: co-browser/vibe#37
File: apps/electron-app/src/main/store/desktop-store.ts:277-294
Timestamp: 2025-07-01T15:07:39.741Z
Learning: In apps/electron-app/src/main/store/desktop-store.ts, the user prefers to maintain key-level encryption in the helper functions getRecoveredData and getAllRecoveredData rather than removing the double encryption approach.
apps/electron-app/src/main/services/update/index.ts (1)
Learnt from: maceip
PR: co-browser/vibe#38
File: apps/electron-app/src/main/services/profile-service.ts:131-135
Timestamp: 2025-07-01T18:24:21.101Z
Learning: In the ProfileService implementation (apps/electron-app/src/main/services/profile-service.ts), the team prefers to keep the updateProfile method simple without filtering out immutable fields like `id` and `createdAt` from updates, since these fields are not currently being used elsewhere in the codebase. They prioritize code simplicity over theoretical protection against field overwriting.
apps/electron-app/src/main/services/update/update-service.ts (1)
Learnt from: maceip
PR: co-browser/vibe#38
File: apps/electron-app/src/main/services/profile-service.ts:131-135
Timestamp: 2025-07-01T18:24:21.101Z
Learning: In the ProfileService implementation (apps/electron-app/src/main/services/profile-service.ts), the team prefers to keep the updateProfile method simple without filtering out immutable fields like `id` and `createdAt` from updates, since these fields are not currently being used elsewhere in the codebase. They prioritize code simplicity over theoretical protection against field overwriting.
apps/electron-app/src/main/services/user-analytics.ts (2)
Learnt from: maceip
PR: co-browser/vibe#38
File: apps/electron-app/src/main/services/profile-service.ts:131-135
Timestamp: 2025-07-01T18:24:21.101Z
Learning: In the ProfileService implementation (apps/electron-app/src/main/services/profile-service.ts), the team prefers to keep the updateProfile method simple without filtering out immutable fields like `id` and `createdAt` from updates, since these fields are not currently being used elsewhere in the codebase. They prioritize code simplicity over theoretical protection against field overwriting.
Learnt from: maceip
PR: co-browser/vibe#37
File: apps/electron-app/src/main/store/desktop-store.ts:0-0
Timestamp: 2025-07-01T18:22:27.574Z
Learning: In the desktop store implementation (apps/electron-app/src/main/store/desktop-store.ts), the team takes a performance-data-driven approach to optimization decisions. They prefer to keep the current implementation (including double encryption scenarios) until they can properly evaluate performance impact, rather than making premature optimizations.
apps/electron-app/src/main/services/update/update-rollback.ts (1)
Learnt from: maceip
PR: co-browser/vibe#38
File: apps/electron-app/src/main/services/profile-service.ts:131-135
Timestamp: 2025-07-01T18:24:21.101Z
Learning: In the ProfileService implementation (apps/electron-app/src/main/services/profile-service.ts), the team prefers to keep the updateProfile method simple without filtering out immutable fields like `id` and `createdAt` from updates, since these fields are not currently being used elsewhere in the codebase. They prioritize code simplicity over theoretical protection against field overwriting.
apps/electron-app/src/main/store/store.ts (3)
Learnt from: maceip
PR: co-browser/vibe#37
File: apps/electron-app/src/main/store/desktop-store.ts:0-0
Timestamp: 2025-07-01T18:22:27.574Z
Learning: In the desktop store implementation (apps/electron-app/src/main/store/desktop-store.ts), the team takes a performance-data-driven approach to optimization decisions. They prefer to keep the current implementation (including double encryption scenarios) until they can properly evaluate performance impact, rather than making premature optimizations.
Learnt from: maceip
PR: co-browser/vibe#37
File: apps/electron-app/src/main/store/desktop-store.ts:254-261
Timestamp: 2025-07-01T15:07:58.879Z
Learning: In the desktop store implementation (apps/electron-app/src/main/store/desktop-store.ts), the team prefers to maintain key-level encryption even when it results in double encryption scenarios like in the UserDataRecover function. They want to keep the consistent encryption handling approach rather than optimizing for performance by storing some data unencrypted.
Learnt from: maceip
PR: co-browser/vibe#37
File: apps/electron-app/src/main/store/desktop-store.ts:277-294
Timestamp: 2025-07-01T15:07:39.741Z
Learning: In apps/electron-app/src/main/store/desktop-store.ts, the user prefers to maintain key-level encryption in the helper functions getRecoveredData and getAllRecoveredData rather than removing the double encryption approach.
apps/electron-app/src/main/services/encryption-service.ts (4)
Learnt from: maceip
PR: co-browser/vibe#37
File: apps/electron-app/src/main/store/desktop-store.ts:277-294
Timestamp: 2025-07-01T15:07:39.741Z
Learning: In apps/electron-app/src/main/store/desktop-store.ts, the user prefers to maintain key-level encryption in the helper functions getRecoveredData and getAllRecoveredData rather than removing the double encryption approach.
Learnt from: maceip
PR: co-browser/vibe#37
File: apps/electron-app/src/main/store/desktop-store.ts:254-261
Timestamp: 2025-07-01T15:07:58.879Z
Learning: In the desktop store implementation (apps/electron-app/src/main/store/desktop-store.ts), the team prefers to maintain key-level encryption even when it results in double encryption scenarios like in the UserDataRecover function. They want to keep the consistent encryption handling approach rather than optimizing for performance by storing some data unencrypted.
Learnt from: maceip
PR: co-browser/vibe#37
File: apps/electron-app/src/main/store/desktop-store.ts:0-0
Timestamp: 2025-07-01T18:22:27.574Z
Learning: In the desktop store implementation (apps/electron-app/src/main/store/desktop-store.ts), the team takes a performance-data-driven approach to optimization decisions. They prefer to keep the current implementation (including double encryption scenarios) until they can properly evaluate performance impact, rather than making premature optimizations.
Learnt from: maceip
PR: co-browser/vibe#38
File: apps/electron-app/src/main/services/profile-service.ts:131-135
Timestamp: 2025-07-01T18:24:21.101Z
Learning: In the ProfileService implementation (apps/electron-app/src/main/services/profile-service.ts), the team prefers to keep the updateProfile method simple without filtering out immutable fields like `id` and `createdAt` from updates, since these fields are not currently being used elsewhere in the codebase. They prioritize code simplicity over theoretical protection against field overwriting.
apps/electron-app/src/main/store/types.ts (2)
Learnt from: maceip
PR: co-browser/vibe#37
File: apps/electron-app/src/main/store/desktop-store.ts:0-0
Timestamp: 2025-07-01T18:22:27.574Z
Learning: In the desktop store implementation (apps/electron-app/src/main/store/desktop-store.ts), the team takes a performance-data-driven approach to optimization decisions. They prefer to keep the current implementation (including double encryption scenarios) until they can properly evaluate performance impact, rather than making premature optimizations.
Learnt from: maceip
PR: co-browser/vibe#37
File: apps/electron-app/src/main/store/desktop-store.ts:254-261
Timestamp: 2025-07-01T15:07:58.879Z
Learning: In the desktop store implementation (apps/electron-app/src/main/store/desktop-store.ts), the team prefers to maintain key-level encryption even when it results in double encryption scenarios like in the UserDataRecover function. They want to keep the consistent encryption handling approach rather than optimizing for performance by storing some data unencrypted.
apps/electron-app/src/main/config/app-config.ts (3)
Learnt from: maceip
PR: co-browser/vibe#37
File: apps/electron-app/src/main/store/desktop-store.ts:0-0
Timestamp: 2025-07-01T18:22:27.574Z
Learning: In the desktop store implementation (apps/electron-app/src/main/store/desktop-store.ts), the team takes a performance-data-driven approach to optimization decisions. They prefer to keep the current implementation (including double encryption scenarios) until they can properly evaluate performance impact, rather than making premature optimizations.
Learnt from: maceip
PR: co-browser/vibe#37
File: apps/electron-app/src/main/store/desktop-store.ts:277-294
Timestamp: 2025-07-01T15:07:39.741Z
Learning: In apps/electron-app/src/main/store/desktop-store.ts, the user prefers to maintain key-level encryption in the helper functions getRecoveredData and getAllRecoveredData rather than removing the double encryption approach.
Learnt from: maceip
PR: co-browser/vibe#37
File: apps/electron-app/src/main/store/desktop-store.ts:254-261
Timestamp: 2025-07-01T15:07:58.879Z
Learning: In the desktop store implementation (apps/electron-app/src/main/store/desktop-store.ts), the team prefers to maintain key-level encryption even when it results in double encryption scenarios like in the UserDataRecover function. They want to keep the consistent encryption handling approach rather than optimizing for performance by storing some data unencrypted.
apps/electron-app/src/main/store/user-profile-store.ts (4)
Learnt from: maceip
PR: co-browser/vibe#37
File: apps/electron-app/src/main/store/desktop-store.ts:254-261
Timestamp: 2025-07-01T15:07:58.879Z
Learning: In the desktop store implementation (apps/electron-app/src/main/store/desktop-store.ts), the team prefers to maintain key-level encryption even when it results in double encryption scenarios like in the UserDataRecover function. They want to keep the consistent encryption handling approach rather than optimizing for performance by storing some data unencrypted.
Learnt from: maceip
PR: co-browser/vibe#37
File: apps/electron-app/src/main/store/desktop-store.ts:0-0
Timestamp: 2025-07-01T18:22:27.574Z
Learning: In the desktop store implementation (apps/electron-app/src/main/store/desktop-store.ts), the team takes a performance-data-driven approach to optimization decisions. They prefer to keep the current implementation (including double encryption scenarios) until they can properly evaluate performance impact, rather than making premature optimizations.
Learnt from: maceip
PR: co-browser/vibe#37
File: apps/electron-app/src/main/store/desktop-store.ts:277-294
Timestamp: 2025-07-01T15:07:39.741Z
Learning: In apps/electron-app/src/main/store/desktop-store.ts, the user prefers to maintain key-level encryption in the helper functions getRecoveredData and getAllRecoveredData rather than removing the double encryption approach.
Learnt from: maceip
PR: co-browser/vibe#38
File: apps/electron-app/src/main/services/profile-service.ts:131-135
Timestamp: 2025-07-01T18:24:21.101Z
Learning: In the ProfileService implementation (apps/electron-app/src/main/services/profile-service.ts), the team prefers to keep the updateProfile method simple without filtering out immutable fields like `id` and `createdAt` from updates, since these fields are not currently being used elsewhere in the codebase. They prioritize code simplicity over theoretical protection against field overwriting.
apps/electron-app/src/main/services/update/update-scheduler.ts (1)
Learnt from: maceip
PR: co-browser/vibe#38
File: apps/electron-app/src/main/services/profile-service.ts:131-135
Timestamp: 2025-07-01T18:24:21.101Z
Learning: In the ProfileService implementation (apps/electron-app/src/main/services/profile-service.ts), the team prefers to keep the updateProfile method simple without filtering out immutable fields like `id` and `createdAt` from updates, since these fields are not currently being used elsewhere in the codebase. They prioritize code simplicity over theoretical protection against field overwriting.
apps/electron-app/src/main/store/profile-actions.ts (4)
Learnt from: maceip
PR: co-browser/vibe#38
File: apps/electron-app/src/main/services/profile-service.ts:131-135
Timestamp: 2025-07-01T18:24:21.101Z
Learning: In the ProfileService implementation (apps/electron-app/src/main/services/profile-service.ts), the team prefers to keep the updateProfile method simple without filtering out immutable fields like `id` and `createdAt` from updates, since these fields are not currently being used elsewhere in the codebase. They prioritize code simplicity over theoretical protection against field overwriting.
Learnt from: maceip
PR: co-browser/vibe#37
File: apps/electron-app/src/main/store/desktop-store.ts:0-0
Timestamp: 2025-07-01T18:22:27.574Z
Learning: In the desktop store implementation (apps/electron-app/src/main/store/desktop-store.ts), the team takes a performance-data-driven approach to optimization decisions. They prefer to keep the current implementation (including double encryption scenarios) until they can properly evaluate performance impact, rather than making premature optimizations.
Learnt from: maceip
PR: co-browser/vibe#37
File: apps/electron-app/src/main/store/desktop-store.ts:254-261
Timestamp: 2025-07-01T15:07:58.879Z
Learning: In the desktop store implementation (apps/electron-app/src/main/store/desktop-store.ts), the team prefers to maintain key-level encryption even when it results in double encryption scenarios like in the UserDataRecover function. They want to keep the consistent encryption handling approach rather than optimizing for performance by storing some data unencrypted.
Learnt from: maceip
PR: co-browser/vibe#37
File: apps/electron-app/src/main/store/desktop-store.ts:277-294
Timestamp: 2025-07-01T15:07:39.741Z
Learning: In apps/electron-app/src/main/store/desktop-store.ts, the user prefers to maintain key-level encryption in the helper functions getRecoveredData and getAllRecoveredData rather than removing the double encryption approach.
🧬 Code Graph Analysis (1)
apps/electron-app/src/main/services/encryption-service.ts (1)
packages/shared-types/src/logger/index.ts (1)
  • createLogger (65-76)
🪛 GitHub Actions: CI
apps/electron-app/src/main/ipc/index.ts

[error] 41-41: TypeScript error TS2307: Cannot find module '@/ipc/browser/download' or its corresponding type declarations.

🪛 GitHub Actions: pull_request
apps/electron-app/src/main/ipc/index.ts

[error] 41-41: TypeScript error TS2307: Cannot find module '@/ipc/browser/download' or its corresponding type declarations.

🪛 LanguageTool
.claude/commands/learn.md

[style] ~15-~15: This phrase is redundant. Consider writing “follow”.
Context: ...the user's research query. ## Steps to follow after receiving the research query: 1. **Rea...

(FOLLOW_AFTER)

🪛 markdownlint-cli2 (0.17.2)
.claude/commands/learn.md

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

(MD040, fenced-code-language)


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

(MD040, fenced-code-language)


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

(MD040, fenced-code-language)


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

(MD040, fenced-code-language)


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

(MD040, fenced-code-language)


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

(MD040, fenced-code-language)


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

(MD040, fenced-code-language)


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

(MD040, fenced-code-language)

.claude/commands/plan.md

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

(MD040, fenced-code-language)


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

(MD040, fenced-code-language)


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

(MD040, fenced-code-language)


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

(MD040, fenced-code-language)


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

(MD040, fenced-code-language)


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

(MD040, fenced-code-language)


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

(MD040, fenced-code-language)


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

(MD040, fenced-code-language)


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

(MD040, fenced-code-language)


243-243: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)


245-245: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)


253-253: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)


270-270: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)


275-275: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)


279-279: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)


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

(MD040, fenced-code-language)


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

(MD040, fenced-code-language)


397-397: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)


405-405: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)


413-413: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)


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

(MD040, fenced-code-language)

.claude/commands/pr.md

5-5: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)


63-63: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)

.claude/commands/commit.md

5-5: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)


29-29: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)


37-37: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)

🪛 Biome (1.9.4)
apps/electron-app/src/main/utils/debounce.ts

[error] 8-168: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)


[error] 22-22: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 25-25: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 30-30: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 38-38: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 39-39: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 43-43: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 55-55: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 62-62: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 71-71: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 72-72: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 84-84: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 87-87: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 88-88: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 98-98: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 105-105: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 112-112: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 114-114: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 133-133: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 136-136: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 148-148: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 150-150: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 151-151: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 152-152: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 164-164: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 165-165: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)

apps/electron-app/src/main/services/update/update-notifier.ts

[error] 23-25: This constructor is unnecessary.

Unsafe fix: Remove the unnecessary constructor.

(lint/complexity/noUselessConstructor)

apps/electron-app/src/types/metadata.ts

[error] 191-271: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)

apps/electron-app/src/main/store/user-profile-store.ts

[error] 2124-2124: Avoid the delete operator which can impact performance.

Unsafe fix: Use an undefined assignment instead.

(lint/performance/noDelete)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: claude-review
🔇 Additional comments (19)
.claude/shared/pr_template.md (1)

12-21: Well-structured PR template sections.

The template provides clear guidance for contributors with appropriate sections for describing changes, verification steps, and changelog entries.

.claude/settings.local.json (1)

3-50: Comprehensive permission configuration for development workflow.

The permission policy appropriately covers the necessary development tools and operations while maintaining security with domain-restricted web fetches.

apps/electron-app/src/main/ipc/index.ts (1)

68-88: Well-structured IPC handler registration and initialization.

The new handler registrations for profile history, password management, autofill, and top sites are properly organized. The downloads service initialization and test logging provide good debugging information.

apps/electron-app/src/main/services/update/index.ts (1)

1-15: Clean and well-organized service index.

The index file provides a clean API surface for the update services with proper separation of class exports and type exports.

apps/electron-app/src/main/store/types.ts (2)

11-20: Well-designed DownloadItem interface.

The interface comprehensively covers download tracking needs with appropriate state management and progress tracking properties.


31-31: Clean integration of downloads into AppState.

The addition of the downloads array to AppState enables proper IPC synchronization for download management.

.claude/commands/commit.md (1)

1-43: Excellent commit documentation structure!

This documentation provides a comprehensive and well-structured approach to commit management with clear guidelines for atomic commits, proper messaging, and user attribution. The 4-step process is logical and the important notes about avoiding co-author attribution are crucial for proper git history.

.github/workflows/slack-notifications.yml (1)

1-87: Well-structured reusable workflow for Slack notifications.

The workflow correctly implements the reusable workflow pattern with proper input validation and secret handling. The conditional logic for success/failure messages is appropriate.

apps/electron-app/src/main/store/index.ts (2)

18-22: Well-designed initialization status interface.

The StoreInitializationStatus interface provides comprehensive state tracking with clear boolean flags and error handling, which is essential for reliable asynchronous store initialization.


39-45: Proper optional lifecycle methods in Store interface.

The addition of optional initialization and cleanup methods to the Store interface follows good TypeScript practices and provides a clean API for store lifecycle management. The optional nature allows for backward compatibility.

.github/workflows/pr.yml (1)

78-78: Robust status calculation for Slack notifications.

The boolean logic for combining job results into a single status is well-implemented, ensuring notifications accurately reflect the overall workflow state.

.github/workflows/push.yml (1)

1-85: Consistent and well-structured CI workflow for main branch.

The workflow maintains consistency with the PR workflow while appropriately targeting the main branch. The job dependencies and Slack notification integration are properly configured.

apps/electron-app/src/main/store/store.ts (1)

64-91: LGTM! Well-implemented async initialization pattern.

The initialization logic properly handles:

  • Preventing multiple concurrent initializations
  • Proper error handling and cleanup
  • Promise reuse for concurrent callers
  • State management with proper cleanup

The implementation follows good async patterns and aligns with the team's preference for keeping implementations straightforward until performance data suggests optimization is needed.

.claude/commands/pr.md (1)

38-43: LGTM! Comprehensive verification handling.

The verification workflow is well-designed:

  • Attempts to run automated verification commands
  • Properly handles both passing and failing tests
  • Clearly separates manual verification steps
  • Documents incomplete verification for user follow-up

This approach ensures thorough testing while maintaining transparency about what has been verified.

apps/electron-app/src/main/services/update/update-scheduler.ts (1)

133-148: LGTM! Well-implemented cleanup with proper age-based filtering.

The cleanup method properly:

  • Uses appropriate time threshold (30 days)
  • Only removes completed updates
  • Persists changes after cleanup
  • Provides appropriate logging

The implementation aligns with the team's preference for straightforward, maintainable code.

apps/electron-app/src/main/services/update/update-rollback.ts (1)

212-229: Implement actual Linux rollback logic.

The Linux rollback implementation is also a placeholder.

apps/electron-app/src/main/store/profile-actions.ts (1)

1-276: Well-designed profile management API

This module provides an excellent abstraction layer over the profile store with:

  • Clear, intuitive function names
  • Comprehensive JSDoc documentation
  • Helpful usage examples
  • Good separation of concerns by category

The API design makes it easy for other parts of the application to interact with user profiles without dealing with internal complexity.

apps/electron-app/src/types/metadata.ts (1)

6-174: Well-structured metadata interfaces with comprehensive type definitions.

The interfaces are properly documented, use appropriate types, and follow a consistent pattern. Good use of union types for constrained values and proper extension of the BaseMetadata interface.

apps/electron-app/src/main/store/user-profile-store.ts (1)

1819-1907: Excellent initialization pattern with proper race condition handling.

The initialization implementation properly handles:

  • Race conditions with initialization promises
  • App readiness checks
  • Error recovery with fallback to default profile
  • Proper cleanup on shutdown

This aligns well with the team's preference for data-driven approaches as mentioned in the learnings.

pull request for inclusion in the changelog:
-->

-->
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix the malformed HTML comment.

There's a closing comment tag without a corresponding opening tag.

--->

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In .claude/shared/pr_template.md at line 27, there is a closing HTML comment tag
without a matching opening tag, causing malformed HTML. Remove the standalone
closing comment tag or add the appropriate opening comment tag before it to
properly balance the comment syntax.

Comment on lines +2 to +3
Please make sure you've read and understood our contributing guidelines;
https://github.com/humanlayer/humanlayer/blob/master/CONTRIBUTING.md
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix the repository reference URL.

The template references humanlayer/humanlayer but this appears to be for the co-browser/vibe repository based on the PR objectives.

-https://github.com/humanlayer/humanlayer/blob/master/CONTRIBUTING.md
+https://github.com/co-browser/vibe/blob/master/CONTRIBUTING.md
📝 Committable suggestion

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

Suggested change
Please make sure you've read and understood our contributing guidelines;
https://github.com/humanlayer/humanlayer/blob/master/CONTRIBUTING.md
Please make sure you've read and understood our contributing guidelines;
https://github.com/co-browser/vibe/blob/master/CONTRIBUTING.md
🤖 Prompt for AI Agents
In .claude/shared/pr_template.md around lines 2 to 3, the contributing
guidelines URL references the wrong repository (humanlayer/humanlayer). Update
the URL to point to the correct repository for this project, which is
co-browser/vibe, ensuring the link directs to the appropriate CONTRIBUTING.md
file for this repository.

Comment on lines +44 to +46
// MCP APIs - direct imports (register themselves)
import "@/ipc/mcp/mcp-status";

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove duplicate MCP API imports.

The MCP APIs are imported twice - once at lines 44-46 and again at lines 56-57.

-// MCP APIs - direct imports (register themselves)
-import "@/ipc/mcp/mcp-status";
-

Also applies to: 56-57

🤖 Prompt for AI Agents
In apps/electron-app/src/main/ipc/index.ts at lines 44-46 and 56-57, the MCP API
imports are duplicated. Remove one of the duplicate import statements to ensure
each MCP API is imported only once, keeping the import that best fits the module
organization and removing the redundant one.

import "@/ipc/browser/windows";
import "@/ipc/browser/navigation";
import "@/ipc/browser/content";
import { downloads } from "@/ipc/browser/download";
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Fix missing module causing pipeline failures.

The import @/ipc/browser/download is causing TypeScript errors as reported in pipeline failures.


🏁 Script executed:

#!/bin/bash
# Check if the download module exists
fd "download" apps/electron-app/src/main/ipc/browser/

Length of output: 53


🏁 Script executed:

#!/bin/bash
# List all files in the `ipc/browser` directory to verify available modules
ls apps/electron-app/src/main/ipc/browser/

# Search the entire repo for any `.ts` file matching "download" or "downloads"
fd -t f -e ts download .
fd -t f -e ts downloads .

Length of output: 153


🏁 Script executed:

#!/bin/bash
# List all import statements in the IPC index file with line numbers
rg -n "^import" --color=never apps/electron-app/src/main/ipc/index.ts

# Show lines 30–90 for context around duplicate imports and handler registrations
sed -n '30,90p' apps/electron-app/src/main/ipc/index.ts

Length of output: 3643


Fix missing download module and remove duplicate MCP imports

The import @/ipc/browser/download does not exist and is causing build failures. Additionally, @/ipc/mcp/mcp-status is imported twice.

• Create or correct apps/electron-app/src/main/ipc/browser/download.ts (or update the import path) so that
import { downloads } from "@/ipc/browser/download"; resolves successfully.
• Remove the duplicate MCP-status import (second occurrence at line 57) in
apps/electron-app/src/main/ipc/index.ts.

🧰 Tools
🪛 GitHub Actions: CI

[error] 41-41: TypeScript error TS2307: Cannot find module '@/ipc/browser/download' or its corresponding type declarations.

🪛 GitHub Actions: pull_request

[error] 41-41: TypeScript error TS2307: Cannot find module '@/ipc/browser/download' or its corresponding type declarations.

🤖 Prompt for AI Agents
In apps/electron-app/src/main/ipc/index.ts at line 41, the import for
"@/ipc/browser/download" is missing or incorrect, causing build failures. Ensure
that the file apps/electron-app/src/main/ipc/browser/download.ts exists and
exports "downloads" properly, or update the import path to the correct location.
Additionally, remove the duplicate import of "@/ipc/mcp/mcp-status" found at
line 57 to avoid redundancy.

Comment on lines +84 to +85
response=$(curl -s -X POST -H 'Content-type: application/json; charset=utf-8' --data "$payload" https://slack.com/api/chat.postMessage -H "Authorization: Bearer ${{ secrets.SLACK_BOT_TOKEN }}" )
echo "Slack API response: $response"
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling and response validation for Slack API calls.

The current implementation doesn't validate the Slack API response or handle potential failures, which could lead to silent notification failures.

-          response=$(curl -s -X POST -H 'Content-type: application/json; charset=utf-8' --data "$payload" https://slack.com/api/chat.postMessage -H "Authorization: Bearer ${{ secrets.SLACK_BOT_TOKEN }}" )
-          echo "Slack API response: $response"
+          response=$(curl -s -X POST -H 'Content-type: application/json; charset=utf-8' --data "$payload" https://slack.com/api/chat.postMessage -H "Authorization: Bearer ${{ secrets.SLACK_BOT_TOKEN }}" )
+          echo "Slack API response: $response"
+          
+          # Validate Slack API response
+          if ! echo "$response" | jq -e '.ok == true' > /dev/null; then
+            echo "Slack API call failed"
+            echo "$response" | jq -r '.error // "Unknown error"'
+            exit 1
+          fi
+          
+          echo "Slack notification sent successfully"
📝 Committable suggestion

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

Suggested change
response=$(curl -s -X POST -H 'Content-type: application/json; charset=utf-8' --data "$payload" https://slack.com/api/chat.postMessage -H "Authorization: Bearer ${{ secrets.SLACK_BOT_TOKEN }}" )
echo "Slack API response: $response"
response=$(curl -s -X POST \
-H 'Content-type: application/json; charset=utf-8' \
--data "$payload" \
https://slack.com/api/chat.postMessage \
-H "Authorization: Bearer ${{ secrets.SLACK_BOT_TOKEN }}" )
echo "Slack API response: $response"
# Validate Slack API response
if ! echo "$response" | jq -e '.ok == true' > /dev/null; then
echo "Slack API call failed"
echo "$response" | jq -r '.error // "Unknown error"'
exit 1
fi
echo "Slack notification sent successfully"
🤖 Prompt for AI Agents
In .github/workflows/slack-notifications.yml at lines 84-85, the Slack API call
response is not validated or checked for errors. Modify the script to parse the
JSON response from the Slack API, verify if the message was sent successfully by
checking the "ok" field, and handle any errors by logging them or failing the
workflow step accordingly to prevent silent failures.

Comment on lines +28 to +66
private generateSecureFallbackKey(): string {
try {
// Use environment variable if available, otherwise derive from machine-specific data
const envKey = process.env.VIBE_ENCRYPTION_KEY;
if (envKey && envKey.length >= 32) {
return envKey.substring(0, 32);
}

// Derive key from machine-specific data
const machineId =
process.env.HOSTNAME || process.env.COMPUTERNAME || "vibe-default";
const appVersion = process.env.npm_package_version || "1.0.0";
const platform = process.platform;

// Combine machine-specific data
const keyMaterial = `${machineId}-${platform}-${appVersion}-vibe-encryption`;

// Use PBKDF2 to derive a secure key
const salt = Buffer.from("vibe-salt-2024", "utf8");
const derivedKey = pbkdf2Sync(keyMaterial, salt, 100000, 32, "sha256");

return derivedKey.toString("hex").substring(0, 32);
} catch {
logger.warn(
"Failed to generate secure fallback key, using minimal fallback",
);
// Last resort fallback - still better than hardcoded
const timestamp = Date.now().toString();
return pbkdf2Sync(
`vibe-${timestamp}`,
"fallback-salt",
10000,
32,
"sha256",
)
.toString("hex")
.substring(0, 32);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Critical: Incorrect key truncation reduces encryption strength

The substring(0, 32) operations on hex-encoded keys only extract 16 bytes worth of key material instead of the intended 32 bytes. A 32-byte key produces 64 hexadecimal characters when encoded.

This reduces the effective key size from 256 bits to 128 bits, significantly weakening the encryption.

Apply this fix to properly handle 32-byte keys:

 private generateSecureFallbackKey(): string {
   try {
     // Use environment variable if available, otherwise derive from machine-specific data
     const envKey = process.env.VIBE_ENCRYPTION_KEY;
     if (envKey && envKey.length >= 32) {
-      return envKey.substring(0, 32);
+      // If the key is hex-encoded, ensure we get 32 bytes (64 hex chars)
+      // If it's raw, take first 32 characters
+      return envKey.length >= 64 && /^[0-9a-fA-F]+$/.test(envKey) 
+        ? envKey.substring(0, 64) 
+        : envKey.substring(0, 32);
     }

     // ... existing code ...

     // Use PBKDF2 to derive a secure key
     const salt = Buffer.from("vibe-salt-2024", "utf8");
     const derivedKey = pbkdf2Sync(keyMaterial, salt, 100000, 32, "sha256");

-    return derivedKey.toString("hex").substring(0, 32);
+    // Return the full hex-encoded 32-byte key (64 hex characters)
+    return derivedKey.toString("hex");
   } catch {
     // ... existing code ...
     return pbkdf2Sync(
       `vibe-${timestamp}`,
       "fallback-salt",
       10000,
       32,
       "sha256",
     )
-      .toString("hex")
-      .substring(0, 32);
+      .toString("hex");
   }
 }
📝 Committable suggestion

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

Suggested change
private generateSecureFallbackKey(): string {
try {
// Use environment variable if available, otherwise derive from machine-specific data
const envKey = process.env.VIBE_ENCRYPTION_KEY;
if (envKey && envKey.length >= 32) {
return envKey.substring(0, 32);
}
// Derive key from machine-specific data
const machineId =
process.env.HOSTNAME || process.env.COMPUTERNAME || "vibe-default";
const appVersion = process.env.npm_package_version || "1.0.0";
const platform = process.platform;
// Combine machine-specific data
const keyMaterial = `${machineId}-${platform}-${appVersion}-vibe-encryption`;
// Use PBKDF2 to derive a secure key
const salt = Buffer.from("vibe-salt-2024", "utf8");
const derivedKey = pbkdf2Sync(keyMaterial, salt, 100000, 32, "sha256");
return derivedKey.toString("hex").substring(0, 32);
} catch {
logger.warn(
"Failed to generate secure fallback key, using minimal fallback",
);
// Last resort fallback - still better than hardcoded
const timestamp = Date.now().toString();
return pbkdf2Sync(
`vibe-${timestamp}`,
"fallback-salt",
10000,
32,
"sha256",
)
.toString("hex")
.substring(0, 32);
}
}
private generateSecureFallbackKey(): string {
try {
// Use environment variable if available, otherwise derive from machine-specific data
const envKey = process.env.VIBE_ENCRYPTION_KEY;
if (envKey && envKey.length >= 32) {
// If the key is hex-encoded, ensure we get 32 bytes (64 hex chars)
// If it's raw, take the first 32 characters
return envKey.length >= 64 && /^[0-9a-fA-F]+$/.test(envKey)
? envKey.substring(0, 64)
: envKey.substring(0, 32);
}
// Derive key from machine-specific data
const machineId =
process.env.HOSTNAME || process.env.COMPUTERNAME || "vibe-default";
const appVersion = process.env.npm_package_version || "1.0.0";
const platform = process.platform;
// Combine machine-specific data
const keyMaterial = `${machineId}-${platform}-${appVersion}-vibe-encryption`;
// Use PBKDF2 to derive a secure key
const salt = Buffer.from("vibe-salt-2024", "utf8");
const derivedKey = pbkdf2Sync(keyMaterial, salt, 100000, 32, "sha256");
// Return the full hex-encoded 32-byte key (64 hex characters)
return derivedKey.toString("hex");
} catch {
logger.warn(
"Failed to generate secure fallback key, using minimal fallback",
);
// Last resort fallback – still better than hardcoded
const timestamp = Date.now().toString();
return pbkdf2Sync(
`vibe-${timestamp}`,
"fallback-salt",
10000,
"sha256",
).toString("hex");
}
}
🤖 Prompt for AI Agents
In apps/electron-app/src/main/services/encryption-service.ts between lines 28
and 66, the code incorrectly truncates the hex-encoded derived key to 32
characters, which only represents 16 bytes instead of the intended 32 bytes. To
fix this, remove the substring(0, 32) calls on the hex strings and instead
return the full 64-character hex string representing the full 32-byte key. This
ensures the encryption key maintains its full 256-bit strength.

Comment on lines +150 to +193
private encryptWithFallback(data: string): string {
try {
const algorithm = "aes-256-cbc";
const key = Buffer.from(this.fallbackKey.padEnd(32, "0").slice(0, 32));
const iv = randomBytes(16);

const cipher = createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(data, "utf8", "hex");
encrypted += cipher.final("hex");

// Prepend IV to encrypted data
return iv.toString("hex") + ":" + encrypted;
} catch (error) {
logger.error("Fallback encryption failed:", error);
throw new Error("Failed to encrypt data");
}
}

/**
* Fallback decryption using Node.js crypto
*/
private decryptWithFallback(encryptedData: string): string {
try {
const algorithm = "aes-256-cbc";
const key = Buffer.from(this.fallbackKey.padEnd(32, "0").slice(0, 32));

// Split IV and encrypted data
const parts = encryptedData.split(":");
if (parts.length !== 2) {
throw new Error("Invalid encrypted data format");
}

const iv = Buffer.from(parts[0], "hex");
const encrypted = parts[1];

const decipher = createDecipheriv(algorithm, key, iv);
let decrypted = decipher.update(encrypted, "hex", "utf8");
decrypted += decipher.final("utf8");
return decrypted;
} catch (error) {
logger.error("Fallback decryption failed:", error);
throw new Error("Failed to decrypt data");
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix key handling in fallback encryption methods

The current implementation incorrectly handles hex-encoded keys from generateSecureFallbackKey(). The Buffer.from() call treats the hex string as UTF-8 text instead of decoding it from hex.

Apply this fix to properly handle hex-encoded keys:

 private encryptWithFallback(data: string): string {
   try {
     const algorithm = "aes-256-cbc";
-    const key = Buffer.from(this.fallbackKey.padEnd(32, "0").slice(0, 32));
+    // Decode hex key to binary (32 bytes)
+    const key = Buffer.from(this.fallbackKey, "hex");
     const iv = randomBytes(16);

     // ... rest of the method
   }
 }

 private decryptWithFallback(encryptedData: string): string {
   try {
     const algorithm = "aes-256-cbc";
-    const key = Buffer.from(this.fallbackKey.padEnd(32, "0").slice(0, 32));
+    // Decode hex key to binary (32 bytes)
+    const key = Buffer.from(this.fallbackKey, "hex");

     // ... rest of the method
   }
 }
📝 Committable suggestion

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

Suggested change
private encryptWithFallback(data: string): string {
try {
const algorithm = "aes-256-cbc";
const key = Buffer.from(this.fallbackKey.padEnd(32, "0").slice(0, 32));
const iv = randomBytes(16);
const cipher = createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(data, "utf8", "hex");
encrypted += cipher.final("hex");
// Prepend IV to encrypted data
return iv.toString("hex") + ":" + encrypted;
} catch (error) {
logger.error("Fallback encryption failed:", error);
throw new Error("Failed to encrypt data");
}
}
/**
* Fallback decryption using Node.js crypto
*/
private decryptWithFallback(encryptedData: string): string {
try {
const algorithm = "aes-256-cbc";
const key = Buffer.from(this.fallbackKey.padEnd(32, "0").slice(0, 32));
// Split IV and encrypted data
const parts = encryptedData.split(":");
if (parts.length !== 2) {
throw new Error("Invalid encrypted data format");
}
const iv = Buffer.from(parts[0], "hex");
const encrypted = parts[1];
const decipher = createDecipheriv(algorithm, key, iv);
let decrypted = decipher.update(encrypted, "hex", "utf8");
decrypted += decipher.final("utf8");
return decrypted;
} catch (error) {
logger.error("Fallback decryption failed:", error);
throw new Error("Failed to decrypt data");
}
}
// File: apps/electron-app/src/main/services/encryption-service.ts
private encryptWithFallback(data: string): string {
try {
const algorithm = "aes-256-cbc";
// Decode hex key to binary (32 bytes)
const key = Buffer.from(this.fallbackKey, "hex");
const iv = randomBytes(16);
const cipher = createCipheriv(algorithm, key, iv);
let encrypted = cipher.update(data, "utf8", "hex");
encrypted += cipher.final("hex");
// Prepend IV to encrypted data
return iv.toString("hex") + ":" + encrypted;
} catch (error) {
logger.error("Fallback encryption failed:", error);
throw new Error("Failed to encrypt data");
}
}
/**
* Fallback decryption using Node.js crypto
*/
private decryptWithFallback(encryptedData: string): string {
try {
const algorithm = "aes-256-cbc";
// Decode hex key to binary (32 bytes)
const key = Buffer.from(this.fallbackKey, "hex");
// Split IV and encrypted data
const parts = encryptedData.split(":");
if (parts.length !== 2) {
throw new Error("Invalid encrypted data format");
}
const iv = Buffer.from(parts[0], "hex");
const encrypted = parts[1];
const decipher = createDecipheriv(algorithm, key, iv);
let decrypted = decipher.update(encrypted, "hex", "utf8");
decrypted += decipher.final("utf8");
return decrypted;
} catch (error) {
logger.error("Fallback decryption failed:", error);
throw new Error("Failed to decrypt data");
}
}
🤖 Prompt for AI Agents
In apps/electron-app/src/main/services/encryption-service.ts between lines 150
and 193, the fallback encryption and decryption methods incorrectly handle the
fallbackKey by treating it as UTF-8 text instead of decoding it as a hex string.
To fix this, change the Buffer.from call to decode the fallbackKey from hex
encoding rather than UTF-8 by specifying 'hex' as the second argument. This
ensures the key is correctly interpreted as a hex-encoded value for both
encryptWithFallback and decryptWithFallback methods.

Comment on lines +191 to +271
export class MetadataHelpers {
/**
* Type guard to check if metadata is navigation history metadata
*/
static isNavigationHistoryMetadata(
metadata: unknown,
): metadata is NavigationHistoryMetadata {
return (
typeof metadata === "object" &&
metadata !== null &&
"url" in metadata &&
"visitCount" in metadata &&
"lastVisit" in metadata
);
}

/**
* Type guard to check if metadata is agent action metadata
*/
static isAgentActionMetadata(
metadata: unknown,
): metadata is AgentActionMetadata {
return (
typeof metadata === "object" &&
metadata !== null &&
"action" in metadata &&
typeof (metadata as any).action === "string"
);
}

/**
* Type guard to check if metadata is search suggestion metadata
*/
static isSearchSuggestionMetadata(
metadata: unknown,
): metadata is SearchSuggestionMetadata {
return (
typeof metadata === "object" &&
metadata !== null &&
"source" in metadata &&
"query" in metadata
);
}

/**
* Type guard to check if metadata is context suggestion metadata
*/
static isContextSuggestionMetadata(
metadata: unknown,
): metadata is ContextSuggestionMetadata {
return (
typeof metadata === "object" &&
metadata !== null &&
"contentType" in metadata &&
typeof (metadata as any).contentType === "string"
);
}

/**
* Creates base metadata with timestamp
*/
static createBaseMetadata(additional?: Partial<BaseMetadata>): BaseMetadata {
return {
timestamp: Date.now(),
...additional,
};
}

/**
* Safely extracts specific metadata type
*/
static extractMetadata<T extends SuggestionMetadata>(
metadata: unknown,
validator: (data: unknown) => data is T,
): T | null {
if (validator(metadata)) {
return metadata;
}
return null;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Refactor static-only class to standalone functions and improve type safety.

The MetadataHelpers class contains only static members, which is an anti-pattern. Additionally, the type guards use unnecessary as any casts that bypass TypeScript's type system.

Convert to standalone functions and fix type safety:

-export class MetadataHelpers {
-  /**
-   * Type guard to check if metadata is navigation history metadata
-   */
-  static isNavigationHistoryMetadata(
-    metadata: unknown,
-  ): metadata is NavigationHistoryMetadata {
-    return (
-      typeof metadata === "object" &&
-      metadata !== null &&
-      "url" in metadata &&
-      "visitCount" in metadata &&
-      "lastVisit" in metadata
-    );
-  }
-
-  /**
-   * Type guard to check if metadata is agent action metadata
-   */
-  static isAgentActionMetadata(
-    metadata: unknown,
-  ): metadata is AgentActionMetadata {
-    return (
-      typeof metadata === "object" &&
-      metadata !== null &&
-      "action" in metadata &&
-      typeof (metadata as any).action === "string"
-    );
-  }
-
-  /**
-   * Type guard to check if metadata is search suggestion metadata
-   */
-  static isSearchSuggestionMetadata(
-    metadata: unknown,
-  ): metadata is SearchSuggestionMetadata {
-    return (
-      typeof metadata === "object" &&
-      metadata !== null &&
-      "source" in metadata &&
-      "query" in metadata
-    );
-  }
-
-  /**
-   * Type guard to check if metadata is context suggestion metadata
-   */
-  static isContextSuggestionMetadata(
-    metadata: unknown,
-  ): metadata is ContextSuggestionMetadata {
-    return (
-      typeof metadata === "object" &&
-      metadata !== null &&
-      "contentType" in metadata &&
-      typeof (metadata as any).contentType === "string"
-    );
-  }
-
-  /**
-   * Creates base metadata with timestamp
-   */
-  static createBaseMetadata(additional?: Partial<BaseMetadata>): BaseMetadata {
-    return {
-      timestamp: Date.now(),
-      ...additional,
-    };
-  }
-
-  /**
-   * Safely extracts specific metadata type
-   */
-  static extractMetadata<T extends SuggestionMetadata>(
-    metadata: unknown,
-    validator: (data: unknown) => data is T,
-  ): T | null {
-    if (validator(metadata)) {
-      return metadata;
-    }
-    return null;
-  }
-}
+/**
+ * Type guard to check if metadata is navigation history metadata
+ */
+export function isNavigationHistoryMetadata(
+  metadata: unknown,
+): metadata is NavigationHistoryMetadata {
+  return (
+    typeof metadata === "object" &&
+    metadata !== null &&
+    "url" in metadata &&
+    "visitCount" in metadata &&
+    "lastVisit" in metadata
+  );
+}
+
+/**
+ * Type guard to check if metadata is agent action metadata
+ */
+export function isAgentActionMetadata(
+  metadata: unknown,
+): metadata is AgentActionMetadata {
+  if (typeof metadata !== "object" || metadata === null) {
+    return false;
+  }
+  
+  const obj = metadata as Record<string, unknown>;
+  return "action" in obj && typeof obj.action === "string";
+}
+
+/**
+ * Type guard to check if metadata is search suggestion metadata
+ */
+export function isSearchSuggestionMetadata(
+  metadata: unknown,
+): metadata is SearchSuggestionMetadata {
+  return (
+    typeof metadata === "object" &&
+    metadata !== null &&
+    "source" in metadata &&
+    "query" in metadata
+  );
+}
+
+/**
+ * Type guard to check if metadata is context suggestion metadata
+ */
+export function isContextSuggestionMetadata(
+  metadata: unknown,
+): metadata is ContextSuggestionMetadata {
+  if (typeof metadata !== "object" || metadata === null) {
+    return false;
+  }
+  
+  const obj = metadata as Record<string, unknown>;
+  return "contentType" in obj && typeof obj.contentType === "string";
+}
+
+/**
+ * Creates base metadata with timestamp
+ */
+export function createBaseMetadata(additional?: Partial<BaseMetadata>): BaseMetadata {
+  return {
+    timestamp: Date.now(),
+    ...additional,
+  };
+}
+
+/**
+ * Safely extracts specific metadata type
+ */
+export function extractMetadata<T extends SuggestionMetadata>(
+  metadata: unknown,
+  validator: (data: unknown) => data is T,
+): T | null {
+  if (validator(metadata)) {
+    return metadata;
+  }
+  return null;
+}
📝 Committable suggestion

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

Suggested change
export class MetadataHelpers {
/**
* Type guard to check if metadata is navigation history metadata
*/
static isNavigationHistoryMetadata(
metadata: unknown,
): metadata is NavigationHistoryMetadata {
return (
typeof metadata === "object" &&
metadata !== null &&
"url" in metadata &&
"visitCount" in metadata &&
"lastVisit" in metadata
);
}
/**
* Type guard to check if metadata is agent action metadata
*/
static isAgentActionMetadata(
metadata: unknown,
): metadata is AgentActionMetadata {
return (
typeof metadata === "object" &&
metadata !== null &&
"action" in metadata &&
typeof (metadata as any).action === "string"
);
}
/**
* Type guard to check if metadata is search suggestion metadata
*/
static isSearchSuggestionMetadata(
metadata: unknown,
): metadata is SearchSuggestionMetadata {
return (
typeof metadata === "object" &&
metadata !== null &&
"source" in metadata &&
"query" in metadata
);
}
/**
* Type guard to check if metadata is context suggestion metadata
*/
static isContextSuggestionMetadata(
metadata: unknown,
): metadata is ContextSuggestionMetadata {
return (
typeof metadata === "object" &&
metadata !== null &&
"contentType" in metadata &&
typeof (metadata as any).contentType === "string"
);
}
/**
* Creates base metadata with timestamp
*/
static createBaseMetadata(additional?: Partial<BaseMetadata>): BaseMetadata {
return {
timestamp: Date.now(),
...additional,
};
}
/**
* Safely extracts specific metadata type
*/
static extractMetadata<T extends SuggestionMetadata>(
metadata: unknown,
validator: (data: unknown) => data is T,
): T | null {
if (validator(metadata)) {
return metadata;
}
return null;
}
}
/**
* Type guard to check if metadata is navigation history metadata
*/
export function isNavigationHistoryMetadata(
metadata: unknown,
): metadata is NavigationHistoryMetadata {
return (
typeof metadata === "object" &&
metadata !== null &&
"url" in metadata &&
"visitCount" in metadata &&
"lastVisit" in metadata
);
}
/**
* Type guard to check if metadata is agent action metadata
*/
export function isAgentActionMetadata(
metadata: unknown,
): metadata is AgentActionMetadata {
if (typeof metadata !== "object" || metadata === null) {
return false;
}
const obj = metadata as Record<string, unknown>;
return "action" in obj && typeof obj.action === "string";
}
/**
* Type guard to check if metadata is search suggestion metadata
*/
export function isSearchSuggestionMetadata(
metadata: unknown,
): metadata is SearchSuggestionMetadata {
return (
typeof metadata === "object" &&
metadata !== null &&
"source" in metadata &&
"query" in metadata
);
}
/**
* Type guard to check if metadata is context suggestion metadata
*/
export function isContextSuggestionMetadata(
metadata: unknown,
): metadata is ContextSuggestionMetadata {
if (typeof metadata !== "object" || metadata === null) {
return false;
}
const obj = metadata as Record<string, unknown>;
return "contentType" in obj && typeof obj.contentType === "string";
}
/**
* Creates base metadata with timestamp
*/
export function createBaseMetadata(additional?: Partial<BaseMetadata>): BaseMetadata {
return {
timestamp: Date.now(),
...additional,
};
}
/**
* Safely extracts specific metadata type
*/
export function extractMetadata<T extends SuggestionMetadata>(
metadata: unknown,
validator: (data: unknown) => data is T,
): T | null {
if (validator(metadata)) {
return metadata;
}
return null;
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 191-271: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)

🤖 Prompt for AI Agents
In apps/electron-app/src/types/metadata.ts from lines 191 to 271, the
MetadataHelpers class only contains static methods and uses unsafe 'as any'
casts in type guards. Refactor by converting each static method into a
standalone exported function. Remove all 'as any' casts by properly typing the
input parameter and using safe property access and type checks to ensure type
safety in the type guard functions.

Comment on lines +1018 to +1618
// Password storage implementation using encrypted secure settings
storeImportedPasswords: async (
profileId: string,
source: string,
passwords: ImportedPasswordEntry[],
) => {
try {
const passwordData: PasswordImportData = {
passwords,
timestamp: Date.now(),
source,
count: passwords.length,
};

// Store encrypted password data using secure settings
const key = `passwords.import.${source}`;
await get().setSecureSetting(
profileId,
key,
JSON.stringify(passwordData),
);

logger.info(
`Stored ${passwords.length} passwords from ${source} securely for profile ${profileId}`,
);
} catch (error) {
logger.error(`Failed to store imported passwords from ${source}:`, error);
throw error;
}
},

getImportedPasswords: async (
profileId: string,
source?: string,
): Promise<ImportedPasswordEntry[]> => {
try {
if (source) {
// Get passwords from specific source
const key = `passwords.import.${source}`;
const encryptedData = await get().getSecureSetting(profileId, key);

if (!encryptedData) {
return [];
}

const passwordData: PasswordImportData = JSON.parse(encryptedData);
return passwordData.passwords || [];
} else {
// Get passwords from all sources
const allSecureSettings = await get().getAllSecureSettings(profileId);
const allPasswords: ImportedPasswordEntry[] = [];

for (const [key, value] of Object.entries(allSecureSettings)) {
if (key.startsWith("passwords.import.")) {
try {
const passwordData: PasswordImportData = JSON.parse(value);
allPasswords.push(...(passwordData.passwords || []));
} catch (error) {
logger.error(
`Failed to parse password data for key ${key}:`,
error,
);
}
}
}

return allPasswords;
}
} catch (error) {
logger.error(`Failed to get imported passwords:`, error);
return [];
}
},

removeImportedPasswords: async (
profileId: string,
source: string,
): Promise<void> => {
try {
const key = `passwords.import.${source}`;
await get().removeSecureSetting(profileId, key);
logger.info(
`Removed imported passwords from ${source} for profile ${profileId}`,
);
} catch (error) {
logger.error(
`Failed to remove imported passwords from ${source}:`,
error,
);
throw error;
}
},

clearAllImportedPasswords: async (profileId: string): Promise<void> => {
try {
const allSecureSettings = await get().getAllSecureSettings(profileId);
const passwordKeys = Object.keys(allSecureSettings).filter(key =>
key.startsWith("passwords.import."),
);

for (const key of passwordKeys) {
const source = key.replace("passwords.import.", "");
await get().removeImportedPasswords(profileId, source);
}

logger.info(`Cleared all imported passwords for profile ${profileId}`);
} catch (error) {
logger.error("Failed to clear all imported passwords:", error);
throw error;
}
},

getPasswordImportSources: async (profileId: string): Promise<string[]> => {
try {
const allSecureSettings = await get().getAllSecureSettings(profileId);
const sources = Object.keys(allSecureSettings)
.filter(key => key.startsWith("passwords.import."))
.map(key => key.replace("passwords.import.", ""));

return sources;
} catch (error) {
logger.error("Failed to get password import sources:", error);
return [];
}
},

// Bookmark storage actions implementation
storeImportedBookmarks: async (
profileId: string,
source: string,
bookmarks: BookmarkEntry[],
) => {
try {
const bookmarkData: BookmarkImportData = {
bookmarks,
timestamp: Date.now(),
source,
count: bookmarks.length,
};

const key = `bookmarks.import.${source}`;
await get().setSecureSetting(
profileId,
key,
JSON.stringify(bookmarkData),
);

logger.info(
`Stored ${bookmarks.length} bookmarks from ${source} securely for profile ${profileId}`,
);
} catch (error) {
logger.error(`Failed to store imported bookmarks from ${source}:`, error);
throw error;
}
},

getImportedBookmarks: async (
profileId: string,
source?: string,
): Promise<BookmarkEntry[]> => {
try {
if (source) {
const key = `bookmarks.import.${source}`;
const encryptedData = await get().getSecureSetting(profileId, key);
if (!encryptedData) return [];

const bookmarkData: BookmarkImportData = JSON.parse(encryptedData);
return bookmarkData.bookmarks || [];
} else {
const allSecureSettings = await get().getAllSecureSettings(profileId);
const allBookmarks: BookmarkEntry[] = [];

for (const [key, value] of Object.entries(allSecureSettings)) {
if (key.startsWith("bookmarks.import.")) {
try {
const bookmarkData: BookmarkImportData = JSON.parse(value);
allBookmarks.push(...(bookmarkData.bookmarks || []));
} catch (error) {
logger.error(
`Failed to parse bookmark data for key ${key}:`,
error,
);
}
}
}

return allBookmarks;
}
} catch (error) {
logger.error(`Failed to get imported bookmarks:`, error);
return [];
}
},

removeImportedBookmarks: async (profileId: string, source: string) => {
try {
const key = `bookmarks.import.${source}`;
await get().removeSecureSetting(profileId, key);
logger.info(
`Removed imported bookmarks from ${source} for profile ${profileId}`,
);
} catch (error) {
logger.error(
`Failed to remove imported bookmarks from ${source}:`,
error,
);
throw error;
}
},

clearAllImportedBookmarks: async (profileId: string) => {
try {
const allSecureSettings = await get().getAllSecureSettings(profileId);
const bookmarkKeys = Object.keys(allSecureSettings).filter(key =>
key.startsWith("bookmarks.import."),
);

for (const key of bookmarkKeys) {
const source = key.replace("bookmarks.import.", "");
await get().removeImportedBookmarks(profileId, source);
}

logger.info(`Cleared all imported bookmarks for profile ${profileId}`);
} catch (error) {
logger.error("Failed to clear all imported bookmarks:", error);
throw error;
}
},

getBookmarkImportSources: async (profileId: string): Promise<string[]> => {
try {
const allSecureSettings = await get().getAllSecureSettings(profileId);
const sources = Object.keys(allSecureSettings)
.filter(key => key.startsWith("bookmarks.import."))
.map(key => key.replace("bookmarks.import.", ""));

return sources;
} catch (error) {
logger.error("Failed to get bookmark import sources:", error);
return [];
}
},

// Enhanced history storage actions implementation
storeImportedHistory: async (
profileId: string,
source: string,
history: NavigationHistoryEntry[],
) => {
try {
const historyData = {
entries: history,
timestamp: Date.now(),
source,
count: history.length,
};

const key = `history.import.${source}`;
await get().setSecureSetting(profileId, key, JSON.stringify(historyData));

logger.info(
`Stored ${history.length} history entries from ${source} securely for profile ${profileId}`,
);
} catch (error) {
logger.error(`Failed to store imported history from ${source}:`, error);
throw error;
}
},

getImportedHistory: async (
profileId: string,
source?: string,
): Promise<NavigationHistoryEntry[]> => {
try {
if (source) {
const key = `history.import.${source}`;
const encryptedData = await get().getSecureSetting(profileId, key);
if (!encryptedData) return [];

const historyData = JSON.parse(encryptedData);
return historyData.entries || [];
} else {
const allSecureSettings = await get().getAllSecureSettings(profileId);
const allHistory: NavigationHistoryEntry[] = [];

for (const [key, value] of Object.entries(allSecureSettings)) {
if (key.startsWith("history.import.")) {
try {
const historyData = JSON.parse(value);
allHistory.push(...(historyData.entries || []));
} catch (error) {
logger.error(
`Failed to parse history data for key ${key}:`,
error,
);
}
}
}

return allHistory;
}
} catch (error) {
logger.error(`Failed to get imported history:`, error);
return [];
}
},

removeImportedHistory: async (profileId: string, source: string) => {
try {
const key = `history.import.${source}`;
await get().removeSecureSetting(profileId, key);
logger.info(
`Removed imported history from ${source} for profile ${profileId}`,
);
} catch (error) {
logger.error(`Failed to remove imported history from ${source}:`, error);
throw error;
}
},

clearAllImportedHistory: async (profileId: string) => {
try {
const allSecureSettings = await get().getAllSecureSettings(profileId);
const historyKeys = Object.keys(allSecureSettings).filter(key =>
key.startsWith("history.import."),
);

for (const key of historyKeys) {
const source = key.replace("history.import.", "");
await get().removeImportedHistory(profileId, source);
}

logger.info(`Cleared all imported history for profile ${profileId}`);
} catch (error) {
logger.error("Failed to clear all imported history:", error);
throw error;
}
},

getHistoryImportSources: async (profileId: string): Promise<string[]> => {
try {
const allSecureSettings = await get().getAllSecureSettings(profileId);
const sources = Object.keys(allSecureSettings)
.filter(key => key.startsWith("history.import."))
.map(key => key.replace("history.import.", ""));

return sources;
} catch (error) {
logger.error("Failed to get history import sources:", error);
return [];
}
},

// Autofill storage actions implementation
storeImportedAutofill: async (
profileId: string,
source: string,
autofillData: AutofillImportData,
) => {
try {
const key = `autofill.import.${source}`;
await get().setSecureSetting(
profileId,
key,
JSON.stringify(autofillData),
);

logger.info(
`Stored ${autofillData.count} autofill items from ${source} securely for profile ${profileId}`,
);
} catch (error) {
logger.error(`Failed to store imported autofill from ${source}:`, error);
throw error;
}
},

getImportedAutofill: async (
profileId: string,
source?: string,
): Promise<AutofillImportData> => {
try {
if (source) {
const key = `autofill.import.${source}`;
const encryptedData = await get().getSecureSetting(profileId, key);
if (!encryptedData)
return { entries: [], profiles: [], timestamp: 0, source, count: 0 };

const autofillData: AutofillImportData = JSON.parse(encryptedData);
return autofillData;
} else {
const allSecureSettings = await get().getAllSecureSettings(profileId);
const combinedData: AutofillImportData = {
entries: [],
profiles: [],
timestamp: Date.now(),
source: "combined",
count: 0,
};

for (const [key, value] of Object.entries(allSecureSettings)) {
if (key.startsWith("autofill.import.")) {
try {
const autofillData: AutofillImportData = JSON.parse(value);
combinedData.entries.push(...(autofillData.entries || []));
combinedData.profiles.push(...(autofillData.profiles || []));
} catch (error) {
logger.error(
`Failed to parse autofill data for key ${key}:`,
error,
);
}
}
}

combinedData.count =
combinedData.entries.length + combinedData.profiles.length;
return combinedData;
}
} catch (error) {
logger.error(`Failed to get imported autofill:`, error);
return {
entries: [],
profiles: [],
timestamp: 0,
source: source || "error",
count: 0,
};
}
},

removeImportedAutofill: async (profileId: string, source: string) => {
try {
const key = `autofill.import.${source}`;
await get().removeSecureSetting(profileId, key);
logger.info(
`Removed imported autofill from ${source} for profile ${profileId}`,
);
} catch (error) {
logger.error(`Failed to remove imported autofill from ${source}:`, error);
throw error;
}
},

clearAllImportedAutofill: async (profileId: string) => {
try {
const allSecureSettings = await get().getAllSecureSettings(profileId);
const autofillKeys = Object.keys(allSecureSettings).filter(key =>
key.startsWith("autofill.import."),
);

for (const key of autofillKeys) {
const source = key.replace("autofill.import.", "");
await get().removeImportedAutofill(profileId, source);
}

logger.info(`Cleared all imported autofill for profile ${profileId}`);
} catch (error) {
logger.error("Failed to clear all imported autofill:", error);
throw error;
}
},

getAutofillImportSources: async (profileId: string): Promise<string[]> => {
try {
const allSecureSettings = await get().getAllSecureSettings(profileId);
const sources = Object.keys(allSecureSettings)
.filter(key => key.startsWith("autofill.import."))
.map(key => key.replace("autofill.import.", ""));

return sources;
} catch (error) {
logger.error("Failed to get autofill import sources:", error);
return [];
}
},

// Search engine storage actions implementation
storeImportedSearchEngines: async (
profileId: string,
source: string,
engines: SearchEngine[],
) => {
try {
const searchEngineData: SearchEngineImportData = {
engines,
timestamp: Date.now(),
source,
count: engines.length,
};

const key = `searchEngines.import.${source}`;
await get().setSecureSetting(
profileId,
key,
JSON.stringify(searchEngineData),
);

logger.info(
`Stored ${engines.length} search engines from ${source} securely for profile ${profileId}`,
);
} catch (error) {
logger.error(
`Failed to store imported search engines from ${source}:`,
error,
);
throw error;
}
},

getImportedSearchEngines: async (
profileId: string,
source?: string,
): Promise<SearchEngine[]> => {
try {
if (source) {
const key = `searchEngines.import.${source}`;
const encryptedData = await get().getSecureSetting(profileId, key);
if (!encryptedData) return [];

const searchEngineData: SearchEngineImportData =
JSON.parse(encryptedData);
return searchEngineData.engines || [];
} else {
const allSecureSettings = await get().getAllSecureSettings(profileId);
const allSearchEngines: SearchEngine[] = [];

for (const [key, value] of Object.entries(allSecureSettings)) {
if (key.startsWith("searchEngines.import.")) {
try {
const searchEngineData: SearchEngineImportData =
JSON.parse(value);
allSearchEngines.push(...(searchEngineData.engines || []));
} catch (error) {
logger.error(
`Failed to parse search engine data for key ${key}:`,
error,
);
}
}
}

return allSearchEngines;
}
} catch (error) {
logger.error(`Failed to get imported search engines:`, error);
return [];
}
},

removeImportedSearchEngines: async (profileId: string, source: string) => {
try {
const key = `searchEngines.import.${source}`;
await get().removeSecureSetting(profileId, key);
logger.info(
`Removed imported search engines from ${source} for profile ${profileId}`,
);
} catch (error) {
logger.error(
`Failed to remove imported search engines from ${source}:`,
error,
);
throw error;
}
},

clearAllImportedSearchEngines: async (profileId: string) => {
try {
const allSecureSettings = await get().getAllSecureSettings(profileId);
const searchEngineKeys = Object.keys(allSecureSettings).filter(key =>
key.startsWith("searchEngines.import."),
);

for (const key of searchEngineKeys) {
const source = key.replace("searchEngines.import.", "");
await get().removeImportedSearchEngines(profileId, source);
}

logger.info(
`Cleared all imported search engines for profile ${profileId}`,
);
} catch (error) {
logger.error("Failed to clear all imported search engines:", error);
throw error;
}
},

getSearchEngineImportSources: async (
profileId: string,
): Promise<string[]> => {
try {
const allSecureSettings = await get().getAllSecureSettings(profileId);
const sources = Object.keys(allSecureSettings)
.filter(key => key.startsWith("searchEngines.import."))
.map(key => key.replace("searchEngines.import.", ""));

return sources;
} catch (error) {
logger.error("Failed to get search engine import sources:", error);
return [];
}
},
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Reduce code duplication in import/export methods with a generic approach.

The import/export methods for passwords, bookmarks, history, autofill, and search engines follow identical patterns. This duplication makes the code harder to maintain and increases the chance of bugs.

Consider creating a generic import/export manager:

interface ImportExportConfig<T> {
  keyPrefix: string;
  serialize: (data: T) => string;
  deserialize: (data: string) => T;
}

class SecureImportExportManager<T> {
  constructor(
    private config: ImportExportConfig<T>,
    private getSecureSetting: (profileId: string, key: string) => Promise<string | null>,
    private setSecureSetting: (profileId: string, key: string, value: string) => Promise<void>,
    private removeSecureSetting: (profileId: string, key: string) => Promise<void>,
    private getAllSecureSettings: (profileId: string) => Promise<Record<string, string>>
  ) {}

  async store(profileId: string, source: string, data: T): Promise<void> {
    const key = `${this.config.keyPrefix}.${source}`;
    await this.setSecureSetting(profileId, key, this.config.serialize(data));
  }

  async get(profileId: string, source?: string): Promise<T | T[]> {
    if (source) {
      const key = `${this.config.keyPrefix}.${source}`;
      const data = await this.getSecureSetting(profileId, key);
      return data ? this.config.deserialize(data) : null;
    }
    // Get all implementation...
  }

  async remove(profileId: string, source: string): Promise<void> {
    const key = `${this.config.keyPrefix}.${source}`;
    await this.removeSecureSetting(profileId, key);
  }

  async getSources(profileId: string): Promise<string[]> {
    const settings = await this.getAllSecureSettings(profileId);
    return Object.keys(settings)
      .filter(key => key.startsWith(this.config.keyPrefix))
      .map(key => key.replace(`${this.config.keyPrefix}.`, ""));
  }
}

This would reduce the store implementation by hundreds of lines while maintaining the same functionality.

🤖 Prompt for AI Agents
In apps/electron-app/src/main/store/user-profile-store.ts around lines 1018 to
1618, the import/export methods for passwords, bookmarks, history, autofill, and
search engines are highly duplicated with nearly identical logic. To fix this,
create a generic SecureImportExportManager class that accepts configuration for
key prefixes and serialization/deserialization functions, and uses the existing
secure setting methods for storing, retrieving, removing, and listing sources.
Then refactor each specific import/export method to use this generic manager,
significantly reducing code duplication and improving maintainability.

Comment on lines +2104 to +2131
destroySessionForProfile: (profileId: string): void => {
const state = get();
const profileSession = state.profileSessions.get(profileId);

if (!profileSession) {
logger.warn(`No session found for profile ${profileId}`);
return;
}

// Clear session data
profileSession.clearStorageData();
profileSession.clearCache();

// Remove from maps
set(state => {
const newSessions = new Map(state.profileSessions);
newSessions.delete(profileId);

const profile = state.profiles.get(profileId);
if (profile) {
delete profile.session;
}

return { profileSessions: newSessions };
});

logger.info(`Destroyed session for profile ${profileId}`);
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Replace delete operator with undefined assignment for better performance.

The delete operator can impact performance by changing the object's shape and causing V8 deoptimization.

       const profile = state.profiles.get(profileId);
       if (profile) {
-        delete profile.session;
+        profile.session = undefined;
       }
📝 Committable suggestion

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

Suggested change
destroySessionForProfile: (profileId: string): void => {
const state = get();
const profileSession = state.profileSessions.get(profileId);
if (!profileSession) {
logger.warn(`No session found for profile ${profileId}`);
return;
}
// Clear session data
profileSession.clearStorageData();
profileSession.clearCache();
// Remove from maps
set(state => {
const newSessions = new Map(state.profileSessions);
newSessions.delete(profileId);
const profile = state.profiles.get(profileId);
if (profile) {
delete profile.session;
}
return { profileSessions: newSessions };
});
logger.info(`Destroyed session for profile ${profileId}`);
},
destroySessionForProfile: (profileId: string): void => {
const state = get();
const profileSession = state.profileSessions.get(profileId);
if (!profileSession) {
logger.warn(`No session found for profile ${profileId}`);
return;
}
// Clear session data
profileSession.clearStorageData();
profileSession.clearCache();
// Remove from maps
set(state => {
const newSessions = new Map(state.profileSessions);
newSessions.delete(profileId);
const profile = state.profiles.get(profileId);
if (profile) {
profile.session = undefined;
}
return { profileSessions: newSessions };
});
logger.info(`Destroyed session for profile ${profileId}`);
},
🧰 Tools
🪛 Biome (1.9.4)

[error] 2124-2124: Avoid the delete operator which can impact performance.

Unsafe fix: Use an undefined assignment instead.

(lint/performance/noDelete)

🤖 Prompt for AI Agents
In apps/electron-app/src/main/store/user-profile-store.ts around lines 2104 to
2131, replace the use of the delete operator on the profile.session property
with an assignment to undefined to avoid V8 deoptimization and improve
performance. Specifically, instead of using "delete profile.session;", assign
"profile.session = undefined;" to clear the session reference.

@@ -0,0 +1,82 @@
on: pull_request
name: pull_request

Copy link
Contributor

Choose a reason for hiding this comment

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

The GitHub workflow files in this PR should be removed. They already exist in main and are identical here. Keeping these duplicates adds unnecessary noise to the diff and could cause merge conflicts.

@@ -0,0 +1,42 @@
# Commit Changes
Copy link
Contributor

Choose a reason for hiding this comment

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

.claude code should be in .gitignore. Everyone has their own coding preferences, and committing these files to the repo will conflict with local setups. CI is there to make sure lint, typecheck, and build all succeed, and we use semver for commits and PRs. You can keep using your own Claude configs locally, just don’t check them in.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants