@returningai/widget-sdk 1.8.3 → 1.8.4

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.
@@ -12491,10 +12491,16 @@ async function refreshAccessToken(config, state, onRefreshScheduled, sendToken)
12491
12491
  if (!state.refreshToken) {
12492
12492
  return false;
12493
12493
  }
12494
+ const presentedRefreshToken = state.refreshToken;
12494
12495
  state.isRefreshing = true;
12495
12496
  state.refreshPromise = (async () => {
12496
12497
  const reauthenticateAfterRefreshFailure = async () => {
12497
- clearState(state);
12498
+ if (state.refreshToken && state.refreshToken !== presentedRefreshToken && state.accessToken && state.accessTokenExpiry && Date.now() < state.accessTokenExpiry) {
12499
+ state.isAuthenticated = true;
12500
+ if (sendToken) sendToken();
12501
+ return true;
12502
+ }
12503
+ clearAuthenticationData(state);
12498
12504
  clearStorage(config);
12499
12505
  const authed = await authenticateServerless(config, state, onRefreshScheduled);
12500
12506
  if (authed && sendToken) sendToken();
@@ -12505,7 +12511,7 @@ async function refreshAccessToken(config, state, onRefreshScheduled, sendToken)
12505
12511
  method: "POST",
12506
12512
  headers: {
12507
12513
  "Content-Type": "application/json",
12508
- Authorization: `Bearer ${state.refreshToken}`
12514
+ Authorization: `Bearer ${presentedRefreshToken}`
12509
12515
  },
12510
12516
  body: JSON.stringify({
12511
12517
  widgetType: config.widgetType,
@@ -12627,15 +12633,13 @@ async function fetchErrorSettings(config, state, opts = {}) {
12627
12633
  applyErrorSettingsToState(state, data);
12628
12634
  return state.errorSettings;
12629
12635
  }
12630
- function clearState(state) {
12636
+ function clearAuthenticationData(state) {
12631
12637
  state.accessToken = null;
12632
12638
  state.refreshToken = null;
12633
12639
  state.tokenFamily = null;
12634
12640
  state.accessTokenExpiry = null;
12635
12641
  state.refreshTokenExpiry = null;
12636
12642
  state.isAuthenticated = false;
12637
- state.isRefreshing = false;
12638
- state.refreshPromise = null;
12639
12643
  state.lastAuthError = null;
12640
12644
  state.widgetData = null;
12641
12645
  if (state.refreshTimer) {
@@ -12647,6 +12651,11 @@ function clearState(state) {
12647
12651
  state.syncTimer = null;
12648
12652
  }
12649
12653
  }
12654
+ function clearState(state) {
12655
+ clearAuthenticationData(state);
12656
+ state.isRefreshing = false;
12657
+ state.refreshPromise = null;
12658
+ }
12650
12659
  const AUTO_THEME_LEASE_STATE = "__RAI_AUTO_THEME_LEASE_STATE__";
12651
12660
  const DISABLE_AUTO_THEME = "__DISABLE_AUTO_THEME__";
12652
12661
  function acquireAutoThemeDisableLease(target = window) {
@@ -12674,6 +12683,145 @@ function acquireAutoThemeDisableLease(target = window) {
12674
12683
  delete target[AUTO_THEME_LEASE_STATE];
12675
12684
  };
12676
12685
  }
12686
+ const REGISTRY_SYMBOL = Symbol.for("returningai.widget-sdk.auth-sessions.v1");
12687
+ function registry() {
12688
+ const root = globalThis;
12689
+ root[REGISTRY_SYMBOL] ?? (root[REGISTRY_SYMBOL] = /* @__PURE__ */ new Map());
12690
+ return root[REGISTRY_SYMBOL];
12691
+ }
12692
+ function stableIdentity(config) {
12693
+ const identifiers = Object.keys(config.userIdentifiers || {}).sort().map((key) => [key, config.userIdentifiers[key]]);
12694
+ return JSON.stringify({
12695
+ embedToken: config.embedToken || "",
12696
+ userIdentifiers: identifiers
12697
+ });
12698
+ }
12699
+ function stableHash(value) {
12700
+ let first = 2166136261;
12701
+ let second = 2654435769;
12702
+ for (let index = 0; index < value.length; index += 1) {
12703
+ const code = value.charCodeAt(index);
12704
+ first = Math.imul(first ^ code, 16777619) >>> 0;
12705
+ second = Math.imul(second ^ code, 2246822507) >>> 0;
12706
+ }
12707
+ return `${first.toString(36)}${second.toString(36)}`;
12708
+ }
12709
+ function normalizeUrl(value) {
12710
+ if (!value) return "";
12711
+ try {
12712
+ const parsed = new URL(value);
12713
+ return `${parsed.origin.toLowerCase()}${parsed.pathname.replace(/\/+$/, "")}`;
12714
+ } catch {
12715
+ return value.replace(/\/+$/, "");
12716
+ }
12717
+ }
12718
+ function createAuthScopeKey(config) {
12719
+ const scope = JSON.stringify({
12720
+ apiUrl: normalizeUrl(config.apiUrl),
12721
+ authUrl: normalizeUrl(config.authUrl),
12722
+ widgetId: config.widgetId,
12723
+ widgetType: config.widgetType,
12724
+ identity: stableIdentity(config)
12725
+ });
12726
+ return `rai-auth-v1:${stableHash(scope)}`;
12727
+ }
12728
+ function captureSnapshot(state) {
12729
+ return {
12730
+ accessToken: state.accessToken,
12731
+ refreshToken: state.refreshToken,
12732
+ tokenFamily: state.tokenFamily,
12733
+ accessTokenExpiry: state.accessTokenExpiry,
12734
+ refreshTokenExpiry: state.refreshTokenExpiry,
12735
+ isAuthenticated: state.isAuthenticated,
12736
+ lastAuthError: state.lastAuthError ? { ...state.lastAuthError } : null,
12737
+ widgetData: state.widgetData
12738
+ };
12739
+ }
12740
+ function applySnapshot(subscriber, snapshot) {
12741
+ var _a;
12742
+ Object.assign(subscriber.state, snapshot);
12743
+ if (snapshot.refreshToken) saveToStorage(subscriber.config, subscriber.state);
12744
+ else clearStorage(subscriber.config);
12745
+ try {
12746
+ (_a = subscriber.onSnapshot) == null ? void 0 : _a.call(subscriber);
12747
+ } catch (error) {
12748
+ console.error("[rai-widget] Auth session subscriber failed:", error);
12749
+ }
12750
+ }
12751
+ function publishSnapshot(session, state) {
12752
+ session.generation += 1;
12753
+ session.snapshot = captureSnapshot(state);
12754
+ for (const subscriber of session.subscribers) {
12755
+ applySnapshot(subscriber, session.snapshot);
12756
+ }
12757
+ }
12758
+ function hasUsableAuthenticatedSnapshot(session) {
12759
+ const snapshot = session.snapshot;
12760
+ return Boolean(
12761
+ (snapshot == null ? void 0 : snapshot.isAuthenticated) && snapshot.accessToken && snapshot.accessTokenExpiry && Date.now() < snapshot.accessTokenExpiry
12762
+ );
12763
+ }
12764
+ function subscribeAuthSession(config, state, onSnapshot) {
12765
+ const sessions = registry();
12766
+ const scopeKey = createAuthScopeKey(config);
12767
+ let session = sessions.get(scopeKey);
12768
+ if (!session) {
12769
+ session = {
12770
+ generation: 0,
12771
+ snapshot: null,
12772
+ inFlight: null,
12773
+ subscribers: /* @__PURE__ */ new Set()
12774
+ };
12775
+ sessions.set(scopeKey, session);
12776
+ }
12777
+ const subscriber = { config, state, onSnapshot };
12778
+ session.subscribers.add(subscriber);
12779
+ if (session.snapshot) applySnapshot(subscriber, session.snapshot);
12780
+ let released = false;
12781
+ return {
12782
+ run(operation, execute) {
12783
+ if (released) return Promise.resolve(false);
12784
+ if (operation === "authenticate" && hasUsableAuthenticatedSnapshot(session)) {
12785
+ applySnapshot(subscriber, session.snapshot);
12786
+ return Promise.resolve(true);
12787
+ }
12788
+ if (session.inFlight) return session.inFlight;
12789
+ if (session.snapshot) applySnapshot(subscriber, session.snapshot);
12790
+ const startingGeneration = session.generation;
12791
+ const inFlight = (async () => {
12792
+ const succeeded = await execute();
12793
+ if (succeeded || session.generation === startingGeneration) {
12794
+ publishSnapshot(session, state);
12795
+ }
12796
+ return succeeded;
12797
+ })().finally(() => {
12798
+ if (session.inFlight === inFlight) session.inFlight = null;
12799
+ if (session.subscribers.size === 0) sessions.delete(scopeKey);
12800
+ });
12801
+ session.inFlight = inFlight;
12802
+ return inFlight;
12803
+ },
12804
+ publish() {
12805
+ if (!released) publishSnapshot(session, state);
12806
+ },
12807
+ clear() {
12808
+ if (released) return;
12809
+ state.accessToken = null;
12810
+ state.refreshToken = null;
12811
+ state.tokenFamily = null;
12812
+ state.accessTokenExpiry = null;
12813
+ state.refreshTokenExpiry = null;
12814
+ state.isAuthenticated = false;
12815
+ publishSnapshot(session, state);
12816
+ },
12817
+ release() {
12818
+ if (released) return;
12819
+ released = true;
12820
+ session.subscribers.delete(subscriber);
12821
+ if (session.subscribers.size === 0 && !session.inFlight) sessions.delete(scopeKey);
12822
+ }
12823
+ };
12824
+ }
12677
12825
  const BUNDLE_READY_TIMEOUT_MS = 3e4;
12678
12826
  function isPromiseLike(value) {
12679
12827
  return (typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function";
@@ -13859,6 +14007,7 @@ class BaseWidget extends HTMLElement {
13859
14007
  __publicField(this, "bannerNode", null);
13860
14008
  __publicField(this, "releaseAutoThemeDisableLease");
13861
14009
  __publicField(this, "bundleUnmount");
14010
+ __publicField(this, "authSession");
13862
14011
  this.shadow = this.attachShadow({ mode: "closed" });
13863
14012
  }
13864
14013
  static get observedAttributes() {
@@ -13913,7 +14062,7 @@ class BaseWidget extends HTMLElement {
13913
14062
  }
13914
14063
  }
13915
14064
  disconnectedCallback() {
13916
- var _a, _b, _c;
14065
+ var _a, _b, _c, _d;
13917
14066
  (_a = this.intersectionObserver) == null ? void 0 : _a.disconnect();
13918
14067
  (_b = this.themeObserver) == null ? void 0 : _b.disconnect();
13919
14068
  (_c = this.identityAttributeObserver) == null ? void 0 : _c.disconnect();
@@ -13921,6 +14070,8 @@ class BaseWidget extends HTMLElement {
13921
14070
  this.clearLottieInstances();
13922
14071
  this.restoreLoaderHeight();
13923
14072
  this.removeBanner();
14073
+ (_d = this.authSession) == null ? void 0 : _d.release();
14074
+ this.authSession = void 0;
13924
14075
  clearState(this.state);
13925
14076
  }
13926
14077
  attributeChangedCallback(name, oldValue, newValue) {
@@ -14521,12 +14672,22 @@ class BaseWidget extends HTMLElement {
14521
14672
  }
14522
14673
  // ── Initialization ─────────────────────────────────────────────────────
14523
14674
  async init() {
14675
+ var _a;
14524
14676
  const initId = ++this.initGeneration;
14525
14677
  this.hasAttemptedInit = true;
14526
14678
  if (!this.config.communityId) {
14527
14679
  this.showError();
14528
14680
  return;
14529
14681
  }
14682
+ if (!this.config.authUrl && !this.authSession) {
14683
+ this.authSession = subscribeAuthSession(this.config, this.state, () => {
14684
+ if (this.config.autoRefresh) this.scheduleRefresh();
14685
+ if (this.state.iframe) {
14686
+ sendTokenToWidget(this.config, this.state, this.state.iframe);
14687
+ }
14688
+ syncBundleAccessToken(this.config, this.state);
14689
+ });
14690
+ }
14530
14691
  if (this.config.bundleUrl) {
14531
14692
  this.prepareBundleRuntimeEnvironment();
14532
14693
  warmBundleRuntime(
@@ -14539,9 +14700,9 @@ class BaseWidget extends HTMLElement {
14539
14700
  (options) => fetchErrorSettings(this.config, this.state, options)
14540
14701
  );
14541
14702
  const launchWidget = () => {
14542
- var _a;
14703
+ var _a2;
14543
14704
  if (!this.isCurrentInit(initId)) return;
14544
- (_a = this.errorEl) == null ? void 0 : _a.classList.remove("visible");
14705
+ (_a2 = this.errorEl) == null ? void 0 : _a2.classList.remove("visible");
14545
14706
  this.shadow.querySelectorAll(".rai-custom-error").forEach((el2) => el2.remove());
14546
14707
  let bannerGated = false;
14547
14708
  let bannerGateTimer = 0;
@@ -14583,7 +14744,10 @@ class BaseWidget extends HTMLElement {
14583
14744
  clearState(this.state);
14584
14745
  clearStorage(this.config);
14585
14746
  const authed2 = await finishStartup(
14586
- authenticateServerless(
14747
+ this.authSession ? this.authSession.run(
14748
+ "authenticate",
14749
+ () => authenticateServerless(this.config, this.state)
14750
+ ) : authenticateServerless(
14587
14751
  this.config,
14588
14752
  this.state,
14589
14753
  () => this.scheduleRefresh()
@@ -14598,10 +14762,22 @@ class BaseWidget extends HTMLElement {
14598
14762
  }
14599
14763
  return;
14600
14764
  }
14601
- const hasStored = loadFromStorage(this.config, this.state);
14765
+ const hasSharedSession = Boolean(
14766
+ this.state.isAuthenticated && this.state.accessToken && this.state.accessTokenExpiry && Date.now() < this.state.accessTokenExpiry
14767
+ );
14768
+ const hasStored = hasSharedSession || loadFromStorage(this.config, this.state);
14602
14769
  if (hasStored) {
14770
+ if (hasSharedSession) {
14771
+ this.emit("rai-authenticated");
14772
+ launchWidget();
14773
+ return;
14774
+ }
14775
+ (_a = this.authSession) == null ? void 0 : _a.publish();
14603
14776
  const refreshed = await finishStartup(
14604
- refreshAccessToken(
14777
+ this.authSession ? this.authSession.run(
14778
+ "refresh",
14779
+ () => refreshAccessToken(this.config, this.state)
14780
+ ) : refreshAccessToken(
14605
14781
  this.config,
14606
14782
  this.state,
14607
14783
  () => this.scheduleRefresh()
@@ -14618,7 +14794,10 @@ class BaseWidget extends HTMLElement {
14618
14794
  return;
14619
14795
  }
14620
14796
  const authed = await finishStartup(
14621
- authenticateServerless(
14797
+ this.authSession ? this.authSession.run(
14798
+ "authenticate",
14799
+ () => authenticateServerless(this.config, this.state)
14800
+ ) : authenticateServerless(
14622
14801
  this.config,
14623
14802
  this.state,
14624
14803
  () => this.scheduleRefresh()
@@ -14861,7 +15040,10 @@ class BaseWidget extends HTMLElement {
14861
15040
  if (ok) pushToken();
14862
15041
  else this.handleSessionExpired();
14863
15042
  } else {
14864
- const ok = await refreshAccessToken(
15043
+ const ok = this.authSession ? await this.authSession.run(
15044
+ "refresh",
15045
+ () => refreshAccessToken(this.config, this.state)
15046
+ ) : await refreshAccessToken(
14865
15047
  this.config,
14866
15048
  this.state,
14867
15049
  () => this.scheduleRefresh(),
@@ -14914,16 +15096,20 @@ class BaseWidget extends HTMLElement {
14914
15096
  }
14915
15097
  // ── Logout helper ─────────────────────────────────────────────────────
14916
15098
  async logoutAndClear() {
15099
+ var _a;
14917
15100
  await logout(this.config, this.state);
15101
+ (_a = this.authSession) == null ? void 0 : _a.clear();
14918
15102
  this.teardownMountedWidget();
14919
15103
  this.removeBanner();
14920
15104
  this.emit("rai-logout");
14921
15105
  }
14922
15106
  // ── Public API ────────────────────────────────────────────────────────
14923
15107
  async reload() {
14924
- var _a;
15108
+ var _a, _b;
14925
15109
  const previousConfig = this.config;
14926
- this.config = readConfig(this, (_a = this.config) == null ? void 0 : _a.communityId);
15110
+ (_a = this.authSession) == null ? void 0 : _a.release();
15111
+ this.authSession = void 0;
15112
+ this.config = readConfig(this, (_b = this.config) == null ? void 0 : _b.communityId);
14927
15113
  this.identitySignature = identitySignatureForConfig(this.config);
14928
15114
  this.style.width = this.config.width;
14929
15115
  this.style.height = this.config.height;
@@ -15067,7 +15253,7 @@ class CustomWidget extends BaseWidget {
15067
15253
  }
15068
15254
  }
15069
15255
  }
15070
- console.log(`[rai-widget] v${"1.8.3"}`);
15256
+ console.log(`[rai-widget] v${"1.8.4"}`);
15071
15257
  class StoreWidgetCompat extends StoreWidget {
15072
15258
  }
15073
15259
  const WIDGET_REGISTRY = [
@@ -15131,7 +15317,7 @@ function exposePublicApi() {
15131
15317
  })();
15132
15318
  const widget = container == null ? void 0 : container.querySelector(ALL_WIDGET_SELECTOR);
15133
15319
  window.ReturningAIWidget = {
15134
- version: "1.8.3",
15320
+ version: "1.8.4",
15135
15321
  reload: () => (widget == null ? void 0 : widget.reload()) ?? Promise.resolve(),
15136
15322
  logout: () => (widget == null ? void 0 : widget.logoutPublic()) ?? Promise.resolve(),
15137
15323
  isAuthenticated: () => (widget == null ? void 0 : widget.isAuthenticated()) ?? false,
@@ -22,6 +22,7 @@ export declare abstract class BaseWidget extends HTMLElement {
22
22
  private bannerNode;
23
23
  private releaseAutoThemeDisableLease?;
24
24
  private bundleUnmount?;
25
+ private authSession?;
25
26
  constructor();
26
27
  registerCallback<K extends keyof ReturningAIWidgetCallbackMap>(callbackName: K, callback: ReturningAIWidgetCallbackMap[K]): void;
27
28
  registerCallback(callbackName: string, callback: ReturningAIWidgetCallback): void;
@@ -0,0 +1,11 @@
1
+ import type { WidgetConfig, WidgetState } from '../types';
2
+ export type AuthSessionOperation = 'authenticate' | 'refresh';
3
+ export interface AuthSessionSubscription {
4
+ run(operation: AuthSessionOperation, execute: () => Promise<boolean>): Promise<boolean>;
5
+ publish(): void;
6
+ clear(): void;
7
+ release(): void;
8
+ }
9
+ export declare function createAuthScopeKey(config: WidgetConfig): string;
10
+ export declare function subscribeAuthSession(config: WidgetConfig, state: WidgetState, onSnapshot?: () => void): AuthSessionSubscription;
11
+ export declare function resetAuthSessionCoordinatorForTests(): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@returningai/widget-sdk",
3
- "version": "1.8.3",
3
+ "version": "1.8.4",
4
4
  "description": "Shadow DOM isolated widget SDK for ReturningAI",
5
5
  "main": "dist/rai-widget.iife.js",
6
6
  "module": "dist/rai-widget.js",
@@ -25,7 +25,7 @@
25
25
  "build:all": "npm run build && npm run build:banner",
26
26
  "build:all:min": "npm run build:min && npm run build:banner:min",
27
27
  "types": "tsc --emitDeclarationOnly --outDir dist/types && cp src/jsx.d.ts dist/types/jsx.d.ts",
28
- "test": "npm run test:host-callback && npm run test:bundle-readiness && npm run test:bundle-runtime-cache && npm run test:bundle-token-sync && npm run test:auth-bootstrap && npm run test:auth-error-reporting && npm run test:auth-refresh-state && npm run test:startup-performance && npm run test:jsx-types",
28
+ "test": "npm run test:host-callback && npm run test:bundle-readiness && npm run test:bundle-runtime-cache && npm run test:bundle-token-sync && npm run test:auth-bootstrap && npm run test:auth-error-reporting && npm run test:auth-refresh-state && npm run test:auth-session-coordinator && npm run test:startup-performance && npm run test:jsx-types",
29
29
  "test:host-callback": "rm -rf test/.tmp && tsc -p test/tsconfig.json && node test/.tmp/test/postmessage-host-callback.test.js",
30
30
  "test:bundle-readiness": "rm -rf test/.tmp && tsc -p test/tsconfig.json && node test/.tmp/test/bundle-readiness.test.js",
31
31
  "test:bundle-runtime-cache": "rm -rf test/.tmp && tsc -p test/tsconfig.json && node test/.tmp/test/bundle-runtime-cache.test.js",
@@ -33,6 +33,7 @@
33
33
  "test:auth-bootstrap": "rm -rf test/.tmp && tsc -p test/tsconfig.json && node test/.tmp/test/auth-bootstrap.test.js",
34
34
  "test:auth-error-reporting": "rm -rf test/.tmp && tsc -p test/tsconfig.json && node test/.tmp/test/auth-error-reporting.test.js",
35
35
  "test:auth-refresh-state": "rm -rf test/.tmp && tsc -p test/tsconfig.json && node test/.tmp/test/auth-refresh-state.test.js",
36
+ "test:auth-session-coordinator": "rm -rf test/.tmp && tsc -p test/tsconfig.json && node test/.tmp/test/auth-session-coordinator.test.js",
36
37
  "test:startup-performance": "rm -rf test/.tmp && tsc -p test/tsconfig.json && node test/.tmp/test/startup-performance.test.js",
37
38
  "test:jsx-types": "tsc --noEmit --strict --lib ES2020,DOM --skipLibCheck false src/jsx.d.ts",
38
39
  "coverage:repowise": "rm -rf coverage && npm run coverage:host-callback && npm run coverage:bundle-readiness && npm run test:jsx-types",