UNPKG

kestrel.markets

Version:

A typed, token-efficient language + runtime for agentic trading: agents author bounded plans, the runtime fires them at the tick. CLI + typed library + MCP server.

168 lines (166 loc) 5.58 kB
// src/cli/credentials.ts import { homedir } from "node:os"; import { join, dirname } from "node:path"; import { mkdirSync, writeFileSync, readFileSync, existsSync, chmodSync, renameSync } from "node:fs"; function defaultCredentialsPath(env = process.env) { const home = env.KESTREL_HOME ?? homedir(); return join(home, ".kestrel", "credentials.json"); } function defaultKeypairPath(env = process.env) { const home = env.KESTREL_HOME ?? homedir(); return join(home, ".kestrel", "agent-key.json"); } function saveCredential(cred, path = defaultCredentialsPath()) { const dir = dirname(path); mkdirSync(dir, { recursive: true, mode: 448 }); writeFileSync(path, JSON.stringify(cred, null, 2) + ` `, { mode: 384 }); chmodSync(path, 384); } function saveKeypair(keypair, path = defaultKeypairPath()) { const dir = dirname(path); mkdirSync(dir, { recursive: true, mode: 448 }); writeFileSync(path, JSON.stringify(keypair, null, 2) + ` `, { mode: 384 }); chmodSync(path, 384); } function loadKeypair(path = defaultKeypairPath()) { try { if (!existsSync(path)) return; const parsed = JSON.parse(readFileSync(path, "utf8")); if (typeof parsed?.subject === "string" && typeof parsed.api === "string" && typeof parsed.audience === "string" && parsed.private_jwk !== undefined && typeof parsed.private_jwk.d === "string") { return parsed; } return; } catch { return; } } function loadCredential(path = defaultCredentialsPath()) { try { if (!existsSync(path)) return; const parsed = JSON.parse(readFileSync(path, "utf8")); if (typeof parsed?.capability === "string" && parsed.capability.length > 0 && typeof parsed.api === "string") { return parsed; } return; } catch { return; } } var SECRET_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/; function defaultSecretsPath(env = process.env) { const home = env.KESTREL_HOME ?? homedir(); return join(home, ".kestrel", ".env"); } function assertValidSecretKey(key) { if (!SECRET_KEY_RE.test(key)) { throw new Error(`kestrel: invalid secret key ${JSON.stringify(key)} — must match ${SECRET_KEY_RE}`); } } function checkOperatorSecretKey(key) { if (/(^|_)LIVE(_|$)/i.test(key)) { return { code: "SECRET_LIVE_KEY_REFUSED", message: `refusing to store live-broker key ${JSON.stringify(key)} in the paper store — ~/.kestrel/.env is paper-only (OSS-ADR-0054 §5)`, hint: `a live key belongs in wrangler, never the paper store: kestrel env -- wrangler secret put ${key} (behind ALPACA_LIVE_ENABLED + allowlist)` }; } if (!/^[A-Z_][A-Z0-9_]*$/.test(key)) { return { code: "SECRET_INVALID_KEY_NAME", message: `invalid secret key name ${JSON.stringify(key)} — must match [A-Z_][A-Z0-9_]* (a canonical uppercase env-var name)`, hint: "use the canonical uppercase name, e.g. `kestrel secrets set DATABENTO_API_KEY`" }; } return; } function needsQuoting(value) { return value.length === 0 || /[\s#"'`$\\]/.test(value); } function serializeSecretLine(key, value) { if (!needsQuoting(value)) return `${key}=${value}`; const escaped = value.replace(/([\\"$`])/g, "\\$1"); return `${key}="${escaped}"`; } function serializeSecrets(store) { let out = ""; for (const [k, v] of store) out += serializeSecretLine(k, v) + ` `; return out; } function parseSecrets(text) { const out = new Map; for (const raw of text.split(` `)) { const line = raw.trim(); if (line.length === 0 || line.startsWith("#")) continue; const eq = line.indexOf("="); if (eq <= 0) continue; const key = line.slice(0, eq).trim(); if (!SECRET_KEY_RE.test(key)) continue; let val = line.slice(eq + 1).trim(); if (val.length >= 2 && val[0] === '"' && val[val.length - 1] === '"') { val = val.slice(1, -1).replace(/\\([\\"$`])/g, "$1"); } else if (val.length >= 2 && val[0] === "'" && val[val.length - 1] === "'") { val = val.slice(1, -1); } out.set(key, val); } return out; } function readSecretStore(path) { try { if (!existsSync(path)) return new Map; return parseSecrets(readFileSync(path, "utf8")); } catch { return new Map; } } function writeSecretStore(path, store) { const dir = dirname(path); mkdirSync(dir, { recursive: true, mode: 448 }); const tmp = `${path}.${process.pid}.tmp`; writeFileSync(tmp, serializeSecrets(store), { mode: 384 }); chmodSync(tmp, 384); renameSync(tmp, path); chmodSync(path, 384); } function saveSecret(key, value, path = defaultSecretsPath()) { assertValidSecretKey(key); const store = readSecretStore(path); store.set(key, value); writeSecretStore(path, store); } function secretNames(path = defaultSecretsPath()) { return [...readSecretStore(path).keys()].sort(); } function deleteSecret(key, path = defaultSecretsPath()) { assertValidSecretKey(key); const store = readSecretStore(path); if (!store.delete(key)) return false; writeSecretStore(path, store); return true; } function loadEnvFallback(opts = {}) { const env = opts.env ?? process.env; const path = opts.path ?? defaultSecretsPath(env); const populated = []; for (const [key, value] of readSecretStore(path)) { if (env[key] === undefined) { env[key] = value; populated.push(key); } } return populated.sort(); } export { defaultCredentialsPath, defaultKeypairPath, saveCredential, saveKeypair, loadKeypair, loadCredential, defaultSecretsPath, checkOperatorSecretKey, saveSecret, secretNames, deleteSecret, loadEnvFallback };