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