← back to Ventura Claw
server/connectors/index.js
151 lines
// Connector registry — real implementations + per-user credential routing.
const slack = require("./slack");
const stripe = require("./stripe");
const cloudflare = require("./cloudflare");
const mailchimp = require("./mailchimp");
const hubspot = require("./hubspot");
const notion = require("./notion");
const airtable = require("./airtable");
const twilio = require("./twilio");
const figma = require("./figma");
const canva = require("./canva");
const etsy = require("./etsy");
const gmail = require("./gmail");
const purelymail = require("./purelymail");
const archive = require("./archive");
const elevenlabs = require("./elevenlabs");
const shopify = require("./shopify");
// Public no-auth APIs (no key required) — pre-connected for everyone.
const met = require("./met");
const cleveland = require("./cleveland");
const aic = require("./aic");
const wikipedia = require("./wikipedia");
const colorapi = require("./colorapi");
// SMB / civic — federal + state + local public-data APIs.
const sec = require("./sec");
const usaspending = require("./usaspending");
const nws = require("./nws");
const lacity = require("./lacity");
const cadata = require("./cadata");
const REAL = { slack, stripe, cloudflare, mailchimp, hubspot, notion, airtable, twilio, figma, canva, etsy, gmail, purelymail, archive, elevenlabs, shopify, met, cleveland, aic, wikipedia, colorapi, sec, usaspending, nws, lacity, cadata };
// Phase 8 — central sensitivity registry. Source of truth for "this action mutates state."
// Anything in WRITE_ACTIONS[id] requires the approval gate. Anything NOT listed is treated as read-only.
// Unknown connector or unknown action → fail-safe to "write" (require approval).
const WRITE_ACTIONS = {
airtable: new Set(["record.create", "record.update"]),
archive: new Set([]), // Internet Archive read-only — no writes exposed
canva: new Set([]),
cloudflare: new Set(["cache.purge_all", "cache.purge_files", "dns.create", "dns.update"]),
etsy: new Set([]),
figma: new Set([]),
gmail: new Set(["message.send"]),
hubspot: new Set(["contact.upsert", "deal.create"]),
mailchimp: new Set(["campaign.send", "member.upsert"]),
notion: new Set(["block.append", "page.create"]),
purelymail: new Set(["user.create"]),
slack: new Set(["channels.create", "chat.postMessage", "reactions.add"]),
stripe: new Set(["charge.create", "customer.create", "payment_link.create", "refund.create", "subscription.cancel"]),
twilio: new Set(["sms.send"]),
elevenlabs: new Set(["textToSpeech"]),
shopify: new Set(["order.fulfill", "order.refund", "product.update"]),
// Public no-auth APIs are read-only — no writes.
met: new Set([]),
cleveland: new Set([]),
aic: new Set([]),
wikipedia: new Set([]),
colorapi: new Set([]),
sec: new Set([]),
usaspending:new Set([]),
nws: new Set([]),
lacity: new Set([]),
cadata: new Set([]),
};
// Read actions enumerated per connector — explicit allowlist for fail-safe behavior.
const READ_ACTIONS = {
airtable: new Set(["bases.list", "tables.list", "records.list"]),
archive: new Set(["wayback.available", "wayback.cdx", "search", "metadata"]),
canva: new Set(["me", "design.get", "designs.list", "folders.list"]),
cloudflare: new Set(["dns.list", "zone.list"]),
etsy: new Set(["me", "shops.findByUser", "listings.active", "receipts.list"]),
figma: new Set(["me", "team.projects", "file.read", "file.comments"]),
gmail: new Set([]),
hubspot: new Set(["contacts.list", "deals.list"]),
mailchimp: new Set(["lists.list", "campaigns.list", "list.members.list"]),
notion: new Set(["search", "databases.query"]),
purelymail: new Set(["domains.list", "users.list"]),
slack: new Set(["channels.list", "users.lookup"]),
stripe: new Set(["balance.get", "charges.list"]),
twilio: new Set(["messages.list", "phone_numbers.list"]),
elevenlabs: new Set(["voicesList", "user", "modelsList"]),
shopify: new Set(["shop.info", "products.list", "products.count", "orders.list", "orders.recent", "collections.list"]),
met: new Set(["search", "object", "departments"]),
cleveland: new Set(["search", "artwork"]),
aic: new Set(["search", "artwork"]),
wikipedia: new Set(["summary", "search", "onthisday"]),
colorapi: new Set(["identify", "scheme"]),
sec: new Set(["ticker_lookup", "submissions", "company_concept"]),
usaspending:new Set(["search_awards", "recipient_summary"]),
nws: new Set(["forecast", "alerts"]),
lacity: new Set(["datasets", "query", "business_search"]),
cadata: new Set(["search_packages", "datastore_query"]),
};
function isWrite(id, action) {
// Unknown connector → fail-safe write.
if (!WRITE_ACTIONS[id]) return true;
// Explicitly listed write → write.
if (WRITE_ACTIONS[id].has(action)) return true;
// Explicitly listed read → read.
if (READ_ACTIONS[id]?.has(action)) return false;
// Unknown action on known connector → fail-safe write.
return true;
}
function get(id) { return REAL[id] || null; }
function listAll() {
return Object.values(REAL).map(c => ({
id: c.meta.id, name: c.meta.name, category: c.meta.category,
docsUrl: c.meta.docsUrl, fields: c.fields || [], realImpl: true
}));
}
function listConfigured(userCreds) {
return Object.values(REAL).map(c => ({
id: c.meta.id, name: c.meta.name,
configured: c.configured(userCreds?.[c.meta.id])
}));
}
async function health(id, userCreds) {
const c = get(id);
if (!c) return { ok: false, reason: "not_implemented" };
return c.health(userCreds);
}
async function execute(id, action, input, userCreds) {
const c = get(id);
if (!c) throw new Error(`connector ${id} has no real impl`);
const fn = c.actions[action];
if (!fn) throw new Error(`action ${action} not implemented on ${id}`);
return fn(input || {}, userCreds);
}
// Phase 8 — gated execute. Single sanctioned path for any caller in the app.
// opts.fromApproval = true → bypass approval gate (called by /api/approvals/:id/decide)
// opts.force = true → admin override; logged separately as connector_exec_forced
// Throws { code: "approval_required" } if gate engaged, so callers can route to the queue.
async function executeGated(id, action, input, userCreds, opts = {}) {
const writes = isWrite(id, action);
if (writes && !opts.fromApproval && !opts.force) {
const err = new Error(`approval_required: ${id}.${action} is a write action — queue it for approval`);
err.code = "approval_required";
err.connector = id;
err.action = action;
err.writes = true;
throw err;
}
return execute(id, action, input, userCreds);
}
module.exports = { get, listAll, listConfigured, health, execute, executeGated, isWrite, REAL, WRITE_ACTIONS, READ_ACTIONS };