@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.
Files changed (94) hide show
  1. package/CHANGELOG.md +1255 -0
  2. package/README.md +16 -4
  3. package/dist/core/auth/auth-client.d.ts +39 -3
  4. package/dist/core/auth/auth-client.d.ts.map +1 -1
  5. package/dist/core/auth/auth-client.js +51 -3
  6. package/dist/core/auth/cookie-config.d.ts +52 -3
  7. package/dist/core/auth/cookie-config.d.ts.map +1 -1
  8. package/dist/core/auth/cookie-config.js +60 -6
  9. package/dist/core/auth/handlers.d.ts +46 -0
  10. package/dist/core/auth/handlers.d.ts.map +1 -1
  11. package/dist/core/auth/handlers.js +9 -2
  12. package/dist/core/auth/session-events.d.ts +38 -0
  13. package/dist/core/auth/session-events.d.ts.map +1 -0
  14. package/dist/core/auth/session-events.js +35 -0
  15. package/dist/core/cart/cart-client.d.ts +10 -1
  16. package/dist/core/cart/cart-client.d.ts.map +1 -1
  17. package/dist/core/cart/cart-client.js +17 -1
  18. package/dist/core/cart/cart-recovery.d.ts +23 -0
  19. package/dist/core/cart/cart-recovery.d.ts.map +1 -1
  20. package/dist/core/cart/cart-recovery.js +20 -3
  21. package/dist/core/cart/types.d.ts +2 -1
  22. package/dist/core/cart/types.d.ts.map +1 -1
  23. package/dist/core/cart/types.js +7 -1
  24. package/dist/core/client/create-client.d.ts.map +1 -1
  25. package/dist/core/client/create-client.js +7 -3
  26. package/dist/core/client/execute.d.ts +29 -3
  27. package/dist/core/client/execute.d.ts.map +1 -1
  28. package/dist/core/client/execute.js +174 -3
  29. package/dist/core/client/types.d.ts +50 -2
  30. package/dist/core/client/types.d.ts.map +1 -1
  31. package/dist/core/errors.d.ts +6 -0
  32. package/dist/core/errors.d.ts.map +1 -1
  33. package/dist/core/errors.js +6 -0
  34. package/dist/core/generated/operation-types.d.ts +937 -182
  35. package/dist/core/generated/operation-types.d.ts.map +1 -1
  36. package/dist/core/generated/operation-types.js +560 -1
  37. package/dist/core/index.d.ts +6 -3
  38. package/dist/core/index.d.ts.map +1 -1
  39. package/dist/core/index.js +12 -2
  40. package/dist/core/middleware/session-retry.d.ts +47 -0
  41. package/dist/core/middleware/session-retry.d.ts.map +1 -0
  42. package/dist/core/middleware/session-retry.js +71 -0
  43. package/dist/core/operations/auth.d.ts.map +1 -1
  44. package/dist/core/operations/auth.js +1 -0
  45. package/dist/core/operations/cart.d.ts +7 -0
  46. package/dist/core/operations/cart.d.ts.map +1 -1
  47. package/dist/core/operations/cart.js +54 -3
  48. package/dist/react/components/PaymentInstrumentSection.d.ts +56 -0
  49. package/dist/react/components/PaymentInstrumentSection.d.ts.map +1 -0
  50. package/dist/react/components/PaymentInstrumentSection.js +89 -0
  51. package/dist/react/components/PaymentInstrumentTile.d.ts +56 -0
  52. package/dist/react/components/PaymentInstrumentTile.d.ts.map +1 -0
  53. package/dist/react/components/PaymentInstrumentTile.js +41 -0
  54. package/dist/react/components/index.d.ts +2 -0
  55. package/dist/react/components/index.d.ts.map +1 -1
  56. package/dist/react/components/index.js +2 -0
  57. package/dist/react/helpers/browser-data.d.ts +89 -0
  58. package/dist/react/helpers/browser-data.d.ts.map +1 -0
  59. package/dist/react/helpers/browser-data.js +84 -0
  60. package/dist/react/hooks/use-cart-manager.d.ts +104 -13
  61. package/dist/react/hooks/use-cart-manager.d.ts.map +1 -1
  62. package/dist/react/hooks/use-cart-manager.js +144 -12
  63. package/dist/react/hooks/use-login.d.ts.map +1 -1
  64. package/dist/react/hooks/use-login.js +3 -3
  65. package/dist/react/hooks/use-refresh-token.d.ts.map +1 -1
  66. package/dist/react/hooks/use-refresh-token.js +6 -4
  67. package/dist/react/hooks/use-session-expired.d.ts +16 -0
  68. package/dist/react/hooks/use-session-expired.d.ts.map +1 -0
  69. package/dist/react/hooks/use-session-expired.js +26 -0
  70. package/dist/react/hooks/use-session-refresh.d.ts +32 -0
  71. package/dist/react/hooks/use-session-refresh.d.ts.map +1 -0
  72. package/dist/react/hooks/use-session-refresh.js +147 -0
  73. package/dist/react/index.d.ts +5 -1
  74. package/dist/react/index.d.ts.map +1 -1
  75. package/dist/react/index.js +5 -1
  76. package/dist/react/providers/storefront-client-provider.d.ts +10 -1
  77. package/dist/react/providers/storefront-client-provider.d.ts.map +1 -1
  78. package/dist/react/providers/storefront-client-provider.js +38 -3
  79. package/dist/react/providers/storefront-provider.d.ts +51 -3
  80. package/dist/react/providers/storefront-provider.d.ts.map +1 -1
  81. package/dist/react/providers/storefront-provider.js +22 -5
  82. package/dist/react/server/create-storefront-auth-route.d.ts +63 -0
  83. package/dist/react/server/create-storefront-auth-route.d.ts.map +1 -0
  84. package/dist/react/server/create-storefront-auth-route.js +239 -0
  85. package/dist/react/server/get-initial-auth.d.ts +57 -0
  86. package/dist/react/server/get-initial-auth.d.ts.map +1 -0
  87. package/dist/react/server/get-initial-auth.js +55 -0
  88. package/dist/react/server/index.d.ts +3 -0
  89. package/dist/react/server/index.d.ts.map +1 -1
  90. package/dist/react/server/index.js +6 -0
  91. package/dist/react/stores/auth.store.d.ts +46 -2
  92. package/dist/react/stores/auth.store.d.ts.map +1 -1
  93. package/dist/react/stores/auth.store.js +19 -7
  94. 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
+ }
@@ -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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"}
@@ -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;CACpC;AAED,wBAAgB,wBAAwB,CAAC,EACvC,QAAQ,EACR,MAAM,EACN,UAAU,EAAE,gBAAqB,EACjC,aAAa,EACb,uBAAuB,GACxB,EAAE,6BAA6B,2CAyC/B;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,IAAI,4BAA4B,CAMzE"}
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
- const authClient = new AuthClient(client);
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 { StorefrontProvider } from '@doswiftly/storefront-sdk/react';
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;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AASH,OAAO,EAA4B,KAAK,6BAA6B,EAAE,MAAM,8BAA8B,CAAC;AAC5G,OAAO,EAAoB,KAAK,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAMnF,MAAM,WAAW,uBAAwB,SAAQ,6BAA6B;IAC5E,QAAQ,EAAE,qBAAqB,CAAC,UAAU,CAAC,CAAC;IAC5C;;;;;;OAMG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,kBAAkB,CAAC,EACjC,QAAQ,EACR,MAAM,EACN,UAAU,EACV,QAAQ,EACR,sBAAsB,EACtB,eAAe,GAChB,EAAE,uBAAuB,2CA+BzB"}
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 { StorefrontProvider } from '@doswiftly/storefront-sdk/react';
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
- export function StorefrontProvider({ children, config, middleware, shopData, initialIsAuthenticated, initialLanguage, }) {
42
- const authStoreRef = useRef(createAuthStore(initialIsAuthenticated));
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"}