@cemscale-voip/voip-sdk 1.25.1 → 1.25.2

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/dist/client.js ADDED
@@ -0,0 +1,1015 @@
1
+ // ============================================================
2
+ // @cemscale/voip-sdk — API Client (HTTP + WebSocket)
3
+ // ============================================================
4
+ // --- HTTP Transport ---
5
+ class HttpError extends Error {
6
+ status;
7
+ statusText;
8
+ body;
9
+ constructor(status, statusText, body) {
10
+ super(`HTTP ${status}: ${typeof body === 'object' && body && 'error' in body ? body.error : statusText}`);
11
+ this.status = status;
12
+ this.statusText = statusText;
13
+ this.body = body;
14
+ this.name = 'HttpError';
15
+ }
16
+ }
17
+ export { HttpError };
18
+ // --- VoIPClient ---
19
+ export class VoIPClient {
20
+ apiUrl;
21
+ token;
22
+ apiKey;
23
+ tenantId;
24
+ timeout;
25
+ // WebSocket
26
+ ws = null;
27
+ wsReconnectTimer = null;
28
+ wsReconnectAttempts = 0;
29
+ WS_MAX_RECONNECT = 10;
30
+ wsListeners = new Map();
31
+ wsPingInterval = null;
32
+ wsConnected = false;
33
+ constructor(config) {
34
+ this.apiUrl = config.apiUrl.replace(/\/+$/, '');
35
+ this.token = config.token ?? null;
36
+ this.apiKey = config.apiKey ?? null;
37
+ this.tenantId = config.tenantId ?? null;
38
+ this.timeout = config.timeout ?? 15000;
39
+ }
40
+ // -------------------------------------------------------------------
41
+ // Credential management
42
+ // -------------------------------------------------------------------
43
+ /** Set or change the API key at runtime */
44
+ setApiKey(apiKey) { this.apiKey = apiKey; }
45
+ /** Get the current API key */
46
+ getApiKey() { return this.apiKey; }
47
+ /** Set JWT token (only for browser login flows — most apps should use apiKey) */
48
+ setToken(token) { this.token = token; }
49
+ /** Get current JWT token */
50
+ getToken() { return this.token; }
51
+ /** Set tenant ID (only used by superadmin) */
52
+ setTenantId(tenantId) { this.tenantId = tenantId; }
53
+ getTenantId() { return this.tenantId; }
54
+ // -------------------------------------------------------------------
55
+ // HTTP helpers
56
+ // -------------------------------------------------------------------
57
+ async request(method, path, body, query, options) {
58
+ let url = `${this.apiUrl}${path}`;
59
+ if (query) {
60
+ const params = new URLSearchParams();
61
+ for (const [key, value] of Object.entries(query)) {
62
+ if (value !== undefined)
63
+ params.set(key, String(value));
64
+ }
65
+ const qs = params.toString();
66
+ if (qs)
67
+ url += `?${qs}`;
68
+ }
69
+ const headers = {};
70
+ if (this.apiKey) {
71
+ headers['X-API-Key'] = this.apiKey;
72
+ }
73
+ else if (this.token) {
74
+ headers['Authorization'] = `Bearer ${this.token}`;
75
+ }
76
+ // Per-request tenantId takes priority over client-level tenantId
77
+ const effectiveTenantId = options?.tenantId ?? this.tenantId;
78
+ if (effectiveTenantId && effectiveTenantId !== 'all')
79
+ headers['X-Tenant-ID'] = effectiveTenantId;
80
+ // Only set Content-Type when there's a body
81
+ if (body != null)
82
+ headers['Content-Type'] = 'application/json';
83
+ const controller = new AbortController();
84
+ const timer = setTimeout(() => controller.abort(), this.timeout);
85
+ try {
86
+ const response = await fetch(url, {
87
+ method,
88
+ headers,
89
+ body: body != null ? JSON.stringify(body) : undefined,
90
+ signal: controller.signal,
91
+ });
92
+ if (response.status === 204)
93
+ return {};
94
+ const contentType = response.headers.get('content-type') || '';
95
+ const data = contentType.includes('application/json')
96
+ ? await response.json()
97
+ : await response.text();
98
+ if (!response.ok)
99
+ throw new HttpError(response.status, response.statusText, data);
100
+ return data;
101
+ }
102
+ finally {
103
+ clearTimeout(timer);
104
+ }
105
+ }
106
+ // -------------------------------------------------------------------
107
+ // Auth
108
+ // -------------------------------------------------------------------
109
+ /** Login as an extension user */
110
+ async login(params) {
111
+ const result = await this.request('POST', '/api/auth/login', params);
112
+ this.token = result.token;
113
+ this.tenantId = params.tenantId;
114
+ return result;
115
+ }
116
+ /** Login as a tenant admin */
117
+ async adminLogin(params) {
118
+ const result = await this.request('POST', '/api/auth/admin-login', params);
119
+ this.token = result.token;
120
+ if (result.tenant)
121
+ this.tenantId = result.tenant.id;
122
+ return result;
123
+ }
124
+ /** Login as platform superadmin */
125
+ async superadminLogin(params) {
126
+ const result = await this.request('POST', '/api/auth/superadmin-login', params);
127
+ this.token = result.token;
128
+ return result;
129
+ }
130
+ /** Get current user info */
131
+ async me() {
132
+ return this.request('GET', '/api/auth/me');
133
+ }
134
+ /** Get TURN credentials for WebRTC */
135
+ async getTurnCredentials() {
136
+ return this.request('GET', '/api/auth/turn-credentials');
137
+ }
138
+ // -------------------------------------------------------------------
139
+ // Calls (CDR)
140
+ // -------------------------------------------------------------------
141
+ /** List call records with pagination and filters */
142
+ async listCalls(params) {
143
+ return this.request('GET', '/api/calls', undefined, params);
144
+ }
145
+ /** Get a single call record */
146
+ async getCall(id) {
147
+ return this.request('GET', `/api/calls/${id}`);
148
+ }
149
+ /** Originate a call via FreeSWITCH. Pass `options.tenantId` when using a global superadmin key. */
150
+ async originate(params, options) {
151
+ return this.request('POST', '/api/calls/originate', params, undefined, options);
152
+ }
153
+ /** Hang up an active call */
154
+ async hangup(uuid) {
155
+ return this.request('POST', `/api/calls/${uuid}/hangup`);
156
+ }
157
+ /** Transfer an active call (blind or attended) */
158
+ async transfer(uuid, params) {
159
+ return this.request('POST', `/api/calls/${uuid}/transfer`, params);
160
+ }
161
+ /** List active calls from FreeSWITCH */
162
+ async getActiveCalls() {
163
+ return this.request('GET', '/api/calls/active');
164
+ }
165
+ /** Get call statistics for a period */
166
+ async getCallStats(period) {
167
+ return this.request('GET', '/api/calls/stats', undefined, period ? { period } : undefined);
168
+ }
169
+ /** Place a call on hold or resume */
170
+ async holdCall(uuid, hold = true) {
171
+ return this.request('POST', `/api/calls/${uuid}/hold`, { hold });
172
+ }
173
+ /** Park an active call */
174
+ async parkCall(uuid, slot) {
175
+ return this.request('POST', `/api/calls/${uuid}/park`, slot ? { slot } : undefined);
176
+ }
177
+ /** List parked calls */
178
+ async getParkedCalls() {
179
+ return this.request('GET', '/api/calls/parked');
180
+ }
181
+ /** Export CDR as CSV. Returns raw CSV text. */
182
+ async exportCalls(params) {
183
+ return this.request('GET', '/api/calls/export', undefined, params);
184
+ }
185
+ // -------------------------------------------------------------------
186
+ // Extensions
187
+ // -------------------------------------------------------------------
188
+ /** List extensions for the current tenant */
189
+ async listExtensions() {
190
+ return this.request('GET', '/api/extensions');
191
+ }
192
+ /** Get a single extension */
193
+ async getExtension(id) {
194
+ return this.request('GET', `/api/extensions/${id}`);
195
+ }
196
+ /** Create an extension (also creates Kamailio subscriber). Pass `options.tenantId` when using a global superadmin key. */
197
+ async createExtension(params, options) {
198
+ return this.request('POST', '/api/extensions', params, undefined, options);
199
+ }
200
+ /** Update an extension */
201
+ async updateExtension(id, params) {
202
+ return this.request('PUT', `/api/extensions/${id}`, params);
203
+ }
204
+ /** Delete an extension */
205
+ async deleteExtension(id) {
206
+ return this.request('DELETE', `/api/extensions/${id}`);
207
+ }
208
+ /** Get call forward settings for an extension */
209
+ async getCallForward(extensionId) {
210
+ return this.request('GET', `/api/extensions/${extensionId}/forward`);
211
+ }
212
+ /** Set call forward for an extension */
213
+ async setCallForward(extensionId, params) {
214
+ return this.request('PUT', `/api/extensions/${extensionId}/forward`, params);
215
+ }
216
+ /** Lookup extension by CRM user ID */
217
+ async getExtensionByCrmUser(crmUserId) {
218
+ return this.request('GET', `/api/extensions/by-crm-user/${crmUserId}`);
219
+ }
220
+ /** Update CRM mapping for an extension */
221
+ async updateCrmMapping(extensionId, params) {
222
+ return this.request('PUT', `/api/extensions/${extensionId}/crm-mapping`, params);
223
+ }
224
+ /**
225
+ * Get SIP credentials for connecting a WebRTC softphone.
226
+ * Returns everything needed to register: extension, password, SIP domain, WebSocket URI.
227
+ * Use this so your app never needs to store SIP passwords.
228
+ *
229
+ * @example
230
+ * const { sipCredentials } = await voip.getSipCredentials('extension-uuid');
231
+ * // Use with WebRTCPhone or useVoIP hook
232
+ */
233
+ async getSipCredentials(extensionId) {
234
+ return this.request('GET', `/api/extensions/${extensionId}/sip-credentials`);
235
+ }
236
+ /**
237
+ * Get SIP credentials by extension number (e.g. "1001") instead of UUID.
238
+ * Same as getSipCredentials() but looks up by number.
239
+ *
240
+ * @example
241
+ * const { sipCredentials } = await voip.getSipCredentialsByNumber('1001');
242
+ */
243
+ async getSipCredentialsByNumber(extensionNumber) {
244
+ return this.request('GET', `/api/extensions/by-number/${extensionNumber}/sip-credentials`);
245
+ }
246
+ // -------------------------------------------------------------------
247
+ // Tenants (superadmin)
248
+ // -------------------------------------------------------------------
249
+ /** List all tenants (superadmin only) */
250
+ async listTenants() {
251
+ return this.request('GET', '/api/tenants');
252
+ }
253
+ /** Get a single tenant */
254
+ async getTenant(id) {
255
+ return this.request('GET', `/api/tenants/${id}`);
256
+ }
257
+ /** Create a tenant (superadmin only) */
258
+ async createTenant(params) {
259
+ return this.request('POST', '/api/tenants', params);
260
+ }
261
+ /** Update a tenant (superadmin only) */
262
+ async updateTenant(id, params) {
263
+ return this.request('PUT', `/api/tenants/${id}`, params);
264
+ }
265
+ /**
266
+ * Delete a tenant and ALL associated data (superadmin only).
267
+ *
268
+ * Performs a complete, immediate cleanup:
269
+ * - Database: tenant, extensions, DIDs, subscribers, locations, call records,
270
+ * voicemails, IVR menus, ring groups, queues, webhooks, schedules, API keys
271
+ * - FreeSWITCH: removes all tenant-owned SIP trunk gateways
272
+ * - Active calls: hangs up any in-progress calls belonging to this tenant
273
+ * - Kamailio: flushes in-memory SIP registrations so softphones are
274
+ * immediately disconnected (cannot place calls after deletion)
275
+ * - Audio files: recordings, voicemail messages, IVR greetings, voicemail greetings
276
+ * - Redis: presence keys
277
+ *
278
+ * This is irreversible. Call with care.
279
+ *
280
+ * @param id - Tenant UUID
281
+ */
282
+ async deleteTenant(id) {
283
+ return this.request('DELETE', `/api/tenants/${id}`);
284
+ }
285
+ /** Get tenant statistics */
286
+ async getTenantStats(id) {
287
+ return this.request('GET', `/api/tenants/${id}/stats`);
288
+ }
289
+ // -------------------------------------------------------------------
290
+ // DIDs
291
+ // -------------------------------------------------------------------
292
+ /** List DID numbers */
293
+ async listDids() {
294
+ return this.request('GET', '/api/dids');
295
+ }
296
+ /** Create a DID. Pass `options.tenantId` when using a global superadmin key. */
297
+ async createDid(params, options) {
298
+ return this.request('POST', '/api/dids', params, undefined, options);
299
+ }
300
+ /** Get a single DID by ID */
301
+ async getDid(id) {
302
+ return this.request('GET', `/api/dids/${id}`);
303
+ }
304
+ /** Update a DID */
305
+ async updateDid(id, params) {
306
+ return this.request('PUT', `/api/dids/${id}`, params);
307
+ }
308
+ /** Delete a DID */
309
+ async deleteDid(id) {
310
+ return this.request('DELETE', `/api/dids/${id}`);
311
+ }
312
+ // -------------------------------------------------------------------
313
+ // IVR
314
+ // -------------------------------------------------------------------
315
+ /** List IVR menus */
316
+ async listIvrMenus() {
317
+ return this.request('GET', '/api/ivr');
318
+ }
319
+ /** Get a single IVR menu */
320
+ async getIvrMenu(id) {
321
+ return this.request('GET', `/api/ivr/${id}`);
322
+ }
323
+ /** Create an IVR menu. Pass `options.tenantId` when using a global superadmin key. */
324
+ async createIvrMenu(params, options) {
325
+ return this.request('POST', '/api/ivr', params, undefined, options);
326
+ }
327
+ /** Update an IVR menu */
328
+ async updateIvrMenu(id, params) {
329
+ return this.request('PUT', `/api/ivr/${id}`, params);
330
+ }
331
+ /** Delete an IVR menu and its audio files */
332
+ async deleteIvrMenu(id) {
333
+ return this.request('DELETE', `/api/ivr/${id}`);
334
+ }
335
+ /**
336
+ * Upload a greeting audio file for an IVR menu.
337
+ * The file is automatically converted to WAV 8kHz mono (FreeSWITCH format).
338
+ * Accepts WAV, MP3, OGG, M4A.
339
+ *
340
+ * @param id - IVR menu ID
341
+ * @param file - File blob or Buffer
342
+ * @param filename - Original filename (e.g. "greeting.mp3")
343
+ */
344
+ async uploadIvrAudio(id, file, filename) {
345
+ const formData = new FormData();
346
+ const blob = file instanceof Blob ? file : new Blob([new Uint8Array(file)]);
347
+ formData.append('file', blob, filename);
348
+ const headers = {};
349
+ if (this.apiKey) {
350
+ headers['X-API-Key'] = this.apiKey;
351
+ }
352
+ else if (this.token) {
353
+ headers['Authorization'] = `Bearer ${this.token}`;
354
+ }
355
+ if (this.tenantId && this.tenantId !== 'all')
356
+ headers['X-Tenant-ID'] = this.tenantId;
357
+ const response = await fetch(`${this.apiUrl}/api/ivr/${id}/audio`, {
358
+ method: 'POST',
359
+ headers,
360
+ body: formData,
361
+ });
362
+ if (!response.ok) {
363
+ const err = await response.json().catch(() => ({ error: response.statusText }));
364
+ throw new Error(err.error || `Upload failed: ${response.status}`);
365
+ }
366
+ return response.json();
367
+ }
368
+ /**
369
+ * Upload a custom "invalid input" audio file for an IVR menu.
370
+ * Played when the caller presses an invalid digit.
371
+ */
372
+ async uploadIvrInvalidAudio(id, file, filename) {
373
+ const formData = new FormData();
374
+ const blob = file instanceof Blob ? file : new Blob([new Uint8Array(file)]);
375
+ formData.append('file', blob, filename);
376
+ const headers = {};
377
+ if (this.apiKey) {
378
+ headers['X-API-Key'] = this.apiKey;
379
+ }
380
+ else if (this.token) {
381
+ headers['Authorization'] = `Bearer ${this.token}`;
382
+ }
383
+ if (this.tenantId && this.tenantId !== 'all')
384
+ headers['X-Tenant-ID'] = this.tenantId;
385
+ const response = await fetch(`${this.apiUrl}/api/ivr/${id}/invalid-audio`, {
386
+ method: 'POST',
387
+ headers,
388
+ body: formData,
389
+ });
390
+ if (!response.ok) {
391
+ const err = await response.json().catch(() => ({ error: response.statusText }));
392
+ throw new Error(err.error || `Upload failed: ${response.status}`);
393
+ }
394
+ return response.json();
395
+ }
396
+ /**
397
+ * Get the greeting audio file URL for streaming/download.
398
+ * The URL includes the API key as a query parameter so it can be used
399
+ * directly in HTML <audio src="..."> elements without custom headers.
400
+ */
401
+ getIvrAudioUrl(id) {
402
+ const base = `${this.apiUrl}/api/ivr/${id}/audio`;
403
+ return this.apiKey ? `${base}?apiKey=${encodeURIComponent(this.apiKey)}` : base;
404
+ }
405
+ /**
406
+ * Get the invalid input audio file URL for streaming/download.
407
+ * The URL includes the API key as a query parameter.
408
+ */
409
+ getIvrInvalidAudioUrl(id) {
410
+ const base = `${this.apiUrl}/api/ivr/${id}/invalid-audio`;
411
+ return this.apiKey ? `${base}?apiKey=${encodeURIComponent(this.apiKey)}` : base;
412
+ }
413
+ /**
414
+ * Download the greeting audio as a Blob (for programmatic use).
415
+ * Use getIvrAudioUrl() for direct browser playback via <audio> tags.
416
+ */
417
+ async downloadIvrAudio(id) {
418
+ const headers = {};
419
+ if (this.apiKey)
420
+ headers['X-API-Key'] = this.apiKey;
421
+ else if (this.token)
422
+ headers['Authorization'] = `Bearer ${this.token}`;
423
+ const response = await fetch(`${this.apiUrl}/api/ivr/${id}/audio`, { headers });
424
+ if (!response.ok) {
425
+ const err = await response.json().catch(() => ({ error: response.statusText }));
426
+ throw new Error(err.error || `Download failed: ${response.status}`);
427
+ }
428
+ return response.blob();
429
+ }
430
+ /**
431
+ * Download the invalid input audio as a Blob (for programmatic use).
432
+ */
433
+ async downloadIvrInvalidAudio(id) {
434
+ const headers = {};
435
+ if (this.apiKey)
436
+ headers['X-API-Key'] = this.apiKey;
437
+ else if (this.token)
438
+ headers['Authorization'] = `Bearer ${this.token}`;
439
+ const response = await fetch(`${this.apiUrl}/api/ivr/${id}/invalid-audio`, { headers });
440
+ if (!response.ok) {
441
+ const err = await response.json().catch(() => ({ error: response.statusText }));
442
+ throw new Error(err.error || `Download failed: ${response.status}`);
443
+ }
444
+ return response.blob();
445
+ }
446
+ /** Delete the greeting audio file (reverts to generic prompts) */
447
+ async deleteIvrAudio(id) {
448
+ return this.request('DELETE', `/api/ivr/${id}/audio`);
449
+ }
450
+ /** Delete the invalid input audio file (reverts to default) */
451
+ async deleteIvrInvalidAudio(id) {
452
+ return this.request('DELETE', `/api/ivr/${id}/invalid-audio`);
453
+ }
454
+ /**
455
+ * Generate greeting audio via ElevenLabs TTS.
456
+ * The generated audio is automatically saved as the IVR greeting.
457
+ */
458
+ async generateIvrTts(id, params) {
459
+ return this.request('POST', `/api/ivr/${id}/tts`, params);
460
+ }
461
+ /** List available TTS voices for IVR greetings */
462
+ async listIvrVoices() {
463
+ return this.request('GET', '/api/ivr/voices');
464
+ }
465
+ // -------------------------------------------------------------------
466
+ // Recordings
467
+ // -------------------------------------------------------------------
468
+ /** List recordings with pagination. Each recording includes an audio_url for direct streaming. */
469
+ async listRecordings(params) {
470
+ return this.request('GET', '/api/recordings', undefined, params);
471
+ }
472
+ /**
473
+ * Get a direct URL to stream/download a recording.
474
+ * The URL supports auth via ?apiKey= query param for browser <audio> elements.
475
+ */
476
+ async getRecordingUrl(id) {
477
+ return this.request('GET', `/api/recordings/${id}/url`);
478
+ }
479
+ /**
480
+ * Get the audio streaming URL for a recording, ready for browser playback.
481
+ * Appends the API key as a query parameter so it works with <audio src="...">.
482
+ */
483
+ getRecordingAudioUrl(id) {
484
+ const base = `${this.apiUrl}/api/recordings/${id}/audio`;
485
+ return this.apiKey ? `${base}?apiKey=${encodeURIComponent(this.apiKey)}` : base;
486
+ }
487
+ /**
488
+ * Get the download URL for a recording (Content-Disposition: attachment).
489
+ * Appends the API key as a query parameter.
490
+ */
491
+ getRecordingDownloadUrl(id) {
492
+ const base = `${this.apiUrl}/api/recordings/${id}/download`;
493
+ return this.apiKey ? `${base}?apiKey=${encodeURIComponent(this.apiKey)}` : base;
494
+ }
495
+ /** Delete a recording and its file from the server */
496
+ async deleteRecording(id) {
497
+ return this.request('DELETE', `/api/recordings/${id}`);
498
+ }
499
+ // -------------------------------------------------------------------
500
+ // Conferences
501
+ // -------------------------------------------------------------------
502
+ /** List active conferences */
503
+ async listConferences() {
504
+ return this.request('GET', '/api/conferences');
505
+ }
506
+ /** Get conference details */
507
+ async getConference(name) {
508
+ return this.request('GET', `/api/conferences/${name}`);
509
+ }
510
+ /** Join a call to a conference */
511
+ async joinConference(conferenceName, callUuid) {
512
+ return this.request('POST', `/api/conferences/${conferenceName}/join`, { callUuid });
513
+ }
514
+ /** Transfer a call to conference */
515
+ async transferToConference(uuid, conferenceName) {
516
+ return this.request('POST', `/api/calls/${uuid}/conference`, conferenceName ? { conferenceName } : undefined);
517
+ }
518
+ /** Kick a member from conference */
519
+ async kickFromConference(conferenceName, memberId) {
520
+ return this.request('POST', `/api/conferences/${conferenceName}/kick`, { memberId });
521
+ }
522
+ /** Mute/unmute a conference member */
523
+ async muteConferenceMember(conferenceName, memberId, mute = true) {
524
+ return this.request('POST', `/api/conferences/${conferenceName}/mute`, { memberId, mute });
525
+ }
526
+ /** Deaf/undeaf a conference member */
527
+ async deafConferenceMember(conferenceName, memberId, deaf = true) {
528
+ return this.request('POST', `/api/conferences/${conferenceName}/deaf`, { memberId, deaf });
529
+ }
530
+ /** Lock/unlock a conference */
531
+ async lockConference(conferenceName, lock = true) {
532
+ return this.request('POST', `/api/conferences/${conferenceName}/lock`, { lock });
533
+ }
534
+ /** Start/stop conference recording */
535
+ async recordConference(conferenceName, action) {
536
+ return this.request('POST', `/api/conferences/${conferenceName}/record`, { action });
537
+ }
538
+ // -------------------------------------------------------------------
539
+ // Ring Groups
540
+ // -------------------------------------------------------------------
541
+ /** List ring groups */
542
+ async listRingGroups() {
543
+ return this.request('GET', '/api/ring-groups');
544
+ }
545
+ /** Get a ring group */
546
+ async getRingGroup(id) {
547
+ return this.request('GET', `/api/ring-groups/${id}`);
548
+ }
549
+ /** Create a ring group. Pass `options.tenantId` when using a global superadmin key. */
550
+ async createRingGroup(params, options) {
551
+ return this.request('POST', '/api/ring-groups', params, undefined, options);
552
+ }
553
+ /** Update a ring group */
554
+ async updateRingGroup(id, params) {
555
+ return this.request('PUT', `/api/ring-groups/${id}`, params);
556
+ }
557
+ /** Delete a ring group */
558
+ async deleteRingGroup(id) {
559
+ return this.request('DELETE', `/api/ring-groups/${id}`);
560
+ }
561
+ // -------------------------------------------------------------------
562
+ // Call Queues
563
+ // -------------------------------------------------------------------
564
+ /** List call queues */
565
+ async listQueues() {
566
+ return this.request('GET', '/api/queues');
567
+ }
568
+ /** Get a call queue */
569
+ async getQueue(id) {
570
+ return this.request('GET', `/api/queues/${id}`);
571
+ }
572
+ /** Create a call queue. Pass `options.tenantId` when using a global superadmin key. */
573
+ async createQueue(params, options) {
574
+ return this.request('POST', '/api/queues', params, undefined, options);
575
+ }
576
+ /** Update a call queue */
577
+ async updateQueue(id, params) {
578
+ return this.request('PUT', `/api/queues/${id}`, params);
579
+ }
580
+ /** Delete a call queue */
581
+ async deleteQueue(id) {
582
+ return this.request('DELETE', `/api/queues/${id}`);
583
+ }
584
+ /** Pause/unpause a queue agent */
585
+ async pauseQueueAgent(queueId, agentId, paused = true) {
586
+ return this.request('POST', `/api/queues/${queueId}/agents/${agentId}/pause`, { paused });
587
+ }
588
+ /** Login/logout a queue agent */
589
+ async loginQueueAgent(queueId, agentId, loggedIn = true) {
590
+ return this.request('POST', `/api/queues/${queueId}/agents/${agentId}/login`, { loggedIn });
591
+ }
592
+ /** Get real-time queue statistics (agent counts, today's call metrics) */
593
+ async getQueueStats(queueId) {
594
+ return this.request('GET', `/api/queues/${queueId}/stats`);
595
+ }
596
+ // -------------------------------------------------------------------
597
+ // Webhooks
598
+ // -------------------------------------------------------------------
599
+ /** List webhooks */
600
+ async listWebhooks() {
601
+ return this.request('GET', '/api/webhooks');
602
+ }
603
+ /** Get webhook details with recent deliveries */
604
+ async getWebhook(id) {
605
+ return this.request('GET', `/api/webhooks/${id}`);
606
+ }
607
+ /** Create a webhook. Pass `options.tenantId` when using a global superadmin key. */
608
+ async createWebhook(params, options) {
609
+ return this.request('POST', '/api/webhooks', params, undefined, options);
610
+ }
611
+ /** Update a webhook */
612
+ async updateWebhook(id, params) {
613
+ return this.request('PUT', `/api/webhooks/${id}`, params);
614
+ }
615
+ /** Delete a webhook */
616
+ async deleteWebhook(id) {
617
+ return this.request('DELETE', `/api/webhooks/${id}`);
618
+ }
619
+ /** Send a test webhook event */
620
+ async testWebhook(id) {
621
+ return this.request('POST', `/api/webhooks/${id}/test`);
622
+ }
623
+ /** List webhook delivery history */
624
+ async listWebhookDeliveries(id, page, limit) {
625
+ return this.request('GET', `/api/webhooks/${id}/deliveries`, undefined, { page, limit });
626
+ }
627
+ // -------------------------------------------------------------------
628
+ // Voicemail
629
+ // -------------------------------------------------------------------
630
+ /** List voicemail messages with pagination */
631
+ async listVoicemails(params) {
632
+ return this.request('GET', '/api/voicemail', undefined, params);
633
+ }
634
+ /** Get voicemail count (total and unread) */
635
+ async getVoicemailCount(extension) {
636
+ return this.request('GET', '/api/voicemail/count', undefined, extension ? { extension } : undefined);
637
+ }
638
+ /** Get a single voicemail */
639
+ async getVoicemail(id) {
640
+ return this.request('GET', `/api/voicemail/${id}`);
641
+ }
642
+ /** Mark voicemail as read */
643
+ async markVoicemailRead(id) {
644
+ return this.request('PUT', `/api/voicemail/${id}/read`);
645
+ }
646
+ /** Delete a voicemail (hard delete — removes audio file from disk) */
647
+ async deleteVoicemail(id) {
648
+ return this.request('DELETE', `/api/voicemail/${id}`);
649
+ }
650
+ /** Bulk delete voicemails */
651
+ async bulkDeleteVoicemails(params) {
652
+ return this.request('DELETE', '/api/voicemail', params);
653
+ }
654
+ /**
655
+ * Get a direct URL to stream a voicemail's audio in a browser `<audio>` element.
656
+ * Appends the API key as a query parameter so no custom headers are needed.
657
+ *
658
+ * @example
659
+ * ```html
660
+ * <audio src={client.getVoicemailAudioUrl(id)} controls />
661
+ * ```
662
+ */
663
+ getVoicemailAudioUrl(id) {
664
+ const base = `${this.apiUrl}/api/voicemail/${id}/audio`;
665
+ return this.apiKey ? `${base}?apiKey=${encodeURIComponent(this.apiKey)}` : base;
666
+ }
667
+ /**
668
+ * Get a direct URL to download a voicemail as an attachment.
669
+ * Appends the API key as a query parameter.
670
+ */
671
+ getVoicemailDownloadUrl(id) {
672
+ const base = `${this.apiUrl}/api/voicemail/${id}/download`;
673
+ return this.apiKey ? `${base}?apiKey=${encodeURIComponent(this.apiKey)}` : base;
674
+ }
675
+ // -------------------------------------------------------------------
676
+ // Voicemail Greeting
677
+ // -------------------------------------------------------------------
678
+ /** Get the current greeting configuration for an extension */
679
+ async getVoicemailGreeting(extension) {
680
+ return this.request('GET', '/api/voicemail/greeting', undefined, extension ? { extension } : undefined);
681
+ }
682
+ /**
683
+ * Update the voicemail greeting type and text.
684
+ *
685
+ * @example
686
+ * ```ts
687
+ * // Use a custom TTS greeting
688
+ * await client.updateVoicemailGreeting({
689
+ * extension: '1001',
690
+ * greetingType: 'custom_tts',
691
+ * greetingText: 'Hi, you have reached John. I am not available right now. Please leave a message.',
692
+ * });
693
+ *
694
+ * // Use the name-based greeting (uses extension's display_name)
695
+ * await client.updateVoicemailGreeting({ extension: '1001', greetingType: 'name' });
696
+ *
697
+ * // Revert to default
698
+ * await client.updateVoicemailGreeting({ extension: '1001', greetingType: 'default' });
699
+ * ```
700
+ */
701
+ async updateVoicemailGreeting(params) {
702
+ return this.request('PUT', '/api/voicemail/greeting', params);
703
+ }
704
+ /**
705
+ * Upload a custom greeting audio file for an extension's voicemail.
706
+ * The file is automatically converted to WAV 8kHz mono for FreeSWITCH.
707
+ * Supported formats: WAV, MP3, OGG, M4A.
708
+ * Automatically sets greetingType to 'custom_audio'.
709
+ *
710
+ * @param audioFile - The audio file (Buffer, Blob, or File)
711
+ * @param extension - The extension number (defaults to authenticated user's extension)
712
+ * @param filename - Optional filename hint
713
+ */
714
+ async uploadVoicemailGreetingAudio(audioFile, extension, filename) {
715
+ const formData = new FormData();
716
+ formData.append('file', audioFile, filename || 'greeting.wav');
717
+ if (extension) {
718
+ formData.append('extension', extension);
719
+ }
720
+ const headers = {};
721
+ if (this.apiKey)
722
+ headers['X-API-Key'] = this.apiKey;
723
+ else if (this.token)
724
+ headers['Authorization'] = `Bearer ${this.token}`;
725
+ if (this.tenantId && this.tenantId !== 'all')
726
+ headers['X-Tenant-ID'] = this.tenantId;
727
+ const response = await fetch(`${this.apiUrl}/api/voicemail/greeting/audio`, {
728
+ method: 'POST',
729
+ headers,
730
+ body: formData,
731
+ });
732
+ if (!response.ok) {
733
+ const error = await response.json().catch(() => ({ error: response.statusText }));
734
+ throw new HttpError(response.status, error.error || response.statusText, error);
735
+ }
736
+ return response.json();
737
+ }
738
+ /**
739
+ * Get a direct URL to stream the custom greeting audio in a browser `<audio>` element.
740
+ * Returns null if the extension has no custom greeting audio.
741
+ */
742
+ getVoicemailGreetingAudioUrl(extension) {
743
+ const base = `${this.apiUrl}/api/voicemail/greeting/audio?extension=${encodeURIComponent(extension)}`;
744
+ return this.apiKey ? `${base}&apiKey=${encodeURIComponent(this.apiKey)}` : base;
745
+ }
746
+ /** Delete custom greeting audio, reverting to default greeting */
747
+ async deleteVoicemailGreetingAudio(extension) {
748
+ return this.request('DELETE', '/api/voicemail/greeting/audio', extension ? { extension } : undefined);
749
+ }
750
+ // -------------------------------------------------------------------
751
+ // Presence (REST)
752
+ // -------------------------------------------------------------------
753
+ /** Get simple presence (extension -> status) */
754
+ async getPresence() {
755
+ return this.request('GET', '/api/presence');
756
+ }
757
+ /** Get detailed presence with call info */
758
+ async getPresenceDetailed() {
759
+ return this.request('GET', '/api/presence/detailed');
760
+ }
761
+ // -------------------------------------------------------------------
762
+ // Reports
763
+ // -------------------------------------------------------------------
764
+ /** Get dashboard summary stats */
765
+ async getDashboardStats() {
766
+ return this.request('GET', '/api/reports/dashboard');
767
+ }
768
+ /** Get calls grouped by day */
769
+ async getCallsByDay(days) {
770
+ return this.request('GET', '/api/reports/calls-by-day', undefined, days ? { days } : undefined);
771
+ }
772
+ /** Get top extensions by call volume */
773
+ async getTopExtensions(days, limit) {
774
+ return this.request('GET', '/api/reports/top-extensions', undefined, { days, limit });
775
+ }
776
+ // -------------------------------------------------------------------
777
+ // Blocklist (Call Blocking)
778
+ // -------------------------------------------------------------------
779
+ /** List all blocked numbers for the tenant */
780
+ async listBlockedNumbers() {
781
+ return this.request('GET', '/api/blocklist');
782
+ }
783
+ /** Block a phone number. Pass `options.tenantId` when using a global superadmin key. */
784
+ async blockNumber(params, options) {
785
+ return this.request('POST', '/api/blocklist', params, undefined, options);
786
+ }
787
+ /** Unblock a phone number by ID */
788
+ async unblockNumber(id) {
789
+ return this.request('DELETE', `/api/blocklist/${id}`);
790
+ }
791
+ /** Check if a specific number is blocked */
792
+ async checkBlocked(number) {
793
+ return this.request('GET', `/api/blocklist/check/${encodeURIComponent(number)}`);
794
+ }
795
+ // -------------------------------------------------------------------
796
+ // Business Hours / Schedules
797
+ // -------------------------------------------------------------------
798
+ /** List business hour schedules */
799
+ async listSchedules() {
800
+ return this.request('GET', '/api/schedules');
801
+ }
802
+ /** Get a single schedule */
803
+ async getSchedule(id) {
804
+ return this.request('GET', `/api/schedules/${id}`);
805
+ }
806
+ /** Create a business hours schedule. Pass `options.tenantId` when using a global superadmin key. */
807
+ async createSchedule(params, options) {
808
+ return this.request('POST', '/api/schedules', params, undefined, options);
809
+ }
810
+ /** Update a business hours schedule */
811
+ async updateSchedule(id, params) {
812
+ return this.request('PUT', `/api/schedules/${id}`, params);
813
+ }
814
+ /** Delete a business hours schedule */
815
+ async deleteSchedule(id) {
816
+ return this.request('DELETE', `/api/schedules/${id}`);
817
+ }
818
+ /** Check if a schedule is currently open or closed */
819
+ async getScheduleStatus(id) {
820
+ return this.request('GET', `/api/schedules/${id}/status`);
821
+ }
822
+ // -------------------------------------------------------------------
823
+ // SIP Trunks
824
+ // -------------------------------------------------------------------
825
+ /** List SIP trunks (passwords masked) */
826
+ async listTrunks() {
827
+ return this.request('GET', '/api/trunks');
828
+ }
829
+ /** Get a single SIP trunk (password masked) */
830
+ async getTrunk(id) {
831
+ return this.request('GET', `/api/trunks/${id}`);
832
+ }
833
+ /**
834
+ * Create a SIP trunk.
835
+ * @superadmin ONLY — tenant admins cannot create trunks.
836
+ * Tenants automatically route outbound calls through the shared global trunk.
837
+ * Pass `options.tenantId` to assign a trunk to a specific tenant (or omit + set `isGlobal: true` for a shared trunk).
838
+ * Response includes `gatewaySync` indicating whether the FreeSWITCH gateway was loaded.
839
+ */
840
+ async createTrunk(params, options) {
841
+ return this.request('POST', '/api/trunks', params, undefined, options);
842
+ }
843
+ /**
844
+ * Update a SIP trunk.
845
+ * @superadmin ONLY — tenant admins cannot modify trunks.
846
+ */
847
+ async updateTrunk(id, params) {
848
+ return this.request('PUT', `/api/trunks/${id}`, params);
849
+ }
850
+ /**
851
+ * Delete a SIP trunk.
852
+ * @superadmin ONLY — tenant admins cannot delete trunks.
853
+ */
854
+ async deleteTrunk(id) {
855
+ return this.request('DELETE', `/api/trunks/${id}`);
856
+ }
857
+ /** Get live gateway status for a trunk from FreeSWITCH */
858
+ async getTrunkStatus(id) {
859
+ return this.request('GET', `/api/trunks/${id}/status`);
860
+ }
861
+ /** Force re-sync a trunk's gateway to FreeSWITCH */
862
+ async syncTrunk(id) {
863
+ return this.request('POST', `/api/trunks/${id}/sync`);
864
+ }
865
+ /** List all active gateways in FreeSWITCH */
866
+ async listActiveGateways() {
867
+ return this.request('GET', '/api/trunks/gateways/active');
868
+ }
869
+ // -------------------------------------------------------------------
870
+ // API Keys
871
+ // -------------------------------------------------------------------
872
+ /** List API keys for the current tenant (admin only) */
873
+ async listApiKeys() {
874
+ return this.request('GET', '/api/api-keys');
875
+ }
876
+ /** Get a single API key detail */
877
+ async getApiKeyDetail(id) {
878
+ return this.request('GET', `/api/api-keys/${id}`);
879
+ }
880
+ /** Create a new API key. The full key is only returned once in the response. Pass `options.tenantId` when using a global superadmin key. */
881
+ async createApiKey(params, options) {
882
+ return this.request('POST', '/api/api-keys', params, undefined, options);
883
+ }
884
+ /** Update an API key (name, role, scopes, expiration) */
885
+ async updateApiKey(id, params) {
886
+ return this.request('PUT', `/api/api-keys/${id}`, params);
887
+ }
888
+ /** Revoke an API key (soft delete — key becomes immediately unusable) */
889
+ async revokeApiKey(id) {
890
+ return this.request('DELETE', `/api/api-keys/${id}`);
891
+ }
892
+ /** Regenerate an API key (new key value, same ID). Full key returned once. */
893
+ async regenerateApiKey(id) {
894
+ return this.request('POST', `/api/api-keys/${id}/regenerate`);
895
+ }
896
+ // -------------------------------------------------------------------
897
+ // WebSocket — real-time events
898
+ // -------------------------------------------------------------------
899
+ /** Connect to the real-time WebSocket */
900
+ connectWebSocket() {
901
+ if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
902
+ return;
903
+ }
904
+ const wsUrl = this.apiUrl.replace(/^http/, 'ws') + '/api/ws';
905
+ this.ws = new WebSocket(wsUrl);
906
+ this.ws.onopen = () => {
907
+ if (this.apiKey) {
908
+ this.ws.send(JSON.stringify({ type: 'auth', apiKey: this.apiKey }));
909
+ }
910
+ else if (this.token) {
911
+ this.ws.send(JSON.stringify({ type: 'auth', token: this.token }));
912
+ }
913
+ this.wsPingInterval = setInterval(() => {
914
+ if (this.ws?.readyState === WebSocket.OPEN) {
915
+ this.ws.send(JSON.stringify({ type: 'ping' }));
916
+ }
917
+ }, 30000);
918
+ };
919
+ this.ws.onmessage = (event) => {
920
+ try {
921
+ const message = JSON.parse(event.data);
922
+ if (message.type === 'auth_success') {
923
+ this.wsConnected = true;
924
+ this.wsReconnectAttempts = 0; // Reset on successful auth
925
+ }
926
+ this.emitWs(message.type, message);
927
+ this.emitWs('*', message);
928
+ }
929
+ catch {
930
+ // Ignore malformed messages
931
+ }
932
+ };
933
+ this.ws.onclose = () => {
934
+ this.wsConnected = false;
935
+ this.clearWsPing();
936
+ this.emitWs('disconnected', undefined);
937
+ this.scheduleWsReconnect();
938
+ };
939
+ this.ws.onerror = () => {
940
+ // onclose will fire after onerror
941
+ };
942
+ }
943
+ /** Disconnect the WebSocket */
944
+ disconnectWebSocket() {
945
+ this.clearWsReconnect();
946
+ this.clearWsPing();
947
+ this.wsReconnectAttempts = 0;
948
+ if (this.ws) {
949
+ this.ws.onclose = null;
950
+ this.ws.close();
951
+ this.ws = null;
952
+ }
953
+ this.wsConnected = false;
954
+ }
955
+ /** Check if WebSocket is connected and authenticated */
956
+ isWebSocketConnected() {
957
+ return this.wsConnected;
958
+ }
959
+ /** Subscribe to a WebSocket event type. Returns unsubscribe function. */
960
+ onWsEvent(eventType, callback) {
961
+ if (!this.wsListeners.has(eventType)) {
962
+ this.wsListeners.set(eventType, new Set());
963
+ }
964
+ this.wsListeners.get(eventType).add(callback);
965
+ return () => { this.wsListeners.get(eventType)?.delete(callback); };
966
+ }
967
+ emitWs(eventType, data) {
968
+ const listeners = this.wsListeners.get(eventType);
969
+ if (listeners) {
970
+ for (const listener of listeners) {
971
+ try {
972
+ listener(data);
973
+ }
974
+ catch { /* Don't let one bad listener break others */ }
975
+ }
976
+ }
977
+ }
978
+ scheduleWsReconnect() {
979
+ if (this.wsReconnectTimer)
980
+ return;
981
+ if (this.wsReconnectAttempts >= this.WS_MAX_RECONNECT) {
982
+ this.emitWs('error', { type: 'error', message: 'Max WebSocket reconnection attempts reached' });
983
+ return;
984
+ }
985
+ const delay = Math.min(3000 * Math.pow(2, this.wsReconnectAttempts), 60000);
986
+ this.wsReconnectAttempts++;
987
+ this.wsReconnectTimer = setTimeout(() => {
988
+ this.wsReconnectTimer = null;
989
+ this.connectWebSocket();
990
+ }, delay);
991
+ }
992
+ clearWsReconnect() {
993
+ if (this.wsReconnectTimer) {
994
+ clearTimeout(this.wsReconnectTimer);
995
+ this.wsReconnectTimer = null;
996
+ }
997
+ }
998
+ clearWsPing() {
999
+ if (this.wsPingInterval) {
1000
+ clearInterval(this.wsPingInterval);
1001
+ this.wsPingInterval = null;
1002
+ }
1003
+ }
1004
+ // -------------------------------------------------------------------
1005
+ // Cleanup
1006
+ // -------------------------------------------------------------------
1007
+ /** Disconnect all connections and clean up */
1008
+ destroy() {
1009
+ this.disconnectWebSocket();
1010
+ this.token = null;
1011
+ this.apiKey = null;
1012
+ this.tenantId = null;
1013
+ }
1014
+ }
1015
+ //# sourceMappingURL=client.js.map