← 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); });