@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/__tests__/client.test.d.ts +2 -0
- package/dist/__tests__/client.test.d.ts.map +1 -0
- package/dist/__tests__/client.test.js +289 -0
- package/dist/__tests__/client.test.js.map +1 -0
- package/dist/client.d.ts +684 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1015 -0
- package/dist/client.js.map +1 -0
- package/dist/hooks/index.d.ts +7 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +5 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/useCallStatus.d.ts +38 -0
- package/dist/hooks/useCallStatus.d.ts.map +1 -0
- package/dist/hooks/useCallStatus.js +98 -0
- package/dist/hooks/useCallStatus.js.map +1 -0
- package/dist/hooks/usePresence.d.ts +22 -0
- package/dist/hooks/usePresence.d.ts.map +1 -0
- package/dist/hooks/usePresence.js +91 -0
- package/dist/hooks/usePresence.js.map +1 -0
- package/dist/hooks/useVoIP.d.ts +49 -0
- package/dist/hooks/useVoIP.d.ts.map +1 -0
- package/dist/hooks/useVoIP.js +254 -0
- package/dist/hooks/useVoIP.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +965 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/webrtc.d.ts +100 -0
- package/dist/webrtc.d.ts.map +1 -0
- package/dist/webrtc.js +624 -0
- package/dist/webrtc.js.map +1 -0
- package/package.json +1 -1
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
|