@doswiftly/storefront-sdk 4.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 (206) hide show
  1. package/README.md +430 -0
  2. package/dist/__tests__/unit/test-helpers.d.ts +46 -0
  3. package/dist/__tests__/unit/test-helpers.d.ts.map +1 -0
  4. package/dist/__tests__/unit/test-helpers.js +72 -0
  5. package/dist/core/auth/auth-client.d.ts +46 -0
  6. package/dist/core/auth/auth-client.d.ts.map +1 -0
  7. package/dist/core/auth/auth-client.js +82 -0
  8. package/dist/core/auth/cookie-config.d.ts +18 -0
  9. package/dist/core/auth/cookie-config.d.ts.map +1 -0
  10. package/dist/core/auth/cookie-config.js +18 -0
  11. package/dist/core/auth/handlers.d.ts +32 -0
  12. package/dist/core/auth/handlers.d.ts.map +1 -0
  13. package/dist/core/auth/handlers.js +127 -0
  14. package/dist/core/auth/routes.d.ts +21 -0
  15. package/dist/core/auth/routes.d.ts.map +1 -0
  16. package/dist/core/auth/routes.js +14 -0
  17. package/dist/core/auth/token-client.d.ts +26 -0
  18. package/dist/core/auth/token-client.d.ts.map +1 -0
  19. package/dist/core/auth/token-client.js +42 -0
  20. package/dist/core/auth/types.d.ts +53 -0
  21. package/dist/core/auth/types.d.ts.map +1 -0
  22. package/dist/core/auth/types.js +4 -0
  23. package/dist/core/cache.d.ts +54 -0
  24. package/dist/core/cache.d.ts.map +1 -0
  25. package/dist/core/cache.js +82 -0
  26. package/dist/core/cart/cart-client.d.ts +57 -0
  27. package/dist/core/cart/cart-client.d.ts.map +1 -0
  28. package/dist/core/cart/cart-client.js +89 -0
  29. package/dist/core/cart/types.d.ts +110 -0
  30. package/dist/core/cart/types.d.ts.map +1 -0
  31. package/dist/core/cart/types.js +6 -0
  32. package/dist/core/client/compose.d.ts +9 -0
  33. package/dist/core/client/compose.d.ts.map +1 -0
  34. package/dist/core/client/compose.js +9 -0
  35. package/dist/core/client/create-client.d.ts +15 -0
  36. package/dist/core/client/create-client.d.ts.map +1 -0
  37. package/dist/core/client/create-client.js +85 -0
  38. package/dist/core/client/dedupe.d.ts +7 -0
  39. package/dist/core/client/dedupe.d.ts.map +1 -0
  40. package/dist/core/client/dedupe.js +16 -0
  41. package/dist/core/client/execute.d.ts +20 -0
  42. package/dist/core/client/execute.d.ts.map +1 -0
  43. package/dist/core/client/execute.js +48 -0
  44. package/dist/core/client/hash.d.ts +7 -0
  45. package/dist/core/client/hash.d.ts.map +1 -0
  46. package/dist/core/client/hash.js +21 -0
  47. package/dist/core/client/operation-name.d.ts +7 -0
  48. package/dist/core/client/operation-name.d.ts.map +1 -0
  49. package/dist/core/client/operation-name.js +10 -0
  50. package/dist/core/client/types.d.ts +126 -0
  51. package/dist/core/client/types.d.ts.map +1 -0
  52. package/dist/core/client/types.js +26 -0
  53. package/dist/core/errors.d.ts +43 -0
  54. package/dist/core/errors.d.ts.map +1 -0
  55. package/dist/core/errors.js +43 -0
  56. package/dist/core/format.d.ts +92 -0
  57. package/dist/core/format.d.ts.map +1 -0
  58. package/dist/core/format.js +216 -0
  59. package/dist/core/helpers/assert-no-user-errors.d.ts +10 -0
  60. package/dist/core/helpers/assert-no-user-errors.d.ts.map +1 -0
  61. package/dist/core/helpers/assert-no-user-errors.js +16 -0
  62. package/dist/core/helpers/normalize-connection.d.ts +36 -0
  63. package/dist/core/helpers/normalize-connection.d.ts.map +1 -0
  64. package/dist/core/helpers/normalize-connection.js +21 -0
  65. package/dist/core/helpers/sanitize-html.d.ts +8 -0
  66. package/dist/core/helpers/sanitize-html.d.ts.map +1 -0
  67. package/dist/core/helpers/sanitize-html.js +35 -0
  68. package/dist/core/index.d.ts +59 -0
  69. package/dist/core/index.d.ts.map +1 -0
  70. package/dist/core/index.js +68 -0
  71. package/dist/core/middleware/auth.d.ts +16 -0
  72. package/dist/core/middleware/auth.d.ts.map +1 -0
  73. package/dist/core/middleware/auth.js +22 -0
  74. package/dist/core/middleware/currency.d.ts +15 -0
  75. package/dist/core/middleware/currency.d.ts.map +1 -0
  76. package/dist/core/middleware/currency.js +21 -0
  77. package/dist/core/middleware/errors.d.ts +24 -0
  78. package/dist/core/middleware/errors.d.ts.map +1 -0
  79. package/dist/core/middleware/errors.js +77 -0
  80. package/dist/core/middleware/retry.d.ts +22 -0
  81. package/dist/core/middleware/retry.d.ts.map +1 -0
  82. package/dist/core/middleware/retry.js +58 -0
  83. package/dist/core/middleware/timeout.d.ts +19 -0
  84. package/dist/core/middleware/timeout.d.ts.map +1 -0
  85. package/dist/core/middleware/timeout.js +51 -0
  86. package/dist/core/operations/auth.d.ts +11 -0
  87. package/dist/core/operations/auth.d.ts.map +1 -0
  88. package/dist/core/operations/auth.js +112 -0
  89. package/dist/core/operations/cart.d.ts +15 -0
  90. package/dist/core/operations/cart.d.ts.map +1 -0
  91. package/dist/core/operations/cart.js +169 -0
  92. package/dist/index.d.ts +24 -0
  93. package/dist/index.d.ts.map +1 -0
  94. package/dist/index.js +24 -0
  95. package/dist/react/cookies.d.ts +28 -0
  96. package/dist/react/cookies.d.ts.map +1 -0
  97. package/dist/react/cookies.js +49 -0
  98. package/dist/react/helpers/create-store-context.d.ts +37 -0
  99. package/dist/react/helpers/create-store-context.d.ts.map +1 -0
  100. package/dist/react/helpers/create-store-context.js +47 -0
  101. package/dist/react/hooks/use-auth.d.ts +65 -0
  102. package/dist/react/hooks/use-auth.d.ts.map +1 -0
  103. package/dist/react/hooks/use-auth.js +168 -0
  104. package/dist/react/hooks/use-cart-manager.d.ts +30 -0
  105. package/dist/react/hooks/use-cart-manager.d.ts.map +1 -0
  106. package/dist/react/hooks/use-cart-manager.js +223 -0
  107. package/dist/react/hooks/use-currency.d.ts +11 -0
  108. package/dist/react/hooks/use-currency.d.ts.map +1 -0
  109. package/dist/react/hooks/use-currency.js +19 -0
  110. package/dist/react/hooks/use-debounced-value.d.ts +15 -0
  111. package/dist/react/hooks/use-debounced-value.d.ts.map +1 -0
  112. package/dist/react/hooks/use-debounced-value.js +25 -0
  113. package/dist/react/hooks/use-hydrated.d.ts +9 -0
  114. package/dist/react/hooks/use-hydrated.d.ts.map +1 -0
  115. package/dist/react/hooks/use-hydrated.js +14 -0
  116. package/dist/react/hooks/use-storefront-client.d.ts +6 -0
  117. package/dist/react/hooks/use-storefront-client.d.ts.map +1 -0
  118. package/dist/react/hooks/use-storefront-client.js +8 -0
  119. package/dist/react/index.d.ts +30 -0
  120. package/dist/react/index.d.ts.map +1 -0
  121. package/dist/react/index.js +34 -0
  122. package/dist/react/providers/currency-provider.d.ts +14 -0
  123. package/dist/react/providers/currency-provider.d.ts.map +1 -0
  124. package/dist/react/providers/currency-provider.js +20 -0
  125. package/dist/react/providers/storefront-client-provider.d.ts +33 -0
  126. package/dist/react/providers/storefront-client-provider.d.ts.map +1 -0
  127. package/dist/react/providers/storefront-client-provider.js +57 -0
  128. package/dist/react/providers/storefront-provider.d.ts +42 -0
  129. package/dist/react/providers/storefront-provider.d.ts.map +1 -0
  130. package/dist/react/providers/storefront-provider.js +40 -0
  131. package/dist/react/server/get-storefront-client.d.ts +42 -0
  132. package/dist/react/server/get-storefront-client.d.ts.map +1 -0
  133. package/dist/react/server/get-storefront-client.js +44 -0
  134. package/dist/react/server/index.d.ts +2 -0
  135. package/dist/react/server/index.d.ts.map +1 -0
  136. package/dist/react/server/index.js +1 -0
  137. package/dist/react/stores/auth.store.d.ts +48 -0
  138. package/dist/react/stores/auth.store.d.ts.map +1 -0
  139. package/dist/react/stores/auth.store.js +67 -0
  140. package/dist/react/stores/currency.store.d.ts +29 -0
  141. package/dist/react/stores/currency.store.d.ts.map +1 -0
  142. package/dist/react/stores/currency.store.js +76 -0
  143. package/dist/react/stores/index.d.ts +8 -0
  144. package/dist/react/stores/index.d.ts.map +1 -0
  145. package/dist/react/stores/index.js +10 -0
  146. package/dist/react/stores/store-context.d.ts +27 -0
  147. package/dist/react/stores/store-context.d.ts.map +1 -0
  148. package/dist/react/stores/store-context.js +62 -0
  149. package/package.json +71 -0
  150. package/src/__tests__/contract/storefront-api.contract.test.ts +450 -0
  151. package/src/__tests__/unit/auth-client.test.ts +210 -0
  152. package/src/__tests__/unit/cart-client.test.ts +233 -0
  153. package/src/__tests__/unit/create-client.test.ts +356 -0
  154. package/src/__tests__/unit/helpers.test.ts +377 -0
  155. package/src/__tests__/unit/middleware.test.ts +374 -0
  156. package/src/__tests__/unit/test-helpers.ts +103 -0
  157. package/src/core/auth/auth-client.ts +123 -0
  158. package/src/core/auth/cookie-config.ts +23 -0
  159. package/src/core/auth/handlers.ts +168 -0
  160. package/src/core/auth/routes.ts +26 -0
  161. package/src/core/auth/token-client.ts +51 -0
  162. package/src/core/auth/types.ts +54 -0
  163. package/src/core/cache.ts +102 -0
  164. package/src/core/cart/cart-client.ts +150 -0
  165. package/src/core/cart/types.ts +104 -0
  166. package/src/core/client/compose.ts +15 -0
  167. package/src/core/client/create-client.ts +129 -0
  168. package/src/core/client/dedupe.ts +19 -0
  169. package/src/core/client/execute.ts +70 -0
  170. package/src/core/client/hash.ts +21 -0
  171. package/src/core/client/operation-name.ts +12 -0
  172. package/src/core/client/types.ts +171 -0
  173. package/src/core/errors.ts +67 -0
  174. package/src/core/format.ts +254 -0
  175. package/src/core/helpers/assert-no-user-errors.ts +21 -0
  176. package/src/core/helpers/normalize-connection.ts +48 -0
  177. package/src/core/helpers/sanitize-html.ts +42 -0
  178. package/src/core/index.ts +148 -0
  179. package/src/core/middleware/auth.ts +27 -0
  180. package/src/core/middleware/currency.ts +26 -0
  181. package/src/core/middleware/errors.ts +86 -0
  182. package/src/core/middleware/retry.ts +75 -0
  183. package/src/core/middleware/timeout.ts +61 -0
  184. package/src/core/operations/auth.ts +123 -0
  185. package/src/core/operations/cart.ts +185 -0
  186. package/src/index.ts +25 -0
  187. package/src/react/cookies.ts +54 -0
  188. package/src/react/helpers/create-store-context.ts +56 -0
  189. package/src/react/hooks/use-auth.ts +218 -0
  190. package/src/react/hooks/use-cart-manager.ts +236 -0
  191. package/src/react/hooks/use-currency.ts +23 -0
  192. package/src/react/hooks/use-debounced-value.ts +30 -0
  193. package/src/react/hooks/use-hydrated.ts +20 -0
  194. package/src/react/hooks/use-storefront-client.ts +12 -0
  195. package/src/react/index.ts +45 -0
  196. package/src/react/providers/currency-provider.tsx +30 -0
  197. package/src/react/providers/storefront-client-provider.tsx +90 -0
  198. package/src/react/providers/storefront-provider.tsx +71 -0
  199. package/src/react/server/get-storefront-client.ts +60 -0
  200. package/src/react/server/index.ts +1 -0
  201. package/src/react/stores/auth.store.ts +112 -0
  202. package/src/react/stores/currency.store.ts +113 -0
  203. package/src/react/stores/index.ts +17 -0
  204. package/src/react/stores/store-context.tsx +82 -0
  205. package/tsconfig.json +20 -0
  206. package/vitest.config.ts +14 -0
package/README.md ADDED
@@ -0,0 +1,430 @@
1
+ # @doswiftly/storefront-sdk
2
+
3
+ Layered runtime SDK for DoSwiftly Commerce storefronts. Hydrogen-aligned architecture with **0 runtime dependencies** in core.
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ @doswiftly/storefront-sdk
9
+ ├── core (.) — Framework-agnostic: transport, middleware, CartClient, AuthClient,
10
+ │ cache, format utilities, sanitizeHtml, normalizeConnection,
11
+ │ auth cookie config/handlers/token client, route matching
12
+ ├── react (./react) — React adapter: providers, Zustand stores (Context-based), hooks,
13
+ │ useHydrated, useDebouncedValue, createStoreContext
14
+ ├── react/server — Server-side client factory
15
+ └── cache (./cache) — Cache strategy functions
16
+ ```
17
+
18
+ **Core** works everywhere: Node.js, Edge Workers, Deno, Bun, CLI scripts — without React.
19
+ **React adapter** requires `react ^18 || ^19` and `zustand ^5` as peer dependencies.
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ pnpm add @doswiftly/storefront-sdk
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ ### Core (framework-agnostic)
30
+
31
+ ```typescript
32
+ import {
33
+ createStorefrontClient,
34
+ authMiddleware,
35
+ currencyMiddleware,
36
+ retryMiddleware,
37
+ timeoutMiddleware,
38
+ errorMiddleware,
39
+ CartClient,
40
+ AuthClient,
41
+ } from '@doswiftly/storefront-sdk';
42
+
43
+ const client = createStorefrontClient({
44
+ apiUrl: 'https://api.doswiftly.pl',
45
+ shopSlug: 'my-shop',
46
+ middleware: [
47
+ authMiddleware(() => getToken()),
48
+ currencyMiddleware(() => getCurrency()),
49
+ retryMiddleware({ maxRetries: 2 }),
50
+ timeoutMiddleware({ timeout: 5000 }),
51
+ errorMiddleware(), // ALWAYS LAST
52
+ ],
53
+ });
54
+
55
+ // Query (deduplicated, cached)
56
+ const data = await client.query(ProductQuery, { handle: 'foo' }, cacheLong());
57
+
58
+ // Mutation (never cached, never retried)
59
+ const result = await client.mutate(CartCreateMutation, { input: {} });
60
+ ```
61
+
62
+ ### React (Next.js)
63
+
64
+ ```tsx
65
+ // app/layout.tsx
66
+ import { StorefrontProvider } from '@doswiftly/storefront-sdk/react';
67
+
68
+ export default function Layout({ children }) {
69
+ return (
70
+ <StorefrontProvider
71
+ config={{ apiUrl: process.env.NEXT_PUBLIC_API_URL!, shopSlug: process.env.NEXT_PUBLIC_SHOP_SLUG! }}
72
+ shopData={shopData}
73
+ >
74
+ {children}
75
+ </StorefrontProvider>
76
+ );
77
+ }
78
+ ```
79
+
80
+ ```tsx
81
+ // Client Component
82
+ 'use client';
83
+ import { useAuth, useCartManager, useAuthStore, useCurrencyStore, useAuthHydrated } from '@doswiftly/storefront-sdk/react';
84
+
85
+ const { login, logout } = useAuth({ onSetToken, onClearToken });
86
+ const { addItem, removeItem, lines, isLoading } = useCartManager();
87
+ const { isAuthenticated, customer } = useAuthStore();
88
+ const authHydrated = useAuthHydrated(); // true after persist rehydration
89
+ const { currency, setCurrency } = useCurrencyStore();
90
+ ```
91
+
92
+ ## Export Paths
93
+
94
+ | Path | Description | Dependencies |
95
+ |------|-------------|-------------|
96
+ | `@doswiftly/storefront-sdk` | Core: transport, middleware, clients, errors, format, sanitize, auth handlers, route matching | **0** |
97
+ | `@doswiftly/storefront-sdk/react` | Providers, hooks, store hooks, useHydrated, useDebouncedValue, createStoreContext | react, zustand |
98
+ | `@doswiftly/storefront-sdk/react/server` | Server-side client factory | react |
99
+ | `@doswiftly/storefront-sdk/cache` | Cache strategy functions | **0** |
100
+
101
+ ## Core API
102
+
103
+ ### createStorefrontClient
104
+
105
+ ```typescript
106
+ const client = createStorefrontClient({
107
+ apiUrl: string,
108
+ shopSlug: string,
109
+ middleware?: Middleware[],
110
+ defaultHeaders?: Record<string, string>,
111
+ fetch?: typeof globalThis.fetch, // custom fetch (polyfill, test mocks)
112
+ debug?: boolean, // log requests in dev
113
+ });
114
+
115
+ client.query<T, V>(document, variables?, cache?): Promise<T>
116
+ client.mutate<T, V>(document, variables?): Promise<T>
117
+ client.use(middleware): void // imperative middleware add
118
+ ```
119
+
120
+ Features: lazy pipeline compilation, same-tick request deduplication, TypedDocumentString support.
121
+
122
+ ### Middleware Pipeline
123
+
124
+ Order matters: `auth → currency → [custom] → retry → timeout → errors (LAST)`
125
+
126
+ ```typescript
127
+ import {
128
+ authMiddleware, // Authorization: Bearer {token}
129
+ currencyMiddleware, // X-Preferred-Currency header
130
+ retryMiddleware, // Exponential backoff (queries only, not mutations)
131
+ timeoutMiddleware, // AbortController, edge-safe (default 5s)
132
+ errorMiddleware, // Normalizes all errors → StorefrontError (ALWAYS LAST)
133
+ } from '@doswiftly/storefront-sdk';
134
+
135
+ // Custom middleware
136
+ const logMiddleware: Middleware = async (req, next) => {
137
+ console.log('Request:', req.operationName);
138
+ const response = await next(req);
139
+ console.log('Response:', response.status);
140
+ return response;
141
+ };
142
+ ```
143
+
144
+ ### CartClient
145
+
146
+ ```typescript
147
+ const cartClient = new CartClient(client);
148
+
149
+ const cart = await cartClient.create();
150
+ const updated = await cartClient.addItems(cartId, [{ merchandiseId: 'v-123', quantity: 1 }]);
151
+ await cartClient.updateItems(cartId, [{ id: 'line-1', quantity: 3 }]);
152
+ await cartClient.removeItems(cartId, ['line-1']);
153
+ await cartClient.updateDiscountCodes(cartId, ['SAVE10']);
154
+ await cartClient.updateNote(cartId, 'Gift message');
155
+ await cartClient.updateBuyerIdentity(cartId, { email: 'user@example.com' });
156
+ const existing = await cartClient.get(cartId);
157
+ ```
158
+
159
+ Auto-throws `StorefrontError` with code `USER_ERROR` on validation failures.
160
+
161
+ ### AuthClient
162
+
163
+ ```typescript
164
+ const authClient = new AuthClient(client);
165
+
166
+ const { accessToken, expiresAt } = await authClient.login('user@example.com', 'pass');
167
+ await authClient.logout(accessToken);
168
+ const renewed = await authClient.renewToken(accessToken);
169
+ const { accessToken, customer } = await authClient.register({ email, password, firstName });
170
+ const customer = await authClient.getCustomer(accessToken);
171
+ ```
172
+
173
+ ### StorefrontError
174
+
175
+ ```typescript
176
+ import { StorefrontError, ErrorCodes } from '@doswiftly/storefront-sdk';
177
+
178
+ try {
179
+ await client.query(ProductQuery, { handle: 'missing' });
180
+ } catch (err) {
181
+ if (err instanceof StorefrontError) {
182
+ err.code; // 'GRAPHQL_ERROR' | 'NETWORK_ERROR' | 'TIMEOUT' | 'USER_ERROR' | ...
183
+ err.status; // HTTP status (0 for network errors)
184
+ err.graphqlErrors; // GraphQL-level errors
185
+ err.userErrors; // Field-level validation errors
186
+ err.hasUserErrors; // boolean
187
+ err.isNetworkError; // boolean
188
+ err.isTimeout; // boolean
189
+ }
190
+ }
191
+ ```
192
+
193
+ ### Format Utilities
194
+
195
+ ```typescript
196
+ import {
197
+ formatPrice,
198
+ formatPriceRange,
199
+ formatAmount,
200
+ formatDate,
201
+ formatDateTime,
202
+ formatNumber,
203
+ formatPercentage,
204
+ getCurrencySymbol,
205
+ CURRENCY_SYMBOLS,
206
+ CURRENCY_LOCALES,
207
+ } from '@doswiftly/storefront-sdk';
208
+
209
+ formatPrice({ amount: '99.99', currencyCode: 'USD' }); // "$99.99"
210
+ formatPriceRange(minPrice, maxPrice); // "$10.00 - $50.00"
211
+ formatAmount('115.20', 'EUR'); // "115,20 €"
212
+ formatDate(new Date()); // "Dec 9, 2025"
213
+ formatPercentage(0.15); // "15%"
214
+ ```
215
+
216
+ ### HTML Sanitizer
217
+
218
+ ```typescript
219
+ import { sanitizeHtml } from '@doswiftly/storefront-sdk';
220
+
221
+ // Defense-in-depth: strips <script>, event handlers, javascript: URLs
222
+ const safe = sanitizeHtml(userHtml);
223
+ ```
224
+
225
+ ### Connection Normalizer
226
+
227
+ ```typescript
228
+ import { normalizeConnection } from '@doswiftly/storefront-sdk';
229
+
230
+ // Relay connection → flat array
231
+ const { items, pageInfo, totalCount } = normalizeConnection(data.products);
232
+ ```
233
+
234
+ ### Auth Cookie Config (Platform Contract)
235
+
236
+ ```typescript
237
+ import { AUTH_COOKIE_NAME, AUTH_COOKIE_DEFAULTS } from '@doswiftly/storefront-sdk';
238
+
239
+ // AUTH_COOKIE_NAME = 'customerAccessToken'
240
+ // AUTH_COOKIE_DEFAULTS = { name, path, sameSite, httpOnly, secure, maxAge }
241
+ ```
242
+
243
+ ### Auth Cookie Handlers (API Route Factories)
244
+
245
+ ```typescript
246
+ import { createSetTokenHandler, createClearTokenHandler } from '@doswiftly/storefront-sdk';
247
+
248
+ // Next.js API route (2 lines):
249
+ // app/api/auth/set-token/route.ts
250
+ export const POST = createSetTokenHandler();
251
+
252
+ // app/api/auth/clear-token/route.ts
253
+ export const POST = createClearTokenHandler();
254
+ ```
255
+
256
+ Uses pure Web API (Request/Response) — 0 deps, framework-agnostic.
257
+ Security: origin validation, Content-Type check, CSRF via SameSite=Lax, httpOnly cookie.
258
+
259
+ ### Auth Token Client (Client-side)
260
+
261
+ ```typescript
262
+ import { createAuthTokenClient } from '@doswiftly/storefront-sdk';
263
+
264
+ const { setToken, clearToken } = createAuthTokenClient();
265
+ await setToken(accessToken); // POST /api/auth/set-token
266
+ await clearToken(); // POST /api/auth/clear-token
267
+ ```
268
+
269
+ ### Route Matching
270
+
271
+ ```typescript
272
+ import { matchesRoute } from '@doswiftly/storefront-sdk';
273
+
274
+ // Supports exact and prefix matching
275
+ matchesRoute('/account/orders', ['/account']); // true
276
+ matchesRoute('/products', ['/account']); // false
277
+ ```
278
+
279
+ ### Cache Strategies
280
+
281
+ ```typescript
282
+ import { cacheLong, cacheShort, cacheNone, cachePrivate, cacheCustom } from '@doswiftly/storefront-sdk/cache';
283
+
284
+ cacheLong() // 1h + 23h stale-while-revalidate
285
+ cacheLong({ tags: ['product', slug] }) // with Next.js revalidation tags
286
+ cacheShort() // 1s + 9s swr
287
+ cacheNone() // no-store
288
+ cachePrivate() // private, 1s + 9s swr
289
+ cacheCustom({ maxAge: 300, swr: 600 }) // 5min + 10min swr
290
+ ```
291
+
292
+ ## React Adapter
293
+
294
+ ### Providers
295
+
296
+ ```tsx
297
+ // Convenience (recommended)
298
+ <StorefrontProvider config={{ apiUrl, shopSlug }} shopData={shop}>
299
+ {children}
300
+ </StorefrontProvider>
301
+ ```
302
+
303
+ `StorefrontProvider` creates Zustand store instances via `useRef` and provides them through React Context. This eliminates module-level singleton issues with Turbopack/bundler module duplication.
304
+
305
+ ### useAuth
306
+
307
+ ```typescript
308
+ const {
309
+ login, // (email, password) => Promise<LoginResult>
310
+ logout, // () => Promise<LogoutResult>
311
+ renewToken, // () => Promise<TokenRenewResult>
312
+ isLoggingIn, isLoggingOut, isRenewingToken, isLoading,
313
+ error,
314
+ } = useAuth({
315
+ onSetToken: async (token) => { /* set httpOnly cookie via server route */ },
316
+ onClearToken: async () => { /* clear httpOnly cookie */ },
317
+ });
318
+ ```
319
+
320
+ ### useCartManager
321
+
322
+ ```typescript
323
+ const {
324
+ getCart, // () => Promise<Cart | null>
325
+ addItem, // (lines: CartLineInput[]) => Promise<Cart>
326
+ updateItem, // (lines: CartLineUpdateInput[]) => Promise<Cart>
327
+ removeItem, // (lineIds: string[]) => Promise<Cart>
328
+ updateDiscountCodes, updateNote, clearCart, getCartId,
329
+ isLoading, error,
330
+ } = useCartManager();
331
+ ```
332
+
333
+ Cart ID is persisted in a cookie (SSR/edge visible).
334
+
335
+ ### useHydrated
336
+
337
+ ```typescript
338
+ import { useHydrated } from '@doswiftly/storefront-sdk/react';
339
+
340
+ const isHydrated = useHydrated();
341
+ // false during SSR and first client render, true after hydration
342
+ // Use to guard browser-only state (localStorage, cookies, window)
343
+ ```
344
+
345
+ ### useDebouncedValue
346
+
347
+ ```typescript
348
+ import { useDebouncedValue } from '@doswiftly/storefront-sdk/react';
349
+
350
+ const debouncedQuery = useDebouncedValue(query, 300);
351
+ ```
352
+
353
+ ### createStoreContext
354
+
355
+ ```typescript
356
+ import { createStoreContext } from '@doswiftly/storefront-sdk/react';
357
+ import { createStore } from 'zustand/vanilla';
358
+
359
+ // Define store factory + Context-based hooks (eliminates module-level singletons)
360
+ const { Provider: CartProvider, useStore: useCartStore, useApi: useCartStoreApi } =
361
+ createStoreContext<CartState>('CartStore');
362
+
363
+ // In layout:
364
+ const cartStore = useRef(createCartStore()).current;
365
+ <CartProvider store={cartStore}>{children}</CartProvider>
366
+
367
+ // In components:
368
+ const isOpen = useCartStore((s) => s.isOpen);
369
+ const api = useCartStoreApi(); // for .getState() in callbacks
370
+ ```
371
+
372
+ ### Zustand Stores (Context-based)
373
+
374
+ Stores use `createStore()` from `zustand/vanilla` + React Context pattern. All store hooks require `StorefrontProvider` wrapper.
375
+
376
+ ```typescript
377
+ import { useAuthStore, useAuthHydrated, useCurrencyStore } from '@doswiftly/storefront-sdk/react';
378
+
379
+ // Auth — state
380
+ const { isAuthenticated, customer, accessToken } = useAuthStore();
381
+ // Auth — with selector
382
+ const isAuthenticated = useAuthStore(s => s.isAuthenticated);
383
+ // Auth — persist hydration (replaces old isHydrated store field)
384
+ const authHydrated = useAuthHydrated(); // true after localStorage rehydration
385
+
386
+ // Currency
387
+ const { currency, baseCurrency, supportedCurrencies, setCurrency, isLoaded } = useCurrencyStore();
388
+
389
+ // For .getState() in callbacks (e.g. logout, renewToken)
390
+ import { useAuthStoreApi, useCurrencyStoreApi } from '@doswiftly/storefront-sdk/react';
391
+ const authStore = useAuthStoreApi();
392
+ const token = authStore.getState().accessToken;
393
+ ```
394
+
395
+ ### Server-side
396
+
397
+ ```typescript
398
+ import { getStorefrontClient } from '@doswiftly/storefront-sdk/react/server';
399
+
400
+ // Server Component or Route Handler
401
+ const client = getStorefrontClient({
402
+ apiUrl: process.env.API_URL!,
403
+ shopSlug: process.env.SHOP_SLUG!,
404
+ });
405
+ ```
406
+
407
+ ## Template Integration
408
+
409
+ Storefronts (Next.js templates) import SDK for **infrastructure** and own their **data-fetching layer**:
410
+
411
+ ```
412
+ SDK provides: Template owns:
413
+ ├── Transport + Middleware ├── codegen.ts + generated/graphql.ts
414
+ ├── CartClient + AuthClient ├── lib/graphql/hooks.ts (React Query)
415
+ ├── Providers + Zustand stores ├── lib/graphql/server.ts (React cache)
416
+ ├── Format utilities ├── lib/graphql/fragments/
417
+ ├── sanitizeHtml ├── stores/ (UI state via createStoreContext)
418
+ ├── normalizeConnection ├── hooks/ (use-auth, use-cart-actions, etc.)
419
+ ├── Auth handlers + token client ├── lib/auth/routes.ts (route config)
420
+ ├── useHydrated + useDebouncedValue
421
+ ├── createStoreContext └── components/providers/
422
+ ├── AUTH_COOKIE_NAME + matchesRoute
423
+ └── Cache strategies
424
+ ```
425
+
426
+ Data-fetching hooks are generated locally via `@graphql-codegen/client-preset` (TypedDocumentString).
427
+
428
+ ## License
429
+
430
+ MIT
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Shared test helpers — mock fetch factory, mock client builder.
3
+ */
4
+ /**
5
+ * Create a mock fetch that returns the specified GraphQL response.
6
+ */
7
+ export declare function createMockFetch(responseData: unknown, options?: {
8
+ errors?: Array<{
9
+ message: string;
10
+ }>;
11
+ status?: number;
12
+ delay?: number;
13
+ }): typeof globalThis.fetch;
14
+ /**
15
+ * Create a mock fetch that tracks all calls and returns specified response.
16
+ */
17
+ export declare function createSpyFetch(responseData: unknown, options?: {
18
+ errors?: Array<{
19
+ message: string;
20
+ }>;
21
+ status?: number;
22
+ }): {
23
+ fetch: typeof globalThis.fetch;
24
+ calls: {
25
+ url: string;
26
+ init: RequestInit;
27
+ }[];
28
+ };
29
+ /**
30
+ * Create a mock fetch that fails with a network error.
31
+ */
32
+ export declare function createNetworkErrorFetch(errorMessage?: string): typeof globalThis.fetch;
33
+ /**
34
+ * Create a mock fetch that returns different responses on subsequent calls.
35
+ */
36
+ export declare function createSequenceFetch(responses: Array<{
37
+ data?: unknown;
38
+ errors?: Array<{
39
+ message: string;
40
+ }>;
41
+ status?: number;
42
+ }>): {
43
+ fetch: typeof globalThis.fetch;
44
+ callCount: () => number;
45
+ };
46
+ //# sourceMappingURL=test-helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-helpers.d.ts","sourceRoot":"","sources":["../../../src/__tests__/unit/test-helpers.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH;;GAEG;AACH,wBAAgB,eAAe,CAC7B,YAAY,EAAE,OAAO,EACrB,OAAO,CAAC,EAAE;IACR,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GACA,OAAO,UAAU,CAAC,KAAK,CAqBzB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,YAAY,EAAE,OAAO,EACrB,OAAO,CAAC,EAAE;IACR,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;WAqB0B,OAAO,UAAU,CAAC,KAAK;;aAnBxB,MAAM;cAAQ,WAAW;;EAoBpD;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,YAAY,SAAiB,GAAG,OAAO,UAAU,CAAC,KAAK,CAI9F;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,KAAK,CAAC;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GACzF;IAAE,KAAK,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAAC,SAAS,EAAE,MAAM,MAAM,CAAA;CAAE,CAiB7D"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Shared test helpers — mock fetch factory, mock client builder.
3
+ */
4
+ /**
5
+ * Create a mock fetch that returns the specified GraphQL response.
6
+ */
7
+ export function createMockFetch(responseData, options) {
8
+ const { errors, status = 200, delay: delayMs } = options ?? {};
9
+ return async (_url, _init) => {
10
+ if (delayMs) {
11
+ await new Promise(r => setTimeout(r, delayMs));
12
+ }
13
+ // Check for abort signal
14
+ if (_init?.signal?.aborted) {
15
+ throw new DOMException('The operation was aborted', 'AbortError');
16
+ }
17
+ const body = { data: responseData };
18
+ if (errors)
19
+ body.errors = errors;
20
+ return new Response(JSON.stringify(body), {
21
+ status,
22
+ headers: { 'Content-Type': 'application/json' },
23
+ });
24
+ };
25
+ }
26
+ /**
27
+ * Create a mock fetch that tracks all calls and returns specified response.
28
+ */
29
+ export function createSpyFetch(responseData, options) {
30
+ const calls = [];
31
+ const { errors, status = 200 } = options ?? {};
32
+ const fetchFn = async (url, init) => {
33
+ calls.push({ url: url.toString(), init: init });
34
+ if (init?.signal?.aborted) {
35
+ throw new DOMException('The operation was aborted', 'AbortError');
36
+ }
37
+ const body = { data: responseData };
38
+ if (errors)
39
+ body.errors = errors;
40
+ return new Response(JSON.stringify(body), {
41
+ status,
42
+ headers: { 'Content-Type': 'application/json' },
43
+ });
44
+ };
45
+ return { fetch: fetchFn, calls };
46
+ }
47
+ /**
48
+ * Create a mock fetch that fails with a network error.
49
+ */
50
+ export function createNetworkErrorFetch(errorMessage = 'fetch failed') {
51
+ return async () => {
52
+ throw new TypeError(errorMessage);
53
+ };
54
+ }
55
+ /**
56
+ * Create a mock fetch that returns different responses on subsequent calls.
57
+ */
58
+ export function createSequenceFetch(responses) {
59
+ let index = 0;
60
+ const fetchFn = async (_url, _init) => {
61
+ const response = responses[Math.min(index, responses.length - 1)];
62
+ index++;
63
+ const body = { data: response.data ?? null };
64
+ if (response.errors)
65
+ body.errors = response.errors;
66
+ return new Response(JSON.stringify(body), {
67
+ status: response.status ?? 200,
68
+ headers: { 'Content-Type': 'application/json' },
69
+ });
70
+ };
71
+ return { fetch: fetchFn, callCount: () => index };
72
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * AuthClient — plain async API for customer authentication (no React).
3
+ *
4
+ * Wraps StorefrontClient.mutate/query with typed auth operations.
5
+ * Auto-throws on userErrors via assertNoUserErrors.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const authClient = new AuthClient(storefrontClient);
10
+ *
11
+ * const result = await authClient.login('user@example.com', 'password');
12
+ * console.log(result.accessToken); // JWT token
13
+ *
14
+ * const customer = await authClient.getCustomer(result.accessToken);
15
+ * ```
16
+ */
17
+ import type { StorefrontClient } from '../client/types';
18
+ import type { AuthResult, Customer, CustomerCreateInput } from './types';
19
+ export declare class AuthClient {
20
+ private readonly client;
21
+ constructor(client: StorefrontClient);
22
+ /**
23
+ * Login with email and password.
24
+ * Returns access token + expiry.
25
+ */
26
+ login(email: string, password: string): Promise<AuthResult>;
27
+ /**
28
+ * Logout — invalidate token on backend.
29
+ * Does not throw on failure (token may already be expired).
30
+ */
31
+ logout(token: string): Promise<void>;
32
+ /**
33
+ * Renew access token — extends expiry.
34
+ */
35
+ renewToken(token: string): Promise<AuthResult>;
36
+ /**
37
+ * Register new customer account.
38
+ * Returns access token + customer data.
39
+ */
40
+ register(input: CustomerCreateInput): Promise<AuthResult>;
41
+ /**
42
+ * Fetch customer data by access token.
43
+ */
44
+ getCustomer(accessToken: string): Promise<Customer | null>;
45
+ }
46
+ //# sourceMappingURL=auth-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-client.d.ts","sourceRoot":"","sources":["../../../src/core/auth/auth-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAUzE,qBAAa,UAAU;IACT,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,gBAAgB;IAErD;;;OAGG;IACG,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAiBjE;;;OAGG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAa1C;;OAEG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAiBpD;;;OAGG;IACG,QAAQ,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,UAAU,CAAC;IAmB/D;;OAEG;IACG,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;CAOjE"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * AuthClient — plain async API for customer authentication (no React).
3
+ *
4
+ * Wraps StorefrontClient.mutate/query with typed auth operations.
5
+ * Auto-throws on userErrors via assertNoUserErrors.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const authClient = new AuthClient(storefrontClient);
10
+ *
11
+ * const result = await authClient.login('user@example.com', 'password');
12
+ * console.log(result.accessToken); // JWT token
13
+ *
14
+ * const customer = await authClient.getCustomer(result.accessToken);
15
+ * ```
16
+ */
17
+ import { assertNoUserErrors } from '../helpers/assert-no-user-errors';
18
+ import { CUSTOMER_LOGIN, CUSTOMER_LOGOUT, CUSTOMER_TOKEN_RENEW, CUSTOMER_CREATE, CUSTOMER_QUERY, } from '../operations/auth';
19
+ export class AuthClient {
20
+ client;
21
+ constructor(client) {
22
+ this.client = client;
23
+ }
24
+ /**
25
+ * Login with email and password.
26
+ * Returns access token + expiry.
27
+ */
28
+ async login(email, password) {
29
+ const data = await this.client.mutate(CUSTOMER_LOGIN, { input: { email, password } });
30
+ assertNoUserErrors(data.customerAccessTokenCreate);
31
+ const token = data.customerAccessTokenCreate.customerAccessToken;
32
+ return {
33
+ accessToken: token.accessToken,
34
+ expiresAt: token.expiresAt,
35
+ };
36
+ }
37
+ /**
38
+ * Logout — invalidate token on backend.
39
+ * Does not throw on failure (token may already be expired).
40
+ */
41
+ async logout(token) {
42
+ try {
43
+ await this.client.mutate(CUSTOMER_LOGOUT, { customerAccessToken: token });
44
+ }
45
+ catch {
46
+ // Silently ignore — token may already be expired
47
+ }
48
+ }
49
+ /**
50
+ * Renew access token — extends expiry.
51
+ */
52
+ async renewToken(token) {
53
+ const data = await this.client.mutate(CUSTOMER_TOKEN_RENEW, { customerAccessToken: token });
54
+ assertNoUserErrors(data.customerAccessTokenRenew);
55
+ const renewed = data.customerAccessTokenRenew.customerAccessToken;
56
+ return {
57
+ accessToken: renewed.accessToken,
58
+ expiresAt: renewed.expiresAt,
59
+ };
60
+ }
61
+ /**
62
+ * Register new customer account.
63
+ * Returns access token + customer data.
64
+ */
65
+ async register(input) {
66
+ const data = await this.client.mutate(CUSTOMER_CREATE, { input });
67
+ assertNoUserErrors(data.customerCreate);
68
+ const token = data.customerCreate.customerAccessToken;
69
+ return {
70
+ accessToken: token.accessToken,
71
+ expiresAt: token.expiresAt,
72
+ customer: data.customerCreate.customer ?? undefined,
73
+ };
74
+ }
75
+ /**
76
+ * Fetch customer data by access token.
77
+ */
78
+ async getCustomer(accessToken) {
79
+ const data = await this.client.query(CUSTOMER_QUERY, { customerAccessToken: accessToken });
80
+ return data.customer;
81
+ }
82
+ }