lightspeed-retail-sdk 3.3.4 → 3.4.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.
package/README.md CHANGED
@@ -2,9 +2,16 @@
2
2
 
3
3
  A modern JavaScript SDK for interacting with the Lightspeed Retail API. This SDK provides a convenient, secure, and flexible way to access Lightspeed Retail's features—including customer, item, and order management.
4
4
 
5
- **Current Version: 3.3.4** — improve error checking on token refresh to prevent false email warnings.
5
+ **Current Version: 3.4.0** — Enhanced CLI with interactive token injection and production environment support.
6
6
 
7
- ## **🆕 Recent Updates (v3.3.4)**
7
+ ## **🆕 Recent Updates (v3.4.0)**
8
+
9
+ - **🔧 Enhanced Token Injection**: Interactive `inject-tokens` command with prompts for access/refresh tokens, expiry settings, and storage backend selection
10
+ - **🏭 Production Environment Support**: Login command now supports headless environments with `--no-browser` option and automatic detection
11
+ - **💡 Improved CLI UX**: Better error messages, token validation, and clear instructions for manual OAuth flows
12
+ - **📖 Enhanced Documentation**: Updated README and CLI help with production deployment guidance
13
+
14
+ ## **Previous Updates (v3.3.5)**
8
15
 
9
16
  - **Add centralized query param builder for API requests**: Add centralized query param builder for API requests. Supports input as object, string, or array, and manages relations/load_relations. Ensures no double-encoding of parameters and handles special cases for 'or' and 'timeStamp'.
10
17
  - **🎯 Enhanced Parameter Support**: All main getter methods now support both legacy and new object-based parameters with full backward compatibility
@@ -66,7 +73,7 @@ const items = await sdk.getItems({
66
73
  ## Table of Contents
67
74
 
68
75
  - [Another Unofficial Lightspeed Retail V3 API SDK](#another-unofficial-lightspeed-retail-v3-api-sdk)
69
- - [**🆕 Recent Updates (v3.3.4)**](#-recent-updates-v334)
76
+ - [**🆕 Recent Updates (v3.3.5)**](#-recent-updates-v335)
70
77
  - [🚀 Key Features](#-key-features)
71
78
  - [🔄 Migrating from 3.1.x](#-migrating-from-31x)
72
79
  - [Backward Compatibility](#backward-compatibility)
@@ -107,6 +114,8 @@ const items = await sdk.getItems({
107
114
  - [Alternative: Local Installation](#alternative-local-installation)
108
115
  - [Basic Usage (In-Memory Storage)](#basic-usage-in-memory-storage)
109
116
  - [Manual Token Management (Advanced)](#manual-token-management-advanced)
117
+ - [Option 1: Interactive Token Injection (Easiest)](#option-1-interactive-token-injection-easiest)
118
+ - [Option 2: Programmatic Token Storage](#option-2-programmatic-token-storage)
110
119
  - [File-Based Storage](#file-based-storage)
111
120
  - [Encrypted Storage (Recommended)](#encrypted-storage-recommended)
112
121
  - [Database Storage (PostgreSQL, SQLite, and MongoDB)](#database-storage-postgresql-sqlite-and-mongodb)
@@ -197,6 +206,9 @@ lightspeed-retail-sdk login --browser firefox
197
206
  lightspeed-retail-sdk login --browser "google chrome"
198
207
  lightspeed-retail-sdk login --browser safari
199
208
 
209
+ # Production/headless environment - display URL without opening browser
210
+ lightspeed-retail-sdk login --no-browser
211
+
200
212
  # Check current token status
201
213
  lightspeed-retail-sdk token-status
202
214
 
@@ -205,6 +217,12 @@ lightspeed-retail-sdk refresh-token
205
217
 
206
218
  # View your account information
207
219
  lightspeed-retail-sdk whoami
220
+
221
+ # Manually inject access and refresh tokens (interactive)
222
+ lightspeed-retail-sdk inject-tokens
223
+
224
+ # Manually inject tokens with command-line options
225
+ lightspeed-retail-sdk inject-tokens --access "your_access_token" --refresh "your_refresh_token"
208
226
  ```
209
227
 
210
228
  #### Storage Management
@@ -249,10 +267,11 @@ lightspeed-retail-sdk login
249
267
  The login process:
250
268
 
251
269
  1. Prompts for your Lightspeed credentials (if not in environment)
252
- 2. Optionally lets you choose a specific browser
253
- 3. Opens browser for OAuth authorization
254
- 4. Automatically exchanges code for tokens
255
- 5. Stores tokens in your chosen backend
270
+ 2. Optionally lets you choose a specific browser (or skips browser opening in production)
271
+ 3. Opens browser for OAuth authorization (or displays URL for manual access)
272
+ 4. Waits for you to paste the authorization code from the redirect URL
273
+ 5. Automatically exchanges code for tokens
274
+ 6. Stores tokens in your chosen backend
256
275
 
257
276
  **Note**: If no scopes are specified via environment variables or user input, the default scope `employee:all` will be used.
258
277
 
@@ -270,6 +289,22 @@ lightspeed-retail-sdk login --browser "google chrome"
270
289
  # The CLI will ask if you want to choose a specific browser
271
290
  ```
272
291
 
292
+ **Production/Headless Environments:**
293
+
294
+ ```bash
295
+ # Skip browser opening and display URL for manual access
296
+ lightspeed-retail-sdk login --no-browser
297
+ ```
298
+
299
+ This is useful when:
300
+
301
+ - Running in Docker containers or cloud environments without GUI
302
+ - SSH sessions without X11 forwarding
303
+ - Automated deployment scripts
304
+ - CI/CD pipelines
305
+
306
+ The CLI will automatically detect headless environments and display the OAuth URL instead of trying to open a browser.
307
+
273
308
  #### Token Management
274
309
 
275
310
  ##### Manual Token Refresh
@@ -655,7 +690,28 @@ const api = new LightspeedRetailSDK({
655
690
 
656
691
  ### Manual Token Management (Advanced)
657
692
 
658
- If you prefer to handle authentication manually without the CLI:
693
+ If you prefer to handle authentication manually without the CLI, you have several options:
694
+
695
+ #### Option 1: Interactive Token Injection (Easiest)
696
+
697
+ Use the CLI to manually input tokens you've obtained from elsewhere:
698
+
699
+ ```bash
700
+ # Interactive prompts for tokens and storage configuration
701
+ lightspeed-retail-sdk inject-tokens
702
+
703
+ # Or specify tokens via command line and use interactive prompts for storage
704
+ lightspeed-retail-sdk inject-tokens --access "your_access_token" --refresh "your_refresh_token"
705
+ ```
706
+
707
+ This method will:
708
+
709
+ - Prompt you to enter access and refresh tokens
710
+ - Let you choose expiry settings (default 1 hour, custom date, or seconds)
711
+ - Allow you to select your storage backend (file, encrypted file, or database)
712
+ - Store the tokens securely in your chosen backend
713
+
714
+ #### Option 2: Programmatic Token Storage
659
715
 
660
716
  #### File-Based Storage
661
717
 
@@ -61,6 +61,10 @@ program
61
61
  "-b, --browser <browser>",
62
62
  "Specify browser to use (chrome, firefox, safari, edge, etc.)"
63
63
  )
64
+ .option(
65
+ "--no-browser",
66
+ "Skip browser opening and display URL for manual access (useful in production/headless environments)"
67
+ )
64
68
  .action(async (options) => {
65
69
  let storageBackend = null;
66
70
  try {
@@ -89,70 +93,83 @@ program
89
93
  clientID
90
94
  )}&scope=${encodeURIComponent(scopes)}`;
91
95
 
92
- console.log("\nOpening browser for authentication...");
96
+ // Detect if we're in a headless/production environment
97
+ const isHeadless = !process.env.DISPLAY && !process.env.SSH_CLIENT && process.platform !== 'darwin' && process.platform !== 'win32';
98
+ const skipBrowser = options.noBrowser || isHeadless;
93
99
 
94
- // Open browser with optional browser specification
95
- const openOptions = {};
96
- if (options.browser) {
97
- openOptions.app = { name: options.browser };
98
- console.log(`Using browser: ${options.browser}`);
100
+ if (skipBrowser) {
101
+ console.log("\n🔗 OAuth Authentication Required");
102
+ console.log("\nPlease open the following URL in your browser to authorize the application:");
103
+ console.log(`\n${authUrl}\n`);
104
+ console.log("After authorizing, you will be redirected to your configured redirect URI");
105
+ console.log("with an authorization code in the URL (e.g., ?code=abc123...)");
99
106
  } else {
100
- // Ask user if they want to choose a specific browser
101
- const { chooseBrowser } = await inquirer.prompt([
102
- {
103
- type: "confirm",
104
- name: "chooseBrowser",
105
- message: "Do you want to choose a specific browser?",
106
- default: false,
107
- },
108
- ]);
107
+ console.log("\nOpening browser for authentication...");
109
108
 
110
- if (chooseBrowser) {
111
- const { browserChoice } = await inquirer.prompt([
109
+ // Open browser with optional browser specification
110
+ const openOptions = {};
111
+ if (options.browser) {
112
+ openOptions.app = { name: options.browser };
113
+ console.log(`Using browser: ${options.browser}`);
114
+ } else {
115
+ // Ask user if they want to choose a specific browser
116
+ const { chooseBrowser } = await inquirer.prompt([
112
117
  {
113
- type: "list",
114
- name: "browserChoice",
115
- message: "Select browser:",
116
- choices: [
117
- { name: "Default browser", value: null },
118
- { name: "Google Chrome", value: "google chrome" },
119
- { name: "Firefox", value: "firefox" },
120
- { name: "Safari", value: "safari" },
121
- { name: "Microsoft Edge", value: "microsoft edge" },
122
- { name: "Brave", value: "brave" },
123
- { name: "Opera", value: "opera" },
124
- { name: "Custom browser name", value: "custom" },
125
- ],
118
+ type: "confirm",
119
+ name: "chooseBrowser",
120
+ message: "Do you want to choose a specific browser?",
121
+ default: false,
126
122
  },
127
123
  ]);
128
124
 
129
- if (browserChoice === "custom") {
130
- const { customBrowser } = await inquirer.prompt([
125
+ if (chooseBrowser) {
126
+ const { browserChoice } = await inquirer.prompt([
131
127
  {
132
- type: "input",
133
- name: "customBrowser",
134
- message: "Enter browser name:",
135
- validate: (input) =>
136
- input.trim() !== "" || "Browser name cannot be empty",
128
+ type: "list",
129
+ name: "browserChoice",
130
+ message: "Select browser:",
131
+ choices: [
132
+ { name: "Default browser", value: null },
133
+ { name: "Google Chrome", value: "google chrome" },
134
+ { name: "Firefox", value: "firefox" },
135
+ { name: "Safari", value: "safari" },
136
+ { name: "Microsoft Edge", value: "microsoft edge" },
137
+ { name: "Brave", value: "brave" },
138
+ { name: "Opera", value: "opera" },
139
+ { name: "Custom browser name", value: "custom" },
140
+ ],
137
141
  },
138
142
  ]);
139
- openOptions.app = { name: customBrowser };
140
- console.log(`Using browser: ${customBrowser}`);
141
- } else if (browserChoice) {
142
- openOptions.app = { name: browserChoice };
143
- console.log(`Using browser: ${browserChoice}`);
143
+
144
+ if (browserChoice === "custom") {
145
+ const { customBrowser } = await inquirer.prompt([
146
+ {
147
+ type: "input",
148
+ name: "customBrowser",
149
+ message: "Enter browser name:",
150
+ validate: (input) =>
151
+ input.trim() !== "" || "Browser name cannot be empty",
152
+ },
153
+ ]);
154
+ openOptions.app = { name: customBrowser };
155
+ console.log(`Using browser: ${customBrowser}`);
156
+ } else if (browserChoice) {
157
+ openOptions.app = { name: browserChoice };
158
+ console.log(`Using browser: ${browserChoice}`);
159
+ }
144
160
  }
145
161
  }
146
- }
147
162
 
148
- try {
149
- await open(authUrl, openOptions);
150
- } catch (err) {
151
- console.log(
152
- `\n⚠️ Could not open browser automatically: ${err.message}`
153
- );
154
- console.log(`\nPlease manually open this URL in your browser:`);
155
- console.log(authUrl);
163
+ try {
164
+ await open(authUrl, openOptions);
165
+ console.log(`\nIf the browser didn't open, manually visit: ${authUrl}`);
166
+ } catch (err) {
167
+ console.log(
168
+ `\n⚠️ Could not open browser automatically: ${err.message}`
169
+ );
170
+ console.log(`\nPlease manually open this URL in your browser:`);
171
+ console.log(`\n${authUrl}\n`);
172
+ }
156
173
  }
157
174
 
158
175
  // 3. Prompt for code
@@ -422,6 +439,153 @@ program
422
439
  }
423
440
  });
424
441
 
442
+ program
443
+ .command("inject-tokens")
444
+ .description("Manually store an access & refresh token in chosen backend")
445
+ .option("--access <accessToken>", "Access token (skip interactive prompt)")
446
+ .option("--refresh <refreshToken>", "Refresh token (skip interactive prompt)")
447
+ .option(
448
+ "--expires-at <isoDatetime>",
449
+ "ISO8601 expiry (e.g. 2025-01-01T12:34:56.000Z). Overrides --expires-in."
450
+ )
451
+ .option(
452
+ "--expires-in <seconds>",
453
+ "Seconds until expiry (default 3600 if neither --expires-at nor --expires-in provided)"
454
+ )
455
+ .action(async (opts) => {
456
+ let storageBackend = null;
457
+ try {
458
+ console.log("\n🔑 Manual Token Injection\n");
459
+ console.log("This command allows you to manually store access and refresh tokens");
460
+ console.log("that you've obtained through other means (e.g., Lightspeed dashboard).\n");
461
+
462
+ // Get tokens either from command line options or interactive prompts
463
+ let accessToken = opts.access;
464
+ let refreshToken = opts.refresh;
465
+
466
+ if (!accessToken) {
467
+ accessToken = await prompt("Enter your access token: ");
468
+ if (!accessToken || accessToken.trim() === "") {
469
+ console.error("❌ Access token is required");
470
+ process.exit(1);
471
+ }
472
+ accessToken = accessToken.trim();
473
+ }
474
+
475
+ if (!refreshToken) {
476
+ refreshToken = await prompt("Enter your refresh token: ");
477
+ if (!refreshToken || refreshToken.trim() === "") {
478
+ console.error("❌ Refresh token is required");
479
+ process.exit(1);
480
+ }
481
+ refreshToken = refreshToken.trim();
482
+ }
483
+
484
+ // Get expiry information
485
+ let expiresAt;
486
+ if (opts.expiresAt) {
487
+ const d = new Date(opts.expiresAt);
488
+ if (isNaN(d.getTime())) {
489
+ console.error("❌ Invalid --expires-at value");
490
+ process.exit(1);
491
+ }
492
+ expiresAt = d.toISOString();
493
+ } else if (opts.expiresIn) {
494
+ const seconds = parseInt(opts.expiresIn, 10);
495
+ if (isNaN(seconds) || seconds <= 0) {
496
+ console.error("❌ Invalid --expires-in value");
497
+ process.exit(1);
498
+ }
499
+ expiresAt = new Date(Date.now() + seconds * 1000).toISOString();
500
+ } else {
501
+ // Interactive prompt for expiry
502
+ const expiryChoice = await inquirer.prompt([
503
+ {
504
+ type: "list",
505
+ name: "expiryMethod",
506
+ message: "How would you like to set the token expiry?",
507
+ choices: [
508
+ { name: "Default (1 hour from now)", value: "default" },
509
+ { name: "Specific date/time (ISO format)", value: "datetime" },
510
+ { name: "Seconds from now", value: "seconds" },
511
+ ],
512
+ default: "default",
513
+ },
514
+ ]);
515
+
516
+ switch (expiryChoice.expiryMethod) {
517
+ case "default":
518
+ expiresAt = new Date(Date.now() + 3600 * 1000).toISOString();
519
+ break;
520
+ case "datetime":
521
+ const { customDateTime } = await inquirer.prompt([
522
+ {
523
+ type: "input",
524
+ name: "customDateTime",
525
+ message: "Enter expiry date/time (ISO format, e.g., 2025-01-01T12:34:56.000Z):",
526
+ validate: (input) => {
527
+ const d = new Date(input);
528
+ return !isNaN(d.getTime()) || "Please enter a valid ISO datetime";
529
+ },
530
+ },
531
+ ]);
532
+ expiresAt = new Date(customDateTime).toISOString();
533
+ break;
534
+ case "seconds":
535
+ const { customSeconds } = await inquirer.prompt([
536
+ {
537
+ type: "input",
538
+ name: "customSeconds",
539
+ message: "Enter seconds until expiry:",
540
+ default: "3600",
541
+ validate: (input) => {
542
+ const num = parseInt(input, 10);
543
+ return (!isNaN(num) && num > 0) || "Please enter a positive number";
544
+ },
545
+ },
546
+ ]);
547
+ expiresAt = new Date(Date.now() + parseInt(customSeconds, 10) * 1000).toISOString();
548
+ break;
549
+ }
550
+ }
551
+
552
+ console.log("\n📁 Token Storage Configuration");
553
+ storageBackend = await selectStorageBackend();
554
+
555
+ // Validate tokens format (basic check)
556
+ if (accessToken.length < 10) {
557
+ console.warn("⚠️ Warning: Access token seems unusually short");
558
+ }
559
+ if (refreshToken.length < 10) {
560
+ console.warn("⚠️ Warning: Refresh token seems unusually short");
561
+ }
562
+
563
+ await storageBackend.setTokens({
564
+ access_token: accessToken,
565
+ refresh_token: refreshToken,
566
+ expires_at: expiresAt,
567
+ expires_in: Math.floor((new Date(expiresAt) - new Date()) / 1000),
568
+ });
569
+
570
+ console.log("\n✅ Tokens injected successfully!");
571
+ console.log("📋 Token Details:");
572
+ console.log(` Access Token: ${accessToken.substring(0, 20)}...`);
573
+ console.log(` Refresh Token: ${refreshToken.substring(0, 20)}...`);
574
+ console.log(` Expires At: ${expiresAt}`);
575
+
576
+ const minutesUntilExpiry = Math.floor((new Date(expiresAt) - new Date()) / 60000);
577
+ console.log(` Time Until Expiry: ${minutesUntilExpiry} minutes`);
578
+
579
+ console.log("\n💡 You can now use the SDK with these tokens.");
580
+ console.log(" Test with: npm run cli whoami");
581
+ } catch (err) {
582
+ console.error("❌ Failed to inject tokens:", err.message);
583
+ process.exit(1);
584
+ } finally {
585
+ await cleanupStorageBackend(storageBackend);
586
+ }
587
+ });
588
+
425
589
  program
426
590
  .command("migrate-tokens")
427
591
  .description(
@@ -291,6 +291,81 @@ class LightspeedSDKCore {
291
291
  this.refreshInProgress = false; // Release the lock
292
292
  }
293
293
  }
294
+ // Core API request handler
295
+ async executeApiRequest(options, retries = 0) {
296
+ await this.handleRateLimit(options);
297
+ const token = await this.getToken();
298
+ if (!token) throw new Error("Error Fetching Token");
299
+ options.headers = {
300
+ Authorization: `Bearer ${token}`,
301
+ "Content-Type": "application/json",
302
+ ...options.headers
303
+ };
304
+ // Centralized query param handling
305
+ if (options.params) {
306
+ const queryString = buildQueryParams(options.params);
307
+ if (queryString) {
308
+ // Remove any trailing ? or & from url
309
+ options.url = options.url.replace(/[?&]+$/, "");
310
+ options.url += (options.url.includes("?") ? "&" : "?") + queryString;
311
+ }
312
+ delete options.params; // Don't let axios try to re-encode
313
+ }
314
+ try {
315
+ const res = await (0, _axios.default)(options);
316
+ this.lastResponse = res;
317
+ if (options.method === "GET") {
318
+ var _res_data_attributes, _res_data_attributes1;
319
+ // Handle successful response with no data or empty data
320
+ if (!res.data || Object.keys(res.data).length === 0) {
321
+ return {
322
+ data: {},
323
+ next: null,
324
+ previous: null
325
+ };
326
+ }
327
+ // Check if response has the expected structure but with empty arrays
328
+ const dataKeys = Object.keys(res.data).filter((key)=>key !== "@attributes");
329
+ if (dataKeys.length > 0) {
330
+ const firstDataKey = dataKeys[0];
331
+ const firstDataValue = res.data[firstDataKey];
332
+ // No need to log for empty arrays - this is normal
333
+ }
334
+ // Handle successful response with data
335
+ return {
336
+ data: res.data,
337
+ next: (_res_data_attributes = res.data["@attributes"]) === null || _res_data_attributes === void 0 ? void 0 : _res_data_attributes.next,
338
+ previous: (_res_data_attributes1 = res.data["@attributes"]) === null || _res_data_attributes1 === void 0 ? void 0 : _res_data_attributes1.prev
339
+ };
340
+ } else {
341
+ return res.data;
342
+ }
343
+ } catch (err) {
344
+ var _err_response;
345
+ // Handle 401 auth errors with automatic retry
346
+ if (((_err_response = err.response) === null || _err_response === void 0 ? void 0 : _err_response.status) === 401 && !options._authRetryAttempted) {
347
+ console.log("🔄 401 error - forcing token refresh and retrying...");
348
+ options._authRetryAttempted = true;
349
+ this.token = null;
350
+ try {
351
+ await this.refreshTokens();
352
+ return this.executeApiRequest(options, retries);
353
+ } catch (refreshError) {
354
+ console.error("Failed to refresh tokens:", refreshError.message);
355
+ throw refreshError;
356
+ }
357
+ }
358
+ // Handle retryable errors
359
+ if (this.isRetryableError(err) && retries < this.maxRetries) {
360
+ this.handleError(`Network Error Retrying in 2 seconds...`, err.message, false);
361
+ await sleep(2000);
362
+ return this.executeApiRequest(options, retries + 1);
363
+ } else {
364
+ // Simple error handling - let the calling method decide how to handle it
365
+ throw err;
366
+ }
367
+ }
368
+ }
294
369
  // Paginated data fetching
295
370
  async getAllData(options) {
296
371
  var _options_params;
@@ -320,6 +320,97 @@ export class LightspeedSDKCore {
320
320
  }
321
321
  }
322
322
 
323
+ // Core API request handler
324
+ async executeApiRequest(options, retries = 0) {
325
+ await this.handleRateLimit(options);
326
+
327
+ const token = await this.getToken();
328
+ if (!token) throw new Error("Error Fetching Token");
329
+
330
+ options.headers = {
331
+ Authorization: `Bearer ${token}`,
332
+ "Content-Type": "application/json",
333
+ ...options.headers,
334
+ };
335
+
336
+ // Centralized query param handling
337
+ if (options.params) {
338
+ const queryString = buildQueryParams(options.params);
339
+ if (queryString) {
340
+ // Remove any trailing ? or & from url
341
+ options.url = options.url.replace(/[?&]+$/, "");
342
+ options.url += (options.url.includes("?") ? "&" : "?") + queryString;
343
+ }
344
+ delete options.params; // Don't let axios try to re-encode
345
+ }
346
+
347
+ try {
348
+ const res = await axios(options);
349
+ this.lastResponse = res;
350
+
351
+ if (options.method === "GET") {
352
+ // Handle successful response with no data or empty data
353
+ if (!res.data || Object.keys(res.data).length === 0) {
354
+ return {
355
+ data: {},
356
+ next: null,
357
+ previous: null,
358
+ };
359
+ }
360
+
361
+ // Check if response has the expected structure but with empty arrays
362
+ const dataKeys = Object.keys(res.data).filter(
363
+ (key) => key !== "@attributes"
364
+ );
365
+ if (dataKeys.length > 0) {
366
+ const firstDataKey = dataKeys[0];
367
+ const firstDataValue = res.data[firstDataKey];
368
+
369
+ // No need to log for empty arrays - this is normal
370
+ }
371
+
372
+ // Handle successful response with data
373
+ return {
374
+ data: res.data,
375
+ next: res.data["@attributes"]?.next,
376
+ previous: res.data["@attributes"]?.prev,
377
+ };
378
+ } else {
379
+ return res.data;
380
+ }
381
+ } catch (err) {
382
+ // Handle 401 auth errors with automatic retry
383
+ if (err.response?.status === 401 && !options._authRetryAttempted) {
384
+ console.log("🔄 401 error - forcing token refresh and retrying...");
385
+
386
+ options._authRetryAttempted = true;
387
+ this.token = null;
388
+
389
+ try {
390
+ await this.refreshTokens();
391
+ return this.executeApiRequest(options, retries);
392
+ } catch (refreshError) {
393
+ console.error("Failed to refresh tokens:", refreshError.message);
394
+ throw refreshError;
395
+ }
396
+ }
397
+
398
+ // Handle retryable errors
399
+ if (this.isRetryableError(err) && retries < this.maxRetries) {
400
+ this.handleError(
401
+ `Network Error Retrying in 2 seconds...`,
402
+ err.message,
403
+ false
404
+ );
405
+ await sleep(2000);
406
+ return this.executeApiRequest(options, retries + 1);
407
+ } else {
408
+ // Simple error handling - let the calling method decide how to handle it
409
+ throw err;
410
+ }
411
+ }
412
+ }
413
+
323
414
  // Paginated data fetching
324
415
  async getAllData(options) {
325
416
  let allData = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightspeed-retail-sdk",
3
- "version": "3.3.4",
3
+ "version": "3.4.0",
4
4
  "description": "Another unofficial Lightspeed Retail API SDK for Node.js",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",