lightspeed-retail-sdk 3.3.5 → 3.4.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/README.md CHANGED
@@ -2,9 +2,18 @@
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.1** — Auto-discovery system for seamless cron job and production deployment.
6
6
 
7
- ## **🆕 Recent Updates (v3.3.5)**
7
+ ## **🆕 Recent Updates (v3.4.0)**
8
+
9
+ - **🤖 Auto-Discovery System**: SDK automatically discovers storage configuration after CLI setup - perfect for cron jobs and automated scripts
10
+ - **🔧 Enhanced Token Injection**: Interactive `inject-tokens` command with prompts for access/refresh tokens, expiry settings, and storage backend selection
11
+ - **🏭 Production Environment Support**: Login command supports headless environments with `--no-browser` option
12
+ - **📁 Storage Configuration Management**: Automatic saving and updating of storage configurations during CLI operations
13
+ - **🔄 Migration Transparency**: Storage migrations automatically update all scripts to use new storage
14
+ - **💡 Improved CLI UX**: Better error messages, token validation, and clear instructions for manual OAuth flows
15
+
16
+ ## **Previous Updates (v3.3.5)**
8
17
 
9
18
  - **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
19
  - **🎯 Enhanced Parameter Support**: All main getter methods now support both legacy and new object-based parameters with full backward compatibility
@@ -20,12 +29,14 @@ A modern JavaScript SDK for interacting with the Lightspeed Retail API. This SDK
20
29
 
21
30
  ## 🚀 Key Features
22
31
 
23
- - **Modern API**: Object-based parameters with full backward compatibility
24
- - **Timestamp Filtering**: Get only records updated since a specific time
25
- - **Robust Error Handling**: Clean, silent error handling with consistent return types
26
- - **Enhanced CLI**: Browser selection, default scopes, and improved authentication
27
- - **Multiple Storage Options**: File, encrypted, database, and in-memory token storage
28
- - **Comprehensive Coverage**: 20+ API methods with consistent interfaces
32
+ - **🤖 Auto-Discovery**: Zero-configuration after CLI setup - perfect for cron jobs and production
33
+ - **🔄 Modern API**: Object-based parameters with full backward compatibility
34
+ - **🕒 Timestamp Filtering**: Get only records updated since a specific time
35
+ - **🛡️ Robust Error Handling**: Clean, silent error handling with consistent return types
36
+ - **🎯 Enhanced CLI**: Interactive setup with storage selection and headless support
37
+ - **🔒 Multiple Storage Options**: File, encrypted, database, and in-memory token storage
38
+ - **📊 Comprehensive Coverage**: 20+ API methods with consistent interfaces
39
+ - **⚡ Seamless Token Management**: Automatic token refresh with failure notifications
29
40
 
30
41
  ## 🔄 Migrating from 3.1.x
31
42
 
@@ -66,7 +77,8 @@ const items = await sdk.getItems({
66
77
  ## Table of Contents
67
78
 
68
79
  - [Another Unofficial Lightspeed Retail V3 API SDK](#another-unofficial-lightspeed-retail-v3-api-sdk)
69
- - [**🆕 Recent Updates (v3.3.5)**](#-recent-updates-v335)
80
+ - [**🆕 Recent Updates (v3.4.0)**](#-recent-updates-v340)
81
+ - [**Previous Updates (v3.3.5)**](#previous-updates-v335)
70
82
  - [🚀 Key Features](#-key-features)
71
83
  - [🔄 Migrating from 3.1.x](#-migrating-from-31x)
72
84
  - [Backward Compatibility](#backward-compatibility)
@@ -105,8 +117,12 @@ const items = await sdk.getItems({
105
117
  - [Quick Start](#quick-start)
106
118
  - [Modern CLI-First Approach (Recommended)](#modern-cli-first-approach-recommended)
107
119
  - [Alternative: Local Installation](#alternative-local-installation)
108
- - [Basic Usage (In-Memory Storage)](#basic-usage-in-memory-storage)
120
+ - [Recommended Usage (Auto-Discovery)](#recommended-usage-auto-discovery)
121
+ - [Explicit Storage (Advanced)](#explicit-storage-advanced)
122
+ - [Production \& Cron Job Setup](#production--cron-job-setup)
109
123
  - [Manual Token Management (Advanced)](#manual-token-management-advanced)
124
+ - [Option 1: Interactive Token Injection (Easiest)](#option-1-interactive-token-injection-easiest)
125
+ - [Option 2: Programmatic Token Storage](#option-2-programmatic-token-storage)
110
126
  - [File-Based Storage](#file-based-storage)
111
127
  - [Encrypted Storage (Recommended)](#encrypted-storage-recommended)
112
128
  - [Database Storage (PostgreSQL, SQLite, and MongoDB)](#database-storage-postgresql-sqlite-and-mongodb)
@@ -197,6 +213,9 @@ lightspeed-retail-sdk login --browser firefox
197
213
  lightspeed-retail-sdk login --browser "google chrome"
198
214
  lightspeed-retail-sdk login --browser safari
199
215
 
216
+ # Production/headless environment - display URL without opening browser
217
+ lightspeed-retail-sdk login --no-browser
218
+
200
219
  # Check current token status
201
220
  lightspeed-retail-sdk token-status
202
221
 
@@ -205,6 +224,12 @@ lightspeed-retail-sdk refresh-token
205
224
 
206
225
  # View your account information
207
226
  lightspeed-retail-sdk whoami
227
+
228
+ # Manually inject access and refresh tokens (interactive)
229
+ lightspeed-retail-sdk inject-tokens
230
+
231
+ # Manually inject tokens with command-line options
232
+ lightspeed-retail-sdk inject-tokens --access "your_access_token" --refresh "your_refresh_token"
208
233
  ```
209
234
 
210
235
  #### Storage Management
@@ -249,10 +274,11 @@ lightspeed-retail-sdk login
249
274
  The login process:
250
275
 
251
276
  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
277
+ 2. Optionally lets you choose a specific browser (or skips browser opening in production)
278
+ 3. Opens browser for OAuth authorization (or displays URL for manual access)
279
+ 4. Waits for you to paste the authorization code from the redirect URL
280
+ 5. Automatically exchanges code for tokens
281
+ 6. Stores tokens in your chosen backend
256
282
 
257
283
  **Note**: If no scopes are specified via environment variables or user input, the default scope `employee:all` will be used.
258
284
 
@@ -270,6 +296,22 @@ lightspeed-retail-sdk login --browser "google chrome"
270
296
  # The CLI will ask if you want to choose a specific browser
271
297
  ```
272
298
 
299
+ **Production/Headless Environments:**
300
+
301
+ ```bash
302
+ # Skip browser opening and display URL for manual access
303
+ lightspeed-retail-sdk login --no-browser
304
+ ```
305
+
306
+ This is useful when:
307
+
308
+ - Running in Docker containers or cloud environments without GUI
309
+ - SSH sessions without X11 forwarding
310
+ - Automated deployment scripts
311
+ - CI/CD pipelines
312
+
313
+ The CLI will automatically detect headless environments and display the OAuth URL instead of trying to open a browser.
314
+
273
315
  #### Token Management
274
316
 
275
317
  ##### Manual Token Refresh
@@ -608,54 +650,119 @@ npx lightspeed-retail-sdk login
608
650
  Then in your code:
609
651
 
610
652
  ```javascript
611
- import LightspeedRetailSDK, {
612
- FileTokenStorage,
613
- EncryptedTokenStorage,
614
- } from "lightspeed-retail-sdk";
615
- import dotenv from "dotenv";
616
- dotenv.config();
617
-
618
- // Use the same storage configuration as your CLI
619
- const fileStorage = new FileTokenStorage(
620
- process.env.LIGHTSPEED_TOKEN_FILE || "./tokens/encrypted-tokens.json"
621
- );
622
- const tokenStorage = process.env.LIGHTSPEED_ENCRYPTION_KEY
623
- ? new EncryptedTokenStorage(
624
- fileStorage,
625
- process.env.LIGHTSPEED_ENCRYPTION_KEY
626
- )
627
- : fileStorage;
653
+ import LightspeedRetailSDK from "lightspeed-retail-sdk";
628
654
 
655
+ // After CLI setup, your code is this simple:
629
656
  const api = new LightspeedRetailSDK({
630
- accountID: process.env.LIGHTSPEED_ACCOUNT_ID,
631
657
  clientID: process.env.LIGHTSPEED_CLIENT_ID,
632
658
  clientSecret: process.env.LIGHTSPEED_CLIENT_SECRET,
633
- tokenStorage,
659
+ accountID: process.env.LIGHTSPEED_ACCOUNT_ID,
660
+ // Storage automatically discovered from CLI setup!
634
661
  });
635
662
 
636
- // The SDK will automatically use stored tokens and refresh as needed
663
+ // The SDK automatically finds your tokens and refreshes them as needed
664
+ const items = await api.getItems();
637
665
  export default api;
638
666
  ```
639
667
 
640
- ### Basic Usage (In-Memory Storage)
668
+ ### Recommended Usage (Auto-Discovery)
669
+
670
+ **Best for most users**: After one-time CLI setup, no configuration needed:
641
671
 
642
672
  ```javascript
643
673
  import LightspeedRetailSDK from "lightspeed-retail-sdk";
644
674
 
675
+ // After running: npm run cli login (one time setup)
645
676
  const api = new LightspeedRetailSDK({
646
- accountID: "Your Account No.",
647
- clientID: "Your client ID.",
648
- clientSecret: "Your client secret.",
649
- refreshToken: "Your initial refresh token.",
650
- // No tokenStorage = uses InMemoryTokenStorage by default
677
+ clientID: process.env.LIGHTSPEED_CLIENT_ID,
678
+ clientSecret: process.env.LIGHTSPEED_CLIENT_SECRET,
679
+ accountID: process.env.LIGHTSPEED_ACCOUNT_ID,
680
+ // No tokenStorage needed - automatically discovered!
651
681
  });
682
+
683
+ // Works immediately, handles token refresh automatically
684
+ const items = await api.getItems();
652
685
  ```
653
686
 
654
- ⚠️ **Warning**: Basic usage stores tokens in memory only. Tokens will be lost on application restart, which may cause issues with Lightspeed's new token rotation system.
687
+ ### Explicit Storage (Advanced)
688
+
689
+ For advanced use cases where you want to specify storage manually:
690
+
691
+ ```javascript
692
+ import LightspeedRetailSDK, {
693
+ FileTokenStorage,
694
+ EncryptedTokenStorage
695
+ } from "lightspeed-retail-sdk";
696
+
697
+ const fileStorage = new FileTokenStorage('./lightspeed-tokens.json');
698
+ const tokenStorage = process.env.LIGHTSPEED_ENCRYPTION_KEY
699
+ ? new EncryptedTokenStorage(fileStorage, process.env.LIGHTSPEED_ENCRYPTION_KEY)
700
+ : fileStorage;
701
+
702
+ const api = new LightspeedRetailSDK({
703
+ clientID: process.env.LIGHTSPEED_CLIENT_ID,
704
+ clientSecret: process.env.LIGHTSPEED_CLIENT_SECRET,
705
+ accountID: process.env.LIGHTSPEED_ACCOUNT_ID,
706
+ tokenStorage: tokenStorage, // Explicit storage choice
707
+ });
708
+ ```
709
+
710
+ ✅ **Auto-Discovery**: The SDK automatically discovers your storage configuration after initial CLI setup. Perfect for cron jobs and automated scripts.
711
+
712
+ ✅ **Explicit Storage**: You can still explicitly provide tokenStorage for advanced use cases. Choose from FileTokenStorage, EncryptedTokenStorage (recommended), DatabaseTokenStorage, or InMemoryTokenStorage (not recommended).
713
+
714
+ ### Production & Cron Job Setup
715
+
716
+ The auto-discovery system is perfect for production environments and automated scripts:
717
+
718
+ 1. **Initial Setup** (run once):
719
+
720
+ ```bash
721
+ # Interactive setup with storage selection
722
+ npm run cli login
723
+
724
+ # Or for headless environments:
725
+ npm run cli login --no-browser
726
+ ```
727
+
728
+ 2. **Your Scripts Work Automatically**:
729
+ - Cron jobs find storage configuration automatically
730
+ - Token refresh happens seamlessly in background
731
+ - Storage migrations update all scripts automatically
732
+ - No configuration management needed
733
+
734
+ 3. **Environment Variables** (recommended):
735
+
736
+ ```bash
737
+ export LIGHTSPEED_CLIENT_ID="your-client-id"
738
+ export LIGHTSPEED_CLIENT_SECRET="your-client-secret"
739
+ export LIGHTSPEED_ACCOUNT_ID="your-account-id"
740
+ ```
655
741
 
656
742
  ### Manual Token Management (Advanced)
657
743
 
658
- If you prefer to handle authentication manually without the CLI:
744
+ If you prefer to handle authentication manually without the CLI, you have several options:
745
+
746
+ #### Option 1: Interactive Token Injection (Easiest)
747
+
748
+ Use the CLI to manually input tokens you've obtained from elsewhere:
749
+
750
+ ```bash
751
+ # Interactive prompts for tokens and storage configuration
752
+ lightspeed-retail-sdk inject-tokens
753
+
754
+ # Or specify tokens via command line and use interactive prompts for storage
755
+ lightspeed-retail-sdk inject-tokens --access "your_access_token" --refresh "your_refresh_token"
756
+ ```
757
+
758
+ This method will:
759
+
760
+ - Prompt you to enter access and refresh tokens
761
+ - Let you choose expiry settings (default 1 hour, custom date, or seconds)
762
+ - Allow you to select your storage backend (file, encrypted file, or database)
763
+ - Store the tokens securely in your chosen backend
764
+
765
+ #### Option 2: Programmatic Token Storage
659
766
 
660
767
  #### File-Based Storage
661
768
 
@@ -5,7 +5,9 @@ import dotenv from "dotenv";
5
5
  import {
6
6
  FileTokenStorage,
7
7
  EncryptedTokenStorage,
8
+ DatabaseTokenStorage,
8
9
  } from "../storage/TokenStorage.mjs";
10
+ import { StorageConfig } from "../storage/StorageConfig.mjs";
9
11
  import LightspeedRetailSDK from "../../index.mjs";
10
12
  import inquirer from "inquirer";
11
13
  import { createInterface } from "readline";
@@ -61,6 +63,10 @@ program
61
63
  "-b, --browser <browser>",
62
64
  "Specify browser to use (chrome, firefox, safari, edge, etc.)"
63
65
  )
66
+ .option(
67
+ "--no-browser",
68
+ "Skip browser opening and display URL for manual access (useful in production/headless environments)"
69
+ )
64
70
  .action(async (options) => {
65
71
  let storageBackend = null;
66
72
  try {
@@ -89,70 +95,83 @@ program
89
95
  clientID
90
96
  )}&scope=${encodeURIComponent(scopes)}`;
91
97
 
92
- console.log("\nOpening browser for authentication...");
98
+ // Detect if we're in a headless/production environment
99
+ const isHeadless = !process.env.DISPLAY && !process.env.SSH_CLIENT && process.platform !== 'darwin' && process.platform !== 'win32';
100
+ const skipBrowser = options.noBrowser || isHeadless;
93
101
 
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}`);
102
+ if (skipBrowser) {
103
+ console.log("\n🔗 OAuth Authentication Required");
104
+ console.log("\nPlease open the following URL in your browser to authorize the application:");
105
+ console.log(`\n${authUrl}\n`);
106
+ console.log("After authorizing, you will be redirected to your configured redirect URI");
107
+ console.log("with an authorization code in the URL (e.g., ?code=abc123...)");
99
108
  } 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
- ]);
109
+ console.log("\nOpening browser for authentication...");
109
110
 
110
- if (chooseBrowser) {
111
- const { browserChoice } = await inquirer.prompt([
111
+ // Open browser with optional browser specification
112
+ const openOptions = {};
113
+ if (options.browser) {
114
+ openOptions.app = { name: options.browser };
115
+ console.log(`Using browser: ${options.browser}`);
116
+ } else {
117
+ // Ask user if they want to choose a specific browser
118
+ const { chooseBrowser } = await inquirer.prompt([
112
119
  {
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
- ],
120
+ type: "confirm",
121
+ name: "chooseBrowser",
122
+ message: "Do you want to choose a specific browser?",
123
+ default: false,
126
124
  },
127
125
  ]);
128
126
 
129
- if (browserChoice === "custom") {
130
- const { customBrowser } = await inquirer.prompt([
127
+ if (chooseBrowser) {
128
+ const { browserChoice } = await inquirer.prompt([
131
129
  {
132
- type: "input",
133
- name: "customBrowser",
134
- message: "Enter browser name:",
135
- validate: (input) =>
136
- input.trim() !== "" || "Browser name cannot be empty",
130
+ type: "list",
131
+ name: "browserChoice",
132
+ message: "Select browser:",
133
+ choices: [
134
+ { name: "Default browser", value: null },
135
+ { name: "Google Chrome", value: "google chrome" },
136
+ { name: "Firefox", value: "firefox" },
137
+ { name: "Safari", value: "safari" },
138
+ { name: "Microsoft Edge", value: "microsoft edge" },
139
+ { name: "Brave", value: "brave" },
140
+ { name: "Opera", value: "opera" },
141
+ { name: "Custom browser name", value: "custom" },
142
+ ],
137
143
  },
138
144
  ]);
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}`);
145
+
146
+ if (browserChoice === "custom") {
147
+ const { customBrowser } = await inquirer.prompt([
148
+ {
149
+ type: "input",
150
+ name: "customBrowser",
151
+ message: "Enter browser name:",
152
+ validate: (input) =>
153
+ input.trim() !== "" || "Browser name cannot be empty",
154
+ },
155
+ ]);
156
+ openOptions.app = { name: customBrowser };
157
+ console.log(`Using browser: ${customBrowser}`);
158
+ } else if (browserChoice) {
159
+ openOptions.app = { name: browserChoice };
160
+ console.log(`Using browser: ${browserChoice}`);
161
+ }
144
162
  }
145
163
  }
146
- }
147
164
 
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);
165
+ try {
166
+ await open(authUrl, openOptions);
167
+ console.log(`\nIf the browser didn't open, manually visit: ${authUrl}`);
168
+ } catch (err) {
169
+ console.log(
170
+ `\n⚠️ Could not open browser automatically: ${err.message}`
171
+ );
172
+ console.log(`\nPlease manually open this URL in your browser:`);
173
+ console.log(`\n${authUrl}\n`);
174
+ }
156
175
  }
157
176
 
158
177
  // 3. Prompt for code
@@ -180,7 +199,7 @@ program
180
199
  ).toISOString();
181
200
 
182
201
  // 5. Store tokens using storage backend
183
- storageBackend = await selectStorageBackend();
202
+ storageBackend = await selectStorageBackend(); // Save config for initial setup
184
203
  await storageBackend.setTokens({
185
204
  access_token: tokenData.access_token,
186
205
  refresh_token: tokenData.refresh_token,
@@ -211,7 +230,7 @@ program
211
230
  .action(async () => {
212
231
  let storageBackend = null;
213
232
  try {
214
- storageBackend = await selectStorageBackend();
233
+ storageBackend = await selectStorageBackend(false); // Read-only, don't save config
215
234
  const tokens = await storageBackend.getTokens();
216
235
  if (!tokens || !tokens.access_token) {
217
236
  console.log("\n⚠️ No tokens found in storage.");
@@ -422,6 +441,153 @@ program
422
441
  }
423
442
  });
424
443
 
444
+ program
445
+ .command("inject-tokens")
446
+ .description("Manually store an access & refresh token in chosen backend")
447
+ .option("--access <accessToken>", "Access token (skip interactive prompt)")
448
+ .option("--refresh <refreshToken>", "Refresh token (skip interactive prompt)")
449
+ .option(
450
+ "--expires-at <isoDatetime>",
451
+ "ISO8601 expiry (e.g. 2025-01-01T12:34:56.000Z). Overrides --expires-in."
452
+ )
453
+ .option(
454
+ "--expires-in <seconds>",
455
+ "Seconds until expiry (default 3600 if neither --expires-at nor --expires-in provided)"
456
+ )
457
+ .action(async (opts) => {
458
+ let storageBackend = null;
459
+ try {
460
+ console.log("\n🔑 Manual Token Injection\n");
461
+ console.log("This command allows you to manually store access and refresh tokens");
462
+ console.log("that you've obtained through other means (e.g., Lightspeed dashboard).\n");
463
+
464
+ // Get tokens either from command line options or interactive prompts
465
+ let accessToken = opts.access;
466
+ let refreshToken = opts.refresh;
467
+
468
+ if (!accessToken) {
469
+ accessToken = await prompt("Enter your access token: ");
470
+ if (!accessToken || accessToken.trim() === "") {
471
+ console.error("❌ Access token is required");
472
+ process.exit(1);
473
+ }
474
+ accessToken = accessToken.trim();
475
+ }
476
+
477
+ if (!refreshToken) {
478
+ refreshToken = await prompt("Enter your refresh token: ");
479
+ if (!refreshToken || refreshToken.trim() === "") {
480
+ console.error("❌ Refresh token is required");
481
+ process.exit(1);
482
+ }
483
+ refreshToken = refreshToken.trim();
484
+ }
485
+
486
+ // Get expiry information
487
+ let expiresAt;
488
+ if (opts.expiresAt) {
489
+ const d = new Date(opts.expiresAt);
490
+ if (isNaN(d.getTime())) {
491
+ console.error("❌ Invalid --expires-at value");
492
+ process.exit(1);
493
+ }
494
+ expiresAt = d.toISOString();
495
+ } else if (opts.expiresIn) {
496
+ const seconds = parseInt(opts.expiresIn, 10);
497
+ if (isNaN(seconds) || seconds <= 0) {
498
+ console.error("❌ Invalid --expires-in value");
499
+ process.exit(1);
500
+ }
501
+ expiresAt = new Date(Date.now() + seconds * 1000).toISOString();
502
+ } else {
503
+ // Interactive prompt for expiry
504
+ const expiryChoice = await inquirer.prompt([
505
+ {
506
+ type: "list",
507
+ name: "expiryMethod",
508
+ message: "How would you like to set the token expiry?",
509
+ choices: [
510
+ { name: "Default (1 hour from now)", value: "default" },
511
+ { name: "Specific date/time (ISO format)", value: "datetime" },
512
+ { name: "Seconds from now", value: "seconds" },
513
+ ],
514
+ default: "default",
515
+ },
516
+ ]);
517
+
518
+ switch (expiryChoice.expiryMethod) {
519
+ case "default":
520
+ expiresAt = new Date(Date.now() + 3600 * 1000).toISOString();
521
+ break;
522
+ case "datetime":
523
+ const { customDateTime } = await inquirer.prompt([
524
+ {
525
+ type: "input",
526
+ name: "customDateTime",
527
+ message: "Enter expiry date/time (ISO format, e.g., 2025-01-01T12:34:56.000Z):",
528
+ validate: (input) => {
529
+ const d = new Date(input);
530
+ return !isNaN(d.getTime()) || "Please enter a valid ISO datetime";
531
+ },
532
+ },
533
+ ]);
534
+ expiresAt = new Date(customDateTime).toISOString();
535
+ break;
536
+ case "seconds":
537
+ const { customSeconds } = await inquirer.prompt([
538
+ {
539
+ type: "input",
540
+ name: "customSeconds",
541
+ message: "Enter seconds until expiry:",
542
+ default: "3600",
543
+ validate: (input) => {
544
+ const num = parseInt(input, 10);
545
+ return (!isNaN(num) && num > 0) || "Please enter a positive number";
546
+ },
547
+ },
548
+ ]);
549
+ expiresAt = new Date(Date.now() + parseInt(customSeconds, 10) * 1000).toISOString();
550
+ break;
551
+ }
552
+ }
553
+
554
+ console.log("\n📁 Token Storage Configuration");
555
+ storageBackend = await selectStorageBackend(); // Save config for inject-tokens setup
556
+
557
+ // Validate tokens format (basic check)
558
+ if (accessToken.length < 10) {
559
+ console.warn("⚠️ Warning: Access token seems unusually short");
560
+ }
561
+ if (refreshToken.length < 10) {
562
+ console.warn("⚠️ Warning: Refresh token seems unusually short");
563
+ }
564
+
565
+ await storageBackend.setTokens({
566
+ access_token: accessToken,
567
+ refresh_token: refreshToken,
568
+ expires_at: expiresAt,
569
+ expires_in: Math.floor((new Date(expiresAt) - new Date()) / 1000),
570
+ });
571
+
572
+ console.log("\n✅ Tokens injected successfully!");
573
+ console.log("📋 Token Details:");
574
+ console.log(` Access Token: ${accessToken.substring(0, 20)}...`);
575
+ console.log(` Refresh Token: ${refreshToken.substring(0, 20)}...`);
576
+ console.log(` Expires At: ${expiresAt}`);
577
+
578
+ const minutesUntilExpiry = Math.floor((new Date(expiresAt) - new Date()) / 60000);
579
+ console.log(` Time Until Expiry: ${minutesUntilExpiry} minutes`);
580
+
581
+ console.log("\n💡 You can now use the SDK with these tokens.");
582
+ console.log(" Test with: npm run cli whoami");
583
+ } catch (err) {
584
+ console.error("❌ Failed to inject tokens:", err.message);
585
+ process.exit(1);
586
+ } finally {
587
+ await cleanupStorageBackend(storageBackend);
588
+ }
589
+ });
590
+
425
591
  program
426
592
  .command("migrate-tokens")
427
593
  .description(
@@ -435,7 +601,7 @@ program
435
601
  console.log("\n🔄 Token Storage Migration Wizard\n");
436
602
  // Prompt for source backend
437
603
  console.log("Select SOURCE storage backend:");
438
- sourceBackend = await selectStorageBackend();
604
+ sourceBackend = await selectStorageBackend(false); // Don't save config for source
439
605
  const sourceTokens = await sourceBackend.getTokens();
440
606
  if (!sourceTokens || !sourceTokens.access_token) {
441
607
  console.error("\n❌ No tokens found in source storage. Aborting.");
@@ -446,7 +612,7 @@ program
446
612
 
447
613
  // Prompt for destination backend
448
614
  console.log("\nSelect DESTINATION storage backend:");
449
- destBackend = await selectStorageBackend();
615
+ destBackend = await selectStorageBackend(false); // Don't save config yet - will save after successful migration
450
616
 
451
617
  // Attempt to create table/collection/file if it doesn't exist (best effort)
452
618
  switch (destBackend.constructor.name) {
@@ -562,6 +728,16 @@ program
562
728
  }
563
729
  await destBackend.setTokens(sourceTokens);
564
730
  console.log("\n🎉 Tokens migrated successfully!");
731
+
732
+ // Update storage configuration to point to the new destination
733
+ try {
734
+ const storageConfig = new StorageConfig();
735
+ const config = await getStorageConfig(destBackend);
736
+ await storageConfig.saveConfig(config);
737
+ console.log("✅ Storage configuration updated for auto-discovery");
738
+ } catch (configError) {
739
+ console.warn(`Warning: Could not update storage config: ${configError.message}`);
740
+ }
565
741
  } catch (error) {
566
742
  console.error("\n❌ Migration failed:", error.message);
567
743
  } finally {
@@ -577,7 +753,7 @@ program
577
753
  .action(async () => {
578
754
  let storageBackend = null;
579
755
  try {
580
- storageBackend = await selectStorageBackend();
756
+ storageBackend = await selectStorageBackend(false); // Read-only, don't save config
581
757
  const tokens = await storageBackend.getTokens();
582
758
  if (!tokens || !tokens.access_token) {
583
759
  console.log("\n⚠️ No tokens found in storage. Please login first.");
@@ -881,7 +1057,70 @@ async function sendTestTokenRefreshFailureEmail(error, accountID) {
881
1057
  }
882
1058
  }
883
1059
 
884
- async function selectStorageBackend() {
1060
+ /**
1061
+ * Extract configuration from a storage instance for saving
1062
+ * @param {TokenStorage} storage - Storage instance
1063
+ * @returns {Object} Configuration object
1064
+ */
1065
+ async function getStorageConfig(storage) {
1066
+ // Handle EncryptedTokenStorage wrapper
1067
+ if (storage.adapter) {
1068
+ const innerConfig = await getStorageConfig(storage.adapter);
1069
+ return {
1070
+ type: innerConfig.type === "file" ? "encrypted-file" : "database",
1071
+ settings: {
1072
+ ...innerConfig.settings,
1073
+ encrypted: true,
1074
+ encryptionKey: process.env.LIGHTSPEED_ENCRYPTION_KEY || "[provided]"
1075
+ }
1076
+ };
1077
+ }
1078
+
1079
+ // Handle FileTokenStorage
1080
+ if (storage.filePath) {
1081
+ return {
1082
+ type: "file",
1083
+ settings: {
1084
+ filePath: storage.filePath
1085
+ }
1086
+ };
1087
+ }
1088
+
1089
+ // Handle DatabaseTokenStorage
1090
+ if (storage.dbConnectionString) {
1091
+ return {
1092
+ type: "database",
1093
+ settings: {
1094
+ connectionString: storage.dbConnectionString,
1095
+ dbType: storage.dbType,
1096
+ tableName: storage.tableName,
1097
+ appId: storage.appId
1098
+ }
1099
+ };
1100
+ }
1101
+
1102
+ throw new Error("Unknown storage type for configuration");
1103
+ }
1104
+
1105
+ async function selectStorageBackend(saveConfig = true) {
1106
+ const storageConfig = new StorageConfig();
1107
+ const storage = await selectStorageBackendInternal();
1108
+
1109
+ // Save configuration for auto-discovery if requested
1110
+ if (saveConfig) {
1111
+ try {
1112
+ const config = await getStorageConfig(storage);
1113
+ await storageConfig.saveConfig(config);
1114
+ console.log("✅ Storage configuration saved for auto-discovery");
1115
+ } catch (error) {
1116
+ console.warn(`Warning: Could not save storage config: ${error.message}`);
1117
+ }
1118
+ }
1119
+
1120
+ return storage;
1121
+ }
1122
+
1123
+ async function selectStorageBackendInternal() {
885
1124
  const { storageType } = await inquirer.prompt([
886
1125
  {
887
1126
  type: "list",
@@ -984,10 +1223,6 @@ async function selectStorageBackend() {
984
1223
  default:
985
1224
  throw new Error("Unsupported database type");
986
1225
  }
987
- // Dynamically import DatabaseTokenStorage
988
- const { DatabaseTokenStorage } = await import(
989
- "../storage/TokenStorage.mjs"
990
- );
991
1226
  const dbStorage = new DatabaseTokenStorage(dbConnectionString, {
992
1227
  dbType,
993
1228
  tableName,
@@ -1075,7 +1310,7 @@ program
1075
1310
  console.log("🔄 Refreshing stored access token...\n");
1076
1311
 
1077
1312
  // Get storage backend
1078
- storageBackend = await selectStorageBackend();
1313
+ storageBackend = await selectStorageBackend(false); // Read-only, don't save config
1079
1314
 
1080
1315
  // Get existing tokens
1081
1316
  const tokens = await storageBackend.getTokens();
@@ -187,6 +187,27 @@ async function sendTokenRefreshFailureEmail(error, accountID) {
187
187
  }
188
188
  }
189
189
  class LightspeedSDKCore {
190
+ /**
191
+ * Initialize token storage through auto-discovery if not explicitly provided
192
+ * @private
193
+ */ async _initializeStorage() {
194
+ if (this._storageInitialized) {
195
+ return;
196
+ }
197
+ if (!this.tokenStorage) {
198
+ try {
199
+ const { autoDiscoverStorage } = await Promise.resolve().then(()=>/*#__PURE__*/ _interop_require_wildcard(require("../storage/StorageConfig.cjs")));
200
+ this.tokenStorage = await autoDiscoverStorage();
201
+ if (!this.tokenStorage) {
202
+ throw new Error("No token storage found. Please either:\n" + "• Run the CLI to set up storage: npm run cli login\n" + "• Or explicitly provide tokenStorage:\n" + " - FileTokenStorage('./tokens.json')\n" + " - EncryptedTokenStorage(fileStorage, key) [recommended]\n" + " - DatabaseTokenStorage(connection, options)\n" + " - InMemoryTokenStorage() [NOT RECOMMENDED]\n\n" + "See documentation: https://github.com/darrylmorley/lightspeed-retail-sdk#token-storage");
203
+ }
204
+ } catch (error) {
205
+ // If auto-discovery fails, throw helpful error
206
+ throw new Error("Failed to auto-discover token storage. " + error.message);
207
+ }
208
+ }
209
+ this._storageInitialized = true;
210
+ }
190
211
  // Core error handling
191
212
  handleError(context, err, shouldThrow = true) {
192
213
  const errorMessage = (err === null || err === void 0 ? void 0 : err.message) || "Unknown error occurred";
@@ -206,6 +227,8 @@ class LightspeedSDKCore {
206
227
  }
207
228
  // Token management
208
229
  async getToken() {
230
+ // Ensure storage is initialized
231
+ await this._initializeStorage();
209
232
  const now = new Date();
210
233
  const bufferTime = 5 * 60 * 1000; // 5-minute buffer
211
234
  const storedTokens = await this.tokenStorage.getTokens();
@@ -479,6 +502,8 @@ class LightspeedSDKCore {
479
502
  }
480
503
  }
481
504
  async getTokenInfo() {
505
+ // Ensure storage is initialized
506
+ await this._initializeStorage();
482
507
  const storedTokens = await this.tokenStorage.getTokens();
483
508
  return {
484
509
  hasAccessToken: !!storedTokens.access_token,
@@ -526,8 +551,14 @@ class LightspeedSDKCore {
526
551
  this.token = null;
527
552
  this.tokenExpiry = null;
528
553
  this.refreshInProgress = false;
529
- // Token storage interface - defaults to in-memory if not provided
530
- this.tokenStorage = tokenStorage || new InMemoryTokenStorage();
554
+ // Token storage interface - can be explicitly provided or auto-discovered
555
+ if (tokenStorage) {
556
+ this.tokenStorage = tokenStorage;
557
+ } else {
558
+ // Auto-discovery will be handled by async initialization
559
+ this.tokenStorage = null;
560
+ this._storageInitialized = false;
561
+ }
531
562
  }
532
563
  }
533
564
  _define_property(LightspeedSDKCore, "BASE_URL", "https://api.lightspeedapp.com/API/V3/Account");
@@ -146,8 +146,51 @@ export class LightspeedSDKCore {
146
146
  this.tokenExpiry = null;
147
147
  this.refreshInProgress = false;
148
148
 
149
- // Token storage interface - defaults to in-memory if not provided
150
- this.tokenStorage = tokenStorage || new InMemoryTokenStorage();
149
+ // Token storage interface - can be explicitly provided or auto-discovered
150
+ if (tokenStorage) {
151
+ this.tokenStorage = tokenStorage;
152
+ } else {
153
+ // Auto-discovery will be handled by async initialization
154
+ this.tokenStorage = null;
155
+ this._storageInitialized = false;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Initialize token storage through auto-discovery if not explicitly provided
161
+ * @private
162
+ */
163
+ async _initializeStorage() {
164
+ if (this._storageInitialized) {
165
+ return;
166
+ }
167
+
168
+ if (!this.tokenStorage) {
169
+ try {
170
+ const { autoDiscoverStorage } = await import("../storage/StorageConfig.mjs");
171
+ this.tokenStorage = await autoDiscoverStorage();
172
+
173
+ if (!this.tokenStorage) {
174
+ throw new Error(
175
+ "No token storage found. Please either:\n" +
176
+ "• Run the CLI to set up storage: npm run cli login\n" +
177
+ "• Or explicitly provide tokenStorage:\n" +
178
+ " - FileTokenStorage('./tokens.json')\n" +
179
+ " - EncryptedTokenStorage(fileStorage, key) [recommended]\n" +
180
+ " - DatabaseTokenStorage(connection, options)\n" +
181
+ " - InMemoryTokenStorage() [NOT RECOMMENDED]\n\n" +
182
+ "See documentation: https://github.com/darrylmorley/lightspeed-retail-sdk#token-storage"
183
+ );
184
+ }
185
+ } catch (error) {
186
+ // If auto-discovery fails, throw helpful error
187
+ throw new Error(
188
+ "Failed to auto-discover token storage. " + error.message
189
+ );
190
+ }
191
+ }
192
+
193
+ this._storageInitialized = true;
151
194
  }
152
195
 
153
196
  // Core error handling
@@ -216,6 +259,9 @@ export class LightspeedSDKCore {
216
259
 
217
260
  // Token management
218
261
  async getToken() {
262
+ // Ensure storage is initialized
263
+ await this._initializeStorage();
264
+
219
265
  const now = new Date();
220
266
  const bufferTime = 5 * 60 * 1000; // 5-minute buffer
221
267
 
@@ -536,6 +582,9 @@ export class LightspeedSDKCore {
536
582
  }
537
583
 
538
584
  async getTokenInfo() {
585
+ // Ensure storage is initialized
586
+ await this._initializeStorage();
587
+
539
588
  const storedTokens = await this.tokenStorage.getTokens();
540
589
  return {
541
590
  hasAccessToken: !!storedTokens.access_token,
@@ -0,0 +1,175 @@
1
+ import { promises as fs } from "fs";
2
+ import path from "path";
3
+
4
+ /**
5
+ * Storage configuration manager that tracks which storage type and settings
6
+ * are being used, enabling automatic storage discovery.
7
+ */
8
+ export class StorageConfig {
9
+ constructor(configPath = ".lightspeed-storage-config.json") {
10
+ this.configPath = path.resolve(configPath);
11
+ }
12
+
13
+ /**
14
+ * Save storage configuration for future auto-discovery
15
+ * @param {Object} config - Storage configuration
16
+ * @param {string} config.type - Storage type: 'file', 'encrypted-file', 'database'
17
+ * @param {Object} config.settings - Storage-specific settings
18
+ */
19
+ async saveConfig(config) {
20
+ try {
21
+ const configData = {
22
+ type: config.type,
23
+ settings: config.settings,
24
+ lastUpdated: new Date().toISOString(),
25
+ version: "1.0"
26
+ };
27
+
28
+ await fs.writeFile(
29
+ this.configPath,
30
+ JSON.stringify(configData, null, 2),
31
+ "utf8"
32
+ );
33
+ } catch (error) {
34
+ console.warn(`Warning: Could not save storage config: ${error.message}`);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Load existing storage configuration
40
+ * @returns {Object|null} Configuration object or null if not found
41
+ */
42
+ async loadConfig() {
43
+ try {
44
+ const data = await fs.readFile(this.configPath, "utf8");
45
+ return JSON.parse(data);
46
+ } catch (error) {
47
+ if (error.code === "ENOENT") {
48
+ return null; // Config file doesn't exist
49
+ }
50
+ console.warn(`Warning: Could not load storage config: ${error.message}`);
51
+ return null;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Check if storage configuration exists
57
+ * @returns {boolean}
58
+ */
59
+ async hasConfig() {
60
+ try {
61
+ await fs.access(this.configPath);
62
+ return true;
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Remove storage configuration
70
+ */
71
+ async removeConfig() {
72
+ try {
73
+ await fs.unlink(this.configPath);
74
+ } catch (error) {
75
+ if (error.code !== "ENOENT") {
76
+ console.warn(`Warning: Could not remove storage config: ${error.message}`);
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Create a token storage instance based on saved configuration
84
+ * @param {Object} config - Storage configuration from StorageConfig
85
+ * @returns {TokenStorage} Configured storage instance
86
+ */
87
+ export async function createStorageFromConfig(config) {
88
+ const {
89
+ FileTokenStorage,
90
+ EncryptedTokenStorage,
91
+ DatabaseTokenStorage
92
+ } = await import("./TokenStorage.mjs");
93
+
94
+ switch (config.type) {
95
+ case "file": {
96
+ return new FileTokenStorage(config.settings.filePath);
97
+ }
98
+
99
+ case "encrypted-file": {
100
+ const fileStorage = new FileTokenStorage(config.settings.filePath);
101
+ return new EncryptedTokenStorage(fileStorage, config.settings.encryptionKey);
102
+ }
103
+
104
+ case "database": {
105
+ const dbStorage = new DatabaseTokenStorage(
106
+ config.settings.connectionString,
107
+ {
108
+ dbType: config.settings.dbType,
109
+ tableName: config.settings.tableName,
110
+ appId: config.settings.appId
111
+ }
112
+ );
113
+
114
+ if (config.settings.encrypted) {
115
+ return new EncryptedTokenStorage(dbStorage, config.settings.encryptionKey);
116
+ }
117
+
118
+ return dbStorage;
119
+ }
120
+
121
+ default:
122
+ throw new Error(`Unknown storage type: ${config.type}`);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Auto-discover and create the appropriate token storage based on
128
+ * previously saved configuration, environment variables, or defaults.
129
+ * @param {string} configPath - Optional path to config file
130
+ * @returns {TokenStorage|null} Storage instance or null if none found
131
+ */
132
+ export async function autoDiscoverStorage(configPath) {
133
+ const storageConfig = new StorageConfig(configPath);
134
+
135
+ // Try to load saved configuration first
136
+ const config = await storageConfig.loadConfig();
137
+ if (config) {
138
+ try {
139
+ return await createStorageFromConfig(config);
140
+ } catch (error) {
141
+ console.warn(`Warning: Could not create storage from saved config: ${error.message}`);
142
+ }
143
+ }
144
+
145
+ // Fallback: Check for environment variables for common setups
146
+ if (process.env.LIGHTSPEED_TOKEN_FILE) {
147
+ const { FileTokenStorage, EncryptedTokenStorage } = await import("./TokenStorage.mjs");
148
+ const fileStorage = new FileTokenStorage(process.env.LIGHTSPEED_TOKEN_FILE);
149
+
150
+ if (process.env.LIGHTSPEED_ENCRYPTION_KEY) {
151
+ return new EncryptedTokenStorage(fileStorage, process.env.LIGHTSPEED_ENCRYPTION_KEY);
152
+ }
153
+
154
+ return fileStorage;
155
+ }
156
+
157
+ // Check for default token file locations
158
+ const defaultPaths = [
159
+ ".lightspeed-tokens.json",
160
+ "./tokens/encrypted-tokens.json",
161
+ "./lightspeed-tokens.json"
162
+ ];
163
+
164
+ for (const defaultPath of defaultPaths) {
165
+ try {
166
+ await fs.access(defaultPath);
167
+ const { FileTokenStorage } = await import("./TokenStorage.mjs");
168
+ return new FileTokenStorage(defaultPath);
169
+ } catch {
170
+ // File doesn't exist, continue to next
171
+ }
172
+ }
173
+
174
+ return null;
175
+ }
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.1",
4
4
  "description": "Another unofficial Lightspeed Retail API SDK for Node.js",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",