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