(apiKey: string)
| 1142 | } |
| 1143 | |
| 1144 | export async function saveApiKey(apiKey: string): Promise<void> { |
| 1145 | if (!isValidApiKey(apiKey)) { |
| 1146 | throw new Error( |
| 1147 | 'Invalid API key format. API key must contain only alphanumeric characters, dashes, and underscores.', |
| 1148 | ) |
| 1149 | } |
| 1150 | |
| 1151 | // Store as primary API key |
| 1152 | await maybeRemoveApiKeyFromMacOSKeychain() |
| 1153 | let savedToKeychain = false |
| 1154 | if (process.platform === 'darwin') { |
| 1155 | try { |
| 1156 | // TODO: migrate to SecureStorage |
| 1157 | const storageServiceName = getMacOsKeychainStorageServiceName() |
| 1158 | const username = getUsername() |
| 1159 | |
| 1160 | // Convert to hexadecimal to avoid any escaping issues |
| 1161 | const hexValue = Buffer.from(apiKey, 'utf-8').toString('hex') |
| 1162 | |
| 1163 | // Use security's interactive mode (-i) with -X (hexadecimal) option |
| 1164 | // This ensures credentials never appear in process command-line arguments |
| 1165 | // Process monitors only see "security -i", not the password |
| 1166 | const command = `add-generic-password -U -a "${username}" -s "${storageServiceName}" -X "${hexValue}"\n` |
| 1167 | |
| 1168 | await execa('security', ['-i'], { |
| 1169 | input: command, |
| 1170 | reject: false, |
| 1171 | }) |
| 1172 | |
| 1173 | logEvent('tengu_api_key_saved_to_keychain', {}) |
| 1174 | savedToKeychain = true |
| 1175 | } catch (e) { |
| 1176 | logError(e) |
| 1177 | logEvent('tengu_api_key_keychain_error', { |
| 1178 | error: errorMessage( |
| 1179 | e, |
| 1180 | ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| 1181 | }) |
| 1182 | logEvent('tengu_api_key_saved_to_config', {}) |
| 1183 | } |
| 1184 | } else { |
| 1185 | logEvent('tengu_api_key_saved_to_config', {}) |
| 1186 | } |
| 1187 | |
| 1188 | const normalizedKey = normalizeApiKeyForConfig(apiKey) |
| 1189 | |
| 1190 | // Save config with all updates |
| 1191 | saveGlobalConfig(current => { |
| 1192 | const approved = current.customApiKeyResponses?.approved ?? [] |
| 1193 | return { |
| 1194 | ...current, |
| 1195 | // Only save to config if keychain save failed or not on darwin |
| 1196 | primaryApiKey: savedToKeychain ? current.primaryApiKey : apiKey, |
| 1197 | customApiKeyResponses: { |
| 1198 | ...current.customApiKeyResponses, |
| 1199 | approved: approved.includes(normalizedKey) |
| 1200 | ? approved |
| 1201 | : [...approved, normalizedKey], |
no test coverage detected