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 +149 -42
- package/dist/src/bin/cli.js +297 -62
- package/dist/src/core/LightspeedSDK.cjs +33 -2
- package/dist/src/core/LightspeedSDK.mjs +51 -2
- package/dist/src/storage/StorageConfig.mjs +175 -0
- package/package.json +1 -1
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.
|
|
5
|
+
**Current Version: 3.4.1** — Auto-discovery system for seamless cron job and production deployment.
|
|
6
6
|
|
|
7
|
-
## **🆕 Recent Updates (v3.
|
|
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
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
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.
|
|
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
|
-
- [
|
|
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.
|
|
255
|
-
5.
|
|
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
|
-
|
|
659
|
+
accountID: process.env.LIGHTSPEED_ACCOUNT_ID,
|
|
660
|
+
// Storage automatically discovered from CLI setup!
|
|
634
661
|
});
|
|
635
662
|
|
|
636
|
-
// The SDK
|
|
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
|
-
###
|
|
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
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
-
|
|
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
|
|
package/dist/src/bin/cli.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
console.log(
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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: "
|
|
114
|
-
name: "
|
|
115
|
-
message: "
|
|
116
|
-
|
|
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 (
|
|
130
|
-
const {
|
|
127
|
+
if (chooseBrowser) {
|
|
128
|
+
const { browserChoice } = await inquirer.prompt([
|
|
131
129
|
{
|
|
132
|
-
type: "
|
|
133
|
-
name: "
|
|
134
|
-
message: "
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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 -
|
|
530
|
-
|
|
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 -
|
|
150
|
-
|
|
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
|
+
}
|