← back to Ventura Claw

server/connectors/stripe.js

73 lines

// VenturaClaw — Stripe connector (REAL API)
// Reads STRIPE_SECRET_KEY from env. Use /secrets skill to inject; pm2 restart --update-env.
// Docs: https://docs.stripe.com/api

const STRIPE_API = "https://api.stripe.com/v1";

function key(creds) {
  const k = creds?.STRIPE_SECRET_KEY || process.env.STRIPE_SECRET_KEY;
  if (!k) throw new Error("STRIPE_SECRET_KEY not set (user creds or env)");
  return k;
}
function form(o) {
  const p = new URLSearchParams();
  const flat = (prefix, val) => {
    if (val === undefined || val === null) return;
    if (Array.isArray(val)) val.forEach((v, i) => flat(`${prefix}[${i}]`, v));
    else if (typeof val === "object") for (const k of Object.keys(val)) flat(`${prefix}[${k}]`, val[k]);
    else p.append(prefix, String(val));
  };
  for (const k of Object.keys(o || {})) flat(k, o[k]);
  return p.toString();
}
async function call(method, path, body, idem, creds) {
  const headers = { "Authorization": `Bearer ${key(creds)}` };
  if (idem) headers["Idempotency-Key"] = idem;
  let url = `${STRIPE_API}${path}`;
  const opts = { method, headers };
  if (method === "GET" && body) url += "?" + form(body);
  else if (body) {
    headers["Content-Type"] = "application/x-www-form-urlencoded";
    opts.body = form(body);
  }
  const r = await fetch(url, opts);
  const d = await r.json();
  if (!r.ok) throw new Error(`stripe ${path} ${r.status}: ${d.error?.message || JSON.stringify(d).slice(0,200)}`);
  return d;
}

const stripe = {
  meta: { id: "stripe", name: "Stripe", category: "payments", docsUrl: "https://docs.stripe.com/api", auth: "api_key", realImpl: true },
  fields: [
    { key: "STRIPE_SECRET_KEY", label: "Secret API key", type: "password", required: true, hint: "starts with sk_live_ or sk_test_ — Stripe Dashboard > Developers > API keys" }
  ],
  configured(creds) { return !!(creds?.STRIPE_SECRET_KEY || process.env.STRIPE_SECRET_KEY); },
  async health(creds) {
    if (!this.configured(creds)) return { ok: false, reason: "no_token" };
    try {
      const acct = await call("GET", "/account", null, null, creds);
      return { ok: true, account_id: acct.id, business: acct.business_profile?.name || acct.email, livemode: acct.charges_enabled && !key(creds).startsWith("sk_test_") };
    } catch (e) { return { ok: false, reason: e.message }; }
  },
  actions: {
    async "refund.create"({ charge, payment_intent, amount, reason }, creds) {
      if (!charge && !payment_intent) throw new Error("charge or payment_intent required");
      const body = {}; if (charge) body.charge = charge; if (payment_intent) body.payment_intent = payment_intent;
      if (amount) body.amount = amount; if (reason) body.reason = reason;
      return call("POST", "/refunds", body, `cc_refund_${charge||payment_intent}_${amount||"full"}`, creds);
    },
    async "charge.create"({ amount, currency, source, description, customer }, creds) {
      return call("POST", "/charges", { amount, currency, source, description, customer }, null, creds);
    },
    async "customer.create"({ email, name, phone }, creds) { return call("POST", "/customers", { email, name, phone }, null, creds); },
    async "payment_link.create"({ price, quantity }, creds) {
      return call("POST", "/payment_links", { "line_items[0][price]": price, "line_items[0][quantity]": quantity || 1 }, null, creds);
    },
    async "subscription.cancel"({ subscription }, creds) { return call("DELETE", `/subscriptions/${subscription}`, null, null, creds); },
    async "balance.get"(_, creds) { return call("GET", "/balance", null, null, creds); },
    async "charges.list"({ limit }, creds) { return call("GET", "/charges", { limit: limit || 10 }, null, creds); }
  }
};

module.exports = stripe;