← back to Ventura Claw
server/connectors/cloudflare.js
75 lines
// VenturaClaw — Cloudflare connector (REAL API)
// Reads CF_API_TOKEN from env (account-scoped or zone-scoped per Steve's MEMORY note about token leak).
// Docs: https://developers.cloudflare.com/api/
const CF_API = "https://api.cloudflare.com/client/v4";
function token(creds) {
const t = creds?.CF_API_TOKEN || process.env.CF_API_TOKEN;
if (!t) throw new Error("CF_API_TOKEN not set (user creds or env)");
return t;
}
async function call(method, path, body, creds) {
const r = await fetch(`${CF_API}${path}`, {
method,
headers: { "Authorization": `Bearer ${token(creds)}`, "Content-Type": "application/json" },
body: body ? JSON.stringify(body) : undefined
});
const d = await r.json();
if (!d.success) throw new Error(`cloudflare ${path} failed: ${JSON.stringify(d.errors).slice(0,300)}`);
return d.result;
}
async function zoneByName(name, creds) {
// Try exact match, then walk parent domains (handles subdomains)
let parts = name.split(".");
while (parts.length >= 2) {
const candidate = parts.join(".");
const zones = await call("GET", `/zones?name=${encodeURIComponent(candidate)}`, null, creds);
if (zones.length) return zones[0];
parts = parts.slice(1);
}
throw new Error(`zone not found for ${name}`);
}
const cloudflare = {
meta: { id: "cloudflare", name: "Cloudflare", category: "security", docsUrl: "https://developers.cloudflare.com/api/", auth: "api_key", realImpl: true },
fields: [
{ key: "CF_API_TOKEN", label: "API Token", type: "password", required: true, hint: "Cloudflare > My Profile > API Tokens. Use account-scoped token, NOT global key." }
],
configured(creds) { return !!(creds?.CF_API_TOKEN || process.env.CF_API_TOKEN); },
async health(creds) {
if (!this.configured(creds)) return { ok: false, reason: "no_token" };
try {
const verify = await call("GET", "/user/tokens/verify", null, creds);
return { ok: true, status: verify.status, expires_on: verify.expires_on };
} catch (e) { return { ok: false, reason: e.message }; }
},
actions: {
async "zone.list"(_, creds) { return call("GET", "/zones?per_page=50", null, creds); },
async "dns.list"({ zone }, creds) {
const z = await zoneByName(zone, creds);
return call("GET", `/zones/${z.id}/dns_records?per_page=200`, null, creds);
},
async "dns.create"({ zone, type, name, content, ttl, proxied }, creds) {
const z = await zoneByName(zone, creds);
return call("POST", `/zones/${z.id}/dns_records`, { type, name, content, ttl: ttl || 1, proxied: !!proxied }, creds);
},
async "dns.update"({ zone, record_id, type, name, content, ttl, proxied }, creds) {
const z = await zoneByName(zone, creds);
return call("PATCH", `/zones/${z.id}/dns_records/${record_id}`, { type, name, content, ttl, proxied }, creds);
},
async "cache.purge_all"({ zone }, creds) {
const z = await zoneByName(zone, creds);
return call("POST", `/zones/${z.id}/purge_cache`, { purge_everything: true }, creds);
},
async "cache.purge_files"({ zone, files }, creds) {
const z = await zoneByName(zone, creds);
return call("POST", `/zones/${z.id}/purge_cache`, { files }, creds);
}
}
};
module.exports = cloudflare;