@commercengine/storefront-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,679 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NextCookieTokenStorage = exports.CookieTokenStorage = exports.BrowserTokenStorage = exports.MemoryTokenStorage = exports.AuthClient = void 0;
4
+ exports.createTokenStorage = createTokenStorage;
5
+ const client_1 = require("./client");
6
+ /**
7
+ * Client for interacting with authentication endpoints
8
+ */
9
+ class AuthClient extends client_1.StorefrontAPIClient {
10
+ constructor(config, tokenStorage) {
11
+ super(config);
12
+ this.autoRefreshTimer = null;
13
+ // Use provided storage or default to memory storage
14
+ this.tokenStorage = tokenStorage || new MemoryTokenStorage();
15
+ // Try to initialize from storage
16
+ const storedToken = this.tokenStorage.getAccessToken();
17
+ if (storedToken) {
18
+ this.setToken(storedToken);
19
+ this.setupAutoRefresh();
20
+ }
21
+ }
22
+ // Override the setToken method to use storage
23
+ setToken(token) {
24
+ super.setToken(token); // Assuming this sets the token in the client
25
+ this.tokenStorage.setAccessToken(token);
26
+ // If we also have a refresh token, store it
27
+ if (this.tokenStorage.getRefreshToken()) {
28
+ this.setupAutoRefresh();
29
+ }
30
+ }
31
+ // Store refresh token separately
32
+ setRefreshToken(token) {
33
+ this.tokenStorage.setRefreshToken(token);
34
+ }
35
+ // Clear tokens from storage
36
+ clearToken() {
37
+ super.clearToken(); // Assuming this clears the token in the client
38
+ this.tokenStorage.clearTokens();
39
+ this.clearAutoRefresh();
40
+ }
41
+ // Setup auto refresh based on token expiry
42
+ setupAutoRefresh() {
43
+ this.clearAutoRefresh();
44
+ const token = this.tokenStorage.getAccessToken();
45
+ if (!token)
46
+ return;
47
+ const expiryTime = this.getTokenExpiry(token);
48
+ if (!expiryTime)
49
+ return;
50
+ // Calculate refresh time (30 seconds before expiry)
51
+ const currentTime = Math.floor(Date.now() / 1000);
52
+ const timeUntilRefresh = Math.max(0, expiryTime - currentTime - 30) * 1000;
53
+ // Cap the timeout to avoid integer overflow (max 2147483647 ms, ~24.8 days)
54
+ const maxTimeout = 2147483647;
55
+ const safeTimeout = Math.min(timeUntilRefresh, maxTimeout);
56
+ // If the token expires far in the future, we'll need to reset the timer periodically
57
+ if (timeUntilRefresh > maxTimeout) {
58
+ this.autoRefreshTimer = setTimeout(() => {
59
+ this.setupAutoRefresh(); // Recalculate the timer
60
+ }, maxTimeout);
61
+ }
62
+ else {
63
+ this.autoRefreshTimer = setTimeout(() => {
64
+ this.handleTokenRefresh();
65
+ }, safeTimeout);
66
+ }
67
+ }
68
+ clearAutoRefresh() {
69
+ if (this.autoRefreshTimer) {
70
+ clearTimeout(this.autoRefreshTimer);
71
+ this.autoRefreshTimer = null;
72
+ }
73
+ }
74
+ async handleTokenRefresh() {
75
+ try {
76
+ const refreshToken = this.tokenStorage.getRefreshToken();
77
+ if (!refreshToken)
78
+ return;
79
+ const tokens = await this.refreshToken(refreshToken);
80
+ this.setToken(tokens.access_token);
81
+ this.setRefreshToken(tokens.refresh_token);
82
+ }
83
+ catch (error) {
84
+ // If refresh fails, clear tokens
85
+ this.clearToken();
86
+ }
87
+ }
88
+ getTokenExpiry(token) {
89
+ try {
90
+ // Simple JWT parsing (payload is the middle part between dots)
91
+ const parts = token.split(".");
92
+ if (parts.length !== 3)
93
+ return null;
94
+ // Base64 decode the payload
95
+ // We need to fix the base64 padding for the browser's atob function
96
+ const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
97
+ const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "=");
98
+ // In Node.js
99
+ let jsonStr;
100
+ if (typeof Buffer !== "undefined") {
101
+ jsonStr = Buffer.from(padded, "base64").toString();
102
+ }
103
+ // In browser
104
+ else if (typeof atob !== "undefined") {
105
+ jsonStr = atob(padded);
106
+ }
107
+ // Fallback
108
+ else {
109
+ return null;
110
+ }
111
+ const payload = JSON.parse(jsonStr);
112
+ return payload.exp;
113
+ }
114
+ catch (error) {
115
+ console.error("Error parsing JWT token:", error);
116
+ return null;
117
+ }
118
+ }
119
+ /**
120
+ * Get an anonymous user token
121
+ *
122
+ * @param options - Options for anonymous authentication
123
+ * @returns Promise with user info and tokens
124
+ */
125
+ async getAnonymousToken(options) {
126
+ return this.executeWithTokenRefresh(async () => {
127
+ // If apiKey is provided in options, temporarily set it for this request only
128
+ const tempApiKey = options?.apiKey;
129
+ const hadExistingApiKey = !!this.config.apiKey;
130
+ const originalApiKey = this.config.apiKey;
131
+ try {
132
+ // Set temporary API key if provided
133
+ if (tempApiKey) {
134
+ this.setApiKey(tempApiKey);
135
+ }
136
+ // Check if we have an API key set (either from constructor or from options)
137
+ if (!this.config.apiKey) {
138
+ throw new Error("X-Api-Key is required for anonymous authentication");
139
+ }
140
+ const { data, error } = await this.client.POST("/auth/anonymous");
141
+ if (error) {
142
+ this.handleError(error);
143
+ }
144
+ if (data?.content?.access_token && data?.content?.refresh_token) {
145
+ this.setToken(data.content.access_token);
146
+ this.setRefreshToken(data.content.refresh_token);
147
+ }
148
+ return data?.content;
149
+ }
150
+ finally {
151
+ // Restore the original API key state if we used a temporary one
152
+ if (tempApiKey) {
153
+ if (hadExistingApiKey && originalApiKey) {
154
+ this.setApiKey(originalApiKey);
155
+ }
156
+ else {
157
+ this.clearApiKey();
158
+ }
159
+ }
160
+ }
161
+ });
162
+ }
163
+ /**
164
+ * Login with phone number
165
+ *
166
+ * @param phoneNumber - Phone number (without country code)
167
+ * @param countryCode - Country code (defaults to +91)
168
+ * @param registerIfNotExists - Whether to register if user doesn't exist
169
+ * @returns Promise with OTP token and action
170
+ */
171
+ async loginWithPhone(phoneNumber, countryCode = "+91", registerIfNotExists = false) {
172
+ return this.executeWithTokenRefresh(async () => {
173
+ const { data, error } = await this.client.POST("/auth/login/phone", {
174
+ body: {
175
+ phone: phoneNumber,
176
+ country_code: countryCode,
177
+ register_if_not_exists: registerIfNotExists,
178
+ },
179
+ });
180
+ if (error) {
181
+ this.handleError(error);
182
+ }
183
+ return data?.content;
184
+ });
185
+ }
186
+ /**
187
+ * Login with email
188
+ *
189
+ * @param email - Email address
190
+ * @param registerIfNotExists - Whether to register if user doesn't exist
191
+ * @returns Promise with OTP token and action
192
+ */
193
+ async loginWithEmail(email, registerIfNotExists = false) {
194
+ return this.executeWithTokenRefresh(async () => {
195
+ const { data, error } = await this.client.POST("/auth/login/email", {
196
+ body: {
197
+ email,
198
+ register_if_not_exists: registerIfNotExists,
199
+ },
200
+ });
201
+ if (error) {
202
+ this.handleError(error);
203
+ }
204
+ return data?.content;
205
+ });
206
+ }
207
+ /**
208
+ * Login with password
209
+ *
210
+ * @param options - Login credentials
211
+ * @returns Promise with user info and tokens
212
+ */
213
+ async loginWithPassword(options) {
214
+ return this.executeWithTokenRefresh(async () => {
215
+ const { data, error } = await this.client.POST("/auth/login/password", {
216
+ body: options,
217
+ });
218
+ if (error) {
219
+ this.handleError(error);
220
+ }
221
+ if (data?.content?.access_token && data?.content?.refresh_token) {
222
+ this.setToken(data.content.access_token);
223
+ this.setRefreshToken(data.content.refresh_token);
224
+ }
225
+ return data?.content;
226
+ });
227
+ }
228
+ /**
229
+ * Verify OTP
230
+ *
231
+ * @param otp - One-time password
232
+ * @param otpToken - OTP token from login request
233
+ * @param otpAction - OTP action from login request
234
+ * @returns Promise with user info and tokens
235
+ */
236
+ async verifyOtp(otp, otpToken, otpAction) {
237
+ return this.executeWithTokenRefresh(async () => {
238
+ const { data, error } = await this.client.POST("/auth/verify-otp", {
239
+ body: {
240
+ otp,
241
+ otp_token: otpToken,
242
+ otp_action: otpAction,
243
+ },
244
+ });
245
+ if (error) {
246
+ this.handleError(error);
247
+ }
248
+ if (data?.content?.access_token && data?.content?.refresh_token) {
249
+ this.setToken(data.content.access_token);
250
+ this.setRefreshToken(data.content.refresh_token);
251
+ }
252
+ return data?.content;
253
+ });
254
+ }
255
+ /**
256
+ * Register with phone
257
+ *
258
+ * @param options - Registration details
259
+ * @returns Promise with user info and tokens
260
+ */
261
+ async registerWithPhone(options) {
262
+ return this.executeWithTokenRefresh(async () => {
263
+ const { data, error } = await this.client.POST("/auth/register/phone", {
264
+ body: {
265
+ ...options,
266
+ country_code: options.country_code,
267
+ },
268
+ });
269
+ if (error) {
270
+ this.handleError(error);
271
+ }
272
+ if (data?.content?.access_token && data?.content?.refresh_token) {
273
+ this.setToken(data.content.access_token);
274
+ this.setRefreshToken(data.content.refresh_token);
275
+ }
276
+ return data?.content;
277
+ });
278
+ }
279
+ /**
280
+ * Register with email
281
+ *
282
+ * @param options - Registration details
283
+ * @returns Promise with user info and tokens
284
+ */
285
+ async registerWithEmail(options) {
286
+ return this.executeWithTokenRefresh(async () => {
287
+ const { data, error } = await this.client.POST("/auth/register/email", {
288
+ body: options,
289
+ });
290
+ if (error) {
291
+ this.handleError(error);
292
+ }
293
+ if (data?.content?.access_token && data?.content?.refresh_token) {
294
+ this.setToken(data.content.access_token);
295
+ this.setRefreshToken(data.content.refresh_token);
296
+ }
297
+ return data?.content;
298
+ });
299
+ }
300
+ /**
301
+ * Refresh token
302
+ *
303
+ * @param refreshToken - Refresh token
304
+ * @returns Promise with new tokens
305
+ */
306
+ async refreshToken(refreshToken) {
307
+ // Don't use executeWithTokenRefresh here to avoid infinite loop
308
+ const { data, error } = await this.client.POST("/auth/refresh-token", {
309
+ body: {
310
+ refresh_token: refreshToken,
311
+ },
312
+ });
313
+ if (error) {
314
+ this.handleError(error);
315
+ }
316
+ if (data?.content?.access_token && data?.content?.refresh_token) {
317
+ this.setToken(data.content.access_token);
318
+ this.setRefreshToken(data.content.refresh_token);
319
+ }
320
+ return data?.content;
321
+ }
322
+ /**
323
+ * Logout
324
+ *
325
+ * @returns Promise that resolves when logout is complete
326
+ */
327
+ async logout() {
328
+ return this.executeWithTokenRefresh(async () => {
329
+ const { error } = await this.client.POST("/auth/logout");
330
+ if (error) {
331
+ this.handleError(error);
332
+ }
333
+ this.clearToken();
334
+ });
335
+ }
336
+ /**
337
+ * Override the base client's attemptTokenRefresh method
338
+ * to implement token refresh for 401 errors
339
+ *
340
+ * @returns Promise that resolves to true if token was refreshed, false otherwise
341
+ */
342
+ async attemptTokenRefresh() {
343
+ const refreshToken = this.tokenStorage.getRefreshToken();
344
+ if (!refreshToken)
345
+ return false;
346
+ try {
347
+ const tokens = await this.refreshToken(refreshToken);
348
+ if (tokens.access_token) {
349
+ this.setToken(tokens.access_token);
350
+ }
351
+ if (tokens.refresh_token) {
352
+ this.setRefreshToken(tokens.refresh_token);
353
+ }
354
+ return true;
355
+ }
356
+ catch (error) {
357
+ // If refresh fails, clear tokens
358
+ this.clearToken();
359
+ return false;
360
+ }
361
+ }
362
+ /**
363
+ * Execute a request with automatic token refresh handling
364
+ * This wraps any API call in logic that will catch authentication errors,
365
+ * attempt to refresh the token, and retry the request once
366
+ *
367
+ * @param requestFn - Function that executes the API request
368
+ * @returns Promise with the API response
369
+ */
370
+ async executeWithTokenRefresh(requestFn) {
371
+ try {
372
+ // Attempt the request
373
+ return await requestFn();
374
+ }
375
+ catch (error) {
376
+ // If error message indicates token was refreshed, retry the request
377
+ if (error instanceof Error &&
378
+ error.message === "Token refreshed, please retry the request") {
379
+ return await requestFn();
380
+ }
381
+ // Otherwise, re-throw the error
382
+ throw error;
383
+ }
384
+ }
385
+ }
386
+ exports.AuthClient = AuthClient;
387
+ /**
388
+ * Default in-memory implementation of token storage
389
+ */
390
+ class MemoryTokenStorage {
391
+ constructor() {
392
+ this.accessToken = null;
393
+ this.refreshToken = null;
394
+ }
395
+ getAccessToken() {
396
+ return this.accessToken;
397
+ }
398
+ setAccessToken(token) {
399
+ this.accessToken = token;
400
+ }
401
+ getRefreshToken() {
402
+ return this.refreshToken;
403
+ }
404
+ setRefreshToken(token) {
405
+ this.refreshToken = token;
406
+ }
407
+ clearTokens() {
408
+ this.accessToken = null;
409
+ this.refreshToken = null;
410
+ }
411
+ }
412
+ exports.MemoryTokenStorage = MemoryTokenStorage;
413
+ /**
414
+ * Browser storage implementation using localStorage
415
+ */
416
+ class BrowserTokenStorage {
417
+ constructor(prefix = "storefront_") {
418
+ this.accessTokenKey = `${prefix}access_token`;
419
+ this.refreshTokenKey = `${prefix}refresh_token`;
420
+ }
421
+ getAccessToken() {
422
+ if (typeof localStorage === "undefined")
423
+ return null;
424
+ return localStorage.getItem(this.accessTokenKey);
425
+ }
426
+ setAccessToken(token) {
427
+ if (typeof localStorage === "undefined")
428
+ return;
429
+ localStorage.setItem(this.accessTokenKey, token);
430
+ }
431
+ getRefreshToken() {
432
+ if (typeof localStorage === "undefined")
433
+ return null;
434
+ return localStorage.getItem(this.refreshTokenKey);
435
+ }
436
+ setRefreshToken(token) {
437
+ if (typeof localStorage === "undefined")
438
+ return;
439
+ localStorage.setItem(this.refreshTokenKey, token);
440
+ }
441
+ clearTokens() {
442
+ if (typeof localStorage === "undefined")
443
+ return;
444
+ localStorage.removeItem(this.accessTokenKey);
445
+ localStorage.removeItem(this.refreshTokenKey);
446
+ }
447
+ }
448
+ exports.BrowserTokenStorage = BrowserTokenStorage;
449
+ /**
450
+ * Cookie-based token storage for browser or server environments
451
+ */
452
+ class CookieTokenStorage {
453
+ constructor(prefix = "storefront_", cookieOptions = {
454
+ path: "/",
455
+ secure: true,
456
+ sameSite: "strict",
457
+ maxAge: 60 * 60 * 24 * 30, // 30 days
458
+ }) {
459
+ this.accessTokenKey = `${prefix}access_token`;
460
+ this.refreshTokenKey = `${prefix}refresh_token`;
461
+ this.cookieOptions = cookieOptions;
462
+ }
463
+ /**
464
+ * Get access token from cookies
465
+ * Works in both browser and Next.js server components
466
+ */
467
+ getAccessToken() {
468
+ // Browser environment
469
+ if (typeof document !== "undefined") {
470
+ return getCookieValue(this.accessTokenKey);
471
+ }
472
+ // Next.js server component - would need to be implemented by user
473
+ // with their specific cookie library
474
+ return null;
475
+ }
476
+ /**
477
+ * Set access token in cookies
478
+ * Works in browser environment
479
+ */
480
+ setAccessToken(token) {
481
+ // Browser environment
482
+ if (typeof document !== "undefined") {
483
+ setCookie(this.accessTokenKey, token, this.cookieOptions);
484
+ }
485
+ // Next.js - would need to be implemented by user
486
+ // with their specific cookie API
487
+ }
488
+ /**
489
+ * Get refresh token from cookies
490
+ * Works in both browser and Next.js server components
491
+ */
492
+ getRefreshToken() {
493
+ // Browser environment
494
+ if (typeof document !== "undefined") {
495
+ return getCookieValue(this.refreshTokenKey);
496
+ }
497
+ // Next.js server component - would need to be implemented by user
498
+ return null;
499
+ }
500
+ /**
501
+ * Set refresh token in cookies
502
+ * Works in browser environment
503
+ */
504
+ setRefreshToken(token) {
505
+ // Browser environment
506
+ if (typeof document !== "undefined") {
507
+ setCookie(this.refreshTokenKey, token, this.cookieOptions);
508
+ }
509
+ // Next.js - would need to be implemented by user
510
+ }
511
+ /**
512
+ * Clear all tokens from cookies
513
+ */
514
+ clearTokens() {
515
+ // Browser environment
516
+ if (typeof document !== "undefined") {
517
+ deleteCookie(this.accessTokenKey);
518
+ deleteCookie(this.refreshTokenKey);
519
+ }
520
+ // Next.js - would need to be implemented by user
521
+ }
522
+ }
523
+ exports.CookieTokenStorage = CookieTokenStorage;
524
+ /**
525
+ * Helper function to get cookie value
526
+ */
527
+ function getCookieValue(name) {
528
+ if (typeof document === "undefined")
529
+ return null;
530
+ const match = document.cookie.match(new RegExp("(^| )" + name + "=([^;]+)"));
531
+ return match ? decodeURIComponent(match[2]) : null;
532
+ }
533
+ /**
534
+ * Helper function to set cookie
535
+ */
536
+ function setCookie(name, value, options = {}) {
537
+ if (typeof document === "undefined")
538
+ return;
539
+ const cookieOptions = {
540
+ path: "/",
541
+ ...options,
542
+ };
543
+ let cookie = `${name}=${encodeURIComponent(value)}`;
544
+ if (cookieOptions.maxAge) {
545
+ cookie += `; max-age=${cookieOptions.maxAge}`;
546
+ }
547
+ if (cookieOptions.domain) {
548
+ cookie += `; domain=${cookieOptions.domain}`;
549
+ }
550
+ if (cookieOptions.path) {
551
+ cookie += `; path=${cookieOptions.path}`;
552
+ }
553
+ if (cookieOptions.secure) {
554
+ cookie += "; secure";
555
+ }
556
+ if (cookieOptions.sameSite) {
557
+ cookie += `; samesite=${cookieOptions.sameSite}`;
558
+ }
559
+ document.cookie = cookie;
560
+ }
561
+ /**
562
+ * Helper function to delete cookie
563
+ */
564
+ function deleteCookie(name, path = "/") {
565
+ if (typeof document === "undefined")
566
+ return;
567
+ document.cookie = `${name}=; path=${path}; expires=Thu, 01 Jan 1970 00:00:01 GMT`;
568
+ }
569
+ /**
570
+ * Next.js specific cookie storage implementation
571
+ * Works with the Next.js cookies API
572
+ */
573
+ class NextCookieTokenStorage {
574
+ constructor(cookieStore, prefix = "storefront_") {
575
+ this.accessTokenKey = `${prefix}access_token`;
576
+ this.refreshTokenKey = `${prefix}refresh_token`;
577
+ this.cookieStore = cookieStore;
578
+ }
579
+ /**
580
+ * Get access token from Next.js cookies
581
+ */
582
+ getAccessToken() {
583
+ try {
584
+ // Assuming cookieStore.get method exists (like in next/headers)
585
+ const cookie = this.cookieStore.get(this.accessTokenKey);
586
+ return cookie ? cookie.value : null;
587
+ }
588
+ catch (error) {
589
+ return null;
590
+ }
591
+ }
592
+ /**
593
+ * Set access token in Next.js cookies
594
+ */
595
+ setAccessToken(token) {
596
+ try {
597
+ // Assuming cookieStore.set method exists
598
+ this.cookieStore.set(this.accessTokenKey, token, {
599
+ path: "/",
600
+ secure: process.env.NODE_ENV === "production",
601
+ httpOnly: true,
602
+ sameSite: "strict",
603
+ maxAge: 60 * 60 * 24 * 30, // 30 days
604
+ });
605
+ }
606
+ catch (error) {
607
+ // Silently fail - might be in an environment where cookies can't be set
608
+ }
609
+ }
610
+ /**
611
+ * Get refresh token from Next.js cookies
612
+ */
613
+ getRefreshToken() {
614
+ try {
615
+ // Assuming cookieStore.get method exists
616
+ const cookie = this.cookieStore.get(this.refreshTokenKey);
617
+ return cookie ? cookie.value : null;
618
+ }
619
+ catch (error) {
620
+ return null;
621
+ }
622
+ }
623
+ /**
624
+ * Set refresh token in Next.js cookies
625
+ */
626
+ setRefreshToken(token) {
627
+ try {
628
+ // Assuming cookieStore.set method exists
629
+ this.cookieStore.set(this.refreshTokenKey, token, {
630
+ path: "/",
631
+ secure: process.env.NODE_ENV === "production",
632
+ httpOnly: true,
633
+ sameSite: "strict",
634
+ maxAge: 60 * 60 * 24 * 30, // 30 days
635
+ });
636
+ }
637
+ catch (error) {
638
+ // Silently fail
639
+ }
640
+ }
641
+ /**
642
+ * Clear all tokens from Next.js cookies
643
+ */
644
+ clearTokens() {
645
+ try {
646
+ // Assuming cookieStore.delete method exists
647
+ this.cookieStore.delete(this.accessTokenKey);
648
+ this.cookieStore.delete(this.refreshTokenKey);
649
+ }
650
+ catch (error) {
651
+ // Silently fail
652
+ }
653
+ }
654
+ }
655
+ exports.NextCookieTokenStorage = NextCookieTokenStorage;
656
+ /**
657
+ * Helper to create a token storage instance based on environment
658
+ * Automatically selects the best storage method based on context
659
+ */
660
+ function createTokenStorage(options) {
661
+ const prefix = options?.prefix || "storefront_";
662
+ // Node.js environment without a cookieStore - use memory
663
+ if (typeof window === "undefined" && !options?.cookieStore) {
664
+ return new MemoryTokenStorage();
665
+ }
666
+ // Next.js server component with cookieStore
667
+ if (typeof window === "undefined" && options?.cookieStore) {
668
+ return new NextCookieTokenStorage(options.cookieStore, prefix);
669
+ }
670
+ // Browser environment - use localStorage by default unless cookies are specified
671
+ if (typeof window !== "undefined") {
672
+ if (options?.useLocalStorage === false) {
673
+ return new CookieTokenStorage(prefix);
674
+ }
675
+ return new BrowserTokenStorage(prefix);
676
+ }
677
+ // Fallback to memory storage
678
+ return new MemoryTokenStorage();
679
+ }