// keychain.mjs — Read a single secret from the macOS Keychain at runtime. // Zero dependencies. The C3 course detector reads its Platform API credentials // straight from the Keychain (never repo/env) — see c3-course-detection-plan.md §3. // // Fail-soft: a missing item (or a non-macOS host where `security` is absent) // returns null so the caller can skip cleanly rather than crash the pipeline. import { execFileSync } from 'node:child_process'; /** * Read a generic-password secret from the macOS Keychain. * @param {string} service — the keychain item's service name (-s) * @param {string} [account='ktg'] — the account name (-a) * @param {{ execImpl?: typeof execFileSync }} [opts] — inject exec for tests * @returns {string|null} the secret (trimmed) or null if the item is missing */ export function readSecret(service, account = 'ktg', { execImpl = execFileSync } = {}) { try { return execImpl( 'security', ['find-generic-password', '-s', service, '-a', account, '-w'], { encoding: 'utf8' }, ).trim(); } catch { // item missing / not macOS → caller fail-softs return null; } }