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 }); } }); }); } function spawnWithStdin(cmd: string, args: string[], stdinData: string): Promise<{ stdout: string; stderr: string }> { return new Promise((resolve, reject) => { const child = plugins.childProcess.spawn(cmd, args, { stdio: ['pipe', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; child.stdout!.on('data', (chunk: Buffer) => { stdout += chunk.toString(); }); child.stderr!.on('data', (chunk: Buffer) => { stderr += chunk.toString(); }); child.on('error', reject); child.on('close', (code) => { if (code === 0) { resolve({ stdout, stderr }); } else { reject(Object.assign(new Error(`secret-tool exited with code ${code}`), { stdout, stderr })); } }); child.stdin!.write(stdinData); child.stdin!.end(); }); } export class LinuxSecretServiceBackend implements ISecretBackend { public readonly backendType: TBackendType = 'linux-secret-service'; private service: string; constructor(service: string) { this.service = service; } async isAvailable(): Promise { if (process.platform !== 'linux') return false; try { await execFile('which', ['secret-tool']); return true; } catch { return false; } } async setSecret(account: string, secret: string): Promise { // secret-tool store reads password from stdin await spawnWithStdin('secret-tool', [ 'store', '--label', `${this.service}:${account}`, 'service', this.service, 'account', account, ], secret); } async getSecret(account: string): Promise { try { const { stdout } = await execFile('secret-tool', [ 'lookup', 'service', this.service, 'account', account, ]); // secret-tool returns empty string if not found (exit 0) or exits non-zero return stdout.length > 0 ? stdout : null; } catch { return null; } } async deleteSecret(account: string): Promise { try { await execFile('secret-tool', [ 'clear', 'service', this.service, 'account', account, ]); return true; } catch { return false; } } async listAccounts(): Promise { try { const { stdout } = await execFile('secret-tool', [ 'search', '--all', 'service', this.service, ]); const accounts: string[] = []; const regex = /attribute\.account = (.+)/g; let match: RegExpExecArray | null; while ((match = regex.exec(stdout)) !== null) { accounts.push(match[1].trim()); } return [...new Set(accounts)]; } catch { return []; } } }