#!/usr/bin/env node /** * keychain-read — синхронное чтение OS-keychain через async-only keytar. * * keytar НЕ имеет getPasswordSync (только async getPassword → Promise). Хуки роутер-наставника * читают ключ СИНХРОННО (resolveReceiptKey/resolveJudgeKey возвращают string|null без await). * Чтобы не делать асинхронными все гейты, читаем ключ в коротком node-subprocess: execFileSync * блокирует вызывающий поток, а внутри subprocess await'ит keytar.getPassword и печатает значение. * * keytar резолвится по АБСОЛЮТНОМУ пути (require.resolve относительно этого .mjs), чтобы subprocess * нашёл его независимо от своего cwd. Любая ошибка (нет keytar / spawn упал / пусто) → null * (fail-closed чтение: отсутствие ключа = неподписанная расписка невалидна на стороне verify). */ import { execFileSync } from 'node:child_process'; import { createRequire } from 'node:module'; /** Pure: node -e скрипт, который читает keytar.getPassword (async) и пишет значение в stdout. */ export function buildKeychainReadScript(keytarPath, service, account) { return `require(${JSON.stringify(keytarPath)}).getPassword(${JSON.stringify(service)},${JSON.stringify(account)})` + `.then(v=>{if(v!=null)process.stdout.write(String(v));}).catch(()=>{});`; } /** Pure: stdout subprocess → ключ или null (пусто/отсутствие → null). */ export function parseKeychainStdout(out) { const v = out == null ? '' : String(out); return v.length ? v : null; } /** * Синхронно прочитать запись keychain (service/account) через async keytar в subprocess. * requireImpl/exec инъектируются для тестов. Никогда не бросает → null на любой сбой. */ export function readKeychainSync(service, account, { requireImpl, exec } = {}) { try { const req = requireImpl || createRequire(import.meta.url); const keytarPath = req.resolve('keytar'); const script = buildKeychainReadScript(keytarPath, service, account); const runner = exec || execFileSync; const out = runner(process.execPath, ['-e', script], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000, }); return parseKeychainStdout(out); } catch { return null; } }