Files
brain/tools/keychain-read.mjs
T

49 lines
2.6 KiB
JavaScript

#!/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;
}
}