@behio/storefront-sdk 2.0.1 → 2.0.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.
- package/README.md +8 -0
- package/dist/{chunk-SCFR6JH7.mjs → chunk-MCWHR3BY.mjs} +67 -12
- package/dist/{chunk-TDIAWIQR.js → chunk-SF7JTW3Y.js} +68 -13
- package/dist/{client-C4dnHtID.d.mts → client-Gf4MBpav.d.mts} +13 -0
- package/dist/{client-C4dnHtID.d.ts → client-Gf4MBpav.d.ts} +13 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/index.mjs +1 -1
- package/dist/next.d.mts +1 -1
- package/dist/next.d.ts +1 -1
- package/dist/next.js +2 -2
- package/dist/next.mjs +1 -1
- package/dist/react.d.mts +13 -0
- package/dist/react.d.ts +13 -0
- package/dist/react.js +101 -22
- package/dist/react.mjs +101 -22
- package/package.json +1 -1
package/dist/next.mjs
CHANGED
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
1082
|
-
|
|
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,52 @@ 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: [
|
|
2736
|
-
queryClient.invalidateQueries({ queryKey: ["behio"
|
|
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
|
}
|
|
2822
|
+
if (client.getAuthContextVersion() !== authVersion) return;
|
|
2749
2823
|
queryClient.invalidateQueries({ queryKey: [...CART_KEY3] });
|
|
2750
2824
|
void refreshBundleQueries(queryClient);
|
|
2751
2825
|
}, [client, storage, queryClient]);
|
|
2752
2826
|
const loginMutation = (0, import_react_query14.useMutation)({
|
|
2753
2827
|
mutationFn: async ({ email, password }) => {
|
|
2754
|
-
|
|
2828
|
+
const tokens = await unwrap(client.auth.login({ email, password }));
|
|
2829
|
+
return { tokens, authVersion: client.getAuthContextVersion() };
|
|
2755
2830
|
},
|
|
2756
|
-
onSuccess: async (tokens) => {
|
|
2831
|
+
onSuccess: async ({ tokens, authVersion }) => {
|
|
2832
|
+
if (client.getAuthContextVersion() !== authVersion) return;
|
|
2757
2833
|
persistTokens(tokens);
|
|
2758
2834
|
await postAuth();
|
|
2759
2835
|
}
|
|
2760
2836
|
});
|
|
2761
2837
|
const registerMutation = (0, import_react_query14.useMutation)({
|
|
2762
2838
|
mutationFn: async (input) => {
|
|
2763
|
-
|
|
2839
|
+
const result = await unwrap(client.auth.register(input));
|
|
2840
|
+
return { result, authVersion: client.getAuthContextVersion() };
|
|
2764
2841
|
},
|
|
2765
|
-
onSuccess: async (result) => {
|
|
2842
|
+
onSuccess: async ({ result, authVersion }) => {
|
|
2843
|
+
if (client.getAuthContextVersion() !== authVersion) return;
|
|
2766
2844
|
if (result.accessToken && result.refreshToken) {
|
|
2767
2845
|
persistTokens({ accessToken: result.accessToken, refreshToken: result.refreshToken });
|
|
2768
2846
|
await postAuth();
|
|
@@ -2771,8 +2849,9 @@ function useAuth() {
|
|
|
2771
2849
|
});
|
|
2772
2850
|
const logoutMutation = (0, import_react_query14.useMutation)({
|
|
2773
2851
|
mutationFn: () => unwrap(client.auth.logout()),
|
|
2774
|
-
|
|
2775
|
-
|
|
2852
|
+
onMutate: () => client.getAuthContextVersion(),
|
|
2853
|
+
onSettled: (_result, _error, _variables, authVersion) => {
|
|
2854
|
+
if (!client.getAccessToken() || client.getAuthContextVersion() === authVersion) return clearAuth();
|
|
2776
2855
|
}
|
|
2777
2856
|
});
|
|
2778
2857
|
const forgotPasswordMutation = (0, import_react_query14.useMutation)({
|