@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,404 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { SignalAdapter } from '../src/adapters/signal.js';
3
+
4
+ // Mock audit
5
+ vi.mock('@auxiora/audit', () => ({
6
+ audit: vi.fn(),
7
+ }));
8
+
9
+ describe('SignalAdapter', () => {
10
+ let adapter: SignalAdapter;
11
+
12
+ beforeEach(() => {
13
+ adapter = new SignalAdapter({
14
+ signalCliEndpoint: 'http://localhost:7583',
15
+ phoneNumber: '+1234567890',
16
+ });
17
+ });
18
+
19
+ afterEach(() => {
20
+ vi.restoreAllMocks();
21
+ });
22
+
23
+ it('should have correct metadata', () => {
24
+ expect(adapter.type).toBe('signal');
25
+ expect(adapter.name).toBe('Signal');
26
+ expect(adapter.isConnected()).toBe(false);
27
+ });
28
+
29
+ it('should connect successfully', async () => {
30
+ vi.spyOn(globalThis, 'fetch')
31
+ .mockResolvedValueOnce({
32
+ ok: true,
33
+ json: async () => ({ jsonrpc: '2.0', id: 1, result: [] }),
34
+ } as Response)
35
+ .mockImplementation(() => new Promise(() => {})); // Hang on poll
36
+
37
+ await adapter.connect();
38
+ expect(adapter.isConnected()).toBe(true);
39
+
40
+ await adapter.disconnect();
41
+ expect(adapter.isConnected()).toBe(false);
42
+ });
43
+
44
+ it('should fail to connect on API error', async () => {
45
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
46
+ ok: false,
47
+ status: 500,
48
+ statusText: 'Internal Server Error',
49
+ } as Response);
50
+
51
+ await expect(adapter.connect()).rejects.toThrow('Signal CLI API error 500');
52
+ });
53
+
54
+ it('should fail to connect on RPC error', async () => {
55
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
56
+ ok: true,
57
+ json: async () => ({
58
+ jsonrpc: '2.0',
59
+ id: 1,
60
+ error: { code: -1, message: 'Account not found' },
61
+ }),
62
+ } as Response);
63
+
64
+ await expect(adapter.connect()).rejects.toThrow('Account not found');
65
+ });
66
+
67
+ it('should send a direct message', async () => {
68
+ // Connect
69
+ vi.spyOn(globalThis, 'fetch')
70
+ .mockResolvedValueOnce({
71
+ ok: true,
72
+ json: async () => ({ jsonrpc: '2.0', id: 1, result: [] }),
73
+ } as Response)
74
+ .mockImplementation(() => new Promise(() => {}));
75
+
76
+ await adapter.connect();
77
+
78
+ // Send
79
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
80
+ ok: true,
81
+ json: async () => ({
82
+ jsonrpc: '2.0',
83
+ id: 3,
84
+ result: { timestamp: 1700000000000 },
85
+ }),
86
+ } as Response);
87
+
88
+ const result = await adapter.send('+0987654321', {
89
+ content: 'Hello from Signal!',
90
+ });
91
+
92
+ expect(result.success).toBe(true);
93
+ expect(result.messageId).toBe('1700000000000');
94
+
95
+ await adapter.disconnect();
96
+ });
97
+
98
+ it('should send a group message', async () => {
99
+ // Connect
100
+ vi.spyOn(globalThis, 'fetch')
101
+ .mockResolvedValueOnce({
102
+ ok: true,
103
+ json: async () => ({ jsonrpc: '2.0', id: 1, result: [] }),
104
+ } as Response)
105
+ .mockImplementation(() => new Promise(() => {}));
106
+
107
+ await adapter.connect();
108
+
109
+ // Send to group (non-+ prefix = group ID)
110
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
111
+ ok: true,
112
+ json: async () => ({
113
+ jsonrpc: '2.0',
114
+ id: 3,
115
+ result: { timestamp: 1700000000001 },
116
+ }),
117
+ } as Response);
118
+
119
+ const result = await adapter.send('group-abc123', {
120
+ content: 'Hello group!',
121
+ });
122
+
123
+ expect(result.success).toBe(true);
124
+
125
+ // Verify the group param was used
126
+ const lastCall = vi.mocked(globalThis.fetch).mock.calls.at(-1);
127
+ const body = JSON.parse(lastCall![1]?.body as string);
128
+ expect(body.params.groupId).toBe('group-abc123');
129
+ expect(body.params.recipient).toBeUndefined();
130
+
131
+ await adapter.disconnect();
132
+ });
133
+
134
+ it('should handle send errors', async () => {
135
+ // Connect
136
+ vi.spyOn(globalThis, 'fetch')
137
+ .mockResolvedValueOnce({
138
+ ok: true,
139
+ json: async () => ({ jsonrpc: '2.0', id: 1, result: [] }),
140
+ } as Response)
141
+ .mockImplementation(() => new Promise(() => {}));
142
+
143
+ await adapter.connect();
144
+
145
+ // Fail send
146
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce({
147
+ ok: true,
148
+ json: async () => ({
149
+ jsonrpc: '2.0',
150
+ id: 3,
151
+ error: { code: -1, message: 'Recipient not found' },
152
+ }),
153
+ } as Response);
154
+
155
+ const result = await adapter.send('+0987654321', {
156
+ content: 'Should fail',
157
+ });
158
+
159
+ expect(result.success).toBe(false);
160
+ expect(result.error).toContain('Recipient not found');
161
+
162
+ await adapter.disconnect();
163
+ });
164
+
165
+ it('should receive messages from poll', async () => {
166
+ const receivedMessages: unknown[] = [];
167
+ adapter.onMessage(async (msg) => {
168
+ receivedMessages.push(msg);
169
+ });
170
+
171
+ const pollResponse = [
172
+ {
173
+ envelope: {
174
+ source: '+0987654321',
175
+ sourceName: 'Alice',
176
+ sourceNumber: '+0987654321',
177
+ timestamp: 1700000000000,
178
+ dataMessage: {
179
+ message: 'Hello!',
180
+ timestamp: 1700000000000,
181
+ },
182
+ },
183
+ },
184
+ ];
185
+
186
+ vi.spyOn(globalThis, 'fetch')
187
+ .mockResolvedValueOnce({
188
+ ok: true,
189
+ json: async () => ({ jsonrpc: '2.0', id: 1, result: [] }),
190
+ } as Response)
191
+ .mockResolvedValueOnce({
192
+ ok: true,
193
+ json: async () => ({ jsonrpc: '2.0', id: 2, result: pollResponse }),
194
+ } as Response)
195
+ .mockImplementation(() => new Promise(() => {}));
196
+
197
+ await adapter.connect();
198
+ await new Promise((resolve) => setTimeout(resolve, 200));
199
+
200
+ expect(receivedMessages).toHaveLength(1);
201
+ const msg = receivedMessages[0] as { content: string; senderId: string };
202
+ expect(msg.content).toBe('Hello!');
203
+ expect(msg.senderId).toBe('+0987654321');
204
+
205
+ await adapter.disconnect();
206
+ });
207
+
208
+ it('should ignore own messages', async () => {
209
+ const receivedMessages: unknown[] = [];
210
+ adapter.onMessage(async (msg) => {
211
+ receivedMessages.push(msg);
212
+ });
213
+
214
+ const pollResponse = [
215
+ {
216
+ envelope: {
217
+ source: '+1234567890',
218
+ sourceNumber: '+1234567890', // Own number
219
+ timestamp: 1700000000000,
220
+ dataMessage: {
221
+ message: 'My own message',
222
+ timestamp: 1700000000000,
223
+ },
224
+ },
225
+ },
226
+ ];
227
+
228
+ vi.spyOn(globalThis, 'fetch')
229
+ .mockResolvedValueOnce({
230
+ ok: true,
231
+ json: async () => ({ jsonrpc: '2.0', id: 1, result: [] }),
232
+ } as Response)
233
+ .mockResolvedValueOnce({
234
+ ok: true,
235
+ json: async () => ({ jsonrpc: '2.0', id: 2, result: pollResponse }),
236
+ } as Response)
237
+ .mockImplementation(() => new Promise(() => {}));
238
+
239
+ await adapter.connect();
240
+ await new Promise((resolve) => setTimeout(resolve, 200));
241
+
242
+ expect(receivedMessages).toHaveLength(0);
243
+
244
+ await adapter.disconnect();
245
+ });
246
+
247
+ it('should handle group messages', async () => {
248
+ const receivedMessages: unknown[] = [];
249
+ adapter.onMessage(async (msg) => {
250
+ receivedMessages.push(msg);
251
+ });
252
+
253
+ const pollResponse = [
254
+ {
255
+ envelope: {
256
+ source: '+0987654321',
257
+ sourceName: 'Bob',
258
+ sourceNumber: '+0987654321',
259
+ timestamp: 1700000000000,
260
+ dataMessage: {
261
+ message: 'Hello group!',
262
+ timestamp: 1700000000000,
263
+ groupInfo: {
264
+ groupId: 'group-xyz',
265
+ type: 'DELIVER',
266
+ },
267
+ },
268
+ },
269
+ },
270
+ ];
271
+
272
+ vi.spyOn(globalThis, 'fetch')
273
+ .mockResolvedValueOnce({
274
+ ok: true,
275
+ json: async () => ({ jsonrpc: '2.0', id: 1, result: [] }),
276
+ } as Response)
277
+ .mockResolvedValueOnce({
278
+ ok: true,
279
+ json: async () => ({ jsonrpc: '2.0', id: 2, result: pollResponse }),
280
+ } as Response)
281
+ .mockImplementation(() => new Promise(() => {}));
282
+
283
+ await adapter.connect();
284
+ await new Promise((resolve) => setTimeout(resolve, 200));
285
+
286
+ expect(receivedMessages).toHaveLength(1);
287
+ const msg = receivedMessages[0] as { channelId: string };
288
+ expect(msg.channelId).toBe('group-xyz');
289
+
290
+ await adapter.disconnect();
291
+ });
292
+
293
+ it('should register error handler', () => {
294
+ const handler = vi.fn();
295
+ adapter.onError(handler);
296
+ });
297
+
298
+ describe('sender filtering', () => {
299
+ it('should allow all messages when allowedNumbers is not set', async () => {
300
+ const receivedMessages: unknown[] = [];
301
+ adapter.onMessage(async (msg) => {
302
+ receivedMessages.push(msg);
303
+ });
304
+
305
+ const pollResponse = [
306
+ {
307
+ envelope: {
308
+ source: '+0987654321',
309
+ sourceName: 'Alice',
310
+ sourceNumber: '+0987654321',
311
+ timestamp: 1700000000000,
312
+ dataMessage: { message: 'Hello!', timestamp: 1700000000000 },
313
+ },
314
+ },
315
+ ];
316
+
317
+ vi.spyOn(globalThis, 'fetch')
318
+ .mockResolvedValueOnce({ ok: true, json: async () => ({ jsonrpc: '2.0', id: 1, result: [] }) } as Response)
319
+ .mockResolvedValueOnce({ ok: true, json: async () => ({ jsonrpc: '2.0', id: 2, result: pollResponse }) } as Response)
320
+ .mockImplementation(() => new Promise(() => {}));
321
+
322
+ await adapter.connect();
323
+ await new Promise((resolve) => setTimeout(resolve, 200));
324
+ expect(receivedMessages).toHaveLength(1);
325
+ await adapter.disconnect();
326
+ });
327
+
328
+ it('should allow messages from allowed numbers', async () => {
329
+ const filteredAdapter = new SignalAdapter({
330
+ signalCliEndpoint: 'http://localhost:7583',
331
+ phoneNumber: '+1234567890',
332
+ allowedNumbers: ['+0987654321'],
333
+ });
334
+
335
+ const receivedMessages: unknown[] = [];
336
+ filteredAdapter.onMessage(async (msg) => {
337
+ receivedMessages.push(msg);
338
+ });
339
+
340
+ const pollResponse = [
341
+ {
342
+ envelope: {
343
+ source: '+0987654321',
344
+ sourceName: 'Alice',
345
+ sourceNumber: '+0987654321',
346
+ timestamp: 1700000000000,
347
+ dataMessage: { message: 'Allowed!', timestamp: 1700000000000 },
348
+ },
349
+ },
350
+ ];
351
+
352
+ vi.spyOn(globalThis, 'fetch')
353
+ .mockResolvedValueOnce({ ok: true, json: async () => ({ jsonrpc: '2.0', id: 1, result: [] }) } as Response)
354
+ .mockResolvedValueOnce({ ok: true, json: async () => ({ jsonrpc: '2.0', id: 2, result: pollResponse }) } as Response)
355
+ .mockImplementation(() => new Promise(() => {}));
356
+
357
+ await filteredAdapter.connect();
358
+ await new Promise((resolve) => setTimeout(resolve, 200));
359
+ expect(receivedMessages).toHaveLength(1);
360
+ await filteredAdapter.disconnect();
361
+ });
362
+
363
+ it('should block messages from non-allowed numbers', async () => {
364
+ const { audit } = await import('@auxiora/audit');
365
+ const filteredAdapter = new SignalAdapter({
366
+ signalCliEndpoint: 'http://localhost:7583',
367
+ phoneNumber: '+1234567890',
368
+ allowedNumbers: ['+0987654321'],
369
+ });
370
+
371
+ const receivedMessages: unknown[] = [];
372
+ filteredAdapter.onMessage(async (msg) => {
373
+ receivedMessages.push(msg);
374
+ });
375
+
376
+ const pollResponse = [
377
+ {
378
+ envelope: {
379
+ source: '+5555555555',
380
+ sourceName: 'Eve',
381
+ sourceNumber: '+5555555555',
382
+ timestamp: 1700000000000,
383
+ dataMessage: { message: 'Blocked!', timestamp: 1700000000000 },
384
+ },
385
+ },
386
+ ];
387
+
388
+ vi.spyOn(globalThis, 'fetch')
389
+ .mockResolvedValueOnce({ ok: true, json: async () => ({ jsonrpc: '2.0', id: 1, result: [] }) } as Response)
390
+ .mockResolvedValueOnce({ ok: true, json: async () => ({ jsonrpc: '2.0', id: 2, result: pollResponse }) } as Response)
391
+ .mockImplementation(() => new Promise(() => {}));
392
+
393
+ await filteredAdapter.connect();
394
+ await new Promise((resolve) => setTimeout(resolve, 200));
395
+ expect(receivedMessages).toHaveLength(0);
396
+ expect(audit).toHaveBeenCalledWith('message.filtered', expect.objectContaining({
397
+ channelType: 'signal',
398
+ senderId: '+5555555555',
399
+ reason: 'number_not_allowed',
400
+ }));
401
+ await filteredAdapter.disconnect();
402
+ });
403
+ });
404
+ });