/** * 1Password CLI integration for `rp ai-test`. Each user keeps their sandbox / * production test credentials in their own 1Password vault — no shared * .env file, no secrets in git, per-user audit trail. * * Required setup on the user's machine: * 1. brew install --cask 1password-cli * 2. op signin (one-time per terminal session) * 3. Have a Login item with the dashboard email + password + 6-digit TOTP * * The skill calls `op read` for password and `op item get --otp` for TOTP, * so we never echo either through stdout in this process. The Claude CLI * subprocess receives them via its argv-passed prompt — that's a tradeoff * we make consciously: the alternative (writing creds to a temp file the * inner agent reads) is strictly worse since the file lingers on disk. */ import { execFileSync } from 'node:child_process'; /** Injectable exec for tests. Real callers leave undefined and get execFileSync. */ export type ExecFn = (cmd: string, args: string[]) => string; const defaultExec: ExecFn = (cmd, args) => execFileSync(cmd, args, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(); export interface OnePasswordCreds { username: string; password: string; totp: string; } export class OnePasswordNotInstalledError extends Error { constructor() { super( 'The 1Password CLI (`op`) is required by `rp ai-test`. Install it with `brew install --cask 1password-cli` and run `op signin` once before retrying.', ); this.name = 'OnePasswordNotInstalledError'; } } export class OnePasswordNotSignedInError extends Error { constructor(stderr: string) { super( `1Password CLI is installed but not signed in for this terminal. Run \`op signin\` and try again.\nUnderlying error: ${stderr.trim()}`, ); this.name = 'OnePasswordNotSignedInError'; } } export class OnePasswordItemMissingError extends Error { constructor(item: string, field: string, stderr: string) { super( `1Password item "${item}" doesn't expose a "${field}" field (or the item itself doesn't exist).\nUnderlying error: ${stderr.trim()}`, ); this.name = 'OnePasswordItemMissingError'; } } /** Run `op` with the given args; map common failures to typed errors. */ export const opExec = (args: string[], exec: ExecFn = defaultExec): string => { try { return exec('op', args); } catch (error) { const e = error as { code?: string; stderr?: Buffer | string }; if (e.code === 'ENOENT') { throw new OnePasswordNotInstalledError(); } const stderr = typeof e.stderr === 'string' ? e.stderr : (e.stderr?.toString() ?? ''); if (/not currently signed in|missing session|sessionToken/i.test(stderr)) { throw new OnePasswordNotSignedInError(stderr); } throw error; } }; /** * Fetch username + password + current TOTP from a 1Password item by its * stable title. `op item get