@xapp/chat-widget 1.105.5 → 1.106.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.js CHANGED
@@ -3490,17 +3490,24 @@ var CLICK_ID_PARAMETERS = [
3490
3490
  "fbclid"
3491
3491
  ];
3492
3492
  var ATTRIBUTION_PARAMETERS = __spreadArray(__spreadArray([], UTM_PARAMETERS, true), CLICK_ID_PARAMETERS);
3493
+ /**
3494
+ * Params AI surfaces put on the links they send people to: Google AI Overviews
3495
+ * (`st_source=ai_overview`), Google's `srsltid` click id, and the generic `ref` / `source`
3496
+ * that ChatGPT, Perplexity and others use. Read only by the widget `load` event, never stored and
3497
+ * never sent with chat messages.
3498
+ */
3499
+ var AI_REFERRAL_PARAMETERS = ["st_source", "srsltid", "ref", "source"];
3493
3500
  var STORAGE_KEY_PREFIX = "xa_tracking_";
3494
3501
  /** Matches the 30 day window the form widget already uses. */
3495
3502
  var DEFAULT_PERSISTENCE_DAYS = 30;
3496
- var MAX_VALUE_LENGTH = 255;
3503
+ var MAX_VALUE_LENGTH$1 = 255;
3497
3504
  /**
3498
3505
  * Values land in a request body and, downstream, in a CRM. Cap the length and
3499
3506
  * reject anything carrying markup or control characters.
3500
3507
  */
3501
- function isValidValue(value) {
3508
+ function isValidValue$1(value) {
3502
3509
  // eslint-disable-next-line no-control-regex
3503
- return value.length <= MAX_VALUE_LENGTH && !/[<>"'\x00-\x1f]/.test(value);
3510
+ return value.length <= MAX_VALUE_LENGTH$1 && !/[<>"'\x00-\x1f]/.test(value);
3504
3511
  }
3505
3512
  /**
3506
3513
  * Reads the attribution parameters present on a URL. Never throws — a URL we
@@ -3517,7 +3524,7 @@ function extractAttributionFromUrl(url) {
3517
3524
  var attribution = {};
3518
3525
  ATTRIBUTION_PARAMETERS.forEach(function (parameter) {
3519
3526
  var value = parsed.searchParams.get(parameter);
3520
- if (value && isValidValue(value)) {
3527
+ if (value && isValidValue$1(value)) {
3521
3528
  attribution[parameter] = value;
3522
3529
  }
3523
3530
  });
@@ -3610,6 +3617,9 @@ function stripQueryString(url) {
3610
3617
  }
3611
3618
  }
3612
3619
 
3620
+ /** Page params whose VALUES the load event may send, each as its own metadata field. */
3621
+ __spreadArray(__spreadArray([], ATTRIBUTION_PARAMETERS, true), AI_REFERRAL_PARAMETERS);
3622
+
3613
3623
  /**
3614
3624
  * Stop the host page scrolling behind an open overlay.
3615
3625
  *
@@ -4177,6 +4187,85 @@ function isTransportFailure(error) {
4177
4187
  return !(Number.isFinite(status) && status >= 100 && status <= 599);
4178
4188
  }
4179
4189
 
4190
+ /**
4191
+ * Rate limiting, as agent-server reports it.
4192
+ *
4193
+ * On 2026-10-07 the XAPP API's WAF rate-limited agent-server and every chat failed. The widget told
4194
+ * visitors the chat was offline and to check their connection, and retried each message twice more —
4195
+ * adding load to the thing that was already over its limit. Rate limiting is the server answering:
4196
+ * it is not offline, an immediate retry will not help, and the visitor should be told what is going on.
4197
+ *
4198
+ * agent-server signals it two ways:
4199
+ * - before the stream: HTTP 429, `Retry-After`, body `{ error, code: "RATE_LIMITED", retryAfter }`
4200
+ * - after the stream starts: SSE `error` event `{ error, code: "RATE_LIMITED", retryAfter }`
4201
+ */
4202
+ var RATE_LIMITED_CODE = "RATE_LIMITED";
4203
+ var RATE_LIMITED_STATUS = 429;
4204
+ /** Shown when the server did not send a message of its own (e.g. a bare 429 from a WAF). */
4205
+ var RATE_LIMITED_FALLBACK_MESSAGE = "We're getting more requests than usual right now. Please try again in a few minutes.";
4206
+ function isRateLimitError(error) {
4207
+ return (error === null || error === void 0 ? void 0 : error.code) === RATE_LIMITED_CODE;
4208
+ }
4209
+ function createRateLimitError(serverMessage, retryAfter) {
4210
+ var message = serverMessage || RATE_LIMITED_FALLBACK_MESSAGE;
4211
+ var error = new Error(message);
4212
+ error.statusCode = RATE_LIMITED_STATUS;
4213
+ error.code = RATE_LIMITED_CODE;
4214
+ if (retryAfter !== undefined) {
4215
+ error.retryAfter = retryAfter;
4216
+ }
4217
+ if (serverMessage) {
4218
+ error.serverMessage = serverMessage;
4219
+ }
4220
+ return error;
4221
+ }
4222
+ /** Seconds from a `retryAfter` body field or a `Retry-After` header. HTTP-date values are ignored. */
4223
+ function parseRetryAfter(value) {
4224
+ if (value === null || value === undefined || value === "") {
4225
+ return undefined;
4226
+ }
4227
+ var seconds = Number(value);
4228
+ return Number.isFinite(seconds) && seconds >= 0 ? seconds : undefined;
4229
+ }
4230
+ /**
4231
+ * Builds the error for a 429 response, reading the agent-server JSON body when there is one. The body
4232
+ * is optional: a 429 from a WAF or proxy in front of agent-server will not carry it.
4233
+ */
4234
+ function rateLimitErrorFromResponse(response) {
4235
+ return __awaiter$1(this, void 0, void 0, function () {
4236
+ var body, text, _a, parsed, serverMessage, retryAfter;
4237
+ var _c, _d;
4238
+ return __generator$1(this, function (_e) {
4239
+ switch (_e.label) {
4240
+ case 0:
4241
+ _e.trys.push([0, 4, , 5]);
4242
+ if (!response.text) return [3 /*break*/, 2];
4243
+ return [4 /*yield*/, response.text()];
4244
+ case 1:
4245
+ _a = _e.sent();
4246
+ return [3 /*break*/, 3];
4247
+ case 2:
4248
+ _a = "";
4249
+ _e.label = 3;
4250
+ case 3:
4251
+ text = _a;
4252
+ parsed = text ? JSON.parse(text) : undefined;
4253
+ if (parsed && typeof parsed === "object") {
4254
+ body = parsed;
4255
+ }
4256
+ return [3 /*break*/, 5];
4257
+ case 4:
4258
+ _e.sent();
4259
+ return [3 /*break*/, 5];
4260
+ case 5:
4261
+ serverMessage = typeof (body === null || body === void 0 ? void 0 : body.error) === "string" && body.error.trim() ? body.error : undefined;
4262
+ retryAfter = (_c = parseRetryAfter(body === null || body === void 0 ? void 0 : body.retryAfter)) !== null && _c !== void 0 ? _c : parseRetryAfter((_d = response.headers) === null || _d === void 0 ? void 0 : _d.get("Retry-After"));
4263
+ return [2 /*return*/, createRateLimitError(serverMessage, retryAfter)];
4264
+ }
4265
+ });
4266
+ });
4267
+ }
4268
+
4180
4269
  /**
4181
4270
  * Default configuration messages
4182
4271
  * @returns
@@ -4575,6 +4664,13 @@ var MCPChat = /** @class */ (function () {
4575
4664
  case 3:
4576
4665
  error_1 = _a.sent();
4577
4666
  err("Failed to validate session: ".concat(error_1));
4667
+ if (isRateLimitError(error_1)) {
4668
+ // Starting a new session would be a second request into the same limit, and would
4669
+ // throw away a session that is still good. Keep it and tell the visitor.
4670
+ this.stopTyping();
4671
+ this.sendFailureMessage(0, 0, this.getErrorMessage(error_1, RATE_LIMITED_STATUS));
4672
+ return [2 /*return*/];
4673
+ }
4578
4674
  // If validation fails (network error, etc.), clear session and start fresh
4579
4675
  clearStoredSession(this._sessionId);
4580
4676
  this._sessionId = "";
@@ -4801,6 +4897,13 @@ var MCPChat = /** @class */ (function () {
4801
4897
  * Determines a user-friendly error message based on the error type.
4802
4898
  */
4803
4899
  MCPChat.prototype.getErrorMessage = function (error, statusCode) {
4900
+ // Rate limited: the server's own message when it sent one
4901
+ if (isRateLimitError(error)) {
4902
+ return error.serverMessage || RATE_LIMITED_FALLBACK_MESSAGE;
4903
+ }
4904
+ if (statusCode === RATE_LIMITED_STATUS) {
4905
+ return RATE_LIMITED_FALLBACK_MESSAGE;
4906
+ }
4804
4907
  // Auth errors
4805
4908
  if (statusCode === 401) {
4806
4909
  return "Authentication failed. Please check your credentials.";
@@ -5300,16 +5403,26 @@ var MCPChat = /** @class */ (function () {
5300
5403
  onopen: function (response) { return __awaiter$1(_this, void 0, void 0, function () {
5301
5404
  var error;
5302
5405
  return __generator$1(this, function (_a) {
5303
- httpStatusCode = response.status;
5304
- if (!response.ok) {
5305
- error = new Error("HTTP error! status: ".concat(response.status));
5306
- error.statusCode = response.status;
5307
- throw error;
5406
+ switch (_a.label) {
5407
+ case 0:
5408
+ httpStatusCode = response.status;
5409
+ if (!(response.status === RATE_LIMITED_STATUS)) return [3 /*break*/, 2];
5410
+ return [4 /*yield*/, rateLimitErrorFromResponse(response)];
5411
+ case 1:
5412
+ // Rate limited: carry the server's message and Retry-After. Not retried (4xx)
5413
+ // and not offline (it has a status code) -- see rateLimit.ts.
5414
+ throw _a.sent();
5415
+ case 2:
5416
+ if (!response.ok) {
5417
+ error = new Error("HTTP error! status: ".concat(response.status));
5418
+ error.statusCode = response.status;
5419
+ throw error;
5420
+ }
5421
+ log("SSE connection opened");
5422
+ // Connection successful - ensure we're marked as online
5423
+ this.setConnectionStatus("online");
5424
+ return [2 /*return*/];
5308
5425
  }
5309
- log("SSE connection opened");
5310
- // Connection successful - ensure we're marked as online
5311
- this.setConnectionStatus("online");
5312
- return [2 /*return*/];
5313
5426
  });
5314
5427
  }); },
5315
5428
  onmessage: function (event) {
@@ -5586,6 +5699,12 @@ var MCPChat = /** @class */ (function () {
5586
5699
  _this.stopTyping();
5587
5700
  _this.stopStreamThrottle();
5588
5701
  err("SSE error: ".concat(data.error));
5702
+ if (data.code === RATE_LIMITED_CODE) {
5703
+ // Carries statusCode 429, so postMessage neither retries it nor
5704
+ // marks the connection offline.
5705
+ reject(createRateLimitError(data.error, parseRetryAfter(data.retryAfter)));
5706
+ break;
5707
+ }
5589
5708
  // Server-side errors should not retry - reject immediately with the error
5590
5709
  reject(new Error(data.error || "Server error during processing"));
5591
5710
  break;
@@ -15514,11 +15633,26 @@ var ModalContent = function (_a) {
15514
15633
  /**
15515
15634
  * Id the visitor (cookies, browser fingerprint, etc)
15516
15635
  *
15636
+ * Kept in localStorage so the widget's load event, which fires before the chat store exists,
15637
+ * reports the same id the store goes on to adopt for a first-time visitor.
15638
+ *
15517
15639
  * @export
15518
15640
  * @returns {string}
15519
15641
  */
15642
+ var VISITOR_ID_KEY = "xapp_chat_visitorId";
15520
15643
  function visitorFingerprint() {
15521
- return uuid_1();
15644
+ try {
15645
+ var stored = localStorage.getItem(VISITOR_ID_KEY);
15646
+ if (stored) {
15647
+ return stored;
15648
+ }
15649
+ var id = uuid_1();
15650
+ localStorage.setItem(VISITOR_ID_KEY, id);
15651
+ return id;
15652
+ }
15653
+ catch (_a) {
15654
+ return uuid_1();
15655
+ }
15522
15656
  }
15523
15657
 
15524
15658
  var DEFAULT_VISITOR = {