@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,168 @@
1
+ /**
2
+ * Auth cookie handlers — factory functions for API routes.
3
+ *
4
+ * 0 deps, pure Web API (Request/Response). Framework-agnostic.
5
+ * Creates set-token and clear-token handlers that any storefront
6
+ * can use as 2-line API routes.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * // app/api/auth/set-token/route.ts
11
+ * import { createSetTokenHandler } from '@doswiftly/storefront-sdk';
12
+ * export const POST = createSetTokenHandler();
13
+ * ```
14
+ */
15
+
16
+ import { AUTH_COOKIE_NAME, AUTH_COOKIE_DEFAULTS } from './cookie-config';
17
+
18
+ interface SetTokenHandlerOptions {
19
+ maxAge?: number;
20
+ }
21
+
22
+ function serializeCookie(
23
+ name: string,
24
+ value: string,
25
+ options: {
26
+ maxAge?: number;
27
+ path?: string;
28
+ sameSite?: string;
29
+ secure?: boolean;
30
+ httpOnly?: boolean;
31
+ },
32
+ ): string {
33
+ const parts = [`${name}=${value}`];
34
+ if (options.maxAge != null) parts.push(`Max-Age=${options.maxAge}`);
35
+ if (options.path) parts.push(`Path=${options.path}`);
36
+ if (options.sameSite) parts.push(`SameSite=${options.sameSite}`);
37
+ if (options.secure) parts.push('Secure');
38
+ if (options.httpOnly) parts.push('HttpOnly');
39
+ return parts.join('; ');
40
+ }
41
+
42
+ function validateOrigin(request: Request): Response | null {
43
+ const origin = request.headers.get('origin');
44
+ const host = request.headers.get('host');
45
+
46
+ if (origin && !origin.includes(host || '')) {
47
+ return new Response(JSON.stringify({ error: 'Invalid origin' }), {
48
+ status: 403,
49
+ headers: { 'Content-Type': 'application/json' },
50
+ });
51
+ }
52
+ return null;
53
+ }
54
+
55
+ /**
56
+ * Create a POST handler that sets the auth token in an httpOnly cookie.
57
+ *
58
+ * Security: origin validation, Content-Type check, CSRF (SameSite=Lax),
59
+ * httpOnly (XSS protection), Secure in production.
60
+ */
61
+ export function createSetTokenHandler(
62
+ overrides?: SetTokenHandlerOptions,
63
+ ): (request: Request) => Promise<Response> {
64
+ const maxAge = overrides?.maxAge ?? AUTH_COOKIE_DEFAULTS.maxAge;
65
+
66
+ return async (request: Request): Promise<Response> => {
67
+ try {
68
+ // 1. CSRF: validate origin
69
+ const originError = validateOrigin(request);
70
+ if (originError) return originError;
71
+
72
+ // 2. Validate Content-Type
73
+ const contentType = request.headers.get('content-type');
74
+ if (!contentType?.includes('application/json')) {
75
+ return new Response(
76
+ JSON.stringify({ error: 'Content-Type must be application/json' }),
77
+ { status: 400, headers: { 'Content-Type': 'application/json' } },
78
+ );
79
+ }
80
+
81
+ // 3. Parse body
82
+ let body: { token?: string };
83
+ try {
84
+ body = await request.json();
85
+ } catch {
86
+ return new Response(
87
+ JSON.stringify({ error: 'Invalid JSON body' }),
88
+ { status: 400, headers: { 'Content-Type': 'application/json' } },
89
+ );
90
+ }
91
+
92
+ const { token } = body;
93
+ if (!token || typeof token !== 'string' || token.trim() === '') {
94
+ return new Response(
95
+ JSON.stringify({ error: 'Token is required and must be a non-empty string' }),
96
+ { status: 400, headers: { 'Content-Type': 'application/json' } },
97
+ );
98
+ }
99
+
100
+ // 4. Set httpOnly cookie
101
+ const cookie = serializeCookie(AUTH_COOKIE_NAME, token, {
102
+ maxAge,
103
+ path: AUTH_COOKIE_DEFAULTS.path,
104
+ sameSite: AUTH_COOKIE_DEFAULTS.sameSite,
105
+ secure: AUTH_COOKIE_DEFAULTS.secure,
106
+ httpOnly: AUTH_COOKIE_DEFAULTS.httpOnly,
107
+ });
108
+
109
+ return new Response(
110
+ JSON.stringify({ success: true, message: 'Token set successfully' }),
111
+ {
112
+ status: 200,
113
+ headers: {
114
+ 'Content-Type': 'application/json',
115
+ 'Set-Cookie': cookie,
116
+ },
117
+ },
118
+ );
119
+ } catch (error) {
120
+ console.error('Error setting auth token:', error);
121
+ return new Response(
122
+ JSON.stringify({ error: 'Internal server error' }),
123
+ { status: 500, headers: { 'Content-Type': 'application/json' } },
124
+ );
125
+ }
126
+ };
127
+ }
128
+
129
+ /**
130
+ * Create a POST handler that clears the auth token cookie.
131
+ *
132
+ * Security: origin validation, immediate expiration (maxAge=0).
133
+ */
134
+ export function createClearTokenHandler(): (request: Request) => Promise<Response> {
135
+ return async (request: Request): Promise<Response> => {
136
+ try {
137
+ // 1. CSRF: validate origin
138
+ const originError = validateOrigin(request);
139
+ if (originError) return originError;
140
+
141
+ // 2. Clear cookie (maxAge=0)
142
+ const cookie = serializeCookie(AUTH_COOKIE_NAME, '', {
143
+ maxAge: 0,
144
+ path: AUTH_COOKIE_DEFAULTS.path,
145
+ sameSite: AUTH_COOKIE_DEFAULTS.sameSite,
146
+ secure: AUTH_COOKIE_DEFAULTS.secure,
147
+ httpOnly: AUTH_COOKIE_DEFAULTS.httpOnly,
148
+ });
149
+
150
+ return new Response(
151
+ JSON.stringify({ success: true, message: 'Token cleared successfully' }),
152
+ {
153
+ status: 200,
154
+ headers: {
155
+ 'Content-Type': 'application/json',
156
+ 'Set-Cookie': cookie,
157
+ },
158
+ },
159
+ );
160
+ } catch (error) {
161
+ console.error('Error clearing auth token:', error);
162
+ return new Response(
163
+ JSON.stringify({ error: 'Internal server error' }),
164
+ { status: 500, headers: { 'Content-Type': 'application/json' } },
165
+ );
166
+ }
167
+ };
168
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Route matching utility for auth redirects.
3
+ *
4
+ * Pure function — route lists stay in the template (SSOT config),
5
+ * but matching logic lives in the SDK (reusable across templates).
6
+ */
7
+
8
+ export interface RouteProtectionConfig {
9
+ protectedRoutes: string[];
10
+ guestOnlyRoutes: string[];
11
+ redirects: {
12
+ unauthenticated: string;
13
+ authenticated: string;
14
+ };
15
+ }
16
+
17
+ /**
18
+ * Check if a pathname matches any route in the list.
19
+ * Supports both exact matches and prefix matches
20
+ * (e.g., "/account" matches "/account/orders").
21
+ */
22
+ export function matchesRoute(pathname: string, routes: string[]): boolean {
23
+ return routes.some(
24
+ (route) => pathname === route || pathname.startsWith(`${route}/`),
25
+ );
26
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Client-side auth token helpers — fetch-based, 0 deps.
3
+ *
4
+ * Calls API routes to set/clear the httpOnly auth cookie.
5
+ * Used by template hooks (use-auth.ts, use-auth-sync.ts, register-form.tsx).
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { createAuthTokenClient } from '@doswiftly/storefront-sdk';
10
+ * const { setToken, clearToken } = createAuthTokenClient();
11
+ *
12
+ * await setToken(accessToken);
13
+ * await clearToken();
14
+ * ```
15
+ */
16
+
17
+ export interface AuthTokenClient {
18
+ setToken: (token: string) => Promise<void>;
19
+ clearToken: () => Promise<void>;
20
+ }
21
+
22
+ /**
23
+ * Create a client for managing auth tokens via API routes.
24
+ *
25
+ * @param basePath - Base path for API routes (default: "/api/auth")
26
+ */
27
+ export function createAuthTokenClient(basePath = '/api/auth'): AuthTokenClient {
28
+ return {
29
+ async setToken(token: string): Promise<void> {
30
+ const response = await fetch(`${basePath}/set-token`, {
31
+ method: 'POST',
32
+ headers: { 'Content-Type': 'application/json' },
33
+ body: JSON.stringify({ token }),
34
+ });
35
+
36
+ if (!response.ok) {
37
+ throw new Error('Failed to set authentication token');
38
+ }
39
+ },
40
+
41
+ async clearToken(): Promise<void> {
42
+ const response = await fetch(`${basePath}/clear-token`, {
43
+ method: 'POST',
44
+ });
45
+
46
+ if (!response.ok) {
47
+ throw new Error('Failed to clear authentication token');
48
+ }
49
+ },
50
+ };
51
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Auth types — manual (no codegen).
3
+ */
4
+
5
+ export interface CustomerAccessToken {
6
+ accessToken: string;
7
+ expiresAt: string;
8
+ }
9
+
10
+ export interface Customer {
11
+ id: string;
12
+ email: string;
13
+ firstName: string | null;
14
+ lastName: string | null;
15
+ displayName: string | null;
16
+ phone: string | null;
17
+ emailVerified: boolean;
18
+ emailMarketingState: string | null;
19
+ defaultAddress: MailingAddress | null;
20
+ ordersCount: number;
21
+ totalSpent: { amount: string; currencyCode: string } | null;
22
+ createdAt: string;
23
+ updatedAt: string;
24
+ }
25
+
26
+ export interface MailingAddress {
27
+ id: string;
28
+ address1: string | null;
29
+ address2: string | null;
30
+ city: string | null;
31
+ company: string | null;
32
+ country: string | null;
33
+ countryCode: string | null;
34
+ firstName: string | null;
35
+ lastName: string | null;
36
+ phone: string | null;
37
+ province: string | null;
38
+ provinceCode: string | null;
39
+ zip: string | null;
40
+ isDefault: boolean;
41
+ }
42
+
43
+ export interface AuthResult {
44
+ accessToken: string;
45
+ expiresAt: string;
46
+ customer?: Customer;
47
+ }
48
+
49
+ export interface CustomerCreateInput {
50
+ email: string;
51
+ password: string;
52
+ firstName?: string;
53
+ lastName?: string;
54
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Cache strategies — functions (not constants) with override support.
3
+ *
4
+ * Inspired by Shopify Hydrogen's caching strategies.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { cacheLong, cacheShort } from '@doswiftly/storefront-sdk';
9
+ *
10
+ * // Default
11
+ * const data = await client.query(ShopQuery, {}, cacheLong());
12
+ *
13
+ * // Override with tags (Next.js revalidateTag)
14
+ * const data = await client.query(ProductQuery, { slug }, cacheLong({ tags: ['product', slug] }));
15
+ * ```
16
+ */
17
+
18
+ import type { CacheStrategy, CacheOptions } from './client/types';
19
+
20
+ export interface CacheOverrides {
21
+ /** Override max-age (seconds) */
22
+ maxAge?: number;
23
+ /** Override stale-while-revalidate (seconds) */
24
+ staleWhileRevalidate?: number;
25
+ /** Cache tags (for Next.js revalidateTag) */
26
+ tags?: string[];
27
+ }
28
+
29
+ /**
30
+ * No caching — every request hits the server.
31
+ * Use for: Cart, Customer data, real-time inventory.
32
+ */
33
+ export function cacheNone(overrides?: CacheOverrides): CacheStrategy {
34
+ return {
35
+ maxAge: 0,
36
+ mode: 'no-store',
37
+ tags: overrides?.tags,
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Short cache — 1s max-age, 9s stale-while-revalidate (10s total).
43
+ * Use for: Product listings, collections, search results.
44
+ */
45
+ export function cacheShort(overrides?: CacheOverrides): CacheStrategy {
46
+ return {
47
+ maxAge: overrides?.maxAge ?? 1,
48
+ staleWhileRevalidate: overrides?.staleWhileRevalidate ?? 9,
49
+ mode: 'public',
50
+ tags: overrides?.tags,
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Long cache — 1h max-age, 23h stale-while-revalidate (24h total).
56
+ * Use for: Static content, shop info, rarely changing data.
57
+ */
58
+ export function cacheLong(overrides?: CacheOverrides): CacheStrategy {
59
+ return {
60
+ maxAge: overrides?.maxAge ?? 3600,
61
+ staleWhileRevalidate: overrides?.staleWhileRevalidate ?? 82800,
62
+ mode: 'public',
63
+ tags: overrides?.tags,
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Private cache — 1s max-age, no sharing between users.
69
+ * Use for: Personalized content, customer-specific data.
70
+ */
71
+ export function cachePrivate(overrides?: CacheOverrides): CacheStrategy {
72
+ return {
73
+ maxAge: overrides?.maxAge ?? 1,
74
+ staleWhileRevalidate: overrides?.staleWhileRevalidate ?? 9,
75
+ mode: 'private',
76
+ tags: overrides?.tags,
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Custom cache — full control over all options.
82
+ */
83
+ export function cacheCustom(options: CacheOptions): CacheStrategy {
84
+ return { ...options };
85
+ }
86
+
87
+ /**
88
+ * Generate Cache-Control header string from cache strategy.
89
+ */
90
+ export function generateCacheControlHeader(cache: CacheStrategy): string {
91
+ if (cache.mode === 'no-store') {
92
+ return 'no-store, no-cache, must-revalidate';
93
+ }
94
+
95
+ const parts: string[] = [cache.mode, `max-age=${cache.maxAge}`];
96
+
97
+ if (cache.staleWhileRevalidate !== undefined) {
98
+ parts.push(`stale-while-revalidate=${cache.staleWhileRevalidate}`);
99
+ }
100
+
101
+ return parts.join(', ');
102
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * CartClient — plain async API for cart operations (no React, no React Query).
3
+ *
4
+ * Wraps StorefrontClient.mutate/query with typed operations.
5
+ * Auto-throws on userErrors via assertNoUserErrors.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const cartClient = new CartClient(storefrontClient);
10
+ *
11
+ * const cart = await cartClient.create();
12
+ * const updated = await cartClient.addItem(cart.id, [
13
+ * { merchandiseId: 'variant-123', quantity: 1 }
14
+ * ]);
15
+ * ```
16
+ */
17
+
18
+ import type { StorefrontClient } from '../client/types';
19
+ import type {
20
+ Cart,
21
+ CartCreateInput,
22
+ CartLineInput,
23
+ CartLineUpdateInput,
24
+ CartBuyerIdentityInput,
25
+ } from './types';
26
+ import { assertNoUserErrors } from '../helpers/assert-no-user-errors';
27
+ import {
28
+ CART_QUERY,
29
+ CART_CREATE,
30
+ CART_LINES_ADD,
31
+ CART_LINES_UPDATE,
32
+ CART_LINES_REMOVE,
33
+ CART_DISCOUNT_CODES_UPDATE,
34
+ CART_NOTE_UPDATE,
35
+ CART_BUYER_IDENTITY_UPDATE,
36
+ } from '../operations/cart';
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Response types (internal — match the GraphQL mutation shapes)
40
+ // ---------------------------------------------------------------------------
41
+
42
+ interface CartMutationResult {
43
+ cart: Cart | null;
44
+ userErrors: Array<{ message: string; field?: string[]; code?: string }>;
45
+ }
46
+
47
+ interface CartQueryResult {
48
+ cart: Cart | null;
49
+ }
50
+
51
+ export class CartClient {
52
+ constructor(private readonly client: StorefrontClient) {}
53
+
54
+ /**
55
+ * Fetch existing cart by ID.
56
+ * Returns null if cart doesn't exist or has expired.
57
+ */
58
+ async get(cartId: string): Promise<Cart | null> {
59
+ const data = await this.client.query<CartQueryResult>(
60
+ CART_QUERY,
61
+ { id: cartId },
62
+ );
63
+ return data.cart;
64
+ }
65
+
66
+ /**
67
+ * Create a new cart, optionally with initial lines.
68
+ */
69
+ async create(input?: CartCreateInput): Promise<Cart> {
70
+ const data = await this.client.mutate<{ cartCreate: CartMutationResult }>(
71
+ CART_CREATE,
72
+ { input: input ?? {} },
73
+ );
74
+ assertNoUserErrors(data.cartCreate);
75
+ return data.cartCreate.cart!;
76
+ }
77
+
78
+ /**
79
+ * Add line items to an existing cart.
80
+ */
81
+ async addItems(cartId: string, lines: CartLineInput[]): Promise<Cart> {
82
+ const data = await this.client.mutate<{ cartLinesAdd: CartMutationResult }>(
83
+ CART_LINES_ADD,
84
+ { cartId, lines },
85
+ );
86
+ assertNoUserErrors(data.cartLinesAdd);
87
+ return data.cartLinesAdd.cart!;
88
+ }
89
+
90
+ /**
91
+ * Update line items (quantity, attributes).
92
+ */
93
+ async updateItems(cartId: string, lines: CartLineUpdateInput[]): Promise<Cart> {
94
+ const data = await this.client.mutate<{ cartLinesUpdate: CartMutationResult }>(
95
+ CART_LINES_UPDATE,
96
+ { cartId, lines },
97
+ );
98
+ assertNoUserErrors(data.cartLinesUpdate);
99
+ return data.cartLinesUpdate.cart!;
100
+ }
101
+
102
+ /**
103
+ * Remove line items by their line IDs.
104
+ */
105
+ async removeItems(cartId: string, lineIds: string[]): Promise<Cart> {
106
+ const data = await this.client.mutate<{ cartLinesRemove: CartMutationResult }>(
107
+ CART_LINES_REMOVE,
108
+ { cartId, lineIds },
109
+ );
110
+ assertNoUserErrors(data.cartLinesRemove);
111
+ return data.cartLinesRemove.cart!;
112
+ }
113
+
114
+ /**
115
+ * Update discount codes (replaces all existing codes).
116
+ * Pass empty array to clear discounts.
117
+ */
118
+ async updateDiscountCodes(cartId: string, discountCodes: string[]): Promise<Cart> {
119
+ const data = await this.client.mutate<{ cartDiscountCodesUpdate: CartMutationResult }>(
120
+ CART_DISCOUNT_CODES_UPDATE,
121
+ { cartId, discountCodes },
122
+ );
123
+ assertNoUserErrors(data.cartDiscountCodesUpdate);
124
+ return data.cartDiscountCodesUpdate.cart!;
125
+ }
126
+
127
+ /**
128
+ * Update cart note / gift message.
129
+ */
130
+ async updateNote(cartId: string, note: string): Promise<Cart> {
131
+ const data = await this.client.mutate<{ cartNoteUpdate: CartMutationResult }>(
132
+ CART_NOTE_UPDATE,
133
+ { cartId, note },
134
+ );
135
+ assertNoUserErrors(data.cartNoteUpdate);
136
+ return data.cartNoteUpdate.cart!;
137
+ }
138
+
139
+ /**
140
+ * Update buyer identity (email, phone, country, customer link).
141
+ */
142
+ async updateBuyerIdentity(cartId: string, buyerIdentity: CartBuyerIdentityInput): Promise<Cart> {
143
+ const data = await this.client.mutate<{ cartBuyerIdentityUpdate: CartMutationResult }>(
144
+ CART_BUYER_IDENTITY_UPDATE,
145
+ { cartId, buyerIdentity },
146
+ );
147
+ assertNoUserErrors(data.cartBuyerIdentityUpdate);
148
+ return data.cartBuyerIdentityUpdate.cart!;
149
+ }
150
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Cart types — manual (no codegen).
3
+ *
4
+ * These match the backend storefront-graphql Cart type.
5
+ */
6
+
7
+ export interface Money {
8
+ amount: string;
9
+ currencyCode: string;
10
+ }
11
+
12
+ export interface CartCost {
13
+ totalAmount: Money;
14
+ subtotalAmount: Money;
15
+ totalTaxAmount: Money | null;
16
+ totalDutyAmount: Money | null;
17
+ }
18
+
19
+ export interface CartLineCost {
20
+ totalAmount: Money;
21
+ amountPerQuantity: Money;
22
+ compareAtAmountPerQuantity: Money | null;
23
+ }
24
+
25
+ export interface CartLineMerchandise {
26
+ id: string;
27
+ title: string;
28
+ sku: string | null;
29
+ image: { url: string; altText: string | null } | null;
30
+ price: Money;
31
+ compareAtPrice: Money | null;
32
+ }
33
+
34
+ export interface CartLine {
35
+ id: string;
36
+ quantity: number;
37
+ merchandise: CartLineMerchandise;
38
+ cost: CartLineCost;
39
+ attributes: Array<{ key: string; value: string }>;
40
+ productId: string;
41
+ productTitle: string;
42
+ productHandle: string;
43
+ }
44
+
45
+ export interface CartDiscountCode {
46
+ code: string;
47
+ applicable: boolean;
48
+ }
49
+
50
+ export interface CartDiscountAllocation {
51
+ discountedAmount: Money;
52
+ }
53
+
54
+ export interface CartBuyerIdentity {
55
+ email: string | null;
56
+ phone: string | null;
57
+ countryCode: string | null;
58
+ }
59
+
60
+ export interface Cart {
61
+ id: string;
62
+ checkoutUrl: string | null;
63
+ totalQuantity: number;
64
+ note: string | null;
65
+ createdAt: string;
66
+ updatedAt: string;
67
+ cost: CartCost;
68
+ lines: { edges: Array<{ node: CartLine }> };
69
+ buyerIdentity: CartBuyerIdentity | null;
70
+ discountCodes: CartDiscountCode[];
71
+ discountAllocations: CartDiscountAllocation[];
72
+ attributes: Array<{ key: string; value: string }>;
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Input types
77
+ // ---------------------------------------------------------------------------
78
+
79
+ export interface CartLineInput {
80
+ merchandiseId: string;
81
+ quantity?: number;
82
+ attributes?: Array<{ key: string; value: string }>;
83
+ }
84
+
85
+ export interface CartLineUpdateInput {
86
+ id: string;
87
+ quantity: number;
88
+ attributes?: Array<{ key: string; value: string }>;
89
+ }
90
+
91
+ export interface CartCreateInput {
92
+ lines?: CartLineInput[];
93
+ buyerIdentity?: CartBuyerIdentityInput;
94
+ discountCodes?: string[];
95
+ note?: string;
96
+ attributes?: Array<{ key: string; value: string }>;
97
+ }
98
+
99
+ export interface CartBuyerIdentityInput {
100
+ email?: string;
101
+ phone?: string;
102
+ countryCode?: string;
103
+ customerId?: string;
104
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Compose middleware chain using reduceRight (Hydrogen pattern).
3
+ *
4
+ * Middleware execute in order: first registered runs first.
5
+ * Each can modify request, modify response, retry, or short-circuit.
6
+ */
7
+
8
+ import type { Middleware, ExecuteFn } from './types';
9
+
10
+ export function compose(middlewares: Middleware[], execute: ExecuteFn): ExecuteFn {
11
+ return middlewares.reduceRight<ExecuteFn>(
12
+ (next, mw) => (req) => mw(req, next),
13
+ execute,
14
+ );
15
+ }