@returningai/widget-sdk 1.4.0 → 1.4.2

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.
@@ -10336,6 +10336,9 @@ var lottie_light = { exports: {} };
10336
10336
  var lottie_lightExports = lottie_light.exports;
10337
10337
  const lottie = /* @__PURE__ */ getDefaultExportFromCjs(lottie_lightExports);
10338
10338
  function storageKey(config) {
10339
+ return `${config.storagePrefix}-${config.widgetId}-${config.container}-auth`;
10340
+ }
10341
+ function legacyStorageKey(config) {
10339
10342
  return `${config.storagePrefix}-${config.widgetId}-auth`;
10340
10343
  }
10341
10344
  function errorSettingsKey(config) {
@@ -10354,13 +10357,21 @@ function saveToStorage(config, state) {
10354
10357
  }
10355
10358
  function loadFromStorage(config, state) {
10356
10359
  try {
10357
- const raw = localStorage.getItem(storageKey(config));
10360
+ let raw = localStorage.getItem(storageKey(config));
10361
+ const loadedLegacyKey = !raw;
10362
+ if (!raw) {
10363
+ raw = localStorage.getItem(legacyStorageKey(config));
10364
+ }
10358
10365
  if (!raw) return false;
10359
10366
  const parsed = JSON.parse(raw);
10360
10367
  if (parsed.refreshToken && !isTokenExpired(parsed.refreshTokenExpiry)) {
10361
10368
  state.refreshToken = parsed.refreshToken;
10362
10369
  state.tokenFamily = parsed.tokenFamily;
10363
10370
  state.refreshTokenExpiry = parsed.refreshTokenExpiry;
10371
+ if (loadedLegacyKey) {
10372
+ saveToStorage(config, state);
10373
+ localStorage.removeItem(legacyStorageKey(config));
10374
+ }
10364
10375
  return true;
10365
10376
  }
10366
10377
  clearStorage(config);
@@ -10372,6 +10383,7 @@ function loadFromStorage(config, state) {
10372
10383
  function clearStorage(config) {
10373
10384
  try {
10374
10385
  localStorage.removeItem(storageKey(config));
10386
+ localStorage.removeItem(legacyStorageKey(config));
10375
10387
  } catch {
10376
10388
  }
10377
10389
  }
@@ -10412,6 +10424,16 @@ function setTokens(config, state, data, onRefreshScheduled) {
10412
10424
  onRefreshScheduled();
10413
10425
  }
10414
10426
  }
10427
+ function buildIdentityProof(config) {
10428
+ const proof = {};
10429
+ if (config.embedToken) {
10430
+ proof.embedToken = config.embedToken;
10431
+ }
10432
+ if (config.userIdentifiers && Object.keys(config.userIdentifiers).length > 0) {
10433
+ proof.userIdentifiers = config.userIdentifiers;
10434
+ }
10435
+ return proof;
10436
+ }
10415
10437
  async function authenticateServerless(config, state, onRefreshScheduled) {
10416
10438
  const nonce = crypto.randomUUID();
10417
10439
  const timestamp = Date.now();
@@ -10428,14 +10450,9 @@ async function authenticateServerless(config, state, onRefreshScheduled) {
10428
10450
  const body = {
10429
10451
  nonce,
10430
10452
  timestamp,
10431
- widgetType: config.widgetType
10453
+ widgetType: config.widgetType,
10454
+ ...buildIdentityProof(config)
10432
10455
  };
10433
- if (config.embedToken) {
10434
- body.embedToken = config.embedToken;
10435
- }
10436
- if (config.userIdentifiers && Object.keys(config.userIdentifiers).length > 0) {
10437
- body.userIdentifiers = config.userIdentifiers;
10438
- }
10439
10456
  const response = await fetch(`${config.apiUrl}/${config.widgetId}/auth/serverless`, {
10440
10457
  method: "POST",
10441
10458
  headers: { "Content-Type": "application/json" },
@@ -10531,6 +10548,13 @@ async function refreshAccessToken(config, state, onRefreshScheduled, sendToken)
10531
10548
  }
10532
10549
  state.isRefreshing = true;
10533
10550
  state.refreshPromise = (async () => {
10551
+ const reauthenticateAfterRefreshFailure = async () => {
10552
+ clearState(state);
10553
+ clearStorage(config);
10554
+ const authed = await authenticateServerless(config, state, onRefreshScheduled);
10555
+ if (authed && sendToken) sendToken();
10556
+ return authed;
10557
+ };
10534
10558
  try {
10535
10559
  const response = await fetch(`${config.apiUrl}/${config.widgetId}/auth/refresh`, {
10536
10560
  method: "POST",
@@ -10538,13 +10562,14 @@ async function refreshAccessToken(config, state, onRefreshScheduled, sendToken)
10538
10562
  "Content-Type": "application/json",
10539
10563
  Authorization: `Bearer ${state.refreshToken}`
10540
10564
  },
10541
- body: JSON.stringify({ widgetType: config.widgetType })
10565
+ body: JSON.stringify({
10566
+ widgetType: config.widgetType,
10567
+ ...buildIdentityProof(config)
10568
+ })
10542
10569
  });
10543
10570
  if (!response.ok) {
10544
- if (response.status === 401 || response.status === 403) {
10545
- clearState(state);
10546
- clearStorage(config);
10547
- return authenticateServerless(config, state, onRefreshScheduled);
10571
+ if (response.status === 400 || response.status === 401 || response.status === 403) {
10572
+ return reauthenticateAfterRefreshFailure();
10548
10573
  }
10549
10574
  const error = await response.json().catch(() => ({ error: "Token refresh failed" }));
10550
10575
  throw new Error(error.error || `HTTP ${response.status}`);
@@ -10557,9 +10582,7 @@ async function refreshAccessToken(config, state, onRefreshScheduled, sendToken)
10557
10582
  if (sendToken) sendToken();
10558
10583
  return true;
10559
10584
  } catch {
10560
- clearState(state);
10561
- clearStorage(config);
10562
- return authenticateServerless(config, state, onRefreshScheduled);
10585
+ return reauthenticateAfterRefreshFailure();
10563
10586
  } finally {
10564
10587
  state.isRefreshing = false;
10565
10588
  state.refreshPromise = null;
@@ -10804,6 +10827,11 @@ const DEFINED_ATTRS = /* @__PURE__ */ new Set([
10804
10827
  "accent",
10805
10828
  "font"
10806
10829
  ]);
10830
+ function isUserIdentifierAttribute(name) {
10831
+ const normalized = name.toLowerCase();
10832
+ if (!normalized.startsWith("data-")) return false;
10833
+ return !DEFINED_ATTRS.has(normalized.slice(5));
10834
+ }
10807
10835
  const WIDGET_GLOBALS = {
10808
10836
  store: "RaiStoreWidget",
10809
10837
  channel: "RaiChannelWidget",
@@ -11030,6 +11058,13 @@ function readConfig(el, existingId) {
11030
11058
  firstPaintFont: get("font") || void 0
11031
11059
  };
11032
11060
  }
11061
+ function identitySignatureForConfig(config) {
11062
+ const identifiers = Object.keys(config.userIdentifiers || {}).sort().map((key) => [key, config.userIdentifiers[key]]);
11063
+ return JSON.stringify({
11064
+ embedToken: config.embedToken || "",
11065
+ userIdentifiers: identifiers
11066
+ });
11067
+ }
11033
11068
  function createInitialState() {
11034
11069
  return {
11035
11070
  accessToken: null,
@@ -11064,11 +11099,20 @@ class BaseWidget extends HTMLElement {
11064
11099
  __publicField(this, "intersectionObserver");
11065
11100
  // #3
11066
11101
  __publicField(this, "themeObserver");
11102
+ __publicField(this, "identityAttributeObserver");
11103
+ __publicField(this, "identitySignature", "");
11104
+ __publicField(this, "hasAttemptedInit", false);
11105
+ __publicField(this, "initGeneration", 0);
11067
11106
  __publicField(this, "lottieInstances", []);
11068
11107
  this.shadow = this.attachShadow({ mode: "closed" });
11069
11108
  }
11109
+ static get observedAttributes() {
11110
+ return ["embed-token", "data-embed-token"];
11111
+ }
11070
11112
  connectedCallback() {
11071
11113
  this.config = readConfig(this, void 0);
11114
+ this.identitySignature = identitySignatureForConfig(this.config);
11115
+ this.setupIdentityAttributeObserver();
11072
11116
  const style = document.createElement("style");
11073
11117
  style.textContent = widgetCSS;
11074
11118
  this.shadow.appendChild(style);
@@ -11098,13 +11142,49 @@ class BaseWidget extends HTMLElement {
11098
11142
  }
11099
11143
  }
11100
11144
  disconnectedCallback() {
11101
- var _a, _b;
11145
+ var _a, _b, _c;
11102
11146
  (_a = this.intersectionObserver) == null ? void 0 : _a.disconnect();
11103
11147
  (_b = this.themeObserver) == null ? void 0 : _b.disconnect();
11104
- if (this.cleanupListener) this.cleanupListener();
11148
+ (_c = this.identityAttributeObserver) == null ? void 0 : _c.disconnect();
11149
+ this.teardownMountedWidget();
11105
11150
  this.clearLottieInstances();
11106
11151
  clearState(this.state);
11107
11152
  }
11153
+ attributeChangedCallback(name, oldValue, newValue) {
11154
+ if (oldValue === newValue) return;
11155
+ if (name !== "embed-token" && name !== "data-embed-token") return;
11156
+ this.handleIdentityConfigChange();
11157
+ }
11158
+ setupIdentityAttributeObserver() {
11159
+ var _a;
11160
+ (_a = this.identityAttributeObserver) == null ? void 0 : _a.disconnect();
11161
+ this.identityAttributeObserver = new MutationObserver((mutations) => {
11162
+ const hasIdentityAttributeChange = mutations.some(
11163
+ (mutation) => mutation.type === "attributes" && !!mutation.attributeName && isUserIdentifierAttribute(mutation.attributeName)
11164
+ );
11165
+ if (hasIdentityAttributeChange) {
11166
+ this.handleIdentityConfigChange();
11167
+ }
11168
+ });
11169
+ this.identityAttributeObserver.observe(this, { attributes: true });
11170
+ }
11171
+ handleIdentityConfigChange() {
11172
+ if (!this.isConnected || !this.config) return;
11173
+ const nextConfig = readConfig(this, this.config.communityId);
11174
+ const nextIdentitySignature = identitySignatureForConfig(nextConfig);
11175
+ if (nextIdentitySignature === this.identitySignature) {
11176
+ this.config = nextConfig;
11177
+ return;
11178
+ }
11179
+ const previousConfig = this.config;
11180
+ this.config = nextConfig;
11181
+ this.identitySignature = nextIdentitySignature;
11182
+ clearStorage(previousConfig);
11183
+ clearStorage(this.config);
11184
+ if (this.hasAttemptedInit) {
11185
+ void this.reload();
11186
+ }
11187
+ }
11108
11188
  // ── DOM event helper (#1) ─────────────────────────────────────────────
11109
11189
  emit(type, detail) {
11110
11190
  this.dispatchEvent(
@@ -11416,12 +11496,19 @@ class BaseWidget extends HTMLElement {
11416
11496
  }
11417
11497
  // ── Initialization ─────────────────────────────────────────────────────
11418
11498
  async init() {
11499
+ const initId = ++this.initGeneration;
11500
+ this.hasAttemptedInit = true;
11419
11501
  if (!this.config.communityId) {
11420
11502
  this.showError();
11421
11503
  return;
11422
11504
  }
11423
11505
  await fetchErrorSettings(this.config, this.state);
11506
+ if (!this.isCurrentInit(initId)) return;
11424
11507
  const launchWidget = () => {
11508
+ var _a;
11509
+ if (!this.isCurrentInit(initId)) return;
11510
+ (_a = this.errorEl) == null ? void 0 : _a.classList.remove("visible");
11511
+ this.shadow.querySelectorAll(".rai-custom-error").forEach((el) => el.remove());
11425
11512
  if (this.config.bundleUrl) {
11426
11513
  this.mountWidget();
11427
11514
  } else {
@@ -11434,6 +11521,7 @@ class BaseWidget extends HTMLElement {
11434
11521
  this.state,
11435
11522
  () => this.scheduleRefresh()
11436
11523
  );
11524
+ if (!this.isCurrentInit(initId)) return;
11437
11525
  if (authed2) {
11438
11526
  this.emit("rai-authenticated");
11439
11527
  launchWidget();
@@ -11450,6 +11538,7 @@ class BaseWidget extends HTMLElement {
11450
11538
  this.state,
11451
11539
  () => this.scheduleRefresh()
11452
11540
  );
11541
+ if (!this.isCurrentInit(initId)) return;
11453
11542
  if (authed2) {
11454
11543
  this.emit("rai-authenticated");
11455
11544
  launchWidget();
@@ -11465,6 +11554,7 @@ class BaseWidget extends HTMLElement {
11465
11554
  this.state,
11466
11555
  () => this.scheduleRefresh()
11467
11556
  );
11557
+ if (!this.isCurrentInit(initId)) return;
11468
11558
  if (!refreshed) {
11469
11559
  this.showError();
11470
11560
  return;
@@ -11479,6 +11569,7 @@ class BaseWidget extends HTMLElement {
11479
11569
  this.state,
11480
11570
  () => this.scheduleRefresh()
11481
11571
  );
11572
+ if (!this.isCurrentInit(initId)) return;
11482
11573
  if (authed) {
11483
11574
  this.emit("rai-authenticated");
11484
11575
  launchWidget();
@@ -11486,7 +11577,26 @@ class BaseWidget extends HTMLElement {
11486
11577
  this.showError();
11487
11578
  }
11488
11579
  }
11580
+ isCurrentInit(initId) {
11581
+ return this.isConnected && initId === this.initGeneration;
11582
+ }
11583
+ teardownMountedWidget() {
11584
+ if (this.cleanupListener) {
11585
+ this.cleanupListener();
11586
+ this.cleanupListener = void 0;
11587
+ }
11588
+ if (this.state.syncTimer) {
11589
+ clearInterval(this.state.syncTimer);
11590
+ this.state.syncTimer = null;
11591
+ }
11592
+ this.state.iframe = null;
11593
+ this.shadow.querySelectorAll("iframe").forEach((el) => el.remove());
11594
+ this.querySelectorAll('[data-rai-widget-root="true"]').forEach(
11595
+ (el) => el.remove()
11596
+ );
11597
+ }
11489
11598
  createIframe() {
11599
+ this.teardownMountedWidget();
11490
11600
  const iframe = document.createElement("iframe");
11491
11601
  iframe.src = this.buildWidgetUrl(this.config);
11492
11602
  iframe.allow = "clipboard-write";
@@ -11524,6 +11634,7 @@ class BaseWidget extends HTMLElement {
11524
11634
  // ── Bundle mode (no-iframe) ──────────────────────────────────────────
11525
11635
  async mountWidget() {
11526
11636
  try {
11637
+ this.teardownMountedWidget();
11527
11638
  const bundleApiUrl = this.config.v2ApiUrl || resolveV2ApiUrl(this.config.domainKey);
11528
11639
  const w = window;
11529
11640
  if (!w.DomainKey) {
@@ -11640,13 +11751,21 @@ class BaseWidget extends HTMLElement {
11640
11751
  // ── Logout helper ─────────────────────────────────────────────────────
11641
11752
  async logoutAndClear() {
11642
11753
  await logout(this.config, this.state);
11643
- this.shadow.querySelectorAll("iframe").forEach((el) => el.remove());
11754
+ this.teardownMountedWidget();
11644
11755
  this.emit("rai-logout");
11645
11756
  }
11646
11757
  // ── Public API ────────────────────────────────────────────────────────
11647
11758
  async reload() {
11759
+ var _a;
11760
+ const previousConfig = this.config;
11761
+ this.config = readConfig(this, (_a = this.config) == null ? void 0 : _a.communityId);
11762
+ this.identitySignature = identitySignatureForConfig(this.config);
11763
+ this.style.width = this.config.width;
11764
+ this.style.height = this.config.height;
11765
+ clearStorage(previousConfig);
11766
+ clearStorage(this.config);
11767
+ this.teardownMountedWidget();
11648
11768
  clearState(this.state);
11649
- this.shadow.querySelectorAll("iframe").forEach((el) => el.remove());
11650
11769
  await this.init();
11651
11770
  }
11652
11771
  async logoutPublic() {
@@ -11767,7 +11886,7 @@ class CustomWidget extends BaseWidget {
11767
11886
  }
11768
11887
  }
11769
11888
  }
11770
- console.log(`[rai-widget] v${"1.4.0"}`);
11889
+ console.log(`[rai-widget] v${"1.4.2"}`);
11771
11890
  class StoreWidgetCompat extends StoreWidget {
11772
11891
  }
11773
11892
  const WIDGET_REGISTRY = [
@@ -11831,7 +11950,7 @@ function exposePublicApi() {
11831
11950
  })();
11832
11951
  const widget = container == null ? void 0 : container.querySelector(ALL_WIDGET_SELECTOR);
11833
11952
  window.ReturningAIWidget = {
11834
- version: "1.4.0",
11953
+ version: "1.4.2",
11835
11954
  reload: () => (widget == null ? void 0 : widget.reload()) ?? Promise.resolve(),
11836
11955
  logout: () => (widget == null ? void 0 : widget.logoutPublic()) ?? Promise.resolve(),
11837
11956
  isAuthenticated: () => (widget == null ? void 0 : widget.isAuthenticated()) ?? false,
@@ -1,6 +1,7 @@
1
1
  import type { WidgetConfig } from "./types";
2
2
  export declare function readConfig(el: Element, existingId?: string): WidgetConfig;
3
3
  export declare abstract class BaseWidget extends HTMLElement {
4
+ static get observedAttributes(): string[];
4
5
  protected shadow: ShadowRoot;
5
6
  protected config: WidgetConfig;
6
7
  private state;
@@ -10,11 +11,18 @@ export declare abstract class BaseWidget extends HTMLElement {
10
11
  private cleanupListener?;
11
12
  private intersectionObserver?;
12
13
  private themeObserver?;
14
+ private identityAttributeObserver?;
15
+ private identitySignature;
16
+ private hasAttemptedInit;
17
+ private initGeneration;
13
18
  private lottieInstances;
14
19
  constructor();
15
20
  protected abstract buildWidgetUrl(config: WidgetConfig): string;
16
21
  connectedCallback(): void;
17
22
  disconnectedCallback(): void;
23
+ attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
24
+ private setupIdentityAttributeObserver;
25
+ private handleIdentityConfigChange;
18
26
  private emit;
19
27
  /**
20
28
  * Apply widget-defined CSS colors before first paint so the loader uses
@@ -77,6 +85,8 @@ export declare abstract class BaseWidget extends HTMLElement {
77
85
  private showError;
78
86
  private renderCustomError;
79
87
  private init;
88
+ private isCurrentInit;
89
+ private teardownMountedWidget;
80
90
  private createIframe;
81
91
  private mountWidget;
82
92
  private scheduleRefresh;
package/package.json CHANGED
@@ -1,33 +1,35 @@
1
- {
2
- "name": "@returningai/widget-sdk",
3
- "version": "1.4.0",
4
- "description": "Shadow DOM isolated widget SDK for ReturningAI",
5
- "main": "dist/rai-widget.iife.js",
6
- "module": "dist/rai-widget.js",
7
- "types": "dist/types/index.d.ts",
8
- "sideEffects": true,
9
- "exports": {
10
- ".": {
11
- "import": "./dist/rai-widget.js",
12
- "default": "./dist/rai-widget.iife.js"
13
- }
14
- },
15
- "files": [
16
- "dist"
17
- ],
18
- "scripts": {
19
- "dev": "vite",
20
- "build": "vite build",
21
- "build:min": "vite build --mode production",
22
- "types": "tsc --emitDeclarationOnly --outDir dist/types",
23
- "prepublishOnly": "npm run build:min && npm run types"
24
- },
25
- "devDependencies": {
26
- "terser": "^5.46.0",
27
- "typescript": "^5.3.3",
28
- "vite": "^5.1.0"
29
- },
30
- "dependencies": {
31
- "lottie-web": "^5.13.0"
32
- }
33
- }
1
+ {
2
+ "name": "@returningai/widget-sdk",
3
+ "version": "1.4.2",
4
+ "description": "Shadow DOM isolated widget SDK for ReturningAI",
5
+ "main": "dist/rai-widget.iife.js",
6
+ "module": "dist/rai-widget.js",
7
+ "types": "dist/types/index.d.ts",
8
+ "sideEffects": true,
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/rai-widget.js",
12
+ "default": "./dist/rai-widget.iife.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "CHANGELOG.md",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "dev": "vite",
22
+ "build": "vite build",
23
+ "build:min": "vite build --mode production",
24
+ "types": "tsc --emitDeclarationOnly --outDir dist/types",
25
+ "prepublishOnly": "npm run build:min && npm run types"
26
+ },
27
+ "devDependencies": {
28
+ "terser": "^5.46.0",
29
+ "typescript": "^5.3.3",
30
+ "vite": "^5.1.0"
31
+ },
32
+ "dependencies": {
33
+ "lottie-web": "^5.13.0"
34
+ }
35
+ }