lightspeed-retail-sdk 3.3.5 → 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.5** — 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.5)**
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
@@ -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(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightspeed-retail-sdk",
3
- "version": "3.3.5",
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",