@behio/storefront-sdk 2.0.1 → 2.0.3

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/next.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  BehioStorefront
3
- } from "./chunk-SCFR6JH7.mjs";
3
+ } from "./chunk-MCWHR3BY.mjs";
4
4
 
5
5
  // src/next.ts
6
6
  import { cookies, headers } from "next/headers";
package/dist/react.d.mts CHANGED
@@ -2855,6 +2855,19 @@ declare class BehioStorefront {
2855
2855
  private isRefreshing;
2856
2856
  private refreshPromise;
2857
2857
  private cartSession?;
2858
+ private requestContextVersion;
2859
+ private authContextVersion;
2860
+ /** @internal Opaque generation, never a token or customer identifier. */
2861
+ getRequestContextVersion(): number;
2862
+ /** @internal Refreshes must not restore a session that has been replaced. */
2863
+ getAuthContextVersion(): number;
2864
+ /** @internal A refresh rotates credentials within the same customer session. */
2865
+ acceptRefreshedTokens(tokens: {
2866
+ accessToken: string;
2867
+ refreshToken: string;
2868
+ }, expectedVersion: number): boolean;
2869
+ /** @internal An obsolete response must not update cache, session or retries. */
2870
+ assertRequestContext(version: number, authOnly?: boolean): void;
2858
2871
  private listeners;
2859
2872
  private requestInterceptors;
2860
2873
  private responseInterceptors;
package/dist/react.d.ts CHANGED
@@ -2855,6 +2855,19 @@ declare class BehioStorefront {
2855
2855
  private isRefreshing;
2856
2856
  private refreshPromise;
2857
2857
  private cartSession?;
2858
+ private requestContextVersion;
2859
+ private authContextVersion;
2860
+ /** @internal Opaque generation, never a token or customer identifier. */
2861
+ getRequestContextVersion(): number;
2862
+ /** @internal Refreshes must not restore a session that has been replaced. */
2863
+ getAuthContextVersion(): number;
2864
+ /** @internal A refresh rotates credentials within the same customer session. */
2865
+ acceptRefreshedTokens(tokens: {
2866
+ accessToken: string;
2867
+ refreshToken: string;
2868
+ }, expectedVersion: number): boolean;
2869
+ /** @internal An obsolete response must not update cache, session or retries. */
2870
+ assertRequestContext(version: number, authOnly?: boolean): void;
2858
2871
  private listeners;
2859
2872
  private requestInterceptors;
2860
2873
  private responseInterceptors;
package/dist/react.js CHANGED
@@ -244,6 +244,8 @@ var BehioStorefront = class {
244
244
  // Token refresh lock
245
245
  this.isRefreshing = false;
246
246
  this.refreshPromise = null;
247
+ this.requestContextVersion = 0;
248
+ this.authContextVersion = 0;
247
249
  // Event emitter
248
250
  this.listeners = /* @__PURE__ */ new Map();
249
251
  // Interceptors
@@ -287,6 +289,28 @@ var BehioStorefront = class {
287
289
  this.subscriptions = new SubscriptionsModule(this);
288
290
  this.certificates = new CourseCertificatesModule(this);
289
291
  }
292
+ /** @internal Opaque generation, never a token or customer identifier. */
293
+ getRequestContextVersion() {
294
+ return this.requestContextVersion;
295
+ }
296
+ /** @internal Refreshes must not restore a session that has been replaced. */
297
+ getAuthContextVersion() {
298
+ return this.authContextVersion;
299
+ }
300
+ /** @internal A refresh rotates credentials within the same customer session. */
301
+ acceptRefreshedTokens(tokens, expectedVersion) {
302
+ if (this.authContextVersion !== expectedVersion) return false;
303
+ if (!this.accessToken) this.setTokens(tokens);
304
+ else {
305
+ this.accessToken = tokens.accessToken;
306
+ this.refreshToken = tokens.refreshToken;
307
+ }
308
+ return true;
309
+ }
310
+ /** @internal An obsolete response must not update cache, session or retries. */
311
+ assertRequestContext(version, authOnly = false) {
312
+ if (version !== (authOnly ? this.authContextVersion : this.requestContextVersion)) throw new BehioApiError(409, null, "The storefront session or pricing context changed. Please retry in the current context.");
313
+ }
290
314
  // --- Public methods ---
291
315
  /**
292
316
  * Called by the analytics tracker when the visitor grants (id) or revokes
@@ -380,11 +404,19 @@ var BehioStorefront = class {
380
404
  }
381
405
  /** Set auth tokens (e.g. from localStorage) */
382
406
  setTokens(tokens) {
407
+ if (this.accessToken !== tokens.accessToken || this.refreshToken !== tokens.refreshToken) {
408
+ this.authContextVersion++;
409
+ this.requestContextVersion++;
410
+ }
383
411
  this.accessToken = tokens.accessToken;
384
412
  this.refreshToken = tokens.refreshToken;
385
413
  }
386
414
  /** Clear auth tokens */
387
415
  clearTokens() {
416
+ if (this.accessToken || this.refreshToken) {
417
+ this.authContextVersion++;
418
+ this.requestContextVersion++;
419
+ }
388
420
  this.accessToken = void 0;
389
421
  this.refreshToken = void 0;
390
422
  }
@@ -398,6 +430,7 @@ var BehioStorefront = class {
398
430
  }
399
431
  /** Set cart session token (e.g. from cookie) */
400
432
  setCartSession(token) {
433
+ if (this.cartSession !== token) this.requestContextVersion++;
401
434
  this.cartSession = token;
402
435
  }
403
436
  /** Get cart session token */
@@ -406,6 +439,7 @@ var BehioStorefront = class {
406
439
  }
407
440
  /** Clear cart session */
408
441
  clearCartSession() {
442
+ if (this.cartSession) this.requestContextVersion++;
409
443
  this.cartSession = void 0;
410
444
  }
411
445
  // --- Default currency / locale (runtime-switchable) ---
@@ -416,6 +450,7 @@ var BehioStorefront = class {
416
450
  * (falls back to the shop's default currency server-side).
417
451
  */
418
452
  setCurrency(currency) {
453
+ if (this.defaultCurrency !== (currency || void 0)) this.requestContextVersion++;
419
454
  this.defaultCurrency = currency || void 0;
420
455
  }
421
456
  /** Get the current default currency, if any. */
@@ -425,13 +460,16 @@ var BehioStorefront = class {
425
460
  /** Set the catalog's delivery country. Use cart.setDestination separately for the cart. */
426
461
  setCountry(country) {
427
462
  const normalized = country?.trim().toUpperCase();
428
- this.defaultCountry = normalized && /^[A-Z]{2}$/.test(normalized) ? normalized : void 0;
463
+ const next = normalized && /^[A-Z]{2}$/.test(normalized) ? normalized : void 0;
464
+ if (this.defaultCountry !== next) this.requestContextVersion++;
465
+ this.defaultCountry = next;
429
466
  }
430
467
  getCountry() {
431
468
  return this.defaultCountry;
432
469
  }
433
470
  /** Set the default locale sent on every catalog request (per-call wins). */
434
471
  setLocale(locale) {
472
+ if (this.defaultLocale !== (locale || void 0)) this.requestContextVersion++;
435
473
  this.defaultLocale = locale || void 0;
436
474
  }
437
475
  /** Get the current default locale, if any. */
@@ -487,18 +525,17 @@ var BehioStorefront = class {
487
525
  return;
488
526
  }
489
527
  this.isRefreshing = true;
528
+ const authVersion = this.authContextVersion;
490
529
  this.refreshPromise = (async () => {
491
530
  try {
492
531
  const res = await this.auth.refresh();
493
- if (res.error) {
532
+ if (res.error) throw new BehioApiError(res.error.status ?? 401, res.error.body, res.error.message);
533
+ if (this.authContextVersion === authVersion) this.emit("auth:token-refresh");
534
+ } catch (err) {
535
+ if (this.authContextVersion === authVersion) {
494
536
  this.clearTokens();
495
537
  this.emit("auth:token-refresh-failed");
496
- throw new BehioApiError(401, null, "Token refresh failed");
497
538
  }
498
- this.emit("auth:token-refresh");
499
- } catch (err) {
500
- this.clearTokens();
501
- this.emit("auth:token-refresh-failed");
502
539
  if (err instanceof BehioApiError) throw err;
503
540
  throw new BehioApiError(401, null, "Token refresh failed");
504
541
  } finally {
@@ -535,6 +572,7 @@ var BehioStorefront = class {
535
572
  * @internal — use the typed module methods (behio.certificates.downloadPdf).
536
573
  */
537
574
  async requestBlob(path) {
575
+ const authVersion = this.authContextVersion;
538
576
  try {
539
577
  const headers = { "X-Api-Key": this.apiKey };
540
578
  if (this.accessToken)
@@ -550,7 +588,9 @@ var BehioStorefront = class {
550
588
  }
551
589
  throw new BehioApiError(res.status, body);
552
590
  }
553
- return ok(await res.blob());
591
+ const blob = await res.blob();
592
+ this.assertRequestContext(authVersion, true);
593
+ return ok(blob);
554
594
  } catch (err) {
555
595
  if (this.throwOnAvailabilityError && isAvailabilityFailure(err)) {
556
596
  throw err;
@@ -564,6 +604,8 @@ var BehioStorefront = class {
564
604
  * public callers must go through `request()` which returns Result.
565
605
  */
566
606
  async rawRequest(method, path, options) {
607
+ const authOnly = path.startsWith("/auth/");
608
+ const requestVersion = authOnly ? this.authContextVersion : this.requestContextVersion;
567
609
  const params = new URLSearchParams();
568
610
  if (options?.query) {
569
611
  for (const [key, value] of Object.entries(options.query)) {
@@ -629,6 +671,7 @@ var BehioStorefront = class {
629
671
  }
630
672
  this.emit("request", { method, path });
631
673
  for (let attempt = 0; attempt <= this.retries; attempt++) {
674
+ this.assertRequestContext(requestVersion, authOnly);
632
675
  const controller = new AbortController();
633
676
  const timeoutId = setTimeout(() => controller.abort(), this.timeout);
634
677
  if (options?.signal) {
@@ -650,6 +693,7 @@ var BehioStorefront = class {
650
693
  });
651
694
  } catch (err) {
652
695
  clearTimeout(timeoutId);
696
+ this.assertRequestContext(requestVersion, authOnly);
653
697
  const isAbort = err instanceof DOMException && err.name === "AbortError";
654
698
  const networkErr = new BehioNetworkError(
655
699
  isAbort ? "Request timed out" : err.message || "Network error",
@@ -666,6 +710,7 @@ var BehioStorefront = class {
666
710
  } finally {
667
711
  clearTimeout(timeoutId);
668
712
  }
713
+ this.assertRequestContext(requestVersion, authOnly);
669
714
  const remaining = res.headers.get("X-RateLimit-Remaining");
670
715
  const reset = res.headers.get("X-RateLimit-Reset");
671
716
  if (remaining) this.rateLimitRemaining = parseInt(remaining, 10);
@@ -679,6 +724,7 @@ var BehioStorefront = class {
679
724
  this.emit("response", { method, path, status: res.status });
680
725
  if (!res.ok) {
681
726
  const body = await res.json().catch(() => null);
727
+ this.assertRequestContext(requestVersion, authOnly);
682
728
  const apiError = new BehioApiError(
683
729
  res.status,
684
730
  body,
@@ -687,11 +733,13 @@ var BehioStorefront = class {
687
733
  if (res.status === 401 && this.refreshToken && options?.auth !== false && !options?._isRetryAfterRefresh) {
688
734
  try {
689
735
  await this.handleTokenRefresh();
736
+ this.assertRequestContext(requestVersion, authOnly);
690
737
  return this.rawRequest(method, path, {
691
738
  ...options,
692
739
  _isRetryAfterRefresh: true
693
740
  });
694
- } catch {
741
+ } catch (error) {
742
+ if (error instanceof BehioApiError && error.status === 409) throw error;
695
743
  this.emit("error", apiError);
696
744
  throw apiError;
697
745
  }
@@ -716,6 +764,7 @@ var BehioStorefront = class {
716
764
  } catch {
717
765
  }
718
766
  }
767
+ this.assertRequestContext(requestVersion, authOnly);
719
768
  return responseData;
720
769
  }
721
770
  throw new BehioNetworkError("Request failed after retries");
@@ -1060,17 +1109,21 @@ var AuthModule = class {
1060
1109
  }
1061
1110
  };
1062
1111
  }
1112
+ const authVersion = this.client.getAuthContextVersion();
1063
1113
  const res = await this.client.request("POST", "/auth/refresh", {
1064
1114
  body: { refreshToken: token },
1065
1115
  auth: false
1066
1116
  });
1067
1117
  if (res.error) return res;
1068
- this.client.setTokens(res.data);
1118
+ if (!this.client.acceptRefreshedTokens(res.data, authVersion)) {
1119
+ return { data: null, error: toSdkError(new BehioApiError(409, null, "The customer session changed.")) };
1120
+ }
1069
1121
  return res;
1070
1122
  }
1071
1123
  /** Logout (invalidate refresh token) */
1072
1124
  async logout(refreshToken) {
1073
1125
  const token = refreshToken || this.client.getRefreshToken();
1126
+ const authVersion = this.client.getAuthContextVersion();
1074
1127
  const res = await this.client.request(
1075
1128
  "POST",
1076
1129
  "/auth/logout",
@@ -1078,8 +1131,10 @@ var AuthModule = class {
1078
1131
  body: { refreshToken: token }
1079
1132
  }
1080
1133
  );
1081
- this.client.clearTokens();
1082
- this.client.emit("auth:logout");
1134
+ if (this.client.getAuthContextVersion() === authVersion) {
1135
+ this.client.clearTokens();
1136
+ this.client.emit("auth:logout");
1137
+ }
1083
1138
  return res;
1084
1139
  }
1085
1140
  /** Request password reset email */
@@ -2705,6 +2760,19 @@ function useCartCount(options) {
2705
2760
  // src/react/hooks/use-auth.ts
2706
2761
  var import_react6 = require("react");
2707
2762
  var import_react_query14 = require("@tanstack/react-query");
2763
+
2764
+ // src/react/utils/clear-customer-queries.ts
2765
+ async function clearCustomerQueries(queryClient, client) {
2766
+ const version = client.getAuthContextVersion();
2767
+ await queryClient.cancelQueries({ queryKey: ["behio"] });
2768
+ if (client.getAuthContextVersion() !== version) return false;
2769
+ for (const query of queryClient.getQueryCache().findAll({ queryKey: ["behio"] })) {
2770
+ query.setState({ data: void 0, dataUpdatedAt: 0, error: null, errorUpdatedAt: 0, status: "pending" });
2771
+ }
2772
+ return true;
2773
+ }
2774
+
2775
+ // src/react/hooks/use-auth.ts
2708
2776
  var CUSTOMER_KEY = ["behio", "customer"];
2709
2777
  var CART_KEY3 = ["behio", "cart"];
2710
2778
  function useAuth() {
@@ -2727,42 +2795,51 @@ function useAuth() {
2727
2795
  },
2728
2796
  [client, storage]
2729
2797
  );
2730
- const clearAuth = (0, import_react6.useCallback)(() => {
2798
+ const clearAuth = (0, import_react6.useCallback)(async () => {
2731
2799
  client.clearTokens();
2732
2800
  storage.remove(STORAGE_KEYS.ACCESS_TOKEN);
2733
2801
  storage.remove(STORAGE_KEYS.REFRESH_TOKEN);
2802
+ if (!await clearCustomerQueries(queryClient, client)) return;
2803
+ queryClient.setQueryData([...CART_KEY3], null);
2734
2804
  queryClient.setQueryData([...CUSTOMER_KEY], null);
2735
- queryClient.invalidateQueries({ queryKey: ["behio", "orders"] });
2736
- queryClient.invalidateQueries({ queryKey: ["behio", "addresses"] });
2737
- void refreshBundleQueries(queryClient);
2805
+ if (client.getCartSession()) void queryClient.invalidateQueries({ queryKey: [...CART_KEY3] });
2806
+ void queryClient.invalidateQueries({ queryKey: ["behio"] });
2738
2807
  }, [client, storage, queryClient]);
2739
2808
  const postAuth = (0, import_react6.useCallback)(async () => {
2809
+ const authVersion = client.getAuthContextVersion();
2810
+ if (!await clearCustomerQueries(queryClient, client)) return;
2740
2811
  await queryClient.invalidateQueries({ queryKey: [...CUSTOMER_KEY] });
2812
+ if (client.getAuthContextVersion() !== authVersion) return;
2741
2813
  if (client.getCartSession()) {
2742
2814
  try {
2743
2815
  await unwrap(client.cart.merge());
2816
+ if (client.getAuthContextVersion() !== authVersion) return;
2744
2817
  client.clearCartSession();
2745
2818
  storage.remove(STORAGE_KEYS.CART_SESSION);
2746
2819
  } catch {
2747
2820
  }
2748
2821
  }
2749
- queryClient.invalidateQueries({ queryKey: [...CART_KEY3] });
2750
- void refreshBundleQueries(queryClient);
2822
+ if (client.getAuthContextVersion() !== authVersion) return;
2823
+ void queryClient.invalidateQueries({ queryKey: ["behio"] });
2751
2824
  }, [client, storage, queryClient]);
2752
2825
  const loginMutation = (0, import_react_query14.useMutation)({
2753
2826
  mutationFn: async ({ email, password }) => {
2754
- return unwrap(client.auth.login({ email, password }));
2827
+ const tokens = await unwrap(client.auth.login({ email, password }));
2828
+ return { tokens, authVersion: client.getAuthContextVersion() };
2755
2829
  },
2756
- onSuccess: async (tokens) => {
2830
+ onSuccess: async ({ tokens, authVersion }) => {
2831
+ if (client.getAuthContextVersion() !== authVersion) return;
2757
2832
  persistTokens(tokens);
2758
2833
  await postAuth();
2759
2834
  }
2760
2835
  });
2761
2836
  const registerMutation = (0, import_react_query14.useMutation)({
2762
2837
  mutationFn: async (input) => {
2763
- return unwrap(client.auth.register(input));
2838
+ const result = await unwrap(client.auth.register(input));
2839
+ return { result, authVersion: client.getAuthContextVersion() };
2764
2840
  },
2765
- onSuccess: async (result) => {
2841
+ onSuccess: async ({ result, authVersion }) => {
2842
+ if (client.getAuthContextVersion() !== authVersion) return;
2766
2843
  if (result.accessToken && result.refreshToken) {
2767
2844
  persistTokens({ accessToken: result.accessToken, refreshToken: result.refreshToken });
2768
2845
  await postAuth();
@@ -2771,8 +2848,9 @@ function useAuth() {
2771
2848
  });
2772
2849
  const logoutMutation = (0, import_react_query14.useMutation)({
2773
2850
  mutationFn: () => unwrap(client.auth.logout()),
2774
- onSettled: () => {
2775
- clearAuth();
2851
+ onMutate: () => client.getAuthContextVersion(),
2852
+ onSettled: (_result, _error, _variables, authVersion) => {
2853
+ if (!client.getAccessToken() || client.getAuthContextVersion() === authVersion) return clearAuth();
2776
2854
  }
2777
2855
  });
2778
2856
  const forgotPasswordMutation = (0, import_react_query14.useMutation)({