n8n-nodes-lemonsqueezy 0.4.0 → 0.6.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 +113 -1
- package/dist/nodes/LemonSqueezy/LemonSqueezy.node.js +56 -5
- package/dist/nodes/LemonSqueezy/LemonSqueezyTrigger.node.js +113 -35
- package/dist/nodes/LemonSqueezy/constants.d.ts +2 -0
- package/dist/nodes/LemonSqueezy/constants.js +3 -1
- package/dist/nodes/LemonSqueezy/helpers.d.ts +318 -18
- package/dist/nodes/LemonSqueezy/helpers.js +376 -28
- package/dist/nodes/LemonSqueezy/resources/index.js +9 -0
- package/dist/nodes/LemonSqueezy/resources/shared.d.ts +99 -0
- package/dist/nodes/LemonSqueezy/resources/shared.js +289 -0
- package/dist/nodes/LemonSqueezy/types.d.ts +63 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,9 +13,11 @@ An [n8n](https://n8n.io/) community node for [Lemon Squeezy](https://lemonsqueez
|
|
|
13
13
|
- **License Key Management** - Validate, activate, and deactivate license keys
|
|
14
14
|
- **Checkout Links** - Create dynamic checkout URLs with custom options
|
|
15
15
|
- **Rate Limiting** - Built-in retry logic with exponential backoff
|
|
16
|
-
- **Input Validation** -
|
|
16
|
+
- **Input Validation** - RFC 5322 compliant email validation, secure URL validation (blocks internal networks)
|
|
17
17
|
- **Detailed Error Messages** - Descriptive error messages with field-level details
|
|
18
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
|
|
19
21
|
|
|
20
22
|
## Installation
|
|
21
23
|
|
|
@@ -147,6 +149,57 @@ Most "Get Many" operations support filtering:
|
|
|
147
149
|
| `licenseKeyId` | Filter by license key | License Key Instances |
|
|
148
150
|
| `discountId` | Filter by discount | Discount Redemptions |
|
|
149
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
|
|
202
|
+
|
|
150
203
|
## Error Handling
|
|
151
204
|
|
|
152
205
|
The node includes built-in error handling with detailed messages:
|
|
@@ -258,6 +311,65 @@ Contributions are welcome! Please feel free to submit a Pull Request.
|
|
|
258
311
|
|
|
259
312
|
## Changelog
|
|
260
313
|
|
|
314
|
+
### v0.6.0
|
|
315
|
+
|
|
316
|
+
**Reliability & Error Handling:**
|
|
317
|
+
- Improved webhook management error handling with proper 404 vs other error distinction
|
|
318
|
+
- Added detailed logging for webhook lifecycle (create, check, delete operations)
|
|
319
|
+
- Added logging for filtered/unsubscribed webhook events to aid debugging
|
|
320
|
+
- Added rate limit visibility logging with wait time information
|
|
321
|
+
- Added retry attempt logging with exponential backoff details
|
|
322
|
+
|
|
323
|
+
**Input Validation:**
|
|
324
|
+
- Added pre-API validation for email fields (customer create/update, checkout)
|
|
325
|
+
- Added pre-API validation for URL fields (webhook URL, redirect URLs, receipt link URLs)
|
|
326
|
+
- Added webhook secret minimum length validation (16 characters) for security
|
|
327
|
+
- Validation errors now fail fast before making API requests
|
|
328
|
+
|
|
329
|
+
**Performance:**
|
|
330
|
+
- Added configurable request timeout (default: 30 seconds) for all API requests
|
|
331
|
+
- Timeout prevents hanging requests and improves workflow reliability
|
|
332
|
+
|
|
333
|
+
**Code Quality:**
|
|
334
|
+
- Added common filter field generators to reduce code duplication
|
|
335
|
+
- Added createFiltersField, createStatusFilter factory functions
|
|
336
|
+
|
|
337
|
+
### v0.5.0
|
|
338
|
+
|
|
339
|
+
**Security & Stability Improvements:**
|
|
340
|
+
- Mandatory webhook signature verification (removed option to disable)
|
|
341
|
+
- Added replay attack protection with configurable event age threshold
|
|
342
|
+
- Improved email validation using RFC 5322 compliant regex
|
|
343
|
+
- Enhanced URL validation to block internal/private network URLs (SSRF protection)
|
|
344
|
+
- IPv6 localhost blocking (`[::1]`) for complete SSRF protection
|
|
345
|
+
- Fixed silent error catching - all errors now logged for debugging
|
|
346
|
+
- Added proper null checks and type safety for custom data handling
|
|
347
|
+
|
|
348
|
+
**New Features:**
|
|
349
|
+
- Added sorting support (created_at, updated_at) for "Get Many" operations
|
|
350
|
+
- Added relationship expansion (include) for fetching related resources in single requests
|
|
351
|
+
- Advanced options available for: Order, Subscription, Customer, License Key, Product, Variant, Checkout, Discount
|
|
352
|
+
- Added pagination timeout protection (default: 5 minutes) to prevent long-running requests
|
|
353
|
+
- Added maxItems limit support for memory optimization on large datasets
|
|
354
|
+
|
|
355
|
+
**Code Quality:**
|
|
356
|
+
- Added comprehensive JSDoc documentation to all helper functions
|
|
357
|
+
- Created shared field generators to reduce code duplication
|
|
358
|
+
- Added TypeScript types for webhooks, errors, and pagination (WebhookMeta, ApiError, PaginationOptions)
|
|
359
|
+
- Improved type safety throughout the codebase
|
|
360
|
+
|
|
361
|
+
**Documentation:**
|
|
362
|
+
- Added SECURITY.md with security policy and vulnerability reporting guidelines
|
|
363
|
+
- Added CONTRIBUTING.md with development setup and contribution guidelines
|
|
364
|
+
|
|
365
|
+
**Test Coverage:**
|
|
366
|
+
- Expanded test suite from 132 to 176 tests (+33%)
|
|
367
|
+
- Added tests for retry logic helpers (sleep, isRateLimitError, isRetryableError)
|
|
368
|
+
- Added webhook signature edge case tests (unicode, long payloads, special characters)
|
|
369
|
+
- Added shared resource options tests
|
|
370
|
+
- Added input validation edge case tests
|
|
371
|
+
- Overall coverage improved to 87%+ statements
|
|
372
|
+
|
|
261
373
|
### v0.4.0
|
|
262
374
|
|
|
263
375
|
- Added User resource for fetching authenticated user information (`getCurrent` operation)
|
|
@@ -10,6 +10,8 @@ async function handleCreate(ctx, resource, itemIndex) {
|
|
|
10
10
|
const name = ctx.getNodeParameter('customerName', itemIndex);
|
|
11
11
|
const email = ctx.getNodeParameter('customerEmail', itemIndex);
|
|
12
12
|
const additionalFields = ctx.getNodeParameter('additionalFields', itemIndex);
|
|
13
|
+
// Validate required fields before API call
|
|
14
|
+
(0, helpers_1.validateField)('email', email, 'email');
|
|
13
15
|
const body = (0, helpers_1.buildJsonApiBody)('customers', { name, email, ...additionalFields }, { store: { type: 'stores', id: storeId } });
|
|
14
16
|
return await helpers_1.lemonSqueezyApiRequest.call(ctx, 'POST', '/customers', body);
|
|
15
17
|
}
|
|
@@ -63,6 +65,8 @@ async function handleCreate(ctx, resource, itemIndex) {
|
|
|
63
65
|
attributes.custom_price = additionalOptions.customPrice;
|
|
64
66
|
}
|
|
65
67
|
if (additionalOptions.email) {
|
|
68
|
+
// Validate email before API call
|
|
69
|
+
(0, helpers_1.validateField)('email', additionalOptions.email, 'email');
|
|
66
70
|
checkoutData.email = additionalOptions.email;
|
|
67
71
|
}
|
|
68
72
|
if (additionalOptions.name) {
|
|
@@ -72,23 +76,42 @@ async function handleCreate(ctx, resource, itemIndex) {
|
|
|
72
76
|
checkoutData.discount_code = additionalOptions.discountCode;
|
|
73
77
|
}
|
|
74
78
|
if (additionalOptions.redirectUrl) {
|
|
79
|
+
// Validate URL before API call
|
|
80
|
+
(0, helpers_1.validateField)('redirectUrl', additionalOptions.redirectUrl, 'url');
|
|
75
81
|
productOptions.redirect_url = additionalOptions.redirectUrl;
|
|
76
82
|
}
|
|
77
83
|
if (additionalOptions.receiptButtonText) {
|
|
78
84
|
productOptions.receipt_button_text = additionalOptions.receiptButtonText;
|
|
79
85
|
}
|
|
80
86
|
if (additionalOptions.receiptLinkUrl) {
|
|
87
|
+
// Validate URL before API call
|
|
88
|
+
(0, helpers_1.validateField)('receiptLinkUrl', additionalOptions.receiptLinkUrl, 'url');
|
|
81
89
|
productOptions.receipt_link_url = additionalOptions.receiptLinkUrl;
|
|
82
90
|
}
|
|
83
91
|
if (additionalOptions.receiptThankYouNote) {
|
|
84
92
|
productOptions.receipt_thank_you_note = additionalOptions.receiptThankYouNote;
|
|
85
93
|
}
|
|
86
|
-
if (additionalOptions.customData) {
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
if (additionalOptions.customData !== undefined && additionalOptions.customData !== null) {
|
|
95
|
+
const customDataValue = additionalOptions.customData;
|
|
96
|
+
if (typeof customDataValue === 'string') {
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(customDataValue);
|
|
99
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
100
|
+
checkoutData.custom = parsed;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
throw new Error('customData must be a valid JSON object');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
throw new Error(`Invalid customData: ${error instanceof Error ? error.message : 'must be valid JSON object'}`);
|
|
108
|
+
}
|
|
89
109
|
}
|
|
90
|
-
|
|
91
|
-
checkoutData.custom =
|
|
110
|
+
else if (typeof customDataValue === 'object' && !Array.isArray(customDataValue)) {
|
|
111
|
+
checkoutData.custom = customDataValue;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
throw new Error('customData must be a JSON string or object');
|
|
92
115
|
}
|
|
93
116
|
}
|
|
94
117
|
if (additionalOptions.expiresAt) {
|
|
@@ -139,6 +162,12 @@ async function handleCreate(ctx, resource, itemIndex) {
|
|
|
139
162
|
const events = ctx.getNodeParameter('webhookEvents', itemIndex);
|
|
140
163
|
const secret = ctx.getNodeParameter('webhookSecret', itemIndex);
|
|
141
164
|
const additionalOptions = ctx.getNodeParameter('additionalOptions', itemIndex, {});
|
|
165
|
+
// Validate URL before API call
|
|
166
|
+
(0, helpers_1.validateField)('url', url, 'url');
|
|
167
|
+
// Validate webhook secret minimum length for security
|
|
168
|
+
if (secret.length < 16) {
|
|
169
|
+
throw new Error('Webhook secret must be at least 16 characters for security');
|
|
170
|
+
}
|
|
142
171
|
const attributes = { url, events, secret };
|
|
143
172
|
if (additionalOptions.testMode !== undefined) {
|
|
144
173
|
attributes.test_mode = additionalOptions.testMode;
|
|
@@ -187,6 +216,8 @@ async function handleUpdate(ctx, resource, itemIndex) {
|
|
|
187
216
|
attributes.name = updateFields.name;
|
|
188
217
|
}
|
|
189
218
|
if (updateFields.email) {
|
|
219
|
+
// Validate email before API call
|
|
220
|
+
(0, helpers_1.validateField)('email', updateFields.email, 'email');
|
|
190
221
|
attributes.email = updateFields.email;
|
|
191
222
|
}
|
|
192
223
|
if (updateFields.city) {
|
|
@@ -225,12 +256,18 @@ async function handleUpdate(ctx, resource, itemIndex) {
|
|
|
225
256
|
const updateFields = ctx.getNodeParameter('updateFields', itemIndex);
|
|
226
257
|
const attributes = {};
|
|
227
258
|
if (updateFields.url) {
|
|
259
|
+
// Validate URL before API call
|
|
260
|
+
(0, helpers_1.validateField)('url', updateFields.url, 'url');
|
|
228
261
|
attributes.url = updateFields.url;
|
|
229
262
|
}
|
|
230
263
|
if (updateFields.events) {
|
|
231
264
|
attributes.events = updateFields.events;
|
|
232
265
|
}
|
|
233
266
|
if (updateFields.secret) {
|
|
267
|
+
// Validate webhook secret minimum length for security
|
|
268
|
+
if (updateFields.secret.length < 16) {
|
|
269
|
+
throw new Error('Webhook secret must be at least 16 characters for security');
|
|
270
|
+
}
|
|
234
271
|
attributes.secret = updateFields.secret;
|
|
235
272
|
}
|
|
236
273
|
const body = (0, helpers_1.buildJsonApiBody)('webhooks', attributes, undefined, webhookId);
|
|
@@ -279,7 +316,21 @@ class LemonSqueezy {
|
|
|
279
316
|
else if (operation === 'getAll') {
|
|
280
317
|
const returnAll = this.getNodeParameter('returnAll', i);
|
|
281
318
|
const filters = this.getNodeParameter('filters', i, {});
|
|
319
|
+
const advancedOptions = this.getNodeParameter('advancedOptions', i, {});
|
|
282
320
|
const qs = (0, helpers_1.buildFilterParams)(filters);
|
|
321
|
+
// Apply sorting if specified
|
|
322
|
+
if (advancedOptions.sortField) {
|
|
323
|
+
const sortDirection = advancedOptions.sortDirection || 'desc';
|
|
324
|
+
const sortPrefix = sortDirection === 'desc' ? '-' : '';
|
|
325
|
+
qs.sort = `${sortPrefix}${advancedOptions.sortField}`;
|
|
326
|
+
}
|
|
327
|
+
// Apply relationship expansion if specified
|
|
328
|
+
if (advancedOptions.include && Array.isArray(advancedOptions.include)) {
|
|
329
|
+
const includes = advancedOptions.include;
|
|
330
|
+
if (includes.length > 0) {
|
|
331
|
+
qs.include = includes.join(',');
|
|
332
|
+
}
|
|
333
|
+
}
|
|
283
334
|
if (returnAll) {
|
|
284
335
|
responseData = await helpers_1.lemonSqueezyApiRequestAllItems.call(this, 'GET', `/${endpoint}`, qs);
|
|
285
336
|
}
|
|
@@ -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
|
},
|
|
@@ -90,6 +90,14 @@ class LemonSqueezyTrigger {
|
|
|
90
90
|
const webhookUrl = this.getNodeWebhookUrl('default');
|
|
91
91
|
const storeId = this.getNodeParameter('storeId');
|
|
92
92
|
const webhookData = this.getWorkflowStaticData('node');
|
|
93
|
+
// Helper to check if error is a 404 (webhook not found)
|
|
94
|
+
const isNotFoundError = (error) => {
|
|
95
|
+
if (error && typeof error === 'object') {
|
|
96
|
+
const err = error;
|
|
97
|
+
return err.statusCode === 404 || err.httpCode === 404 || err.code === 404;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
};
|
|
93
101
|
// Check if we have stored webhook data
|
|
94
102
|
if (webhookData.webhookId) {
|
|
95
103
|
try {
|
|
@@ -97,8 +105,18 @@ class LemonSqueezyTrigger {
|
|
|
97
105
|
await helpers_1.lemonSqueezyApiRequest.call(this, 'GET', `/webhooks/${String(webhookData.webhookId)}`);
|
|
98
106
|
return true;
|
|
99
107
|
}
|
|
100
|
-
catch {
|
|
101
|
-
|
|
108
|
+
catch (error) {
|
|
109
|
+
const webhookId = String(webhookData.webhookId);
|
|
110
|
+
if (isNotFoundError(error)) {
|
|
111
|
+
// Webhook was deleted externally - this is expected, recreate it
|
|
112
|
+
// eslint-disable-next-line no-console
|
|
113
|
+
console.log(`[LemonSqueezy] Webhook ${webhookId} not found (404) - will recreate`);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
// Unexpected error - could be auth, network, or server issue
|
|
117
|
+
// eslint-disable-next-line no-console
|
|
118
|
+
console.warn(`[LemonSqueezy] Webhook ${webhookId} check failed with unexpected error: ${error instanceof Error ? error.message : 'Unknown error'}. Will attempt to recreate.`);
|
|
119
|
+
}
|
|
102
120
|
delete webhookData.webhookId;
|
|
103
121
|
return false;
|
|
104
122
|
}
|
|
@@ -114,12 +132,16 @@ class LemonSqueezyTrigger {
|
|
|
114
132
|
const existingWebhook = webhooks.find((webhook) => { var _a; return ((_a = webhook.attributes) === null || _a === void 0 ? void 0 : _a.url) === webhookUrl; });
|
|
115
133
|
if (existingWebhook) {
|
|
116
134
|
webhookData.webhookId = existingWebhook.id;
|
|
135
|
+
// eslint-disable-next-line no-console
|
|
136
|
+
console.log(`[LemonSqueezy] Found existing webhook ${String(existingWebhook.id)} for URL ${webhookUrl}`);
|
|
117
137
|
return true;
|
|
118
138
|
}
|
|
119
139
|
}
|
|
120
140
|
}
|
|
121
|
-
catch {
|
|
122
|
-
// Error checking webhooks
|
|
141
|
+
catch (error) {
|
|
142
|
+
// Error checking webhooks - this is more serious as it could indicate API/auth issues
|
|
143
|
+
// eslint-disable-next-line no-console
|
|
144
|
+
console.error(`[LemonSqueezy] Error checking existing webhooks for store ${storeId}: ${error instanceof Error ? error.message : 'Unknown error'}. Will attempt to create new webhook.`);
|
|
123
145
|
}
|
|
124
146
|
return false;
|
|
125
147
|
},
|
|
@@ -161,11 +183,28 @@ class LemonSqueezyTrigger {
|
|
|
161
183
|
async delete() {
|
|
162
184
|
const webhookData = this.getWorkflowStaticData('node');
|
|
163
185
|
if (webhookData.webhookId) {
|
|
186
|
+
const webhookId = String(webhookData.webhookId);
|
|
164
187
|
try {
|
|
165
|
-
await helpers_1.lemonSqueezyApiRequest.call(this, 'DELETE', `/webhooks/${
|
|
188
|
+
await helpers_1.lemonSqueezyApiRequest.call(this, 'DELETE', `/webhooks/${webhookId}`);
|
|
189
|
+
// eslint-disable-next-line no-console
|
|
190
|
+
console.log(`[LemonSqueezy] Webhook ${webhookId} deleted successfully`);
|
|
166
191
|
}
|
|
167
|
-
catch {
|
|
168
|
-
//
|
|
192
|
+
catch (error) {
|
|
193
|
+
// Check if it's a 404 (already deleted) vs other errors
|
|
194
|
+
const is404 = error &&
|
|
195
|
+
typeof error === 'object' &&
|
|
196
|
+
(error.statusCode === 404 ||
|
|
197
|
+
error.httpCode === 404);
|
|
198
|
+
if (is404) {
|
|
199
|
+
// Webhook was already deleted - this is fine
|
|
200
|
+
// eslint-disable-next-line no-console
|
|
201
|
+
console.log(`[LemonSqueezy] Webhook ${webhookId} already deleted (404)`);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
// Unexpected error during deletion - log as warning
|
|
205
|
+
// eslint-disable-next-line no-console
|
|
206
|
+
console.warn(`[LemonSqueezy] Webhook ${webhookId} deletion failed: ${error instanceof Error ? error.message : 'Unknown error'}. Continuing cleanup.`);
|
|
207
|
+
}
|
|
169
208
|
}
|
|
170
209
|
delete webhookData.webhookId;
|
|
171
210
|
}
|
|
@@ -177,40 +216,79 @@ class LemonSqueezyTrigger {
|
|
|
177
216
|
async webhook() {
|
|
178
217
|
const options = this.getNodeParameter('options');
|
|
179
218
|
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
|
-
}
|
|
219
|
+
// Always verify signature - this is a security requirement
|
|
220
|
+
const signature = this.getHeaderData()['x-signature'];
|
|
221
|
+
if (!signature) {
|
|
222
|
+
return {
|
|
223
|
+
webhookResponse: {
|
|
224
|
+
status: 401,
|
|
225
|
+
body: { error: 'Missing signature header' },
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
const bodyData = this.getBodyData();
|
|
230
|
+
const rawBody = JSON.stringify(bodyData);
|
|
231
|
+
const isValid = (0, helpers_1.verifyWebhookSignature)(rawBody, signature, webhookSecret);
|
|
232
|
+
if (!isValid) {
|
|
233
|
+
return {
|
|
234
|
+
webhookResponse: {
|
|
235
|
+
status: 401,
|
|
236
|
+
body: { error: 'Invalid signature' },
|
|
237
|
+
},
|
|
238
|
+
};
|
|
202
239
|
}
|
|
203
240
|
const body = this.getBodyData();
|
|
204
241
|
const meta = body.meta;
|
|
205
242
|
const eventName = meta === null || meta === void 0 ? void 0 : meta.event_name;
|
|
243
|
+
// Replay attack protection: check event timestamp
|
|
244
|
+
const maxEventAgeMinutes = typeof options.maxEventAgeMinutes === 'number' ? options.maxEventAgeMinutes : 5;
|
|
245
|
+
if (maxEventAgeMinutes > 0 && (meta === null || meta === void 0 ? void 0 : meta.custom_data)) {
|
|
246
|
+
const customData = meta.custom_data;
|
|
247
|
+
const eventTimestamp = customData.event_created_at;
|
|
248
|
+
if (eventTimestamp) {
|
|
249
|
+
const eventTime = new Date(eventTimestamp).getTime();
|
|
250
|
+
const now = Date.now();
|
|
251
|
+
const maxAgeMs = maxEventAgeMinutes * 60 * 1000;
|
|
252
|
+
if (now - eventTime > maxAgeMs) {
|
|
253
|
+
return {
|
|
254
|
+
webhookResponse: {
|
|
255
|
+
status: 400,
|
|
256
|
+
body: { error: 'Event too old - possible replay attack' },
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Also check the created_at field in the data payload if available
|
|
263
|
+
if (maxEventAgeMinutes > 0) {
|
|
264
|
+
const data = body.data;
|
|
265
|
+
const attributes = data === null || data === void 0 ? void 0 : data.attributes;
|
|
266
|
+
const createdAt = attributes === null || attributes === void 0 ? void 0 : attributes.created_at;
|
|
267
|
+
if (createdAt) {
|
|
268
|
+
const eventTime = new Date(createdAt).getTime();
|
|
269
|
+
const now = Date.now();
|
|
270
|
+
const maxAgeMs = maxEventAgeMinutes * 60 * 1000;
|
|
271
|
+
if (now - eventTime > maxAgeMs) {
|
|
272
|
+
return {
|
|
273
|
+
webhookResponse: {
|
|
274
|
+
status: 400,
|
|
275
|
+
body: { error: 'Event too old - possible replay attack' },
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
206
281
|
// Check if we should process this event
|
|
207
282
|
const subscribedEvents = this.getNodeParameter('events');
|
|
208
283
|
if (!eventName || !subscribedEvents.includes(eventName)) {
|
|
209
284
|
// Event not subscribed, acknowledge but don't trigger workflow
|
|
285
|
+
// Log for debugging - helps identify misconfigured webhooks
|
|
286
|
+
// eslint-disable-next-line no-console
|
|
287
|
+
console.log(`[LemonSqueezy] Received event '${eventName || 'unknown'}' but not in subscribed events [${subscribedEvents.join(', ')}]. Acknowledged but not processed.`);
|
|
210
288
|
return {
|
|
211
289
|
webhookResponse: {
|
|
212
290
|
status: 200,
|
|
213
|
-
body: { received: true, processed: false },
|
|
291
|
+
body: { received: true, processed: false, event: eventName },
|
|
214
292
|
},
|
|
215
293
|
};
|
|
216
294
|
}
|
|
@@ -6,6 +6,8 @@ export declare const DEFAULT_PAGE_SIZE = 100;
|
|
|
6
6
|
export declare const MAX_RETRIES = 3;
|
|
7
7
|
export declare const RETRY_DELAY_MS = 1000;
|
|
8
8
|
export declare const RATE_LIMIT_DELAY_MS = 60000;
|
|
9
|
+
/** Default timeout for API requests in milliseconds (30 seconds) */
|
|
10
|
+
export declare const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
9
11
|
/**
|
|
10
12
|
* Resource to API endpoint mapping
|
|
11
13
|
*/
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
* Lemon Squeezy API constants
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.INTERVAL_TYPES = exports.PRODUCT_STATUSES = exports.PAUSE_MODES = exports.DISCOUNT_DURATION_TYPES = exports.DISCOUNT_AMOUNT_TYPES = exports.LICENSE_KEY_STATUSES = exports.CUSTOMER_STATUSES = exports.ORDER_STATUSES = exports.SUBSCRIPTION_STATUSES = exports.WEBHOOK_EVENTS = exports.RESOURCE_ID_PARAMS = exports.RESOURCE_ENDPOINTS = exports.RATE_LIMIT_DELAY_MS = exports.RETRY_DELAY_MS = exports.MAX_RETRIES = exports.DEFAULT_PAGE_SIZE = exports.API_BASE_URL = void 0;
|
|
6
|
+
exports.INTERVAL_TYPES = exports.PRODUCT_STATUSES = exports.PAUSE_MODES = exports.DISCOUNT_DURATION_TYPES = exports.DISCOUNT_AMOUNT_TYPES = exports.LICENSE_KEY_STATUSES = exports.CUSTOMER_STATUSES = exports.ORDER_STATUSES = exports.SUBSCRIPTION_STATUSES = exports.WEBHOOK_EVENTS = exports.RESOURCE_ID_PARAMS = exports.RESOURCE_ENDPOINTS = exports.DEFAULT_REQUEST_TIMEOUT_MS = exports.RATE_LIMIT_DELAY_MS = exports.RETRY_DELAY_MS = exports.MAX_RETRIES = exports.DEFAULT_PAGE_SIZE = exports.API_BASE_URL = void 0;
|
|
7
7
|
exports.API_BASE_URL = 'https://api.lemonsqueezy.com/v1';
|
|
8
8
|
exports.DEFAULT_PAGE_SIZE = 100;
|
|
9
9
|
exports.MAX_RETRIES = 3;
|
|
10
10
|
exports.RETRY_DELAY_MS = 1000;
|
|
11
11
|
exports.RATE_LIMIT_DELAY_MS = 60000;
|
|
12
|
+
/** Default timeout for API requests in milliseconds (30 seconds) */
|
|
13
|
+
exports.DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
12
14
|
/**
|
|
13
15
|
* Resource to API endpoint mapping
|
|
14
16
|
*/
|