All files / packages/gemini-core/src/utils userAccountManager.ts

6.55% Statements 4/61
0% Branches 0/30
0% Functions 0/10
6.66% Lines 4/60

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140            6x 6x 6x             6x                                                                                                                                                                                                                                                        
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
 
import path from 'node:path';
import { promises as fsp, readFileSync } from 'node:fs';
import { Storage } from '../config/storage';
 
interface UserAccounts {
  active: string | null;
  old: string[];
}
 
export class UserAccountManager {
  private getGoogleAccountsCachePath(): string {
    return Storage.getGoogleAccountsPath();
  }
 
  /**
   * Parses and validates the string content of an accounts file.
   * @param content The raw string content from the file.
   * @returns A valid UserAccounts object.
   */
  private parseAndValidateAccounts(content: string): UserAccounts {
    const defaultState = { active: null, old: [] };
    Iif (!content.trim()) {
      return defaultState;
    }
 
    const parsed = JSON.parse(content);
 
    // Inlined validation logic
    Iif (typeof parsed !== 'object' || parsed === null) {
      console.log('Invalid accounts file schema, starting fresh.');
      return defaultState;
    }
    const { active, old } = parsed as Partial<UserAccounts>;
    const isValid =
      (active === undefined || active === null || typeof active === 'string') &&
      (old === undefined ||
        (Array.isArray(old) && old.every((i) => typeof i === 'string')));
 
    Iif (!isValid) {
      console.log('Invalid accounts file schema, starting fresh.');
      return defaultState;
    }
 
    return {
      active: parsed.active ?? null,
      old: parsed.old ?? [],
    };
  }
 
  private readAccountsSync(filePath: string): UserAccounts {
    const defaultState = { active: null, old: [] };
    try {
      const content = readFileSync(filePath, 'utf-8');
      return this.parseAndValidateAccounts(content);
    } catch (error) {
      Iif (
        error instanceof Error &&
        'code' in error &&
        error.code === 'ENOENT'
      ) {
        return defaultState;
      }
      console.log('Error during sync read of accounts, starting fresh.', error);
      return defaultState;
    }
  }
 
  private async readAccounts(filePath: string): Promise<UserAccounts> {
    const defaultState = { active: null, old: [] };
    try {
      const content = await fsp.readFile(filePath, 'utf-8');
      return this.parseAndValidateAccounts(content);
    } catch (error) {
      Iif (
        error instanceof Error &&
        'code' in error &&
        error.code === 'ENOENT'
      ) {
        return defaultState;
      }
      console.log('Could not parse accounts file, starting fresh.', error);
      return defaultState;
    }
  }
 
  async cacheGoogleAccount(email: string): Promise<void> {
    const filePath = this.getGoogleAccountsCachePath();
    await fsp.mkdir(path.dirname(filePath), { recursive: true });
 
    const accounts = await this.readAccounts(filePath);
 
    Iif (accounts.active && accounts.active !== email) {
      Iif (!accounts.old.includes(accounts.active)) {
        accounts.old.push(accounts.active);
      }
    }
 
    // If the new email was in the old list, remove it
    accounts.old = accounts.old.filter((oldEmail) => oldEmail !== email);
 
    accounts.active = email;
    await fsp.writeFile(filePath, JSON.stringify(accounts, null, 2), 'utf-8');
  }
 
  getCachedGoogleAccount(): string | null {
    const filePath = this.getGoogleAccountsCachePath();
    const accounts = this.readAccountsSync(filePath);
    return accounts.active;
  }
 
  getLifetimeGoogleAccounts(): number {
    const filePath = this.getGoogleAccountsCachePath();
    const accounts = this.readAccountsSync(filePath);
    const allAccounts = new Set(accounts.old);
    Iif (accounts.active) {
      allAccounts.add(accounts.active);
    }
    return allAccounts.size;
  }
 
  async clearCachedGoogleAccount(): Promise<void> {
    const filePath = this.getGoogleAccountsCachePath();
    const accounts = await this.readAccounts(filePath);
 
    Iif (accounts.active) {
      Iif (!accounts.old.includes(accounts.active)) {
        accounts.old.push(accounts.active);
      }
      accounts.active = null;
    }
 
    await fsp.writeFile(filePath, JSON.stringify(accounts, null, 2), 'utf-8');
  }
}