← back to Ventura Claw
relay/server.js
264 lines
#!/usr/bin/env node
// VenturaClaw — local Mac relay (loopback only, never exposed to LAN/Internet).
// Lets Steve paste KEY=value lines ONCE; writes to ~/Projects/secrets-manager/.env
// and triggers immediate VenturaClaw sync via launchctl.
const http = require("http");
const fs = require("fs");
const path = require("path");
const os = require("os");
const { spawn } = require("child_process");
const PORT = 9790;
const HOST = "127.0.0.1";
const SECRETS_ENV = path.join(os.homedir(), "Projects", "secrets-manager", ".env");
const ROOT = path.resolve(__dirname);
const PUB = path.join(ROOT, "public");
if (!fs.existsSync(PUB)) fs.mkdirSync(PUB, { recursive: true });
function send(res, code, body, headers = {}) {
res.writeHead(code, {
"Content-Type": typeof body === "string" ? "text/html; charset=utf-8" : "application/json",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, GET, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
...headers
});
res.end(typeof body === "string" ? body : JSON.stringify(body));
}
function parseEnvBlob(text) {
const out = {};
for (const line of (text || "").split("\n")) {
const m = line.trim().match(/^(?:export\s+)?([A-Z0-9_]+)\s*=\s*(.+?)\s*$/);
if (!m) continue;
let v = m[2];
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
out[m[1]] = v;
}
return out;
}
function readEnvFile(p) {
if (!fs.existsSync(p)) return { lines: [], map: {} };
const lines = fs.readFileSync(p, "utf8").split("\n");
const map = {};
lines.forEach((ln, i) => {
const m = ln.match(/^(?:export\s+)?([A-Z0-9_]+)\s*=/);
if (m) map[m[1]] = i;
});
return { lines, map };
}
function writeEnvMerge(p, kv) {
const { lines, map } = readEnvFile(p);
let added = 0, updated = 0;
for (const [k, v] of Object.entries(kv)) {
if (!v) continue;
if (k in map) {
lines[map[k]] = `${k}=${v}`;
updated++;
} else {
lines.push(`${k}=${v}`);
added++;
}
}
if (added || updated) {
fs.writeFileSync(p, lines.join("\n"));
}
return { added, updated };
}
function triggerVenturaClawSync() {
return new Promise((resolve) => {
const p = spawn("launchctl", ["start", "com.steve.venturaclaw-sync"]);
p.on("close", (code) => resolve({ code }));
p.on("error", (err) => resolve({ error: err.message }));
});
}
const HTML = `<!doctype html>
<html lang="en"><head>
<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" />
<title>VenturaClaw — Local Token Relay</title>
<style>
:root {
--ink: #f4f1ea;
--ink-mute: rgba(244,241,234,0.62);
--bg: #0e0e10;
--rule: rgba(244,241,234,0.10);
--gold: #d4a04a;
--good: #8fb89a;
--bad: #c9856e;
--serif: "Cormorant Garamond", Georgia, serif;
--mono: "JetBrains Mono", ui-monospace, monospace;
--sans: "Inter", -apple-system, system-ui, sans-serif;
}
@import url("https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,500;1,500&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400&display=swap");
* { box-sizing: border-box; }
html, body { margin: 0; min-height: 100%; background: var(--bg); color: var(--ink); font-family: var(--sans); }
body::before { content:""; position:fixed; inset:0; background: radial-gradient(ellipse 80% 60% at 18% -10%, rgba(212,160,74,0.06), transparent 60%); pointer-events:none; }
main { max-width: 760px; margin: 40px auto; padding: 0 24px; position: relative; z-index: 1; }
.card { background: rgba(244,241,234,0.04); border: 1px solid var(--rule); border-radius: 4px; padding: 24px; margin-bottom: 18px; backdrop-filter: blur(12px); }
h1 { font-family: var(--serif); font-weight: 500; font-size: 28px; margin: 0 0 6px; letter-spacing: -.01em; }
h1 em { color: var(--gold); }
.sub { font-family: var(--mono); font-size: 10px; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-mute); }
textarea { width: 100%; background: rgba(244,241,234,0.04); border: 1px solid var(--rule); color: var(--ink); padding: 14px; border-radius: 2px; font-family: var(--mono); font-size: 12px; line-height: 1.55; min-height: 280px; outline: none; resize: vertical; }
textarea:focus { border-color: var(--gold); }
.row { display: flex; justify-content: space-between; align-items: center; margin-top: 14px; gap: 14px; }
button { font-family: var(--mono); font-size: 10px; letter-spacing: .14em; text-transform: uppercase; background: var(--gold); border: 1px solid var(--gold); color: var(--bg); padding: 10px 18px; border-radius: 2px; cursor: pointer; transition: background 160ms; }
button:hover { background: #e0b160; }
button:disabled { opacity: 0.5; cursor: not-allowed; }
.status { font-family: var(--mono); font-size: 10px; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-mute); }
.results { font-family: var(--mono); font-size: 12px; line-height: 1.7; margin-top: 14px; }
.results .ok { color: var(--good); }
.results .err { color: var(--bad); }
.results .info { color: var(--ink-mute); }
small { font-family: var(--mono); font-size: 10px; letter-spacing: .08em; color: var(--ink-mute); }
code { font-family: var(--mono); background: rgba(244,241,234,0.04); padding: 1px 6px; border-radius: 2px; }
</style>
</head><body>
<main>
<div class="card">
<div class="sub">LOCAL · 127.0.0.1:9790 · loopback only</div>
<h1>Paste your <em>tokens</em> once</h1>
<p style="font-size:13px;opacity:0.78;line-height:1.6;margin:8px 0 0">
One paste writes <code>KEY=value</code> lines to <code>~/Projects/secrets-manager/.env</code>
AND immediately triggers <code>com.steve.venturaclaw-sync</code> so the
<a href="https://venturaclaw.com/admin/connectors" style="color:var(--gold)">VenturaClaw connectors page</a>
flips tiles green within seconds. From this moment on, the launchd job keeps everything in sync — no further pasting ever.
</p>
</div>
<div class="card">
<textarea id="paste" placeholder="# example
SLACK_BOT_TOKEN=xoxb-1234-5678-...
MAILCHIMP_API_KEY=abcdef-us6
HUBSPOT_TOKEN=pat-na1-...
NOTION_TOKEN=ntn_...
AIRTABLE_PAT=pat...
TWILIO_ACCOUNT_SID=AC...
TWILIO_AUTH_TOKEN=...
FIGMA_TOKEN=figd_...
CANVA_TOKEN=...
ETSY_KEYSTRING=...
ETSY_ACCESS_TOKEN=..."></textarea>
<div class="row">
<span class="status" id="status">paste keys above</span>
<button id="save">Save & sync</button>
</div>
<div id="results" class="results"></div>
</div>
<small>This server runs only on <code>127.0.0.1</code>, never accessible from outside your Mac. Tokens travel: browser → loopback → secrets-manager file → launchd → venturaclaw.</small>
</main>
<script>
const paste = document.getElementById("paste");
const status = document.getElementById("status");
const save = document.getElementById("save");
const results = document.getElementById("results");
paste.addEventListener("input", () => {
const lines = paste.value.split("\\n").filter(l => /^[A-Z0-9_]+=/.test(l.trim()));
status.textContent = lines.length + " keys detected";
});
save.addEventListener("click", async () => {
if (!paste.value.trim()) return;
save.disabled = true; save.textContent = "Saving…";
results.innerHTML = '<div class="info">writing to secrets-manager .env…</div>';
try {
const r = await fetch("/save", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ paste: paste.value }) });
const d = await r.json();
let html = "";
html += '<div class="ok">✓ secrets-manager: ' + d.added + ' added · ' + d.updated + ' updated</div>';
html += '<div class="info">→ launching com.steve.venturaclaw-sync…</div>';
html += d.synced ? '<div class="ok">✓ launchd triggered</div>' : '<div class="err">✗ launchd: ' + (d.syncError || 'failed') + '</div>';
html += '<div class="info" style="margin-top:8px">— now reload <a href="https://venturaclaw.com/admin/connectors" style="color:var(--gold)">/admin/connectors</a> in 5 sec —</div>';
results.innerHTML = html;
save.textContent = "Saved";
setTimeout(() => { save.textContent = "Save & sync"; save.disabled = false; }, 2000);
} catch (e) {
results.innerHTML = '<div class="err">✗ ' + e.message + '</div>';
save.textContent = "Save & sync"; save.disabled = false;
}
});
</script>
</body></html>`;
const server = http.createServer(async (req, res) => {
if (req.method === "OPTIONS") return send(res, 200, "ok");
if (req.method === "GET" && (req.url === "/" || req.url === "/import")) return send(res, 200, HTML);
if (req.method === "GET" && req.url === "/healthz") return send(res, 200, { ok: true });
// Expose MCP server config from ~/.claude.json — names + env-key names only, NEVER values.
// Used by VenturaClaw /api/anthropic/discover-mcps when running on the same machine.
if (req.method === "GET" && req.url === "/api/mcp-config") {
try {
const j = JSON.parse(fs.readFileSync(path.join(os.homedir(), ".claude.json"), "utf8"));
const safe = {};
for (const [name, def] of Object.entries(j.mcpServers || {})) {
safe[name] = {
command: def.command || null,
env: Object.fromEntries(Object.keys(def.env || {}).map(k => [k, "[redacted]"]))
};
}
return send(res, 200, { mcpServers: safe });
} catch (e) { return send(res, 500, { error: e.message }); }
}
// PRIVILEGED: returns actual MCP env values + secrets-manager values.
// Loopback-only by virtue of relay binding to 127.0.0.1.
// Steve approved (auto-import flow) 2026-05-06.
if (req.method === "GET" && req.url === "/api/mcp-tokens") {
try {
const out = { mcp: {}, env: {} };
// ~/.claude.json mcpServers env values
try {
const j = JSON.parse(fs.readFileSync(path.join(os.homedir(), ".claude.json"), "utf8"));
for (const [name, def] of Object.entries(j.mcpServers || {})) {
out.mcp[name] = { ...(def.env || {}) };
}
} catch {}
// secrets-manager .env values
try {
const text = fs.readFileSync(path.join(os.homedir(), "Projects", "secrets-manager", ".env"), "utf8");
for (const line of text.split("\n")) {
const m = line.match(/^\s*(?:export\s+)?([A-Z_][A-Z0-9_]*)\s*=\s*(.*?)\s*$/);
if (!m) continue;
let v = m[2];
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
if (v) out.env[m[1]] = v;
}
} catch {}
return send(res, 200, out);
} catch (e) { return send(res, 500, { error: e.message }); }
}
if (req.method === "POST" && req.url === "/save") {
let body = "";
req.on("data", c => body += c);
req.on("end", async () => {
try {
const { paste } = JSON.parse(body || "{}");
if (!paste) return send(res, 400, { error: "missing paste" });
const kv = parseEnvBlob(paste);
if (!Object.keys(kv).length) return send(res, 400, { error: "no recognized KEY=value lines" });
const { added, updated } = writeEnvMerge(SECRETS_ENV, kv);
const sync = await triggerVenturaClawSync();
send(res, 200, { added, updated, keys: Object.keys(kv), synced: !sync.error, syncError: sync.error || null });
} catch (e) {
send(res, 500, { error: e.message });
}
});
return;
}
send(res, 404, { error: "not found" });
});
server.listen(PORT, HOST, () => {
console.log(`[bc-relay] listening on http://${HOST}:${PORT} (loopback only)`);
console.log(`[bc-relay] open http://${HOST}:${PORT}/import to paste tokens`);
});