@doswiftly/storefront-sdk 16.1.0 → 18.0.0
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/CHANGELOG.md +1255 -0
- package/README.md +16 -4
- package/dist/core/auth/auth-client.d.ts +39 -3
- package/dist/core/auth/auth-client.d.ts.map +1 -1
- package/dist/core/auth/auth-client.js +51 -3
- package/dist/core/auth/cookie-config.d.ts +52 -3
- package/dist/core/auth/cookie-config.d.ts.map +1 -1
- package/dist/core/auth/cookie-config.js +60 -6
- package/dist/core/auth/handlers.d.ts +46 -0
- package/dist/core/auth/handlers.d.ts.map +1 -1
- package/dist/core/auth/handlers.js +9 -2
- package/dist/core/auth/session-events.d.ts +38 -0
- package/dist/core/auth/session-events.d.ts.map +1 -0
- package/dist/core/auth/session-events.js +35 -0
- package/dist/core/cart/cart-client.d.ts +10 -1
- package/dist/core/cart/cart-client.d.ts.map +1 -1
- package/dist/core/cart/cart-client.js +17 -1
- package/dist/core/cart/cart-recovery.d.ts +23 -0
- package/dist/core/cart/cart-recovery.d.ts.map +1 -1
- package/dist/core/cart/cart-recovery.js +20 -3
- package/dist/core/cart/types.d.ts +2 -1
- package/dist/core/cart/types.d.ts.map +1 -1
- package/dist/core/cart/types.js +7 -1
- package/dist/core/client/create-client.d.ts.map +1 -1
- package/dist/core/client/create-client.js +7 -3
- package/dist/core/client/execute.d.ts +29 -3
- package/dist/core/client/execute.d.ts.map +1 -1
- package/dist/core/client/execute.js +174 -3
- package/dist/core/client/types.d.ts +50 -2
- package/dist/core/client/types.d.ts.map +1 -1
- package/dist/core/errors.d.ts +6 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +6 -0
- package/dist/core/generated/operation-types.d.ts +937 -182
- package/dist/core/generated/operation-types.d.ts.map +1 -1
- package/dist/core/generated/operation-types.js +560 -1
- package/dist/core/index.d.ts +6 -3
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +12 -2
- package/dist/core/middleware/session-retry.d.ts +47 -0
- package/dist/core/middleware/session-retry.d.ts.map +1 -0
- package/dist/core/middleware/session-retry.js +71 -0
- package/dist/core/operations/auth.d.ts.map +1 -1
- package/dist/core/operations/auth.js +1 -0
- package/dist/core/operations/cart.d.ts +7 -0
- package/dist/core/operations/cart.d.ts.map +1 -1
- package/dist/core/operations/cart.js +54 -3
- package/dist/react/components/PaymentInstrumentSection.d.ts +56 -0
- package/dist/react/components/PaymentInstrumentSection.d.ts.map +1 -0
- package/dist/react/components/PaymentInstrumentSection.js +89 -0
- package/dist/react/components/PaymentInstrumentTile.d.ts +56 -0
- package/dist/react/components/PaymentInstrumentTile.d.ts.map +1 -0
- package/dist/react/components/PaymentInstrumentTile.js +41 -0
- package/dist/react/components/index.d.ts +2 -0
- package/dist/react/components/index.d.ts.map +1 -1
- package/dist/react/components/index.js +2 -0
- package/dist/react/helpers/browser-data.d.ts +89 -0
- package/dist/react/helpers/browser-data.d.ts.map +1 -0
- package/dist/react/helpers/browser-data.js +84 -0
- package/dist/react/hooks/use-cart-manager.d.ts +104 -13
- package/dist/react/hooks/use-cart-manager.d.ts.map +1 -1
- package/dist/react/hooks/use-cart-manager.js +144 -12
- package/dist/react/hooks/use-login.d.ts.map +1 -1
- package/dist/react/hooks/use-login.js +3 -3
- package/dist/react/hooks/use-refresh-token.d.ts.map +1 -1
- package/dist/react/hooks/use-refresh-token.js +6 -4
- package/dist/react/hooks/use-session-expired.d.ts +16 -0
- package/dist/react/hooks/use-session-expired.d.ts.map +1 -0
- package/dist/react/hooks/use-session-expired.js +26 -0
- package/dist/react/hooks/use-session-refresh.d.ts +32 -0
- package/dist/react/hooks/use-session-refresh.d.ts.map +1 -0
- package/dist/react/hooks/use-session-refresh.js +147 -0
- package/dist/react/index.d.ts +5 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +5 -1
- package/dist/react/providers/storefront-client-provider.d.ts +10 -1
- package/dist/react/providers/storefront-client-provider.d.ts.map +1 -1
- package/dist/react/providers/storefront-client-provider.js +38 -3
- package/dist/react/providers/storefront-provider.d.ts +51 -3
- package/dist/react/providers/storefront-provider.d.ts.map +1 -1
- package/dist/react/providers/storefront-provider.js +22 -5
- package/dist/react/server/create-storefront-auth-route.d.ts +63 -0
- package/dist/react/server/create-storefront-auth-route.d.ts.map +1 -0
- package/dist/react/server/create-storefront-auth-route.js +239 -0
- package/dist/react/server/get-initial-auth.d.ts +57 -0
- package/dist/react/server/get-initial-auth.d.ts.map +1 -0
- package/dist/react/server/get-initial-auth.js +55 -0
- package/dist/react/server/index.d.ts +3 -0
- package/dist/react/server/index.d.ts.map +1 -1
- package/dist/react/server/index.js +6 -0
- package/dist/react/stores/auth.store.d.ts +46 -2
- package/dist/react/stores/auth.store.d.ts.map +1 -1
- package/dist/react/stores/auth.store.js +19 -7
- package/package.json +4 -2
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSessionRefresh — proactive, browser-only session-refresh scheduler.
|
|
3
|
+
*
|
|
4
|
+
* Renews the access token shortly *before* it expires so an active buyer is
|
|
5
|
+
* never logged out mid-session, reschedules from each new expiry, and never runs
|
|
6
|
+
* on the server. On tab wake after the token already lapsed it tries once and —
|
|
7
|
+
* if the session can no longer be recovered — emits `session-expired` and clears
|
|
8
|
+
* local auth.
|
|
9
|
+
*
|
|
10
|
+
* The refresh goes through the same-origin BFF route via
|
|
11
|
+
* `AuthClient.refreshSession()` (`POST {authBasePath}/refresh`). The route handler
|
|
12
|
+
* reads the httpOnly refresh cookie server-side, rotates it against the backend,
|
|
13
|
+
* and sets the new first-party cookies — so an ALREADY-EXPIRED access token still
|
|
14
|
+
* refreshes (the old GraphQL `customerRefreshToken` could not, as it needed a
|
|
15
|
+
* valid access token). No `setToken` round-trip and no consumer callback.
|
|
16
|
+
*
|
|
17
|
+
* The timer lives in the effect closure (provider lifecycle), never in module state.
|
|
18
|
+
*/
|
|
19
|
+
'use client';
|
|
20
|
+
import { useEffect, useRef } from 'react';
|
|
21
|
+
import { useAuthStoreApi } from '../stores/store-context';
|
|
22
|
+
import { useStorefrontClientContext } from '../providers/storefront-client-provider';
|
|
23
|
+
const DEFAULT_BUFFER_MS = 60_000;
|
|
24
|
+
/** Backoff before retrying a proactive refresh that failed while the token was still valid. */
|
|
25
|
+
const PROACTIVE_RETRY_MS = 15_000;
|
|
26
|
+
/**
|
|
27
|
+
* When the token is already within the buffer window but still valid, space out
|
|
28
|
+
* refreshes by this much so a token whose lifetime is <= the buffer cannot
|
|
29
|
+
* busy-loop the scheduler.
|
|
30
|
+
*/
|
|
31
|
+
const WITHIN_BUFFER_RETRY_MS = 5_000;
|
|
32
|
+
export function useSessionRefresh(options = {}) {
|
|
33
|
+
const { enabled, bufferMs = DEFAULT_BUFFER_MS, sessionExpiredEmitter } = options;
|
|
34
|
+
const isBrowser = typeof window !== 'undefined';
|
|
35
|
+
const active = (enabled ?? isBrowser) && isBrowser;
|
|
36
|
+
const { authClient } = useStorefrontClientContext();
|
|
37
|
+
const authStore = useAuthStoreApi();
|
|
38
|
+
// Moving parts in refs so the scheduling effect re-runs only on structural
|
|
39
|
+
// deps (active / bufferMs / authBasePath / store identity), not on every render.
|
|
40
|
+
const authClientRef = useRef(authClient);
|
|
41
|
+
authClientRef.current = authClient;
|
|
42
|
+
const emitterRef = useRef(sessionExpiredEmitter);
|
|
43
|
+
emitterRef.current = sessionExpiredEmitter;
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!active)
|
|
46
|
+
return;
|
|
47
|
+
let timer;
|
|
48
|
+
let disposed = false;
|
|
49
|
+
let inFlight = false;
|
|
50
|
+
// Tracks the expiry we last scheduled against, so the store subscription
|
|
51
|
+
// ignores our own writes (we reschedule explicitly after a refresh).
|
|
52
|
+
let lastExpiresAt = authStore.getState().expiresAt;
|
|
53
|
+
const clearTimer = () => {
|
|
54
|
+
if (timer !== undefined) {
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
timer = undefined;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const doRefresh = async (tokenAlreadyExpired) => {
|
|
60
|
+
if (inFlight)
|
|
61
|
+
return;
|
|
62
|
+
inFlight = true;
|
|
63
|
+
try {
|
|
64
|
+
// Same-origin BFF refresh: the route reads the httpOnly refresh cookie
|
|
65
|
+
// server-side, rotates it against the backend, and sets the new
|
|
66
|
+
// first-party cookies. No `setToken` round-trip — the route owns the cookie.
|
|
67
|
+
const result = await authClientRef.current.refreshSession();
|
|
68
|
+
if (disposed)
|
|
69
|
+
return;
|
|
70
|
+
lastExpiresAt = result.expiresAt;
|
|
71
|
+
authStore.getState().setAuth(authStore.getState().customer, result.accessToken, result.expiresAt);
|
|
72
|
+
schedule();
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if (disposed)
|
|
76
|
+
return;
|
|
77
|
+
if (tokenAlreadyExpired) {
|
|
78
|
+
// The access token had already lapsed and the BFF refresh could not
|
|
79
|
+
// recover the session (refresh token expired/reused/revoked). Clear
|
|
80
|
+
// local auth and notify so the storefront can prompt re-login.
|
|
81
|
+
authStore.getState().clearAuth();
|
|
82
|
+
emitterRef.current?.emit({ reason: 'wake-refresh-failed', cause: err });
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// Token still valid — a proactive refresh failed transiently
|
|
86
|
+
// (network / server blip). Do NOT log the buyer out; retry shortly.
|
|
87
|
+
clearTimer();
|
|
88
|
+
timer = setTimeout(schedule, PROACTIVE_RETRY_MS);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
inFlight = false;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const schedule = () => {
|
|
96
|
+
clearTimer();
|
|
97
|
+
const { expiresAt, isAuthenticated } = authStore.getState();
|
|
98
|
+
if (!expiresAt || !isAuthenticated)
|
|
99
|
+
return;
|
|
100
|
+
const expiryMs = new Date(expiresAt).getTime();
|
|
101
|
+
if (Number.isNaN(expiryMs))
|
|
102
|
+
return;
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
let fireIn;
|
|
105
|
+
if (now >= expiryMs) {
|
|
106
|
+
// Already expired (e.g. tab slept past expiry) — recover ASAP.
|
|
107
|
+
fireIn = 0;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
const untilBuffer = expiryMs - bufferMs - now;
|
|
111
|
+
// Within the buffer but still valid — space retries so a token whose
|
|
112
|
+
// lifetime is <= the buffer cannot busy-loop.
|
|
113
|
+
fireIn = untilBuffer > 0 ? untilBuffer : WITHIN_BUFFER_RETRY_MS;
|
|
114
|
+
}
|
|
115
|
+
// ALWAYS arm a timer — never call doRefresh synchronously. A synchronous
|
|
116
|
+
// call can re-enter while a refresh is in flight and be dropped by the
|
|
117
|
+
// in-flight guard, leaving the scheduler permanently stalled; the timer
|
|
118
|
+
// fires after the current call stack, by which point `inFlight` has reset.
|
|
119
|
+
// `tokenAlreadyExpired` is re-derived at fire time (the clock has moved).
|
|
120
|
+
timer = setTimeout(() => void doRefresh(Date.now() >= expiryMs), fireIn);
|
|
121
|
+
};
|
|
122
|
+
// Reschedule when expiresAt changes from outside (login, manual refresh).
|
|
123
|
+
const unsubscribe = authStore.subscribe((state) => {
|
|
124
|
+
if (disposed)
|
|
125
|
+
return;
|
|
126
|
+
if (state.expiresAt !== lastExpiresAt) {
|
|
127
|
+
lastExpiresAt = state.expiresAt;
|
|
128
|
+
schedule();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
// Wake-from-sleep: background tabs throttle/pause timers — on return,
|
|
132
|
+
// recompute (and refresh immediately if the token already lapsed).
|
|
133
|
+
const onVisibility = () => {
|
|
134
|
+
if (document.visibilityState === 'visible' && !disposed)
|
|
135
|
+
schedule();
|
|
136
|
+
};
|
|
137
|
+
document.addEventListener('visibilitychange', onVisibility);
|
|
138
|
+
schedule();
|
|
139
|
+
return () => {
|
|
140
|
+
disposed = true;
|
|
141
|
+
clearTimer();
|
|
142
|
+
unsubscribe();
|
|
143
|
+
document.removeEventListener('visibilitychange', onVisibility);
|
|
144
|
+
};
|
|
145
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
146
|
+
}, [active, bufferMs, authStore]);
|
|
147
|
+
}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -19,6 +19,9 @@ export { useAuth, type UseAuthOptions, type LoginResult, type LogoutResult, type
|
|
|
19
19
|
export { useLogin, type UseLoginOptions, type UseLoginReturn } from './hooks/use-login';
|
|
20
20
|
export { useLogout, type UseLogoutOptions, type UseLogoutReturn } from './hooks/use-logout';
|
|
21
21
|
export { useRefreshToken, type UseRefreshTokenOptions, type UseRefreshTokenReturn } from './hooks/use-refresh-token';
|
|
22
|
+
export { useSessionRefresh, type UseSessionRefreshOptions } from './hooks/use-session-refresh';
|
|
23
|
+
export { useSessionExpired } from './hooks/use-session-expired';
|
|
24
|
+
export type { SessionExpiredEvent, SessionExpiredReason, SessionExpiredEmitter } from '../core/auth/session-events';
|
|
22
25
|
export { useCartManager, type CartManagerOperation, type CartManagerStatus, type UseCartManagerResult } from './hooks/use-cart-manager';
|
|
23
26
|
export { useCart, type UseCartOptions, type UseCartResult, type ServerCartOperation } from './hooks/use-cart';
|
|
24
27
|
export { useStorefrontClient } from './hooks/use-storefront-client';
|
|
@@ -41,5 +44,6 @@ export { createCartStore, selectCartId, selectIsCartOpen, selectCartIsLoading, }
|
|
|
41
44
|
export type { CartState, CartStoreConfig, CartActions, CartData, CartMutationAction, CartLineInput, CartLineUpdateInput, } from './stores/cart.store';
|
|
42
45
|
export { CartProvider, useCartStore, useCartStoreApi } from './stores/cart.context';
|
|
43
46
|
export { createStoreContext } from './helpers/create-store-context';
|
|
44
|
-
export { Money, type MoneyProps, Image, type ImageComponentProps, CartCount, type CartCountProps, AddToCartButton, type AddToCartButtonProps, PriceDisplay, type PriceDisplayProps, CartTotals, type CartTotalsProps, type CartTotalsLabels, } from './components';
|
|
47
|
+
export { Money, type MoneyProps, Image, type ImageComponentProps, CartCount, type CartCountProps, AddToCartButton, type AddToCartButtonProps, PriceDisplay, type PriceDisplayProps, CartTotals, type CartTotalsProps, type CartTotalsLabels, PaymentInstrumentTile, type PaymentInstrumentTileProps, type PaymentInstrumentTileInstrument, PaymentInstrumentSection, type PaymentInstrumentSectionProps, type PaymentInstrumentSectionMethod, } from './components';
|
|
48
|
+
export { getBrowserDataForPayment, BrowserDataNotAvailableError, type PaymentBrowserData, } from './helpers/browser-data';
|
|
45
49
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
|
package/dist/react/index.js
CHANGED
|
@@ -21,6 +21,8 @@ export { useAuth } from './hooks/use-auth';
|
|
|
21
21
|
export { useLogin } from './hooks/use-login';
|
|
22
22
|
export { useLogout } from './hooks/use-logout';
|
|
23
23
|
export { useRefreshToken } from './hooks/use-refresh-token';
|
|
24
|
+
export { useSessionRefresh } from './hooks/use-session-refresh';
|
|
25
|
+
export { useSessionExpired } from './hooks/use-session-expired';
|
|
24
26
|
export { useCartManager } from './hooks/use-cart-manager';
|
|
25
27
|
export { useCart } from './hooks/use-cart';
|
|
26
28
|
export { useStorefrontClient } from './hooks/use-storefront-client';
|
|
@@ -47,4 +49,6 @@ export { CartProvider, useCartStore, useCartStoreApi } from './stores/cart.conte
|
|
|
47
49
|
// Store context helper
|
|
48
50
|
export { createStoreContext } from './helpers/create-store-context';
|
|
49
51
|
// Pre-built components (headless — no styling, accessibility-aware)
|
|
50
|
-
export { Money, Image, CartCount, AddToCartButton, PriceDisplay, CartTotals, } from './components';
|
|
52
|
+
export { Money, Image, CartCount, AddToCartButton, PriceDisplay, CartTotals, PaymentInstrumentTile, PaymentInstrumentSection, } from './components';
|
|
53
|
+
// Browser context helpers (PSD2/3DS2 — browser-only)
|
|
54
|
+
export { getBrowserDataForPayment, BrowserDataNotAvailableError, } from './helpers/browser-data';
|
|
@@ -11,6 +11,7 @@ import { CartClient } from '../../core/cart/cart-client';
|
|
|
11
11
|
import { AuthClient } from '../../core/auth/auth-client';
|
|
12
12
|
import { type BotProtectionTokenProvider } from '../../core/middleware/bot-protection';
|
|
13
13
|
import type { StorefrontClient, StorefrontClientConfig, Middleware } from '../../core/client/types';
|
|
14
|
+
import type { SessionExpiredEmitter } from '../../core/auth/session-events';
|
|
14
15
|
export interface StorefrontClientContextValue {
|
|
15
16
|
client: StorefrontClient;
|
|
16
17
|
cartClient: CartClient;
|
|
@@ -28,8 +29,16 @@ export interface StorefrontClientProviderProps {
|
|
|
28
29
|
botProtection?: BotProtectionTokenProvider | null;
|
|
29
30
|
/** Operations that require bot protection (from shop query) */
|
|
30
31
|
botProtectionOperations?: string[];
|
|
32
|
+
/**
|
|
33
|
+
* Auth-level session-expired emitter (from StorefrontProvider). When present,
|
|
34
|
+
* a reactive 401 on a read query triggers a single deduped refresh + replay,
|
|
35
|
+
* while a 401 on a mutation — or a refresh that also fails — fires this emitter.
|
|
36
|
+
*/
|
|
37
|
+
sessionExpiredEmitter?: SessionExpiredEmitter;
|
|
38
|
+
/** Base path of the auth route handlers used to sync the httpOnly cookie after a reactive refresh (default `/api/auth`). */
|
|
39
|
+
authBasePath?: string;
|
|
31
40
|
}
|
|
32
|
-
export declare function StorefrontClientProvider({ children, config, middleware: customMiddleware, botProtection, botProtectionOperations, }: StorefrontClientProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
41
|
+
export declare function StorefrontClientProvider({ children, config, middleware: customMiddleware, botProtection, botProtectionOperations, sessionExpiredEmitter, authBasePath, }: StorefrontClientProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
33
42
|
/**
|
|
34
43
|
* Get StorefrontClient context value.
|
|
35
44
|
* Must be used within StorefrontClientProvider.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storefront-client-provider.d.ts","sourceRoot":"","sources":["../../../src/react/providers/storefront-client-provider.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAA6C,MAAM,OAAO,CAAC;AAElE,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAIzD,OAAO,EAA2B,KAAK,0BAA0B,EAAE,MAAM,sCAAsC,CAAC;AAKhH,OAAO,KAAK,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAEpG,MAAM,WAAW,4BAA4B;IAC3C,MAAM,EAAE,gBAAgB,CAAC;IACzB,UAAU,EAAE,UAAU,CAAC;IACvB,UAAU,EAAE,UAAU,CAAC;CACxB;AAID,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,MAAM,EAAE,sBAAsB,CAAC;IAC/B;;;OAGG;IACH,UAAU,CAAC,EAAE,UAAU,EAAE,CAAC;IAC1B,oEAAoE;IACpE,aAAa,CAAC,EAAE,0BAA0B,GAAG,IAAI,CAAC;IAClD,+DAA+D;IAC/D,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"storefront-client-provider.d.ts","sourceRoot":"","sources":["../../../src/react/providers/storefront-client-provider.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAA6C,MAAM,OAAO,CAAC;AAElE,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAIzD,OAAO,EAA2B,KAAK,0BAA0B,EAAE,MAAM,sCAAsC,CAAC;AAKhH,OAAO,KAAK,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAEpG,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAE5E,MAAM,WAAW,4BAA4B;IAC3C,MAAM,EAAE,gBAAgB,CAAC;IACzB,UAAU,EAAE,UAAU,CAAC;IACvB,UAAU,EAAE,UAAU,CAAC;CACxB;AAID,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,MAAM,EAAE,sBAAsB,CAAC;IAC/B;;;OAGG;IACH,UAAU,CAAC,EAAE,UAAU,EAAE,CAAC;IAC1B,oEAAoE;IACpE,aAAa,CAAC,EAAE,0BAA0B,GAAG,IAAI,CAAC;IAClD,+DAA+D;IAC/D,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;IACnC;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,qBAAqB,CAAC;IAC9C,4HAA4H;IAC5H,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,wBAAwB,CAAC,EACvC,QAAQ,EACR,MAAM,EACN,UAAU,EAAE,gBAAqB,EACjC,aAAa,EACb,uBAAuB,EACvB,qBAAqB,EACrB,YAAY,GACb,EAAE,6BAA6B,2CAyE/B;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,IAAI,4BAA4B,CAMzE"}
|
|
@@ -20,15 +20,46 @@ import { retryMiddleware } from '../../core/middleware/retry';
|
|
|
20
20
|
import { timeoutMiddleware } from '../../core/middleware/timeout';
|
|
21
21
|
import { errorMiddleware } from '../../core/middleware/errors';
|
|
22
22
|
import { useAuthStoreApi, useCurrencyStoreApi, useLanguageStoreApi } from '../stores/store-context';
|
|
23
|
+
import { sessionRetryMiddleware } from '../../core/middleware/session-retry';
|
|
23
24
|
const StorefrontClientContext = createContext(null);
|
|
24
|
-
export function StorefrontClientProvider({ children, config, middleware: customMiddleware = [], botProtection, botProtectionOperations, }) {
|
|
25
|
+
export function StorefrontClientProvider({ children, config, middleware: customMiddleware = [], botProtection, botProtectionOperations, sessionExpiredEmitter, authBasePath, }) {
|
|
25
26
|
const authStore = useAuthStoreApi();
|
|
26
27
|
const currencyStore = useCurrencyStoreApi();
|
|
27
28
|
const languageStore = useLanguageStoreApi();
|
|
28
29
|
const value = useMemo(() => {
|
|
30
|
+
// Reactive-401 renewal via the same-origin BFF route (`refreshSession`): the
|
|
31
|
+
// route reads the httpOnly refresh cookie server-side and sets the new
|
|
32
|
+
// first-party cookies, so no GraphQL refresh and no `setToken` round-trip.
|
|
33
|
+
// Forward-declared so this closure can reach the AuthClient created below.
|
|
34
|
+
// Deduped so concurrent 401s share a single renewal.
|
|
35
|
+
let authClientForRefresh;
|
|
36
|
+
let inFlightRefresh = null;
|
|
37
|
+
const refresh = () => {
|
|
38
|
+
if (inFlightRefresh)
|
|
39
|
+
return inFlightRefresh;
|
|
40
|
+
inFlightRefresh = (async () => {
|
|
41
|
+
try {
|
|
42
|
+
const result = await authClientForRefresh.refreshSession();
|
|
43
|
+
authStore.getState().setAuth(authStore.getState().customer, result.accessToken, result.expiresAt);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
authStore.getState().clearAuth();
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
inFlightRefresh = null;
|
|
52
|
+
}
|
|
53
|
+
})();
|
|
54
|
+
return inFlightRefresh;
|
|
55
|
+
};
|
|
29
56
|
const client = createStorefrontClient({
|
|
30
57
|
...config,
|
|
31
58
|
middleware: [
|
|
59
|
+
// Reactive 401 — OUTERMOST so the replay re-runs auth with the fresh token.
|
|
60
|
+
...(sessionExpiredEmitter
|
|
61
|
+
? [sessionRetryMiddleware({ refresh, onSessionExpired: (e) => sessionExpiredEmitter.emit(e) })]
|
|
62
|
+
: []),
|
|
32
63
|
// Header middleware (runs first)
|
|
33
64
|
authMiddleware(() => authStore.getState().accessToken),
|
|
34
65
|
currencyMiddleware(() => currencyStore.getState().currency),
|
|
@@ -49,10 +80,14 @@ export function StorefrontClientProvider({ children, config, middleware: customM
|
|
|
49
80
|
],
|
|
50
81
|
});
|
|
51
82
|
const cartClient = new CartClient(client);
|
|
52
|
-
|
|
83
|
+
// The AuthClient's `refreshSession()` posts to the same-origin BFF route at
|
|
84
|
+
// `${authBasePath}/refresh`; pass the configured base so a non-standard mount
|
|
85
|
+
// still resolves (default `/api/auth`).
|
|
86
|
+
const authClient = new AuthClient(client, { authBasePath });
|
|
87
|
+
authClientForRefresh = authClient;
|
|
53
88
|
return { client, cartClient, authClient };
|
|
54
89
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
55
|
-
}, [config.apiUrl, config.shopSlug, authStore, currencyStore, languageStore]);
|
|
90
|
+
}, [config.apiUrl, config.shopSlug, authStore, currencyStore, languageStore, sessionExpiredEmitter, authBasePath]);
|
|
56
91
|
return (_jsx(StorefrontClientContext.Provider, { value: value, children: children }));
|
|
57
92
|
}
|
|
58
93
|
/**
|
|
@@ -10,14 +10,18 @@
|
|
|
10
10
|
* @example
|
|
11
11
|
* ```tsx
|
|
12
12
|
* // app/layout.tsx
|
|
13
|
-
* import {
|
|
13
|
+
* import { cookies } from 'next/headers';
|
|
14
|
+
* import { StorefrontProvider, AUTH_COOKIE_NAME } from '@doswiftly/storefront-sdk';
|
|
14
15
|
*
|
|
15
16
|
* export default async function RootLayout({ children }) {
|
|
16
|
-
* const shopData = await fetchShop();
|
|
17
|
+
* const [shopData, cookieStore] = await Promise.all([fetchShop(), cookies()]);
|
|
18
|
+
* const initialAccessToken = cookieStore.get(AUTH_COOKIE_NAME)?.value ?? null;
|
|
19
|
+
*
|
|
17
20
|
* return (
|
|
18
21
|
* <StorefrontProvider
|
|
19
22
|
* config={{ apiUrl: '...', shopSlug: '...' }}
|
|
20
23
|
* shopData={shopData}
|
|
24
|
+
* initialAccessToken={initialAccessToken}
|
|
21
25
|
* >
|
|
22
26
|
* {children}
|
|
23
27
|
* </StorefrontProvider>
|
|
@@ -35,14 +39,58 @@ export interface StorefrontProviderProps extends StorefrontClientProviderProps {
|
|
|
35
39
|
* eliminating the flash of "Sign In" while Zustand persist rehydrates from localStorage.
|
|
36
40
|
*
|
|
37
41
|
* Read from cookies() in a Server Component (layout.tsx) and pass here.
|
|
42
|
+
*
|
|
43
|
+
* Defaults to `!!initialAccessToken` when omitted (raw token implies authenticated).
|
|
44
|
+
* Pass `false` explicitly to override that default in edge cases (opt-out flow,
|
|
45
|
+
* recovery banner that holds a token without claiming auth state).
|
|
38
46
|
*/
|
|
39
47
|
initialIsAuthenticated?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Server-side token seed. See {@link CreateAuthStoreOptions.initialAccessToken}
|
|
50
|
+
* for full semantics and security guarantees (token kept in memory, never
|
|
51
|
+
* persisted). Wire up from your Server Component with `cookies()` + `AUTH_COOKIE_NAME`.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```tsx
|
|
55
|
+
* // app/layout.tsx
|
|
56
|
+
* import { cookies } from 'next/headers';
|
|
57
|
+
* import { AUTH_COOKIE_NAME } from '@doswiftly/storefront-sdk';
|
|
58
|
+
*
|
|
59
|
+
* const cookieStore = await cookies();
|
|
60
|
+
* const initialAccessToken = cookieStore.get(AUTH_COOKIE_NAME)?.value ?? null;
|
|
61
|
+
*
|
|
62
|
+
* <StorefrontProvider initialAccessToken={initialAccessToken} ...>
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
initialAccessToken?: string | null;
|
|
40
66
|
/**
|
|
41
67
|
* Server-side language hint — pass the URL locale from next-intl params.
|
|
42
68
|
* Eliminates flash of wrong language on first render by initializing the
|
|
43
69
|
* language store with the correct value from the server.
|
|
44
70
|
*/
|
|
45
71
|
initialLanguage?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Proactive session refresh. Defaults to ON in the browser and OFF on the
|
|
74
|
+
* server — you do NOT need to pass it. When active, the SDK renews the access
|
|
75
|
+
* token shortly before it expires so an active buyer is never logged out
|
|
76
|
+
* mid-session, and fires a global `session-expired` signal (subscribe via
|
|
77
|
+
* `useSessionExpired`) when the session can no longer be kept alive. Pass
|
|
78
|
+
* `autoRefresh={false}` to opt out and drive refreshing yourself.
|
|
79
|
+
*/
|
|
80
|
+
autoRefresh?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Base path of the SDK-BFF auth route handlers (default `/api/auth`). The
|
|
83
|
+
* proactive scheduler and the reactive-401 renewal post to `${authBasePath}/refresh`
|
|
84
|
+
* (same-origin), which rotates the refresh cookie server-side. Override only for
|
|
85
|
+
* non-standard mounts — it must match where `createStorefrontAuthRoute` is mounted.
|
|
86
|
+
*/
|
|
87
|
+
authBasePath?: string;
|
|
88
|
+
/**
|
|
89
|
+
* Server-side session-expiry seed (ISO 8601) — typically the readable
|
|
90
|
+
* `session-expiry` cookie value read in a Server Component. Lets the refresh
|
|
91
|
+
* scheduler arm on the first render (cold start) without a whoami round-trip.
|
|
92
|
+
*/
|
|
93
|
+
initialExpiresAt?: string | null;
|
|
46
94
|
}
|
|
47
|
-
export declare function StorefrontProvider({ children, config, middleware, shopData, initialIsAuthenticated, initialLanguage, }: StorefrontProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
95
|
+
export declare function StorefrontProvider({ children, config, middleware, shopData, initialIsAuthenticated, initialAccessToken, initialExpiresAt, initialLanguage, autoRefresh, authBasePath, }: StorefrontProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
48
96
|
//# sourceMappingURL=storefront-provider.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storefront-provider.d.ts","sourceRoot":"","sources":["../../../src/react/providers/storefront-provider.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"storefront-provider.d.ts","sourceRoot":"","sources":["../../../src/react/providers/storefront-provider.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AASH,OAAO,EAA4B,KAAK,6BAA6B,EAAE,MAAM,8BAA8B,CAAC;AAC5G,OAAO,EAAoB,KAAK,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AASnF,MAAM,WAAW,uBAAwB,SAAQ,6BAA6B;IAC5E,QAAQ,EAAE,qBAAqB,CAAC,UAAU,CAAC,CAAC;IAC5C;;;;;;;;;;OAUG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC;;;;;;;;;;;;;;;;OAgBG;IACH,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,wBAAgB,kBAAkB,CAAC,EACjC,QAAQ,EACR,MAAM,EACN,UAAU,EACV,QAAQ,EACR,sBAAsB,EACtB,kBAAkB,EAClB,gBAAgB,EAChB,eAAe,EACf,WAAW,EACX,YAAY,GACb,EAAE,uBAAuB,2CAwCzB"}
|
|
@@ -10,14 +10,18 @@
|
|
|
10
10
|
* @example
|
|
11
11
|
* ```tsx
|
|
12
12
|
* // app/layout.tsx
|
|
13
|
-
* import {
|
|
13
|
+
* import { cookies } from 'next/headers';
|
|
14
|
+
* import { StorefrontProvider, AUTH_COOKIE_NAME } from '@doswiftly/storefront-sdk';
|
|
14
15
|
*
|
|
15
16
|
* export default async function RootLayout({ children }) {
|
|
16
|
-
* const shopData = await fetchShop();
|
|
17
|
+
* const [shopData, cookieStore] = await Promise.all([fetchShop(), cookies()]);
|
|
18
|
+
* const initialAccessToken = cookieStore.get(AUTH_COOKIE_NAME)?.value ?? null;
|
|
19
|
+
*
|
|
17
20
|
* return (
|
|
18
21
|
* <StorefrontProvider
|
|
19
22
|
* config={{ apiUrl: '...', shopSlug: '...' }}
|
|
20
23
|
* shopData={shopData}
|
|
24
|
+
* initialAccessToken={initialAccessToken}
|
|
21
25
|
* >
|
|
22
26
|
* {children}
|
|
23
27
|
* </StorefrontProvider>
|
|
@@ -38,10 +42,23 @@ import { LanguageProvider } from './language-provider';
|
|
|
38
42
|
import { createBotProtectionManager } from '../../core/bot-protection/create-manager';
|
|
39
43
|
import { BotProtectionContext } from '../bot-protection/bot-protection-context';
|
|
40
44
|
import { BotProtectionWidget } from '../bot-protection/bot-protection-widget';
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
import { createSessionExpiredEmitter } from '../../core/auth/session-events';
|
|
46
|
+
import { SessionExpiredContext } from '../hooks/use-session-expired';
|
|
47
|
+
import { useSessionRefresh } from '../hooks/use-session-refresh';
|
|
48
|
+
export function StorefrontProvider({ children, config, middleware, shopData, initialIsAuthenticated, initialAccessToken, initialExpiresAt, initialLanguage, autoRefresh, authBasePath, }) {
|
|
49
|
+
const authStoreRef = useRef(createAuthStore({ initialIsAuthenticated, initialAccessToken, initialExpiresAt }));
|
|
43
50
|
const currencyStoreRef = useRef(createCurrencyStore());
|
|
44
51
|
const languageStoreRef = useRef(createLanguageStore(initialLanguage));
|
|
52
|
+
const sessionExpiredRef = useRef(createSessionExpiredEmitter());
|
|
45
53
|
const botProtectionRef = useRef(shopData.botProtection ? createBotProtectionManager(shopData.botProtection) : null);
|
|
46
|
-
return (_jsx(AuthStoreContext.Provider, { value: authStoreRef.current, children: _jsx(CurrencyStoreContext.Provider, { value: currencyStoreRef.current, children: _jsx(LanguageStoreContext.Provider, { value: languageStoreRef.current, children: _jsx(StorefrontClientProvider, { config: config, middleware: middleware, botProtection: botProtectionRef.current, botProtectionOperations: shopData.botProtection?.protectedOperations, children: _jsx(BotProtectionContext.Provider, { value: { manager: botProtectionRef.current }, children: _jsx(CurrencyProvider, { shopData: shopData, children: _jsxs(LanguageProvider, { shopData: shopData, children: [_jsx(BotProtectionWidget, { manager: botProtectionRef.current }), children] }) }) }) }) }) }) }));
|
|
54
|
+
return (_jsx(AuthStoreContext.Provider, { value: authStoreRef.current, children: _jsx(CurrencyStoreContext.Provider, { value: currencyStoreRef.current, children: _jsx(LanguageStoreContext.Provider, { value: languageStoreRef.current, children: _jsx(StorefrontClientProvider, { config: config, middleware: middleware, botProtection: botProtectionRef.current, botProtectionOperations: shopData.botProtection?.protectedOperations, sessionExpiredEmitter: sessionExpiredRef.current, authBasePath: authBasePath, children: _jsx(BotProtectionContext.Provider, { value: { manager: botProtectionRef.current }, children: _jsx(CurrencyProvider, { shopData: shopData, children: _jsxs(LanguageProvider, { shopData: shopData, children: [_jsx(BotProtectionWidget, { manager: botProtectionRef.current }), _jsx(SessionRefreshRunner, { autoRefresh: autoRefresh, emitter: sessionExpiredRef.current }), _jsx(SessionExpiredContext.Provider, { value: sessionExpiredRef.current, children: children })] }) }) }) }) }) }) }));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Internal — drives the proactive refresh scheduler from inside the provider
|
|
58
|
+
* tree (so it can read the client + auth store via context). Renders nothing.
|
|
59
|
+
* The scheduler itself is a browser-only no-op on the server.
|
|
60
|
+
*/
|
|
61
|
+
function SessionRefreshRunner({ autoRefresh, emitter, }) {
|
|
62
|
+
useSessionRefresh({ enabled: autoRefresh, sessionExpiredEmitter: emitter });
|
|
63
|
+
return null;
|
|
47
64
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createStorefrontAuthRoute — SDK-BFF auth route handlers.
|
|
3
|
+
*
|
|
4
|
+
* Generates the four same-origin auth route handlers that live on the storefront
|
|
5
|
+
* domain (`/api/auth/[action]`): `login`, `refresh`, `logout` (POST) and `whoami`
|
|
6
|
+
* (GET). Each handler calls the backend `/storefront/auth/*` namespace
|
|
7
|
+
* server-to-server with `X-Shop-Slug` as a routing hint, owns the first-party
|
|
8
|
+
* httpOnly cookies on the storefront domain, and returns ONLY
|
|
9
|
+
* `{ accessToken, expiresAt[, customer] }` to JavaScript — the refresh token is
|
|
10
|
+
* read server-side and never reaches the browser's JS.
|
|
11
|
+
*
|
|
12
|
+
* This is the universal auth transport: the route handlers live on the
|
|
13
|
+
* storefront's own domain, so first-party cookies work identically on a platform
|
|
14
|
+
* subdomain, a custom domain, and off-platform hosting (e.g. Vercel) —
|
|
15
|
+
* independent of any reverse proxy in front. The backend never emits a
|
|
16
|
+
* `Set-Cookie` to a browser (tokens travel in the server-to-server response
|
|
17
|
+
* body); the BFF sets the cookies here.
|
|
18
|
+
*
|
|
19
|
+
* 0 runtime dependencies — pure Web API (`Request`/`Response`/`fetch`). No React,
|
|
20
|
+
* no `next/*`. Mount it in a Next.js (or any framework) route:
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* // app/api/auth/[action]/route.ts
|
|
25
|
+
* import { createStorefrontAuthRoute, trustedForwardedHostValidator } from '@doswiftly/storefront-sdk/react/server';
|
|
26
|
+
*
|
|
27
|
+
* export const { GET, POST } = createStorefrontAuthRoute({
|
|
28
|
+
* apiUrl: process.env.NEXT_PUBLIC_API_URL!,
|
|
29
|
+
* shopSlug: process.env.NEXT_PUBLIC_SHOP_SLUG!,
|
|
30
|
+
* isTrustedOrigin: trustedForwardedHostValidator, // when behind a reverse proxy that rewrites Host
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
import { type OriginValidator } from '../../core/auth/handlers';
|
|
35
|
+
export interface StorefrontAuthRouteOptions {
|
|
36
|
+
/** Backend base URL (e.g. `https://api.doswiftly.pl`). The server-to-server namespace is `${apiUrl}/storefront/auth/*`. */
|
|
37
|
+
apiUrl: string;
|
|
38
|
+
/** Shop slug forwarded as the `X-Shop-Slug` routing hint (selects the tenant; never binds the rotation family). */
|
|
39
|
+
shopSlug: string;
|
|
40
|
+
/**
|
|
41
|
+
* CSRF defense-in-depth predicate. When the storefront runs behind a reverse
|
|
42
|
+
* proxy that rewrites/strips `Host` (Cloudflare Workers, Vercel, NGINX),
|
|
43
|
+
* pass `trustedForwardedHostValidator`. This is NOT the primary control — the
|
|
44
|
+
* backend's protocol controls (rotation + reuse-detection + rate-limit +
|
|
45
|
+
* possession-proof) are. See {@link OriginValidator}.
|
|
46
|
+
*/
|
|
47
|
+
isTrustedOrigin?: OriginValidator | null;
|
|
48
|
+
/**
|
|
49
|
+
* Base path the route is mounted at (default `/api/auth`). The refresh cookie's
|
|
50
|
+
* `Path` is set to it so the cookie reaches both the refresh and logout routes
|
|
51
|
+
* (siblings under this base) but not GraphQL data traffic, which lives on a
|
|
52
|
+
* different path.
|
|
53
|
+
*/
|
|
54
|
+
authBasePath?: string;
|
|
55
|
+
/** Custom fetch (tests, non-standard runtimes). Defaults to `globalThis.fetch`. */
|
|
56
|
+
fetch?: typeof globalThis.fetch;
|
|
57
|
+
}
|
|
58
|
+
export interface StorefrontAuthRouteHandlers {
|
|
59
|
+
GET: (request: Request) => Promise<Response>;
|
|
60
|
+
POST: (request: Request) => Promise<Response>;
|
|
61
|
+
}
|
|
62
|
+
export declare function createStorefrontAuthRoute(options: StorefrontAuthRouteOptions): StorefrontAuthRouteHandlers;
|
|
63
|
+
//# sourceMappingURL=create-storefront-auth-route.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-storefront-auth-route.d.ts","sourceRoot":"","sources":["../../../src/react/server/create-storefront-auth-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAGL,KAAK,eAAe,EACrB,MAAM,0BAA0B,CAAC;AAOlC,MAAM,WAAW,0BAA0B;IACzC,2HAA2H;IAC3H,MAAM,EAAE,MAAM,CAAC;IACf,mHAAmH;IACnH,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IACzC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mFAAmF;IACnF,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CACjC;AAED,MAAM,WAAW,2BAA2B;IAC1C,GAAG,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC7C,IAAI,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC/C;AAgDD,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,0BAA0B,GAClC,2BAA2B,CA+L7B"}
|