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