import { execFile } from "node:child_process"; // macOS Keychain store for connector secrets. Mirrors the pattern Backend.kt // uses for the soul's provider key (service "ai.neuron.soul"); connectors live // under their own service so they never collide with the LLM key. // // Account convention: // → static bearer token (auth: "token") // :oauth → JSON-encoded OAuthTokens blob (auth: "oauth") // :verifier → transient PKCE code_verifier during a flow // :client → JSON OAuthClientInformation from dynamic registration const SERVICE = "ai.neuron.connect"; function run(args) { return new Promise((resolve) => { execFile("/usr/bin/security", args, (err, stdout) => { resolve({ code: err ? (err.code ?? 1) : 0, stdout: stdout ?? "" }); }); }); } export async function kcGet(account) { const { code, stdout } = await run(["find-generic-password", "-s", SERVICE, "-a", account, "-w"]); if (code !== 0) return undefined; const v = stdout.replace(/\n$/, ""); return v.length ? v : undefined; } export async function kcSet(account, secret) { // -U updates in place if the item already exists. const { code } = await run(["add-generic-password", "-U", "-s", SERVICE, "-a", account, "-w", secret]); return code === 0; } export async function kcDelete(account) { await run(["delete-generic-password", "-s", SERVICE, "-a", account]); } // JSON helpers for the OAuth blobs. export async function kcGetJson(account) { const raw = await kcGet(account); if (!raw) return undefined; try { return JSON.parse(raw); } catch { return undefined; } } export async function kcSetJson(account, value) { return kcSet(account, JSON.stringify(value)); }