(apiKey: string)
| 1088 | } |
| 1089 | |
| 1090 | export async function saveApiKey(apiKey: string): Promise<void> { |
| 1091 | if (!isValidApiKey(apiKey)) { |
| 1092 | throw new Error( |
| 1093 | 'Invalid API key format. API key must contain only alphanumeric characters, dashes, and underscores.', |
| 1094 | ) |
| 1095 | } |
| 1096 | |
| 1097 | // Store as primary API key |
| 1098 | await maybeRemoveApiKeyFromMacOSKeychain() |
| 1099 | let savedToKeychain = false |
| 1100 | if (process.platform === 'darwin') { |
| 1101 | try { |
| 1102 | // TODO: migrate to SecureStorage |
| 1103 | const storageServiceName = getMacOsKeychainStorageServiceName() |
| 1104 | const username = getUsername() |
| 1105 | |
| 1106 | // Convert to hexadecimal to avoid any escaping issues |
| 1107 | const hexValue = Buffer.from(apiKey, 'utf-8').toString('hex') |
| 1108 | |
| 1109 | // Use security's interactive mode (-i) with -X (hexadecimal) option |
| 1110 | // This ensures credentials never appear in process command-line arguments |
| 1111 | // Process monitors only see "security -i", not the password |
| 1112 | const command = `add-generic-password -U -a "${username}" -s "${storageServiceName}" -X "${hexValue}"\n` |
| 1113 | |
| 1114 | await execa('security', ['-i'], { |
| 1115 | input: command, |
| 1116 | reject: false, |
| 1117 | }) |
| 1118 | |
| 1119 | logEvent('tengu_api_key_saved_to_keychain', {}) |
| 1120 | savedToKeychain = true |
| 1121 | } catch (e) { |
| 1122 | logError(e) |
| 1123 | logEvent('tengu_api_key_keychain_error', { |
| 1124 | error: errorMessage( |
| 1125 | e, |
| 1126 | ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| 1127 | }) |
| 1128 | logEvent('tengu_api_key_saved_to_config', {}) |
| 1129 | } |
| 1130 | } else { |
| 1131 | logEvent('tengu_api_key_saved_to_config', {}) |
| 1132 | } |
| 1133 | |
| 1134 | const normalizedKey = normalizeApiKeyForConfig(apiKey) |
| 1135 | |
| 1136 | // Save config with all updates |
| 1137 | saveGlobalConfig(current => { |
| 1138 | const approved = current.customApiKeyResponses?.approved ?? [] |
| 1139 | return { |
| 1140 | ...current, |
| 1141 | // Only save to config if keychain save failed or not on darwin |
| 1142 | primaryApiKey: savedToKeychain ? current.primaryApiKey : apiKey, |
| 1143 | customApiKeyResponses: { |
| 1144 | ...current.customApiKeyResponses, |
| 1145 | approved: approved.includes(normalizedKey) |
| 1146 | ? approved |
| 1147 | : [...approved, normalizedKey], |
no test coverage detected