EXTENSION for treeos-connect
gateway-sms
SMS channel type for the gateway extension. Registers the sms channel type at boot, enabling trees to send and receive text messages through Twilio. No Twilio SDK required. The handler calls the Twilio REST API directly using basic auth, sending form-encoded POST requests to the Messages endpoint. Outbound messages are composed from notification title and content, trimmed to 1500 characters to stay within Twilio's segmentation limits. Three directions are supported: input (receive SMS into a tree), output (send SMS from a tree), and input-output (bidirectional conversation). For output channels, a toNumber in E.164 format is required. Twilio credentials (account SID, auth token, from number) can be set globally via environment variables or overridden per channel through encrypted config. Per-channel secrets are stored using the gateway core's AES encryption, so one land can run multiple Twilio accounts across different trees. Inbound SMS arrives via a Twilio webhook at POST /api/v1/gateway/sms/:channelId. The endpoint responds immediately with empty TwiML to prevent Twilio retries, then processes the message asynchronously. Twilio sends form-encoded data including From, Body, and MessageSid. The handler validates the channel exists and is enabled, extracts the sender phone number and message text, then delegates to the gateway core's processGatewayMessage function which runs the message through the tree's AI in the configured mode. For input-output channels, the AI reply is sent back as an SMS to the sender's phone number. Trees in your pocket without an app.
v1.0.1 by TreeOS Site 0 downloads 4 files 268 lines 9.4 KB published 38d ago
treeos ext install gateway-sms
View changelog

Manifest

Requires

  • extensions: gateway
SHA256: fc748e2983ded5c9e3b04fb031de82c3e32c6830f1b26723f1d3335ebd96a576

Dependents

1 package depend on this

PackageTypeRelationship
treeos-connect v1.0.3bundleincludes

Environment Variables

KeyRequiredDescription
TWILIO_ACCOUNT_SID No Twilio Account SID
TWILIO_AUTH_TOKEN secret No Twilio Auth Token
TWILIO_FROM_NUMBER No Twilio phone number to send from (e.g., +15551234567)

Source Code

1// SMS channel type handler (Twilio).
2// Output: sends SMS via Twilio API.
3// Input: receives inbound SMS via Twilio webhook.
4// Config: toNumber (required for output). accountSid, authToken, fromNumber from env or per-channel.
5
6import log from "../../seed/log.js";
7import crypto from "crypto";
8
9const PHONE_RE = /^\+[1-9]\d{6,14}$/;
10
11function validateConfig(config, direction) {
12  const hasOutput = direction === "output" || direction === "input-output";
13
14  if (hasOutput) {
15    if (!config.toNumber || !PHONE_RE.test(config.toNumber)) {
16      throw new Error("SMS output requires a valid toNumber in E.164 format (e.g., +15551234567)");
17    }
18  }
19
20  const sid = config.accountSid || process.env.TWILIO_ACCOUNT_SID;
21  const token = config.authToken || process.env.TWILIO_AUTH_TOKEN;
22  const from = config.fromNumber || process.env.TWILIO_FROM_NUMBER;
23
24  if (!sid || !token || !from) {
25    throw new Error("SMS requires TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_FROM_NUMBER in .env or channel config");
26  }
27}
28
29function buildEncryptedConfig(config, direction) {
30  const secrets = {};
31  const metadata = {};
32
33  // Per-channel Twilio overrides (optional, falls back to env)
34  if (config.accountSid) secrets.accountSid = config.accountSid;
35  if (config.authToken) secrets.authToken = config.authToken;
36  if (config.fromNumber) secrets.fromNumber = config.fromNumber;
37
38  if (config.toNumber) metadata.toNumber = config.toNumber;
39
40  // Webhook verification token for inbound
41  const hasInput = direction === "input" || direction === "input-output";
42  if (hasInput) {
43    metadata.webhookSecret = crypto.randomBytes(24).toString("hex");
44  }
45
46  return {
47    secrets,
48    metadata,
49    displayIdentifier: config.toNumber || config.fromNumber || "sms",
50  };
51}
52
53function getTwilioCreds(secrets) {
54  return {
55    accountSid: secrets.accountSid || process.env.TWILIO_ACCOUNT_SID,
56    authToken: secrets.authToken || process.env.TWILIO_AUTH_TOKEN,
57    fromNumber: secrets.fromNumber || process.env.TWILIO_FROM_NUMBER,
58  };
59}
60
61async function sendSms(creds, to, body) {
62  // Twilio REST API. No SDK needed.
63  const url = `https://api.twilio.com/2010-04-01/Accounts/${creds.accountSid}/Messages.json`;
64  const auth = Buffer.from(`${creds.accountSid}:${creds.authToken}`).toString("base64");
65
66  const params = new URLSearchParams();
67  params.append("To", to);
68  params.append("From", creds.fromNumber);
69  params.append("Body", body);
70
71  const res = await fetch(url, {
72    method: "POST",
73    headers: {
74      "Authorization": `Basic ${auth}`,
75      "Content-Type": "application/x-www-form-urlencoded",
76    },
77    body: params.toString(),
78  });
79
80  if (!res.ok) {
81    const text = await res.text();
82    throw new Error(`Twilio error ${res.status}: ${text}`);
83  }
84}
85
86async function send(secrets, metadata, notification) {
87  const creds = getTwilioCreds(secrets);
88  const body = notification.title
89    ? `${notification.title}\n\n${notification.content}`
90    : notification.content;
91
92  // SMS max 1600 chars (Twilio auto-segments, but keep it reasonable)
93  const trimmed = body.length > 1500 ? body.slice(0, 1497) + "..." : body;
94  await sendSms(creds, metadata.toNumber, trimmed);
95}
96
97async function registerInput(channel, secrets) {
98  log.info("GatewaySMS",
99    `SMS input registered for channel ${channel._id}. ` +
100    `Webhook: POST /api/v1/gateway/sms/${channel._id}`,
101  );
102}
103
104async function unregisterInput(channel, secrets) {
105  log.verbose("GatewaySMS", `SMS input unregistered for channel ${channel._id}`);
106}
107
108// Exported for routes.js reply sending
109export { sendSms, getTwilioCreds };
110
111export default {
112  allowedDirections: ["input", "output", "input-output"],
113  validateConfig,
114  buildEncryptedConfig,
115  send,
116  registerInput,
117  unregisterInput,
118};
119
1import log from "../../seed/log.js";
2import handler from "./handler.js";
3import { getExtension } from "../loader.js";
4
5export async function init(core) {
6  const gateway = getExtension("gateway");
7  if (!gateway?.exports?.registerChannelType) {
8    throw new Error("gateway-sms requires the gateway extension to be loaded first");
9  }
10
11  gateway.exports.registerChannelType("sms", handler);
12  log.verbose("GatewaySMS", "Registered SMS channel type");
13
14  const { default: router } = await import("./routes.js");
15
16  return { router };
17}
18
1export default {
2  name: "gateway-sms",
3  version: "1.0.1",
4  builtFor: "treeos-connect",
5  description:
6    "SMS channel type for the gateway extension. Registers the sms channel type at boot, enabling " +
7    "trees to send and receive text messages through Twilio. No Twilio SDK required. The handler " +
8    "calls the Twilio REST API directly using basic auth, sending form-encoded POST requests to " +
9    "the Messages endpoint. Outbound messages are composed from notification title and content, " +
10    "trimmed to 1500 characters to stay within Twilio's segmentation limits.\n\n" +
11    "Three directions are supported: input (receive SMS into a tree), output (send SMS from a " +
12    "tree), and input-output (bidirectional conversation). For output channels, a toNumber in " +
13    "E.164 format is required. Twilio credentials (account SID, auth token, from number) can " +
14    "be set globally via environment variables or overridden per channel through encrypted config. " +
15    "Per-channel secrets are stored using the gateway core's AES encryption, so one land can run " +
16    "multiple Twilio accounts across different trees.\n\n" +
17    "Inbound SMS arrives via a Twilio webhook at POST /api/v1/gateway/sms/:channelId. The endpoint " +
18    "responds immediately with empty TwiML to prevent Twilio retries, then processes the message " +
19    "asynchronously. Twilio sends form-encoded data including From, Body, and MessageSid. The " +
20    "handler validates the channel exists and is enabled, extracts the sender phone number and " +
21    "message text, then delegates to the gateway core's processGatewayMessage function which runs " +
22    "the message through the tree's AI in the configured mode. For input-output channels, the AI " +
23    "reply is sent back as an SMS to the sender's phone number. Trees in your pocket without an app.",
24
25  needs: {
26    extensions: ["gateway"],
27  },
28
29  optional: {},
30
31  provides: {
32    models: {},
33    routes: false,
34    tools: false,
35    jobs: false,
36    orchestrator: false,
37    energyActions: {},
38    sessionTypes: {},
39    env: [
40      { key: "TWILIO_ACCOUNT_SID", required: false, description: "Twilio Account SID" },
41      { key: "TWILIO_AUTH_TOKEN", required: false, secret: true, description: "Twilio Auth Token" },
42      { key: "TWILIO_FROM_NUMBER", required: false, description: "Twilio phone number to send from (e.g., +15551234567)" },
43    ],
44    cli: [],
45  },
46};
47
1// Twilio inbound SMS webhook receiver.
2// No auth middleware. Twilio calls this directly.
3// Twilio sends form-encoded POST: From, To, Body, MessageSid, etc.
4// Webhook URL: POST /api/v1/gateway/sms/:channelId
5// Configure in Twilio console: Messaging > Phone Number > Webhook URL
6
7import log from "../../seed/log.js";
8import { sendOk } from "../../seed/protocol.js";
9import express from "express";
10
11const router = express.Router();
12
13router.use("/gateway/sms/:channelId", express.urlencoded({ extended: true, limit: "64kb" }));
14
15router.post("/gateway/sms/:channelId", async (req, res) => {
16  // Respond with empty TwiML (Twilio expects XML, but empty 200 works too)
17  res.type("text/xml").send("<Response></Response>");
18
19  try {
20    const channelId = req.params.channelId;
21    const body = req.body;
22
23    // Twilio sends: From, To, Body, MessageSid, AccountSid, NumMedia, etc.
24    if (!body || !body.Body || !body.From) return;
25
26    const { getExtension } = await import("../loader.js");
27    const GatewayChannel = getExtension("gateway")?.exports?.GatewayChannel;
28    const channel = await GatewayChannel.findById(channelId).lean();
29    if (!channel || !channel.enabled) return;
30    if (channel.type !== "sms") return;
31
32    const hasInput = channel.direction === "input" || channel.direction === "input-output";
33    if (!hasInput) return;
34
35    // Twilio request validation (optional but recommended)
36    // If TWILIO_AUTH_TOKEN is set, verify the X-Twilio-Signature header
37    // For now, channel-level verification via the unique webhook URL is sufficient.
38
39    const senderName = body.From; // phone number
40    const senderPlatformId = body.From;
41    const messageText = body.Body.trim();
42
43    if (!messageText) return;
44
45    log.verbose("GatewaySMS",
46      `SMS on channel ${channelId} from ${senderName}: "${messageText.slice(0, 80)}"`,
47    );
48
49    // Process via gateway core
50    const gateway = getExtension("gateway");
51    if (!gateway?.exports?.processGatewayMessage) {
52      log.error("GatewaySMS", "Gateway core not loaded");
53      return;
54    }
55
56    const result = await gateway.exports.processGatewayMessage(channelId, {
57      senderName,
58      senderPlatformId,
59      messageText,
60    });
61
62    // Reply via SMS if input-output and there's a reply
63    if (result.reply && channel.direction === "input-output") {
64      const { sendSms, getTwilioCreds } = await import("./handler.js");
65      const fullChannel = await gateway.exports.getChannelWithSecrets(channel._id);
66      const secrets = fullChannel?.config?.decryptedSecrets || {};
67      const creds = getTwilioCreds(secrets);
68
69      const reply = result.reply.length > 1500
70        ? result.reply.slice(0, 1497) + "..."
71        : result.reply;
72
73      await sendSms(creds, body.From, reply);
74    }
75  } catch (err) {
76    log.error("GatewaySMS",
77      `SMS webhook error for channel ${req.params.channelId}:`,
78      err.message,
79    );
80  }
81});
82
83export default router;
84

Versions

Version Published Downloads
1.0.1 38d ago 0
1.0.0 48d ago 0
0 stars
0 flags
React from the CLI: treeos ext star gateway-sms

Comments

Loading comments...

Post comments from the CLI: treeos ext comment gateway-sms "your comment"
Max 3 comments per extension. One star and one flag per user.