(apiKey: string)
| 1166 | } |
| 1167 | |
| 1168 | export async function saveApiKey(apiKey: string): Promise<void> { |
| 1169 | if (!isValidApiKey(apiKey)) { |
| 1170 | throw new Error( |
| 1171 | 'Invalid API key format. API key must contain only alphanumeric characters, dashes, and underscores.', |
| 1172 | ) |
| 1173 | } |
| 1174 | |
| 1175 | // Store as primary API key |
| 1176 | await maybeRemoveApiKeyFromMacOSKeychain() |
| 1177 | let savedToKeychain = false |
| 1178 | if (process.platform === 'darwin') { |
| 1179 | try { |
| 1180 | // TODO: migrate to SecureStorage |
| 1181 | const storageServiceName = getMacOsKeychainStorageServiceName() |
| 1182 | const username = getUsername() |
| 1183 | |
| 1184 | // Convert to hexadecimal to avoid any escaping issues |
| 1185 | const hexValue = Buffer.from(apiKey, 'utf-8').toString('hex') |
| 1186 | |
| 1187 | // Use security's interactive mode (-i) with -X (hexadecimal) option |
| 1188 | // This ensures credentials never appear in process command-line arguments |
| 1189 | // Process monitors only see "security -i", not the password |
| 1190 | const command = `add-generic-password -U -a "${username}" -s "${storageServiceName}" -X "${hexValue}"\n` |
| 1191 | |
| 1192 | await execa('security', ['-i'], { |
| 1193 | input: command, |
| 1194 | reject: false, |
| 1195 | }) |
| 1196 | |
| 1197 | logEvent('ncode_api_key_saved_to_keychain', {}) |
| 1198 | savedToKeychain = true |
| 1199 | } catch (e) { |
| 1200 | logError(e) |
| 1201 | logEvent('ncode_api_key_keychain_error', { |
| 1202 | error: errorMessage( |
| 1203 | e, |
| 1204 | ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| 1205 | }) |
| 1206 | logEvent('ncode_api_key_saved_to_config', {}) |
| 1207 | } |
| 1208 | } else { |
| 1209 | logEvent('ncode_api_key_saved_to_config', {}) |
| 1210 | } |
| 1211 | |
| 1212 | const normalizedKey = normalizeApiKeyForConfig(apiKey) |
| 1213 | |
| 1214 | // Save config with all updates |
| 1215 | saveGlobalConfig(current => { |
| 1216 | const approved = current.customApiKeyResponses?.approved ?? [] |
| 1217 | return { |
| 1218 | ...current, |
| 1219 | // Only save to config if keychain save failed or not on darwin |
| 1220 | primaryApiKey: savedToKeychain ? current.primaryApiKey : apiKey, |
| 1221 | customApiKeyResponses: { |
| 1222 | ...current.customApiKeyResponses, |
| 1223 | approved: approved.includes(normalizedKey) |
| 1224 | ? approved |
| 1225 | : [...approved, normalizedKey], |
no test coverage detected