@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,364 @@
1
+ import { audit } from '@auxiora/audit';
2
+ import type {
3
+ ChannelAdapter,
4
+ InboundMessage,
5
+ OutboundMessage,
6
+ SendResult,
7
+ } from '../types.js';
8
+
9
+ export interface GoogleChatAdapterConfig {
10
+ serviceAccountKey: string;
11
+ allowedSpaces?: string[];
12
+ }
13
+
14
+ interface GoogleChatEvent {
15
+ type: string;
16
+ eventTime: string;
17
+ message?: {
18
+ name: string;
19
+ sender: {
20
+ name: string;
21
+ displayName: string;
22
+ type: string;
23
+ };
24
+ createTime: string;
25
+ text: string;
26
+ thread?: {
27
+ name: string;
28
+ };
29
+ space: {
30
+ name: string;
31
+ type: string;
32
+ displayName?: string;
33
+ };
34
+ argumentText?: string;
35
+ attachment?: Array<{
36
+ name: string;
37
+ contentName: string;
38
+ contentType: string;
39
+ downloadUri?: string;
40
+ thumbnailUri?: string;
41
+ source: string;
42
+ }>;
43
+ };
44
+ space?: {
45
+ name: string;
46
+ type: string;
47
+ displayName?: string;
48
+ };
49
+ user?: {
50
+ name: string;
51
+ displayName: string;
52
+ type: string;
53
+ };
54
+ configCompleteRedirectUrl?: string;
55
+ }
56
+
57
+ interface GoogleChatMessageResponse {
58
+ name: string;
59
+ sender: {
60
+ name: string;
61
+ displayName: string;
62
+ };
63
+ createTime: string;
64
+ text: string;
65
+ thread: {
66
+ name: string;
67
+ };
68
+ }
69
+
70
+ interface GoogleTokenResponse {
71
+ access_token: string;
72
+ expires_in: number;
73
+ token_type: string;
74
+ }
75
+
76
+ interface ServiceAccountKey {
77
+ client_email: string;
78
+ private_key: string;
79
+ token_uri: string;
80
+ }
81
+
82
+ const CHAT_API_BASE = 'https://chat.googleapis.com/v1';
83
+ const MAX_MESSAGE_LENGTH = 4096;
84
+
85
+ export class GoogleChatAdapter implements ChannelAdapter {
86
+ readonly type = 'googlechat' as const;
87
+ readonly name = 'Google Chat';
88
+
89
+ private config: GoogleChatAdapterConfig;
90
+ private messageHandler?: (message: InboundMessage) => Promise<void>;
91
+ private errorHandler?: (error: Error) => void;
92
+ private connected = false;
93
+ private accessToken?: string;
94
+ private tokenExpiry = 0;
95
+ private serviceAccount: ServiceAccountKey;
96
+
97
+ constructor(config: GoogleChatAdapterConfig) {
98
+ this.config = config;
99
+ try {
100
+ this.serviceAccount = JSON.parse(config.serviceAccountKey) as ServiceAccountKey;
101
+ } catch {
102
+ throw new Error('Invalid Google Chat service account key: must be valid JSON');
103
+ }
104
+ }
105
+
106
+ async connect(): Promise<void> {
107
+ // Verify credentials by obtaining a token
108
+ await this.getAccessToken();
109
+ this.connected = true;
110
+
111
+ audit('channel.connected', {
112
+ channelType: 'googlechat',
113
+ serviceAccount: this.serviceAccount.client_email,
114
+ });
115
+ }
116
+
117
+ async disconnect(): Promise<void> {
118
+ this.connected = false;
119
+ this.accessToken = undefined;
120
+ this.tokenExpiry = 0;
121
+ audit('channel.disconnected', { channelType: 'googlechat' });
122
+ }
123
+
124
+ isConnected(): boolean {
125
+ return this.connected;
126
+ }
127
+
128
+ private async getAccessToken(): Promise<string> {
129
+ if (this.accessToken && Date.now() < this.tokenExpiry) {
130
+ return this.accessToken;
131
+ }
132
+
133
+ const now = Math.floor(Date.now() / 1000);
134
+ const header = btoa(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));
135
+ const payload = btoa(
136
+ JSON.stringify({
137
+ iss: this.serviceAccount.client_email,
138
+ scope: 'https://www.googleapis.com/auth/chat.bot',
139
+ aud: this.serviceAccount.token_uri,
140
+ iat: now,
141
+ exp: now + 3600,
142
+ }),
143
+ );
144
+
145
+ // Sign the JWT using the Web Crypto API
146
+ const signingInput = `${header}.${payload}`;
147
+ const key = await this.importPrivateKey(this.serviceAccount.private_key);
148
+ const signature = await crypto.subtle.sign(
149
+ 'RSASSA-PKCS1-v1_5',
150
+ key,
151
+ new TextEncoder().encode(signingInput),
152
+ );
153
+ const sig = btoa(String.fromCharCode(...new Uint8Array(signature)))
154
+ .replace(/\+/g, '-')
155
+ .replace(/\//g, '_')
156
+ .replace(/=+$/, '');
157
+
158
+ const jwt = `${header}.${payload}.${sig}`;
159
+
160
+ const params = new URLSearchParams({
161
+ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
162
+ assertion: jwt,
163
+ });
164
+
165
+ const response = await fetch(this.serviceAccount.token_uri, {
166
+ method: 'POST',
167
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
168
+ body: params.toString(),
169
+ });
170
+
171
+ if (!response.ok) {
172
+ throw new Error(
173
+ `Failed to obtain Google Chat token: ${response.status} ${response.statusText}`,
174
+ );
175
+ }
176
+
177
+ const data = (await response.json()) as GoogleTokenResponse;
178
+ this.accessToken = data.access_token;
179
+ // Expire 5 minutes early
180
+ this.tokenExpiry = Date.now() + (data.expires_in - 300) * 1000;
181
+
182
+ return this.accessToken;
183
+ }
184
+
185
+ private async importPrivateKey(pem: string): Promise<CryptoKey> {
186
+ const pemBody = pem
187
+ .replace(/-----BEGIN PRIVATE KEY-----/, '')
188
+ .replace(/-----END PRIVATE KEY-----/, '')
189
+ .replace(/\s/g, '');
190
+ const binaryDer = Uint8Array.from(atob(pemBody), (c) => c.charCodeAt(0));
191
+
192
+ return crypto.subtle.importKey(
193
+ 'pkcs8',
194
+ binaryDer.buffer,
195
+ { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
196
+ false,
197
+ ['sign'],
198
+ );
199
+ }
200
+
201
+ /**
202
+ * Handle incoming event from Google Chat.
203
+ * Call this from your HTTP server when receiving POST to the bot endpoint.
204
+ */
205
+ async handleWebhook(event: GoogleChatEvent): Promise<void> {
206
+ if (event.type !== 'MESSAGE') return;
207
+ if (!event.message) return;
208
+
209
+ // Check allowed spaces
210
+ const spaceName = event.message.space.name;
211
+ if (
212
+ this.config.allowedSpaces?.length &&
213
+ !this.config.allowedSpaces.includes(spaceName)
214
+ ) {
215
+ audit('message.filtered', {
216
+ channelType: 'googlechat',
217
+ senderId: event.message.sender.name,
218
+ channelId: spaceName,
219
+ reason: 'space_not_allowed',
220
+ });
221
+ return;
222
+ }
223
+
224
+ const inbound = this.toInboundMessage(event);
225
+
226
+ audit('message.received', {
227
+ channelType: 'googlechat',
228
+ senderId: inbound.senderId,
229
+ channelId: inbound.channelId,
230
+ });
231
+
232
+ if (this.messageHandler) {
233
+ try {
234
+ await this.messageHandler(inbound);
235
+ } catch (error) {
236
+ this.errorHandler?.(error instanceof Error ? error : new Error(String(error)));
237
+ }
238
+ }
239
+ }
240
+
241
+ private toInboundMessage(event: GoogleChatEvent): InboundMessage {
242
+ const message = event.message!;
243
+ // Use argumentText if available (strips @mention), fall back to text
244
+ const content = message.argumentText?.trim() || message.text || '';
245
+
246
+ return {
247
+ id: message.name,
248
+ channelType: 'googlechat',
249
+ channelId: message.space.name,
250
+ senderId: message.sender.name,
251
+ senderName: message.sender.displayName,
252
+ content,
253
+ timestamp: new Date(message.createTime).getTime(),
254
+ replyToId: message.thread?.name,
255
+ attachments: message.attachment?.map((a) => ({
256
+ type: a.contentType.startsWith('image/')
257
+ ? ('image' as const)
258
+ : a.contentType.startsWith('audio/')
259
+ ? ('audio' as const)
260
+ : a.contentType.startsWith('video/')
261
+ ? ('video' as const)
262
+ : ('file' as const),
263
+ url: a.downloadUri,
264
+ mimeType: a.contentType,
265
+ filename: a.contentName,
266
+ })),
267
+ raw: event,
268
+ };
269
+ }
270
+
271
+ async send(channelId: string, message: OutboundMessage): Promise<SendResult> {
272
+ try {
273
+ const token = await this.getAccessToken();
274
+ const chunks = this.chunkMessage(message.content);
275
+ let lastMessageName: string | undefined;
276
+
277
+ for (const chunk of chunks) {
278
+ const body: Record<string, unknown> = {
279
+ text: chunk,
280
+ };
281
+
282
+ if (message.replyToId) {
283
+ body.thread = { name: message.replyToId };
284
+ }
285
+
286
+ const url = `${CHAT_API_BASE}/${channelId}/messages`;
287
+ const response = await fetch(url, {
288
+ method: 'POST',
289
+ headers: {
290
+ Authorization: `Bearer ${token}`,
291
+ 'Content-Type': 'application/json',
292
+ },
293
+ body: JSON.stringify(body),
294
+ });
295
+
296
+ if (!response.ok) {
297
+ const errorText = await response.text().catch(() => response.statusText);
298
+ throw new Error(`Google Chat API error ${response.status}: ${errorText}`);
299
+ }
300
+
301
+ const result = (await response.json()) as GoogleChatMessageResponse;
302
+ lastMessageName = result.name;
303
+ }
304
+
305
+ audit('message.sent', {
306
+ channelType: 'googlechat',
307
+ channelId,
308
+ messageId: lastMessageName,
309
+ });
310
+
311
+ return { success: true, messageId: lastMessageName };
312
+ } catch (error) {
313
+ const errorMessage = error instanceof Error ? error.message : String(error);
314
+ audit('channel.error', {
315
+ channelType: 'googlechat',
316
+ action: 'send',
317
+ error: errorMessage,
318
+ });
319
+ return { success: false, error: errorMessage };
320
+ }
321
+ }
322
+
323
+ private chunkMessage(content: string): string[] {
324
+ if (content.length <= MAX_MESSAGE_LENGTH) {
325
+ return [content];
326
+ }
327
+
328
+ const chunks: string[] = [];
329
+ let remaining = content;
330
+
331
+ while (remaining.length > 0) {
332
+ if (remaining.length <= MAX_MESSAGE_LENGTH) {
333
+ chunks.push(remaining);
334
+ break;
335
+ }
336
+
337
+ let breakPoint = remaining.lastIndexOf('\n', MAX_MESSAGE_LENGTH);
338
+ if (breakPoint === -1 || breakPoint < MAX_MESSAGE_LENGTH / 2) {
339
+ breakPoint = remaining.lastIndexOf(' ', MAX_MESSAGE_LENGTH);
340
+ }
341
+ if (breakPoint === -1 || breakPoint < MAX_MESSAGE_LENGTH / 2) {
342
+ breakPoint = MAX_MESSAGE_LENGTH;
343
+ }
344
+
345
+ chunks.push(remaining.slice(0, breakPoint));
346
+ remaining = remaining.slice(breakPoint).trimStart();
347
+ }
348
+
349
+ return chunks;
350
+ }
351
+
352
+ async startTyping(_channelId: string): Promise<() => void> {
353
+ // Google Chat does not support typing indicators via the API
354
+ return () => {};
355
+ }
356
+
357
+ onMessage(handler: (message: InboundMessage) => Promise<void>): void {
358
+ this.messageHandler = handler;
359
+ }
360
+
361
+ onError(handler: (error: Error) => void): void {
362
+ this.errorHandler = handler;
363
+ }
364
+ }