@dora-cell/sdk 1.0.2 → 2.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
@@ -23037,7 +23037,30 @@ var ApiTokenAuthProvider = class {
23037
23037
  if (!this.sessionToken) {
23038
23038
  throw new AuthenticationError("No session token returned after key verification");
23039
23039
  }
23040
- this.credentials = this.parseCredentials(authData);
23040
+ console.log("SDK: Performing secondary validation...");
23041
+ const origin = typeof window !== "undefined" ? window.location.origin : "";
23042
+ const validateResponse = await fetch(`${baseUrl}/sdk/v1/auth/validate`, {
23043
+ method: "GET",
23044
+ headers: {
23045
+ "Authorization": `Bearer ${this.sessionToken}`,
23046
+ "x-dora-public-key": this.publicKey,
23047
+ "Origin": origin,
23048
+ "Accept": "application/json"
23049
+ }
23050
+ });
23051
+ if (!validateResponse.ok) {
23052
+ throw new AuthenticationError(
23053
+ `Secondary validation failed: ${validateResponse.status}`,
23054
+ { status: validateResponse.status }
23055
+ );
23056
+ }
23057
+ const validateData = await validateResponse.json();
23058
+ console.log(`SDK: Secondary validation successful for "${validateData.app_name}"`);
23059
+ if (validateData.features && !validateData.features.includes("voice")) {
23060
+ console.warn('SDK: App token does not have the "voice" feature enabled.');
23061
+ }
23062
+ const actualResponseData = authData.data && typeof authData.data === "object" ? authData.data : authData;
23063
+ this.credentials = this.parseCredentials(actualResponseData);
23041
23064
  return this.credentials;
23042
23065
  } catch (error) {
23043
23066
  if (error instanceof AuthenticationError) {
@@ -23076,6 +23099,8 @@ var ApiTokenAuthProvider = class {
23076
23099
  const ext = extensions[0].extension;
23077
23100
  sipUri = `sip:${ext}@${sipDomain}`;
23078
23101
  console.log(`SDK: Constructed SIP URI from extension: ${sipUri}`);
23102
+ } else if (!sipUri) {
23103
+ sipUri = "";
23079
23104
  }
23080
23105
  return {
23081
23106
  wsUrl,
@@ -23176,6 +23201,7 @@ var CallSession = class {
23176
23201
  // JsSIP RTCSession
23177
23202
  this._isMuted = false;
23178
23203
  this.remoteStreamValue = null;
23204
+ this.lastKnownSSRCs = /* @__PURE__ */ new Set();
23179
23205
  this.id = this.generateCallId();
23180
23206
  this.session = session;
23181
23207
  this.direction = direction;
@@ -23193,6 +23219,9 @@ var CallSession = class {
23193
23219
  if (code === 180 || code === 183) {
23194
23220
  this.status = "ringing";
23195
23221
  this.events.emit("call:ringing", this);
23222
+ if (this.session.connection) {
23223
+ setTimeout(() => this.reattachFromReceivers(this.session.connection), 200);
23224
+ }
23196
23225
  }
23197
23226
  });
23198
23227
  this.session.on("confirmed", () => {
@@ -23200,28 +23229,81 @@ var CallSession = class {
23200
23229
  this.startTime = Date.now();
23201
23230
  this.startDurationTimer();
23202
23231
  this.events.emit("call:connected", this);
23232
+ if (this.session.connection) {
23233
+ this.reattachFromReceivers(this.session.connection);
23234
+ this.startSSRCWatch(this.session.connection);
23235
+ }
23203
23236
  });
23204
23237
  this.session.on("peerconnection", (evt) => {
23205
- evt.peerconnection.ontrack = (event) => {
23238
+ const pc = evt.peerconnection;
23239
+ pc.addEventListener("track", (event) => {
23240
+ setTimeout(() => this.reattachFromReceivers(pc), 150);
23206
23241
  if (event.streams && event.streams[0]) {
23207
23242
  this.remoteStreamValue = event.streams[0];
23243
+ this.events.emit("call:stream", this, event.streams[0]);
23244
+ }
23245
+ });
23246
+ pc.oniceconnectionstatechange = () => {
23247
+ if (pc.iceConnectionState === "connected" || pc.iceConnectionState === "completed") {
23248
+ this.reattachFromReceivers(pc);
23208
23249
  }
23209
23250
  };
23210
23251
  });
23211
23252
  this.session.on("ended", (evt) => {
23253
+ console.log(`SDK: Call ended. Cause: ${evt?.cause || "Normal"}`);
23212
23254
  this.handleCallEnd(evt?.cause);
23213
23255
  });
23214
23256
  this.session.on("failed", (evt) => {
23257
+ console.warn(`SDK: Call failed. Cause: ${evt?.cause || "Unknown failure"}`);
23215
23258
  this.handleCallEnd(evt?.cause || "Call failed");
23216
23259
  });
23217
23260
  this.session.on("rejected", (evt) => {
23261
+ console.warn(`SDK: Call rejected. Cause: ${evt?.cause || "Rejected"}`);
23218
23262
  this.handleCallEnd(evt?.cause || "Call rejected");
23219
23263
  });
23220
23264
  }
23265
+ startSSRCWatch(pc) {
23266
+ this.stopSSRCWatch();
23267
+ this.lastKnownSSRCs.clear();
23268
+ this.ssrcWatchInterval = window.setInterval(async () => {
23269
+ try {
23270
+ if (!pc) return;
23271
+ const stats = await pc.getStats();
23272
+ stats.forEach((report) => {
23273
+ if (report.type === "inbound-rtp" && report.kind === "audio") {
23274
+ const ssrc = report.ssrc;
23275
+ if (ssrc && !this.lastKnownSSRCs.has(ssrc)) {
23276
+ this.lastKnownSSRCs.add(ssrc);
23277
+ console.log(`SDK: SSRC Switch Detected: ${ssrc}. Re-binding audio...`);
23278
+ setTimeout(() => this.reattachFromReceivers(pc), 200);
23279
+ }
23280
+ }
23281
+ });
23282
+ } catch (e) {
23283
+ }
23284
+ }, 1e3);
23285
+ }
23286
+ stopSSRCWatch() {
23287
+ if (this.ssrcWatchInterval) {
23288
+ clearInterval(this.ssrcWatchInterval);
23289
+ this.ssrcWatchInterval = void 0;
23290
+ }
23291
+ }
23292
+ reattachFromReceivers(pc) {
23293
+ if (!pc) return;
23294
+ const liveAudioTracks = pc.getReceivers().map((r) => r.track).filter((t) => t && t.kind === "audio" && t.readyState === "live");
23295
+ if (liveAudioTracks.length > 0) {
23296
+ console.log(`SDK: Audio Fix: Rebuilding stream from ${liveAudioTracks.length} receivers`);
23297
+ const newStream = new MediaStream(liveAudioTracks);
23298
+ this.remoteStreamValue = newStream;
23299
+ this.events.emit("call:stream", this, newStream);
23300
+ }
23301
+ }
23221
23302
  handleCallEnd(reason) {
23222
23303
  this.status = "ended";
23223
23304
  this.endTime = Date.now();
23224
23305
  this.stopDurationTimer();
23306
+ this.stopSSRCWatch();
23225
23307
  this.remoteStreamValue = null;
23226
23308
  this._isMuted = false;
23227
23309
  this.events.emit("call:ended", this, reason);
@@ -23302,6 +23384,7 @@ var CallManager = class {
23302
23384
  const extension = options?.extension || this.getDefaultExtension();
23303
23385
  const sipDomain = this.extractDomain(this.credentials.sipUri);
23304
23386
  const sipTarget = formatPhoneToSIP(targetNumber, sipDomain);
23387
+ console.log(`SDK: Calling ${sipTarget} using extension ${extension}...`);
23305
23388
  const session = this.ua.call(sipTarget, {
23306
23389
  mediaConstraints: options?.mediaConstraints || { audio: true },
23307
23390
  pcConfig: this.callConfig.pcConfig
@@ -23328,12 +23411,13 @@ var CallManager = class {
23328
23411
  * Answer the pending incoming call.
23329
23412
  * Uses the stored pendingSession from handleIncomingCall.
23330
23413
  */
23331
- answerCurrentCall() {
23414
+ async answerCurrentCall() {
23332
23415
  const session = this.pendingSession;
23333
23416
  if (!session) {
23334
23417
  throw new CallError("No pending incoming call to answer");
23335
23418
  }
23336
23419
  try {
23420
+ await navigator.mediaDevices.getUserMedia({ audio: true });
23337
23421
  session.answer({
23338
23422
  mediaConstraints: { audio: true },
23339
23423
  pcConfig: this.callConfig.pcConfig
@@ -23465,6 +23549,7 @@ var DoraCell = class {
23465
23549
  this.connectionStatus = "disconnected";
23466
23550
  this.retryCount = 0;
23467
23551
  this.maxRetries = 3;
23552
+ this.userId = null;
23468
23553
  this.config = {
23469
23554
  autoSelectExtension: true,
23470
23555
  debug: false,
@@ -23489,17 +23574,32 @@ var DoraCell = class {
23489
23574
  );
23490
23575
  if (this.authProvider instanceof ApiTokenAuthProvider) {
23491
23576
  const token = this.authProvider.getSessionToken();
23492
- if (token) this.apiClient.setSessionToken(token);
23577
+ if (token) {
23578
+ console.log("SDK: Session token acquired and set in API client");
23579
+ this.apiClient.setSessionToken(token);
23580
+ } else {
23581
+ console.warn("SDK: No session token found in auth provider");
23582
+ }
23583
+ }
23584
+ await this.getWallet().catch(() => {
23585
+ });
23586
+ if (!this.credentials?.extensions || this.credentials.extensions.length === 0) {
23587
+ console.log("SDK: No extensions in auth response, fetching from API...");
23588
+ await this.fetchExtensions();
23493
23589
  }
23494
23590
  if (this.config.autoSelectExtension && this.credentials?.extensions && this.credentials.extensions.length > 0) {
23495
23591
  const primary = this.credentials.extensions.find((e) => e.isPrimary) || this.credentials.extensions[0];
23496
- const domain = this.credentials.sipDomain || "cell.usedora.com";
23592
+ const domain = this.credentials.sipDomain || "64.227.10.164";
23497
23593
  this.credentials.sipUri = `sip:${primary.extension}@${domain}`;
23498
23594
  console.log(`SDK: Auto-selected extension ${primary.extension}`);
23499
23595
  }
23500
- await this.initializeUserAgent();
23501
- this.initializeCallManager();
23502
- await this.waitForRegistration();
23596
+ if (this.credentials?.sipUri) {
23597
+ await this.initializeUserAgent();
23598
+ this.initializeCallManager();
23599
+ await this.waitForRegistration();
23600
+ } else {
23601
+ console.warn("SDK: No SIP URI available yet. UA initialization deferred.");
23602
+ }
23503
23603
  } catch (error) {
23504
23604
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
23505
23605
  this.emitError(new ConnectionError(`Initialization failed: ${errorMessage}`));
@@ -23533,6 +23633,11 @@ var DoraCell = class {
23533
23633
  if (!this.credentials) {
23534
23634
  throw new ConnectionError("No credentials available");
23535
23635
  }
23636
+ if (this.ua) {
23637
+ console.log("SDK: Stopping existing User Agent...");
23638
+ this.ua.stop();
23639
+ this.ua = null;
23640
+ }
23536
23641
  try {
23537
23642
  const socket = new import_jssip.default.WebSocketInterface(this.credentials.wsUrl);
23538
23643
  const pcConfig = {
@@ -23544,7 +23649,8 @@ var DoraCell = class {
23544
23649
  sockets: [socket],
23545
23650
  register: true,
23546
23651
  display_name: this.getDisplayName(),
23547
- sessionTimers: false,
23652
+ sessionTimers: true,
23653
+ session_timers_refresh_method: "UPDATE",
23548
23654
  trickleIce: false,
23549
23655
  pcConfig,
23550
23656
  instance_id: this.generateInstanceId()
@@ -23554,6 +23660,9 @@ var DoraCell = class {
23554
23660
  this.setupUserAgentHandlers();
23555
23661
  console.log("SDK: Starting UA...");
23556
23662
  this.ua.start();
23663
+ if (this.callManager) {
23664
+ this.callManager.setUserAgent(this.ua);
23665
+ }
23557
23666
  } catch (error) {
23558
23667
  throw new ConnectionError(
23559
23668
  `Failed to initialize User Agent: ${error instanceof Error ? error.message : "Unknown error"}`
@@ -23623,7 +23732,7 @@ var DoraCell = class {
23623
23732
  /**
23624
23733
  * Answer an incoming call
23625
23734
  */
23626
- answerCall() {
23735
+ async answerCall() {
23627
23736
  const currentCall = this.callManager?.getCurrentCall();
23628
23737
  if (!currentCall) {
23629
23738
  throw new CallError("No incoming call to answer");
@@ -23631,7 +23740,7 @@ var DoraCell = class {
23631
23740
  if (currentCall.direction !== "inbound") {
23632
23741
  throw new CallError("Current call is not an incoming call");
23633
23742
  }
23634
- this.callManager.answerCurrentCall();
23743
+ await this.callManager.answerCurrentCall();
23635
23744
  }
23636
23745
  /**
23637
23746
  * Hangup the current call
@@ -23667,11 +23776,12 @@ var DoraCell = class {
23667
23776
  return { balance: 0, currency: "NGN" };
23668
23777
  }
23669
23778
  const primary = wallets[0];
23779
+ this.userId = primary.user_id || null;
23670
23780
  const result = {
23671
- balance: parseFloat(primary.balance || "0"),
23781
+ balance: parseFloat(primary.balance || primary.amount || "0"),
23672
23782
  currency: primary.currency || "NGN"
23673
23783
  };
23674
- console.log("SDK: Wallet balance fetched:", result);
23784
+ console.log("SDK: Wallet balance fetched successfully, userId:", this.userId);
23675
23785
  return result;
23676
23786
  } catch (error) {
23677
23787
  console.error("SDK: Failed to fetch wallet:", error);
@@ -23692,7 +23802,9 @@ var DoraCell = class {
23692
23802
  throw new Error("SDK not authenticated. Call initialize() first.");
23693
23803
  }
23694
23804
  try {
23695
- const response = await this.apiClient.get("/extensions");
23805
+ const path = this.userId ? `/user/${this.userId}/extensions` : "/extensions";
23806
+ console.log(`SDK: Fetching extensions from: ${path}`);
23807
+ const response = await this.apiClient.get(path);
23696
23808
  const extensions = response.data || response.extensions || response;
23697
23809
  if (this.credentials && Array.isArray(extensions)) {
23698
23810
  this.credentials.extensions = extensions;
@@ -23715,7 +23827,7 @@ var DoraCell = class {
23715
23827
  async setExtension(extension) {
23716
23828
  console.log(`SDK: Switching to extension ${extension}...`);
23717
23829
  if (this.credentials) {
23718
- const domain = this.credentials.sipDomain || "cell.usedora.com";
23830
+ const domain = this.credentials.sipDomain || "64.227.10.164";
23719
23831
  this.credentials.sipUri = `sip:${extension}@${domain}`;
23720
23832
  await this.initializeUserAgent();
23721
23833
  this.emitConnectionStatus(this.connectionStatus);
@@ -23802,7 +23914,7 @@ var DoraCell = class {
23802
23914
  case "staging":
23803
23915
  case "dev":
23804
23916
  return "https://dev.api.cell.usedora.com/api";
23805
- case "production":
23917
+ default:
23806
23918
  return "https://api.cell.usedora.com/api";
23807
23919
  }
23808
23920
  }