@exotel-npm-dev/exotel-ai-assist 1.0.0 → 1.0.1

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.
@@ -398,13 +398,21 @@ class BroadcastChannelTransport {
398
398
  if (this.socket) {
399
399
  this.socket.close(1000, "reconnecting");
400
400
  }
401
- // authToken is passed as a subprotocol so it is sent in the
402
- // Sec-WebSocket-Protocol header — the only way to include auth
403
- // credentials in a browser WebSocket handshake.
404
401
  this.socket = new WebSocket(url);
405
402
  this.socket.onopen = () => {
406
403
  var _a;
407
404
  this.socketConnected = true;
405
+ // First-message auth protocol: send the JWT before any other message.
406
+ // The server does not send an auth_required prompt; the client is
407
+ // responsible for sending this immediately after the socket opens.
408
+ console.log('Sending token', {
409
+ type: "auth",
410
+ access_token: authToken
411
+ });
412
+ this.socket.send(JSON.stringify({
413
+ type: "auth",
414
+ access_token: authToken
415
+ }));
408
416
  const msg = {
409
417
  type: "CONNECTED"
410
418
  };
@@ -429,11 +437,12 @@ class BroadcastChannelTransport {
429
437
  (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
430
438
  this.channel.postMessage(msg);
431
439
  };
432
- this.socket.onclose = () => {
440
+ this.socket.onclose = ev => {
433
441
  var _a;
434
442
  this.socketConnected = false;
435
443
  const msg = {
436
- type: "DISCONNECTED"
444
+ type: "DISCONNECTED",
445
+ code: ev.code
437
446
  };
438
447
  (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
439
448
  this.channel.postMessage(msg);
@@ -1534,12 +1543,14 @@ class Utils {
1534
1543
  }
1535
1544
  static constructQueryParams(params) {
1536
1545
  // Keys consumed by the library — never forwarded to the server.
1546
+ // authToken is intentionally excluded: authentication is done via the
1547
+ // first WebSocket text message (first-message auth protocol), not the URL.
1537
1548
  const INTERNAL_KEYS = new Set(["authToken", "accountId", "wssBaseUrl", "reconnectInterval", "maxReconnectAttempts"]);
1538
- // Mandatory server params remapped to the names the API expects.
1549
+ // Stream-key resolution params sent as query parameters.
1539
1550
  // accountId is embedded in the URL path — never sent as a query param.
1540
- const mandatory = [["access_token", String(params.authToken)]];
1551
+ const pairs = [];
1541
1552
  if (params.call_sid !== undefined && params.call_sid !== null) {
1542
- mandatory.push(["call_sid", String(params.call_sid)]);
1553
+ pairs.push(["call_sid", String(params.call_sid)]);
1543
1554
  }
1544
1555
  // Extra caller-supplied params, excluding internals and already-mapped keys.
1545
1556
  const extra = Object.entries(params).filter(([k, v]) => !INTERNAL_KEYS.has(k) && k !== "call_sid" && v !== undefined && v !== null).map(([k, v]) => [k, String(v)]);
@@ -1549,7 +1560,7 @@ class Utils {
1549
1560
  // array as a plain object, turning index 0 into a key and the inner
1550
1561
  // array's toString() (comma-joined) into the value.
1551
1562
  const query = new URLSearchParams();
1552
- [...mandatory, ...extra].forEach(([k, v]) => query.append(k, v));
1563
+ [...pairs, ...extra].forEach(([k, v]) => query.append(k, v));
1553
1564
  return query.toString();
1554
1565
  }
1555
1566
  /**
@@ -1645,7 +1656,11 @@ class ExotelAIAssistController extends EventEmitter {
1645
1656
  case "DISCONNECTED":
1646
1657
  this._setStatus("disconnected");
1647
1658
  this.emit("onCallEnd");
1648
- this._scheduleReconnect();
1659
+ // Close codes 4001 (auth_failed) and 4002 (auth_timeout) are permanent
1660
+ // auth failures — reconnecting with the same token would fail again.
1661
+ if (msg.code !== 4001 && msg.code !== 4002) {
1662
+ this._scheduleReconnect();
1663
+ }
1649
1664
  break;
1650
1665
  case "ERROR":
1651
1666
  this._setStatus("error");
@@ -1657,14 +1672,26 @@ class ExotelAIAssistController extends EventEmitter {
1657
1672
  }
1658
1673
  }
1659
1674
  _handleServerPayload(raw) {
1675
+ var _a;
1660
1676
  let parsed;
1661
1677
  try {
1662
1678
  parsed = JSON.parse(raw);
1663
- } catch (_a) {
1679
+ } catch (_b) {
1664
1680
  this.emit("error", new Error(`Failed to parse server message: ${raw}`));
1665
1681
  return;
1666
1682
  }
1667
1683
  this.emit("raw", parsed);
1684
+ // Auth error messages arrive before the server closes the connection.
1685
+ // Treat both as fatal — the server will follow up with a 4001/4002 close.
1686
+ const msgType = parsed.type;
1687
+ if (msgType === "auth_failed" || msgType === "auth_timeout") {
1688
+ const detail = (_a = parsed.error) !== null && _a !== void 0 ? _a : msgType;
1689
+ const err = new Error(detail);
1690
+ err.code = msgType.toUpperCase();
1691
+ this._setStatus("error");
1692
+ this.emit("error", err);
1693
+ return;
1694
+ }
1668
1695
  if (parsed.config) {
1669
1696
  this.emit("botConfig", parsed.config);
1670
1697
  }
package/dist/index.js CHANGED
@@ -19816,13 +19816,21 @@ class BroadcastChannelTransport {
19816
19816
  if (this.socket) {
19817
19817
  this.socket.close(1000, "reconnecting");
19818
19818
  }
19819
- // authToken is passed as a subprotocol so it is sent in the
19820
- // Sec-WebSocket-Protocol header — the only way to include auth
19821
- // credentials in a browser WebSocket handshake.
19822
19819
  this.socket = new WebSocket(url);
19823
19820
  this.socket.onopen = () => {
19824
19821
  var _a;
19825
19822
  this.socketConnected = true;
19823
+ // First-message auth protocol: send the JWT before any other message.
19824
+ // The server does not send an auth_required prompt; the client is
19825
+ // responsible for sending this immediately after the socket opens.
19826
+ console.log('Sending token', {
19827
+ type: "auth",
19828
+ access_token: authToken
19829
+ });
19830
+ this.socket.send(JSON.stringify({
19831
+ type: "auth",
19832
+ access_token: authToken
19833
+ }));
19826
19834
  const msg = {
19827
19835
  type: "CONNECTED"
19828
19836
  };
@@ -19847,11 +19855,12 @@ class BroadcastChannelTransport {
19847
19855
  (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
19848
19856
  this.channel.postMessage(msg);
19849
19857
  };
19850
- this.socket.onclose = () => {
19858
+ this.socket.onclose = ev => {
19851
19859
  var _a;
19852
19860
  this.socketConnected = false;
19853
19861
  const msg = {
19854
- type: "DISCONNECTED"
19862
+ type: "DISCONNECTED",
19863
+ code: ev.code
19855
19864
  };
19856
19865
  (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
19857
19866
  this.channel.postMessage(msg);
@@ -20952,12 +20961,14 @@ class Utils {
20952
20961
  }
20953
20962
  static constructQueryParams(params) {
20954
20963
  // Keys consumed by the library — never forwarded to the server.
20964
+ // authToken is intentionally excluded: authentication is done via the
20965
+ // first WebSocket text message (first-message auth protocol), not the URL.
20955
20966
  const INTERNAL_KEYS = new Set(["authToken", "accountId", "wssBaseUrl", "reconnectInterval", "maxReconnectAttempts"]);
20956
- // Mandatory server params remapped to the names the API expects.
20967
+ // Stream-key resolution params sent as query parameters.
20957
20968
  // accountId is embedded in the URL path — never sent as a query param.
20958
- const mandatory = [["access_token", String(params.authToken)]];
20969
+ const pairs = [];
20959
20970
  if (params.call_sid !== undefined && params.call_sid !== null) {
20960
- mandatory.push(["call_sid", String(params.call_sid)]);
20971
+ pairs.push(["call_sid", String(params.call_sid)]);
20961
20972
  }
20962
20973
  // Extra caller-supplied params, excluding internals and already-mapped keys.
20963
20974
  const extra = Object.entries(params).filter(([k, v]) => !INTERNAL_KEYS.has(k) && k !== "call_sid" && v !== undefined && v !== null).map(([k, v]) => [k, String(v)]);
@@ -20967,7 +20978,7 @@ class Utils {
20967
20978
  // array as a plain object, turning index 0 into a key and the inner
20968
20979
  // array's toString() (comma-joined) into the value.
20969
20980
  const query = new URLSearchParams();
20970
- [...mandatory, ...extra].forEach(([k, v]) => query.append(k, v));
20981
+ [...pairs, ...extra].forEach(([k, v]) => query.append(k, v));
20971
20982
  return query.toString();
20972
20983
  }
20973
20984
  /**
@@ -21063,7 +21074,11 @@ class ExotelAIAssistController extends EventEmitter {
21063
21074
  case "DISCONNECTED":
21064
21075
  this._setStatus("disconnected");
21065
21076
  this.emit("onCallEnd");
21066
- this._scheduleReconnect();
21077
+ // Close codes 4001 (auth_failed) and 4002 (auth_timeout) are permanent
21078
+ // auth failures — reconnecting with the same token would fail again.
21079
+ if (msg.code !== 4001 && msg.code !== 4002) {
21080
+ this._scheduleReconnect();
21081
+ }
21067
21082
  break;
21068
21083
  case "ERROR":
21069
21084
  this._setStatus("error");
@@ -21075,14 +21090,26 @@ class ExotelAIAssistController extends EventEmitter {
21075
21090
  }
21076
21091
  }
21077
21092
  _handleServerPayload(raw) {
21093
+ var _a;
21078
21094
  let parsed;
21079
21095
  try {
21080
21096
  parsed = JSON.parse(raw);
21081
- } catch (_a) {
21097
+ } catch (_b) {
21082
21098
  this.emit("error", new Error(`Failed to parse server message: ${raw}`));
21083
21099
  return;
21084
21100
  }
21085
21101
  this.emit("raw", parsed);
21102
+ // Auth error messages arrive before the server closes the connection.
21103
+ // Treat both as fatal — the server will follow up with a 4001/4002 close.
21104
+ const msgType = parsed.type;
21105
+ if (msgType === "auth_failed" || msgType === "auth_timeout") {
21106
+ const detail = (_a = parsed.error) !== null && _a !== void 0 ? _a : msgType;
21107
+ const err = new Error(detail);
21108
+ err.code = msgType.toUpperCase();
21109
+ this._setStatus("error");
21110
+ this.emit("error", err);
21111
+ return;
21112
+ }
21086
21113
  if (parsed.config) {
21087
21114
  this.emit("botConfig", parsed.config);
21088
21115
  }
@@ -5102,13 +5102,21 @@ class BroadcastChannelTransport {
5102
5102
  if (this.socket) {
5103
5103
  this.socket.close(1000, "reconnecting");
5104
5104
  }
5105
- // authToken is passed as a subprotocol so it is sent in the
5106
- // Sec-WebSocket-Protocol header — the only way to include auth
5107
- // credentials in a browser WebSocket handshake.
5108
5105
  this.socket = new WebSocket(url);
5109
5106
  this.socket.onopen = () => {
5110
5107
  var _a;
5111
5108
  this.socketConnected = true;
5109
+ // First-message auth protocol: send the JWT before any other message.
5110
+ // The server does not send an auth_required prompt; the client is
5111
+ // responsible for sending this immediately after the socket opens.
5112
+ console.log('Sending token', {
5113
+ type: "auth",
5114
+ access_token: authToken
5115
+ });
5116
+ this.socket.send(JSON.stringify({
5117
+ type: "auth",
5118
+ access_token: authToken
5119
+ }));
5112
5120
  const msg = {
5113
5121
  type: "CONNECTED"
5114
5122
  };
@@ -5133,11 +5141,12 @@ class BroadcastChannelTransport {
5133
5141
  (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
5134
5142
  this.channel.postMessage(msg);
5135
5143
  };
5136
- this.socket.onclose = () => {
5144
+ this.socket.onclose = ev => {
5137
5145
  var _a;
5138
5146
  this.socketConnected = false;
5139
5147
  const msg = {
5140
- type: "DISCONNECTED"
5148
+ type: "DISCONNECTED",
5149
+ code: ev.code
5141
5150
  };
5142
5151
  (_a = this.handler) === null || _a === void 0 ? void 0 : _a.call(this, msg);
5143
5152
  this.channel.postMessage(msg);
@@ -6238,12 +6247,14 @@ class Utils {
6238
6247
  }
6239
6248
  static constructQueryParams(params) {
6240
6249
  // Keys consumed by the library — never forwarded to the server.
6250
+ // authToken is intentionally excluded: authentication is done via the
6251
+ // first WebSocket text message (first-message auth protocol), not the URL.
6241
6252
  const INTERNAL_KEYS = new Set(["authToken", "accountId", "wssBaseUrl", "reconnectInterval", "maxReconnectAttempts"]);
6242
- // Mandatory server params remapped to the names the API expects.
6253
+ // Stream-key resolution params sent as query parameters.
6243
6254
  // accountId is embedded in the URL path — never sent as a query param.
6244
- const mandatory = [["access_token", String(params.authToken)]];
6255
+ const pairs = [];
6245
6256
  if (params.call_sid !== undefined && params.call_sid !== null) {
6246
- mandatory.push(["call_sid", String(params.call_sid)]);
6257
+ pairs.push(["call_sid", String(params.call_sid)]);
6247
6258
  }
6248
6259
  // Extra caller-supplied params, excluding internals and already-mapped keys.
6249
6260
  const extra = Object.entries(params).filter(([k, v]) => !INTERNAL_KEYS.has(k) && k !== "call_sid" && v !== undefined && v !== null).map(([k, v]) => [k, String(v)]);
@@ -6253,7 +6264,7 @@ class Utils {
6253
6264
  // array as a plain object, turning index 0 into a key and the inner
6254
6265
  // array's toString() (comma-joined) into the value.
6255
6266
  const query = new URLSearchParams();
6256
- [...mandatory, ...extra].forEach(([k, v]) => query.append(k, v));
6267
+ [...pairs, ...extra].forEach(([k, v]) => query.append(k, v));
6257
6268
  return query.toString();
6258
6269
  }
6259
6270
  /**
@@ -6349,7 +6360,11 @@ class ExotelAIAssistController extends EventEmitter {
6349
6360
  case "DISCONNECTED":
6350
6361
  this._setStatus("disconnected");
6351
6362
  this.emit("onCallEnd");
6352
- this._scheduleReconnect();
6363
+ // Close codes 4001 (auth_failed) and 4002 (auth_timeout) are permanent
6364
+ // auth failures — reconnecting with the same token would fail again.
6365
+ if (msg.code !== 4001 && msg.code !== 4002) {
6366
+ this._scheduleReconnect();
6367
+ }
6353
6368
  break;
6354
6369
  case "ERROR":
6355
6370
  this._setStatus("error");
@@ -6361,14 +6376,26 @@ class ExotelAIAssistController extends EventEmitter {
6361
6376
  }
6362
6377
  }
6363
6378
  _handleServerPayload(raw) {
6379
+ var _a;
6364
6380
  let parsed;
6365
6381
  try {
6366
6382
  parsed = JSON.parse(raw);
6367
- } catch (_a) {
6383
+ } catch (_b) {
6368
6384
  this.emit("error", new Error(`Failed to parse server message: ${raw}`));
6369
6385
  return;
6370
6386
  }
6371
6387
  this.emit("raw", parsed);
6388
+ // Auth error messages arrive before the server closes the connection.
6389
+ // Treat both as fatal — the server will follow up with a 4001/4002 close.
6390
+ const msgType = parsed.type;
6391
+ if (msgType === "auth_failed" || msgType === "auth_timeout") {
6392
+ const detail = (_a = parsed.error) !== null && _a !== void 0 ? _a : msgType;
6393
+ const err = new Error(detail);
6394
+ err.code = msgType.toUpperCase();
6395
+ this._setStatus("error");
6396
+ this.emit("error", err);
6397
+ return;
6398
+ }
6372
6399
  if (parsed.config) {
6373
6400
  this.emit("botConfig", parsed.config);
6374
6401
  }
@@ -82,6 +82,7 @@ export type WorkerInboundMessage = {
82
82
  type: "CONNECTED";
83
83
  } | {
84
84
  type: "DISCONNECTED";
85
+ code?: number;
85
86
  } | {
86
87
  type: "ERROR";
87
88
  message: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exotel-npm-dev/exotel-ai-assist",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "AI-powered real-time suggestions, transcript, and sentiment for agent calls",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/index.js",