@dora-cell/sdk 1.0.2 → 3.0.0

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 CHANGED
@@ -23011,7 +23011,6 @@ var ApiTokenAuthProvider = class {
23011
23011
  }
23012
23012
  const baseUrl = apiBaseUrl?.replace(/\/$/, "") || "https://api.cell.usedora.com/api";
23013
23013
  try {
23014
- console.log("SDK: Verifying keys at:", `${baseUrl}/sdk/v1/auth/session`);
23015
23014
  const authResponse = await fetch(`${baseUrl}/sdk/v1/auth/session`, {
23016
23015
  method: "POST",
23017
23016
  headers: {
@@ -23033,7 +23032,29 @@ var ApiTokenAuthProvider = class {
23033
23032
  if (!this.sessionToken) {
23034
23033
  throw new AuthenticationError("No session token returned after key verification");
23035
23034
  }
23036
- this.credentials = this.parseCredentials(authData);
23035
+ const origin = typeof window !== "undefined" ? window.location.origin : "";
23036
+ const validateResponse = await fetch(`${baseUrl}/sdk/v1/auth/validate`, {
23037
+ method: "GET",
23038
+ headers: {
23039
+ "Authorization": `Bearer ${this.sessionToken}`,
23040
+ "x-dora-public-key": this.publicKey,
23041
+ "Origin": origin,
23042
+ "Accept": "application/json"
23043
+ }
23044
+ });
23045
+ if (!validateResponse.ok) {
23046
+ throw new AuthenticationError(
23047
+ `Secondary validation failed: ${validateResponse.status}`,
23048
+ { status: validateResponse.status }
23049
+ );
23050
+ }
23051
+ const validateData = await validateResponse.json();
23052
+ console.log(`Dora Cell SDK: Authenticated successfully`);
23053
+ if (validateData.features && !validateData.features.includes("voice")) {
23054
+ console.warn('Dora Cell SDK: App token does not have the "voice" feature enabled.');
23055
+ }
23056
+ const actualResponseData = authData.data && typeof authData.data === "object" ? authData.data : authData;
23057
+ this.credentials = this.parseCredentials(actualResponseData);
23037
23058
  return this.credentials;
23038
23059
  } catch (error) {
23039
23060
  if (error instanceof AuthenticationError) {
@@ -23071,7 +23092,8 @@ var ApiTokenAuthProvider = class {
23071
23092
  if (!sipUri && extensions.length > 0) {
23072
23093
  const ext = extensions[0].extension;
23073
23094
  sipUri = `sip:${ext}@${sipDomain}`;
23074
- console.log(`SDK: Constructed SIP URI from extension: ${sipUri}`);
23095
+ } else if (!sipUri) {
23096
+ sipUri = "";
23075
23097
  }
23076
23098
  return {
23077
23099
  wsUrl,
@@ -23172,6 +23194,7 @@ var CallSession = class {
23172
23194
  // JsSIP RTCSession
23173
23195
  this._isMuted = false;
23174
23196
  this.remoteStreamValue = null;
23197
+ this.lastKnownSSRCs = /* @__PURE__ */ new Set();
23175
23198
  this.id = this.generateCallId();
23176
23199
  this.session = session;
23177
23200
  this.direction = direction;
@@ -23189,6 +23212,9 @@ var CallSession = class {
23189
23212
  if (code === 180 || code === 183) {
23190
23213
  this.status = "ringing";
23191
23214
  this.events.emit("call:ringing", this);
23215
+ if (this.session.connection) {
23216
+ setTimeout(() => this.reattachFromReceivers(this.session.connection), 200);
23217
+ }
23192
23218
  }
23193
23219
  });
23194
23220
  this.session.on("confirmed", () => {
@@ -23196,28 +23222,79 @@ var CallSession = class {
23196
23222
  this.startTime = Date.now();
23197
23223
  this.startDurationTimer();
23198
23224
  this.events.emit("call:connected", this);
23225
+ if (this.session.connection) {
23226
+ this.reattachFromReceivers(this.session.connection);
23227
+ this.startSSRCWatch(this.session.connection);
23228
+ }
23199
23229
  });
23200
23230
  this.session.on("peerconnection", (evt) => {
23201
- evt.peerconnection.ontrack = (event) => {
23231
+ const pc = evt.peerconnection;
23232
+ pc.addEventListener("track", (event) => {
23233
+ setTimeout(() => this.reattachFromReceivers(pc), 150);
23202
23234
  if (event.streams && event.streams[0]) {
23203
23235
  this.remoteStreamValue = event.streams[0];
23236
+ this.events.emit("call:stream", this, event.streams[0]);
23237
+ }
23238
+ });
23239
+ pc.oniceconnectionstatechange = () => {
23240
+ if (pc.iceConnectionState === "connected" || pc.iceConnectionState === "completed") {
23241
+ this.reattachFromReceivers(pc);
23204
23242
  }
23205
23243
  };
23206
23244
  });
23207
23245
  this.session.on("ended", (evt) => {
23246
+ console.log(`Dora Cell SDK: Call ended (${evt?.cause || "Normal"})`);
23208
23247
  this.handleCallEnd(evt?.cause);
23209
23248
  });
23210
23249
  this.session.on("failed", (evt) => {
23250
+ console.warn(`Dora Cell SDK: Call failed (${evt?.cause || "Internal Error"})`);
23211
23251
  this.handleCallEnd(evt?.cause || "Call failed");
23212
23252
  });
23213
23253
  this.session.on("rejected", (evt) => {
23254
+ console.warn(`SDK: Call rejected. Cause: ${evt?.cause || "Rejected"}`);
23214
23255
  this.handleCallEnd(evt?.cause || "Call rejected");
23215
23256
  });
23216
23257
  }
23258
+ startSSRCWatch(pc) {
23259
+ this.stopSSRCWatch();
23260
+ this.lastKnownSSRCs.clear();
23261
+ this.ssrcWatchInterval = window.setInterval(async () => {
23262
+ try {
23263
+ if (!pc) return;
23264
+ const stats = await pc.getStats();
23265
+ stats.forEach((report) => {
23266
+ if (report.type === "inbound-rtp" && report.kind === "audio") {
23267
+ const ssrc = report.ssrc;
23268
+ if (ssrc && !this.lastKnownSSRCs.has(ssrc)) {
23269
+ this.lastKnownSSRCs.add(ssrc);
23270
+ setTimeout(() => this.reattachFromReceivers(pc), 200);
23271
+ }
23272
+ }
23273
+ });
23274
+ } catch (e) {
23275
+ }
23276
+ }, 1e3);
23277
+ }
23278
+ stopSSRCWatch() {
23279
+ if (this.ssrcWatchInterval) {
23280
+ clearInterval(this.ssrcWatchInterval);
23281
+ this.ssrcWatchInterval = void 0;
23282
+ }
23283
+ }
23284
+ reattachFromReceivers(pc) {
23285
+ if (!pc) return;
23286
+ const liveAudioTracks = pc.getReceivers().map((r) => r.track).filter((t) => t && t.kind === "audio" && t.readyState === "live");
23287
+ if (liveAudioTracks.length > 0) {
23288
+ const newStream = new MediaStream(liveAudioTracks);
23289
+ this.remoteStreamValue = newStream;
23290
+ this.events.emit("call:stream", this, newStream);
23291
+ }
23292
+ }
23217
23293
  handleCallEnd(reason) {
23218
23294
  this.status = "ended";
23219
23295
  this.endTime = Date.now();
23220
23296
  this.stopDurationTimer();
23297
+ this.stopSSRCWatch();
23221
23298
  this.remoteStreamValue = null;
23222
23299
  this._isMuted = false;
23223
23300
  this.events.emit("call:ended", this, reason);
@@ -23298,6 +23375,7 @@ var CallManager = class {
23298
23375
  const extension = options?.extension || this.getDefaultExtension();
23299
23376
  const sipDomain = this.extractDomain(this.credentials.sipUri);
23300
23377
  const sipTarget = formatPhoneToSIP(targetNumber, sipDomain);
23378
+ console.log(`Dora Cell SDK: Initiating call to ${targetNumber}...`);
23301
23379
  const session = this.ua.call(sipTarget, {
23302
23380
  mediaConstraints: options?.mediaConstraints || { audio: true },
23303
23381
  pcConfig: this.callConfig.pcConfig
@@ -23324,12 +23402,13 @@ var CallManager = class {
23324
23402
  * Answer the pending incoming call.
23325
23403
  * Uses the stored pendingSession from handleIncomingCall.
23326
23404
  */
23327
- answerCurrentCall() {
23405
+ async answerCurrentCall() {
23328
23406
  const session = this.pendingSession;
23329
23407
  if (!session) {
23330
23408
  throw new CallError("No pending incoming call to answer");
23331
23409
  }
23332
23410
  try {
23411
+ await navigator.mediaDevices.getUserMedia({ audio: true });
23333
23412
  session.answer({
23334
23413
  mediaConstraints: { audio: true },
23335
23414
  pcConfig: this.callConfig.pcConfig
@@ -23374,10 +23453,13 @@ var CallManager = class {
23374
23453
  this.pendingSession = null;
23375
23454
  }
23376
23455
  getDefaultExtension() {
23456
+ if (this.credentials.sipUri) {
23457
+ return this.extractExtension(this.credentials.sipUri);
23458
+ }
23377
23459
  if (this.credentials.extensions && this.credentials.extensions.length > 0) {
23378
23460
  return this.credentials.extensions[0].extension;
23379
23461
  }
23380
- return this.extractExtension(this.credentials.sipUri);
23462
+ return "unknown";
23381
23463
  }
23382
23464
  extractExtension(sipUri) {
23383
23465
  const match = sipUri.match(/sip:([^@]+)@/);
@@ -23461,6 +23543,7 @@ var DoraCell = class {
23461
23543
  this.connectionStatus = "disconnected";
23462
23544
  this.retryCount = 0;
23463
23545
  this.maxRetries = 3;
23546
+ this.userId = null;
23464
23547
  this.config = {
23465
23548
  autoSelectExtension: true,
23466
23549
  debug: false,
@@ -23485,17 +23568,25 @@ var DoraCell = class {
23485
23568
  );
23486
23569
  if (this.authProvider instanceof ApiTokenAuthProvider) {
23487
23570
  const token = this.authProvider.getSessionToken();
23488
- if (token) this.apiClient.setSessionToken(token);
23571
+ if (token) {
23572
+ this.apiClient.setSessionToken(token);
23573
+ }
23574
+ }
23575
+ await this.getWallet().catch(() => {
23576
+ });
23577
+ if (!this.credentials?.extensions || this.credentials.extensions.length === 0) {
23578
+ await this.fetchExtensions();
23489
23579
  }
23490
23580
  if (this.config.autoSelectExtension && this.credentials?.extensions && this.credentials.extensions.length > 0) {
23491
23581
  const primary = this.credentials.extensions.find((e) => e.isPrimary) || this.credentials.extensions[0];
23492
- const domain = this.credentials.sipDomain || "cell.usedora.com";
23582
+ const domain = this.credentials.sipDomain || "64.227.10.164";
23493
23583
  this.credentials.sipUri = `sip:${primary.extension}@${domain}`;
23494
- console.log(`SDK: Auto-selected extension ${primary.extension}`);
23495
23584
  }
23496
- await this.initializeUserAgent();
23497
- this.initializeCallManager();
23498
- await this.waitForRegistration();
23585
+ if (this.credentials?.sipUri) {
23586
+ await this.initializeUserAgent();
23587
+ this.initializeCallManager();
23588
+ await this.waitForRegistration();
23589
+ }
23499
23590
  } catch (error) {
23500
23591
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
23501
23592
  this.emitError(new ConnectionError(`Initialization failed: ${errorMessage}`));
@@ -23529,6 +23620,16 @@ var DoraCell = class {
23529
23620
  if (!this.credentials) {
23530
23621
  throw new ConnectionError("No credentials available");
23531
23622
  }
23623
+ this.connectionStatus = "connecting";
23624
+ this.emitConnectionStatus();
23625
+ if (this.ua) {
23626
+ try {
23627
+ this.ua.stop();
23628
+ } catch (e) {
23629
+ }
23630
+ this.ua = null;
23631
+ await new Promise((resolve) => setTimeout(resolve, 500));
23632
+ }
23532
23633
  try {
23533
23634
  const socket = new import_jssip.default.WebSocketInterface(this.credentials.wsUrl);
23534
23635
  const pcConfig = {
@@ -23540,16 +23641,18 @@ var DoraCell = class {
23540
23641
  sockets: [socket],
23541
23642
  register: true,
23542
23643
  display_name: this.getDisplayName(),
23543
- sessionTimers: false,
23644
+ sessionTimers: true,
23645
+ session_timers_refresh_method: "UPDATE",
23544
23646
  trickleIce: false,
23545
23647
  pcConfig,
23546
23648
  instance_id: this.generateInstanceId()
23547
23649
  };
23548
- console.log("SDK: Initializing UA with config:", { ...uaConfig, password: "***" });
23549
23650
  this.ua = new import_jssip.default.UA(uaConfig);
23550
23651
  this.setupUserAgentHandlers();
23551
- console.log("SDK: Starting UA...");
23552
23652
  this.ua.start();
23653
+ if (this.callManager) {
23654
+ this.callManager.setUserAgent(this.ua);
23655
+ }
23553
23656
  } catch (error) {
23554
23657
  throw new ConnectionError(
23555
23658
  `Failed to initialize User Agent: ${error instanceof Error ? error.message : "Unknown error"}`
@@ -23569,6 +23672,7 @@ var DoraCell = class {
23569
23672
  this.ua.on("registered", () => {
23570
23673
  this.connectionStatus = "registered";
23571
23674
  this.retryCount = 0;
23675
+ console.log(`Dora Cell SDK: Connected (${this.getDisplayName()})`);
23572
23676
  this.emitConnectionStatus();
23573
23677
  });
23574
23678
  this.ua.on("registrationFailed", (e) => {
@@ -23583,9 +23687,7 @@ var DoraCell = class {
23583
23687
  });
23584
23688
  this.ua.on("newRTCSession", (e) => {
23585
23689
  const session = e.session;
23586
- console.log(`SDK: New session detected (${session.direction}):`, session.remote_identity?.uri?.toString());
23587
23690
  if (session.direction === "incoming") {
23588
- console.log("SDK: Handling incoming call event");
23589
23691
  this.callManager?.handleIncomingCall(session);
23590
23692
  }
23591
23693
  });
@@ -23619,7 +23721,7 @@ var DoraCell = class {
23619
23721
  /**
23620
23722
  * Answer an incoming call
23621
23723
  */
23622
- answerCall() {
23724
+ async answerCall() {
23623
23725
  const currentCall = this.callManager?.getCurrentCall();
23624
23726
  if (!currentCall) {
23625
23727
  throw new CallError("No incoming call to answer");
@@ -23627,7 +23729,7 @@ var DoraCell = class {
23627
23729
  if (currentCall.direction !== "inbound") {
23628
23730
  throw new CallError("Current call is not an incoming call");
23629
23731
  }
23630
- this.callManager.answerCurrentCall();
23732
+ await this.callManager.answerCurrentCall();
23631
23733
  }
23632
23734
  /**
23633
23735
  * Hangup the current call
@@ -23655,7 +23757,6 @@ var DoraCell = class {
23655
23757
  return { balance: 0, currency: "NGN" };
23656
23758
  }
23657
23759
  try {
23658
- console.log("SDK: Fetching wallet balance...");
23659
23760
  const response = await this.apiClient.get("/wallets");
23660
23761
  const wallets = Array.isArray(response) ? response : response.data || [];
23661
23762
  if (wallets.length === 0) {
@@ -23663,11 +23764,11 @@ var DoraCell = class {
23663
23764
  return { balance: 0, currency: "NGN" };
23664
23765
  }
23665
23766
  const primary = wallets[0];
23767
+ this.userId = primary.user_id || null;
23666
23768
  const result = {
23667
- balance: parseFloat(primary.balance || "0"),
23769
+ balance: parseFloat(primary.balance || primary.amount || "0"),
23668
23770
  currency: primary.currency || "NGN"
23669
23771
  };
23670
- console.log("SDK: Wallet balance fetched:", result);
23671
23772
  return result;
23672
23773
  } catch (error) {
23673
23774
  console.error("SDK: Failed to fetch wallet:", error);
@@ -23688,14 +23789,14 @@ var DoraCell = class {
23688
23789
  throw new Error("SDK not authenticated. Call initialize() first.");
23689
23790
  }
23690
23791
  try {
23691
- const response = await this.apiClient.get("/extensions");
23792
+ const path = this.userId ? `/user/${this.userId}/extensions` : "/extensions";
23793
+ const response = await this.apiClient.get(path);
23692
23794
  const extensions = response.data || response.extensions || response;
23693
23795
  if (this.credentials && Array.isArray(extensions)) {
23694
23796
  this.credentials.extensions = extensions;
23695
23797
  }
23696
23798
  return Array.isArray(extensions) ? extensions : [];
23697
23799
  } catch (error) {
23698
- console.error("SDK: Failed to fetch extensions:", error);
23699
23800
  return this.credentials?.extensions || [];
23700
23801
  }
23701
23802
  }
@@ -23709,11 +23810,11 @@ var DoraCell = class {
23709
23810
  * Update active extension and re-initialize SIP connection
23710
23811
  */
23711
23812
  async setExtension(extension) {
23712
- console.log(`SDK: Switching to extension ${extension}...`);
23713
23813
  if (this.credentials) {
23714
- const domain = this.credentials.sipDomain || "cell.usedora.com";
23814
+ const domain = this.credentials.sipDomain || "64.227.10.164";
23715
23815
  this.credentials.sipUri = `sip:${extension}@${domain}`;
23716
23816
  await this.initializeUserAgent();
23817
+ await this.waitForRegistration();
23717
23818
  this.emitConnectionStatus(this.connectionStatus);
23718
23819
  }
23719
23820
  }
@@ -23747,10 +23848,14 @@ var DoraCell = class {
23747
23848
  }
23748
23849
  // Helper methods
23749
23850
  emitConnectionStatus(error) {
23750
- const extension = this.credentials?.extensions?.[0]?.extension || (this.credentials?.sipUri ? extractNumberFromSipUri(this.credentials.sipUri) : void 0);
23851
+ let activeExt = this.credentials?.sipUri ? extractNumberFromSipUri(this.credentials.sipUri) : void 0;
23852
+ if (!activeExt && this.credentials?.extensions && this.credentials.extensions.length > 0) {
23853
+ const primary = this.credentials.extensions.find((e) => e.isPrimary) || this.credentials.extensions[0];
23854
+ activeExt = primary.extension;
23855
+ }
23751
23856
  const state = {
23752
23857
  status: this.connectionStatus,
23753
- extension,
23858
+ extension: activeExt,
23754
23859
  error
23755
23860
  };
23756
23861
  this.events.emit("connection:status", state);
@@ -23776,6 +23881,12 @@ var DoraCell = class {
23776
23881
  ];
23777
23882
  }
23778
23883
  getDisplayName() {
23884
+ const currentExt = this.credentials?.sipUri ? extractNumberFromSipUri(this.credentials.sipUri) : null;
23885
+ if (currentExt && this.credentials?.extensions) {
23886
+ const found = this.credentials.extensions.find((e) => e.extension === currentExt);
23887
+ if (found?.displayName) return found.displayName;
23888
+ if (found?.extension) return `Ext ${found.extension}`;
23889
+ }
23779
23890
  if (this.credentials?.extensions?.[0]?.displayName) {
23780
23891
  return this.credentials.extensions[0].displayName;
23781
23892
  }
@@ -23798,7 +23909,7 @@ var DoraCell = class {
23798
23909
  case "staging":
23799
23910
  case "dev":
23800
23911
  return "https://dev.api.cell.usedora.com/api";
23801
- case "production":
23912
+ default:
23802
23913
  return "https://api.cell.usedora.com/api";
23803
23914
  }
23804
23915
  }