@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/react.mjs
CHANGED
|
@@ -134,6 +134,8 @@ var BehioStorefront = class {
|
|
|
134
134
|
// Token refresh lock
|
|
135
135
|
this.isRefreshing = false;
|
|
136
136
|
this.refreshPromise = null;
|
|
137
|
+
this.requestContextVersion = 0;
|
|
138
|
+
this.authContextVersion = 0;
|
|
137
139
|
// Event emitter
|
|
138
140
|
this.listeners = /* @__PURE__ */ new Map();
|
|
139
141
|
// Interceptors
|
|
@@ -177,6 +179,28 @@ var BehioStorefront = class {
|
|
|
177
179
|
this.subscriptions = new SubscriptionsModule(this);
|
|
178
180
|
this.certificates = new CourseCertificatesModule(this);
|
|
179
181
|
}
|
|
182
|
+
/** @internal Opaque generation, never a token or customer identifier. */
|
|
183
|
+
getRequestContextVersion() {
|
|
184
|
+
return this.requestContextVersion;
|
|
185
|
+
}
|
|
186
|
+
/** @internal Refreshes must not restore a session that has been replaced. */
|
|
187
|
+
getAuthContextVersion() {
|
|
188
|
+
return this.authContextVersion;
|
|
189
|
+
}
|
|
190
|
+
/** @internal A refresh rotates credentials within the same customer session. */
|
|
191
|
+
acceptRefreshedTokens(tokens, expectedVersion) {
|
|
192
|
+
if (this.authContextVersion !== expectedVersion) return false;
|
|
193
|
+
if (!this.accessToken) this.setTokens(tokens);
|
|
194
|
+
else {
|
|
195
|
+
this.accessToken = tokens.accessToken;
|
|
196
|
+
this.refreshToken = tokens.refreshToken;
|
|
197
|
+
}
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
/** @internal An obsolete response must not update cache, session or retries. */
|
|
201
|
+
assertRequestContext(version, authOnly = false) {
|
|
202
|
+
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.");
|
|
203
|
+
}
|
|
180
204
|
// --- Public methods ---
|
|
181
205
|
/**
|
|
182
206
|
* Called by the analytics tracker when the visitor grants (id) or revokes
|
|
@@ -270,11 +294,19 @@ var BehioStorefront = class {
|
|
|
270
294
|
}
|
|
271
295
|
/** Set auth tokens (e.g. from localStorage) */
|
|
272
296
|
setTokens(tokens) {
|
|
297
|
+
if (this.accessToken !== tokens.accessToken || this.refreshToken !== tokens.refreshToken) {
|
|
298
|
+
this.authContextVersion++;
|
|
299
|
+
this.requestContextVersion++;
|
|
300
|
+
}
|
|
273
301
|
this.accessToken = tokens.accessToken;
|
|
274
302
|
this.refreshToken = tokens.refreshToken;
|
|
275
303
|
}
|
|
276
304
|
/** Clear auth tokens */
|
|
277
305
|
clearTokens() {
|
|
306
|
+
if (this.accessToken || this.refreshToken) {
|
|
307
|
+
this.authContextVersion++;
|
|
308
|
+
this.requestContextVersion++;
|
|
309
|
+
}
|
|
278
310
|
this.accessToken = void 0;
|
|
279
311
|
this.refreshToken = void 0;
|
|
280
312
|
}
|
|
@@ -288,6 +320,7 @@ var BehioStorefront = class {
|
|
|
288
320
|
}
|
|
289
321
|
/** Set cart session token (e.g. from cookie) */
|
|
290
322
|
setCartSession(token) {
|
|
323
|
+
if (this.cartSession !== token) this.requestContextVersion++;
|
|
291
324
|
this.cartSession = token;
|
|
292
325
|
}
|
|
293
326
|
/** Get cart session token */
|
|
@@ -296,6 +329,7 @@ var BehioStorefront = class {
|
|
|
296
329
|
}
|
|
297
330
|
/** Clear cart session */
|
|
298
331
|
clearCartSession() {
|
|
332
|
+
if (this.cartSession) this.requestContextVersion++;
|
|
299
333
|
this.cartSession = void 0;
|
|
300
334
|
}
|
|
301
335
|
// --- Default currency / locale (runtime-switchable) ---
|
|
@@ -306,6 +340,7 @@ var BehioStorefront = class {
|
|
|
306
340
|
* (falls back to the shop's default currency server-side).
|
|
307
341
|
*/
|
|
308
342
|
setCurrency(currency) {
|
|
343
|
+
if (this.defaultCurrency !== (currency || void 0)) this.requestContextVersion++;
|
|
309
344
|
this.defaultCurrency = currency || void 0;
|
|
310
345
|
}
|
|
311
346
|
/** Get the current default currency, if any. */
|
|
@@ -315,13 +350,16 @@ var BehioStorefront = class {
|
|
|
315
350
|
/** Set the catalog's delivery country. Use cart.setDestination separately for the cart. */
|
|
316
351
|
setCountry(country) {
|
|
317
352
|
const normalized = country?.trim().toUpperCase();
|
|
318
|
-
|
|
353
|
+
const next = normalized && /^[A-Z]{2}$/.test(normalized) ? normalized : void 0;
|
|
354
|
+
if (this.defaultCountry !== next) this.requestContextVersion++;
|
|
355
|
+
this.defaultCountry = next;
|
|
319
356
|
}
|
|
320
357
|
getCountry() {
|
|
321
358
|
return this.defaultCountry;
|
|
322
359
|
}
|
|
323
360
|
/** Set the default locale sent on every catalog request (per-call wins). */
|
|
324
361
|
setLocale(locale) {
|
|
362
|
+
if (this.defaultLocale !== (locale || void 0)) this.requestContextVersion++;
|
|
325
363
|
this.defaultLocale = locale || void 0;
|
|
326
364
|
}
|
|
327
365
|
/** Get the current default locale, if any. */
|
|
@@ -377,18 +415,17 @@ var BehioStorefront = class {
|
|
|
377
415
|
return;
|
|
378
416
|
}
|
|
379
417
|
this.isRefreshing = true;
|
|
418
|
+
const authVersion = this.authContextVersion;
|
|
380
419
|
this.refreshPromise = (async () => {
|
|
381
420
|
try {
|
|
382
421
|
const res = await this.auth.refresh();
|
|
383
|
-
if (res.error)
|
|
422
|
+
if (res.error) throw new BehioApiError(res.error.status ?? 401, res.error.body, res.error.message);
|
|
423
|
+
if (this.authContextVersion === authVersion) this.emit("auth:token-refresh");
|
|
424
|
+
} catch (err) {
|
|
425
|
+
if (this.authContextVersion === authVersion) {
|
|
384
426
|
this.clearTokens();
|
|
385
427
|
this.emit("auth:token-refresh-failed");
|
|
386
|
-
throw new BehioApiError(401, null, "Token refresh failed");
|
|
387
428
|
}
|
|
388
|
-
this.emit("auth:token-refresh");
|
|
389
|
-
} catch (err) {
|
|
390
|
-
this.clearTokens();
|
|
391
|
-
this.emit("auth:token-refresh-failed");
|
|
392
429
|
if (err instanceof BehioApiError) throw err;
|
|
393
430
|
throw new BehioApiError(401, null, "Token refresh failed");
|
|
394
431
|
} finally {
|
|
@@ -425,6 +462,7 @@ var BehioStorefront = class {
|
|
|
425
462
|
* @internal — use the typed module methods (behio.certificates.downloadPdf).
|
|
426
463
|
*/
|
|
427
464
|
async requestBlob(path) {
|
|
465
|
+
const authVersion = this.authContextVersion;
|
|
428
466
|
try {
|
|
429
467
|
const headers = { "X-Api-Key": this.apiKey };
|
|
430
468
|
if (this.accessToken)
|
|
@@ -440,7 +478,9 @@ var BehioStorefront = class {
|
|
|
440
478
|
}
|
|
441
479
|
throw new BehioApiError(res.status, body);
|
|
442
480
|
}
|
|
443
|
-
|
|
481
|
+
const blob = await res.blob();
|
|
482
|
+
this.assertRequestContext(authVersion, true);
|
|
483
|
+
return ok(blob);
|
|
444
484
|
} catch (err) {
|
|
445
485
|
if (this.throwOnAvailabilityError && isAvailabilityFailure(err)) {
|
|
446
486
|
throw err;
|
|
@@ -454,6 +494,8 @@ var BehioStorefront = class {
|
|
|
454
494
|
* public callers must go through `request()` which returns Result.
|
|
455
495
|
*/
|
|
456
496
|
async rawRequest(method, path, options) {
|
|
497
|
+
const authOnly = path.startsWith("/auth/");
|
|
498
|
+
const requestVersion = authOnly ? this.authContextVersion : this.requestContextVersion;
|
|
457
499
|
const params = new URLSearchParams();
|
|
458
500
|
if (options?.query) {
|
|
459
501
|
for (const [key, value] of Object.entries(options.query)) {
|
|
@@ -519,6 +561,7 @@ var BehioStorefront = class {
|
|
|
519
561
|
}
|
|
520
562
|
this.emit("request", { method, path });
|
|
521
563
|
for (let attempt = 0; attempt <= this.retries; attempt++) {
|
|
564
|
+
this.assertRequestContext(requestVersion, authOnly);
|
|
522
565
|
const controller = new AbortController();
|
|
523
566
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
524
567
|
if (options?.signal) {
|
|
@@ -540,6 +583,7 @@ var BehioStorefront = class {
|
|
|
540
583
|
});
|
|
541
584
|
} catch (err) {
|
|
542
585
|
clearTimeout(timeoutId);
|
|
586
|
+
this.assertRequestContext(requestVersion, authOnly);
|
|
543
587
|
const isAbort = err instanceof DOMException && err.name === "AbortError";
|
|
544
588
|
const networkErr = new BehioNetworkError(
|
|
545
589
|
isAbort ? "Request timed out" : err.message || "Network error",
|
|
@@ -556,6 +600,7 @@ var BehioStorefront = class {
|
|
|
556
600
|
} finally {
|
|
557
601
|
clearTimeout(timeoutId);
|
|
558
602
|
}
|
|
603
|
+
this.assertRequestContext(requestVersion, authOnly);
|
|
559
604
|
const remaining = res.headers.get("X-RateLimit-Remaining");
|
|
560
605
|
const reset = res.headers.get("X-RateLimit-Reset");
|
|
561
606
|
if (remaining) this.rateLimitRemaining = parseInt(remaining, 10);
|
|
@@ -569,6 +614,7 @@ var BehioStorefront = class {
|
|
|
569
614
|
this.emit("response", { method, path, status: res.status });
|
|
570
615
|
if (!res.ok) {
|
|
571
616
|
const body = await res.json().catch(() => null);
|
|
617
|
+
this.assertRequestContext(requestVersion, authOnly);
|
|
572
618
|
const apiError = new BehioApiError(
|
|
573
619
|
res.status,
|
|
574
620
|
body,
|
|
@@ -577,11 +623,13 @@ var BehioStorefront = class {
|
|
|
577
623
|
if (res.status === 401 && this.refreshToken && options?.auth !== false && !options?._isRetryAfterRefresh) {
|
|
578
624
|
try {
|
|
579
625
|
await this.handleTokenRefresh();
|
|
626
|
+
this.assertRequestContext(requestVersion, authOnly);
|
|
580
627
|
return this.rawRequest(method, path, {
|
|
581
628
|
...options,
|
|
582
629
|
_isRetryAfterRefresh: true
|
|
583
630
|
});
|
|
584
|
-
} catch {
|
|
631
|
+
} catch (error) {
|
|
632
|
+
if (error instanceof BehioApiError && error.status === 409) throw error;
|
|
585
633
|
this.emit("error", apiError);
|
|
586
634
|
throw apiError;
|
|
587
635
|
}
|
|
@@ -606,6 +654,7 @@ var BehioStorefront = class {
|
|
|
606
654
|
} catch {
|
|
607
655
|
}
|
|
608
656
|
}
|
|
657
|
+
this.assertRequestContext(requestVersion, authOnly);
|
|
609
658
|
return responseData;
|
|
610
659
|
}
|
|
611
660
|
throw new BehioNetworkError("Request failed after retries");
|
|
@@ -950,17 +999,21 @@ var AuthModule = class {
|
|
|
950
999
|
}
|
|
951
1000
|
};
|
|
952
1001
|
}
|
|
1002
|
+
const authVersion = this.client.getAuthContextVersion();
|
|
953
1003
|
const res = await this.client.request("POST", "/auth/refresh", {
|
|
954
1004
|
body: { refreshToken: token },
|
|
955
1005
|
auth: false
|
|
956
1006
|
});
|
|
957
1007
|
if (res.error) return res;
|
|
958
|
-
this.client.
|
|
1008
|
+
if (!this.client.acceptRefreshedTokens(res.data, authVersion)) {
|
|
1009
|
+
return { data: null, error: toSdkError(new BehioApiError(409, null, "The customer session changed.")) };
|
|
1010
|
+
}
|
|
959
1011
|
return res;
|
|
960
1012
|
}
|
|
961
1013
|
/** Logout (invalidate refresh token) */
|
|
962
1014
|
async logout(refreshToken) {
|
|
963
1015
|
const token = refreshToken || this.client.getRefreshToken();
|
|
1016
|
+
const authVersion = this.client.getAuthContextVersion();
|
|
964
1017
|
const res = await this.client.request(
|
|
965
1018
|
"POST",
|
|
966
1019
|
"/auth/logout",
|
|
@@ -968,8 +1021,10 @@ var AuthModule = class {
|
|
|
968
1021
|
body: { refreshToken: token }
|
|
969
1022
|
}
|
|
970
1023
|
);
|
|
971
|
-
this.client.
|
|
972
|
-
|
|
1024
|
+
if (this.client.getAuthContextVersion() === authVersion) {
|
|
1025
|
+
this.client.clearTokens();
|
|
1026
|
+
this.client.emit("auth:logout");
|
|
1027
|
+
}
|
|
973
1028
|
return res;
|
|
974
1029
|
}
|
|
975
1030
|
/** Request password reset email */
|
|
@@ -2595,6 +2650,19 @@ function useCartCount(options) {
|
|
|
2595
2650
|
// src/react/hooks/use-auth.ts
|
|
2596
2651
|
import { useCallback as useCallback4 } from "react";
|
|
2597
2652
|
import { useQuery as useQuery12, useMutation as useMutation2, useQueryClient as useQueryClient3 } from "@tanstack/react-query";
|
|
2653
|
+
|
|
2654
|
+
// src/react/utils/clear-customer-queries.ts
|
|
2655
|
+
async function clearCustomerQueries(queryClient, client) {
|
|
2656
|
+
const version = client.getAuthContextVersion();
|
|
2657
|
+
await queryClient.cancelQueries({ queryKey: ["behio"] });
|
|
2658
|
+
if (client.getAuthContextVersion() !== version) return false;
|
|
2659
|
+
for (const query of queryClient.getQueryCache().findAll({ queryKey: ["behio"] })) {
|
|
2660
|
+
query.setState({ data: void 0, dataUpdatedAt: 0, error: null, errorUpdatedAt: 0, status: "pending" });
|
|
2661
|
+
}
|
|
2662
|
+
return true;
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
// src/react/hooks/use-auth.ts
|
|
2598
2666
|
var CUSTOMER_KEY = ["behio", "customer"];
|
|
2599
2667
|
var CART_KEY3 = ["behio", "cart"];
|
|
2600
2668
|
function useAuth() {
|
|
@@ -2617,42 +2685,52 @@ function useAuth() {
|
|
|
2617
2685
|
},
|
|
2618
2686
|
[client, storage]
|
|
2619
2687
|
);
|
|
2620
|
-
const clearAuth = useCallback4(() => {
|
|
2688
|
+
const clearAuth = useCallback4(async () => {
|
|
2621
2689
|
client.clearTokens();
|
|
2622
2690
|
storage.remove(STORAGE_KEYS.ACCESS_TOKEN);
|
|
2623
2691
|
storage.remove(STORAGE_KEYS.REFRESH_TOKEN);
|
|
2692
|
+
if (!await clearCustomerQueries(queryClient, client)) return;
|
|
2693
|
+
queryClient.setQueryData([...CART_KEY3], null);
|
|
2624
2694
|
queryClient.setQueryData([...CUSTOMER_KEY], null);
|
|
2625
|
-
queryClient.invalidateQueries({ queryKey: [
|
|
2626
|
-
queryClient.invalidateQueries({ queryKey: ["behio"
|
|
2627
|
-
void refreshBundleQueries(queryClient);
|
|
2695
|
+
if (client.getCartSession()) void queryClient.invalidateQueries({ queryKey: [...CART_KEY3] });
|
|
2696
|
+
void queryClient.invalidateQueries({ queryKey: ["behio"] });
|
|
2628
2697
|
}, [client, storage, queryClient]);
|
|
2629
2698
|
const postAuth = useCallback4(async () => {
|
|
2699
|
+
const authVersion = client.getAuthContextVersion();
|
|
2700
|
+
if (!await clearCustomerQueries(queryClient, client)) return;
|
|
2630
2701
|
await queryClient.invalidateQueries({ queryKey: [...CUSTOMER_KEY] });
|
|
2702
|
+
if (client.getAuthContextVersion() !== authVersion) return;
|
|
2631
2703
|
if (client.getCartSession()) {
|
|
2632
2704
|
try {
|
|
2633
2705
|
await unwrap(client.cart.merge());
|
|
2706
|
+
if (client.getAuthContextVersion() !== authVersion) return;
|
|
2634
2707
|
client.clearCartSession();
|
|
2635
2708
|
storage.remove(STORAGE_KEYS.CART_SESSION);
|
|
2636
2709
|
} catch {
|
|
2637
2710
|
}
|
|
2638
2711
|
}
|
|
2712
|
+
if (client.getAuthContextVersion() !== authVersion) return;
|
|
2639
2713
|
queryClient.invalidateQueries({ queryKey: [...CART_KEY3] });
|
|
2640
2714
|
void refreshBundleQueries(queryClient);
|
|
2641
2715
|
}, [client, storage, queryClient]);
|
|
2642
2716
|
const loginMutation = useMutation2({
|
|
2643
2717
|
mutationFn: async ({ email, password }) => {
|
|
2644
|
-
|
|
2718
|
+
const tokens = await unwrap(client.auth.login({ email, password }));
|
|
2719
|
+
return { tokens, authVersion: client.getAuthContextVersion() };
|
|
2645
2720
|
},
|
|
2646
|
-
onSuccess: async (tokens) => {
|
|
2721
|
+
onSuccess: async ({ tokens, authVersion }) => {
|
|
2722
|
+
if (client.getAuthContextVersion() !== authVersion) return;
|
|
2647
2723
|
persistTokens(tokens);
|
|
2648
2724
|
await postAuth();
|
|
2649
2725
|
}
|
|
2650
2726
|
});
|
|
2651
2727
|
const registerMutation = useMutation2({
|
|
2652
2728
|
mutationFn: async (input) => {
|
|
2653
|
-
|
|
2729
|
+
const result = await unwrap(client.auth.register(input));
|
|
2730
|
+
return { result, authVersion: client.getAuthContextVersion() };
|
|
2654
2731
|
},
|
|
2655
|
-
onSuccess: async (result) => {
|
|
2732
|
+
onSuccess: async ({ result, authVersion }) => {
|
|
2733
|
+
if (client.getAuthContextVersion() !== authVersion) return;
|
|
2656
2734
|
if (result.accessToken && result.refreshToken) {
|
|
2657
2735
|
persistTokens({ accessToken: result.accessToken, refreshToken: result.refreshToken });
|
|
2658
2736
|
await postAuth();
|
|
@@ -2661,8 +2739,9 @@ function useAuth() {
|
|
|
2661
2739
|
});
|
|
2662
2740
|
const logoutMutation = useMutation2({
|
|
2663
2741
|
mutationFn: () => unwrap(client.auth.logout()),
|
|
2664
|
-
|
|
2665
|
-
|
|
2742
|
+
onMutate: () => client.getAuthContextVersion(),
|
|
2743
|
+
onSettled: (_result, _error, _variables, authVersion) => {
|
|
2744
|
+
if (!client.getAccessToken() || client.getAuthContextVersion() === authVersion) return clearAuth();
|
|
2666
2745
|
}
|
|
2667
2746
|
});
|
|
2668
2747
|
const forgotPasswordMutation = useMutation2({
|