@commercengine/storefront-sdk 0.8.2 → 0.9.1

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.
package/dist/index.js CHANGED
@@ -1,648 +1,825 @@
1
1
  import createClient from "openapi-fetch";
2
2
  import { decodeJwt } from "jose";
3
3
 
4
- //#region src/lib/jwt-utils.ts
4
+ //#region ../sdk-core/dist/index.js
5
5
  /**
6
- * Decode and extract user information from a JWT token
7
- *
8
- * @param token - The JWT token to decode
9
- * @returns User information or null if token is invalid
6
+ * Response utilities for debugging and working with Response objects
10
7
  */
11
- function extractUserInfoFromToken(token) {
12
- try {
13
- const payload = decodeJwt(token);
8
+ var ResponseUtils = class {
9
+ /**
10
+ * Get response headers as a plain object
11
+ */
12
+ static getHeaders(response) {
13
+ return Object.fromEntries(response.headers.entries());
14
+ }
15
+ /**
16
+ * Get specific header value
17
+ */
18
+ static getHeader(response, name) {
19
+ return response.headers.get(name);
20
+ }
21
+ /**
22
+ * Check if response was successful
23
+ */
24
+ static isSuccess(response) {
25
+ return response.ok;
26
+ }
27
+ /**
28
+ * Get response metadata
29
+ */
30
+ static getMetadata(response) {
14
31
  return {
15
- id: payload.ulid,
16
- email: payload.email,
17
- phone: payload.phone,
18
- username: payload.username,
19
- firstName: payload.first_name,
20
- lastName: payload.last_name,
21
- storeId: payload.store_id,
22
- isLoggedIn: payload.is_logged_in,
23
- isAnonymous: !payload.is_logged_in,
24
- customerId: payload.customer_id,
25
- customerGroupId: payload.customer_group_id,
26
- anonymousId: payload.anonymous_id,
27
- tokenExpiry: /* @__PURE__ */ new Date(payload.exp * 1e3),
28
- tokenIssuedAt: /* @__PURE__ */ new Date(payload.iat * 1e3)
32
+ status: response.status,
33
+ statusText: response.statusText,
34
+ ok: response.ok,
35
+ url: response.url,
36
+ redirected: response.redirected,
37
+ type: response.type,
38
+ headers: Object.fromEntries(response.headers.entries())
29
39
  };
30
- } catch (error) {
31
- console.warn("Failed to decode JWT token:", error);
32
- return null;
33
40
  }
34
- }
41
+ /**
42
+ * Clone and read response as text (useful for debugging)
43
+ * Note: This can only be called once per response
44
+ */
45
+ static async getText(response) {
46
+ const cloned = response.clone();
47
+ return await cloned.text();
48
+ }
49
+ /**
50
+ * Clone and read response as JSON (useful for debugging)
51
+ * Note: This can only be called once per response
52
+ */
53
+ static async getJSON(response) {
54
+ const cloned = response.clone();
55
+ return await cloned.json();
56
+ }
57
+ /**
58
+ * Format response information for debugging
59
+ */
60
+ static format(response) {
61
+ const metadata = this.getMetadata(response);
62
+ return `${metadata.status} ${metadata.statusText} - ${metadata.url}`;
63
+ }
64
+ /**
65
+ * Format response for logging purposes (enhanced version)
66
+ */
67
+ static formatResponse(response) {
68
+ return {
69
+ status: response.status,
70
+ statusText: response.statusText,
71
+ url: response.url,
72
+ ok: response.ok
73
+ };
74
+ }
75
+ };
35
76
  /**
36
- * Check if a JWT token is expired
37
- *
38
- * @param token - The JWT token to check
39
- * @param bufferSeconds - Buffer time in seconds (default: 30)
40
- * @returns True if token is expired or will expire within buffer time
77
+ * Debug logging utilities
41
78
  */
42
- function isTokenExpired(token, bufferSeconds = 30) {
79
+ var DebugLogger = class {
80
+ logger;
81
+ responseTextCache = /* @__PURE__ */ new Map();
82
+ constructor(logger) {
83
+ this.logger = logger || ((level, message, data) => {
84
+ console.log(`[${level.toUpperCase()}]`, message);
85
+ if (data) console.log(data);
86
+ });
87
+ }
88
+ /**
89
+ * Log debug information about API request
90
+ */
91
+ logRequest(request, requestBody) {
92
+ this.logger("info", "API Request Debug Info", {
93
+ method: request.method,
94
+ url: request.url,
95
+ headers: Object.fromEntries(request.headers.entries()),
96
+ body: requestBody,
97
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
98
+ });
99
+ }
100
+ /**
101
+ * Log debug information about API response
102
+ */
103
+ async logResponse(response, responseBody) {
104
+ if (responseBody && typeof responseBody === "string") this.responseTextCache.set(response.url, responseBody);
105
+ this.logger("info", "API Response Debug Info", {
106
+ url: response.url,
107
+ status: response.status,
108
+ statusText: response.statusText,
109
+ ok: response.ok,
110
+ headers: Object.fromEntries(response.headers.entries()),
111
+ redirected: response.redirected,
112
+ type: response.type,
113
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
114
+ });
115
+ if (responseBody) this.logger("info", "API Response Data", {
116
+ data: responseBody,
117
+ contentType: response.headers.get("content-type"),
118
+ contentLength: response.headers.get("content-length")
119
+ });
120
+ }
121
+ /**
122
+ * Log error information
123
+ */
124
+ logError(message, error) {
125
+ this.logger("error", message, error);
126
+ }
127
+ /**
128
+ * Get cached response text for a URL (if available)
129
+ */
130
+ getCachedResponseText(url) {
131
+ return this.responseTextCache.get(url) || null;
132
+ }
133
+ /**
134
+ * Clear cached response texts
135
+ */
136
+ clearCache() {
137
+ this.responseTextCache.clear();
138
+ }
139
+ info(message, data) {
140
+ this.logger("info", message, data);
141
+ }
142
+ warn(message, data) {
143
+ this.logger("warn", message, data);
144
+ }
145
+ error(message, data) {
146
+ this.logger("error", message, data);
147
+ }
148
+ };
149
+ /**
150
+ * Extract request body for logging
151
+ */
152
+ async function extractRequestBody(request) {
153
+ if (request.method === "GET" || request.method === "HEAD") return null;
43
154
  try {
44
- const payload = decodeJwt(token);
45
- if (!payload.exp) return true;
46
- const currentTime = Math.floor(Date.now() / 1e3);
47
- const expiryTime = payload.exp;
48
- return currentTime >= expiryTime - bufferSeconds;
155
+ const clonedRequest = request.clone();
156
+ const contentType = request.headers.get("content-type")?.toLowerCase();
157
+ if (contentType?.startsWith("application/json")) return await clonedRequest.json();
158
+ else if (contentType?.startsWith("multipart/form-data")) return "[FormData - cannot display]";
159
+ else if (contentType?.startsWith("text/")) return await clonedRequest.text();
160
+ return "[Request body - unknown format]";
49
161
  } catch (error) {
50
- console.warn("Failed to decode JWT token:", error);
51
- return true;
162
+ return "[Request body unavailable]";
52
163
  }
53
164
  }
54
165
  /**
55
- * Get the user ID from a JWT token
56
- *
57
- * @param token - The JWT token
58
- * @returns User ID (ulid) or null if token is invalid
166
+ * Create debug middleware for openapi-fetch (internal use)
167
+ * Enhanced version that combines original functionality with duration tracking
59
168
  */
60
- function getUserIdFromToken(token) {
61
- const userInfo = extractUserInfoFromToken(token);
62
- return userInfo?.id || null;
169
+ function createDebugMiddleware(logger) {
170
+ const debugLogger = new DebugLogger(logger);
171
+ return {
172
+ async onRequest({ request }) {
173
+ request.__debugStartTime = Date.now();
174
+ const requestBody = await extractRequestBody(request);
175
+ debugLogger.logRequest(request, requestBody);
176
+ return request;
177
+ },
178
+ async onResponse({ request, response }) {
179
+ const startTime = request.__debugStartTime;
180
+ const duration = startTime ? Date.now() - startTime : 0;
181
+ const cloned = response.clone();
182
+ let responseBody = null;
183
+ try {
184
+ const contentType = response.headers.get("content-type")?.toLowerCase();
185
+ if (contentType?.startsWith("application/json")) responseBody = await cloned.json();
186
+ else if (contentType?.startsWith("text/")) responseBody = await cloned.text();
187
+ } catch (error) {}
188
+ await debugLogger.logResponse(response, responseBody);
189
+ if (duration > 0) debugLogger.info(`Request completed in ${duration}ms`, {
190
+ url: request.url,
191
+ method: request.method
192
+ });
193
+ return response;
194
+ },
195
+ async onError({ error, request }) {
196
+ debugLogger.logError("API Request Failed", {
197
+ error: {
198
+ name: error instanceof Error ? error.name : "Unknown",
199
+ message: error instanceof Error ? error.message : String(error),
200
+ stack: error instanceof Error ? error.stack : void 0
201
+ },
202
+ request: {
203
+ method: request.method,
204
+ url: request.url,
205
+ headers: Object.fromEntries(request.headers.entries())
206
+ },
207
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
208
+ });
209
+ throw error;
210
+ }
211
+ };
63
212
  }
64
213
  /**
65
- * Check if user is logged in based on JWT token
66
- *
67
- * @param token - The JWT token
68
- * @returns True if user is logged in, false otherwise
214
+ * Timeout middleware for Commerce Engine SDKs
69
215
  */
70
- function isUserLoggedIn(token) {
71
- const userInfo = extractUserInfoFromToken(token);
72
- return userInfo?.isLoggedIn || false;
73
- }
74
216
  /**
75
- * Check if user is anonymous based on JWT token
217
+ * Create timeout middleware for openapi-fetch
218
+ * Adds configurable request timeout functionality
76
219
  *
77
- * @param token - The JWT token
78
- * @returns True if user is anonymous, false otherwise
220
+ * @param timeoutMs - Timeout duration in milliseconds
221
+ * @returns Middleware object with onRequest handler
79
222
  */
80
- function isUserAnonymous(token) {
81
- const userInfo = extractUserInfoFromToken(token);
82
- return userInfo?.isAnonymous || true;
223
+ function createTimeoutMiddleware(timeoutMs) {
224
+ return { onRequest: async ({ request }) => {
225
+ const controller = new AbortController();
226
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
227
+ if (request.signal) request.signal.addEventListener("abort", () => controller.abort());
228
+ const newRequest = new Request(request, { signal: controller.signal });
229
+ controller.signal.addEventListener("abort", () => clearTimeout(timeoutId));
230
+ return newRequest;
231
+ } };
83
232
  }
84
-
85
- //#endregion
86
- //#region src/lib/auth-utils.ts
87
233
  /**
88
- * Extract pathname from URL
234
+ * Transform headers using a transformation mapping
235
+ * Headers not in the transformation map are passed through unchanged
236
+ *
237
+ * @param headers - Headers object with original names
238
+ * @param transformations - Mapping of original names to transformed names
239
+ * @returns Headers object with transformed names
89
240
  */
90
- function getPathnameFromUrl(url) {
91
- try {
92
- const urlObj = new URL(url);
93
- return urlObj.pathname;
94
- } catch {
95
- return url.split("?")[0];
241
+ function transformHeaders(headers, transformations) {
242
+ const transformed = {};
243
+ for (const [key, value] of Object.entries(headers)) if (value !== void 0) {
244
+ const headerName = transformations[key] || key;
245
+ transformed[headerName] = value;
96
246
  }
247
+ return transformed;
97
248
  }
98
249
  /**
99
- * Check if a URL path is an auth endpoint that should use API key
100
- */
101
- function isAnonymousAuthEndpoint(pathname) {
102
- return pathname.endsWith("/auth/anonymous");
103
- }
104
- /**
105
- * Check if a URL path is a login/register endpoint that returns tokens
250
+ * Merge headers with transformation support
251
+ * Transforms default headers, then merges with method headers
252
+ *
253
+ * @param defaultHeaders - Default headers from SDK configuration
254
+ * @param methodHeaders - Headers passed to the specific method call
255
+ * @param transformations - Mapping for header name transformations
256
+ * @returns Merged headers object with transformations applied
106
257
  */
107
- function isTokenReturningEndpoint(pathname) {
108
- const tokenEndpoints = [
109
- "/auth/login/password",
110
- "/auth/register/phone",
111
- "/auth/register/email",
112
- "/auth/verify-otp",
113
- "/auth/refresh-token"
114
- ];
115
- return tokenEndpoints.some((endpoint) => pathname.endsWith(endpoint));
258
+ function mergeAndTransformHeaders(defaultHeaders, methodHeaders, transformations) {
259
+ const merged = {};
260
+ if (defaultHeaders && transformations) {
261
+ const transformedDefaults = transformHeaders(defaultHeaders, transformations);
262
+ Object.assign(merged, transformedDefaults);
263
+ } else if (defaultHeaders) Object.assign(merged, defaultHeaders);
264
+ if (methodHeaders) Object.assign(merged, methodHeaders);
265
+ Object.keys(merged).forEach((key) => {
266
+ if (merged[key] === void 0) delete merged[key];
267
+ });
268
+ return merged;
116
269
  }
117
270
  /**
118
- * Check if a URL path is the logout endpoint
271
+ * Execute a request and handle the response consistently
272
+ * This provides unified error handling and response processing across all SDKs
273
+ *
274
+ * @param apiCall - Function that executes the API request
275
+ * @returns Promise with the API response in standardized format
119
276
  */
120
- function isLogoutEndpoint(pathname) {
121
- return pathname.endsWith("/auth/logout");
277
+ async function executeRequest(apiCall) {
278
+ try {
279
+ const { data, error, response } = await apiCall();
280
+ if (error) return {
281
+ data: null,
282
+ error,
283
+ response
284
+ };
285
+ if (data && data.content !== void 0) return {
286
+ data: data.content,
287
+ error: null,
288
+ response
289
+ };
290
+ return {
291
+ data,
292
+ error: null,
293
+ response
294
+ };
295
+ } catch (err) {
296
+ const mockResponse = new Response(null, {
297
+ status: 0,
298
+ statusText: "Network Error"
299
+ });
300
+ const errorResult = {
301
+ data: null,
302
+ error: {
303
+ success: false,
304
+ code: "NETWORK_ERROR",
305
+ message: "Network error occurred",
306
+ error: err
307
+ },
308
+ response: mockResponse
309
+ };
310
+ return errorResult;
311
+ }
122
312
  }
123
-
124
- //#endregion
125
- //#region src/lib/middleware.ts
126
313
  /**
127
- * Simple in-memory token storage implementation
314
+ * Generic base API client that all Commerce Engine SDKs can extend
315
+ * Handles common functionality like middleware setup, request execution, and header management
316
+ * Does NOT include token management - that's SDK-specific
317
+ *
318
+ * @template TPaths - OpenAPI paths type
319
+ * @template THeaders - Supported default headers type
128
320
  */
129
- var MemoryTokenStorage = class {
130
- accessToken = null;
131
- refreshToken = null;
132
- async getAccessToken() {
133
- return this.accessToken;
321
+ var BaseAPIClient = class {
322
+ client;
323
+ config;
324
+ baseUrl;
325
+ headerTransformations;
326
+ /**
327
+ * Create a new BaseAPIClient
328
+ *
329
+ * @param config - Configuration for the API client
330
+ * @param baseUrl - The base URL for the API (must be provided by subclass)
331
+ * @param headerTransformations - Header name transformations for this SDK
332
+ */
333
+ constructor(config, baseUrl, headerTransformations = {}) {
334
+ this.config = { ...config };
335
+ this.headerTransformations = headerTransformations;
336
+ this.baseUrl = baseUrl;
337
+ this.client = createClient({ baseUrl: this.baseUrl });
338
+ this.setupMiddleware();
134
339
  }
135
- async setAccessToken(token) {
136
- this.accessToken = token;
340
+ /**
341
+ * Set up all middleware for the client
342
+ */
343
+ setupMiddleware() {
344
+ if (this.config.timeout) {
345
+ const timeoutMiddleware = createTimeoutMiddleware(this.config.timeout);
346
+ this.client.use(timeoutMiddleware);
347
+ }
348
+ if (this.config.debug) {
349
+ const debugMiddleware = createDebugMiddleware(this.config.logger);
350
+ this.client.use(debugMiddleware);
351
+ }
137
352
  }
138
- async getRefreshToken() {
139
- return this.refreshToken;
353
+ /**
354
+ * Get the base URL of the API
355
+ *
356
+ * @returns The base URL of the API
357
+ */
358
+ getBaseUrl() {
359
+ return this.baseUrl;
140
360
  }
141
- async setRefreshToken(token) {
142
- this.refreshToken = token;
361
+ /**
362
+ * Execute a request and handle the response consistently
363
+ * This provides unified error handling and response processing
364
+ *
365
+ * @param apiCall - Function that executes the API request
366
+ * @returns Promise with the API response in standardized format
367
+ */
368
+ async executeRequest(apiCall) {
369
+ return executeRequest(apiCall);
143
370
  }
144
- async clearTokens() {
145
- this.accessToken = null;
146
- this.refreshToken = null;
371
+ /**
372
+ * Merge default headers with method-level headers
373
+ * Method-level headers take precedence over default headers
374
+ * Automatically applies SDK-specific header transformations
375
+ *
376
+ * @param methodHeaders - Headers passed to the specific method call
377
+ * @returns Merged headers object with proper HTTP header names
378
+ */
379
+ mergeHeaders(methodHeaders) {
380
+ return mergeAndTransformHeaders(this.config.defaultHeaders, methodHeaders, this.headerTransformations);
381
+ }
382
+ /**
383
+ * Set default headers for the client
384
+ *
385
+ * @param headers - Default headers to set
386
+ */
387
+ setDefaultHeaders(headers) {
388
+ this.config.defaultHeaders = headers;
389
+ }
390
+ /**
391
+ * Get current default headers
392
+ *
393
+ * @returns Current default headers
394
+ */
395
+ getDefaultHeaders() {
396
+ return this.config.defaultHeaders;
147
397
  }
148
398
  };
149
399
  /**
150
- * Browser localStorage token storage implementation
400
+ * Generic URL utility functions for any SDK
151
401
  */
152
- var BrowserTokenStorage = class {
153
- accessTokenKey;
154
- refreshTokenKey;
155
- constructor(prefix = "storefront_") {
156
- this.accessTokenKey = `${prefix}access_token`;
157
- this.refreshTokenKey = `${prefix}refresh_token`;
158
- }
159
- async getAccessToken() {
160
- if (typeof localStorage === "undefined") return null;
161
- return localStorage.getItem(this.accessTokenKey);
162
- }
163
- async setAccessToken(token) {
164
- if (typeof localStorage !== "undefined") localStorage.setItem(this.accessTokenKey, token);
165
- }
166
- async getRefreshToken() {
167
- if (typeof localStorage === "undefined") return null;
168
- return localStorage.getItem(this.refreshTokenKey);
169
- }
170
- async setRefreshToken(token) {
171
- if (typeof localStorage !== "undefined") localStorage.setItem(this.refreshTokenKey, token);
172
- }
173
- async clearTokens() {
174
- if (typeof localStorage !== "undefined") {
175
- localStorage.removeItem(this.accessTokenKey);
176
- localStorage.removeItem(this.refreshTokenKey);
177
- }
402
+ /**
403
+ * Extract pathname from URL
404
+ * Useful for middleware that needs to inspect request paths
405
+ */
406
+ function getPathnameFromUrl(url) {
407
+ try {
408
+ const urlObj = new URL(url);
409
+ return urlObj.pathname;
410
+ } catch {
411
+ return url.split("?")[0] || url;
178
412
  }
179
- };
413
+ }
414
+
415
+ //#endregion
416
+ //#region src/lib/jwt-utils.ts
180
417
  /**
181
- * Cookie-based token storage implementation
418
+ * Decode and extract user information from a JWT token
419
+ *
420
+ * @param token - The JWT token to decode
421
+ * @returns User information or null if token is invalid
182
422
  */
183
- var CookieTokenStorage = class {
184
- accessTokenKey;
185
- refreshTokenKey;
186
- options;
187
- constructor(options = {}) {
188
- const prefix = options.prefix || "storefront_";
189
- this.accessTokenKey = `${prefix}access_token`;
190
- this.refreshTokenKey = `${prefix}refresh_token`;
191
- this.options = {
192
- maxAge: options.maxAge || 10080 * 60,
193
- path: options.path || "/",
194
- domain: options.domain,
195
- secure: options.secure ?? (typeof window !== "undefined" && window.location?.protocol === "https:"),
196
- sameSite: options.sameSite || "Lax",
197
- httpOnly: false
423
+ function extractUserInfoFromToken(token) {
424
+ try {
425
+ const payload = decodeJwt(token);
426
+ return {
427
+ id: payload.ulid,
428
+ email: payload.email,
429
+ phone: payload.phone,
430
+ username: payload.username,
431
+ firstName: payload.first_name,
432
+ lastName: payload.last_name,
433
+ storeId: payload.store_id,
434
+ isLoggedIn: payload.is_logged_in,
435
+ isAnonymous: !payload.is_logged_in,
436
+ customerId: payload.customer_id,
437
+ customerGroupId: payload.customer_group_id,
438
+ anonymousId: payload.anonymous_id,
439
+ tokenExpiry: /* @__PURE__ */ new Date(payload.exp * 1e3),
440
+ tokenIssuedAt: /* @__PURE__ */ new Date(payload.iat * 1e3)
198
441
  };
199
- }
200
- async getAccessToken() {
201
- return this.getCookie(this.accessTokenKey);
202
- }
203
- async setAccessToken(token) {
204
- this.setCookie(this.accessTokenKey, token);
205
- }
206
- async getRefreshToken() {
207
- return this.getCookie(this.refreshTokenKey);
208
- }
209
- async setRefreshToken(token) {
210
- this.setCookie(this.refreshTokenKey, token);
211
- }
212
- async clearTokens() {
213
- this.deleteCookie(this.accessTokenKey);
214
- this.deleteCookie(this.refreshTokenKey);
215
- }
216
- getCookie(name) {
217
- if (typeof document === "undefined") return null;
218
- const value = `; ${document.cookie}`;
219
- const parts = value.split(`; ${name}=`);
220
- if (parts.length === 2) {
221
- const cookieValue = parts.pop()?.split(";").shift();
222
- return cookieValue ? decodeURIComponent(cookieValue) : null;
223
- }
442
+ } catch (error) {
443
+ console.warn("Failed to decode JWT token:", error);
224
444
  return null;
225
445
  }
226
- setCookie(name, value) {
227
- if (typeof document === "undefined") return;
228
- const encodedValue = encodeURIComponent(value);
229
- let cookieString = `${name}=${encodedValue}`;
230
- if (this.options.maxAge) cookieString += `; Max-Age=${this.options.maxAge}`;
231
- if (this.options.path) cookieString += `; Path=${this.options.path}`;
232
- if (this.options.domain) cookieString += `; Domain=${this.options.domain}`;
233
- if (this.options.secure) cookieString += `; Secure`;
234
- if (this.options.sameSite) cookieString += `; SameSite=${this.options.sameSite}`;
235
- document.cookie = cookieString;
236
- }
237
- deleteCookie(name) {
238
- if (typeof document === "undefined") return;
239
- let cookieString = `${name}=; Max-Age=0`;
240
- if (this.options.path) cookieString += `; Path=${this.options.path}`;
241
- if (this.options.domain) cookieString += `; Domain=${this.options.domain}`;
242
- document.cookie = cookieString;
446
+ }
447
+ /**
448
+ * Check if a JWT token is expired
449
+ *
450
+ * @param token - The JWT token to check
451
+ * @param bufferSeconds - Buffer time in seconds (default: 30)
452
+ * @returns True if token is expired or will expire within buffer time
453
+ */
454
+ function isTokenExpired(token, bufferSeconds = 30) {
455
+ try {
456
+ const payload = decodeJwt(token);
457
+ if (!payload.exp) return true;
458
+ const currentTime = Math.floor(Date.now() / 1e3);
459
+ const expiryTime = payload.exp;
460
+ return currentTime >= expiryTime - bufferSeconds;
461
+ } catch (error) {
462
+ console.warn("Failed to decode JWT token:", error);
463
+ return true;
243
464
  }
244
- };
465
+ }
245
466
  /**
246
- * Create authentication middleware for openapi-fetch
467
+ * Get the user ID from a JWT token
468
+ *
469
+ * @param token - The JWT token
470
+ * @returns User ID (ulid) or null if token is invalid
247
471
  */
248
- function createAuthMiddleware(config) {
249
- let isRefreshing = false;
250
- let refreshPromise = null;
251
- let hasAssessedTokens = false;
252
- const assessTokenStateOnce = async () => {
253
- if (hasAssessedTokens) return;
254
- hasAssessedTokens = true;
255
- try {
256
- const accessToken = await config.tokenStorage.getAccessToken();
257
- const refreshToken = await config.tokenStorage.getRefreshToken();
258
- if (!accessToken && refreshToken) {
259
- await config.tokenStorage.clearTokens();
260
- console.info("Cleaned up orphaned refresh token");
261
- }
262
- } catch (error) {
263
- console.warn("Token state assessment failed:", error);
264
- }
265
- };
266
- const refreshTokens = async () => {
267
- if (isRefreshing && refreshPromise) return refreshPromise;
268
- isRefreshing = true;
269
- refreshPromise = (async () => {
270
- try {
271
- const refreshToken = await config.tokenStorage.getRefreshToken();
272
- let newTokens;
273
- if (refreshToken && !isTokenExpired(refreshToken)) if (config.refreshTokenFn) newTokens = await config.refreshTokenFn(refreshToken);
274
- else {
275
- const response = await fetch(`${config.baseUrl}/auth/refresh-token`, {
276
- method: "POST",
277
- headers: { "Content-Type": "application/json" },
278
- body: JSON.stringify({ refresh_token: refreshToken })
279
- });
280
- if (!response.ok) throw new Error(`Token refresh failed: ${response.status}`);
281
- const data = await response.json();
282
- newTokens = data.content;
283
- }
284
- else {
285
- const currentAccessToken = await config.tokenStorage.getAccessToken();
286
- if (!currentAccessToken) throw new Error("No tokens available for refresh");
287
- const reason = refreshToken ? "refresh token expired" : "no refresh token available";
288
- const response = await fetch(`${config.baseUrl}/auth/anonymous`, {
289
- method: "POST",
290
- headers: {
291
- "Content-Type": "application/json",
292
- ...config.apiKey && { "X-Api-Key": config.apiKey },
293
- Authorization: `Bearer ${currentAccessToken}`
294
- }
295
- });
296
- if (!response.ok) throw new Error(`Anonymous token fallback failed: ${response.status}`);
297
- const data = await response.json();
298
- newTokens = data.content;
299
- console.info(`Token refreshed via anonymous fallback (${reason}) - user may need to re-authenticate for privileged operations`);
300
- }
301
- await config.tokenStorage.setAccessToken(newTokens.access_token);
302
- await config.tokenStorage.setRefreshToken(newTokens.refresh_token);
303
- config.onTokensUpdated?.(newTokens.access_token, newTokens.refresh_token);
304
- } catch (error) {
305
- console.error("Token refresh failed:", error);
306
- await config.tokenStorage.clearTokens();
307
- config.onTokensCleared?.();
308
- throw error;
309
- } finally {
310
- isRefreshing = false;
311
- refreshPromise = null;
312
- }
313
- })();
314
- return refreshPromise;
315
- };
316
- return {
317
- async onRequest({ request }) {
318
- const pathname = getPathnameFromUrl(request.url);
319
- await assessTokenStateOnce();
320
- if (isAnonymousAuthEndpoint(pathname)) {
321
- if (config.apiKey) request.headers.set("X-Api-Key", config.apiKey);
322
- const existingToken = await config.tokenStorage.getAccessToken();
323
- if (existingToken && !isTokenExpired(existingToken) && isUserLoggedIn(existingToken)) return new Response(JSON.stringify({
324
- message: "Cannot create anonymous session while authenticated",
325
- success: false,
326
- code: "USER_ALREADY_AUTHENTICATED"
327
- }), {
328
- status: 400,
329
- headers: { "Content-Type": "application/json" }
330
- });
331
- if (existingToken) request.headers.set("Authorization", `Bearer ${existingToken}`);
332
- return request;
333
- }
334
- let accessToken = await config.tokenStorage.getAccessToken();
335
- if (!accessToken) try {
336
- const response = await fetch(`${config.baseUrl}/auth/anonymous`, {
337
- method: "POST",
338
- headers: {
339
- "Content-Type": "application/json",
340
- ...config.apiKey && { "X-Api-Key": config.apiKey }
341
- }
342
- });
343
- if (response.ok) {
344
- const data = await response.json();
345
- const tokens = data.content;
346
- if (tokens?.access_token && tokens?.refresh_token) {
347
- await config.tokenStorage.setAccessToken(tokens.access_token);
348
- await config.tokenStorage.setRefreshToken(tokens.refresh_token);
349
- accessToken = tokens.access_token;
350
- config.onTokensUpdated?.(tokens.access_token, tokens.refresh_token);
351
- console.info("Automatically created anonymous session for first API request");
352
- }
353
- }
354
- } catch (error) {
355
- console.warn("Failed to automatically create anonymous tokens:", error);
356
- }
357
- if (accessToken && isTokenExpired(accessToken)) try {
358
- await refreshTokens();
359
- accessToken = await config.tokenStorage.getAccessToken();
360
- } catch (error) {}
361
- if (accessToken) request.headers.set("Authorization", `Bearer ${accessToken}`);
362
- return request;
363
- },
364
- async onResponse({ request, response }) {
365
- const pathname = getPathnameFromUrl(request.url);
366
- if (response.ok) {
367
- if (isTokenReturningEndpoint(pathname) || isAnonymousAuthEndpoint(pathname)) try {
368
- const data = await response.clone().json();
369
- const content = data.content;
370
- if (content?.access_token && content?.refresh_token) {
371
- await config.tokenStorage.setAccessToken(content.access_token);
372
- await config.tokenStorage.setRefreshToken(content.refresh_token);
373
- config.onTokensUpdated?.(content.access_token, content.refresh_token);
374
- }
375
- } catch (error) {
376
- console.warn("Failed to extract tokens from response:", error);
377
- }
378
- else if (isLogoutEndpoint(pathname)) {
379
- await config.tokenStorage.clearTokens();
380
- config.onTokensCleared?.();
381
- }
382
- }
383
- if (response.status === 401 && !isAnonymousAuthEndpoint(pathname)) {
384
- const currentToken = await config.tokenStorage.getAccessToken();
385
- if (currentToken && isTokenExpired(currentToken, 0)) try {
386
- await refreshTokens();
387
- const newToken = await config.tokenStorage.getAccessToken();
388
- if (newToken) {
389
- const retryRequest = request.clone();
390
- retryRequest.headers.set("Authorization", `Bearer ${newToken}`);
391
- return fetch(retryRequest);
392
- }
393
- } catch (error) {
394
- console.warn("Token refresh failed on 401 response:", error);
395
- }
396
- }
397
- return response;
398
- }
399
- };
472
+ function getUserIdFromToken(token) {
473
+ const userInfo = extractUserInfoFromToken(token);
474
+ return userInfo?.id || null;
475
+ }
476
+ /**
477
+ * Check if user is logged in based on JWT token
478
+ *
479
+ * @param token - The JWT token
480
+ * @returns True if user is logged in, false otherwise
481
+ */
482
+ function isUserLoggedIn(token) {
483
+ const userInfo = extractUserInfoFromToken(token);
484
+ return userInfo?.isLoggedIn || false;
485
+ }
486
+ /**
487
+ * Check if user is anonymous based on JWT token
488
+ *
489
+ * @param token - The JWT token
490
+ * @returns True if user is anonymous, false otherwise
491
+ */
492
+ function isUserAnonymous(token) {
493
+ const userInfo = extractUserInfoFromToken(token);
494
+ return userInfo?.isAnonymous || true;
495
+ }
496
+
497
+ //#endregion
498
+ //#region src/lib/auth-utils.ts
499
+ /**
500
+ * Check if a URL path is an auth endpoint that should use API key
501
+ */
502
+ function isAnonymousAuthEndpoint(pathname) {
503
+ return pathname.endsWith("/auth/anonymous");
504
+ }
505
+ /**
506
+ * Check if a URL path is a login/register endpoint that returns tokens
507
+ */
508
+ function isTokenReturningEndpoint(pathname) {
509
+ const tokenEndpoints = [
510
+ "/auth/login/password",
511
+ "/auth/register/phone",
512
+ "/auth/register/email",
513
+ "/auth/verify-otp",
514
+ "/auth/refresh-token"
515
+ ];
516
+ return tokenEndpoints.some((endpoint) => pathname.endsWith(endpoint));
400
517
  }
401
518
  /**
402
- * Helper function to create auth middleware with sensible defaults
519
+ * Check if a URL path is the logout endpoint
403
520
  */
404
- function createDefaultAuthMiddleware(options) {
405
- const tokenStorage = options.tokenStorage || (typeof localStorage !== "undefined" ? new BrowserTokenStorage() : new MemoryTokenStorage());
406
- return createAuthMiddleware({
407
- tokenStorage,
408
- apiKey: options.apiKey,
409
- baseUrl: options.baseUrl,
410
- onTokensUpdated: options.onTokensUpdated,
411
- onTokensCleared: options.onTokensCleared
412
- });
521
+ function isLogoutEndpoint(pathname) {
522
+ return pathname.endsWith("/auth/logout");
413
523
  }
414
524
 
415
525
  //#endregion
416
- //#region src/lib/logger-utils.ts
526
+ //#region src/lib/middleware.ts
417
527
  /**
418
- * Response utilities for debugging and working with Response objects
528
+ * Simple in-memory token storage implementation
419
529
  */
420
- var ResponseUtils = class {
421
- /**
422
- * Get response headers as a plain object
423
- */
424
- static getHeaders(response) {
425
- return Object.fromEntries(response.headers.entries());
426
- }
427
- /**
428
- * Get specific header value
429
- */
430
- static getHeader(response, name) {
431
- return response.headers.get(name);
432
- }
433
- /**
434
- * Check if response was successful
435
- */
436
- static isSuccess(response) {
437
- return response.ok;
530
+ var MemoryTokenStorage = class {
531
+ accessToken = null;
532
+ refreshToken = null;
533
+ async getAccessToken() {
534
+ return this.accessToken;
438
535
  }
439
- /**
440
- * Get response metadata
441
- */
442
- static getMetadata(response) {
443
- return {
444
- status: response.status,
445
- statusText: response.statusText,
446
- ok: response.ok,
447
- url: response.url,
448
- redirected: response.redirected,
449
- type: response.type,
450
- headers: Object.fromEntries(response.headers.entries())
451
- };
536
+ async setAccessToken(token) {
537
+ this.accessToken = token;
452
538
  }
453
- /**
454
- * Clone and read response as text (useful for debugging)
455
- * Note: This can only be called once per response
456
- */
457
- static async getText(response) {
458
- const cloned = response.clone();
459
- return await cloned.text();
539
+ async getRefreshToken() {
540
+ return this.refreshToken;
460
541
  }
461
- /**
462
- * Clone and read response as JSON (useful for debugging)
463
- * Note: This can only be called once per response
464
- */
465
- static async getJSON(response) {
466
- const cloned = response.clone();
467
- return await cloned.json();
542
+ async setRefreshToken(token) {
543
+ this.refreshToken = token;
468
544
  }
469
- /**
470
- * Format response information for debugging
471
- */
472
- static format(response) {
473
- const metadata = this.getMetadata(response);
474
- return `${metadata.status} ${metadata.statusText} - ${metadata.url}`;
545
+ async clearTokens() {
546
+ this.accessToken = null;
547
+ this.refreshToken = null;
475
548
  }
476
549
  };
477
550
  /**
478
- * Debug logging utilities
551
+ * Browser localStorage token storage implementation
479
552
  */
480
- var DebugLogger = class {
481
- logger;
482
- responseTextCache = /* @__PURE__ */ new Map();
483
- constructor(logger) {
484
- this.logger = logger || ((level, message, data) => {
485
- console.log(`[${level.toUpperCase()}]`, message);
486
- if (data) console.log(data);
487
- });
553
+ var BrowserTokenStorage = class {
554
+ accessTokenKey;
555
+ refreshTokenKey;
556
+ constructor(prefix = "storefront_") {
557
+ this.accessTokenKey = `${prefix}access_token`;
558
+ this.refreshTokenKey = `${prefix}refresh_token`;
488
559
  }
489
- /**
490
- * Log debug information about API request
491
- */
492
- logRequest(request, requestBody) {
493
- this.logger("info", "API Request Debug Info", {
494
- method: request.method,
495
- url: request.url,
496
- headers: Object.fromEntries(request.headers.entries()),
497
- body: requestBody,
498
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
499
- });
560
+ async getAccessToken() {
561
+ if (typeof localStorage === "undefined") return null;
562
+ return localStorage.getItem(this.accessTokenKey);
500
563
  }
501
- /**
502
- * Log debug information about API response
503
- */
504
- async logResponse(response, responseBody) {
505
- if (responseBody && typeof responseBody === "string") this.responseTextCache.set(response.url, responseBody);
506
- this.logger("info", "API Response Debug Info", {
507
- url: response.url,
508
- status: response.status,
509
- statusText: response.statusText,
510
- ok: response.ok,
511
- headers: Object.fromEntries(response.headers.entries()),
512
- redirected: response.redirected,
513
- type: response.type,
514
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
515
- });
516
- if (responseBody) this.logger("info", "API Response Data", {
517
- data: responseBody,
518
- contentType: response.headers.get("content-type"),
519
- contentLength: response.headers.get("content-length")
520
- });
564
+ async setAccessToken(token) {
565
+ if (typeof localStorage !== "undefined") localStorage.setItem(this.accessTokenKey, token);
521
566
  }
522
- /**
523
- * Log error information
524
- */
525
- logError(message, error) {
526
- this.logger("error", message, error);
567
+ async getRefreshToken() {
568
+ if (typeof localStorage === "undefined") return null;
569
+ return localStorage.getItem(this.refreshTokenKey);
527
570
  }
528
- /**
529
- * Get cached response text for a URL (if available)
530
- */
531
- getCachedResponseText(url) {
532
- return this.responseTextCache.get(url) || null;
571
+ async setRefreshToken(token) {
572
+ if (typeof localStorage !== "undefined") localStorage.setItem(this.refreshTokenKey, token);
533
573
  }
534
- /**
535
- * Clear cached response texts
536
- */
537
- clearCache() {
538
- this.responseTextCache.clear();
574
+ async clearTokens() {
575
+ if (typeof localStorage !== "undefined") {
576
+ localStorage.removeItem(this.accessTokenKey);
577
+ localStorage.removeItem(this.refreshTokenKey);
578
+ }
539
579
  }
540
580
  };
541
581
  /**
542
- * Extract request body for logging
582
+ * Cookie-based token storage implementation
543
583
  */
544
- async function extractRequestBody(request) {
545
- if (request.method === "GET" || request.method === "HEAD") return null;
546
- try {
547
- const clonedRequest = request.clone();
548
- const contentType = request.headers.get("content-type")?.toLowerCase();
549
- if (contentType?.startsWith("application/json")) return await clonedRequest.json();
550
- else if (contentType?.startsWith("multipart/form-data")) return "[FormData - cannot display]";
551
- else if (contentType?.startsWith("text/")) return await clonedRequest.text();
552
- return "[Request body - unknown format]";
553
- } catch (error) {
554
- return "[Request body unavailable]";
584
+ var CookieTokenStorage = class {
585
+ accessTokenKey;
586
+ refreshTokenKey;
587
+ options;
588
+ constructor(options = {}) {
589
+ const prefix = options.prefix || "storefront_";
590
+ this.accessTokenKey = `${prefix}access_token`;
591
+ this.refreshTokenKey = `${prefix}refresh_token`;
592
+ this.options = {
593
+ maxAge: options.maxAge || 10080 * 60,
594
+ path: options.path || "/",
595
+ domain: options.domain,
596
+ secure: options.secure ?? (typeof window !== "undefined" && window.location?.protocol === "https:"),
597
+ sameSite: options.sameSite || "Lax",
598
+ httpOnly: false
599
+ };
555
600
  }
556
- }
601
+ async getAccessToken() {
602
+ return this.getCookie(this.accessTokenKey);
603
+ }
604
+ async setAccessToken(token) {
605
+ this.setCookie(this.accessTokenKey, token);
606
+ }
607
+ async getRefreshToken() {
608
+ return this.getCookie(this.refreshTokenKey);
609
+ }
610
+ async setRefreshToken(token) {
611
+ this.setCookie(this.refreshTokenKey, token);
612
+ }
613
+ async clearTokens() {
614
+ this.deleteCookie(this.accessTokenKey);
615
+ this.deleteCookie(this.refreshTokenKey);
616
+ }
617
+ getCookie(name) {
618
+ if (typeof document === "undefined") return null;
619
+ const value = `; ${document.cookie}`;
620
+ const parts = value.split(`; ${name}=`);
621
+ if (parts.length === 2) {
622
+ const cookieValue = parts.pop()?.split(";").shift();
623
+ return cookieValue ? decodeURIComponent(cookieValue) : null;
624
+ }
625
+ return null;
626
+ }
627
+ setCookie(name, value) {
628
+ if (typeof document === "undefined") return;
629
+ const encodedValue = encodeURIComponent(value);
630
+ let cookieString = `${name}=${encodedValue}`;
631
+ if (this.options.maxAge) cookieString += `; Max-Age=${this.options.maxAge}`;
632
+ if (this.options.path) cookieString += `; Path=${this.options.path}`;
633
+ if (this.options.domain) cookieString += `; Domain=${this.options.domain}`;
634
+ if (this.options.secure) cookieString += `; Secure`;
635
+ if (this.options.sameSite) cookieString += `; SameSite=${this.options.sameSite}`;
636
+ document.cookie = cookieString;
637
+ }
638
+ deleteCookie(name) {
639
+ if (typeof document === "undefined") return;
640
+ let cookieString = `${name}=; Max-Age=0`;
641
+ if (this.options.path) cookieString += `; Path=${this.options.path}`;
642
+ if (this.options.domain) cookieString += `; Domain=${this.options.domain}`;
643
+ document.cookie = cookieString;
644
+ }
645
+ };
557
646
  /**
558
- * Create debug middleware for openapi-fetch (internal use)
647
+ * Create authentication middleware for openapi-fetch
559
648
  */
560
- function createDebugMiddleware(logger) {
561
- const debugLogger = new DebugLogger(logger);
649
+ function createAuthMiddleware(config) {
650
+ let isRefreshing = false;
651
+ let refreshPromise = null;
652
+ let hasAssessedTokens = false;
653
+ const assessTokenStateOnce = async () => {
654
+ if (hasAssessedTokens) return;
655
+ hasAssessedTokens = true;
656
+ try {
657
+ const accessToken = await config.tokenStorage.getAccessToken();
658
+ const refreshToken = await config.tokenStorage.getRefreshToken();
659
+ if (!accessToken && refreshToken) {
660
+ await config.tokenStorage.clearTokens();
661
+ console.info("Cleaned up orphaned refresh token");
662
+ }
663
+ } catch (error) {
664
+ console.warn("Token state assessment failed:", error);
665
+ }
666
+ };
667
+ const refreshTokens = async () => {
668
+ if (isRefreshing && refreshPromise) return refreshPromise;
669
+ isRefreshing = true;
670
+ refreshPromise = (async () => {
671
+ try {
672
+ const refreshToken = await config.tokenStorage.getRefreshToken();
673
+ let newTokens;
674
+ if (refreshToken && !isTokenExpired(refreshToken)) if (config.refreshTokenFn) newTokens = await config.refreshTokenFn(refreshToken);
675
+ else {
676
+ const response = await fetch(`${config.baseUrl}/auth/refresh-token`, {
677
+ method: "POST",
678
+ headers: { "Content-Type": "application/json" },
679
+ body: JSON.stringify({ refresh_token: refreshToken })
680
+ });
681
+ if (!response.ok) throw new Error(`Token refresh failed: ${response.status}`);
682
+ const data = await response.json();
683
+ newTokens = data.content;
684
+ }
685
+ else {
686
+ const currentAccessToken = await config.tokenStorage.getAccessToken();
687
+ if (!currentAccessToken) throw new Error("No tokens available for refresh");
688
+ const reason = refreshToken ? "refresh token expired" : "no refresh token available";
689
+ const response = await fetch(`${config.baseUrl}/auth/anonymous`, {
690
+ method: "POST",
691
+ headers: {
692
+ "Content-Type": "application/json",
693
+ ...config.apiKey && { "X-Api-Key": config.apiKey },
694
+ Authorization: `Bearer ${currentAccessToken}`
695
+ }
696
+ });
697
+ if (!response.ok) throw new Error(`Anonymous token fallback failed: ${response.status}`);
698
+ const data = await response.json();
699
+ newTokens = data.content;
700
+ console.info(`Token refreshed via anonymous fallback (${reason}) - user may need to re-authenticate for privileged operations`);
701
+ }
702
+ await config.tokenStorage.setAccessToken(newTokens.access_token);
703
+ await config.tokenStorage.setRefreshToken(newTokens.refresh_token);
704
+ config.onTokensUpdated?.(newTokens.access_token, newTokens.refresh_token);
705
+ } catch (error) {
706
+ console.error("Token refresh failed:", error);
707
+ await config.tokenStorage.clearTokens();
708
+ config.onTokensCleared?.();
709
+ throw error;
710
+ } finally {
711
+ isRefreshing = false;
712
+ refreshPromise = null;
713
+ }
714
+ })();
715
+ return refreshPromise;
716
+ };
562
717
  return {
563
718
  async onRequest({ request }) {
564
- const requestBody = await extractRequestBody(request);
565
- debugLogger.logRequest(request, requestBody);
719
+ const pathname = getPathnameFromUrl(request.url);
720
+ await assessTokenStateOnce();
721
+ if (isAnonymousAuthEndpoint(pathname)) {
722
+ if (config.apiKey) request.headers.set("X-Api-Key", config.apiKey);
723
+ const existingToken = await config.tokenStorage.getAccessToken();
724
+ if (existingToken && !isTokenExpired(existingToken) && isUserLoggedIn(existingToken)) return new Response(JSON.stringify({
725
+ message: "Cannot create anonymous session while authenticated",
726
+ success: false,
727
+ code: "USER_ALREADY_AUTHENTICATED"
728
+ }), {
729
+ status: 400,
730
+ headers: { "Content-Type": "application/json" }
731
+ });
732
+ if (existingToken) request.headers.set("Authorization", `Bearer ${existingToken}`);
733
+ return request;
734
+ }
735
+ let accessToken = await config.tokenStorage.getAccessToken();
736
+ if (!accessToken) try {
737
+ const response = await fetch(`${config.baseUrl}/auth/anonymous`, {
738
+ method: "POST",
739
+ headers: {
740
+ "Content-Type": "application/json",
741
+ ...config.apiKey && { "X-Api-Key": config.apiKey }
742
+ }
743
+ });
744
+ if (response.ok) {
745
+ const data = await response.json();
746
+ const tokens = data.content;
747
+ if (tokens?.access_token && tokens?.refresh_token) {
748
+ await config.tokenStorage.setAccessToken(tokens.access_token);
749
+ await config.tokenStorage.setRefreshToken(tokens.refresh_token);
750
+ accessToken = tokens.access_token;
751
+ config.onTokensUpdated?.(tokens.access_token, tokens.refresh_token);
752
+ console.info("Automatically created anonymous session for first API request");
753
+ }
754
+ }
755
+ } catch (error) {
756
+ console.warn("Failed to automatically create anonymous tokens:", error);
757
+ }
758
+ if (accessToken && isTokenExpired(accessToken)) try {
759
+ await refreshTokens();
760
+ accessToken = await config.tokenStorage.getAccessToken();
761
+ } catch (error) {}
762
+ if (accessToken) request.headers.set("Authorization", `Bearer ${accessToken}`);
566
763
  return request;
567
764
  },
568
- async onResponse({ response }) {
569
- const cloned = response.clone();
570
- let responseBody = null;
571
- try {
572
- const contentType = response.headers.get("content-type")?.toLowerCase();
573
- if (contentType?.startsWith("application/json")) responseBody = await cloned.json();
574
- else if (contentType?.startsWith("text/")) responseBody = await cloned.text();
575
- } catch (error) {}
576
- await debugLogger.logResponse(response, responseBody);
765
+ async onResponse({ request, response }) {
766
+ const pathname = getPathnameFromUrl(request.url);
767
+ if (response.ok) {
768
+ if (isTokenReturningEndpoint(pathname) || isAnonymousAuthEndpoint(pathname)) try {
769
+ const data = await response.clone().json();
770
+ const content = data.content;
771
+ if (content?.access_token && content?.refresh_token) {
772
+ await config.tokenStorage.setAccessToken(content.access_token);
773
+ await config.tokenStorage.setRefreshToken(content.refresh_token);
774
+ config.onTokensUpdated?.(content.access_token, content.refresh_token);
775
+ }
776
+ } catch (error) {
777
+ console.warn("Failed to extract tokens from response:", error);
778
+ }
779
+ else if (isLogoutEndpoint(pathname)) {
780
+ await config.tokenStorage.clearTokens();
781
+ config.onTokensCleared?.();
782
+ }
783
+ }
784
+ if (response.status === 401 && !isAnonymousAuthEndpoint(pathname)) {
785
+ const currentToken = await config.tokenStorage.getAccessToken();
786
+ if (currentToken && isTokenExpired(currentToken, 0)) try {
787
+ await refreshTokens();
788
+ const newToken = await config.tokenStorage.getAccessToken();
789
+ if (newToken) {
790
+ const retryRequest = request.clone();
791
+ retryRequest.headers.set("Authorization", `Bearer ${newToken}`);
792
+ return fetch(retryRequest);
793
+ }
794
+ } catch (error) {
795
+ console.warn("Token refresh failed on 401 response:", error);
796
+ }
797
+ }
577
798
  return response;
578
- },
579
- async onError({ error, request }) {
580
- debugLogger.logError("API Request Failed", {
581
- error: {
582
- name: error instanceof Error ? error.name : "Unknown",
583
- message: error instanceof Error ? error.message : String(error),
584
- stack: error instanceof Error ? error.stack : void 0
585
- },
586
- request: {
587
- method: request.method,
588
- url: request.url,
589
- headers: Object.fromEntries(request.headers.entries())
590
- },
591
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
592
- });
593
- throw error;
594
799
  }
595
800
  };
596
801
  }
597
-
598
- //#endregion
599
- //#region src/lib/header-utils.ts
600
- /**
601
- * Mapping of SDK header parameter names to actual HTTP header names
602
- * Only include headers that need transformation - others pass through as-is
603
- */
604
- const HEADER_TRANSFORMATIONS = { customer_group_id: "x-customer-group-id" };
605
- /**
606
- * Transform SDK header parameters to actual HTTP header names
607
- * Headers not in the transformation map are passed through unchanged
608
- *
609
- * @param headers - Headers object with SDK parameter names
610
- * @returns Headers object with actual HTTP header names
611
- */
612
- function transformHeaders(headers) {
613
- const transformed = {};
614
- for (const [key, value] of Object.entries(headers)) if (value !== void 0) {
615
- const headerName = HEADER_TRANSFORMATIONS[key] || key;
616
- transformed[headerName] = value;
617
- }
618
- return transformed;
619
- }
620
802
  /**
621
- * Merge default headers with method-level headers
622
- * Method-level headers take precedence over default headers
623
- * Automatically transforms SDK parameter names to HTTP header names
624
- *
625
- * @param defaultHeaders - Default headers from SDK configuration
626
- * @param methodHeaders - Headers passed to the specific method call
627
- * @returns Merged headers object with proper HTTP header names
803
+ * Helper function to create auth middleware with sensible defaults
628
804
  */
629
- function mergeHeaders(defaultHeaders, methodHeaders) {
630
- const merged = {};
631
- if (defaultHeaders) {
632
- const transformedDefaults = transformHeaders(defaultHeaders);
633
- Object.assign(merged, transformedDefaults);
634
- }
635
- if (methodHeaders) Object.assign(merged, methodHeaders);
636
- Object.keys(merged).forEach((key) => {
637
- if (merged[key] === void 0) delete merged[key];
805
+ function createDefaultAuthMiddleware(options) {
806
+ const tokenStorage = options.tokenStorage || (typeof localStorage !== "undefined" ? new BrowserTokenStorage() : new MemoryTokenStorage());
807
+ return createAuthMiddleware({
808
+ tokenStorage,
809
+ apiKey: options.apiKey,
810
+ baseUrl: options.baseUrl,
811
+ onTokensUpdated: options.onTokensUpdated,
812
+ onTokensCleared: options.onTokensCleared
638
813
  });
639
- return merged;
640
814
  }
641
815
 
642
816
  //#endregion
643
- //#region src/lib/client.ts
817
+ //#region src/lib/url-utils.ts
644
818
  /**
645
- * Available API environments
819
+ * URL utility functions for the Storefront SDK
820
+ */
821
+ /**
822
+ * Available API environments for Commerce Engine
646
823
  */
647
824
  let Environment = /* @__PURE__ */ function(Environment$1) {
648
825
  /**
@@ -656,12 +833,26 @@ let Environment = /* @__PURE__ */ function(Environment$1) {
656
833
  return Environment$1;
657
834
  }({});
658
835
  /**
659
- * Base API client for Storefront API
836
+ * Build base URL for Storefront API
660
837
  */
661
- var StorefrontAPIClient = class {
662
- client;
838
+ function buildStorefrontURL(config) {
839
+ if (config.baseUrl) return config.baseUrl;
840
+ const env = config.environment || Environment.Production;
841
+ switch (env) {
842
+ case Environment.Staging: return `https://staging.api.commercengine.io/api/v1/${config.storeId}/storefront`;
843
+ case Environment.Production:
844
+ default: return `https://prod.api.commercengine.io/api/v1/${config.storeId}/storefront`;
845
+ }
846
+ }
847
+
848
+ //#endregion
849
+ //#region src/lib/client.ts
850
+ /**
851
+ * Storefront API client that extends the generic BaseAPIClient
852
+ * Adds Commerce Engine specific authentication and token management
853
+ */
854
+ var StorefrontAPIClient = class extends BaseAPIClient {
663
855
  config;
664
- baseUrl;
665
856
  initializationPromise = null;
666
857
  /**
667
858
  * Create a new StorefrontAPIClient
@@ -669,74 +860,51 @@ var StorefrontAPIClient = class {
669
860
  * @param config - Configuration for the API client
670
861
  */
671
862
  constructor(config) {
863
+ const baseUrl = buildStorefrontURL({
864
+ storeId: config.storeId,
865
+ environment: config.environment,
866
+ baseUrl: config.baseUrl
867
+ });
868
+ const headerTransformations = { customer_group_id: "x-customer-group-id" };
869
+ super({
870
+ baseUrl,
871
+ timeout: config.timeout,
872
+ defaultHeaders: config.defaultHeaders,
873
+ debug: config.debug,
874
+ logger: config.logger
875
+ }, baseUrl, headerTransformations);
672
876
  this.config = { ...config };
673
- this.baseUrl = this.getBaseUrlFromConfig(this.config);
674
- this.client = createClient({ baseUrl: this.baseUrl });
675
- if (this.config.tokenStorage) {
877
+ this.setupStorefrontAuth();
878
+ }
879
+ /**
880
+ * Set up Storefront-specific authentication middleware
881
+ */
882
+ setupStorefrontAuth() {
883
+ const config = this.config;
884
+ if (config.tokenStorage) {
676
885
  const authMiddleware = createDefaultAuthMiddleware({
677
- apiKey: this.config.apiKey,
678
- baseUrl: this.baseUrl,
679
- tokenStorage: this.config.tokenStorage,
680
- onTokensUpdated: this.config.onTokensUpdated,
681
- onTokensCleared: this.config.onTokensCleared
886
+ apiKey: config.apiKey,
887
+ baseUrl: this.getBaseUrl(),
888
+ tokenStorage: config.tokenStorage,
889
+ onTokensUpdated: config.onTokensUpdated,
890
+ onTokensCleared: config.onTokensCleared
682
891
  });
683
892
  this.client.use(authMiddleware);
684
- if (this.config.accessToken) {
685
- this.initializationPromise = this.initializeTokens(this.config.accessToken, this.config.refreshToken);
686
- this.config.accessToken = void 0;
687
- this.config.refreshToken = void 0;
893
+ if (config.accessToken) {
894
+ this.initializationPromise = this.initializeTokens(config.accessToken, config.refreshToken);
895
+ config.accessToken = void 0;
896
+ config.refreshToken = void 0;
688
897
  }
689
898
  } else this.client.use({ onRequest: async ({ request }) => {
690
899
  const pathname = getPathnameFromUrl(request.url);
691
900
  if (isAnonymousAuthEndpoint(pathname)) {
692
- if (this.config.apiKey) request.headers.set("X-Api-Key", this.config.apiKey);
693
- if (this.config.accessToken) request.headers.set("Authorization", `Bearer ${this.config.accessToken}`);
901
+ if (config.apiKey) request.headers.set("X-Api-Key", config.apiKey);
902
+ if (config.accessToken) request.headers.set("Authorization", `Bearer ${config.accessToken}`);
694
903
  return request;
695
904
  }
696
- if (this.config.accessToken) request.headers.set("Authorization", `Bearer ${this.config.accessToken}`);
905
+ if (config.accessToken) request.headers.set("Authorization", `Bearer ${config.accessToken}`);
697
906
  return request;
698
907
  } });
699
- if (this.config.timeout) this.setupTimeoutMiddleware();
700
- if (this.config.debug) {
701
- const debugMiddleware = createDebugMiddleware(this.config.logger);
702
- this.client.use(debugMiddleware);
703
- }
704
- }
705
- /**
706
- * Set up timeout middleware
707
- */
708
- setupTimeoutMiddleware() {
709
- this.client.use({ onRequest: async ({ request }) => {
710
- const controller = new AbortController();
711
- const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
712
- if (request.signal) request.signal.addEventListener("abort", () => controller.abort());
713
- const newRequest = new Request(request, { signal: controller.signal });
714
- controller.signal.addEventListener("abort", () => clearTimeout(timeoutId));
715
- return newRequest;
716
- } });
717
- }
718
- /**
719
- * Constructs the base URL from the configuration
720
- *
721
- * @param config - The client configuration
722
- * @returns The constructed base URL
723
- */
724
- getBaseUrlFromConfig(config) {
725
- if (config.baseUrl) return config.baseUrl;
726
- const env = config.environment || Environment.Production;
727
- switch (env) {
728
- case Environment.Staging: return `https://staging.api.commercengine.io/api/v1/${config.storeId}/storefront`;
729
- case Environment.Production:
730
- default: return `https://prod.api.commercengine.io/api/v1/${config.storeId}/storefront`;
731
- }
732
- }
733
- /**
734
- * Get the base URL of the API
735
- *
736
- * @returns The base URL of the API
737
- */
738
- getBaseUrl() {
739
- return this.baseUrl;
740
908
  }
741
909
  /**
742
910
  * Get the authorization header value
@@ -798,48 +966,6 @@ var StorefrontAPIClient = class {
798
966
  this.config.apiKey = void 0;
799
967
  }
800
968
  /**
801
- * Execute a request and handle the response
802
- *
803
- * @param apiCall - Function that executes the API request
804
- * @returns Promise with the API response
805
- */
806
- async executeRequest(apiCall) {
807
- try {
808
- const { data, error, response } = await apiCall();
809
- if (error) return {
810
- data: null,
811
- error,
812
- response
813
- };
814
- if (data && data.content !== void 0) return {
815
- data: data.content,
816
- error: null,
817
- response
818
- };
819
- return {
820
- data,
821
- error: null,
822
- response
823
- };
824
- } catch (err) {
825
- const mockResponse = new Response(null, {
826
- status: 0,
827
- statusText: "Network Error"
828
- });
829
- const errorResult = {
830
- data: null,
831
- error: {
832
- success: false,
833
- code: "NETWORK_ERROR",
834
- message: "Network error occurred",
835
- error: err
836
- },
837
- response: mockResponse
838
- };
839
- return errorResult;
840
- }
841
- }
842
- /**
843
969
  * Initialize tokens in storage (private helper method)
844
970
  */
845
971
  async initializeTokens(accessToken, refreshToken) {
@@ -852,16 +978,6 @@ var StorefrontAPIClient = class {
852
978
  console.warn("Failed to initialize tokens in storage:", error);
853
979
  }
854
980
  }
855
- /**
856
- * Merge default headers with method-level headers
857
- * Method-level headers take precedence over default headers
858
- *
859
- * @param methodHeaders - Headers passed to the specific method call
860
- * @returns Merged headers object with proper HTTP header names
861
- */
862
- mergeHeaders(methodHeaders) {
863
- return mergeHeaders(this.config.defaultHeaders, methodHeaders);
864
- }
865
981
  };
866
982
 
867
983
  //#endregion
@@ -3646,11 +3762,16 @@ var StorefrontSDK = class {
3646
3762
  */
3647
3763
  store;
3648
3764
  /**
3765
+ * Centrally stored default headers for consistency
3766
+ */
3767
+ defaultHeaders;
3768
+ /**
3649
3769
  * Create a new StorefrontSDK instance
3650
3770
  *
3651
3771
  * @param options - Configuration options for the SDK
3652
3772
  */
3653
3773
  constructor(options) {
3774
+ this.defaultHeaders = options.defaultHeaders;
3654
3775
  const config = {
3655
3776
  storeId: options.storeId,
3656
3777
  environment: options.environment,
@@ -3810,26 +3931,23 @@ var StorefrontSDK = class {
3810
3931
  * @param headers - Default headers to set (only supported headers allowed)
3811
3932
  */
3812
3933
  setDefaultHeaders(headers) {
3813
- const newConfig = {
3814
- ...this.catalog["config"],
3815
- defaultHeaders: headers
3816
- };
3817
- this.catalog["config"] = newConfig;
3818
- this.cart["config"] = newConfig;
3819
- this.auth["config"] = newConfig;
3820
- this.customer["config"] = newConfig;
3821
- this.helpers["config"] = newConfig;
3822
- this.shipping["config"] = newConfig;
3823
- this.order["config"] = newConfig;
3824
- this.store["config"] = newConfig;
3934
+ this.defaultHeaders = headers;
3935
+ this.catalog.setDefaultHeaders(headers);
3936
+ this.cart.setDefaultHeaders(headers);
3937
+ this.auth.setDefaultHeaders(headers);
3938
+ this.customer.setDefaultHeaders(headers);
3939
+ this.helpers.setDefaultHeaders(headers);
3940
+ this.shipping.setDefaultHeaders(headers);
3941
+ this.order.setDefaultHeaders(headers);
3942
+ this.store.setDefaultHeaders(headers);
3825
3943
  }
3826
3944
  /**
3827
3945
  * Get current default headers
3828
3946
  *
3829
- * @returns Current default headers
3947
+ * @returns Current default headers from central storage (always consistent)
3830
3948
  */
3831
3949
  getDefaultHeaders() {
3832
- return this.catalog["config"].defaultHeaders;
3950
+ return this.defaultHeaders;
3833
3951
  }
3834
3952
  };
3835
3953
  var src_default = StorefrontSDK;