signer-meta-handler.ts raw

   1  /* eslint-disable @typescript-eslint/no-explicit-any */
   2  import { Bookmark, EncryptedVault, SyncFlow, ExtensionSettings, VaultSnapshot } from './types';
   3  import { v4 as uuidv4 } from 'uuid';
   4  
   5  /**
   6   * Handler for extension settings stored outside the encrypted vault.
   7   * This includes sync preferences, backups, reckless mode, whitelisted hosts, etc.
   8   */
   9  export abstract class SignerMetaHandler {
  10    get extensionSettings(): ExtensionSettings | undefined {
  11      return this.#extensionSettings;
  12    }
  13  
  14    /** @deprecated Use extensionSettings instead */
  15    get signerMetaData(): ExtensionSettings | undefined {
  16      return this.#extensionSettings;
  17    }
  18  
  19    #extensionSettings?: ExtensionSettings;
  20  
  21    readonly metaProperties = ['syncFlow', 'vaultSnapshots', 'maxBackups', 'recklessMode', 'whitelistedHosts', 'bookmarks', 'devMode'];
  22    readonly DEFAULT_MAX_BACKUPS = 5;
  23    /**
  24     * Load the full data from the storage. If the storage is used for storing
  25     * other data (e.g. browser sync data when the user decided to NOT sync),
  26     * make sure to handle the "meta properties" to only load these.
  27     *
  28     * ATTENTION: Make sure to call "setFullData(..)" afterwards to update the in-memory data.
  29     */
  30    abstract loadFullData(): Promise<Partial<Record<string, any>>>;
  31  
  32    setFullData(data: ExtensionSettings) {
  33      this.#extensionSettings = data;
  34    }
  35  
  36    abstract saveFullData(data: ExtensionSettings): Promise<void>;
  37  
  38    /**
  39     * Sets the sync flow preference for the user and immediately saves it.
  40     */
  41    async setSyncFlow(flow: SyncFlow): Promise<void> {
  42      if (!this.#extensionSettings) {
  43        this.#extensionSettings = {
  44          syncFlow: flow,
  45        };
  46      } else {
  47        this.#extensionSettings.syncFlow = flow;
  48      }
  49  
  50      await this.saveFullData(this.#extensionSettings);
  51    }
  52  
  53    /** @deprecated Use setSyncFlow instead */
  54    async setBrowserSyncFlow(flow: SyncFlow): Promise<void> {
  55      return this.setSyncFlow(flow);
  56    }
  57  
  58    abstract clearData(keep: string[]): Promise<void>;
  59  
  60    /**
  61     * Sets the reckless mode and immediately saves it.
  62     */
  63    async setRecklessMode(enabled: boolean): Promise<void> {
  64      if (!this.#extensionSettings) {
  65        this.#extensionSettings = {
  66          recklessMode: enabled,
  67        };
  68      } else {
  69        this.#extensionSettings.recklessMode = enabled;
  70      }
  71  
  72      await this.saveFullData(this.#extensionSettings);
  73    }
  74  
  75    /**
  76     * Sets dev mode and immediately saves it.
  77     */
  78    async setDevMode(enabled: boolean): Promise<void> {
  79      if (!this.#extensionSettings) {
  80        this.#extensionSettings = {
  81          devMode: enabled,
  82        };
  83      } else {
  84        this.#extensionSettings.devMode = enabled;
  85      }
  86  
  87      await this.saveFullData(this.#extensionSettings);
  88    }
  89  
  90    /**
  91     * Adds a host to the whitelist and immediately saves it.
  92     */
  93    async addWhitelistedHost(host: string): Promise<void> {
  94      if (!this.#extensionSettings) {
  95        this.#extensionSettings = {
  96          whitelistedHosts: [host],
  97        };
  98      } else {
  99        const hosts = this.#extensionSettings.whitelistedHosts ?? [];
 100        if (!hosts.includes(host)) {
 101          hosts.push(host);
 102          this.#extensionSettings.whitelistedHosts = hosts;
 103        }
 104      }
 105  
 106      await this.saveFullData(this.#extensionSettings);
 107    }
 108  
 109    /**
 110     * Removes a host from the whitelist and immediately saves it.
 111     */
 112    async removeWhitelistedHost(host: string): Promise<void> {
 113      if (!this.#extensionSettings?.whitelistedHosts) {
 114        return;
 115      }
 116  
 117      this.#extensionSettings.whitelistedHosts = this.#extensionSettings.whitelistedHosts.filter(
 118        (h) => h !== host
 119      );
 120  
 121      await this.saveFullData(this.#extensionSettings);
 122    }
 123  
 124    /**
 125     * Sets the bookmarks array and immediately saves it.
 126     */
 127    async setBookmarks(bookmarks: Bookmark[]): Promise<void> {
 128      if (!this.#extensionSettings) {
 129        this.#extensionSettings = {
 130          bookmarks,
 131        };
 132      } else {
 133        this.#extensionSettings.bookmarks = bookmarks;
 134      }
 135  
 136      await this.saveFullData(this.#extensionSettings);
 137    }
 138  
 139    /**
 140     * Gets the current bookmarks.
 141     */
 142    getBookmarks(): Bookmark[] {
 143      return this.#extensionSettings?.bookmarks ?? [];
 144    }
 145  
 146    /**
 147     * Gets the maximum number of backups to keep.
 148     */
 149    getMaxBackups(): number {
 150      return this.#extensionSettings?.maxBackups ?? this.DEFAULT_MAX_BACKUPS;
 151    }
 152  
 153    /**
 154     * Sets the maximum number of backups to keep and immediately saves it.
 155     */
 156    async setMaxBackups(count: number): Promise<void> {
 157      const clampedCount = Math.max(1, Math.min(20, count)); // Clamp between 1-20
 158      if (!this.#extensionSettings) {
 159        this.#extensionSettings = {
 160          maxBackups: clampedCount,
 161        };
 162      } else {
 163        this.#extensionSettings.maxBackups = clampedCount;
 164      }
 165  
 166      await this.saveFullData(this.#extensionSettings);
 167    }
 168  
 169    /**
 170     * Gets all vault backups, sorted newest first.
 171     */
 172    getBackups(): VaultSnapshot[] {
 173      const backups = this.#extensionSettings?.vaultSnapshots ?? [];
 174      return [...backups].sort((a, b) =>
 175        new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
 176      );
 177    }
 178  
 179    /**
 180     * Gets a specific backup by ID.
 181     */
 182    getBackupById(id: string): VaultSnapshot | undefined {
 183      return this.#extensionSettings?.vaultSnapshots?.find(b => b.id === id);
 184    }
 185  
 186    /**
 187     * Creates a new backup of the vault data.
 188     * Automatically removes old backups if exceeding maxBackups.
 189     */
 190    async createBackup(
 191      encryptedVault: EncryptedVault,
 192      reason: 'manual' | 'auto' | 'pre-restore' = 'manual'
 193    ): Promise<VaultSnapshot> {
 194      const now = new Date();
 195      const dateTimeString = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
 196      const identityCount = encryptedVault.identities?.length ?? 0;
 197  
 198      const snapshot: VaultSnapshot = {
 199        id: uuidv4(),
 200        fileName: `Vault Backup - ${dateTimeString}`,
 201        createdAt: now.toISOString(),
 202        data: JSON.parse(JSON.stringify(encryptedVault)), // Deep clone
 203        identityCount,
 204        reason,
 205      };
 206  
 207      if (!this.#extensionSettings) {
 208        this.#extensionSettings = {
 209          vaultSnapshots: [snapshot],
 210        };
 211      } else {
 212        const existingBackups = this.#extensionSettings.vaultSnapshots ?? [];
 213        existingBackups.push(snapshot);
 214  
 215        // Enforce max backups limit (only for auto backups, keep manual and pre-restore)
 216        const maxBackups = this.getMaxBackups();
 217        const autoBackups = existingBackups.filter(b => b.reason === 'auto');
 218        const otherBackups = existingBackups.filter(b => b.reason !== 'auto');
 219  
 220        // Sort auto backups by date (newest first) and keep only maxBackups
 221        autoBackups.sort((a, b) =>
 222          new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
 223        );
 224        const trimmedAutoBackups = autoBackups.slice(0, maxBackups);
 225  
 226        this.#extensionSettings.vaultSnapshots = [...otherBackups, ...trimmedAutoBackups];
 227      }
 228  
 229      await this.saveFullData(this.#extensionSettings);
 230      return snapshot;
 231    }
 232  
 233    /**
 234     * Deletes a backup by ID.
 235     */
 236    async deleteBackup(backupId: string): Promise<boolean> {
 237      if (!this.#extensionSettings?.vaultSnapshots) {
 238        return false;
 239      }
 240  
 241      const initialLength = this.#extensionSettings.vaultSnapshots.length;
 242      this.#extensionSettings.vaultSnapshots = this.#extensionSettings.vaultSnapshots.filter(
 243        b => b.id !== backupId
 244      );
 245  
 246      if (this.#extensionSettings.vaultSnapshots.length < initialLength) {
 247        await this.saveFullData(this.#extensionSettings);
 248        return true;
 249      }
 250      return false;
 251    }
 252  
 253    /**
 254     * Gets the data from a backup for restoration.
 255     * Note: The caller should create a pre-restore backup before calling this.
 256     */
 257    getBackupData(backupId: string): EncryptedVault | undefined {
 258      const backup = this.getBackupById(backupId);
 259      return backup?.data;
 260    }
 261  }
 262