@buni.ai/chatbot-core 1.0.19 → 1.0.21

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
@@ -4,6 +4,8 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  // Default BuniAI platform URL
6
6
  const BUNI_PLATFORM_URL = "https://www.buni.ai";
7
+ const PARENT_HANDSHAKE_TYPE = "buni:parent_handshake";
8
+ const IFRAME_HANDSHAKE_ACK_TYPE = "buni:iframe_handshake_ack";
7
9
  // Core widget loader class
8
10
  class BuniChatWidget {
9
11
  constructor() {
@@ -19,6 +21,34 @@ class BuniChatWidget {
19
21
  this.chatIframe = null;
20
22
  this.customerData = null;
21
23
  this.sessionVariables = null;
24
+ this.chatTargetOrigin = null;
25
+ this.handshakeComplete = false;
26
+ this.outboundMessageQueue = [];
27
+ this.displayMode = "hidden";
28
+ this.serverBehavior = {};
29
+ }
30
+ isMinimalModeEnabled() {
31
+ var _a, _b;
32
+ if (typeof ((_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.config) === null || _b === void 0 ? void 0 : _b.enableMinimalMode) === "boolean") {
33
+ return this.options.config.enableMinimalMode;
34
+ }
35
+ return Boolean(this.serverBehavior.enableMinimalMode);
36
+ }
37
+ syncServerBehaviorFromData(data) {
38
+ if (!data || typeof data !== "object") {
39
+ return;
40
+ }
41
+ const behaviorCandidate = data.behavior && typeof data.behavior === "object"
42
+ ? data.behavior
43
+ : data;
44
+ if (typeof behaviorCandidate.enableMinimalMode === "boolean") {
45
+ this.serverBehavior.enableMinimalMode = behaviorCandidate.enableMinimalMode;
46
+ }
47
+ if (behaviorCandidate.defaultMode === "full" ||
48
+ behaviorCandidate.defaultMode === "minimal" ||
49
+ behaviorCandidate.defaultMode === "hidden") {
50
+ this.serverBehavior.defaultMode = behaviorCandidate.defaultMode;
51
+ }
22
52
  }
23
53
  async initialize(options) {
24
54
  this.options = options;
@@ -182,6 +212,15 @@ class BuniChatWidget {
182
212
  params.set("startButtonText", config.startButtonText);
183
213
  if (config.preChatFormFields)
184
214
  params.set("preChatFormFields", JSON.stringify(config.preChatFormFields));
215
+ if (config.enableMinimalMode !== undefined) {
216
+ params.set("enableMinimalMode", String(config.enableMinimalMode));
217
+ }
218
+ if (config.defaultMode) {
219
+ params.set("defaultMode", config.defaultMode);
220
+ }
221
+ if (config.autoMessages && Array.isArray(config.autoMessages)) {
222
+ params.set("autoMessages", JSON.stringify(config.autoMessages));
223
+ }
185
224
  chatIframe.src = `${this.getBaseUrl()}/embed/chat?${params.toString()}`;
186
225
  // Chat iframe styling - initially hidden
187
226
  chatIframe.style.cssText = `
@@ -205,52 +244,103 @@ class BuniChatWidget {
205
244
  chatIframe.onload = () => {
206
245
  // Set visibility hidden after iframe loads to allow content initialization
207
246
  chatIframe.style.visibility = "hidden";
247
+ // Compute the iframe origin once we know the final src.
248
+ try {
249
+ this.chatTargetOrigin = new URL(chatIframe.src).origin;
250
+ }
251
+ catch (_a) {
252
+ this.chatTargetOrigin = this.getBaseUrl();
253
+ }
254
+ // Reset handshake state for this iframe instance.
255
+ this.handshakeComplete = false;
256
+ this.outboundMessageQueue = [];
208
257
  this.setupPostMessageAPI(chatIframe);
258
+ this.sendParentHandshake();
209
259
  // Now that both iframes are loaded, resolve the promise
210
260
  this.widgetElement = container;
211
261
  this.triggerIframe = triggerIframe;
212
262
  this.chatIframe = chatIframe;
213
263
  this.state.isMinimized = true;
264
+ this.state.displayMode = "hidden";
265
+ this.displayMode = "hidden";
214
266
  this.state.isLoaded = true;
215
267
  resolve();
216
268
  };
217
269
  };
218
270
  // Listen for trigger and connection events
219
271
  window.addEventListener("message", (event) => {
220
- var _a;
221
- if (!event.data.type)
272
+ var _a, _b, _c, _d, _e, _f;
273
+ const payload = event.data;
274
+ if (!payload || typeof payload !== "object")
275
+ return;
276
+ if (typeof payload.type !== "string")
222
277
  return;
223
- switch (event.data.type) {
278
+ switch (payload.type) {
224
279
  case "connection_ready":
225
280
  // Check if message is from chat iframe
226
- if (event.source === ((_a = this.chatIframe) === null || _a === void 0 ? void 0 : _a.contentWindow)) {
281
+ if (event.source === ((_a = this.chatIframe) === null || _a === void 0 ? void 0 : _a.contentWindow) &&
282
+ event.origin === this.getBaseUrl()) {
283
+ this.syncServerBehaviorFromData(payload.data);
227
284
  // Connection is ready, show the trigger button
228
285
  if (container && !config.hideDefaultTrigger) {
229
286
  container.style.display = "block";
230
287
  }
231
288
  // Emit connection_ready event for consumers
232
- this.emit("connection_ready", { timestamp: Date.now() });
289
+ this.emit("connection_ready", {
290
+ timestamp: Date.now(),
291
+ behavior: (_b = payload.data) === null || _b === void 0 ? void 0 : _b.behavior,
292
+ });
233
293
  if (this.options.onConnectionReady &&
234
294
  typeof this.options.onConnectionReady === "function") {
235
- this.options.onConnectionReady({ timestamp: Date.now() });
295
+ this.options.onConnectionReady({
296
+ timestamp: Date.now(),
297
+ behavior: (_c = payload.data) === null || _c === void 0 ? void 0 : _c.behavior,
298
+ });
299
+ }
300
+ }
301
+ break;
302
+ case "chatbot:close":
303
+ if (event.source === ((_d = this.chatIframe) === null || _d === void 0 ? void 0 : _d.contentWindow) &&
304
+ event.origin === this.getBaseUrl()) {
305
+ this.closeChat();
306
+ this.displayMode = "hidden";
307
+ this.state.displayMode = "hidden";
308
+ }
309
+ break;
310
+ case "chatbot:minimize":
311
+ if (event.source === ((_e = this.chatIframe) === null || _e === void 0 ? void 0 : _e.contentWindow) &&
312
+ event.origin === this.getBaseUrl()) {
313
+ if (this.isMinimalModeEnabled()) {
314
+ if (!this.state.isOpen) {
315
+ void this.openChat();
316
+ }
317
+ this.postMessageToWidget("minimize");
318
+ this.displayMode = "minimal";
319
+ this.state.displayMode = "minimal";
320
+ }
321
+ else {
322
+ this.closeChat();
323
+ this.displayMode = "hidden";
324
+ this.state.displayMode = "hidden";
236
325
  }
237
326
  }
238
327
  break;
239
328
  case "trigger_clicked":
240
- this.openChat();
329
+ if (event.source === ((_f = this.triggerIframe) === null || _f === void 0 ? void 0 : _f.contentWindow) &&
330
+ event.origin === window.location.origin) {
331
+ this.openChat();
332
+ }
241
333
  break;
242
334
  }
243
335
  });
244
336
  // Trigger the load event
245
337
  // if by 10 seconds the ready message is not received, we want to manually, set the container to visible
246
338
  setTimeout(() => {
247
- if (!this.state.isLoaded) {
248
- if (container &&
249
- !config.hideDefaultTrigger &&
250
- container.style.display === "none" &&
251
- this.state.isOpen === false) {
252
- container.style.display = "block";
253
- }
339
+ if (container &&
340
+ !config.hideDefaultTrigger &&
341
+ container.style.display === "none" &&
342
+ this.state.isOpen === false) {
343
+ container.style.display = "block";
254
344
  }
255
345
  }, 10000);
256
346
  triggerIframe.src = "about:blank";
@@ -352,11 +442,15 @@ class BuniChatWidget {
352
442
  </div>
353
443
  <script>
354
444
  function handleClick() {
355
- window.parent.postMessage({ type: 'trigger_clicked' }, '*');
445
+ // Trigger iframe is same-origin with parent (about:blank + document.write),
446
+ // so we can safely target the parent's origin.
447
+ window.parent.postMessage({ type: 'trigger_clicked' }, window.location.origin);
356
448
  }
357
449
 
358
450
  // Listen for unread count updates
359
451
  window.addEventListener('message', function(event) {
452
+ if (event.source !== window.parent) return;
453
+ if (event.origin !== window.location.origin) return;
360
454
  if (event.data.type === 'updateUnreadCount') {
361
455
  const badge = document.getElementById('badge');
362
456
  const count = event.data.count || 0;
@@ -423,6 +517,8 @@ class BuniChatWidget {
423
517
  this.chatIframe.style.visibility = "visible";
424
518
  this.state.isMinimized = false;
425
519
  this.state.isOpen = true;
520
+ this.displayMode = "full";
521
+ this.state.displayMode = "full";
426
522
  this.emit("maximized", { timestamp: Date.now() });
427
523
  }
428
524
  closeChat() {
@@ -455,6 +551,8 @@ class BuniChatWidget {
455
551
  this.triggerIframe.style.display = "block";
456
552
  this.state.isMinimized = true;
457
553
  this.state.isOpen = false;
554
+ this.displayMode = "hidden";
555
+ this.state.displayMode = "hidden";
458
556
  this.emit("minimized", { timestamp: Date.now() });
459
557
  }
460
558
  getPositionStyles(position, isMinimized = false) {
@@ -504,12 +602,24 @@ class BuniChatWidget {
504
602
  // Listen for messages from the iframe
505
603
  window.addEventListener("message", (event) => {
506
604
  var _a;
605
+ if (event.source !== iframe.contentWindow) {
606
+ return;
607
+ }
507
608
  // Verify origin for security
508
609
  if (event.origin !== this.getBaseUrl()) {
509
610
  return;
510
611
  }
511
- const { type, data } = event.data;
612
+ const payload = event.data;
613
+ if (!payload || typeof payload !== "object")
614
+ return;
615
+ const { type, data } = payload;
616
+ if (typeof type !== "string")
617
+ return;
512
618
  switch (type) {
619
+ case IFRAME_HANDSHAKE_ACK_TYPE:
620
+ this.handshakeComplete = true;
621
+ this.flushOutboundMessageQueue();
622
+ break;
513
623
  case "ready":
514
624
  this.state.isLoaded = true;
515
625
  this.emit("ready", data);
@@ -534,14 +644,24 @@ class BuniChatWidget {
534
644
  }
535
645
  break;
536
646
  case "minimized":
537
- // User clicked minimize in chat - close chat and show trigger
538
- this.closeChat();
647
+ this.syncServerBehaviorFromData(data);
648
+ if (this.isMinimalModeEnabled()) {
649
+ if (!this.state.isOpen) {
650
+ void this.openChat();
651
+ }
652
+ this.displayMode = "minimal";
653
+ this.state.displayMode = "minimal";
654
+ }
655
+ else {
656
+ // Backward-compatible behavior.
657
+ this.closeChat();
658
+ }
539
659
  break;
540
660
  case "new_unread_message":
541
661
  // Update unread count in trigger
542
662
  this.state.unreadCount++;
543
663
  if ((_a = this.triggerIframe) === null || _a === void 0 ? void 0 : _a.contentWindow) {
544
- this.triggerIframe.contentWindow.postMessage({ type: "updateUnreadCount", count: this.state.unreadCount }, "*");
664
+ this.triggerIframe.contentWindow.postMessage({ type: "updateUnreadCount", count: this.state.unreadCount }, window.location.origin);
545
665
  }
546
666
  break;
547
667
  case "customer_data_updated":
@@ -561,17 +681,69 @@ class BuniChatWidget {
561
681
  }
562
682
  });
563
683
  }
684
+ sendParentHandshake() {
685
+ var _a, _b;
686
+ const iframeWindow = (_a = this.chatIframe) === null || _a === void 0 ? void 0 : _a.contentWindow;
687
+ if (!iframeWindow)
688
+ return;
689
+ const targetOrigin = (_b = this.chatTargetOrigin) !== null && _b !== void 0 ? _b : this.getBaseUrl();
690
+ try {
691
+ iframeWindow.postMessage({
692
+ type: PARENT_HANDSHAKE_TYPE,
693
+ data: { origin: window.location.origin },
694
+ }, targetOrigin);
695
+ }
696
+ catch (_c) {
697
+ // Best-effort; iframe may fall back to document.referrer for origin discovery.
698
+ }
699
+ }
700
+ flushOutboundMessageQueue() {
701
+ var _a, _b;
702
+ const iframeWindow = (_a = this.chatIframe) === null || _a === void 0 ? void 0 : _a.contentWindow;
703
+ if (!iframeWindow)
704
+ return;
705
+ const targetOrigin = (_b = this.chatTargetOrigin) !== null && _b !== void 0 ? _b : this.getBaseUrl();
706
+ const queued = this.outboundMessageQueue;
707
+ if (queued.length === 0)
708
+ return;
709
+ this.outboundMessageQueue = [];
710
+ for (const msg of queued) {
711
+ iframeWindow.postMessage({ type: msg.type, data: msg.data }, targetOrigin);
712
+ }
713
+ }
564
714
  postMessageToWidget(type, data) {
565
- var _a;
566
- if ((_a = this.chatIframe) === null || _a === void 0 ? void 0 : _a.contentWindow) {
567
- this.chatIframe.contentWindow.postMessage({ type, data }, this.getBaseUrl());
715
+ var _a, _b;
716
+ const iframeWindow = (_a = this.chatIframe) === null || _a === void 0 ? void 0 : _a.contentWindow;
717
+ if (!iframeWindow)
718
+ return;
719
+ const targetOrigin = (_b = this.chatTargetOrigin) !== null && _b !== void 0 ? _b : this.getBaseUrl();
720
+ // Ensure handshake is sent before any other commands; queue until ack.
721
+ if (!this.handshakeComplete && type !== PARENT_HANDSHAKE_TYPE) {
722
+ const queue = this.outboundMessageQueue;
723
+ if (queue.length < 50) {
724
+ queue.push({ type, data });
725
+ }
726
+ else {
727
+ this.outboundMessageQueue = [...queue.slice(-49), { type, data }];
728
+ }
729
+ this.sendParentHandshake();
730
+ return;
568
731
  }
732
+ iframeWindow.postMessage({ type, data }, targetOrigin);
569
733
  }
570
734
  getBaseUrl() {
735
+ var _a, _b;
571
736
  // Return the base URL for the BuniAI platform
572
737
  // Priority: 1. config.baseUrl, 2. global BUNI_API_URL, 3. default platform URL
738
+ const configUrl = (_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.config) === null || _b === void 0 ? void 0 : _b.baseUrl;
573
739
  const globalUrl = globalThis.BUNI_API_URL;
574
- return globalUrl || BUNI_PLATFORM_URL;
740
+ const rawUrl = configUrl || globalUrl || BUNI_PLATFORM_URL;
741
+ try {
742
+ return new URL(rawUrl).origin;
743
+ }
744
+ catch (_c) {
745
+ return rawUrl;
746
+ }
575
747
  }
576
748
  destroy() {
577
749
  this.postMessageToWidget("destroy");
@@ -618,10 +790,21 @@ class BuniChatWidget {
618
790
  }
619
791
  }
620
792
  minimize() {
793
+ if (this.isMinimalModeEnabled()) {
794
+ if (!this.state.isOpen) {
795
+ void this.openChat();
796
+ }
797
+ this.postMessageToWidget("minimize");
798
+ this.displayMode = "minimal";
799
+ this.state.displayMode = "minimal";
800
+ return;
801
+ }
621
802
  this.closeChat();
622
803
  }
623
804
  maximize() {
624
- this.openChat();
805
+ void this.openChat();
806
+ this.displayMode = "full";
807
+ this.state.displayMode = "full";
625
808
  }
626
809
  setCustomerData(data) {
627
810
  this.customerData = { ...this.customerData, ...data };
@@ -662,6 +845,8 @@ class BuniChatWidget {
662
845
  }
663
846
  close() {
664
847
  this.closeChat();
848
+ this.displayMode = "hidden";
849
+ this.state.displayMode = "hidden";
665
850
  }
666
851
  on(event, callback) {
667
852
  if (!this.eventListeners.has(event)) {
@@ -697,7 +882,10 @@ class BuniChatWidget {
697
882
  }
698
883
  }
699
884
  getState() {
700
- return { ...this.state };
885
+ return {
886
+ ...this.state,
887
+ displayMode: this.displayMode,
888
+ };
701
889
  }
702
890
  isReady() {
703
891
  return this.state.isLoaded;