← back to Ventura Claw

server/connectors/twilio.js

54 lines

// Twilio — REAL. Account SID + Auth Token (Basic auth).
function sid(c) {
  const s = c?.TWILIO_ACCOUNT_SID || process.env.TWILIO_ACCOUNT_SID;
  if (!s) throw new Error("TWILIO_ACCOUNT_SID not set");
  return s;
}
function authToken(c) {
  const t = c?.TWILIO_AUTH_TOKEN || process.env.TWILIO_AUTH_TOKEN;
  if (!t) throw new Error("TWILIO_AUTH_TOKEN not set");
  return t;
}
async function call(method, path, params, c) {
  const url = `https://api.twilio.com/2010-04-01/Accounts/${sid(c)}${path}`;
  const auth = Buffer.from(`${sid(c)}:${authToken(c)}`).toString("base64");
  const opts = { method, headers: { "Authorization": `Basic ${auth}` } };
  let finalUrl = url;
  if (method === "GET" && params) finalUrl += "?" + new URLSearchParams(params).toString();
  else if (params) {
    opts.headers["Content-Type"] = "application/x-www-form-urlencoded";
    opts.body = new URLSearchParams(params).toString();
  }
  const r = await fetch(finalUrl, opts);
  const d = await r.json();
  if (!r.ok) throw new Error(`twilio ${path} ${r.status}: ${d.message || JSON.stringify(d).slice(0,200)}`);
  return d;
}
module.exports = {
  meta: { id: "twilio", name: "Twilio", category: "comms", docsUrl: "https://www.twilio.com/docs/usage/api", auth: "basic", realImpl: true },
  fields: [
    { key: "TWILIO_ACCOUNT_SID", label: "Account SID",   type: "text",     required: true, hint: "ACxxxxx — console.twilio.com" },
    { key: "TWILIO_AUTH_TOKEN",  label: "Auth Token",    type: "password", required: true, hint: "next to Account SID" },
    { key: "TWILIO_FROM_NUMBER", label: "From Number",   type: "text",     required: false, hint: "+15551234567 — your Twilio number (optional default)" }
  ],
  configured(c) { return !!((c?.TWILIO_ACCOUNT_SID || process.env.TWILIO_ACCOUNT_SID) && (c?.TWILIO_AUTH_TOKEN || process.env.TWILIO_AUTH_TOKEN)); },
  async health(c) {
    if (!this.configured(c)) return { ok: false, reason: "no_token" };
    try { const d = await call("GET", ".json", null, c); return { ok: true, friendly_name: d.friendly_name, status: d.status }; }
    catch (e) { return { ok: false, reason: e.message }; }
  },
  actions: {
    async "sms.send"({ to, body, from }, c) {
      const f = from || c?.TWILIO_FROM_NUMBER || process.env.TWILIO_FROM_NUMBER;
      if (!to || !body || !f) throw new Error("to, body, from required");
      return call("POST", "/Messages.json", { To: to, From: f, Body: body }, c);
    },
    async "messages.list"({ limit }, c) {
      return call("GET", "/Messages.json", { PageSize: String(limit || 20) }, c);
    },
    async "phone_numbers.list"(_, c) {
      return call("GET", "/IncomingPhoneNumbers.json", null, c);
    }
  }
};