← back to Ventura Claw
scripts/sync-tokens.js
115 lines
#!/usr/bin/env node
// VenturaClaw — token sync agent.
// Runs locally on Steve's Mac (launchd com.steve.venturaclaw-sync).
// Reads ~/.claude.json MCP env + ~/Projects/secrets-manager/.env, POSTs to https://venturaclaw.com
// Token values NEVER leave Steve's machine except to land in his own VenturaClaw account.
const fs = require("fs");
const path = require("path");
const os = require("os");
const PROD = process.env.BC_URL || "https://venturaclaw.com";
const EMAIL = process.env.BC_USER || "steve@venturaclaw.com";
const PASS = process.env.BC_PASS;
if (!PASS) { console.error("FATAL: BC_PASS env var required (was hardcoded as a fallback — leaked in git history; rotate)"); process.exit(1); }
const LOG = path.join(os.homedir(), "Projects", "ventura-claw", "logs", "sync.log");
fs.mkdirSync(path.dirname(LOG), { recursive: true });
const log = (m) => { const line = `[${new Date().toISOString()}] ${m}\n`; fs.appendFileSync(LOG, line); process.stdout.write(line); };
function readEnvFile(p) {
if (!fs.existsSync(p)) return {};
const m = {};
for (const line of fs.readFileSync(p, "utf8").split("\n")) {
const mm = line.match(/^([A-Z0-9_]+)=(.*)$/);
if (!mm) continue;
let v = mm[2].trim();
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
m[mm[1]] = v;
}
return m;
}
function readClaudeMcp() {
try {
const j = JSON.parse(fs.readFileSync(path.join(os.homedir(), ".claude.json"), "utf8"));
return j.mcpServers || {};
} catch { return {}; }
}
function scanProjectEnvs(homeDir) {
// Walk ~/Projects/*/.env, .env.local, .env.production for known token keys.
// Returns merged map; later wins. Skip secrets-manager (already sourced separately).
const merged = {};
const projectsDir = path.join(homeDir, "Projects");
if (!fs.existsSync(projectsDir)) return merged;
for (const proj of fs.readdirSync(projectsDir)) {
if (proj === "secrets-manager") continue;
if (proj.startsWith(".")) continue;
for (const fname of [".env", ".env.local", ".env.production", "server/.env"]) {
const p = path.join(projectsDir, proj, fname);
if (!fs.existsSync(p)) continue;
try {
const m = readEnvFile(p);
for (const k of Object.keys(m)) {
if (m[k] && !merged[k]) merged[k] = m[k]; // first-wins to avoid clobbering
}
} catch {}
}
}
return merged;
}
(async () => {
const env = readEnvFile(path.join(os.homedir(), "Projects", "secrets-manager", ".env"));
const proj = scanProjectEnvs(os.homedir());
const mcp = readClaudeMcp();
const mcpEnv = (n) => mcp[n]?.env || {};
const pick = (...keys) => { for (const k of keys) { if (env[k]) return env[k]; if (proj[k]) return proj[k]; } return undefined; };
log(`scan: secrets-manager=${Object.keys(env).length} keys · project-envs=${Object.keys(proj).length} keys · mcp-servers=${Object.keys(mcp).length}`);
// Routing — same as injection script we used earlier
const ROUTES = [
["stripe", { STRIPE_SECRET_KEY: pick("STRIPE_SECRET_KEY") }],
["cloudflare", { CF_API_TOKEN: pick("CLOUDFLARE_API_TOKEN", "CF_API_TOKEN") }],
["figma", { FIGMA_TOKEN: pick("FIGMA_TOKEN", "FIGMA_API_TOKEN", "FIGMA_PERSONAL_ACCESS_TOKEN") || mcpEnv("figma").FIGMA_API_TOKEN || mcpEnv("figma").FIGMA_TOKEN }],
["canva", { CANVA_TOKEN: pick("CANVA_TOKEN", "CANVA_API_TOKEN") || mcpEnv("canva").CANVA_TOKEN }],
["etsy", { ETSY_KEYSTRING: pick("ETSY_KEYSTRING", "ETSY_API_KEY") || mcpEnv("etsy").ETSY_KEYSTRING,
ETSY_ACCESS_TOKEN: pick("ETSY_ACCESS_TOKEN") || mcpEnv("etsy").ETSY_ACCESS_TOKEN }],
["gmail", { GEORGE_URL: mcpEnv("george").GEORGE_URL || "http://127.0.0.1:9850",
GEORGE_BASIC_AUTH: mcpEnv("george").GEORGE_BASIC_AUTH || pick("GEORGE_BASIC_AUTH") }],
["purelymail", { PURELYMAIL_API_TOKEN: pick("PURELYMAIL_API_TOKEN") || mcpEnv("purelymail").PURELYMAIL_API_TOKEN }],
["mailchimp", { MAILCHIMP_API_KEY: pick("MAILCHIMP_API_KEY") }],
["hubspot", { HUBSPOT_TOKEN: pick("HUBSPOT_TOKEN", "HUBSPOT_ACCESS_TOKEN", "HUBSPOT_PRIVATE_APP_TOKEN") }],
["notion", { NOTION_TOKEN: pick("NOTION_TOKEN", "NOTION_API_KEY") }],
["airtable", { AIRTABLE_PAT: pick("AIRTABLE_PAT", "AIRTABLE_TOKEN", "AIRTABLE_API_KEY"), AIRTABLE_BASE_ID: pick("AIRTABLE_BASE_ID") }],
["slack", { SLACK_BOT_TOKEN: pick("SLACK_BOT_TOKEN", "SLACK_TOKEN") }],
["twilio", { TWILIO_ACCOUNT_SID: pick("TWILIO_ACCOUNT_SID", "TWILIO_SID"),
TWILIO_AUTH_TOKEN: pick("TWILIO_AUTH_TOKEN"),
TWILIO_FROM_NUMBER: pick("TWILIO_FROM_NUMBER", "TWILIO_PHONE_NUMBER", "TWILIO_FROM") }],
["elevenlabs", { ELEVENLABS_API_KEY: pick("ELEVENLABS_API_KEY") }],
];
// Login
const r = await fetch(`${PROD}/api/auth/login`, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: EMAIL, password: PASS })
});
const cookie = (r.headers.get("set-cookie") || "").match(/cc_session=[^;]+/)?.[0];
if (!cookie) { log(`LOGIN FAILED status=${r.status}`); process.exit(1); }
log(`login ok as ${EMAIL}`);
let live = 0, missing = 0, failed = 0;
for (const [id, payload] of ROUTES) {
const clean = Object.fromEntries(Object.entries(payload).filter(([_, v]) => v));
if (!Object.keys(clean).length) { log(` ⊘ ${id}: no value found`); missing++; continue; }
await fetch(`${PROD}/api/me/connections/${id}`, {
method: "POST",
headers: { "Content-Type": "application/json", "Cookie": cookie },
body: JSON.stringify(clean)
});
const h = await fetch(`${PROD}/api/connectors/${id}/health`, { headers: { "Cookie": cookie } }).then(r => r.json());
if (h.ok) { log(` ✓ ${id}: ${h.user || h.team || h.account_id || h.account || h.user_id || h.endpoint || "live"}`); live++; }
else { log(` ✗ ${id}: ${h.reason}`); failed++; }
}
log(`done · live=${live} missing=${missing} failed=${failed}`);
})().catch(e => { log(`ERROR: ${e.message}`); process.exit(1); });