import * as plugins from './smartsecret.plugins.js'; import type { ISecretBackend, TBackendType } from './smartsecret.backends.base.js'; function execFile(cmd: string, args: string[]): Promise<{ stdout: string; stderr: string }> { return new Promise((resolve, reject) => { plugins.childProcess.execFile(cmd, args, { encoding: 'utf8' }, (err, stdout, stderr) => { if (err) { reject(Object.assign(err, { stdout, stderr })); } else { resolve({ stdout: stdout as string, stderr: stderr as string }); } }); }); } export class MacosKeychainBackend implements ISecretBackend { public readonly backendType: TBackendType = 'macos-keychain'; private service: string; constructor(service: string) { this.service = service; } async isAvailable(): Promise { if (process.platform !== 'darwin') return false; try { await execFile('which', ['security']); return true; } catch { return false; } } async setSecret(account: string, secret: string): Promise { // Delete existing entry first (ignore errors if not found) try { await execFile('security', [ 'delete-generic-password', '-s', this.service, '-a', account, ]); } catch { // Not found — fine } await execFile('security', [ 'add-generic-password', '-s', this.service, '-a', account, '-w', secret, '-U', ]); } async getSecret(account: string): Promise { try { const { stdout } = await execFile('security', [ 'find-generic-password', '-s', this.service, '-a', account, '-w', ]); return stdout.trim(); } catch { return null; } } async deleteSecret(account: string): Promise { try { await execFile('security', [ 'delete-generic-password', '-s', this.service, '-a', account, ]); return true; } catch { return false; } } async listAccounts(): Promise { try { const { stdout } = await execFile('security', [ 'dump-keychain', ]); const accounts: string[] = []; const serviceRegex = new RegExp(`"svce"="${this.service.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"`); const lines = stdout.split('\n'); for (let i = 0; i < lines.length; i++) { if (serviceRegex.test(lines[i])) { // Look for the account line nearby for (let j = Math.max(0, i - 5); j < Math.min(lines.length, i + 5); j++) { const match = lines[j].match(/"acct"="([^"]+)"/); if (match) { accounts.push(match[1]); break; } } } } return [...new Set(accounts)]; } catch { return []; } } }