@doswiftly/storefront-sdk 17.0.0 → 18.1.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 (86) hide show
  1. package/CHANGELOG.md +976 -0
  2. package/README.md +47 -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-recovery.d.ts +23 -0
  16. package/dist/core/cart/cart-recovery.d.ts.map +1 -1
  17. package/dist/core/cart/cart-recovery.js +20 -3
  18. package/dist/core/cart/types.d.ts +2 -1
  19. package/dist/core/cart/types.d.ts.map +1 -1
  20. package/dist/core/cart/types.js +7 -1
  21. package/dist/core/client/create-client.d.ts.map +1 -1
  22. package/dist/core/client/create-client.js +7 -3
  23. package/dist/core/client/execute.d.ts +29 -3
  24. package/dist/core/client/execute.d.ts.map +1 -1
  25. package/dist/core/client/execute.js +174 -3
  26. package/dist/core/client/types.d.ts +50 -2
  27. package/dist/core/client/types.d.ts.map +1 -1
  28. package/dist/core/errors.d.ts +6 -0
  29. package/dist/core/errors.d.ts.map +1 -1
  30. package/dist/core/errors.js +6 -0
  31. package/dist/core/generated/operation-types.d.ts +838 -221
  32. package/dist/core/generated/operation-types.d.ts.map +1 -1
  33. package/dist/core/generated/operation-types.js +560 -1
  34. package/dist/core/index.d.ts +6 -3
  35. package/dist/core/index.d.ts.map +1 -1
  36. package/dist/core/index.js +12 -2
  37. package/dist/core/middleware/session-retry.d.ts +47 -0
  38. package/dist/core/middleware/session-retry.d.ts.map +1 -0
  39. package/dist/core/middleware/session-retry.js +71 -0
  40. package/dist/core/operations/auth.d.ts.map +1 -1
  41. package/dist/core/operations/auth.js +1 -0
  42. package/dist/core/operations/cart.d.ts.map +1 -1
  43. package/dist/core/operations/cart.js +15 -11
  44. package/dist/react/components/PaymentInstrumentSection.d.ts.map +1 -1
  45. package/dist/react/components/PaymentInstrumentSection.js +4 -4
  46. package/dist/react/components/PaymentInstrumentTile.d.ts +7 -7
  47. package/dist/react/components/PaymentInstrumentTile.d.ts.map +1 -1
  48. package/dist/react/components/PaymentInstrumentTile.js +4 -3
  49. package/dist/react/hooks/use-cart-manager.d.ts +133 -13
  50. package/dist/react/hooks/use-cart-manager.d.ts.map +1 -1
  51. package/dist/react/hooks/use-cart-manager.js +220 -16
  52. package/dist/react/hooks/use-login.d.ts.map +1 -1
  53. package/dist/react/hooks/use-login.js +3 -3
  54. package/dist/react/hooks/use-refresh-token.d.ts.map +1 -1
  55. package/dist/react/hooks/use-refresh-token.js +6 -4
  56. package/dist/react/hooks/use-session-expired.d.ts +16 -0
  57. package/dist/react/hooks/use-session-expired.d.ts.map +1 -0
  58. package/dist/react/hooks/use-session-expired.js +26 -0
  59. package/dist/react/hooks/use-session-refresh.d.ts +32 -0
  60. package/dist/react/hooks/use-session-refresh.d.ts.map +1 -0
  61. package/dist/react/hooks/use-session-refresh.js +147 -0
  62. package/dist/react/index.d.ts +5 -1
  63. package/dist/react/index.d.ts.map +1 -1
  64. package/dist/react/index.js +3 -0
  65. package/dist/react/providers/cart-manager-provider.d.ts +50 -0
  66. package/dist/react/providers/cart-manager-provider.d.ts.map +1 -0
  67. package/dist/react/providers/cart-manager-provider.js +59 -0
  68. package/dist/react/providers/storefront-client-provider.d.ts +10 -1
  69. package/dist/react/providers/storefront-client-provider.d.ts.map +1 -1
  70. package/dist/react/providers/storefront-client-provider.js +38 -3
  71. package/dist/react/providers/storefront-provider.d.ts +51 -3
  72. package/dist/react/providers/storefront-provider.d.ts.map +1 -1
  73. package/dist/react/providers/storefront-provider.js +22 -5
  74. package/dist/react/server/create-storefront-auth-route.d.ts +63 -0
  75. package/dist/react/server/create-storefront-auth-route.d.ts.map +1 -0
  76. package/dist/react/server/create-storefront-auth-route.js +239 -0
  77. package/dist/react/server/get-initial-auth.d.ts +57 -0
  78. package/dist/react/server/get-initial-auth.d.ts.map +1 -0
  79. package/dist/react/server/get-initial-auth.js +55 -0
  80. package/dist/react/server/index.d.ts +3 -0
  81. package/dist/react/server/index.d.ts.map +1 -1
  82. package/dist/react/server/index.js +6 -0
  83. package/dist/react/stores/auth.store.d.ts +46 -2
  84. package/dist/react/stores/auth.store.d.ts.map +1 -1
  85. package/dist/react/stores/auth.store.js +19 -7
  86. package/package.json +4 -2
@@ -16,11 +16,14 @@ export { StorefrontProvider } from './providers/storefront-provider';
16
16
  export { StorefrontClientProvider } from './providers/storefront-client-provider';
17
17
  export { CurrencyProvider } from './providers/currency-provider';
18
18
  export { LanguageProvider } from './providers/language-provider';
19
+ export { CartManagerProvider, useCartManagerContext } from './providers/cart-manager-provider';
19
20
  // Hooks
20
21
  export { useAuth } from './hooks/use-auth';
21
22
  export { useLogin } from './hooks/use-login';
22
23
  export { useLogout } from './hooks/use-logout';
23
24
  export { useRefreshToken } from './hooks/use-refresh-token';
25
+ export { useSessionRefresh } from './hooks/use-session-refresh';
26
+ export { useSessionExpired } from './hooks/use-session-expired';
24
27
  export { useCartManager } from './hooks/use-cart-manager';
25
28
  export { useCart } from './hooks/use-cart';
26
29
  export { useStorefrontClient } from './hooks/use-storefront-client';
@@ -0,0 +1,50 @@
1
+ /**
2
+ * CartManagerProvider — shares a single `useCartManager` instance across the tree.
3
+ *
4
+ * `useCartManager` owns per-mount state (status, recovery coordinator, cart-expired
5
+ * listeners), so calling it in several components creates independent managers
6
+ * with separate loading state and separate recovery queues. Wrap the checkout
7
+ * subtree in `<CartManagerProvider>` and read the shared instance with
8
+ * `useCartManagerContext()` to get one source of truth: one loading indicator,
9
+ * one recovery coordinator, one cart-expired subscription.
10
+ *
11
+ * Must be rendered inside `<StorefrontProvider>` — it builds on the storefront
12
+ * client from context. For deliberately independent managers (e.g. a multi-cart
13
+ * admin view) call `useCartManager()` directly instead of using this provider.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * // app/checkout/page.tsx — Server Component resolves the cart-id
18
+ * <CartManagerProvider
19
+ * initialCartId={cartId}
20
+ * onMutationError={(operation, error) => toast.error(error.message)}
21
+ * onMutationSuccess={() => router.refresh()}
22
+ * >
23
+ * <CheckoutForm />
24
+ * </CartManagerProvider>
25
+ *
26
+ * // CheckoutForm.tsx
27
+ * const { addItem, complete, status } = useCartManagerContext();
28
+ * ```
29
+ */
30
+ import { type ReactNode } from 'react';
31
+ import { type CartManagerLifecycleCallbacks, type UseCartManagerResult } from '../hooks/use-cart-manager';
32
+ export interface CartManagerProviderProps extends CartManagerLifecycleCallbacks {
33
+ /**
34
+ * Server-known cart-id seed forwarded to `useCartManager`. Used only when the
35
+ * `cart-id` cookie is empty on mount — the cookie always wins when present.
36
+ */
37
+ initialCartId?: string | null;
38
+ children: ReactNode;
39
+ }
40
+ /**
41
+ * Creates one `useCartManager` instance and exposes it to descendants via
42
+ * Context. Lifecycle callbacks are forwarded to the underlying hook.
43
+ */
44
+ export declare function CartManagerProvider({ initialCartId, onMutationStart, onMutationSuccess, onMutationError, children, }: CartManagerProviderProps): import("react/jsx-runtime").JSX.Element;
45
+ /**
46
+ * Read the shared cart manager provided by the nearest `<CartManagerProvider>`.
47
+ * Throws when called outside a provider.
48
+ */
49
+ export declare function useCartManagerContext(): UseCartManagerResult;
50
+ //# sourceMappingURL=cart-manager-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cart-manager-provider.d.ts","sourceRoot":"","sources":["../../../src/react/providers/cart-manager-provider.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAIH,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAClE,OAAO,EAEL,KAAK,6BAA6B,EAClC,KAAK,oBAAoB,EAC1B,MAAM,2BAA2B,CAAC;AAKnC,MAAM,WAAW,wBAAyB,SAAQ,6BAA6B;IAC7E;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,QAAQ,GACT,EAAE,wBAAwB,2CAS1B;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,IAAI,oBAAoB,CAM5D"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * CartManagerProvider — shares a single `useCartManager` instance across the tree.
3
+ *
4
+ * `useCartManager` owns per-mount state (status, recovery coordinator, cart-expired
5
+ * listeners), so calling it in several components creates independent managers
6
+ * with separate loading state and separate recovery queues. Wrap the checkout
7
+ * subtree in `<CartManagerProvider>` and read the shared instance with
8
+ * `useCartManagerContext()` to get one source of truth: one loading indicator,
9
+ * one recovery coordinator, one cart-expired subscription.
10
+ *
11
+ * Must be rendered inside `<StorefrontProvider>` — it builds on the storefront
12
+ * client from context. For deliberately independent managers (e.g. a multi-cart
13
+ * admin view) call `useCartManager()` directly instead of using this provider.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * // app/checkout/page.tsx — Server Component resolves the cart-id
18
+ * <CartManagerProvider
19
+ * initialCartId={cartId}
20
+ * onMutationError={(operation, error) => toast.error(error.message)}
21
+ * onMutationSuccess={() => router.refresh()}
22
+ * >
23
+ * <CheckoutForm />
24
+ * </CartManagerProvider>
25
+ *
26
+ * // CheckoutForm.tsx
27
+ * const { addItem, complete, status } = useCartManagerContext();
28
+ * ```
29
+ */
30
+ 'use client';
31
+ import { jsx as _jsx } from "react/jsx-runtime";
32
+ import { createContext, useContext } from 'react';
33
+ import { useCartManager, } from '../hooks/use-cart-manager';
34
+ const CartManagerContext = createContext(null);
35
+ CartManagerContext.displayName = 'CartManagerContext';
36
+ /**
37
+ * Creates one `useCartManager` instance and exposes it to descendants via
38
+ * Context. Lifecycle callbacks are forwarded to the underlying hook.
39
+ */
40
+ export function CartManagerProvider({ initialCartId, onMutationStart, onMutationSuccess, onMutationError, children, }) {
41
+ const manager = useCartManager({
42
+ initialCartId,
43
+ onMutationStart,
44
+ onMutationSuccess,
45
+ onMutationError,
46
+ });
47
+ return _jsx(CartManagerContext.Provider, { value: manager, children: children });
48
+ }
49
+ /**
50
+ * Read the shared cart manager provided by the nearest `<CartManagerProvider>`.
51
+ * Throws when called outside a provider.
52
+ */
53
+ export function useCartManagerContext() {
54
+ const ctx = useContext(CartManagerContext);
55
+ if (!ctx) {
56
+ throw new Error('useCartManagerContext must be used within CartManagerProvider');
57
+ }
58
+ return ctx;
59
+ }
@@ -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"}