@auxiora/channels 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.
Files changed (89) hide show
  1. package/LICENSE +191 -0
  2. package/dist/adapters/bluebubbles.d.ts +63 -0
  3. package/dist/adapters/bluebubbles.d.ts.map +1 -0
  4. package/dist/adapters/bluebubbles.js +197 -0
  5. package/dist/adapters/bluebubbles.js.map +1 -0
  6. package/dist/adapters/discord.d.ts +27 -0
  7. package/dist/adapters/discord.d.ts.map +1 -0
  8. package/dist/adapters/discord.js +202 -0
  9. package/dist/adapters/discord.js.map +1 -0
  10. package/dist/adapters/email.d.ts +39 -0
  11. package/dist/adapters/email.d.ts.map +1 -0
  12. package/dist/adapters/email.js +359 -0
  13. package/dist/adapters/email.js.map +1 -0
  14. package/dist/adapters/googlechat.d.ts +77 -0
  15. package/dist/adapters/googlechat.d.ts.map +1 -0
  16. package/dist/adapters/googlechat.js +232 -0
  17. package/dist/adapters/googlechat.js.map +1 -0
  18. package/dist/adapters/matrix.d.ts +37 -0
  19. package/dist/adapters/matrix.d.ts.map +1 -0
  20. package/dist/adapters/matrix.js +262 -0
  21. package/dist/adapters/matrix.js.map +1 -0
  22. package/dist/adapters/signal.d.ts +32 -0
  23. package/dist/adapters/signal.d.ts.map +1 -0
  24. package/dist/adapters/signal.js +216 -0
  25. package/dist/adapters/signal.js.map +1 -0
  26. package/dist/adapters/slack.d.ts +29 -0
  27. package/dist/adapters/slack.d.ts.map +1 -0
  28. package/dist/adapters/slack.js +202 -0
  29. package/dist/adapters/slack.js.map +1 -0
  30. package/dist/adapters/teams.d.ts +66 -0
  31. package/dist/adapters/teams.d.ts.map +1 -0
  32. package/dist/adapters/teams.js +227 -0
  33. package/dist/adapters/teams.js.map +1 -0
  34. package/dist/adapters/telegram.d.ts +28 -0
  35. package/dist/adapters/telegram.d.ts.map +1 -0
  36. package/dist/adapters/telegram.js +170 -0
  37. package/dist/adapters/telegram.js.map +1 -0
  38. package/dist/adapters/twilio.d.ts +63 -0
  39. package/dist/adapters/twilio.d.ts.map +1 -0
  40. package/dist/adapters/twilio.js +193 -0
  41. package/dist/adapters/twilio.js.map +1 -0
  42. package/dist/adapters/whatsapp.d.ts +99 -0
  43. package/dist/adapters/whatsapp.d.ts.map +1 -0
  44. package/dist/adapters/whatsapp.js +218 -0
  45. package/dist/adapters/whatsapp.js.map +1 -0
  46. package/dist/adapters/zalo.d.ts +64 -0
  47. package/dist/adapters/zalo.d.ts.map +1 -0
  48. package/dist/adapters/zalo.js +216 -0
  49. package/dist/adapters/zalo.js.map +1 -0
  50. package/dist/index.d.ts +15 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +16 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/manager.d.ts +35 -0
  55. package/dist/manager.d.ts.map +1 -0
  56. package/dist/manager.js +127 -0
  57. package/dist/manager.js.map +1 -0
  58. package/dist/types.d.ts +71 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +2 -0
  61. package/dist/types.js.map +1 -0
  62. package/package.json +32 -0
  63. package/src/adapters/bluebubbles.ts +294 -0
  64. package/src/adapters/discord.ts +253 -0
  65. package/src/adapters/email.ts +457 -0
  66. package/src/adapters/googlechat.ts +364 -0
  67. package/src/adapters/matrix.ts +376 -0
  68. package/src/adapters/signal.ts +313 -0
  69. package/src/adapters/slack.ts +252 -0
  70. package/src/adapters/teams.ts +320 -0
  71. package/src/adapters/telegram.ts +208 -0
  72. package/src/adapters/twilio.ts +256 -0
  73. package/src/adapters/whatsapp.ts +342 -0
  74. package/src/adapters/zalo.ts +319 -0
  75. package/src/index.ts +78 -0
  76. package/src/manager.ts +180 -0
  77. package/src/types.ts +84 -0
  78. package/tests/bluebubbles.test.ts +438 -0
  79. package/tests/email.test.ts +136 -0
  80. package/tests/googlechat.test.ts +439 -0
  81. package/tests/matrix.test.ts +564 -0
  82. package/tests/signal.test.ts +404 -0
  83. package/tests/slack.test.ts +343 -0
  84. package/tests/teams.test.ts +429 -0
  85. package/tests/twilio.test.ts +269 -0
  86. package/tests/whatsapp.test.ts +530 -0
  87. package/tests/zalo.test.ts +499 -0
  88. package/tsconfig.json +8 -0
  89. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,499 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { ZaloAdapter } from '../src/adapters/zalo.js';
3
+
4
+ // Mock audit
5
+ vi.mock('@auxiora/audit', () => ({
6
+ audit: vi.fn(),
7
+ }));
8
+
9
+ describe('ZaloAdapter', () => {
10
+ let adapter: ZaloAdapter;
11
+
12
+ beforeEach(() => {
13
+ adapter = new ZaloAdapter({
14
+ oaAccessToken: 'test-access-token',
15
+ oaSecretKey: 'test-secret-key',
16
+ });
17
+ });
18
+
19
+ afterEach(() => {
20
+ vi.restoreAllMocks();
21
+ });
22
+
23
+ it('should have correct metadata', () => {
24
+ expect(adapter.type).toBe('zalo');
25
+ expect(adapter.name).toBe('Zalo');
26
+ expect(adapter.isConnected()).toBe(false);
27
+ });
28
+
29
+ it('should connect successfully', async () => {
30
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
31
+ ok: true,
32
+ json: async () => ({ error: 0, message: 'Success', data: {} }),
33
+ } as Response);
34
+
35
+ await adapter.connect();
36
+ expect(adapter.isConnected()).toBe(true);
37
+
38
+ await adapter.disconnect();
39
+ expect(adapter.isConnected()).toBe(false);
40
+ });
41
+
42
+ it('should fail to connect with invalid credentials', async () => {
43
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
44
+ ok: false,
45
+ status: 401,
46
+ statusText: 'Unauthorized',
47
+ } as Response);
48
+
49
+ await expect(adapter.connect()).rejects.toThrow('Failed to verify Zalo credentials');
50
+ });
51
+
52
+ it('should fail to connect on API error code', async () => {
53
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
54
+ ok: true,
55
+ json: async () => ({ error: -216, message: 'Invalid access token' }),
56
+ } as Response);
57
+
58
+ await expect(adapter.connect()).rejects.toThrow('Zalo API error: Invalid access token');
59
+ });
60
+
61
+ it('should handle incoming text message', async () => {
62
+ vi.spyOn(globalThis, 'fetch')
63
+ .mockResolvedValueOnce({
64
+ ok: true,
65
+ json: async () => ({ error: 0, message: 'Success', data: {} }),
66
+ } as Response)
67
+ // getUserName call
68
+ .mockResolvedValueOnce({
69
+ ok: true,
70
+ json: async () => ({
71
+ error: 0,
72
+ message: 'Success',
73
+ data: { display_name: 'Alice', user_id: 'user-1' },
74
+ }),
75
+ } as Response);
76
+
77
+ await adapter.connect();
78
+
79
+ const receivedMessages: unknown[] = [];
80
+ adapter.onMessage(async (msg) => {
81
+ receivedMessages.push(msg);
82
+ });
83
+
84
+ await adapter.handleWebhook({
85
+ app_id: 'app-1',
86
+ sender: { id: 'user-1' },
87
+ recipient: { id: 'oa-1' },
88
+ event_name: 'user_send_text',
89
+ message: {
90
+ msg_id: 'msg-1',
91
+ text: 'Hello Zalo!',
92
+ },
93
+ timestamp: '1700000000000',
94
+ });
95
+
96
+ expect(receivedMessages).toHaveLength(1);
97
+ const msg = receivedMessages[0] as {
98
+ content: string;
99
+ senderId: string;
100
+ senderName: string;
101
+ channelId: string;
102
+ };
103
+ expect(msg.content).toBe('Hello Zalo!');
104
+ expect(msg.senderId).toBe('user-1');
105
+ expect(msg.senderName).toBe('Alice');
106
+ expect(msg.channelId).toBe('user-1');
107
+
108
+ await adapter.disconnect();
109
+ });
110
+
111
+ it('should handle incoming image message', async () => {
112
+ vi.spyOn(globalThis, 'fetch')
113
+ .mockResolvedValueOnce({
114
+ ok: true,
115
+ json: async () => ({ error: 0, message: 'Success', data: {} }),
116
+ } as Response)
117
+ .mockResolvedValueOnce({
118
+ ok: true,
119
+ json: async () => ({
120
+ error: 0,
121
+ data: { display_name: 'Alice', user_id: 'user-1' },
122
+ }),
123
+ } as Response);
124
+
125
+ await adapter.connect();
126
+
127
+ const receivedMessages: unknown[] = [];
128
+ adapter.onMessage(async (msg) => {
129
+ receivedMessages.push(msg);
130
+ });
131
+
132
+ await adapter.handleWebhook({
133
+ app_id: 'app-1',
134
+ sender: { id: 'user-1' },
135
+ recipient: { id: 'oa-1' },
136
+ event_name: 'user_send_image',
137
+ message: {
138
+ msg_id: 'msg-img-1',
139
+ attachments: [
140
+ {
141
+ type: 'image',
142
+ payload: {
143
+ url: 'https://example.com/photo.jpg',
144
+ thumbnail: 'https://example.com/photo_thumb.jpg',
145
+ },
146
+ },
147
+ ],
148
+ },
149
+ timestamp: '1700000000000',
150
+ });
151
+
152
+ expect(receivedMessages).toHaveLength(1);
153
+ const msg = receivedMessages[0] as {
154
+ content: string;
155
+ attachments: Array<{ type: string; url: string }>;
156
+ };
157
+ expect(msg.content).toBe('');
158
+ expect(msg.attachments).toHaveLength(1);
159
+ expect(msg.attachments[0].type).toBe('image');
160
+ expect(msg.attachments[0].url).toBe('https://example.com/photo.jpg');
161
+
162
+ await adapter.disconnect();
163
+ });
164
+
165
+ it('should ignore unsupported event types', async () => {
166
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
167
+ ok: true,
168
+ json: async () => ({ error: 0, message: 'Success', data: {} }),
169
+ } as Response);
170
+ await adapter.connect();
171
+
172
+ const receivedMessages: unknown[] = [];
173
+ adapter.onMessage(async (msg) => {
174
+ receivedMessages.push(msg);
175
+ });
176
+
177
+ await adapter.handleWebhook({
178
+ app_id: 'app-1',
179
+ sender: { id: 'user-1' },
180
+ recipient: { id: 'oa-1' },
181
+ event_name: 'user_seen_message',
182
+ timestamp: '1700000000000',
183
+ });
184
+
185
+ expect(receivedMessages).toHaveLength(0);
186
+
187
+ await adapter.disconnect();
188
+ });
189
+
190
+ it('should handle reply context', async () => {
191
+ vi.spyOn(globalThis, 'fetch')
192
+ .mockResolvedValueOnce({
193
+ ok: true,
194
+ json: async () => ({ error: 0, message: 'Success', data: {} }),
195
+ } as Response)
196
+ .mockResolvedValueOnce({
197
+ ok: true,
198
+ json: async () => ({ error: 0, data: { display_name: 'Bob', user_id: 'user-2' } }),
199
+ } as Response);
200
+
201
+ await adapter.connect();
202
+
203
+ const receivedMessages: unknown[] = [];
204
+ adapter.onMessage(async (msg) => {
205
+ receivedMessages.push(msg);
206
+ });
207
+
208
+ await adapter.handleWebhook({
209
+ app_id: 'app-1',
210
+ sender: { id: 'user-2' },
211
+ recipient: { id: 'oa-1' },
212
+ event_name: 'user_send_text',
213
+ message: {
214
+ msg_id: 'msg-reply',
215
+ text: 'This is a reply',
216
+ quote_msg_id: 'msg-original',
217
+ },
218
+ timestamp: '1700000000000',
219
+ });
220
+
221
+ expect(receivedMessages).toHaveLength(1);
222
+ const msg = receivedMessages[0] as { replyToId: string };
223
+ expect(msg.replyToId).toBe('msg-original');
224
+
225
+ await adapter.disconnect();
226
+ });
227
+
228
+ it('should send a message successfully', async () => {
229
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
230
+ ok: true,
231
+ json: async () => ({ error: 0, message: 'Success', data: {} }),
232
+ } as Response);
233
+ await adapter.connect();
234
+
235
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
236
+ ok: true,
237
+ json: async () => ({
238
+ error: 0,
239
+ message: 'Success',
240
+ data: { message_id: 'msg-sent-1' },
241
+ }),
242
+ } as Response);
243
+
244
+ const result = await adapter.send('user-1', {
245
+ content: 'Hello from bot!',
246
+ });
247
+
248
+ expect(result.success).toBe(true);
249
+ expect(result.messageId).toBe('msg-sent-1');
250
+
251
+ await adapter.disconnect();
252
+ });
253
+
254
+ it('should handle send errors from HTTP', async () => {
255
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
256
+ ok: true,
257
+ json: async () => ({ error: 0, message: 'Success', data: {} }),
258
+ } as Response);
259
+ await adapter.connect();
260
+
261
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
262
+ ok: false,
263
+ status: 500,
264
+ statusText: 'Internal Server Error',
265
+ text: async () => 'Server error',
266
+ } as unknown as Response);
267
+
268
+ const result = await adapter.send('user-1', {
269
+ content: 'Should fail',
270
+ });
271
+
272
+ expect(result.success).toBe(false);
273
+ expect(result.error).toContain('500');
274
+
275
+ await adapter.disconnect();
276
+ });
277
+
278
+ it('should handle send errors from API error code', async () => {
279
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
280
+ ok: true,
281
+ json: async () => ({ error: 0, message: 'Success', data: {} }),
282
+ } as Response);
283
+ await adapter.connect();
284
+
285
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
286
+ ok: true,
287
+ json: async () => ({
288
+ error: -201,
289
+ message: 'User not found',
290
+ }),
291
+ } as Response);
292
+
293
+ const result = await adapter.send('invalid-user', {
294
+ content: 'Should fail',
295
+ });
296
+
297
+ expect(result.success).toBe(false);
298
+ expect(result.error).toContain('User not found');
299
+
300
+ await adapter.disconnect();
301
+ });
302
+
303
+ it('should send a reply with quote_message_id', async () => {
304
+ const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
305
+ ok: true,
306
+ json: async () => ({ error: 0, message: 'Success', data: {} }),
307
+ } as Response);
308
+ await adapter.connect();
309
+
310
+ fetchSpy.mockResolvedValueOnce({
311
+ ok: true,
312
+ json: async () => ({
313
+ error: 0,
314
+ message: 'Success',
315
+ data: { message_id: 'msg-reply-sent' },
316
+ }),
317
+ } as Response);
318
+
319
+ await adapter.send('user-1', {
320
+ content: 'Reply',
321
+ replyToId: 'msg-original',
322
+ });
323
+
324
+ const sendCall = fetchSpy.mock.calls.find(
325
+ (call) =>
326
+ typeof call[0] === 'string' &&
327
+ call[0].includes('/message/cs'),
328
+ );
329
+ expect(sendCall).toBeDefined();
330
+ const body = JSON.parse(sendCall![1]?.body as string);
331
+ expect(body.quote_message_id).toBe('msg-original');
332
+
333
+ await adapter.disconnect();
334
+ });
335
+
336
+ it('should verify webhook signature', () => {
337
+ // The HMAC is deterministic based on key + body
338
+ const body = '{"test":"data"}';
339
+ const signature = adapter.verifyWebhookSignature(body, 'wrong-sig');
340
+ expect(signature).toBe(false);
341
+ });
342
+
343
+ it('should register error handler', () => {
344
+ const handler = vi.fn();
345
+ adapter.onError(handler);
346
+ });
347
+
348
+ describe('sender filtering', () => {
349
+ it('should allow all messages when allowedUserIds is not set', async () => {
350
+ vi.spyOn(globalThis, 'fetch')
351
+ .mockResolvedValueOnce({
352
+ ok: true,
353
+ json: async () => ({ error: 0, message: 'Success', data: {} }),
354
+ } as Response)
355
+ .mockResolvedValueOnce({
356
+ ok: true,
357
+ json: async () => ({ error: 0, data: { display_name: 'Anyone', user_id: 'any' } }),
358
+ } as Response);
359
+
360
+ await adapter.connect();
361
+
362
+ const receivedMessages: unknown[] = [];
363
+ adapter.onMessage(async (msg) => {
364
+ receivedMessages.push(msg);
365
+ });
366
+
367
+ await adapter.handleWebhook({
368
+ app_id: 'app-1',
369
+ sender: { id: 'any-user' },
370
+ recipient: { id: 'oa-1' },
371
+ event_name: 'user_send_text',
372
+ message: { msg_id: 'msg-filter-1', text: 'Hello!' },
373
+ timestamp: '1700000000000',
374
+ });
375
+
376
+ expect(receivedMessages).toHaveLength(1);
377
+ await adapter.disconnect();
378
+ });
379
+
380
+ it('should block messages from non-allowed users', async () => {
381
+ const { audit } = await import('@auxiora/audit');
382
+ const filteredAdapter = new ZaloAdapter({
383
+ oaAccessToken: 'test-access-token',
384
+ oaSecretKey: 'test-secret-key',
385
+ allowedUserIds: ['user-1'],
386
+ });
387
+
388
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
389
+ ok: true,
390
+ json: async () => ({ error: 0, message: 'Success', data: {} }),
391
+ } as Response);
392
+ await filteredAdapter.connect();
393
+
394
+ const receivedMessages: unknown[] = [];
395
+ filteredAdapter.onMessage(async (msg) => {
396
+ receivedMessages.push(msg);
397
+ });
398
+
399
+ await filteredAdapter.handleWebhook({
400
+ app_id: 'app-1',
401
+ sender: { id: 'user-2' },
402
+ recipient: { id: 'oa-1' },
403
+ event_name: 'user_send_text',
404
+ message: { msg_id: 'msg-filter-blocked', text: 'Blocked!' },
405
+ timestamp: '1700000000000',
406
+ });
407
+
408
+ expect(receivedMessages).toHaveLength(0);
409
+ expect(audit).toHaveBeenCalledWith(
410
+ 'message.filtered',
411
+ expect.objectContaining({
412
+ channelType: 'zalo',
413
+ senderId: 'user-2',
414
+ reason: 'user_not_allowed',
415
+ }),
416
+ );
417
+ await filteredAdapter.disconnect();
418
+ });
419
+ });
420
+
421
+ it('should handle message handler errors gracefully', async () => {
422
+ vi.spyOn(globalThis, 'fetch')
423
+ .mockResolvedValueOnce({
424
+ ok: true,
425
+ json: async () => ({ error: 0, message: 'Success', data: {} }),
426
+ } as Response)
427
+ .mockResolvedValueOnce({
428
+ ok: true,
429
+ json: async () => ({ error: 0, data: { display_name: 'Alice', user_id: 'user-1' } }),
430
+ } as Response);
431
+
432
+ await adapter.connect();
433
+
434
+ const errorHandler = vi.fn();
435
+ adapter.onError(errorHandler);
436
+ adapter.onMessage(async () => {
437
+ throw new Error('Handler error');
438
+ });
439
+
440
+ await adapter.handleWebhook({
441
+ app_id: 'app-1',
442
+ sender: { id: 'user-1' },
443
+ recipient: { id: 'oa-1' },
444
+ event_name: 'user_send_text',
445
+ message: { msg_id: 'msg-err', text: 'Trigger error' },
446
+ timestamp: '1700000000000',
447
+ });
448
+
449
+ expect(errorHandler).toHaveBeenCalledWith(expect.any(Error));
450
+
451
+ await adapter.disconnect();
452
+ });
453
+
454
+ it('should cache user names', async () => {
455
+ const fetchSpy = vi.spyOn(globalThis, 'fetch')
456
+ .mockResolvedValueOnce({
457
+ ok: true,
458
+ json: async () => ({ error: 0, message: 'Success', data: {} }),
459
+ } as Response)
460
+ // First getUserName call
461
+ .mockResolvedValueOnce({
462
+ ok: true,
463
+ json: async () => ({ error: 0, data: { display_name: 'Alice', user_id: 'user-1' } }),
464
+ } as Response);
465
+
466
+ await adapter.connect();
467
+
468
+ const receivedMessages: unknown[] = [];
469
+ adapter.onMessage(async (msg) => {
470
+ receivedMessages.push(msg);
471
+ });
472
+
473
+ // First message from user-1
474
+ await adapter.handleWebhook({
475
+ app_id: 'app-1',
476
+ sender: { id: 'user-1' },
477
+ recipient: { id: 'oa-1' },
478
+ event_name: 'user_send_text',
479
+ message: { msg_id: 'msg-cache-1', text: 'Hello 1' },
480
+ timestamp: '1700000000000',
481
+ });
482
+
483
+ // Second message from user-1 - should use cache
484
+ await adapter.handleWebhook({
485
+ app_id: 'app-1',
486
+ sender: { id: 'user-1' },
487
+ recipient: { id: 'oa-1' },
488
+ event_name: 'user_send_text',
489
+ message: { msg_id: 'msg-cache-2', text: 'Hello 2' },
490
+ timestamp: '1700000001000',
491
+ });
492
+
493
+ expect(receivedMessages).toHaveLength(2);
494
+ // 1 connect + 1 getUserName = 2 total fetch calls (second message uses cache)
495
+ expect(fetchSpy).toHaveBeenCalledTimes(2);
496
+
497
+ await adapter.disconnect();
498
+ });
499
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src/**/*"]
8
+ }