File size: 3,889 Bytes
f0743f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import path from 'path';
import axios from 'axios';
import { logger } from '@librechat/data-schemas';
import { readFileAsString } from './files';

export interface GoogleServiceKey {
  type?: string;
  project_id?: string;
  private_key_id?: string;
  private_key?: string;
  client_email?: string;
  client_id?: string;
  auth_uri?: string;
  token_uri?: string;
  auth_provider_x509_cert_url?: string;
  client_x509_cert_url?: string;
  [key: string]: unknown;
}

/**
 * Load Google service key from file path, URL, or stringified JSON
 * @param keyPath - The path to the service key file, URL to fetch it from, or stringified JSON
 * @returns The parsed service key object or null if failed
 */
export async function loadServiceKey(keyPath: string): Promise<GoogleServiceKey | null> {
  if (!keyPath) {
    return null;
  }

  let serviceKey: unknown;

  // Check if it's base64 encoded (common pattern for storing in env vars)
  if (keyPath.trim().match(/^[A-Za-z0-9+/]+=*$/)) {
    try {
      const decoded = Buffer.from(keyPath.trim(), 'base64').toString('utf-8');
      // Try to parse the decoded string as JSON
      serviceKey = JSON.parse(decoded);
    } catch {
      // Not base64 or not valid JSON after decoding, continue with other methods
      // Silent failure - not critical
    }
  }

  // Check if it's a stringified JSON (starts with '{')
  if (!serviceKey && keyPath.trim().startsWith('{')) {
    try {
      serviceKey = JSON.parse(keyPath);
    } catch (error) {
      logger.error('Failed to parse service key from stringified JSON', error);
      return null;
    }
  }
  // Check if it's a URL
  else if (!serviceKey && /^https?:\/\//.test(keyPath)) {
    try {
      const response = await axios.get(keyPath);
      serviceKey = response.data;
    } catch (error) {
      logger.error(`Failed to fetch the service key from URL: ${keyPath}`, error);
      return null;
    }
  } else if (!serviceKey) {
    // It's a file path
    try {
      const absolutePath = path.isAbsolute(keyPath) ? keyPath : path.resolve(keyPath);
      const { content: fileContent } = await readFileAsString(absolutePath);
      serviceKey = JSON.parse(fileContent);
    } catch (error) {
      logger.error(`Failed to load service key from file: ${keyPath}`, error);
      return null;
    }
  }

  // If the response is a string (e.g., from a URL that returns JSON as text), parse it
  if (typeof serviceKey === 'string') {
    try {
      serviceKey = JSON.parse(serviceKey);
    } catch (parseError) {
      logger.error(`Failed to parse service key JSON from ${keyPath}`, parseError);
      return null;
    }
  }

  // Validate the service key has required fields
  if (!serviceKey || typeof serviceKey !== 'object') {
    logger.error(`Invalid service key format from ${keyPath}`);
    return null;
  }

  // Fix private key formatting if needed
  const key = serviceKey as GoogleServiceKey;
  if (key.private_key && typeof key.private_key === 'string') {
    // Replace escaped newlines with actual newlines
    // When JSON.parse processes "\\n", it becomes "\n" (single backslash + n)
    // When JSON.parse processes "\n", it becomes an actual newline character
    key.private_key = key.private_key.replace(/\\n/g, '\n');

    // Also handle the String.raw`\n` case mentioned in Stack Overflow
    key.private_key = key.private_key.split(String.raw`\n`).join('\n');

    // Ensure proper PEM format
    if (!key.private_key.includes('\n')) {
      // If no newlines are present, try to format it properly
      const privateKeyMatch = key.private_key.match(
        /^(-----BEGIN [A-Z ]+-----)(.*)(-----END [A-Z ]+-----)$/,
      );
      if (privateKeyMatch) {
        const [, header, body, footer] = privateKeyMatch;
        // Add newlines after header and before footer
        key.private_key = `${header}\n${body}\n${footer}`;
      }
    }
  }

  return key;
}