@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
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Formatting Utilities
3
+ *
4
+ * Pure functions for formatting prices, dates, numbers.
5
+ * 0 runtime dependencies — works in Node.js, Edge, Deno, Bun.
6
+ */
7
+
8
+ // ============================================================================
9
+ // TYPES
10
+ // ============================================================================
11
+
12
+ export interface PriceMoney {
13
+ amount: string;
14
+ currencyCode: string;
15
+ }
16
+
17
+ // ============================================================================
18
+ // CONSTANTS
19
+ // ============================================================================
20
+
21
+ /** Currency symbols mapping */
22
+ export const CURRENCY_SYMBOLS: Record<string, string> = {
23
+ PLN: 'zł',
24
+ EUR: '€',
25
+ USD: '$',
26
+ GBP: '£',
27
+ CHF: 'CHF',
28
+ CZK: 'Kč',
29
+ SEK: 'kr',
30
+ NOK: 'kr',
31
+ DKK: 'kr',
32
+ JPY: '¥',
33
+ CNY: '¥',
34
+ AUD: 'A$',
35
+ CAD: 'C$',
36
+ };
37
+
38
+ /** Currency locale mapping for proper formatting */
39
+ export const CURRENCY_LOCALES: Record<string, string> = {
40
+ PLN: 'pl-PL',
41
+ EUR: 'de-DE',
42
+ USD: 'en-US',
43
+ GBP: 'en-GB',
44
+ CHF: 'de-CH',
45
+ CZK: 'cs-CZ',
46
+ SEK: 'sv-SE',
47
+ NOK: 'nb-NO',
48
+ DKK: 'da-DK',
49
+ JPY: 'ja-JP',
50
+ CNY: 'zh-CN',
51
+ AUD: 'en-AU',
52
+ CAD: 'en-CA',
53
+ };
54
+
55
+ // ============================================================================
56
+ // FORMATTER CACHE (avoids re-creating Intl objects on every call)
57
+ // ============================================================================
58
+
59
+ const numberFormatCache = new Map<string, Intl.NumberFormat>();
60
+ const dateFormatCache = new Map<string, Intl.DateTimeFormat>();
61
+
62
+ function getCurrencyFormatter(locale: string, currency: string): Intl.NumberFormat {
63
+ const key = `${locale}:${currency}`;
64
+ let fmt = numberFormatCache.get(key);
65
+ if (!fmt) {
66
+ fmt = new Intl.NumberFormat(locale, {
67
+ style: 'currency',
68
+ currency,
69
+ minimumFractionDigits: 2,
70
+ maximumFractionDigits: 2,
71
+ });
72
+ numberFormatCache.set(key, fmt);
73
+ }
74
+ return fmt;
75
+ }
76
+
77
+ function getNumberFormatter(locale: string): Intl.NumberFormat {
78
+ let fmt = numberFormatCache.get(locale);
79
+ if (!fmt) {
80
+ fmt = new Intl.NumberFormat(locale);
81
+ numberFormatCache.set(locale, fmt);
82
+ }
83
+ return fmt;
84
+ }
85
+
86
+ function getDateFormatter(locale: string, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {
87
+ const key = `${locale}:${JSON.stringify(options)}`;
88
+ let fmt = dateFormatCache.get(key);
89
+ if (!fmt) {
90
+ fmt = new Intl.DateTimeFormat(locale, options);
91
+ dateFormatCache.set(key, fmt);
92
+ }
93
+ return fmt;
94
+ }
95
+
96
+ // ============================================================================
97
+ // UTILITY FUNCTIONS
98
+ // ============================================================================
99
+
100
+ /**
101
+ * Get currency symbol
102
+ */
103
+ export function getCurrencySymbol(code: string): string {
104
+ return CURRENCY_SYMBOLS[code] || code;
105
+ }
106
+
107
+ // ============================================================================
108
+ // PRICE FORMATTING
109
+ // ============================================================================
110
+
111
+ /**
112
+ * Format price with currency symbol
113
+ *
114
+ * @example
115
+ * ```typescript
116
+ * formatPrice({ amount: "99.99", currencyCode: "USD" })
117
+ * // => "$99.99"
118
+ * ```
119
+ */
120
+ export function formatPrice(price: PriceMoney | null | undefined): string {
121
+ if (!price) return '';
122
+
123
+ const amount = parseFloat(price.amount);
124
+ const code = price.currencyCode;
125
+ const locale = CURRENCY_LOCALES[code] || 'en-US';
126
+
127
+ try {
128
+ return getCurrencyFormatter(locale, code).format(amount);
129
+ } catch {
130
+ const symbol = CURRENCY_SYMBOLS[code] || code;
131
+ return `${amount.toFixed(2)} ${symbol}`;
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Format price range
137
+ *
138
+ * @example
139
+ * ```typescript
140
+ * formatPriceRange(
141
+ * { amount: "10.00", currencyCode: "USD" },
142
+ * { amount: "50.00", currencyCode: "USD" }
143
+ * )
144
+ * // => "$10.00 - $50.00"
145
+ * ```
146
+ */
147
+ export function formatPriceRange(
148
+ minPrice: PriceMoney,
149
+ maxPrice: PriceMoney,
150
+ ): string {
151
+ if (minPrice.amount === maxPrice.amount) {
152
+ return formatPrice(minPrice);
153
+ }
154
+
155
+ return `${formatPrice(minPrice)} - ${formatPrice(maxPrice)}`;
156
+ }
157
+
158
+ /**
159
+ * Format amount with currency
160
+ *
161
+ * @example
162
+ * ```tsx
163
+ * const formatted = formatAmount("115.20", "EUR");
164
+ * // => "115,20 €"
165
+ * ```
166
+ */
167
+ export function formatAmount(
168
+ amount: string | number,
169
+ currencyCode: string,
170
+ ): string {
171
+ const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount;
172
+ const locale = CURRENCY_LOCALES[currencyCode] || 'en-US';
173
+
174
+ try {
175
+ return getCurrencyFormatter(locale, currencyCode).format(numAmount);
176
+ } catch {
177
+ const symbol = CURRENCY_SYMBOLS[currencyCode] || currencyCode;
178
+ return `${numAmount.toFixed(2)} ${symbol}`;
179
+ }
180
+ }
181
+
182
+ // ============================================================================
183
+ // DATE FORMATTING
184
+ // ============================================================================
185
+
186
+ /**
187
+ * Format date to locale string
188
+ *
189
+ * @example
190
+ * ```typescript
191
+ * formatDate(new Date())
192
+ * // => "Dec 9, 2025"
193
+ * ```
194
+ */
195
+ export function formatDate(date: Date | string): string {
196
+ const d = typeof date === 'string' ? new Date(date) : date;
197
+
198
+ return getDateFormatter('en-US', {
199
+ year: 'numeric',
200
+ month: 'short',
201
+ day: 'numeric',
202
+ }).format(d);
203
+ }
204
+
205
+ /**
206
+ * Format date with time
207
+ *
208
+ * @example
209
+ * ```typescript
210
+ * formatDateTime(new Date())
211
+ * // => "Dec 9, 2025, 10:30 PM"
212
+ * ```
213
+ */
214
+ export function formatDateTime(date: Date | string): string {
215
+ const d = typeof date === 'string' ? new Date(date) : date;
216
+
217
+ return getDateFormatter('en-US', {
218
+ year: 'numeric',
219
+ month: 'short',
220
+ day: 'numeric',
221
+ hour: 'numeric',
222
+ minute: '2-digit',
223
+ }).format(d);
224
+ }
225
+
226
+ // ============================================================================
227
+ // NUMBER FORMATTING
228
+ // ============================================================================
229
+
230
+ /**
231
+ * Format number with thousands separator
232
+ *
233
+ * @example
234
+ * ```typescript
235
+ * formatNumber(1234567)
236
+ * // => "1,234,567"
237
+ * ```
238
+ */
239
+ export function formatNumber(num: number): string {
240
+ return getNumberFormatter('en-US').format(num);
241
+ }
242
+
243
+ /**
244
+ * Format percentage
245
+ *
246
+ * @example
247
+ * ```typescript
248
+ * formatPercentage(0.15)
249
+ * // => "15%"
250
+ * ```
251
+ */
252
+ export function formatPercentage(value: number): string {
253
+ return `${Math.round(value * 100)}%`;
254
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Auto-throw on userErrors — used internally by CartClient and AuthClient.
3
+ *
4
+ * Frontend code doesn't need to manually check `if (userErrors.length)`.
5
+ */
6
+
7
+ import type { UserError } from '../client/types';
8
+ import { StorefrontError, ErrorCodes } from '../errors';
9
+
10
+ export function assertNoUserErrors(
11
+ result: { userErrors?: UserError[] | null },
12
+ ): void {
13
+ if (result.userErrors && result.userErrors.length > 0) {
14
+ const firstError = result.userErrors[0];
15
+ throw new StorefrontError({
16
+ code: ErrorCodes.USER_ERROR,
17
+ message: firstError.message,
18
+ userErrors: result.userErrors,
19
+ });
20
+ }
21
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Relay-style GraphQL connection normalizer.
3
+ *
4
+ * Converts `{ edges: [{ node }], pageInfo, totalCount }` → flat array.
5
+ * Pure function, 0 deps, works everywhere.
6
+ */
7
+
8
+ export interface ConnectionEdge<T> {
9
+ node: T;
10
+ cursor: string;
11
+ }
12
+
13
+ export interface ConnectionPageInfo {
14
+ hasNextPage: boolean;
15
+ hasPreviousPage: boolean;
16
+ startCursor?: string | null;
17
+ endCursor?: string | null;
18
+ }
19
+
20
+ export interface Connection<T> {
21
+ edges: ConnectionEdge<T>[];
22
+ pageInfo: ConnectionPageInfo;
23
+ totalCount?: number;
24
+ }
25
+
26
+ export interface NormalizedConnection<T> {
27
+ items: T[];
28
+ pageInfo: ConnectionPageInfo;
29
+ totalCount?: number;
30
+ }
31
+
32
+ /**
33
+ * Normalize a GraphQL Relay connection to a flat array.
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * const { items, pageInfo, totalCount } = normalizeConnection(data.products);
38
+ * ```
39
+ */
40
+ export function normalizeConnection<T>(
41
+ connection: Connection<T>,
42
+ ): NormalizedConnection<T> {
43
+ return {
44
+ items: connection.edges.map((edge) => edge.node),
45
+ pageInfo: connection.pageInfo,
46
+ totalCount: connection.totalCount,
47
+ };
48
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Lightweight HTML sanitizer for defense-in-depth.
3
+ * Primary sanitization happens on the backend (sanitize-html library).
4
+ * This strips dangerous tags/attributes as an extra safety layer.
5
+ * Works in both Node.js and Edge runtimes (no external dependencies).
6
+ */
7
+
8
+ const DANGEROUS_TAG_PATTERNS = [
9
+ // Tags with content that must be fully removed
10
+ /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script\s*>/gi,
11
+ /<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe\s*>/gi,
12
+ /<object\b[^<]*(?:(?!<\/object>)<[^<]*)*<\/object\s*>/gi,
13
+ /<form\b[^<]*(?:(?!<\/form>)<[^<]*)*<\/form\s*>/gi,
14
+ /<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style\s*>/gi,
15
+ // Self-closing / void dangerous tags
16
+ /<embed\b[^>]*\/?>/gi,
17
+ /<link\b[^>]*\/?>/gi,
18
+ /<meta\b[^>]*\/?>/gi,
19
+ /<base\b[^>]*\/?>/gi,
20
+ /<applet\b[^<]*(?:(?!<\/applet>)<[^<]*)*<\/applet\s*>/gi,
21
+ ];
22
+
23
+ // Event handler attributes (onclick, onerror, onload, etc.)
24
+ const EVENT_HANDLER_PATTERN = /\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi;
25
+
26
+ // javascript: protocol in href/src/action attributes
27
+ const JS_PROTOCOL_PATTERN = /(href|src|action)\s*=\s*["']\s*javascript\s*:/gi;
28
+
29
+ export function sanitizeHtml(html: string): string {
30
+ if (!html) return html;
31
+
32
+ let result = html;
33
+
34
+ for (const pattern of DANGEROUS_TAG_PATTERNS) {
35
+ result = result.replace(pattern, '');
36
+ }
37
+
38
+ result = result.replace(EVENT_HANDLER_PATTERN, '');
39
+ result = result.replace(JS_PROTOCOL_PATTERN, '$1="');
40
+
41
+ return result;
42
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * @doswiftly/storefront-sdk — Core (framework-agnostic)
3
+ *
4
+ * 100% framework-agnostic, 0 runtime dependencies.
5
+ * Works in Node.js, Edge Workers, Deno, Bun, CLI scripts — without React.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import {
10
+ * createStorefrontClient,
11
+ * authMiddleware,
12
+ * currencyMiddleware,
13
+ * retryMiddleware,
14
+ * timeoutMiddleware,
15
+ * errorMiddleware,
16
+ * CartClient,
17
+ * AuthClient,
18
+ * } from '@doswiftly/storefront-sdk';
19
+ *
20
+ * const client = createStorefrontClient({
21
+ * apiUrl: 'https://api.doswiftly.pl',
22
+ * shopSlug: 'my-shop',
23
+ * middleware: [
24
+ * authMiddleware(() => getToken()),
25
+ * currencyMiddleware(() => getCurrency()),
26
+ * retryMiddleware({ maxRetries: 2 }),
27
+ * timeoutMiddleware({ timeout: 5000 }),
28
+ * errorMiddleware(),
29
+ * ],
30
+ * });
31
+ *
32
+ * const cartClient = new CartClient(client);
33
+ * const cart = await cartClient.create();
34
+ * ```
35
+ */
36
+
37
+ // Client factory
38
+ export { createStorefrontClient } from './client/create-client';
39
+
40
+ // Client types
41
+ export type {
42
+ StorefrontClient,
43
+ StorefrontClientConfig,
44
+ Middleware,
45
+ ExecuteFn,
46
+ GraphQLRequest,
47
+ GraphQLResponse,
48
+ GraphQLErrorInfo,
49
+ UserError,
50
+ CacheStrategy,
51
+ CacheOptions,
52
+ TypedDocumentString,
53
+ } from './client/types';
54
+
55
+ // Middleware
56
+ export { authMiddleware } from './middleware/auth';
57
+ export { currencyMiddleware } from './middleware/currency';
58
+ export { retryMiddleware, type RetryOptions } from './middleware/retry';
59
+ export { timeoutMiddleware, type TimeoutOptions } from './middleware/timeout';
60
+ export { errorMiddleware } from './middleware/errors';
61
+
62
+ // Errors
63
+ export { StorefrontError, ErrorCodes, type StorefrontErrorOptions } from './errors';
64
+
65
+ // Cache strategies
66
+ export {
67
+ cacheNone,
68
+ cacheShort,
69
+ cacheLong,
70
+ cachePrivate,
71
+ cacheCustom,
72
+ generateCacheControlHeader,
73
+ type CacheOverrides,
74
+ } from './cache';
75
+
76
+ // Cart client
77
+ export { CartClient } from './cart/cart-client';
78
+ export type {
79
+ Cart,
80
+ CartLine,
81
+ CartLineMerchandise,
82
+ CartLineCost,
83
+ CartCost,
84
+ CartBuyerIdentity,
85
+ CartDiscountCode,
86
+ CartDiscountAllocation,
87
+ CartLineInput,
88
+ CartLineUpdateInput,
89
+ CartCreateInput,
90
+ CartBuyerIdentityInput,
91
+ Money,
92
+ } from './cart/types';
93
+
94
+ // Auth client
95
+ export { AuthClient } from './auth/auth-client';
96
+ export type {
97
+ Customer,
98
+ CustomerAccessToken,
99
+ MailingAddress,
100
+ AuthResult,
101
+ CustomerCreateInput,
102
+ } from './auth/types';
103
+
104
+ // Helpers
105
+ export { assertNoUserErrors } from './helpers/assert-no-user-errors';
106
+ export { sanitizeHtml } from './helpers/sanitize-html';
107
+ export {
108
+ normalizeConnection,
109
+ type Connection,
110
+ type ConnectionEdge,
111
+ type ConnectionPageInfo,
112
+ type NormalizedConnection,
113
+ } from './helpers/normalize-connection';
114
+
115
+ // Format utilities
116
+ export {
117
+ formatPrice,
118
+ formatPriceRange,
119
+ formatAmount,
120
+ formatDate,
121
+ formatDateTime,
122
+ formatNumber,
123
+ formatPercentage,
124
+ getCurrencySymbol,
125
+ CURRENCY_SYMBOLS,
126
+ CURRENCY_LOCALES,
127
+ type PriceMoney,
128
+ } from './format';
129
+
130
+ // Auth cookie config (platform contract)
131
+ export {
132
+ AUTH_COOKIE_NAME,
133
+ AUTH_COOKIE_DEFAULTS,
134
+ type AuthCookieConfig,
135
+ } from './auth/cookie-config';
136
+
137
+ // Auth route matching
138
+ export { matchesRoute, type RouteProtectionConfig } from './auth/routes';
139
+
140
+ // Auth cookie handlers (API route factories)
141
+ export { createSetTokenHandler, createClearTokenHandler } from './auth/handlers';
142
+
143
+ // Auth token client (client-side fetch helpers)
144
+ export { createAuthTokenClient, type AuthTokenClient } from './auth/token-client';
145
+
146
+ // Utilities
147
+ export { getOperationName } from './client/operation-name';
148
+ export { hashQuery } from './client/hash';
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Auth middleware — adds Authorization header from token getter.
3
+ *
4
+ * Token is resolved lazily (on each request) so it picks up
5
+ * store changes, token renewals, etc.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { authMiddleware } from '@doswiftly/storefront-sdk';
10
+ *
11
+ * client.use(authMiddleware(() => authStore.getState().accessToken));
12
+ * ```
13
+ */
14
+
15
+ import type { Middleware } from '../client/types';
16
+
17
+ export function authMiddleware(
18
+ getToken: () => string | null | undefined,
19
+ ): Middleware {
20
+ return (request, next) => {
21
+ const token = getToken();
22
+ if (token) {
23
+ request.headers['Authorization'] = `Bearer ${token}`;
24
+ }
25
+ return next(request);
26
+ };
27
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Currency middleware — adds X-Preferred-Currency header.
3
+ *
4
+ * Currency is resolved lazily so currency switches are reflected immediately.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { currencyMiddleware } from '@doswiftly/storefront-sdk';
9
+ *
10
+ * client.use(currencyMiddleware(() => currencyStore.getState().currency));
11
+ * ```
12
+ */
13
+
14
+ import type { Middleware } from '../client/types';
15
+
16
+ export function currencyMiddleware(
17
+ getCurrency: () => string | null | undefined,
18
+ ): Middleware {
19
+ return (request, next) => {
20
+ const currency = getCurrency();
21
+ if (currency) {
22
+ request.headers['X-Preferred-Currency'] = currency;
23
+ }
24
+ return next(request);
25
+ };
26
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Error normalization middleware — ALWAYS LAST in the pipeline.
3
+ *
4
+ * Catches all errors from downstream middleware and the transport,
5
+ * normalizes them into StorefrontError instances.
6
+ *
7
+ * Handles:
8
+ * - Network errors (fetch failures)
9
+ * - HTTP errors (non-200 responses)
10
+ * - GraphQL errors (errors array in response)
11
+ * - Abort/timeout errors
12
+ * - Missing data in response
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * import { errorMiddleware } from '@doswiftly/storefront-sdk';
17
+ *
18
+ * // Always add as the LAST middleware
19
+ * client.use(errorMiddleware());
20
+ * ```
21
+ */
22
+
23
+ import type { Middleware } from '../client/types';
24
+ import { StorefrontError, ErrorCodes } from '../errors';
25
+
26
+ export function errorMiddleware(): Middleware {
27
+ return async (request, next) => {
28
+ let response;
29
+
30
+ try {
31
+ response = await next(request);
32
+ } catch (error) {
33
+ // Already a StorefrontError — re-throw as-is
34
+ if (error instanceof StorefrontError) {
35
+ throw error;
36
+ }
37
+
38
+ // AbortError (timeout handled by timeout middleware, manual abort here)
39
+ if (error instanceof Error && error.name === 'AbortError') {
40
+ throw new StorefrontError({
41
+ code: ErrorCodes.TIMEOUT,
42
+ message: 'Request was aborted',
43
+ cause: error,
44
+ });
45
+ }
46
+
47
+ // Network error (fetch failed — DNS, connection refused, etc.)
48
+ throw new StorefrontError({
49
+ code: ErrorCodes.NETWORK_ERROR,
50
+ message: error instanceof Error ? error.message : 'Network request failed',
51
+ cause: error,
52
+ });
53
+ }
54
+
55
+ // HTTP error (non-2xx response)
56
+ if (response.status >= 400) {
57
+ throw new StorefrontError({
58
+ code: ErrorCodes.HTTP_ERROR,
59
+ message: `HTTP ${response.status}`,
60
+ status: response.status,
61
+ graphqlErrors: response.errors ?? [],
62
+ });
63
+ }
64
+
65
+ // GraphQL errors in the response
66
+ if (response.errors?.length) {
67
+ throw new StorefrontError({
68
+ code: ErrorCodes.GRAPHQL_ERROR,
69
+ message: response.errors[0].message,
70
+ status: response.status,
71
+ graphqlErrors: response.errors,
72
+ });
73
+ }
74
+
75
+ // No data returned
76
+ if (response.data === undefined || response.data === null) {
77
+ throw new StorefrontError({
78
+ code: ErrorCodes.NO_DATA,
79
+ message: 'No data returned from GraphQL',
80
+ status: response.status,
81
+ });
82
+ }
83
+
84
+ return response;
85
+ };
86
+ }