lightspeed-retail-sdk 3.0.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A JavaScript SDK for interacting with the Lightspeed Retail API. This SDK provides a convenient way to access Lightspeed Retail's functionalities, including customer, item, order management, and more.
4
4
 
5
- **Current Version: 3.0.0** - Updated with new OAuth system support and enhanced token management.
5
+ **Current Version: 3.0.2** - Updated with new OAuth system support and enhanced token management.
6
6
 
7
7
  ## 🚨 Important Update - New OAuth System
8
8
 
@@ -104,6 +104,57 @@ const api = new LightspeedRetailSDK({
104
104
  export default api;
105
105
  ```
106
106
 
107
+ #### Encrypted Storage (Recommended)
108
+
109
+ You can now store your tokens **encrypted at rest** using the built-in `EncryptedTokenStorage` class. This works as a wrapper around any storage adapter (such as `FileTokenStorage`) and uses AES-256-GCM encryption with a key you provide.
110
+
111
+ **Generate a key** (if you haven't already):
112
+
113
+ ```bash
114
+ npm run generate-key
115
+ ```
116
+
117
+ Add the generated key to your `.env` file:
118
+
119
+ ```env
120
+ LIGHTSPEED_ENCRYPTION_KEY=your_64_char_hex_key_here
121
+ ```
122
+
123
+ **Usage Example:**
124
+
125
+ ```javascript
126
+ import LightspeedRetailSDK, { FileTokenStorage } from "lightspeed-retail-sdk";
127
+ import { EncryptedTokenStorage } from "lightspeed-retail-sdk/src/storage/TokenStorage.mjs";
128
+ import dotenv from "dotenv";
129
+ dotenv.config();
130
+
131
+ const fileStorage = new FileTokenStorage("./lightspeed-tokens.json");
132
+ const encryptionKey = process.env.LIGHTSPEED_ENCRYPTION_KEY;
133
+
134
+ const tokenStorage = encryptionKey
135
+ ? new EncryptedTokenStorage(fileStorage, encryptionKey)
136
+ : fileStorage;
137
+
138
+ const api = new LightspeedRetailSDK({
139
+ accountID: "Your Account No.",
140
+ clientID: "Your client ID.",
141
+ clientSecret: "Your client secret.",
142
+ refreshToken: "Your initial refresh token.",
143
+ tokenStorage,
144
+ });
145
+
146
+ export default api;
147
+ ```
148
+
149
+ - If `LIGHTSPEED_ENCRYPTION_KEY` is set, your tokens will be encrypted transparently.
150
+ - If not, it falls back to plain file storage (backward compatible).
151
+ - The encryption uses AES-256-GCM and is fully compatible with existing token files (it will auto-detect and migrate as needed).
152
+
153
+ **Note:**
154
+ Keep your encryption key secure and never commit it to version control!
155
+
156
+ ---
157
+
107
158
  #### Database Storage (Built-in Base Class)
108
159
 
109
160
  The SDK provides a `DatabaseTokenStorage` base class that you can extend:
@@ -12,6 +12,9 @@ _export(exports, {
12
12
  get DatabaseTokenStorage () {
13
13
  return DatabaseTokenStorage;
14
14
  },
15
+ get EncryptedTokenStorage () {
16
+ return EncryptedTokenStorage;
17
+ },
15
18
  get FileTokenStorage () {
16
19
  return FileTokenStorage;
17
20
  },
@@ -24,6 +27,7 @@ _export(exports, {
24
27
  });
25
28
  const _fs = require("fs");
26
29
  const _path = /*#__PURE__*/ _interop_require_default(require("path"));
30
+ const _crypto = /*#__PURE__*/ _interop_require_default(require("crypto"));
27
31
  function _interop_require_default(obj) {
28
32
  return obj && obj.__esModule ? obj : {
29
33
  default: obj
@@ -37,6 +41,66 @@ class TokenStorage {
37
41
  throw new Error("setTokens() must be implemented by subclass");
38
42
  }
39
43
  }
44
+ class EncryptedTokenStorage {
45
+ async getTokens() {
46
+ const encrypted = await this.adapter.getTokens();
47
+ // Backwards compatibility: if it's a plain object with access_token, return as-is
48
+ if (encrypted && typeof encrypted === "object" && encrypted.access_token) {
49
+ return encrypted;
50
+ }
51
+ // Otherwise, expect { iv, tag, data }
52
+ if (encrypted && typeof encrypted === "object" && encrypted.iv && encrypted.tag && encrypted.data) {
53
+ try {
54
+ const iv = Buffer.from(encrypted.iv, "hex");
55
+ const tag = Buffer.from(encrypted.tag, "hex");
56
+ const encryptedData = Buffer.from(encrypted.data, "hex");
57
+ const decipher = _crypto.default.createDecipheriv(this.algorithm, this.key, iv);
58
+ decipher.setAuthTag(tag);
59
+ let decrypted = decipher.update(encryptedData, undefined, "utf8");
60
+ decrypted += decipher.final("utf8");
61
+ return JSON.parse(decrypted);
62
+ } catch (err) {
63
+ throw new Error("Failed to decrypt tokens: " + err.message);
64
+ }
65
+ }
66
+ // If file is empty or not recognized, return empty object
67
+ return {};
68
+ }
69
+ async setTokens(tokens) {
70
+ // Backwards compatibility: if tokens are already encrypted, just store as-is
71
+ if (tokens && typeof tokens === "object" && tokens.iv && tokens.tag && tokens.data) {
72
+ return this.adapter.setTokens(tokens);
73
+ }
74
+ // Encrypt the tokens object
75
+ const iv = _crypto.default.randomBytes(12); // 96 bits for GCM
76
+ const cipher = _crypto.default.createCipheriv(this.algorithm, this.key, iv);
77
+ let encrypted = cipher.update(JSON.stringify(tokens), "utf8");
78
+ encrypted = Buffer.concat([
79
+ encrypted,
80
+ cipher.final()
81
+ ]);
82
+ const tag = cipher.getAuthTag();
83
+ const encryptedPayload = {
84
+ iv: iv.toString("hex"),
85
+ tag: tag.toString("hex"),
86
+ data: encrypted.toString("hex")
87
+ };
88
+ return this.adapter.setTokens(encryptedPayload);
89
+ }
90
+ constructor(storageAdapter, encryptionKey){
91
+ this.adapter = storageAdapter;
92
+ // Accept hex string or Buffer
93
+ if (typeof encryptionKey === "string") {
94
+ this.key = Buffer.from(encryptionKey, "hex");
95
+ } else {
96
+ this.key = encryptionKey;
97
+ }
98
+ if (this.key.length !== 32) {
99
+ throw new Error("Encryption key must be 32 bytes (256 bits)");
100
+ }
101
+ this.algorithm = "aes-256-gcm";
102
+ }
103
+ }
40
104
  class InMemoryTokenStorage extends TokenStorage {
41
105
  async getTokens() {
42
106
  return this.tokens;
@@ -1,5 +1,6 @@
1
1
  import { promises as fs } from "fs";
2
2
  import path from "path";
3
+ import crypto from "crypto";
3
4
 
4
5
  // Base class for token storage
5
6
  export class TokenStorage {
@@ -12,6 +13,90 @@ export class TokenStorage {
12
13
  }
13
14
  }
14
15
 
16
+ /**
17
+ * @param {TokenStorage} storageAdapter - Any TokenStorage instance (e.g., FileTokenStorage)
18
+ * @param {string} encryptionKey - 32-byte (256-bit) key as a hex string or Buffer
19
+ */
20
+ export class EncryptedTokenStorage {
21
+ constructor(storageAdapter, encryptionKey) {
22
+ this.adapter = storageAdapter;
23
+ // Accept hex string or Buffer
24
+ if (typeof encryptionKey === "string") {
25
+ this.key = Buffer.from(encryptionKey, "hex");
26
+ } else {
27
+ this.key = encryptionKey;
28
+ }
29
+ if (this.key.length !== 32) {
30
+ throw new Error("Encryption key must be 32 bytes (256 bits)");
31
+ }
32
+ this.algorithm = "aes-256-gcm";
33
+ }
34
+
35
+ async getTokens() {
36
+ const encrypted = await this.adapter.getTokens();
37
+
38
+ // Backwards compatibility: if it's a plain object with access_token, return as-is
39
+ if (encrypted && typeof encrypted === "object" && encrypted.access_token) {
40
+ return encrypted;
41
+ }
42
+
43
+ // Otherwise, expect { iv, tag, data }
44
+ if (
45
+ encrypted &&
46
+ typeof encrypted === "object" &&
47
+ encrypted.iv &&
48
+ encrypted.tag &&
49
+ encrypted.data
50
+ ) {
51
+ try {
52
+ const iv = Buffer.from(encrypted.iv, "hex");
53
+ const tag = Buffer.from(encrypted.tag, "hex");
54
+ const encryptedData = Buffer.from(encrypted.data, "hex");
55
+
56
+ const decipher = crypto.createDecipheriv(this.algorithm, this.key, iv);
57
+ decipher.setAuthTag(tag);
58
+ let decrypted = decipher.update(encryptedData, undefined, "utf8");
59
+ decrypted += decipher.final("utf8");
60
+ return JSON.parse(decrypted);
61
+ } catch (err) {
62
+ throw new Error("Failed to decrypt tokens: " + err.message);
63
+ }
64
+ }
65
+
66
+ // If file is empty or not recognized, return empty object
67
+ return {};
68
+ }
69
+
70
+ async setTokens(tokens) {
71
+ // Backwards compatibility: if tokens are already encrypted, just store as-is
72
+ if (
73
+ tokens &&
74
+ typeof tokens === "object" &&
75
+ tokens.iv &&
76
+ tokens.tag &&
77
+ tokens.data
78
+ ) {
79
+ return this.adapter.setTokens(tokens);
80
+ }
81
+
82
+ // Encrypt the tokens object
83
+ const iv = crypto.randomBytes(12); // 96 bits for GCM
84
+ const cipher = crypto.createCipheriv(this.algorithm, this.key, iv);
85
+
86
+ let encrypted = cipher.update(JSON.stringify(tokens), "utf8");
87
+ encrypted = Buffer.concat([encrypted, cipher.final()]);
88
+ const tag = cipher.getAuthTag();
89
+
90
+ const encryptedPayload = {
91
+ iv: iv.toString("hex"),
92
+ tag: tag.toString("hex"),
93
+ data: encrypted.toString("hex"),
94
+ };
95
+
96
+ return this.adapter.setTokens(encryptedPayload);
97
+ }
98
+ }
99
+
15
100
  // In-memory storage (fallback)
16
101
  export class InMemoryTokenStorage extends TokenStorage {
17
102
  constructor() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightspeed-retail-sdk",
3
- "version": "3.0.1",
3
+ "version": "3.1.0",
4
4
  "description": "Another unofficial Lightspeed Retail API SDK for Node.js",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -25,7 +25,9 @@
25
25
  "build:main": "swc index.mjs -o dist/index.cjs",
26
26
  "fix-paths": "find dist -name '*.cjs' -exec sed -i '' 's/\\.mjs/.cjs/g' {} +",
27
27
  "prebuild": "mkdir -p dist/src/core dist/src/storage",
28
- "clean": "rm -rf dist"
28
+ "clean": "rm -rf dist",
29
+ "generate-key": "node scripts/generate-key.js",
30
+ "publish": "npm run build && npm publish"
29
31
  },
30
32
  "files": [
31
33
  "dist/",