@dora-cell/sdk 0.1.1-beta.6

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/index.js ADDED
@@ -0,0 +1,664 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var JsSIP = require('jssip');
6
+ var EventEmitter = require('eventemitter3');
7
+
8
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
+
10
+ var JsSIP__default = /*#__PURE__*/_interopDefault(JsSIP);
11
+ var EventEmitter__default = /*#__PURE__*/_interopDefault(EventEmitter);
12
+
13
+ // src/core/DoraCell.ts
14
+
15
+ // src/types/index.ts
16
+ var DoraCellError = class extends Error {
17
+ constructor(message, code, details) {
18
+ super(message);
19
+ this.code = code;
20
+ this.details = details;
21
+ this.name = "DoraCellError";
22
+ }
23
+ };
24
+ var AuthenticationError = class extends DoraCellError {
25
+ constructor(message, details) {
26
+ super(message, "AUTH_ERROR", details);
27
+ this.name = "AuthenticationError";
28
+ }
29
+ };
30
+ var CallError = class extends DoraCellError {
31
+ constructor(message, details) {
32
+ super(message, "CALL_ERROR", details);
33
+ this.name = "CallError";
34
+ }
35
+ };
36
+ var ConnectionError = class extends DoraCellError {
37
+ constructor(message, details) {
38
+ super(message, "CONNECTION_ERROR", details);
39
+ this.name = "ConnectionError";
40
+ }
41
+ };
42
+
43
+ // src/core/AuthProvider.ts
44
+ var ApiTokenAuthProvider = class {
45
+ constructor(apiToken, apiBaseUrl) {
46
+ this.credentials = null;
47
+ this.apiToken = apiToken;
48
+ this.apiBaseUrl = apiBaseUrl || process.env.NEXT_PUBLIC_BASE_API_URL || "";
49
+ }
50
+ async authenticate(config) {
51
+ if (config.type !== "api-token") {
52
+ throw new AuthenticationError("Invalid auth config type for ApiTokenAuthProvider");
53
+ }
54
+ try {
55
+ const response = await fetch(`${this.apiBaseUrl}/api/sip-credentials`, {
56
+ method: "GET",
57
+ headers: {
58
+ "Authorization": `Bearer ${this.apiToken}`,
59
+ "Content-Type": "application/json",
60
+ "Accept": "application/json"
61
+ },
62
+ credentials: "include"
63
+ });
64
+ if (!response.ok) {
65
+ if (response.status === 401 || response.status === 403) {
66
+ throw new AuthenticationError(
67
+ "Invalid API token or insufficient permissions",
68
+ { status: response.status }
69
+ );
70
+ }
71
+ throw new AuthenticationError(
72
+ `Failed to fetch SIP credentials: ${response.status}`,
73
+ { status: response.status }
74
+ );
75
+ }
76
+ const data = await response.json();
77
+ this.credentials = this.parseCredentials(data);
78
+ return this.credentials;
79
+ } catch (error) {
80
+ if (error instanceof AuthenticationError) {
81
+ throw error;
82
+ }
83
+ throw new AuthenticationError(
84
+ `Authentication failed: ${error instanceof Error ? error.message : "Unknown error"}`,
85
+ { originalError: error }
86
+ );
87
+ }
88
+ }
89
+ async refreshCredentials() {
90
+ return this.authenticate({ type: "api-token", apiToken: this.apiToken });
91
+ }
92
+ isAuthenticated() {
93
+ return this.credentials !== null;
94
+ }
95
+ parseCredentials(data) {
96
+ const wsUrl = data.ws_url || data.wsUrl;
97
+ const sipUri = data.sip_uri || data.sipUri;
98
+ const password = data.password;
99
+ const sipDomain = data.sip_domain || data.sipDomain;
100
+ const extensions = data.extensions;
101
+ const expiresIn = data.expires_in || data.expiresIn;
102
+ if (!wsUrl || !sipUri) {
103
+ throw new AuthenticationError(
104
+ "Invalid credentials response: missing ws_url or sip_uri"
105
+ );
106
+ }
107
+ return {
108
+ wsUrl,
109
+ sipUri,
110
+ password: password || "",
111
+ sipDomain,
112
+ extensions,
113
+ expiresIn
114
+ };
115
+ }
116
+ };
117
+ var DirectCredentialsAuthProvider = class {
118
+ constructor() {
119
+ this.credentials = null;
120
+ }
121
+ async authenticate(config) {
122
+ if (config.type !== "direct") {
123
+ throw new AuthenticationError("Invalid auth config type for DirectCredentialsAuthProvider");
124
+ }
125
+ this.credentials = {
126
+ wsUrl: config.wsUrl,
127
+ sipUri: config.sipUri,
128
+ password: config.password
129
+ };
130
+ return this.credentials;
131
+ }
132
+ isAuthenticated() {
133
+ return this.credentials !== null;
134
+ }
135
+ };
136
+ function createAuthProvider(config) {
137
+ switch (config.type) {
138
+ case "api-token":
139
+ return new ApiTokenAuthProvider(config.apiToken, config.apiBaseUrl);
140
+ case "direct":
141
+ return new DirectCredentialsAuthProvider();
142
+ default:
143
+ throw new AuthenticationError("Unknown authentication type");
144
+ }
145
+ }
146
+
147
+ // src/utils/phoneFormatter.ts
148
+ function formatPhoneToSIP(phone, sipDomain) {
149
+ let cleaned = phone.replace(/\D/g, "");
150
+ if (cleaned.length <= 4) {
151
+ return `sip:${cleaned}@${sipDomain}`;
152
+ }
153
+ if (cleaned.startsWith("0")) {
154
+ cleaned = "234" + cleaned.substring(1);
155
+ return `sip:${cleaned}@${sipDomain}`;
156
+ }
157
+ if (cleaned.startsWith("234")) {
158
+ return `sip:${cleaned}@${sipDomain}`;
159
+ }
160
+ cleaned = "234" + cleaned;
161
+ return `sip:${cleaned}@${sipDomain}`;
162
+ }
163
+ function normalizePhoneNumber(phone, countryCode = "234") {
164
+ let cleaned = phone.replace(/\D/g, "");
165
+ if (cleaned.length <= 4) {
166
+ return cleaned;
167
+ }
168
+ if (cleaned.startsWith("0")) {
169
+ return countryCode + cleaned.substring(1);
170
+ }
171
+ if (cleaned.startsWith(countryCode)) {
172
+ return cleaned;
173
+ }
174
+ return countryCode + cleaned;
175
+ }
176
+ function extractNumberFromSipUri(sipUri) {
177
+ const match = sipUri.match(/sip:([^@]+)@/);
178
+ return match ? match[1] : sipUri;
179
+ }
180
+ function isValidPhoneNumber(phone, minLength = 3) {
181
+ const cleaned = phone.replace(/\D/g, "");
182
+ return cleaned.length >= minLength;
183
+ }
184
+
185
+ // src/core/CallManager.ts
186
+ var CallSession = class {
187
+ constructor(session, direction, remoteNumber, localExtension, events) {
188
+ this.status = "idle";
189
+ this.duration = 0;
190
+ // JsSIP RTCSession
191
+ this._isMuted = false;
192
+ this.remoteStreamValue = null;
193
+ this.id = this.generateCallId();
194
+ this.session = session;
195
+ this.direction = direction;
196
+ this.remoteNumber = remoteNumber;
197
+ this.localExtension = localExtension;
198
+ this.events = events;
199
+ this.setupSessionHandlers();
200
+ }
201
+ generateCallId() {
202
+ return `call_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
203
+ }
204
+ setupSessionHandlers() {
205
+ this.session.on("progress", (evt) => {
206
+ const code = evt.response?.status_code;
207
+ if (code === 180 || code === 183) {
208
+ this.status = "ringing";
209
+ this.events.emit("call:ringing", this);
210
+ }
211
+ });
212
+ this.session.on("confirmed", () => {
213
+ this.status = "ongoing";
214
+ this.startTime = Date.now();
215
+ this.startDurationTimer();
216
+ this.events.emit("call:connected", this);
217
+ });
218
+ this.session.on("peerconnection", (evt) => {
219
+ evt.peerconnection.ontrack = (event) => {
220
+ if (event.streams && event.streams[0]) {
221
+ this.remoteStreamValue = event.streams[0];
222
+ }
223
+ };
224
+ });
225
+ this.session.on("ended", (evt) => {
226
+ this.handleCallEnd(evt?.cause);
227
+ });
228
+ this.session.on("failed", (evt) => {
229
+ this.handleCallEnd(evt?.cause || "Call failed");
230
+ });
231
+ this.session.on("rejected", (evt) => {
232
+ this.handleCallEnd(evt?.cause || "Call rejected");
233
+ });
234
+ }
235
+ handleCallEnd(reason) {
236
+ this.status = "ended";
237
+ this.endTime = Date.now();
238
+ this.stopDurationTimer();
239
+ this.remoteStreamValue = null;
240
+ this._isMuted = false;
241
+ this.events.emit("call:ended", this, reason);
242
+ }
243
+ startDurationTimer() {
244
+ this.durationInterval = window.setInterval(() => {
245
+ if (this.startTime) {
246
+ this.duration = Math.floor((Date.now() - this.startTime) / 1e3);
247
+ }
248
+ }, 1e3);
249
+ }
250
+ stopDurationTimer() {
251
+ if (this.durationInterval) {
252
+ clearInterval(this.durationInterval);
253
+ this.durationInterval = void 0;
254
+ }
255
+ }
256
+ // Public methods
257
+ mute() {
258
+ if (this.session && !this._isMuted) {
259
+ this.session.mute({ audio: true });
260
+ this._isMuted = true;
261
+ }
262
+ }
263
+ unmute() {
264
+ if (this.session && this._isMuted) {
265
+ this.session.unmute({ audio: true });
266
+ this._isMuted = false;
267
+ }
268
+ }
269
+ hangup() {
270
+ if (this.session) {
271
+ this.session.terminate();
272
+ }
273
+ }
274
+ isMuted() {
275
+ return this._isMuted;
276
+ }
277
+ getRemoteStream() {
278
+ return this.remoteStreamValue;
279
+ }
280
+ getFormattedDuration() {
281
+ const mm = String(Math.floor(this.duration / 60)).padStart(2, "0");
282
+ const ss = String(this.duration % 60).padStart(2, "0");
283
+ return `${mm}:${ss}`;
284
+ }
285
+ destroy() {
286
+ this.stopDurationTimer();
287
+ this.session = null;
288
+ this.remoteStreamValue = null;
289
+ }
290
+ };
291
+ var CallManager = class {
292
+ constructor(credentials, events, callConfig) {
293
+ this.ua = null;
294
+ // JsSIP User Agent
295
+ this.currentCall = null;
296
+ this.credentials = credentials;
297
+ this.events = events;
298
+ this.callConfig = callConfig;
299
+ }
300
+ setUserAgent(ua) {
301
+ this.ua = ua;
302
+ }
303
+ /**
304
+ * Initiate an outbound call
305
+ */
306
+ async initiateCall(targetNumber, options) {
307
+ if (!this.ua) {
308
+ throw new CallError("User Agent not initialized");
309
+ }
310
+ if (this.currentCall && this.currentCall.status !== "ended") {
311
+ throw new CallError("A call is already in progress");
312
+ }
313
+ try {
314
+ await navigator.mediaDevices.getUserMedia({ audio: true });
315
+ const extension = options?.extension || this.getDefaultExtension();
316
+ const sipDomain = this.extractDomain(this.credentials.sipUri);
317
+ const sipTarget = formatPhoneToSIP(targetNumber, sipDomain);
318
+ const session = this.ua.call(sipTarget, {
319
+ mediaConstraints: options?.mediaConstraints || { audio: true },
320
+ pcConfig: this.callConfig.pcConfig
321
+ });
322
+ const call = new CallSession(
323
+ session,
324
+ "outbound",
325
+ targetNumber,
326
+ extension,
327
+ this.events
328
+ );
329
+ call.status = "connecting";
330
+ this.currentCall = call;
331
+ this.events.emit("call:outgoing", call);
332
+ return call;
333
+ } catch (error) {
334
+ throw new CallError(
335
+ `Failed to initiate call: ${error instanceof Error ? error.message : "Unknown error"}`,
336
+ { originalError: error }
337
+ );
338
+ }
339
+ }
340
+ /**
341
+ * Answer an incoming call
342
+ */
343
+ answerCall(session) {
344
+ if (!session) {
345
+ throw new CallError("No session provided");
346
+ }
347
+ try {
348
+ const remoteUri = session.remote_identity?.uri?.toString() || "Unknown";
349
+ const extension = this.getDefaultExtension();
350
+ session.answer({
351
+ mediaConstraints: { audio: true },
352
+ pcConfig: this.callConfig.pcConfig
353
+ });
354
+ const call = new CallSession(
355
+ session,
356
+ "inbound",
357
+ remoteUri,
358
+ extension,
359
+ this.events
360
+ );
361
+ this.currentCall = call;
362
+ return call;
363
+ } catch (error) {
364
+ throw new CallError(
365
+ `Failed to answer call: ${error instanceof Error ? error.message : "Unknown error"}`,
366
+ { originalError: error }
367
+ );
368
+ }
369
+ }
370
+ /**
371
+ * Handle incoming call from JsSIP
372
+ */
373
+ handleIncomingCall(session) {
374
+ const remoteUri = session.remote_identity?.uri?.toString() || "Unknown";
375
+ const extension = this.getDefaultExtension();
376
+ const call = new CallSession(
377
+ session,
378
+ "inbound",
379
+ remoteUri,
380
+ extension,
381
+ this.events
382
+ );
383
+ call.status = "ringing";
384
+ this.currentCall = call;
385
+ this.events.emit("call:incoming", call);
386
+ return call;
387
+ }
388
+ getCurrentCall() {
389
+ return this.currentCall;
390
+ }
391
+ clearCurrentCall() {
392
+ if (this.currentCall) {
393
+ this.currentCall.destroy();
394
+ this.currentCall = null;
395
+ }
396
+ }
397
+ getDefaultExtension() {
398
+ if (this.credentials.extensions && this.credentials.extensions.length > 0) {
399
+ return this.credentials.extensions[0].extension;
400
+ }
401
+ return this.extractExtension(this.credentials.sipUri);
402
+ }
403
+ extractExtension(sipUri) {
404
+ const match = sipUri.match(/sip:([^@]+)@/);
405
+ return match ? match[1] : "unknown";
406
+ }
407
+ extractDomain(sipUri) {
408
+ const match = sipUri.match(/@(.+)$/);
409
+ return match ? match[1] : "";
410
+ }
411
+ destroy() {
412
+ this.clearCurrentCall();
413
+ this.ua = null;
414
+ }
415
+ };
416
+
417
+ // src/core/DoraCell.ts
418
+ var DoraCell = class {
419
+ constructor(config) {
420
+ this.credentials = null;
421
+ this.ua = null;
422
+ // JsSIP User Agent
423
+ this.callManager = null;
424
+ this.connectionStatus = "disconnected";
425
+ this.retryCount = 0;
426
+ this.maxRetries = 3;
427
+ this.config = {
428
+ autoSelectExtension: true,
429
+ debug: false,
430
+ ...config
431
+ };
432
+ this.events = new EventEmitter__default.default();
433
+ this.authProvider = createAuthProvider(config.auth);
434
+ if (this.config.debug) {
435
+ JsSIP__default.default.debug.enable("JsSIP:*");
436
+ }
437
+ }
438
+ /**
439
+ * Initialize the SDK - authenticate and connect to SIP server
440
+ */
441
+ async initialize() {
442
+ try {
443
+ this.credentials = await this.authProvider.authenticate(this.config.auth);
444
+ await this.initializeUserAgent();
445
+ this.initializeCallManager();
446
+ } catch (error) {
447
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
448
+ this.emitError(new ConnectionError(`Initialization failed: ${errorMessage}`));
449
+ throw error;
450
+ }
451
+ }
452
+ async initializeUserAgent() {
453
+ if (!this.credentials) {
454
+ throw new ConnectionError("No credentials available");
455
+ }
456
+ try {
457
+ const socket = new JsSIP__default.default.WebSocketInterface(this.credentials.wsUrl);
458
+ const pcConfig = {
459
+ iceServers: this.config.turnServers || this.getDefaultTurnServers()
460
+ };
461
+ const uaConfig = {
462
+ uri: this.credentials.sipUri,
463
+ password: this.credentials.password,
464
+ sockets: [socket],
465
+ register: true,
466
+ display_name: this.getDisplayName(),
467
+ session_timers: false,
468
+ use_preloaded_route: false,
469
+ pcConfig,
470
+ instance_id: this.generateInstanceId()
471
+ };
472
+ this.ua = new JsSIP__default.default.UA(uaConfig);
473
+ this.setupUserAgentHandlers();
474
+ this.ua.start();
475
+ } catch (error) {
476
+ throw new ConnectionError(
477
+ `Failed to initialize User Agent: ${error instanceof Error ? error.message : "Unknown error"}`
478
+ );
479
+ }
480
+ }
481
+ setupUserAgentHandlers() {
482
+ if (!this.ua) return;
483
+ this.ua.on("connected", () => {
484
+ this.connectionStatus = "connected";
485
+ this.emitConnectionStatus();
486
+ });
487
+ this.ua.on("disconnected", () => {
488
+ this.connectionStatus = "disconnected";
489
+ this.emitConnectionStatus();
490
+ });
491
+ this.ua.on("registered", () => {
492
+ this.connectionStatus = "registered";
493
+ this.retryCount = 0;
494
+ this.emitConnectionStatus();
495
+ });
496
+ this.ua.on("registrationFailed", (e) => {
497
+ this.connectionStatus = "registrationFailed";
498
+ this.emitConnectionStatus(e.cause);
499
+ if (this.retryCount < this.maxRetries) {
500
+ this.retryCount++;
501
+ setTimeout(() => {
502
+ this.ua?.register();
503
+ }, 3e3);
504
+ }
505
+ });
506
+ this.ua.on("newRTCSession", (e) => {
507
+ const session = e.session;
508
+ if (session.direction === "incoming") {
509
+ this.callManager?.handleIncomingCall(session);
510
+ }
511
+ });
512
+ }
513
+ initializeCallManager() {
514
+ if (!this.credentials) {
515
+ throw new ConnectionError("No credentials available for call manager");
516
+ }
517
+ const pcConfig = {
518
+ iceServers: this.config.turnServers || this.getDefaultTurnServers()
519
+ };
520
+ this.callManager = new CallManager(
521
+ this.credentials,
522
+ this.events,
523
+ { pcConfig }
524
+ );
525
+ this.callManager.setUserAgent(this.ua);
526
+ }
527
+ /**
528
+ * Make an outbound call
529
+ */
530
+ async call(phoneNumber, options) {
531
+ if (!this.callManager) {
532
+ throw new CallError("SDK not initialized. Call initialize() first.");
533
+ }
534
+ if (this.connectionStatus !== "registered") {
535
+ throw new CallError("Not registered to SIP server");
536
+ }
537
+ return this.callManager.initiateCall(phoneNumber, options);
538
+ }
539
+ /**
540
+ * Answer an incoming call
541
+ */
542
+ answerCall() {
543
+ const currentCall = this.callManager?.getCurrentCall();
544
+ if (!currentCall) {
545
+ throw new CallError("No incoming call to answer");
546
+ }
547
+ if (currentCall.direction !== "inbound") {
548
+ throw new CallError("Current call is not an incoming call");
549
+ }
550
+ }
551
+ /**
552
+ * Hangup the current call
553
+ */
554
+ hangup() {
555
+ const currentCall = this.callManager?.getCurrentCall();
556
+ if (!currentCall) {
557
+ throw new CallError("No active call to hang up");
558
+ }
559
+ currentCall.hangup();
560
+ }
561
+ /**
562
+ * Get current call
563
+ */
564
+ getCurrentCall() {
565
+ return this.callManager?.getCurrentCall() || null;
566
+ }
567
+ /**
568
+ * Get connection status
569
+ */
570
+ getStatus() {
571
+ return this.connectionStatus;
572
+ }
573
+ /**
574
+ * Get available extensions
575
+ */
576
+ getExtensions() {
577
+ if (!this.credentials?.extensions) {
578
+ return [];
579
+ }
580
+ return this.credentials.extensions.map((ext) => ext.extension);
581
+ }
582
+ /**
583
+ * Event listener management
584
+ */
585
+ on(event, handler) {
586
+ this.events.on(event, handler);
587
+ }
588
+ off(event, handler) {
589
+ this.events.off(event, handler);
590
+ }
591
+ once(event, handler) {
592
+ this.events.once(event, handler);
593
+ }
594
+ /**
595
+ * Cleanup and destroy the SDK instance
596
+ */
597
+ destroy() {
598
+ if (this.ua) {
599
+ this.ua.stop();
600
+ this.ua = null;
601
+ }
602
+ if (this.callManager) {
603
+ this.callManager.destroy();
604
+ this.callManager = null;
605
+ }
606
+ this.events.removeAllListeners();
607
+ this.connectionStatus = "disconnected";
608
+ this.credentials = null;
609
+ }
610
+ // Helper methods
611
+ emitConnectionStatus(error) {
612
+ const state = {
613
+ status: this.connectionStatus,
614
+ extension: this.credentials?.extensions?.[0]?.extension,
615
+ error
616
+ };
617
+ this.events.emit("connection:status", state);
618
+ }
619
+ emitError(error) {
620
+ this.events.emit("error", error);
621
+ }
622
+ getDefaultTurnServers() {
623
+ const turnUri = process.env.NEXT_PUBLIC_TURN_URI || "turn:64.227.10.164:3478";
624
+ const turnUser = process.env.NEXT_PUBLIC_TURN_USER || "02018890089";
625
+ const turnPass = process.env.NEXT_PUBLIC_TURN_PASS || "dora12345";
626
+ return [
627
+ {
628
+ urls: turnUri,
629
+ username: turnUser,
630
+ credential: turnPass
631
+ }
632
+ ];
633
+ }
634
+ getDisplayName() {
635
+ if (this.credentials?.extensions?.[0]?.displayName) {
636
+ return this.credentials.extensions[0].displayName;
637
+ }
638
+ if (this.credentials?.extensions?.[0]?.extension) {
639
+ return `Ext ${this.credentials.extensions[0].extension}`;
640
+ }
641
+ return "Dora Cell User";
642
+ }
643
+ generateInstanceId() {
644
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
645
+ const r = Math.random() * 16 | 0;
646
+ const v = c === "x" ? r : r & 3 | 8;
647
+ return v.toString(16);
648
+ });
649
+ }
650
+ };
651
+
652
+ exports.AuthenticationError = AuthenticationError;
653
+ exports.CallError = CallError;
654
+ exports.CallSession = CallSession;
655
+ exports.ConnectionError = ConnectionError;
656
+ exports.DoraCell = DoraCell;
657
+ exports.DoraCellError = DoraCellError;
658
+ exports.default = DoraCell;
659
+ exports.extractNumberFromSipUri = extractNumberFromSipUri;
660
+ exports.formatPhoneToSIP = formatPhoneToSIP;
661
+ exports.isValidPhoneNumber = isValidPhoneNumber;
662
+ exports.normalizePhoneNumber = normalizePhoneNumber;
663
+ //# sourceMappingURL=index.js.map
664
+ //# sourceMappingURL=index.js.map