@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.d.mts CHANGED
@@ -79,6 +79,7 @@ type DoraCellEventMap = {
79
79
  'call:connected': (call: Call) => void;
80
80
  'call:ended': (call: Call, reason?: string) => void;
81
81
  'call:failed': (call: Call, error: string) => void;
82
+ 'call:stream': (call: Call, stream: MediaStream) => void;
82
83
  'error': (error: Error) => void;
83
84
  };
84
85
  type DoraCellEvent = keyof DoraCellEventMap;
@@ -114,6 +115,7 @@ declare class DoraCell {
114
115
  private connectionStatus;
115
116
  private retryCount;
116
117
  private maxRetries;
118
+ private userId;
117
119
  constructor(config: DoraCellConfig);
118
120
  /**
119
121
  * Initialize the SDK - authenticate and connect to SIP server
@@ -130,7 +132,7 @@ declare class DoraCell {
130
132
  /**
131
133
  * Answer an incoming call
132
134
  */
133
- answerCall(): void;
135
+ answerCall(): Promise<void>;
134
136
  /**
135
137
  * Hangup the current call
136
138
  */
@@ -199,9 +201,14 @@ declare class CallSession implements Call {
199
201
  private remoteStreamValue;
200
202
  private durationInterval?;
201
203
  private events;
204
+ private ssrcWatchInterval?;
205
+ private lastKnownSSRCs;
202
206
  constructor(session: any, direction: CallDirection, remoteNumber: string, localExtension: string, events: EventEmitter);
203
207
  private generateCallId;
204
208
  private setupSessionHandlers;
209
+ private startSSRCWatch;
210
+ private stopSSRCWatch;
211
+ private reattachFromReceivers;
205
212
  private handleCallEnd;
206
213
  private startDurationTimer;
207
214
  private stopDurationTimer;
package/dist/index.d.ts CHANGED
@@ -79,6 +79,7 @@ type DoraCellEventMap = {
79
79
  'call:connected': (call: Call) => void;
80
80
  'call:ended': (call: Call, reason?: string) => void;
81
81
  'call:failed': (call: Call, error: string) => void;
82
+ 'call:stream': (call: Call, stream: MediaStream) => void;
82
83
  'error': (error: Error) => void;
83
84
  };
84
85
  type DoraCellEvent = keyof DoraCellEventMap;
@@ -114,6 +115,7 @@ declare class DoraCell {
114
115
  private connectionStatus;
115
116
  private retryCount;
116
117
  private maxRetries;
118
+ private userId;
117
119
  constructor(config: DoraCellConfig);
118
120
  /**
119
121
  * Initialize the SDK - authenticate and connect to SIP server
@@ -130,7 +132,7 @@ declare class DoraCell {
130
132
  /**
131
133
  * Answer an incoming call
132
134
  */
133
- answerCall(): void;
135
+ answerCall(): Promise<void>;
134
136
  /**
135
137
  * Hangup the current call
136
138
  */
@@ -199,9 +201,14 @@ declare class CallSession implements Call {
199
201
  private remoteStreamValue;
200
202
  private durationInterval?;
201
203
  private events;
204
+ private ssrcWatchInterval?;
205
+ private lastKnownSSRCs;
202
206
  constructor(session: any, direction: CallDirection, remoteNumber: string, localExtension: string, events: EventEmitter);
203
207
  private generateCallId;
204
208
  private setupSessionHandlers;
209
+ private startSSRCWatch;
210
+ private stopSSRCWatch;
211
+ private reattachFromReceivers;
205
212
  private handleCallEnd;
206
213
  private startDurationTimer;
207
214
  private stopDurationTimer;
package/dist/index.js CHANGED
@@ -23015,7 +23015,6 @@ var ApiTokenAuthProvider = class {
23015
23015
  }
23016
23016
  const baseUrl = apiBaseUrl?.replace(/\/$/, "") || "https://api.cell.usedora.com/api";
23017
23017
  try {
23018
- console.log("SDK: Verifying keys at:", `${baseUrl}/sdk/v1/auth/session`);
23019
23018
  const authResponse = await fetch(`${baseUrl}/sdk/v1/auth/session`, {
23020
23019
  method: "POST",
23021
23020
  headers: {
@@ -23037,7 +23036,29 @@ var ApiTokenAuthProvider = class {
23037
23036
  if (!this.sessionToken) {
23038
23037
  throw new AuthenticationError("No session token returned after key verification");
23039
23038
  }
23040
- this.credentials = this.parseCredentials(authData);
23039
+ const origin = typeof window !== "undefined" ? window.location.origin : "";
23040
+ const validateResponse = await fetch(`${baseUrl}/sdk/v1/auth/validate`, {
23041
+ method: "GET",
23042
+ headers: {
23043
+ "Authorization": `Bearer ${this.sessionToken}`,
23044
+ "x-dora-public-key": this.publicKey,
23045
+ "Origin": origin,
23046
+ "Accept": "application/json"
23047
+ }
23048
+ });
23049
+ if (!validateResponse.ok) {
23050
+ throw new AuthenticationError(
23051
+ `Secondary validation failed: ${validateResponse.status}`,
23052
+ { status: validateResponse.status }
23053
+ );
23054
+ }
23055
+ const validateData = await validateResponse.json();
23056
+ console.log(`Dora Cell SDK: Authenticated successfully`);
23057
+ if (validateData.features && !validateData.features.includes("voice")) {
23058
+ console.warn('Dora Cell SDK: App token does not have the "voice" feature enabled.');
23059
+ }
23060
+ const actualResponseData = authData.data && typeof authData.data === "object" ? authData.data : authData;
23061
+ this.credentials = this.parseCredentials(actualResponseData);
23041
23062
  return this.credentials;
23042
23063
  } catch (error) {
23043
23064
  if (error instanceof AuthenticationError) {
@@ -23075,7 +23096,8 @@ var ApiTokenAuthProvider = class {
23075
23096
  if (!sipUri && extensions.length > 0) {
23076
23097
  const ext = extensions[0].extension;
23077
23098
  sipUri = `sip:${ext}@${sipDomain}`;
23078
- console.log(`SDK: Constructed SIP URI from extension: ${sipUri}`);
23099
+ } else if (!sipUri) {
23100
+ sipUri = "";
23079
23101
  }
23080
23102
  return {
23081
23103
  wsUrl,
@@ -23176,6 +23198,7 @@ var CallSession = class {
23176
23198
  // JsSIP RTCSession
23177
23199
  this._isMuted = false;
23178
23200
  this.remoteStreamValue = null;
23201
+ this.lastKnownSSRCs = /* @__PURE__ */ new Set();
23179
23202
  this.id = this.generateCallId();
23180
23203
  this.session = session;
23181
23204
  this.direction = direction;
@@ -23193,6 +23216,9 @@ var CallSession = class {
23193
23216
  if (code === 180 || code === 183) {
23194
23217
  this.status = "ringing";
23195
23218
  this.events.emit("call:ringing", this);
23219
+ if (this.session.connection) {
23220
+ setTimeout(() => this.reattachFromReceivers(this.session.connection), 200);
23221
+ }
23196
23222
  }
23197
23223
  });
23198
23224
  this.session.on("confirmed", () => {
@@ -23200,28 +23226,79 @@ var CallSession = class {
23200
23226
  this.startTime = Date.now();
23201
23227
  this.startDurationTimer();
23202
23228
  this.events.emit("call:connected", this);
23229
+ if (this.session.connection) {
23230
+ this.reattachFromReceivers(this.session.connection);
23231
+ this.startSSRCWatch(this.session.connection);
23232
+ }
23203
23233
  });
23204
23234
  this.session.on("peerconnection", (evt) => {
23205
- evt.peerconnection.ontrack = (event) => {
23235
+ const pc = evt.peerconnection;
23236
+ pc.addEventListener("track", (event) => {
23237
+ setTimeout(() => this.reattachFromReceivers(pc), 150);
23206
23238
  if (event.streams && event.streams[0]) {
23207
23239
  this.remoteStreamValue = event.streams[0];
23240
+ this.events.emit("call:stream", this, event.streams[0]);
23241
+ }
23242
+ });
23243
+ pc.oniceconnectionstatechange = () => {
23244
+ if (pc.iceConnectionState === "connected" || pc.iceConnectionState === "completed") {
23245
+ this.reattachFromReceivers(pc);
23208
23246
  }
23209
23247
  };
23210
23248
  });
23211
23249
  this.session.on("ended", (evt) => {
23250
+ console.log(`Dora Cell SDK: Call ended (${evt?.cause || "Normal"})`);
23212
23251
  this.handleCallEnd(evt?.cause);
23213
23252
  });
23214
23253
  this.session.on("failed", (evt) => {
23254
+ console.warn(`Dora Cell SDK: Call failed (${evt?.cause || "Internal Error"})`);
23215
23255
  this.handleCallEnd(evt?.cause || "Call failed");
23216
23256
  });
23217
23257
  this.session.on("rejected", (evt) => {
23258
+ console.warn(`SDK: Call rejected. Cause: ${evt?.cause || "Rejected"}`);
23218
23259
  this.handleCallEnd(evt?.cause || "Call rejected");
23219
23260
  });
23220
23261
  }
23262
+ startSSRCWatch(pc) {
23263
+ this.stopSSRCWatch();
23264
+ this.lastKnownSSRCs.clear();
23265
+ this.ssrcWatchInterval = window.setInterval(async () => {
23266
+ try {
23267
+ if (!pc) return;
23268
+ const stats = await pc.getStats();
23269
+ stats.forEach((report) => {
23270
+ if (report.type === "inbound-rtp" && report.kind === "audio") {
23271
+ const ssrc = report.ssrc;
23272
+ if (ssrc && !this.lastKnownSSRCs.has(ssrc)) {
23273
+ this.lastKnownSSRCs.add(ssrc);
23274
+ setTimeout(() => this.reattachFromReceivers(pc), 200);
23275
+ }
23276
+ }
23277
+ });
23278
+ } catch (e) {
23279
+ }
23280
+ }, 1e3);
23281
+ }
23282
+ stopSSRCWatch() {
23283
+ if (this.ssrcWatchInterval) {
23284
+ clearInterval(this.ssrcWatchInterval);
23285
+ this.ssrcWatchInterval = void 0;
23286
+ }
23287
+ }
23288
+ reattachFromReceivers(pc) {
23289
+ if (!pc) return;
23290
+ const liveAudioTracks = pc.getReceivers().map((r) => r.track).filter((t) => t && t.kind === "audio" && t.readyState === "live");
23291
+ if (liveAudioTracks.length > 0) {
23292
+ const newStream = new MediaStream(liveAudioTracks);
23293
+ this.remoteStreamValue = newStream;
23294
+ this.events.emit("call:stream", this, newStream);
23295
+ }
23296
+ }
23221
23297
  handleCallEnd(reason) {
23222
23298
  this.status = "ended";
23223
23299
  this.endTime = Date.now();
23224
23300
  this.stopDurationTimer();
23301
+ this.stopSSRCWatch();
23225
23302
  this.remoteStreamValue = null;
23226
23303
  this._isMuted = false;
23227
23304
  this.events.emit("call:ended", this, reason);
@@ -23302,6 +23379,7 @@ var CallManager = class {
23302
23379
  const extension = options?.extension || this.getDefaultExtension();
23303
23380
  const sipDomain = this.extractDomain(this.credentials.sipUri);
23304
23381
  const sipTarget = formatPhoneToSIP(targetNumber, sipDomain);
23382
+ console.log(`Dora Cell SDK: Initiating call to ${targetNumber}...`);
23305
23383
  const session = this.ua.call(sipTarget, {
23306
23384
  mediaConstraints: options?.mediaConstraints || { audio: true },
23307
23385
  pcConfig: this.callConfig.pcConfig
@@ -23328,12 +23406,13 @@ var CallManager = class {
23328
23406
  * Answer the pending incoming call.
23329
23407
  * Uses the stored pendingSession from handleIncomingCall.
23330
23408
  */
23331
- answerCurrentCall() {
23409
+ async answerCurrentCall() {
23332
23410
  const session = this.pendingSession;
23333
23411
  if (!session) {
23334
23412
  throw new CallError("No pending incoming call to answer");
23335
23413
  }
23336
23414
  try {
23415
+ await navigator.mediaDevices.getUserMedia({ audio: true });
23337
23416
  session.answer({
23338
23417
  mediaConstraints: { audio: true },
23339
23418
  pcConfig: this.callConfig.pcConfig
@@ -23378,10 +23457,13 @@ var CallManager = class {
23378
23457
  this.pendingSession = null;
23379
23458
  }
23380
23459
  getDefaultExtension() {
23460
+ if (this.credentials.sipUri) {
23461
+ return this.extractExtension(this.credentials.sipUri);
23462
+ }
23381
23463
  if (this.credentials.extensions && this.credentials.extensions.length > 0) {
23382
23464
  return this.credentials.extensions[0].extension;
23383
23465
  }
23384
- return this.extractExtension(this.credentials.sipUri);
23466
+ return "unknown";
23385
23467
  }
23386
23468
  extractExtension(sipUri) {
23387
23469
  const match = sipUri.match(/sip:([^@]+)@/);
@@ -23465,6 +23547,7 @@ var DoraCell = class {
23465
23547
  this.connectionStatus = "disconnected";
23466
23548
  this.retryCount = 0;
23467
23549
  this.maxRetries = 3;
23550
+ this.userId = null;
23468
23551
  this.config = {
23469
23552
  autoSelectExtension: true,
23470
23553
  debug: false,
@@ -23489,17 +23572,25 @@ var DoraCell = class {
23489
23572
  );
23490
23573
  if (this.authProvider instanceof ApiTokenAuthProvider) {
23491
23574
  const token = this.authProvider.getSessionToken();
23492
- if (token) this.apiClient.setSessionToken(token);
23575
+ if (token) {
23576
+ this.apiClient.setSessionToken(token);
23577
+ }
23578
+ }
23579
+ await this.getWallet().catch(() => {
23580
+ });
23581
+ if (!this.credentials?.extensions || this.credentials.extensions.length === 0) {
23582
+ await this.fetchExtensions();
23493
23583
  }
23494
23584
  if (this.config.autoSelectExtension && this.credentials?.extensions && this.credentials.extensions.length > 0) {
23495
23585
  const primary = this.credentials.extensions.find((e) => e.isPrimary) || this.credentials.extensions[0];
23496
- const domain = this.credentials.sipDomain || "cell.usedora.com";
23586
+ const domain = this.credentials.sipDomain || "64.227.10.164";
23497
23587
  this.credentials.sipUri = `sip:${primary.extension}@${domain}`;
23498
- console.log(`SDK: Auto-selected extension ${primary.extension}`);
23499
23588
  }
23500
- await this.initializeUserAgent();
23501
- this.initializeCallManager();
23502
- await this.waitForRegistration();
23589
+ if (this.credentials?.sipUri) {
23590
+ await this.initializeUserAgent();
23591
+ this.initializeCallManager();
23592
+ await this.waitForRegistration();
23593
+ }
23503
23594
  } catch (error) {
23504
23595
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
23505
23596
  this.emitError(new ConnectionError(`Initialization failed: ${errorMessage}`));
@@ -23533,6 +23624,16 @@ var DoraCell = class {
23533
23624
  if (!this.credentials) {
23534
23625
  throw new ConnectionError("No credentials available");
23535
23626
  }
23627
+ this.connectionStatus = "connecting";
23628
+ this.emitConnectionStatus();
23629
+ if (this.ua) {
23630
+ try {
23631
+ this.ua.stop();
23632
+ } catch (e) {
23633
+ }
23634
+ this.ua = null;
23635
+ await new Promise((resolve) => setTimeout(resolve, 500));
23636
+ }
23536
23637
  try {
23537
23638
  const socket = new import_jssip.default.WebSocketInterface(this.credentials.wsUrl);
23538
23639
  const pcConfig = {
@@ -23544,16 +23645,18 @@ var DoraCell = class {
23544
23645
  sockets: [socket],
23545
23646
  register: true,
23546
23647
  display_name: this.getDisplayName(),
23547
- sessionTimers: false,
23648
+ sessionTimers: true,
23649
+ session_timers_refresh_method: "UPDATE",
23548
23650
  trickleIce: false,
23549
23651
  pcConfig,
23550
23652
  instance_id: this.generateInstanceId()
23551
23653
  };
23552
- console.log("SDK: Initializing UA with config:", { ...uaConfig, password: "***" });
23553
23654
  this.ua = new import_jssip.default.UA(uaConfig);
23554
23655
  this.setupUserAgentHandlers();
23555
- console.log("SDK: Starting UA...");
23556
23656
  this.ua.start();
23657
+ if (this.callManager) {
23658
+ this.callManager.setUserAgent(this.ua);
23659
+ }
23557
23660
  } catch (error) {
23558
23661
  throw new ConnectionError(
23559
23662
  `Failed to initialize User Agent: ${error instanceof Error ? error.message : "Unknown error"}`
@@ -23573,6 +23676,7 @@ var DoraCell = class {
23573
23676
  this.ua.on("registered", () => {
23574
23677
  this.connectionStatus = "registered";
23575
23678
  this.retryCount = 0;
23679
+ console.log(`Dora Cell SDK: Connected (${this.getDisplayName()})`);
23576
23680
  this.emitConnectionStatus();
23577
23681
  });
23578
23682
  this.ua.on("registrationFailed", (e) => {
@@ -23587,9 +23691,7 @@ var DoraCell = class {
23587
23691
  });
23588
23692
  this.ua.on("newRTCSession", (e) => {
23589
23693
  const session = e.session;
23590
- console.log(`SDK: New session detected (${session.direction}):`, session.remote_identity?.uri?.toString());
23591
23694
  if (session.direction === "incoming") {
23592
- console.log("SDK: Handling incoming call event");
23593
23695
  this.callManager?.handleIncomingCall(session);
23594
23696
  }
23595
23697
  });
@@ -23623,7 +23725,7 @@ var DoraCell = class {
23623
23725
  /**
23624
23726
  * Answer an incoming call
23625
23727
  */
23626
- answerCall() {
23728
+ async answerCall() {
23627
23729
  const currentCall = this.callManager?.getCurrentCall();
23628
23730
  if (!currentCall) {
23629
23731
  throw new CallError("No incoming call to answer");
@@ -23631,7 +23733,7 @@ var DoraCell = class {
23631
23733
  if (currentCall.direction !== "inbound") {
23632
23734
  throw new CallError("Current call is not an incoming call");
23633
23735
  }
23634
- this.callManager.answerCurrentCall();
23736
+ await this.callManager.answerCurrentCall();
23635
23737
  }
23636
23738
  /**
23637
23739
  * Hangup the current call
@@ -23659,7 +23761,6 @@ var DoraCell = class {
23659
23761
  return { balance: 0, currency: "NGN" };
23660
23762
  }
23661
23763
  try {
23662
- console.log("SDK: Fetching wallet balance...");
23663
23764
  const response = await this.apiClient.get("/wallets");
23664
23765
  const wallets = Array.isArray(response) ? response : response.data || [];
23665
23766
  if (wallets.length === 0) {
@@ -23667,11 +23768,11 @@ var DoraCell = class {
23667
23768
  return { balance: 0, currency: "NGN" };
23668
23769
  }
23669
23770
  const primary = wallets[0];
23771
+ this.userId = primary.user_id || null;
23670
23772
  const result = {
23671
- balance: parseFloat(primary.balance || "0"),
23773
+ balance: parseFloat(primary.balance || primary.amount || "0"),
23672
23774
  currency: primary.currency || "NGN"
23673
23775
  };
23674
- console.log("SDK: Wallet balance fetched:", result);
23675
23776
  return result;
23676
23777
  } catch (error) {
23677
23778
  console.error("SDK: Failed to fetch wallet:", error);
@@ -23692,14 +23793,14 @@ var DoraCell = class {
23692
23793
  throw new Error("SDK not authenticated. Call initialize() first.");
23693
23794
  }
23694
23795
  try {
23695
- const response = await this.apiClient.get("/extensions");
23796
+ const path = this.userId ? `/user/${this.userId}/extensions` : "/extensions";
23797
+ const response = await this.apiClient.get(path);
23696
23798
  const extensions = response.data || response.extensions || response;
23697
23799
  if (this.credentials && Array.isArray(extensions)) {
23698
23800
  this.credentials.extensions = extensions;
23699
23801
  }
23700
23802
  return Array.isArray(extensions) ? extensions : [];
23701
23803
  } catch (error) {
23702
- console.error("SDK: Failed to fetch extensions:", error);
23703
23804
  return this.credentials?.extensions || [];
23704
23805
  }
23705
23806
  }
@@ -23713,11 +23814,11 @@ var DoraCell = class {
23713
23814
  * Update active extension and re-initialize SIP connection
23714
23815
  */
23715
23816
  async setExtension(extension) {
23716
- console.log(`SDK: Switching to extension ${extension}...`);
23717
23817
  if (this.credentials) {
23718
- const domain = this.credentials.sipDomain || "cell.usedora.com";
23818
+ const domain = this.credentials.sipDomain || "64.227.10.164";
23719
23819
  this.credentials.sipUri = `sip:${extension}@${domain}`;
23720
23820
  await this.initializeUserAgent();
23821
+ await this.waitForRegistration();
23721
23822
  this.emitConnectionStatus(this.connectionStatus);
23722
23823
  }
23723
23824
  }
@@ -23751,10 +23852,14 @@ var DoraCell = class {
23751
23852
  }
23752
23853
  // Helper methods
23753
23854
  emitConnectionStatus(error) {
23754
- const extension = this.credentials?.extensions?.[0]?.extension || (this.credentials?.sipUri ? extractNumberFromSipUri(this.credentials.sipUri) : void 0);
23855
+ let activeExt = this.credentials?.sipUri ? extractNumberFromSipUri(this.credentials.sipUri) : void 0;
23856
+ if (!activeExt && this.credentials?.extensions && this.credentials.extensions.length > 0) {
23857
+ const primary = this.credentials.extensions.find((e) => e.isPrimary) || this.credentials.extensions[0];
23858
+ activeExt = primary.extension;
23859
+ }
23755
23860
  const state = {
23756
23861
  status: this.connectionStatus,
23757
- extension,
23862
+ extension: activeExt,
23758
23863
  error
23759
23864
  };
23760
23865
  this.events.emit("connection:status", state);
@@ -23780,6 +23885,12 @@ var DoraCell = class {
23780
23885
  ];
23781
23886
  }
23782
23887
  getDisplayName() {
23888
+ const currentExt = this.credentials?.sipUri ? extractNumberFromSipUri(this.credentials.sipUri) : null;
23889
+ if (currentExt && this.credentials?.extensions) {
23890
+ const found = this.credentials.extensions.find((e) => e.extension === currentExt);
23891
+ if (found?.displayName) return found.displayName;
23892
+ if (found?.extension) return `Ext ${found.extension}`;
23893
+ }
23783
23894
  if (this.credentials?.extensions?.[0]?.displayName) {
23784
23895
  return this.credentials.extensions[0].displayName;
23785
23896
  }
@@ -23802,7 +23913,7 @@ var DoraCell = class {
23802
23913
  case "staging":
23803
23914
  case "dev":
23804
23915
  return "https://dev.api.cell.usedora.com/api";
23805
- case "production":
23916
+ default:
23806
23917
  return "https://api.cell.usedora.com/api";
23807
23918
  }
23808
23919
  }