n8n-nodes-lemonsqueezy 0.2.0 → 0.5.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 +170 -6
- package/dist/nodes/LemonSqueezy/LemonSqueezy.node.js +37 -5
- package/dist/nodes/LemonSqueezy/LemonSqueezyTrigger.node.js +78 -33
- package/dist/nodes/LemonSqueezy/constants.js +11 -2
- package/dist/nodes/LemonSqueezy/helpers.d.ts +346 -7
- package/dist/nodes/LemonSqueezy/helpers.js +542 -16
- package/dist/nodes/LemonSqueezy/resources/discountRedemption.d.ts +3 -0
- package/dist/nodes/LemonSqueezy/resources/discountRedemption.js +109 -0
- package/dist/nodes/LemonSqueezy/resources/index.d.ts +7 -1
- package/dist/nodes/LemonSqueezy/resources/index.js +46 -1
- package/dist/nodes/LemonSqueezy/resources/licenseKeyInstance.d.ts +3 -0
- package/dist/nodes/LemonSqueezy/resources/licenseKeyInstance.js +102 -0
- package/dist/nodes/LemonSqueezy/resources/orderItem.d.ts +3 -0
- package/dist/nodes/LemonSqueezy/resources/orderItem.js +116 -0
- package/dist/nodes/LemonSqueezy/resources/shared.d.ts +64 -0
- package/dist/nodes/LemonSqueezy/resources/shared.js +196 -0
- package/dist/nodes/LemonSqueezy/resources/subscriptionInvoice.d.ts +3 -0
- package/dist/nodes/LemonSqueezy/resources/subscriptionInvoice.js +129 -0
- package/dist/nodes/LemonSqueezy/resources/usageRecord.d.ts +3 -0
- package/dist/nodes/LemonSqueezy/resources/usageRecord.js +102 -0
- package/dist/nodes/LemonSqueezy/resources/user.d.ts +3 -0
- package/dist/nodes/LemonSqueezy/resources/user.js +26 -0
- package/dist/nodes/LemonSqueezy/types.d.ts +63 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/n8n-nodes-lemonsqueezy)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://github.com/janmaaarc/n8n-nodes-lemonsqueezy/actions/workflows/ci.yml)
|
|
5
6
|
|
|
6
7
|
An [n8n](https://n8n.io/) community node for [Lemon Squeezy](https://lemonsqueezy.com) - a platform for selling digital products, subscriptions, and software licenses.
|
|
7
8
|
|
|
@@ -12,7 +13,11 @@ An [n8n](https://n8n.io/) community node for [Lemon Squeezy](https://lemonsqueez
|
|
|
12
13
|
- **License Key Management** - Validate, activate, and deactivate license keys
|
|
13
14
|
- **Checkout Links** - Create dynamic checkout URLs with custom options
|
|
14
15
|
- **Rate Limiting** - Built-in retry logic with exponential backoff
|
|
16
|
+
- **Input Validation** - RFC 5322 compliant email validation, secure URL validation (blocks internal networks)
|
|
17
|
+
- **Detailed Error Messages** - Descriptive error messages with field-level details
|
|
15
18
|
- **Type Safety** - Full TypeScript support with comprehensive type definitions
|
|
19
|
+
- **Advanced Query Options** - Sorting and relationship expansion for "Get Many" operations
|
|
20
|
+
- **Security Hardened** - Mandatory webhook signature verification with replay attack protection
|
|
16
21
|
|
|
17
22
|
## Installation
|
|
18
23
|
|
|
@@ -51,11 +56,17 @@ The main node for interacting with the Lemon Squeezy API.
|
|
|
51
56
|
| **Checkout** | Create, Get, Get Many |
|
|
52
57
|
| **Customer** | Create, Update, Delete, Get, Get Many |
|
|
53
58
|
| **Discount** | Create, Delete, Get, Get Many |
|
|
59
|
+
| **Discount Redemption** | Get, Get Many |
|
|
54
60
|
| **License Key** | Get, Get Many, Update, Validate, Activate, Deactivate |
|
|
61
|
+
| **License Key Instance** | Get, Get Many |
|
|
55
62
|
| **Order** | Get, Get Many, Refund |
|
|
63
|
+
| **Order Item** | Get, Get Many |
|
|
56
64
|
| **Product** | Get, Get Many |
|
|
57
65
|
| **Store** | Get, Get Many |
|
|
58
66
|
| **Subscription** | Get, Get Many, Update, Cancel, Resume |
|
|
67
|
+
| **Subscription Invoice** | Get, Get Many |
|
|
68
|
+
| **Usage Record** | Get, Get Many |
|
|
69
|
+
| **User** | Get Current |
|
|
59
70
|
| **Variant** | Get, Get Many |
|
|
60
71
|
| **Webhook** | Create, Update, Delete, Get, Get Many |
|
|
61
72
|
|
|
@@ -129,19 +140,87 @@ Most "Get Many" operations support filtering:
|
|
|
129
140
|
| Filter | Description | Available On |
|
|
130
141
|
|--------|-------------|--------------|
|
|
131
142
|
| `storeId` | Filter by store | All resources |
|
|
132
|
-
| `status` | Filter by status | Orders, Subscriptions, Customers, License Keys |
|
|
143
|
+
| `status` | Filter by status | Orders, Subscriptions, Customers, License Keys, Subscription Invoices |
|
|
133
144
|
| `email` | Filter by email | Orders, Customers |
|
|
134
|
-
| `productId` | Filter by product | Subscriptions, License Keys, Variants |
|
|
135
|
-
| `variantId` | Filter by variant | Subscriptions, Checkouts |
|
|
136
|
-
| `orderId` | Filter by order | Subscriptions, License Keys |
|
|
145
|
+
| `productId` | Filter by product | Subscriptions, License Keys, Variants, Order Items |
|
|
146
|
+
| `variantId` | Filter by variant | Subscriptions, Checkouts, Order Items |
|
|
147
|
+
| `orderId` | Filter by order | Subscriptions, License Keys, Order Items, Discount Redemptions |
|
|
148
|
+
| `subscriptionId` | Filter by subscription | Subscription Invoices |
|
|
149
|
+
| `licenseKeyId` | Filter by license key | License Key Instances |
|
|
150
|
+
| `discountId` | Filter by discount | Discount Redemptions |
|
|
151
|
+
|
|
152
|
+
## Advanced Options
|
|
153
|
+
|
|
154
|
+
"Get Many" operations support advanced query options for sorting and including related resources.
|
|
155
|
+
|
|
156
|
+
### Sorting
|
|
157
|
+
|
|
158
|
+
Sort results by any of the following fields:
|
|
159
|
+
|
|
160
|
+
| Sort Field | Description |
|
|
161
|
+
|------------|-------------|
|
|
162
|
+
| `created_at` | Sort by creation date |
|
|
163
|
+
| `updated_at` | Sort by last update date |
|
|
164
|
+
|
|
165
|
+
Choose ascending or descending order.
|
|
166
|
+
|
|
167
|
+
### Relationship Expansion
|
|
168
|
+
|
|
169
|
+
Include related resources in a single request to reduce API calls:
|
|
170
|
+
|
|
171
|
+
| Resource | Available Relationships |
|
|
172
|
+
|----------|------------------------|
|
|
173
|
+
| **Order** | store, customer, order-items, subscriptions, license-keys, discount-redemptions |
|
|
174
|
+
| **Subscription** | store, customer, order, order-item, product, variant |
|
|
175
|
+
| **Customer** | store, orders, subscriptions, license-keys |
|
|
176
|
+
| **License Key** | store, customer, order, order-item, product, license-key-instances |
|
|
177
|
+
| **Product** | store, variants |
|
|
178
|
+
| **Variant** | product, files |
|
|
179
|
+
| **Checkout** | store, variant |
|
|
180
|
+
| **Discount** | store, discount-redemptions |
|
|
181
|
+
|
|
182
|
+
**Example:** When fetching orders, include `customer` and `order-items` to get all related data in one request.
|
|
183
|
+
|
|
184
|
+
## Security
|
|
185
|
+
|
|
186
|
+
### Webhook Security
|
|
187
|
+
|
|
188
|
+
The webhook trigger includes built-in security features:
|
|
189
|
+
|
|
190
|
+
- **Mandatory Signature Verification** - All webhooks are verified using HMAC-SHA256 signatures
|
|
191
|
+
- **Replay Attack Protection** - Events older than the configured threshold (default: 5 minutes) are rejected
|
|
192
|
+
- **Configurable Event Age** - Set `Max Event Age (Minutes)` option (0 to disable)
|
|
193
|
+
|
|
194
|
+
### Input Validation
|
|
195
|
+
|
|
196
|
+
- **Email Validation** - RFC 5322 compliant validation
|
|
197
|
+
- **URL Validation** - Blocks internal/private network URLs to prevent SSRF attacks:
|
|
198
|
+
- localhost, 127.0.0.1, 0.0.0.0
|
|
199
|
+
- Private ranges: 10.x.x.x, 172.16-31.x.x, 192.168.x.x
|
|
200
|
+
- Link-local: 169.254.x.x (AWS metadata endpoint)
|
|
201
|
+
- Only allows http:// and https:// protocols
|
|
137
202
|
|
|
138
203
|
## Error Handling
|
|
139
204
|
|
|
140
|
-
The node includes built-in error handling:
|
|
205
|
+
The node includes built-in error handling with detailed messages:
|
|
141
206
|
|
|
142
207
|
- **Rate Limiting**: Automatically waits and retries when rate limited (429 errors)
|
|
143
208
|
- **Retry Logic**: Retries failed requests with exponential backoff for 5xx errors
|
|
144
209
|
- **Continue on Fail**: Enable to process remaining items even if some fail
|
|
210
|
+
- **Detailed Errors**: Field-level error details for validation failures
|
|
211
|
+
|
|
212
|
+
### Error Code Reference
|
|
213
|
+
|
|
214
|
+
| Status Code | Description |
|
|
215
|
+
|-------------|-------------|
|
|
216
|
+
| 400 | Bad Request - Invalid or malformed request |
|
|
217
|
+
| 401 | Unauthorized - Invalid or missing API key |
|
|
218
|
+
| 403 | Forbidden - No permission to access resource |
|
|
219
|
+
| 404 | Not Found - Resource does not exist |
|
|
220
|
+
| 409 | Conflict - Resource already exists |
|
|
221
|
+
| 422 | Unprocessable Entity - Invalid request data |
|
|
222
|
+
| 429 | Rate Limited - Too many requests |
|
|
223
|
+
| 500+ | Server Error - Something went wrong on the server |
|
|
145
224
|
|
|
146
225
|
## Troubleshooting
|
|
147
226
|
|
|
@@ -166,11 +245,28 @@ The node includes built-in error handling:
|
|
|
166
245
|
|
|
167
246
|
### Rate Limiting Issues
|
|
168
247
|
|
|
169
|
-
The node handles rate limiting automatically
|
|
248
|
+
The node handles rate limiting automatically with the following defaults:
|
|
249
|
+
|
|
250
|
+
| Setting | Value | Description |
|
|
251
|
+
|---------|-------|-------------|
|
|
252
|
+
| Max Retries | 3 | Maximum retry attempts for failed requests |
|
|
253
|
+
| Retry Delay | 1 second | Initial delay between retries (exponential backoff) |
|
|
254
|
+
| Rate Limit Wait | 60 seconds | Wait time when rate limited (429 response) |
|
|
255
|
+
|
|
256
|
+
If you're hitting rate limits frequently:
|
|
170
257
|
|
|
171
258
|
1. Reduce the frequency of API calls
|
|
172
259
|
2. Use "Return All" sparingly for large datasets
|
|
173
260
|
3. Consider caching responses where appropriate
|
|
261
|
+
4. Space out bulk operations with delays
|
|
262
|
+
|
|
263
|
+
### Validation Errors
|
|
264
|
+
|
|
265
|
+
If you receive validation errors:
|
|
266
|
+
|
|
267
|
+
1. Check email fields contain valid email addresses
|
|
268
|
+
2. Verify URLs are complete (including https://)
|
|
269
|
+
3. Ensure dates are in ISO 8601 format (e.g., 2024-01-15T10:30:00Z)
|
|
174
270
|
|
|
175
271
|
## Development
|
|
176
272
|
|
|
@@ -184,11 +280,17 @@ npm run build
|
|
|
184
280
|
# Run tests
|
|
185
281
|
npm test
|
|
186
282
|
|
|
283
|
+
# Run tests with coverage
|
|
284
|
+
npm run test:coverage
|
|
285
|
+
|
|
187
286
|
# Run linter
|
|
188
287
|
npm run lint
|
|
189
288
|
|
|
190
289
|
# Format code
|
|
191
290
|
npm run format
|
|
291
|
+
|
|
292
|
+
# Type check
|
|
293
|
+
npm run typecheck
|
|
192
294
|
```
|
|
193
295
|
|
|
194
296
|
## Contributing
|
|
@@ -207,6 +309,68 @@ Contributions are welcome! Please feel free to submit a Pull Request.
|
|
|
207
309
|
- [n8n Community Nodes Documentation](https://docs.n8n.io/integrations/community-nodes/)
|
|
208
310
|
- [n8n Community Forum](https://community.n8n.io/)
|
|
209
311
|
|
|
312
|
+
## Changelog
|
|
313
|
+
|
|
314
|
+
### v0.5.0
|
|
315
|
+
|
|
316
|
+
**Security & Stability Improvements:**
|
|
317
|
+
- Mandatory webhook signature verification (removed option to disable)
|
|
318
|
+
- Added replay attack protection with configurable event age threshold
|
|
319
|
+
- Improved email validation using RFC 5322 compliant regex
|
|
320
|
+
- Enhanced URL validation to block internal/private network URLs (SSRF protection)
|
|
321
|
+
- IPv6 localhost blocking (`[::1]`) for complete SSRF protection
|
|
322
|
+
- Fixed silent error catching - all errors now logged for debugging
|
|
323
|
+
- Added proper null checks and type safety for custom data handling
|
|
324
|
+
|
|
325
|
+
**New Features:**
|
|
326
|
+
- Added sorting support (created_at, updated_at) for "Get Many" operations
|
|
327
|
+
- Added relationship expansion (include) for fetching related resources in single requests
|
|
328
|
+
- Advanced options available for: Order, Subscription, Customer, License Key, Product, Variant, Checkout, Discount
|
|
329
|
+
- Added pagination timeout protection (default: 5 minutes) to prevent long-running requests
|
|
330
|
+
- Added maxItems limit support for memory optimization on large datasets
|
|
331
|
+
|
|
332
|
+
**Code Quality:**
|
|
333
|
+
- Added comprehensive JSDoc documentation to all helper functions
|
|
334
|
+
- Created shared field generators to reduce code duplication
|
|
335
|
+
- Added TypeScript types for webhooks, errors, and pagination (WebhookMeta, ApiError, PaginationOptions)
|
|
336
|
+
- Improved type safety throughout the codebase
|
|
337
|
+
|
|
338
|
+
**Documentation:**
|
|
339
|
+
- Added SECURITY.md with security policy and vulnerability reporting guidelines
|
|
340
|
+
- Added CONTRIBUTING.md with development setup and contribution guidelines
|
|
341
|
+
|
|
342
|
+
**Test Coverage:**
|
|
343
|
+
- Expanded test suite from 132 to 176 tests (+33%)
|
|
344
|
+
- Added tests for retry logic helpers (sleep, isRateLimitError, isRetryableError)
|
|
345
|
+
- Added webhook signature edge case tests (unicode, long payloads, special characters)
|
|
346
|
+
- Added shared resource options tests
|
|
347
|
+
- Added input validation edge case tests
|
|
348
|
+
- Overall coverage improved to 87%+ statements
|
|
349
|
+
|
|
350
|
+
### v0.4.0
|
|
351
|
+
|
|
352
|
+
- Added User resource for fetching authenticated user information (`getCurrent` operation)
|
|
353
|
+
- Expanded test suite to 130 tests with 85%+ statement coverage
|
|
354
|
+
- Added comprehensive tests for credentials, node descriptions, and helpers
|
|
355
|
+
- Fixed TypeScript strict mode warnings in test files
|
|
356
|
+
- Updated coverage thresholds to 70%
|
|
357
|
+
|
|
358
|
+
### v0.3.0
|
|
359
|
+
|
|
360
|
+
- Added new resources: Order Items, Subscription Invoices, License Key Instances, Discount Redemptions, Usage Records
|
|
361
|
+
- Added input validation for emails, URLs, and dates
|
|
362
|
+
- Improved error messages with field-level details
|
|
363
|
+
- Added advanced filtering with sorting support
|
|
364
|
+
- Added relationship expansion helpers
|
|
365
|
+
- Added security audit in CI pipeline
|
|
366
|
+
- Added coverage reporting with lcov output
|
|
367
|
+
|
|
368
|
+
### v0.2.0
|
|
369
|
+
|
|
370
|
+
- Initial release with full Lemon Squeezy API support
|
|
371
|
+
- Webhook trigger node
|
|
372
|
+
- Rate limiting and retry logic
|
|
373
|
+
|
|
210
374
|
## License
|
|
211
375
|
|
|
212
376
|
[MIT](LICENSE)
|
|
@@ -83,12 +83,27 @@ async function handleCreate(ctx, resource, itemIndex) {
|
|
|
83
83
|
if (additionalOptions.receiptThankYouNote) {
|
|
84
84
|
productOptions.receipt_thank_you_note = additionalOptions.receiptThankYouNote;
|
|
85
85
|
}
|
|
86
|
-
if (additionalOptions.customData) {
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
if (additionalOptions.customData !== undefined && additionalOptions.customData !== null) {
|
|
87
|
+
const customDataValue = additionalOptions.customData;
|
|
88
|
+
if (typeof customDataValue === 'string') {
|
|
89
|
+
try {
|
|
90
|
+
const parsed = JSON.parse(customDataValue);
|
|
91
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
92
|
+
checkoutData.custom = parsed;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
throw new Error('customData must be a valid JSON object');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
throw new Error(`Invalid customData: ${error instanceof Error ? error.message : 'must be valid JSON object'}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else if (typeof customDataValue === 'object' && !Array.isArray(customDataValue)) {
|
|
103
|
+
checkoutData.custom = customDataValue;
|
|
89
104
|
}
|
|
90
|
-
|
|
91
|
-
|
|
105
|
+
else {
|
|
106
|
+
throw new Error('customData must be a JSON string or object');
|
|
92
107
|
}
|
|
93
108
|
}
|
|
94
109
|
if (additionalOptions.expiresAt) {
|
|
@@ -279,7 +294,21 @@ class LemonSqueezy {
|
|
|
279
294
|
else if (operation === 'getAll') {
|
|
280
295
|
const returnAll = this.getNodeParameter('returnAll', i);
|
|
281
296
|
const filters = this.getNodeParameter('filters', i, {});
|
|
297
|
+
const advancedOptions = this.getNodeParameter('advancedOptions', i, {});
|
|
282
298
|
const qs = (0, helpers_1.buildFilterParams)(filters);
|
|
299
|
+
// Apply sorting if specified
|
|
300
|
+
if (advancedOptions.sortField) {
|
|
301
|
+
const sortDirection = advancedOptions.sortDirection || 'desc';
|
|
302
|
+
const sortPrefix = sortDirection === 'desc' ? '-' : '';
|
|
303
|
+
qs.sort = `${sortPrefix}${advancedOptions.sortField}`;
|
|
304
|
+
}
|
|
305
|
+
// Apply relationship expansion if specified
|
|
306
|
+
if (advancedOptions.include && Array.isArray(advancedOptions.include)) {
|
|
307
|
+
const includes = advancedOptions.include;
|
|
308
|
+
if (includes.length > 0) {
|
|
309
|
+
qs.include = includes.join(',');
|
|
310
|
+
}
|
|
311
|
+
}
|
|
283
312
|
if (returnAll) {
|
|
284
313
|
responseData = await helpers_1.lemonSqueezyApiRequestAllItems.call(this, 'GET', `/${endpoint}`, qs);
|
|
285
314
|
}
|
|
@@ -338,6 +367,9 @@ class LemonSqueezy {
|
|
|
338
367
|
});
|
|
339
368
|
}
|
|
340
369
|
}
|
|
370
|
+
else if (resource === 'user' && operation === 'getCurrent') {
|
|
371
|
+
responseData = await helpers_1.lemonSqueezyApiRequest.call(this, 'GET', '/users/me');
|
|
372
|
+
}
|
|
341
373
|
const executionData = this.helpers.constructExecutionMetaData(this.helpers.returnJsonArray(responseData), { itemData: { item: i } });
|
|
342
374
|
returnData.push(...executionData);
|
|
343
375
|
}
|
|
@@ -74,11 +74,11 @@ class LemonSqueezyTrigger {
|
|
|
74
74
|
description: 'Whether to only receive test mode events',
|
|
75
75
|
},
|
|
76
76
|
{
|
|
77
|
-
displayName: '
|
|
78
|
-
name: '
|
|
79
|
-
type: '
|
|
80
|
-
default:
|
|
81
|
-
description: '
|
|
77
|
+
displayName: 'Max Event Age (Minutes)',
|
|
78
|
+
name: 'maxEventAgeMinutes',
|
|
79
|
+
type: 'number',
|
|
80
|
+
default: 5,
|
|
81
|
+
description: 'Maximum age of webhook events in minutes. Events older than this will be rejected to prevent replay attacks. Set to 0 to disable.',
|
|
82
82
|
},
|
|
83
83
|
],
|
|
84
84
|
},
|
|
@@ -97,8 +97,11 @@ class LemonSqueezyTrigger {
|
|
|
97
97
|
await helpers_1.lemonSqueezyApiRequest.call(this, 'GET', `/webhooks/${String(webhookData.webhookId)}`);
|
|
98
98
|
return true;
|
|
99
99
|
}
|
|
100
|
-
catch {
|
|
101
|
-
// Webhook doesn't exist anymore
|
|
100
|
+
catch (error) {
|
|
101
|
+
// Webhook doesn't exist anymore or API error occurred
|
|
102
|
+
// Log for debugging but don't fail - we'll recreate the webhook
|
|
103
|
+
// eslint-disable-next-line no-console
|
|
104
|
+
console.debug(`Webhook ${String(webhookData.webhookId)} check failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
102
105
|
delete webhookData.webhookId;
|
|
103
106
|
return false;
|
|
104
107
|
}
|
|
@@ -118,8 +121,11 @@ class LemonSqueezyTrigger {
|
|
|
118
121
|
}
|
|
119
122
|
}
|
|
120
123
|
}
|
|
121
|
-
catch {
|
|
122
|
-
// Error checking webhooks
|
|
124
|
+
catch (error) {
|
|
125
|
+
// Error checking webhooks - log for debugging
|
|
126
|
+
// This could indicate API issues, but we'll try to create a new webhook
|
|
127
|
+
// eslint-disable-next-line no-console
|
|
128
|
+
console.debug(`Error checking existing webhooks: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
123
129
|
}
|
|
124
130
|
return false;
|
|
125
131
|
},
|
|
@@ -164,8 +170,11 @@ class LemonSqueezyTrigger {
|
|
|
164
170
|
try {
|
|
165
171
|
await helpers_1.lemonSqueezyApiRequest.call(this, 'DELETE', `/webhooks/${String(webhookData.webhookId)}`);
|
|
166
172
|
}
|
|
167
|
-
catch {
|
|
168
|
-
// Webhook might already be deleted
|
|
173
|
+
catch (error) {
|
|
174
|
+
// Webhook might already be deleted or API error
|
|
175
|
+
// Log for debugging but don't fail - we're cleaning up anyway
|
|
176
|
+
// eslint-disable-next-line no-console
|
|
177
|
+
console.debug(`Webhook ${String(webhookData.webhookId)} deletion failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
169
178
|
}
|
|
170
179
|
delete webhookData.webhookId;
|
|
171
180
|
}
|
|
@@ -177,32 +186,68 @@ class LemonSqueezyTrigger {
|
|
|
177
186
|
async webhook() {
|
|
178
187
|
const options = this.getNodeParameter('options');
|
|
179
188
|
const webhookSecret = this.getNodeParameter('webhookSecret');
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
};
|
|
201
|
-
}
|
|
189
|
+
// Always verify signature - this is a security requirement
|
|
190
|
+
const signature = this.getHeaderData()['x-signature'];
|
|
191
|
+
if (!signature) {
|
|
192
|
+
return {
|
|
193
|
+
webhookResponse: {
|
|
194
|
+
status: 401,
|
|
195
|
+
body: { error: 'Missing signature header' },
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const bodyData = this.getBodyData();
|
|
200
|
+
const rawBody = JSON.stringify(bodyData);
|
|
201
|
+
const isValid = (0, helpers_1.verifyWebhookSignature)(rawBody, signature, webhookSecret);
|
|
202
|
+
if (!isValid) {
|
|
203
|
+
return {
|
|
204
|
+
webhookResponse: {
|
|
205
|
+
status: 401,
|
|
206
|
+
body: { error: 'Invalid signature' },
|
|
207
|
+
},
|
|
208
|
+
};
|
|
202
209
|
}
|
|
203
210
|
const body = this.getBodyData();
|
|
204
211
|
const meta = body.meta;
|
|
205
212
|
const eventName = meta === null || meta === void 0 ? void 0 : meta.event_name;
|
|
213
|
+
// Replay attack protection: check event timestamp
|
|
214
|
+
const maxEventAgeMinutes = typeof options.maxEventAgeMinutes === 'number' ? options.maxEventAgeMinutes : 5;
|
|
215
|
+
if (maxEventAgeMinutes > 0 && (meta === null || meta === void 0 ? void 0 : meta.custom_data)) {
|
|
216
|
+
const customData = meta.custom_data;
|
|
217
|
+
const eventTimestamp = customData.event_created_at;
|
|
218
|
+
if (eventTimestamp) {
|
|
219
|
+
const eventTime = new Date(eventTimestamp).getTime();
|
|
220
|
+
const now = Date.now();
|
|
221
|
+
const maxAgeMs = maxEventAgeMinutes * 60 * 1000;
|
|
222
|
+
if (now - eventTime > maxAgeMs) {
|
|
223
|
+
return {
|
|
224
|
+
webhookResponse: {
|
|
225
|
+
status: 400,
|
|
226
|
+
body: { error: 'Event too old - possible replay attack' },
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Also check the created_at field in the data payload if available
|
|
233
|
+
if (maxEventAgeMinutes > 0) {
|
|
234
|
+
const data = body.data;
|
|
235
|
+
const attributes = data === null || data === void 0 ? void 0 : data.attributes;
|
|
236
|
+
const createdAt = attributes === null || attributes === void 0 ? void 0 : attributes.created_at;
|
|
237
|
+
if (createdAt) {
|
|
238
|
+
const eventTime = new Date(createdAt).getTime();
|
|
239
|
+
const now = Date.now();
|
|
240
|
+
const maxAgeMs = maxEventAgeMinutes * 60 * 1000;
|
|
241
|
+
if (now - eventTime > maxAgeMs) {
|
|
242
|
+
return {
|
|
243
|
+
webhookResponse: {
|
|
244
|
+
status: 400,
|
|
245
|
+
body: { error: 'Event too old - possible replay attack' },
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
206
251
|
// Check if we should process this event
|
|
207
252
|
const subscribedEvents = this.getNodeParameter('events');
|
|
208
253
|
if (!eventName || !subscribedEvents.includes(eventName)) {
|
|
@@ -15,15 +15,20 @@ exports.RATE_LIMIT_DELAY_MS = 60000;
|
|
|
15
15
|
exports.RESOURCE_ENDPOINTS = {
|
|
16
16
|
product: 'products',
|
|
17
17
|
order: 'orders',
|
|
18
|
+
orderItem: 'order-items',
|
|
18
19
|
subscription: 'subscriptions',
|
|
20
|
+
subscriptionInvoice: 'subscription-invoices',
|
|
19
21
|
customer: 'customers',
|
|
20
22
|
licenseKey: 'license-keys',
|
|
23
|
+
licenseKeyInstance: 'license-key-instances',
|
|
21
24
|
discount: 'discounts',
|
|
25
|
+
discountRedemption: 'discount-redemptions',
|
|
22
26
|
store: 'stores',
|
|
23
27
|
variant: 'variants',
|
|
24
28
|
checkout: 'checkouts',
|
|
25
29
|
webhook: 'webhooks',
|
|
26
|
-
|
|
30
|
+
usageRecord: 'usage-records',
|
|
31
|
+
user: 'users',
|
|
27
32
|
};
|
|
28
33
|
/**
|
|
29
34
|
* Resource to ID parameter mapping
|
|
@@ -31,15 +36,19 @@ exports.RESOURCE_ENDPOINTS = {
|
|
|
31
36
|
exports.RESOURCE_ID_PARAMS = {
|
|
32
37
|
product: 'productId',
|
|
33
38
|
order: 'orderId',
|
|
39
|
+
orderItem: 'orderItemId',
|
|
34
40
|
subscription: 'subscriptionId',
|
|
41
|
+
subscriptionInvoice: 'subscriptionInvoiceId',
|
|
35
42
|
customer: 'customerId',
|
|
36
43
|
licenseKey: 'licenseKeyId',
|
|
44
|
+
licenseKeyInstance: 'licenseKeyInstanceId',
|
|
37
45
|
discount: 'discountId',
|
|
46
|
+
discountRedemption: 'discountRedemptionId',
|
|
38
47
|
store: 'storeId',
|
|
39
48
|
variant: 'variantId',
|
|
40
49
|
checkout: 'checkoutId',
|
|
41
50
|
webhook: 'webhookId',
|
|
42
|
-
|
|
51
|
+
usageRecord: 'usageRecordId',
|
|
43
52
|
};
|
|
44
53
|
/**
|
|
45
54
|
* Webhook event types with descriptions
|