@plosson/agentio 0.4.2 → 0.4.3

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.
@@ -0,0 +1,791 @@
1
+ import type { Server } from 'bun';
2
+ import type { ServiceName } from '../types/config';
3
+ import type {
4
+ GatewayConfig,
5
+ InboxPullRequest,
6
+ InboxPullResponse,
7
+ InboxGetRequest,
8
+ InboxGetResponse,
9
+ InboxAckRequest,
10
+ InboxAckResponse,
11
+ InboxReplyRequest,
12
+ InboxReplyResponse,
13
+ InboxStatsRequest,
14
+ InboxStatsResponse,
15
+ OutboxSendRequest,
16
+ OutboxSendResponse,
17
+ OutboxStatusRequest,
18
+ OutboxStatusResponse,
19
+ OutboxListRequest,
20
+ OutboxListResponse,
21
+ GatewayStatusResponse,
22
+ HealthResponse,
23
+ WhatsAppPairResponse,
24
+ WhatsAppGroupListRequest,
25
+ WhatsAppGroupListResponse,
26
+ WhatsAppGroupGetRequest,
27
+ WhatsAppGroupGetResponse,
28
+ WhatsAppGroupCreateRequest,
29
+ WhatsAppGroupCreateResponse,
30
+ WhatsAppGroupUpdateRequest,
31
+ WhatsAppGroupUpdateResponse,
32
+ WhatsAppGroupParticipantsRequest,
33
+ WhatsAppGroupParticipantsResponse,
34
+ WhatsAppGroupLeaveRequest,
35
+ WhatsAppGroupLeaveResponse,
36
+ WhatsAppGroupInviteRequest,
37
+ WhatsAppGroupInviteResponse,
38
+ WhatsAppGroupJoinRequest,
39
+ WhatsAppGroupJoinResponse,
40
+ WhatsAppGroupResolveRequest,
41
+ WhatsAppGroupResolveResponse,
42
+ } from './types';
43
+ import { DEFAULT_GATEWAY_CONFIG } from './types';
44
+ import {
45
+ getInboxMessages,
46
+ getInboxMessage,
47
+ ackInboxMessage,
48
+ getInboxStats,
49
+ queueOutboxMessage,
50
+ getOutboxMessage,
51
+ getOutboxMessages,
52
+ importWhatsAppAuthState,
53
+ type WhatsAppAuthExport,
54
+ } from './store';
55
+ import type { ServiceAdapter } from './adapters/types';
56
+ import type { WhatsAppAdapter } from './adapters/whatsapp';
57
+
58
+ let server: Server<unknown> | null = null;
59
+ let apiSecret: string = '';
60
+ let startTime: number = 0;
61
+ let adapters: Map<ServiceName, ServiceAdapter> = new Map();
62
+
63
+ /**
64
+ * JSON error response helper
65
+ */
66
+ function jsonError(message: string, status: number = 400): Response {
67
+ return new Response(JSON.stringify({ error: message }), {
68
+ status,
69
+ headers: { 'Content-Type': 'application/json' },
70
+ });
71
+ }
72
+
73
+ /**
74
+ * JSON success response helper
75
+ */
76
+ function jsonResponse<T>(data: T, status: number = 200): Response {
77
+ return new Response(JSON.stringify(data), {
78
+ status,
79
+ headers: { 'Content-Type': 'application/json' },
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Verify authorization header
85
+ */
86
+ function verifyAuth(request: Request): boolean {
87
+ if (!apiSecret) return true; // No auth configured
88
+
89
+ const authHeader = request.headers.get('Authorization');
90
+ if (!authHeader) return false;
91
+
92
+ const [type, token] = authHeader.split(' ');
93
+ return type === 'Bearer' && token === apiSecret;
94
+ }
95
+
96
+ /**
97
+ * Parse JSON body safely
98
+ */
99
+ async function parseJsonBody<T>(request: Request): Promise<T | null> {
100
+ try {
101
+ return await request.json() as T;
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Handle inbox pull request
109
+ */
110
+ async function handleInboxPull(request: Request): Promise<Response> {
111
+ const body = await parseJsonBody<InboxPullRequest>(request);
112
+
113
+ const messages = getInboxMessages({
114
+ service: body?.service,
115
+ profile: body?.profile,
116
+ conversationId: body?.conversationId,
117
+ status: body?.status ?? 'pending',
118
+ limit: body?.limit ?? 50,
119
+ });
120
+
121
+ const response: InboxPullResponse = { messages };
122
+ return jsonResponse(response);
123
+ }
124
+
125
+ /**
126
+ * Handle inbox get request
127
+ */
128
+ async function handleInboxGet(request: Request): Promise<Response> {
129
+ const body = await parseJsonBody<InboxGetRequest>(request);
130
+
131
+ if (!body?.id) {
132
+ return jsonError('Message ID is required');
133
+ }
134
+
135
+ const message = getInboxMessage(body.id);
136
+ const response: InboxGetResponse = { message };
137
+ return jsonResponse(response);
138
+ }
139
+
140
+ /**
141
+ * Handle inbox ack request
142
+ */
143
+ async function handleInboxAck(request: Request): Promise<Response> {
144
+ const body = await parseJsonBody<InboxAckRequest>(request);
145
+
146
+ if (!body?.id) {
147
+ return jsonError('Message ID is required');
148
+ }
149
+
150
+ const success = ackInboxMessage(body.id);
151
+ const response: InboxAckResponse = { success };
152
+ return jsonResponse(response);
153
+ }
154
+
155
+ /**
156
+ * Handle inbox reply request
157
+ */
158
+ async function handleInboxReply(request: Request): Promise<Response> {
159
+ const body = await parseJsonBody<InboxReplyRequest>(request);
160
+
161
+ if (!body?.id) {
162
+ return jsonError('Message ID is required');
163
+ }
164
+ if (!body?.content) {
165
+ return jsonError('Content is required');
166
+ }
167
+
168
+ // Get the original inbox message
169
+ const inboxMessage = getInboxMessage(body.id);
170
+ if (!inboxMessage) {
171
+ return jsonError('Message not found', 404);
172
+ }
173
+
174
+ // Queue reply to outbox
175
+ const outboxMessage = queueOutboxMessage({
176
+ service: inboxMessage.service,
177
+ profile: inboxMessage.profile,
178
+ conversationId: inboxMessage.conversationId,
179
+ content: body.content,
180
+ mediaPath: body.mediaPath,
181
+ mediaType: body.mediaType,
182
+ replyToPlatformId: inboxMessage.platformId,
183
+ });
184
+
185
+ // Auto-ack the inbox message
186
+ ackInboxMessage(body.id);
187
+
188
+ const response: InboxReplyResponse = {
189
+ outboxId: outboxMessage.id,
190
+ status: outboxMessage.status,
191
+ };
192
+ return jsonResponse(response);
193
+ }
194
+
195
+ /**
196
+ * Handle inbox stats request
197
+ */
198
+ async function handleInboxStats(request: Request): Promise<Response> {
199
+ const body = await parseJsonBody<InboxStatsRequest>(request);
200
+
201
+ const stats = getInboxStats({
202
+ service: body?.service,
203
+ profile: body?.profile,
204
+ });
205
+
206
+ const response: InboxStatsResponse = stats;
207
+ return jsonResponse(response);
208
+ }
209
+
210
+ /**
211
+ * Handle outbox send request
212
+ */
213
+ async function handleOutboxSend(request: Request): Promise<Response> {
214
+ const body = await parseJsonBody<OutboxSendRequest>(request);
215
+
216
+ if (!body?.service) {
217
+ return jsonError('Service is required');
218
+ }
219
+ if (!body?.profile) {
220
+ return jsonError('Profile is required');
221
+ }
222
+ if (!body?.conversationId) {
223
+ return jsonError('Conversation ID is required');
224
+ }
225
+ if (!body?.content && !body?.mediaPath) {
226
+ return jsonError('Content or media is required');
227
+ }
228
+
229
+ const outboxMessage = queueOutboxMessage({
230
+ service: body.service,
231
+ profile: body.profile,
232
+ conversationId: body.conversationId,
233
+ content: body.content,
234
+ mediaPath: body.mediaPath,
235
+ mediaType: body.mediaType,
236
+ replyToPlatformId: body.replyToPlatformId,
237
+ metadata: body.metadata,
238
+ });
239
+
240
+ const response: OutboxSendResponse = {
241
+ id: outboxMessage.id,
242
+ status: outboxMessage.status,
243
+ };
244
+ return jsonResponse(response);
245
+ }
246
+
247
+ /**
248
+ * Handle outbox status request
249
+ */
250
+ async function handleOutboxStatus(request: Request): Promise<Response> {
251
+ const body = await parseJsonBody<OutboxStatusRequest>(request);
252
+
253
+ if (!body?.id) {
254
+ return jsonError('Message ID is required');
255
+ }
256
+
257
+ const message = getOutboxMessage(body.id);
258
+ const response: OutboxStatusResponse = { message };
259
+ return jsonResponse(response);
260
+ }
261
+
262
+ /**
263
+ * Handle outbox list request
264
+ */
265
+ async function handleOutboxList(request: Request): Promise<Response> {
266
+ const body = await parseJsonBody<OutboxListRequest>(request);
267
+
268
+ const messages = getOutboxMessages({
269
+ service: body?.service,
270
+ profile: body?.profile,
271
+ status: body?.status,
272
+ limit: body?.limit ?? 50,
273
+ });
274
+
275
+ const response: OutboxListResponse = { messages };
276
+ return jsonResponse(response);
277
+ }
278
+
279
+ /**
280
+ * Handle health check
281
+ */
282
+ function handleHealth(): Response {
283
+ const response: HealthResponse = {
284
+ status: 'ok',
285
+ timestamp: Math.floor(Date.now() / 1000),
286
+ };
287
+ return jsonResponse(response);
288
+ }
289
+
290
+ /**
291
+ * Handle gateway status
292
+ */
293
+ function handleStatus(): Response {
294
+ const adapterStatus: GatewayStatusResponse['adapters'] = [];
295
+
296
+ for (const [service, adapter] of adapters) {
297
+ for (const profile of adapter.getConnectedProfiles()) {
298
+ const state = adapter.getConnectionState(profile);
299
+ adapterStatus.push({
300
+ service,
301
+ profile,
302
+ connected: state.connected,
303
+ error: state.error,
304
+ });
305
+ }
306
+ }
307
+
308
+ const response: GatewayStatusResponse = {
309
+ running: true,
310
+ uptime: Math.floor((Date.now() - startTime) / 1000),
311
+ adapters: adapterStatus,
312
+ };
313
+ return jsonResponse(response);
314
+ }
315
+
316
+ /**
317
+ * Handle media request
318
+ */
319
+ function handleMedia(id: string): Response {
320
+ const message = getInboxMessage(id);
321
+ if (!message) {
322
+ return jsonError('Message not found', 404);
323
+ }
324
+ if (!message.mediaPath) {
325
+ return jsonError('No media attached', 404);
326
+ }
327
+
328
+ try {
329
+ const file = Bun.file(message.mediaPath);
330
+ return new Response(file);
331
+ } catch {
332
+ return jsonError('Media file not found', 404);
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Handle WhatsApp pairing request
338
+ */
339
+ function handleWhatsAppPair(profile: string): Response {
340
+ const whatsappAdapter = adapters.get('whatsapp') as WhatsAppAdapter | undefined;
341
+
342
+ if (!whatsappAdapter) {
343
+ const response: WhatsAppPairResponse = {
344
+ status: 'not_configured',
345
+ message: 'WhatsApp is not configured. Add a profile first.',
346
+ };
347
+ return jsonResponse(response);
348
+ }
349
+
350
+ const state = whatsappAdapter.getWhatsAppState(profile);
351
+
352
+ if (state.connected) {
353
+ const response: WhatsAppPairResponse = {
354
+ status: 'connected',
355
+ phoneNumber: state.phoneNumber,
356
+ displayName: undefined, // Will be filled if available
357
+ message: `Connected as ${state.phoneNumber || 'WhatsApp user'}`,
358
+ };
359
+ return jsonResponse(response);
360
+ }
361
+
362
+ if (state.qrCode) {
363
+ const response: WhatsAppPairResponse = {
364
+ status: 'waiting_qr',
365
+ qrCode: state.qrCode,
366
+ message: 'Scan QR code with WhatsApp on your phone',
367
+ };
368
+ return jsonResponse(response);
369
+ }
370
+
371
+ const response: WhatsAppPairResponse = {
372
+ status: 'connecting',
373
+ message: state.error || 'Connecting to WhatsApp...',
374
+ };
375
+ return jsonResponse(response);
376
+ }
377
+
378
+ /**
379
+ * Handle WhatsApp auth import (for teleport)
380
+ */
381
+ async function handleWhatsAppImport(profile: string, request: Request): Promise<Response> {
382
+ try {
383
+ const authExport = await parseJsonBody<WhatsAppAuthExport>(request);
384
+
385
+ if (!authExport || !authExport.profile) {
386
+ return jsonError('Invalid auth export data');
387
+ }
388
+
389
+ // Import the auth state
390
+ await importWhatsAppAuthState({
391
+ ...authExport,
392
+ profile, // Use URL profile, not body profile (security)
393
+ });
394
+
395
+ // Disconnect and reconnect WhatsApp adapter to use new credentials
396
+ const whatsappAdapter = adapters.get('whatsapp') as WhatsAppAdapter | undefined;
397
+ if (whatsappAdapter) {
398
+ await whatsappAdapter.disconnect(profile);
399
+ // Note: reconnection will happen on next daemon cycle or manual reload
400
+ }
401
+
402
+ return jsonResponse({ success: true, message: 'Auth state imported. Reload gateway to reconnect.' });
403
+ } catch (error) {
404
+ const message = error instanceof Error ? error.message : 'Import failed';
405
+ return jsonError(message, 500);
406
+ }
407
+ }
408
+
409
+ // ============ WHATSAPP GROUP HANDLERS ============
410
+
411
+ /**
412
+ * Handle WhatsApp group list request
413
+ */
414
+ async function handleWhatsAppGroupList(request: Request): Promise<Response> {
415
+ const body = await parseJsonBody<WhatsAppGroupListRequest>(request);
416
+
417
+ if (!body?.profile) {
418
+ return jsonError('Profile is required');
419
+ }
420
+
421
+ const whatsappAdapter = adapters.get('whatsapp') as WhatsAppAdapter | undefined;
422
+ if (!whatsappAdapter) {
423
+ return jsonError('WhatsApp not configured', 404);
424
+ }
425
+
426
+ try {
427
+ const groups = await whatsappAdapter.listGroups(body.profile);
428
+ const response: WhatsAppGroupListResponse = { groups };
429
+ return jsonResponse(response);
430
+ } catch (error) {
431
+ const message = error instanceof Error ? error.message : 'Failed to list groups';
432
+ return jsonError(message, 500);
433
+ }
434
+ }
435
+
436
+ /**
437
+ * Handle WhatsApp group get request
438
+ */
439
+ async function handleWhatsAppGroupGet(request: Request): Promise<Response> {
440
+ const body = await parseJsonBody<WhatsAppGroupGetRequest>(request);
441
+
442
+ if (!body?.profile) {
443
+ return jsonError('Profile is required');
444
+ }
445
+ if (!body?.groupId) {
446
+ return jsonError('Group ID is required');
447
+ }
448
+
449
+ const whatsappAdapter = adapters.get('whatsapp') as WhatsAppAdapter | undefined;
450
+ if (!whatsappAdapter) {
451
+ return jsonError('WhatsApp not configured', 404);
452
+ }
453
+
454
+ try {
455
+ const group = await whatsappAdapter.getGroup(body.profile, body.groupId);
456
+ const response: WhatsAppGroupGetResponse = { group };
457
+ return jsonResponse(response);
458
+ } catch (error) {
459
+ const message = error instanceof Error ? error.message : 'Failed to get group';
460
+ return jsonError(message, 500);
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Handle WhatsApp group create request
466
+ */
467
+ async function handleWhatsAppGroupCreate(request: Request): Promise<Response> {
468
+ const body = await parseJsonBody<WhatsAppGroupCreateRequest>(request);
469
+
470
+ if (!body?.profile) {
471
+ return jsonError('Profile is required');
472
+ }
473
+ if (!body?.name) {
474
+ return jsonError('Group name is required');
475
+ }
476
+ if (!body?.participants || body.participants.length === 0) {
477
+ return jsonError('At least one participant is required');
478
+ }
479
+
480
+ const whatsappAdapter = adapters.get('whatsapp') as WhatsAppAdapter | undefined;
481
+ if (!whatsappAdapter) {
482
+ return jsonError('WhatsApp not configured', 404);
483
+ }
484
+
485
+ try {
486
+ const group = await whatsappAdapter.createGroup(body.profile, body.name, body.participants, body.picture);
487
+ const response: WhatsAppGroupCreateResponse = { group };
488
+ return jsonResponse(response);
489
+ } catch (error) {
490
+ const message = error instanceof Error ? error.message : 'Failed to create group';
491
+ return jsonError(message, 500);
492
+ }
493
+ }
494
+
495
+ /**
496
+ * Handle WhatsApp group update request
497
+ */
498
+ async function handleWhatsAppGroupUpdate(request: Request): Promise<Response> {
499
+ const body = await parseJsonBody<WhatsAppGroupUpdateRequest>(request);
500
+
501
+ if (!body?.profile) {
502
+ return jsonError('Profile is required');
503
+ }
504
+ if (!body?.groupId) {
505
+ return jsonError('Group ID is required');
506
+ }
507
+
508
+ const whatsappAdapter = adapters.get('whatsapp') as WhatsAppAdapter | undefined;
509
+ if (!whatsappAdapter) {
510
+ return jsonError('WhatsApp not configured', 404);
511
+ }
512
+
513
+ try {
514
+ if (body.subject) {
515
+ await whatsappAdapter.updateGroupSubject(body.profile, body.groupId, body.subject);
516
+ }
517
+ if (body.description !== undefined) {
518
+ await whatsappAdapter.updateGroupDescription(body.profile, body.groupId, body.description);
519
+ }
520
+ if (body.picture) {
521
+ await whatsappAdapter.updateGroupPicture(body.profile, body.groupId, body.picture);
522
+ }
523
+ const response: WhatsAppGroupUpdateResponse = { success: true };
524
+ return jsonResponse(response);
525
+ } catch (error) {
526
+ const message = error instanceof Error ? error.message : 'Failed to update group';
527
+ return jsonError(message, 500);
528
+ }
529
+ }
530
+
531
+ /**
532
+ * Handle WhatsApp group participants update request
533
+ */
534
+ async function handleWhatsAppGroupParticipants(request: Request): Promise<Response> {
535
+ const body = await parseJsonBody<WhatsAppGroupParticipantsRequest>(request);
536
+
537
+ if (!body?.profile) {
538
+ return jsonError('Profile is required');
539
+ }
540
+ if (!body?.groupId) {
541
+ return jsonError('Group ID is required');
542
+ }
543
+ if (!body?.participants || body.participants.length === 0) {
544
+ return jsonError('At least one participant is required');
545
+ }
546
+ if (!body?.action) {
547
+ return jsonError('Action is required (add, remove, promote, demote)');
548
+ }
549
+
550
+ const whatsappAdapter = adapters.get('whatsapp') as WhatsAppAdapter | undefined;
551
+ if (!whatsappAdapter) {
552
+ return jsonError('WhatsApp not configured', 404);
553
+ }
554
+
555
+ try {
556
+ const results = await whatsappAdapter.updateParticipants(
557
+ body.profile,
558
+ body.groupId,
559
+ body.participants,
560
+ body.action
561
+ );
562
+ const response: WhatsAppGroupParticipantsResponse = { success: true, results };
563
+ return jsonResponse(response);
564
+ } catch (error) {
565
+ const message = error instanceof Error ? error.message : 'Failed to update participants';
566
+ return jsonError(message, 500);
567
+ }
568
+ }
569
+
570
+ /**
571
+ * Handle WhatsApp group leave request
572
+ */
573
+ async function handleWhatsAppGroupLeave(request: Request): Promise<Response> {
574
+ const body = await parseJsonBody<WhatsAppGroupLeaveRequest>(request);
575
+
576
+ if (!body?.profile) {
577
+ return jsonError('Profile is required');
578
+ }
579
+ if (!body?.groupId) {
580
+ return jsonError('Group ID is required');
581
+ }
582
+
583
+ const whatsappAdapter = adapters.get('whatsapp') as WhatsAppAdapter | undefined;
584
+ if (!whatsappAdapter) {
585
+ return jsonError('WhatsApp not configured', 404);
586
+ }
587
+
588
+ try {
589
+ await whatsappAdapter.leaveGroup(body.profile, body.groupId);
590
+ const response: WhatsAppGroupLeaveResponse = { success: true };
591
+ return jsonResponse(response);
592
+ } catch (error) {
593
+ const message = error instanceof Error ? error.message : 'Failed to leave group';
594
+ return jsonError(message, 500);
595
+ }
596
+ }
597
+
598
+ /**
599
+ * Handle WhatsApp group invite code request
600
+ */
601
+ async function handleWhatsAppGroupInvite(request: Request): Promise<Response> {
602
+ const body = await parseJsonBody<WhatsAppGroupInviteRequest>(request);
603
+
604
+ if (!body?.profile) {
605
+ return jsonError('Profile is required');
606
+ }
607
+ if (!body?.groupId) {
608
+ return jsonError('Group ID is required');
609
+ }
610
+
611
+ const whatsappAdapter = adapters.get('whatsapp') as WhatsAppAdapter | undefined;
612
+ if (!whatsappAdapter) {
613
+ return jsonError('WhatsApp not configured', 404);
614
+ }
615
+
616
+ try {
617
+ const inviteCode = await whatsappAdapter.getGroupInviteCode(body.profile, body.groupId);
618
+ const response: WhatsAppGroupInviteResponse = {
619
+ inviteCode,
620
+ inviteLink: `https://chat.whatsapp.com/${inviteCode}`,
621
+ };
622
+ return jsonResponse(response);
623
+ } catch (error) {
624
+ const message = error instanceof Error ? error.message : 'Failed to get invite code';
625
+ return jsonError(message, 500);
626
+ }
627
+ }
628
+
629
+ /**
630
+ * Handle WhatsApp group join request
631
+ */
632
+ async function handleWhatsAppGroupJoin(request: Request): Promise<Response> {
633
+ const body = await parseJsonBody<WhatsAppGroupJoinRequest>(request);
634
+
635
+ if (!body?.profile) {
636
+ return jsonError('Profile is required');
637
+ }
638
+ if (!body?.inviteCode) {
639
+ return jsonError('Invite code is required');
640
+ }
641
+
642
+ const whatsappAdapter = adapters.get('whatsapp') as WhatsAppAdapter | undefined;
643
+ if (!whatsappAdapter) {
644
+ return jsonError('WhatsApp not configured', 404);
645
+ }
646
+
647
+ try {
648
+ const groupId = await whatsappAdapter.joinGroupViaInvite(body.profile, body.inviteCode);
649
+ const response: WhatsAppGroupJoinResponse = { groupId };
650
+ return jsonResponse(response);
651
+ } catch (error) {
652
+ const message = error instanceof Error ? error.message : 'Failed to join group';
653
+ return jsonError(message, 500);
654
+ }
655
+ }
656
+
657
+ /**
658
+ * Handle WhatsApp group resolve request (name to JID or JID to name)
659
+ */
660
+ async function handleWhatsAppGroupResolve(request: Request): Promise<Response> {
661
+ const body = await parseJsonBody<WhatsAppGroupResolveRequest>(request);
662
+
663
+ if (!body?.profile) {
664
+ return jsonError('Profile is required');
665
+ }
666
+ if (!body?.nameOrId) {
667
+ return jsonError('Name or ID is required');
668
+ }
669
+
670
+ const whatsappAdapter = adapters.get('whatsapp') as WhatsAppAdapter | undefined;
671
+ if (!whatsappAdapter) {
672
+ return jsonError('WhatsApp not configured', 404);
673
+ }
674
+
675
+ try {
676
+ const result = await whatsappAdapter.resolveGroup(body.profile, body.nameOrId);
677
+ const response: WhatsAppGroupResolveResponse = result;
678
+ return jsonResponse(response);
679
+ } catch (error) {
680
+ const message = error instanceof Error ? error.message : 'Failed to resolve group';
681
+ return jsonError(message, 500);
682
+ }
683
+ }
684
+
685
+ /**
686
+ * Main request handler
687
+ */
688
+ async function handleRequest(request: Request): Promise<Response> {
689
+ const url = new URL(request.url);
690
+ const path = url.pathname;
691
+
692
+ // CORS preflight
693
+ if (request.method === 'OPTIONS') {
694
+ return new Response(null, {
695
+ status: 204,
696
+ headers: {
697
+ 'Access-Control-Allow-Origin': '*',
698
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
699
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
700
+ },
701
+ });
702
+ }
703
+
704
+ // Health check doesn't require auth
705
+ if (path === '/health' && request.method === 'GET') {
706
+ return handleHealth();
707
+ }
708
+
709
+ // All other endpoints require auth
710
+ if (!verifyAuth(request)) {
711
+ return jsonError('Unauthorized', 401);
712
+ }
713
+
714
+ // Route requests
715
+ if (request.method === 'GET') {
716
+ if (path === '/status') return handleStatus();
717
+ if (path.startsWith('/media/')) {
718
+ const id = path.slice('/media/'.length);
719
+ return handleMedia(id);
720
+ }
721
+ if (path.startsWith('/whatsapp/pair/')) {
722
+ const profile = decodeURIComponent(path.slice('/whatsapp/pair/'.length));
723
+ return handleWhatsAppPair(profile);
724
+ }
725
+ }
726
+
727
+ if (request.method === 'POST') {
728
+ if (path === '/inbox/pull') return handleInboxPull(request);
729
+ if (path === '/inbox/get') return handleInboxGet(request);
730
+ if (path === '/inbox/ack') return handleInboxAck(request);
731
+ if (path === '/inbox/reply') return handleInboxReply(request);
732
+ if (path === '/inbox/stats') return handleInboxStats(request);
733
+ if (path === '/outbox/send') return handleOutboxSend(request);
734
+ if (path === '/outbox/status') return handleOutboxStatus(request);
735
+ if (path === '/outbox/list') return handleOutboxList(request);
736
+ // Teleport import endpoints
737
+ if (path.startsWith('/import/whatsapp/')) {
738
+ const profile = decodeURIComponent(path.slice('/import/whatsapp/'.length));
739
+ return handleWhatsAppImport(profile, request);
740
+ }
741
+ // WhatsApp group endpoints
742
+ if (path === '/whatsapp/groups/list') return handleWhatsAppGroupList(request);
743
+ if (path === '/whatsapp/groups/get') return handleWhatsAppGroupGet(request);
744
+ if (path === '/whatsapp/groups/create') return handleWhatsAppGroupCreate(request);
745
+ if (path === '/whatsapp/groups/update') return handleWhatsAppGroupUpdate(request);
746
+ if (path === '/whatsapp/groups/participants') return handleWhatsAppGroupParticipants(request);
747
+ if (path === '/whatsapp/groups/leave') return handleWhatsAppGroupLeave(request);
748
+ if (path === '/whatsapp/groups/invite') return handleWhatsAppGroupInvite(request);
749
+ if (path === '/whatsapp/groups/join') return handleWhatsAppGroupJoin(request);
750
+ if (path === '/whatsapp/groups/resolve') return handleWhatsAppGroupResolve(request);
751
+ }
752
+
753
+ return jsonError('Not found', 404);
754
+ }
755
+
756
+ /**
757
+ * Start the API server
758
+ */
759
+ export function startApiServer(config: GatewayConfig['api'], serviceAdapters: Map<ServiceName, ServiceAdapter>): Server<unknown> {
760
+ const port = config?.port ?? DEFAULT_GATEWAY_CONFIG.api.port;
761
+ const host = config?.host ?? DEFAULT_GATEWAY_CONFIG.api.host;
762
+ apiSecret = config?.secret ?? '';
763
+ startTime = Date.now();
764
+ adapters = serviceAdapters;
765
+
766
+ server = Bun.serve({
767
+ port,
768
+ hostname: host,
769
+ fetch: handleRequest,
770
+ });
771
+
772
+ console.log(`Gateway API listening on http://${host}:${port}`);
773
+ return server;
774
+ }
775
+
776
+ /**
777
+ * Stop the API server
778
+ */
779
+ export function stopApiServer(): void {
780
+ if (server) {
781
+ server.stop();
782
+ server = null;
783
+ }
784
+ }
785
+
786
+ /**
787
+ * Check if server is running
788
+ */
789
+ export function isApiServerRunning(): boolean {
790
+ return server !== null;
791
+ }