@xenterprises/fastify-xtwilio 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +63 -0
- package/.env.example +257 -0
- package/API.md +973 -0
- package/CHANGELOG.md +189 -0
- package/LICENSE +15 -0
- package/README.md +261 -0
- package/SECURITY.md +721 -0
- package/index.d.ts +999 -0
- package/package.json +55 -0
- package/server/app.js +88 -0
- package/src/services/conversations.js +328 -0
- package/src/services/email.js +362 -0
- package/src/services/rcs.js +284 -0
- package/src/services/sms.js +268 -0
- package/src/xTwilio.js +37 -0
- package/test/xTwilio.test.js +511 -0
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xenterprises/fastify-xtwilio",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Fastify plugin for Twilio communications (SMS, Conversations, RCS) and SendGrid email.",
|
|
6
|
+
"main": "src/xTwilio.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/xTwilio.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "fastify start -l info server/app.js",
|
|
12
|
+
"dev": "fastify start -w -l info -P server/app.js",
|
|
13
|
+
"test": "node --test test/xTwilio.test.js"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20.0.0",
|
|
17
|
+
"npm": ">=10.0.0"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"fastify",
|
|
21
|
+
"twilio",
|
|
22
|
+
"sms",
|
|
23
|
+
"rcs",
|
|
24
|
+
"conversations",
|
|
25
|
+
"sendgrid",
|
|
26
|
+
"email",
|
|
27
|
+
"communications",
|
|
28
|
+
"messaging",
|
|
29
|
+
"plugin"
|
|
30
|
+
],
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/xenterprises/fastify-xtwilio.git"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/xenterprises/fastify-xtwilio/issues"
|
|
37
|
+
},
|
|
38
|
+
"author": "Tim Mushen",
|
|
39
|
+
"license": "ISC",
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^22.7.4",
|
|
42
|
+
"fastify": "^5.1.0",
|
|
43
|
+
"fastify-plugin": "^5.0.0",
|
|
44
|
+
"typescript": "^5.6.3"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@sendgrid/client": "^8.1.3",
|
|
48
|
+
"@sendgrid/mail": "^8.1.3",
|
|
49
|
+
"fastify-plugin": "^5.0.0",
|
|
50
|
+
"twilio": "^5.3.2"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"fastify": "^5.0.0"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/server/app.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// server/app.js - Example Fastify server using xTwilio
|
|
2
|
+
import Fastify from 'fastify';
|
|
3
|
+
import xTwilio from '../src/xTwilio.js';
|
|
4
|
+
|
|
5
|
+
const fastify = Fastify({
|
|
6
|
+
logger: true,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// Register xTwilio plugin
|
|
10
|
+
await fastify.register(xTwilio, {
|
|
11
|
+
twilio: {
|
|
12
|
+
accountSid: process.env.TWILIO_ACCOUNT_SID,
|
|
13
|
+
authToken: process.env.TWILIO_AUTH_TOKEN,
|
|
14
|
+
phoneNumber: process.env.TWILIO_PHONE_NUMBER,
|
|
15
|
+
messagingServiceSid: process.env.TWILIO_MESSAGING_SERVICE_SID,
|
|
16
|
+
},
|
|
17
|
+
sendgrid: {
|
|
18
|
+
apiKey: process.env.SENDGRID_API_KEY,
|
|
19
|
+
fromEmail: process.env.SENDGRID_FROM_EMAIL,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Example SMS route
|
|
24
|
+
fastify.post('/sms/send', async (request, reply) => {
|
|
25
|
+
const { to, body } = request.body;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const result = await fastify.sms.send(to, body);
|
|
29
|
+
return { success: true, messageSid: result.sid };
|
|
30
|
+
} catch (error) {
|
|
31
|
+
reply.code(500);
|
|
32
|
+
return { success: false, error: error.message };
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Example Conversation route
|
|
37
|
+
fastify.post('/conversations/create', async (request, reply) => {
|
|
38
|
+
const { friendlyName } = request.body;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const conversation = await fastify.conversations.create(friendlyName);
|
|
42
|
+
return { success: true, conversationSid: conversation.sid };
|
|
43
|
+
} catch (error) {
|
|
44
|
+
reply.code(500);
|
|
45
|
+
return { success: false, error: error.message };
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Example RCS route
|
|
50
|
+
fastify.post('/rcs/send-card', async (request, reply) => {
|
|
51
|
+
const { to, card } = request.body;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const result = await fastify.rcs.sendRichCard(to, card);
|
|
55
|
+
return { success: true, messageSid: result.sid };
|
|
56
|
+
} catch (error) {
|
|
57
|
+
reply.code(500);
|
|
58
|
+
return { success: false, error: error.message };
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Example Email route
|
|
63
|
+
fastify.post('/email/send', async (request, reply) => {
|
|
64
|
+
const { to, subject, html } = request.body;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const result = await fastify.email.send(to, subject, html);
|
|
68
|
+
return { success: true, messageId: result.messageId };
|
|
69
|
+
} catch (error) {
|
|
70
|
+
reply.code(500);
|
|
71
|
+
return { success: false, error: error.message };
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Health check route
|
|
76
|
+
fastify.get('/health', async () => {
|
|
77
|
+
return {
|
|
78
|
+
status: 'ok',
|
|
79
|
+
services: {
|
|
80
|
+
sms: !!fastify.sms,
|
|
81
|
+
conversations: !!fastify.conversations,
|
|
82
|
+
rcs: !!fastify.rcs,
|
|
83
|
+
email: !!fastify.email,
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export default fastify;
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
// src/services/conversations.js
|
|
2
|
+
import Twilio from "twilio";
|
|
3
|
+
|
|
4
|
+
export async function setupConversations(fastify, options) {
|
|
5
|
+
if (options.active === false) return;
|
|
6
|
+
|
|
7
|
+
// Validate required credentials
|
|
8
|
+
if (!options.accountSid || !options.authToken) {
|
|
9
|
+
throw new Error("Twilio accountSid and authToken must be provided for Conversations.");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Initialize Twilio client
|
|
13
|
+
const twilioClient = Twilio(options.accountSid, options.authToken);
|
|
14
|
+
|
|
15
|
+
fastify.decorate("conversations", {
|
|
16
|
+
/**
|
|
17
|
+
* Create a new conversation
|
|
18
|
+
* @param {string} friendlyName - Human-readable name
|
|
19
|
+
* @param {object} attributes - Optional custom attributes (JSON object)
|
|
20
|
+
* @returns {Promise<object>} Conversation object
|
|
21
|
+
*/
|
|
22
|
+
create: async (friendlyName, attributes = {}) => {
|
|
23
|
+
try {
|
|
24
|
+
const conversation = await twilioClient.conversations.v1.conversations.create({
|
|
25
|
+
friendlyName,
|
|
26
|
+
attributes: JSON.stringify(attributes),
|
|
27
|
+
});
|
|
28
|
+
return conversation;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
fastify.log.error("Conversations create failed:", error);
|
|
31
|
+
throw new Error("Failed to create conversation.");
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get a conversation by SID
|
|
37
|
+
* @param {string} conversationSid - Conversation SID
|
|
38
|
+
* @returns {Promise<object>} Conversation object
|
|
39
|
+
*/
|
|
40
|
+
get: async (conversationSid) => {
|
|
41
|
+
try {
|
|
42
|
+
const conversation = await twilioClient.conversations.v1
|
|
43
|
+
.conversations(conversationSid)
|
|
44
|
+
.fetch();
|
|
45
|
+
return conversation;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
fastify.log.error("Conversations get failed:", error);
|
|
48
|
+
throw new Error("Failed to get conversation.");
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Update a conversation
|
|
54
|
+
* @param {string} conversationSid - Conversation SID
|
|
55
|
+
* @param {object} updates - Fields to update (friendlyName, attributes, etc.)
|
|
56
|
+
* @returns {Promise<object>} Updated conversation object
|
|
57
|
+
*/
|
|
58
|
+
update: async (conversationSid, updates) => {
|
|
59
|
+
try {
|
|
60
|
+
const conversation = await twilioClient.conversations.v1
|
|
61
|
+
.conversations(conversationSid)
|
|
62
|
+
.update(updates);
|
|
63
|
+
return conversation;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
fastify.log.error("Conversations update failed:", error);
|
|
66
|
+
throw new Error("Failed to update conversation.");
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* List all conversations
|
|
72
|
+
* @param {object} filters - Filter options (limit)
|
|
73
|
+
* @returns {Promise<object[]>} List of conversations
|
|
74
|
+
*/
|
|
75
|
+
list: async (filters = {}) => {
|
|
76
|
+
try {
|
|
77
|
+
const conversations = await twilioClient.conversations.v1.conversations.list({
|
|
78
|
+
limit: filters.limit || 50,
|
|
79
|
+
});
|
|
80
|
+
return conversations;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
fastify.log.error("Conversations list failed:", error);
|
|
83
|
+
throw new Error("Failed to list conversations.");
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Delete a conversation
|
|
89
|
+
* @param {string} conversationSid - Conversation SID
|
|
90
|
+
* @returns {Promise<boolean>} Success status
|
|
91
|
+
*/
|
|
92
|
+
delete: async (conversationSid) => {
|
|
93
|
+
try {
|
|
94
|
+
await twilioClient.conversations.v1.conversations(conversationSid).remove();
|
|
95
|
+
return true;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
fastify.log.error("Conversations delete failed:", error);
|
|
98
|
+
throw new Error("Failed to delete conversation.");
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Add a participant to a conversation
|
|
104
|
+
* @param {string} conversationSid - Conversation SID
|
|
105
|
+
* @param {string} identity - Participant identity (for chat users)
|
|
106
|
+
* @param {string} messagingBindingAddress - Phone number (for SMS participants, e.g., "+1234567890")
|
|
107
|
+
* @returns {Promise<object>} Participant object
|
|
108
|
+
*/
|
|
109
|
+
addParticipant: async (conversationSid, identity = null, messagingBindingAddress = null) => {
|
|
110
|
+
try {
|
|
111
|
+
const participantData = {};
|
|
112
|
+
|
|
113
|
+
if (identity) {
|
|
114
|
+
participantData.identity = identity;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (messagingBindingAddress) {
|
|
118
|
+
participantData["messagingBinding.address"] = messagingBindingAddress;
|
|
119
|
+
participantData["messagingBinding.proxyAddress"] = options.phoneNumber || options.messagingServiceSid;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const participant = await twilioClient.conversations.v1
|
|
123
|
+
.conversations(conversationSid)
|
|
124
|
+
.participants.create(participantData);
|
|
125
|
+
|
|
126
|
+
return participant;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
fastify.log.error("Conversations addParticipant failed:", error);
|
|
129
|
+
throw new Error("Failed to add participant.");
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* List participants in a conversation
|
|
135
|
+
* @param {string} conversationSid - Conversation SID
|
|
136
|
+
* @returns {Promise<object[]>} List of participants
|
|
137
|
+
*/
|
|
138
|
+
listParticipants: async (conversationSid) => {
|
|
139
|
+
try {
|
|
140
|
+
const participants = await twilioClient.conversations.v1
|
|
141
|
+
.conversations(conversationSid)
|
|
142
|
+
.participants.list();
|
|
143
|
+
return participants;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
fastify.log.error("Conversations listParticipants failed:", error);
|
|
146
|
+
throw new Error("Failed to list participants.");
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Remove a participant from a conversation
|
|
152
|
+
* @param {string} conversationSid - Conversation SID
|
|
153
|
+
* @param {string} participantSid - Participant SID
|
|
154
|
+
* @returns {Promise<boolean>} Success status
|
|
155
|
+
*/
|
|
156
|
+
removeParticipant: async (conversationSid, participantSid) => {
|
|
157
|
+
try {
|
|
158
|
+
await twilioClient.conversations.v1
|
|
159
|
+
.conversations(conversationSid)
|
|
160
|
+
.participants(participantSid)
|
|
161
|
+
.remove();
|
|
162
|
+
return true;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
fastify.log.error("Conversations removeParticipant failed:", error);
|
|
165
|
+
throw new Error("Failed to remove participant.");
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Send a message to a conversation
|
|
171
|
+
* @param {string} conversationSid - Conversation SID
|
|
172
|
+
* @param {string} body - Message content
|
|
173
|
+
* @param {string} author - Message author (optional, identity or participant SID)
|
|
174
|
+
* @param {object} attributes - Optional custom attributes
|
|
175
|
+
* @returns {Promise<object>} Message object
|
|
176
|
+
*/
|
|
177
|
+
sendMessage: async (conversationSid, body, author = null, attributes = {}) => {
|
|
178
|
+
try {
|
|
179
|
+
const messageData = { body };
|
|
180
|
+
|
|
181
|
+
if (author) {
|
|
182
|
+
messageData.author = author;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (Object.keys(attributes).length > 0) {
|
|
186
|
+
messageData.attributes = JSON.stringify(attributes);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const message = await twilioClient.conversations.v1
|
|
190
|
+
.conversations(conversationSid)
|
|
191
|
+
.messages.create(messageData);
|
|
192
|
+
|
|
193
|
+
return message;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
fastify.log.error("Conversations sendMessage failed:", error);
|
|
196
|
+
throw new Error("Failed to send message.");
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Send a media message to a conversation
|
|
202
|
+
* @param {string} conversationSid - Conversation SID
|
|
203
|
+
* @param {string} mediaUrl - URL of the media file
|
|
204
|
+
* @param {string} body - Optional message body
|
|
205
|
+
* @param {string} author - Message author (optional)
|
|
206
|
+
* @returns {Promise<object>} Message object
|
|
207
|
+
*/
|
|
208
|
+
sendMediaMessage: async (conversationSid, mediaUrl, body = "", author = null) => {
|
|
209
|
+
try {
|
|
210
|
+
const messageData = {
|
|
211
|
+
body,
|
|
212
|
+
mediaUrl: [mediaUrl],
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
if (author) {
|
|
216
|
+
messageData.author = author;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const message = await twilioClient.conversations.v1
|
|
220
|
+
.conversations(conversationSid)
|
|
221
|
+
.messages.create(messageData);
|
|
222
|
+
|
|
223
|
+
return message;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
fastify.log.error("Conversations sendMediaMessage failed:", error);
|
|
226
|
+
throw new Error("Failed to send media message.");
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get messages from a conversation
|
|
232
|
+
* @param {string} conversationSid - Conversation SID
|
|
233
|
+
* @param {object} options - Pagination options (limit, order)
|
|
234
|
+
* @returns {Promise<object[]>} List of messages
|
|
235
|
+
*/
|
|
236
|
+
getMessages: async (conversationSid, options = {}) => {
|
|
237
|
+
try {
|
|
238
|
+
const messages = await twilioClient.conversations.v1
|
|
239
|
+
.conversations(conversationSid)
|
|
240
|
+
.messages.list({
|
|
241
|
+
limit: options.limit || 50,
|
|
242
|
+
order: options.order || "desc",
|
|
243
|
+
});
|
|
244
|
+
return messages;
|
|
245
|
+
} catch (error) {
|
|
246
|
+
fastify.log.error("Conversations getMessages failed:", error);
|
|
247
|
+
throw new Error("Failed to get messages.");
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Get a specific message
|
|
253
|
+
* @param {string} conversationSid - Conversation SID
|
|
254
|
+
* @param {string} messageSid - Message SID
|
|
255
|
+
* @returns {Promise<object>} Message object
|
|
256
|
+
*/
|
|
257
|
+
getMessage: async (conversationSid, messageSid) => {
|
|
258
|
+
try {
|
|
259
|
+
const message = await twilioClient.conversations.v1
|
|
260
|
+
.conversations(conversationSid)
|
|
261
|
+
.messages(messageSid)
|
|
262
|
+
.fetch();
|
|
263
|
+
return message;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
fastify.log.error("Conversations getMessage failed:", error);
|
|
266
|
+
throw new Error("Failed to get message.");
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Delete a message from a conversation
|
|
272
|
+
* @param {string} conversationSid - Conversation SID
|
|
273
|
+
* @param {string} messageSid - Message SID
|
|
274
|
+
* @returns {Promise<boolean>} Success status
|
|
275
|
+
*/
|
|
276
|
+
deleteMessage: async (conversationSid, messageSid) => {
|
|
277
|
+
try {
|
|
278
|
+
await twilioClient.conversations.v1
|
|
279
|
+
.conversations(conversationSid)
|
|
280
|
+
.messages(messageSid)
|
|
281
|
+
.remove();
|
|
282
|
+
return true;
|
|
283
|
+
} catch (error) {
|
|
284
|
+
fastify.log.error("Conversations deleteMessage failed:", error);
|
|
285
|
+
throw new Error("Failed to delete message.");
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Get conversation webhooks configuration
|
|
291
|
+
* @param {string} conversationSid - Conversation SID
|
|
292
|
+
* @returns {Promise<object>} Webhooks configuration
|
|
293
|
+
*/
|
|
294
|
+
getWebhooks: async (conversationSid) => {
|
|
295
|
+
try {
|
|
296
|
+
const webhooks = await twilioClient.conversations.v1
|
|
297
|
+
.conversations(conversationSid)
|
|
298
|
+
.webhooks()
|
|
299
|
+
.fetch();
|
|
300
|
+
return webhooks;
|
|
301
|
+
} catch (error) {
|
|
302
|
+
fastify.log.error("Conversations getWebhooks failed:", error);
|
|
303
|
+
throw new Error("Failed to get webhooks.");
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Update conversation webhooks
|
|
309
|
+
* @param {string} conversationSid - Conversation SID
|
|
310
|
+
* @param {object} webhookConfig - Webhook configuration
|
|
311
|
+
* @returns {Promise<object>} Updated webhooks configuration
|
|
312
|
+
*/
|
|
313
|
+
updateWebhooks: async (conversationSid, webhookConfig) => {
|
|
314
|
+
try {
|
|
315
|
+
const webhooks = await twilioClient.conversations.v1
|
|
316
|
+
.conversations(conversationSid)
|
|
317
|
+
.webhooks()
|
|
318
|
+
.update(webhookConfig);
|
|
319
|
+
return webhooks;
|
|
320
|
+
} catch (error) {
|
|
321
|
+
fastify.log.error("Conversations updateWebhooks failed:", error);
|
|
322
|
+
throw new Error("Failed to update webhooks.");
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
console.info(" ✅ Conversations Service Enabled");
|
|
328
|
+
}
|