n8n-nodes-lemonsqueezy 0.6.0 → 0.7.2
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 +31 -21
- package/dist/nodes/LemonSqueezy/LemonSqueezy.node.js +19 -7
- package/dist/nodes/LemonSqueezy/LemonSqueezyTrigger.node.js +43 -52
- package/dist/nodes/LemonSqueezy/helpers.d.ts +0 -12
- package/dist/nodes/LemonSqueezy/helpers.js +10 -54
- package/dist/nodes/LemonSqueezy/resources/usageRecord.js +63 -0
- package/dist/nodes/LemonSqueezy/resources/webhook.js +11 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ The main node for interacting with the Lemon Squeezy API.
|
|
|
65
65
|
| **Store** | Get, Get Many |
|
|
66
66
|
| **Subscription** | Get, Get Many, Update, Cancel, Resume |
|
|
67
67
|
| **Subscription Invoice** | Get, Get Many |
|
|
68
|
-
| **Usage Record** | Get, Get Many |
|
|
68
|
+
| **Usage Record** | Create, Get, Get Many |
|
|
69
69
|
| **User** | Get Current |
|
|
70
70
|
| **Variant** | Get, Get Many |
|
|
71
71
|
| **Webhook** | Create, Update, Delete, Get, Get Many |
|
|
@@ -204,10 +204,9 @@ The webhook trigger includes built-in security features:
|
|
|
204
204
|
|
|
205
205
|
The node includes built-in error handling with detailed messages:
|
|
206
206
|
|
|
207
|
-
- **Rate Limiting**: Automatically waits and retries when rate limited (429 errors)
|
|
208
|
-
- **Retry Logic**: Retries failed requests with exponential backoff for 5xx errors
|
|
209
207
|
- **Continue on Fail**: Enable to process remaining items even if some fail
|
|
210
208
|
- **Detailed Errors**: Field-level error details for validation failures
|
|
209
|
+
- **Workflow Retry**: Use n8n's built-in workflow error handling for retry logic
|
|
211
210
|
|
|
212
211
|
### Error Code Reference
|
|
213
212
|
|
|
@@ -245,20 +244,13 @@ The node includes built-in error handling with detailed messages:
|
|
|
245
244
|
|
|
246
245
|
### Rate Limiting Issues
|
|
247
246
|
|
|
248
|
-
|
|
247
|
+
If you encounter rate limiting (429 errors):
|
|
249
248
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
If you're hitting rate limits frequently:
|
|
257
|
-
|
|
258
|
-
1. Reduce the frequency of API calls
|
|
259
|
-
2. Use "Return All" sparingly for large datasets
|
|
260
|
-
3. Consider caching responses where appropriate
|
|
261
|
-
4. Space out bulk operations with delays
|
|
249
|
+
1. Configure n8n's workflow error handling to retry on failure
|
|
250
|
+
2. Reduce the frequency of API calls
|
|
251
|
+
3. Use "Return All" sparingly for large datasets
|
|
252
|
+
4. Consider caching responses where appropriate
|
|
253
|
+
5. Space out bulk operations using the Wait node
|
|
262
254
|
|
|
263
255
|
### Validation Errors
|
|
264
256
|
|
|
@@ -311,14 +303,32 @@ Contributions are welcome! Please feel free to submit a Pull Request.
|
|
|
311
303
|
|
|
312
304
|
## Changelog
|
|
313
305
|
|
|
306
|
+
### v0.7.2
|
|
307
|
+
|
|
308
|
+
**n8n Community Package Compliance:**
|
|
309
|
+
- Resolved all n8n community package scanner ESLint violations
|
|
310
|
+
- Replaced deprecated `requestWithAuthentication` with `httpRequestWithAuthentication`
|
|
311
|
+
- Removed restricted globals (use n8n's built-in workflow retry for error handling)
|
|
312
|
+
|
|
313
|
+
### v0.7.0
|
|
314
|
+
|
|
315
|
+
**New Features:**
|
|
316
|
+
- Added Usage Record Create operation for metered billing support
|
|
317
|
+
- Added configurable pagination timeout in Advanced Options UI for "Return All" operations
|
|
318
|
+
- Added field hints with examples and documentation links for better UX
|
|
319
|
+
- Added CHANGELOG.md with migration guide for breaking changes
|
|
320
|
+
|
|
321
|
+
**Security:**
|
|
322
|
+
- Increased webhook secret minimum length from 16 to 32 characters
|
|
323
|
+
- Added webhook creation deduplication to prevent race conditions
|
|
324
|
+
|
|
325
|
+
**Bug Fixes:**
|
|
326
|
+
- Fixed pagination timeout=0 handling (now correctly treated as "no timeout")
|
|
327
|
+
|
|
314
328
|
### v0.6.0
|
|
315
329
|
|
|
316
330
|
**Reliability & Error Handling:**
|
|
317
331
|
- 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
332
|
|
|
323
333
|
**Input Validation:**
|
|
324
334
|
- Added pre-API validation for email fields (customer create/update, checkout)
|
|
@@ -342,7 +352,7 @@ Contributions are welcome! Please feel free to submit a Pull Request.
|
|
|
342
352
|
- Improved email validation using RFC 5322 compliant regex
|
|
343
353
|
- Enhanced URL validation to block internal/private network URLs (SSRF protection)
|
|
344
354
|
- IPv6 localhost blocking (`[::1]`) for complete SSRF protection
|
|
345
|
-
-
|
|
355
|
+
- Improved error handling with proper error propagation
|
|
346
356
|
- Added proper null checks and type safety for custom data handling
|
|
347
357
|
|
|
348
358
|
**New Features:**
|
|
@@ -156,6 +156,13 @@ async function handleCreate(ctx, resource, itemIndex) {
|
|
|
156
156
|
});
|
|
157
157
|
return await helpers_1.lemonSqueezyApiRequest.call(ctx, 'POST', '/checkouts', body);
|
|
158
158
|
}
|
|
159
|
+
if (resource === 'usageRecord') {
|
|
160
|
+
const subscriptionItemId = ctx.getNodeParameter('subscriptionItemId', itemIndex);
|
|
161
|
+
const quantity = ctx.getNodeParameter('quantity', itemIndex);
|
|
162
|
+
const action = ctx.getNodeParameter('action', itemIndex);
|
|
163
|
+
const body = (0, helpers_1.buildJsonApiBody)('usage-records', { quantity, action }, { 'subscription-item': { type: 'subscription-items', id: subscriptionItemId } });
|
|
164
|
+
return await helpers_1.lemonSqueezyApiRequest.call(ctx, 'POST', '/usage-records', body);
|
|
165
|
+
}
|
|
159
166
|
if (resource === 'webhook') {
|
|
160
167
|
const storeId = ctx.getNodeParameter('webhookStoreId', itemIndex);
|
|
161
168
|
const url = ctx.getNodeParameter('webhookUrl', itemIndex);
|
|
@@ -164,9 +171,9 @@ async function handleCreate(ctx, resource, itemIndex) {
|
|
|
164
171
|
const additionalOptions = ctx.getNodeParameter('additionalOptions', itemIndex, {});
|
|
165
172
|
// Validate URL before API call
|
|
166
173
|
(0, helpers_1.validateField)('url', url, 'url');
|
|
167
|
-
// Validate webhook secret minimum length for security
|
|
168
|
-
if (secret.length <
|
|
169
|
-
throw new Error('Webhook secret must be at least
|
|
174
|
+
// Validate webhook secret minimum length for security (32+ chars recommended)
|
|
175
|
+
if (secret.length < 32) {
|
|
176
|
+
throw new Error('Webhook secret must be at least 32 characters for security. Generate one using: openssl rand -hex 32');
|
|
170
177
|
}
|
|
171
178
|
const attributes = { url, events, secret };
|
|
172
179
|
if (additionalOptions.testMode !== undefined) {
|
|
@@ -264,9 +271,9 @@ async function handleUpdate(ctx, resource, itemIndex) {
|
|
|
264
271
|
attributes.events = updateFields.events;
|
|
265
272
|
}
|
|
266
273
|
if (updateFields.secret) {
|
|
267
|
-
// Validate webhook secret minimum length for security
|
|
268
|
-
if (updateFields.secret.length <
|
|
269
|
-
throw new Error('Webhook secret must be at least
|
|
274
|
+
// Validate webhook secret minimum length for security (32+ chars recommended)
|
|
275
|
+
if (updateFields.secret.length < 32) {
|
|
276
|
+
throw new Error('Webhook secret must be at least 32 characters for security. Generate one using: openssl rand -hex 32');
|
|
270
277
|
}
|
|
271
278
|
attributes.secret = updateFields.secret;
|
|
272
279
|
}
|
|
@@ -300,6 +307,7 @@ class LemonSqueezy {
|
|
|
300
307
|
};
|
|
301
308
|
}
|
|
302
309
|
async execute() {
|
|
310
|
+
var _a;
|
|
303
311
|
const items = this.getInputData();
|
|
304
312
|
const returnData = [];
|
|
305
313
|
const resource = this.getNodeParameter('resource', 0);
|
|
@@ -332,7 +340,11 @@ class LemonSqueezy {
|
|
|
332
340
|
}
|
|
333
341
|
}
|
|
334
342
|
if (returnAll) {
|
|
335
|
-
|
|
343
|
+
// Convert pagination timeout from seconds to milliseconds (0 = no timeout)
|
|
344
|
+
const paginationTimeout = (_a = advancedOptions.paginationTimeout) !== null && _a !== void 0 ? _a : 300;
|
|
345
|
+
responseData = await helpers_1.lemonSqueezyApiRequestAllItems.call(this, 'GET', `/${endpoint}`, qs, {
|
|
346
|
+
timeout: paginationTimeout > 0 ? paginationTimeout * 1000 : 0,
|
|
347
|
+
});
|
|
336
348
|
}
|
|
337
349
|
else {
|
|
338
350
|
const limit = this.getNodeParameter('limit', i);
|
|
@@ -90,14 +90,6 @@ 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
|
-
};
|
|
101
93
|
// Check if we have stored webhook data
|
|
102
94
|
if (webhookData.webhookId) {
|
|
103
95
|
try {
|
|
@@ -106,17 +98,9 @@ class LemonSqueezyTrigger {
|
|
|
106
98
|
return true;
|
|
107
99
|
}
|
|
108
100
|
catch (error) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
}
|
|
101
|
+
// Webhook not found or error occurred - will recreate
|
|
102
|
+
// Silently handle both 404 (deleted externally) and other errors
|
|
103
|
+
void error; // Acknowledge error without logging
|
|
120
104
|
delete webhookData.webhookId;
|
|
121
105
|
return false;
|
|
122
106
|
}
|
|
@@ -132,16 +116,12 @@ class LemonSqueezyTrigger {
|
|
|
132
116
|
const existingWebhook = webhooks.find((webhook) => { var _a; return ((_a = webhook.attributes) === null || _a === void 0 ? void 0 : _a.url) === webhookUrl; });
|
|
133
117
|
if (existingWebhook) {
|
|
134
118
|
webhookData.webhookId = existingWebhook.id;
|
|
135
|
-
// eslint-disable-next-line no-console
|
|
136
|
-
console.log(`[LemonSqueezy] Found existing webhook ${String(existingWebhook.id)} for URL ${webhookUrl}`);
|
|
137
119
|
return true;
|
|
138
120
|
}
|
|
139
121
|
}
|
|
140
122
|
}
|
|
141
|
-
catch
|
|
142
|
-
// Error checking webhooks -
|
|
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
|
+
catch {
|
|
124
|
+
// Error checking webhooks - will attempt to create new webhook
|
|
145
125
|
}
|
|
146
126
|
return false;
|
|
147
127
|
},
|
|
@@ -171,12 +151,42 @@ class LemonSqueezyTrigger {
|
|
|
171
151
|
},
|
|
172
152
|
},
|
|
173
153
|
};
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
154
|
+
try {
|
|
155
|
+
const response = await helpers_1.lemonSqueezyApiRequest.call(this, 'POST', '/webhooks', body);
|
|
156
|
+
const responseData = response;
|
|
157
|
+
const data = responseData.data;
|
|
158
|
+
if (data === null || data === void 0 ? void 0 : data.id) {
|
|
159
|
+
webhookData.webhookId = data.id;
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
// Handle race condition: if webhook with same URL was created between checkExists and create
|
|
165
|
+
// Check if error is 409 Conflict or similar, then try to find existing webhook
|
|
166
|
+
const isConflictOrDuplicate = error &&
|
|
167
|
+
typeof error === 'object' &&
|
|
168
|
+
(error.statusCode === 409 ||
|
|
169
|
+
error.statusCode === 422);
|
|
170
|
+
if (isConflictOrDuplicate) {
|
|
171
|
+
// Try to find the existing webhook
|
|
172
|
+
try {
|
|
173
|
+
const existingResponse = await helpers_1.lemonSqueezyApiRequest.call(this, 'GET', '/webhooks', undefined, { 'filter[store_id]': storeId });
|
|
174
|
+
const existingData = existingResponse;
|
|
175
|
+
const webhooks = existingData.data;
|
|
176
|
+
if (Array.isArray(webhooks)) {
|
|
177
|
+
const existingWebhook = webhooks.find((webhook) => { var _a; return ((_a = webhook.attributes) === null || _a === void 0 ? void 0 : _a.url) === webhookUrl; });
|
|
178
|
+
if (existingWebhook === null || existingWebhook === void 0 ? void 0 : existingWebhook.id) {
|
|
179
|
+
webhookData.webhookId = existingWebhook.id;
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// Failed to fetch existing webhook after conflict - will re-throw original error
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Re-throw original error if not a handled conflict
|
|
189
|
+
throw error;
|
|
180
190
|
}
|
|
181
191
|
return false;
|
|
182
192
|
},
|
|
@@ -186,25 +196,9 @@ class LemonSqueezyTrigger {
|
|
|
186
196
|
const webhookId = String(webhookData.webhookId);
|
|
187
197
|
try {
|
|
188
198
|
await helpers_1.lemonSqueezyApiRequest.call(this, 'DELETE', `/webhooks/${webhookId}`);
|
|
189
|
-
// eslint-disable-next-line no-console
|
|
190
|
-
console.log(`[LemonSqueezy] Webhook ${webhookId} deleted successfully`);
|
|
191
199
|
}
|
|
192
|
-
catch
|
|
193
|
-
//
|
|
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
|
-
}
|
|
200
|
+
catch {
|
|
201
|
+
// Silently handle deletion errors (404 = already deleted, others = continue cleanup)
|
|
208
202
|
}
|
|
209
203
|
delete webhookData.webhookId;
|
|
210
204
|
}
|
|
@@ -282,9 +276,6 @@ class LemonSqueezyTrigger {
|
|
|
282
276
|
const subscribedEvents = this.getNodeParameter('events');
|
|
283
277
|
if (!eventName || !subscribedEvents.includes(eventName)) {
|
|
284
278
|
// 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.`);
|
|
288
279
|
return {
|
|
289
280
|
webhookResponse: {
|
|
290
281
|
status: 200,
|
|
@@ -118,18 +118,6 @@ export declare function validateField(fieldName: string, value: unknown, validat
|
|
|
118
118
|
* // Throws: "config contains invalid JSON"
|
|
119
119
|
*/
|
|
120
120
|
export declare function safeJsonParse<T = unknown>(jsonString: string, fieldName: string): T;
|
|
121
|
-
/**
|
|
122
|
-
* Pauses execution for a specified duration.
|
|
123
|
-
*
|
|
124
|
-
* Used for implementing retry delays and rate limit backoff.
|
|
125
|
-
*
|
|
126
|
-
* @param ms - The number of milliseconds to sleep
|
|
127
|
-
* @returns A promise that resolves after the specified duration
|
|
128
|
-
*
|
|
129
|
-
* @example
|
|
130
|
-
* await sleep(1000) // Wait 1 second
|
|
131
|
-
*/
|
|
132
|
-
export declare function sleep(ms: number): Promise<void>;
|
|
133
121
|
/**
|
|
134
122
|
* Checks if an error is a rate limit error (HTTP 429).
|
|
135
123
|
*
|
|
@@ -51,7 +51,6 @@ exports.isValidIsoDate = isValidIsoDate;
|
|
|
51
51
|
exports.isPositiveInteger = isPositiveInteger;
|
|
52
52
|
exports.validateField = validateField;
|
|
53
53
|
exports.safeJsonParse = safeJsonParse;
|
|
54
|
-
exports.sleep = sleep;
|
|
55
54
|
exports.isRateLimitError = isRateLimitError;
|
|
56
55
|
exports.isRetryableError = isRetryableError;
|
|
57
56
|
exports.lemonSqueezyApiRequest = lemonSqueezyApiRequest;
|
|
@@ -270,20 +269,6 @@ function safeJsonParse(jsonString, fieldName) {
|
|
|
270
269
|
throw new Error(`${fieldName} contains invalid JSON`);
|
|
271
270
|
}
|
|
272
271
|
}
|
|
273
|
-
/**
|
|
274
|
-
* Pauses execution for a specified duration.
|
|
275
|
-
*
|
|
276
|
-
* Used for implementing retry delays and rate limit backoff.
|
|
277
|
-
*
|
|
278
|
-
* @param ms - The number of milliseconds to sleep
|
|
279
|
-
* @returns A promise that resolves after the specified duration
|
|
280
|
-
*
|
|
281
|
-
* @example
|
|
282
|
-
* await sleep(1000) // Wait 1 second
|
|
283
|
-
*/
|
|
284
|
-
function sleep(ms) {
|
|
285
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
286
|
-
}
|
|
287
272
|
/**
|
|
288
273
|
* Checks if an error is a rate limit error (HTTP 429).
|
|
289
274
|
*
|
|
@@ -376,37 +361,14 @@ async function lemonSqueezyApiRequest(method, endpoint, body, qs = {}, timeout =
|
|
|
376
361
|
if (body) {
|
|
377
362
|
options.body = body;
|
|
378
363
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (isRateLimitError(error)) {
|
|
387
|
-
// Log rate limit for visibility
|
|
388
|
-
// eslint-disable-next-line no-console
|
|
389
|
-
console.warn(`[LemonSqueezy] Rate limited (429) on ${method} ${endpoint}. Waiting ${constants_1.RATE_LIMIT_DELAY_MS / 1000}s before retry...`);
|
|
390
|
-
await sleep(constants_1.RATE_LIMIT_DELAY_MS);
|
|
391
|
-
continue;
|
|
392
|
-
}
|
|
393
|
-
if (isRetryableError(error) && attempt < constants_1.MAX_RETRIES - 1) {
|
|
394
|
-
const delayMs = constants_1.RETRY_DELAY_MS * Math.pow(2, attempt);
|
|
395
|
-
// eslint-disable-next-line no-console
|
|
396
|
-
console.warn(`[LemonSqueezy] Retryable error on ${method} ${endpoint} (attempt ${attempt + 1}/${constants_1.MAX_RETRIES}). Retrying in ${delayMs}ms...`);
|
|
397
|
-
await sleep(delayMs);
|
|
398
|
-
continue;
|
|
399
|
-
}
|
|
400
|
-
// Non-retryable error, throw immediately
|
|
401
|
-
throw new n8n_workflow_1.NodeApiError(this.getNode(), error, {
|
|
402
|
-
message: getErrorMessage(error),
|
|
403
|
-
});
|
|
404
|
-
}
|
|
364
|
+
try {
|
|
365
|
+
return (await this.helpers.httpRequestWithAuthentication.call(this, 'lemonSqueezyApi', options));
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
throw new n8n_workflow_1.NodeApiError(this.getNode(), error, {
|
|
369
|
+
message: getErrorMessage(error),
|
|
370
|
+
});
|
|
405
371
|
}
|
|
406
|
-
// All retries exhausted
|
|
407
|
-
throw new n8n_workflow_1.NodeApiError(this.getNode(), lastError, {
|
|
408
|
-
message: getErrorMessage(lastError),
|
|
409
|
-
});
|
|
410
372
|
}
|
|
411
373
|
/**
|
|
412
374
|
* Makes paginated requests to fetch all items from a Lemon Squeezy API endpoint.
|
|
@@ -446,8 +408,8 @@ async function lemonSqueezyApiRequestAllItems(method, endpoint, qs = {}, paginat
|
|
|
446
408
|
const startTime = Date.now();
|
|
447
409
|
qs['page[size]'] = pageSize;
|
|
448
410
|
do {
|
|
449
|
-
// Check timeout
|
|
450
|
-
if (Date.now() - startTime > timeout) {
|
|
411
|
+
// Check timeout (0 = no timeout)
|
|
412
|
+
if (timeout > 0 && Date.now() - startTime > timeout) {
|
|
451
413
|
throw new n8n_workflow_1.NodeApiError(this.getNode(), {}, {
|
|
452
414
|
message: `Pagination timeout exceeded (${timeout}ms). Retrieved ${returnData.length} items before timeout.`,
|
|
453
415
|
});
|
|
@@ -460,15 +422,9 @@ async function lemonSqueezyApiRequestAllItems(method, endpoint, qs = {}, paginat
|
|
|
460
422
|
};
|
|
461
423
|
let responseData;
|
|
462
424
|
try {
|
|
463
|
-
responseData = (await this.helpers.
|
|
425
|
+
responseData = (await this.helpers.httpRequestWithAuthentication.call(this, 'lemonSqueezyApi', options));
|
|
464
426
|
}
|
|
465
427
|
catch (error) {
|
|
466
|
-
if (isRateLimitError(error)) {
|
|
467
|
-
// eslint-disable-next-line no-console
|
|
468
|
-
console.warn(`[LemonSqueezy] Rate limited during pagination (${returnData.length} items fetched). Waiting ${constants_1.RATE_LIMIT_DELAY_MS / 1000}s...`);
|
|
469
|
-
await sleep(constants_1.RATE_LIMIT_DELAY_MS);
|
|
470
|
-
continue;
|
|
471
|
-
}
|
|
472
428
|
throw new n8n_workflow_1.NodeApiError(this.getNode(), error, {
|
|
473
429
|
message: getErrorMessage(error),
|
|
474
430
|
});
|
|
@@ -13,6 +13,12 @@ exports.usageRecordOperations = [
|
|
|
13
13
|
},
|
|
14
14
|
},
|
|
15
15
|
options: [
|
|
16
|
+
{
|
|
17
|
+
name: 'Create',
|
|
18
|
+
value: 'create',
|
|
19
|
+
description: 'Create a usage record',
|
|
20
|
+
action: 'Create a usage record',
|
|
21
|
+
},
|
|
16
22
|
{
|
|
17
23
|
name: 'Get',
|
|
18
24
|
value: 'get',
|
|
@@ -30,6 +36,63 @@ exports.usageRecordOperations = [
|
|
|
30
36
|
},
|
|
31
37
|
];
|
|
32
38
|
exports.usageRecordFields = [
|
|
39
|
+
// Create
|
|
40
|
+
{
|
|
41
|
+
displayName: 'Subscription Item ID',
|
|
42
|
+
name: 'subscriptionItemId',
|
|
43
|
+
type: 'string',
|
|
44
|
+
required: true,
|
|
45
|
+
default: '',
|
|
46
|
+
displayOptions: {
|
|
47
|
+
show: {
|
|
48
|
+
resource: ['usageRecord'],
|
|
49
|
+
operation: ['create'],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
description: 'The ID of the subscription item to record usage for',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
displayName: 'Quantity',
|
|
56
|
+
name: 'quantity',
|
|
57
|
+
type: 'number',
|
|
58
|
+
required: true,
|
|
59
|
+
default: 1,
|
|
60
|
+
typeOptions: {
|
|
61
|
+
minValue: 1,
|
|
62
|
+
},
|
|
63
|
+
displayOptions: {
|
|
64
|
+
show: {
|
|
65
|
+
resource: ['usageRecord'],
|
|
66
|
+
operation: ['create'],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
description: 'The usage quantity to record',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
displayName: 'Action',
|
|
73
|
+
name: 'action',
|
|
74
|
+
type: 'options',
|
|
75
|
+
default: 'increment',
|
|
76
|
+
displayOptions: {
|
|
77
|
+
show: {
|
|
78
|
+
resource: ['usageRecord'],
|
|
79
|
+
operation: ['create'],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
options: [
|
|
83
|
+
{
|
|
84
|
+
name: 'Increment',
|
|
85
|
+
value: 'increment',
|
|
86
|
+
description: 'Add to existing usage',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'Set',
|
|
90
|
+
value: 'set',
|
|
91
|
+
description: 'Set the usage to an exact value',
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
description: 'Whether to increment the existing usage or set it to an exact value',
|
|
95
|
+
},
|
|
33
96
|
// Get
|
|
34
97
|
{
|
|
35
98
|
displayName: 'Usage Record ID',
|
|
@@ -64,7 +64,8 @@ exports.webhookFields = [
|
|
|
64
64
|
type: 'string',
|
|
65
65
|
required: true,
|
|
66
66
|
default: '',
|
|
67
|
-
|
|
67
|
+
placeholder: 'e.g., 12345',
|
|
68
|
+
description: 'The ID of the store this webhook belongs to. Find this in your <a href="https://app.lemonsqueezy.com/settings/stores" target="_blank">Lemon Squeezy Dashboard</a>.',
|
|
68
69
|
displayOptions: {
|
|
69
70
|
show: { resource: ['webhook'], operation: ['create'] },
|
|
70
71
|
},
|
|
@@ -76,7 +77,7 @@ exports.webhookFields = [
|
|
|
76
77
|
required: true,
|
|
77
78
|
default: '',
|
|
78
79
|
placeholder: 'https://your-app.com/webhooks/lemonsqueezy',
|
|
79
|
-
description: 'The URL to
|
|
80
|
+
description: 'The publicly accessible HTTPS URL to receive webhook events. Must be reachable from the internet.',
|
|
80
81
|
displayOptions: {
|
|
81
82
|
show: { resource: ['webhook'], operation: ['create'] },
|
|
82
83
|
},
|
|
@@ -88,7 +89,7 @@ exports.webhookFields = [
|
|
|
88
89
|
required: true,
|
|
89
90
|
options: constants_1.WEBHOOK_EVENTS,
|
|
90
91
|
default: [],
|
|
91
|
-
description: '
|
|
92
|
+
description: 'Select which events should trigger this webhook. See <a href="https://docs.lemonsqueezy.com/api/webhooks#event-types" target="_blank">Lemon Squeezy Webhook Events</a> for details.',
|
|
92
93
|
displayOptions: {
|
|
93
94
|
show: { resource: ['webhook'], operation: ['create'] },
|
|
94
95
|
},
|
|
@@ -100,7 +101,8 @@ exports.webhookFields = [
|
|
|
100
101
|
required: true,
|
|
101
102
|
default: '',
|
|
102
103
|
typeOptions: { password: true },
|
|
103
|
-
|
|
104
|
+
placeholder: 'e.g., a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6',
|
|
105
|
+
description: 'A secure random string (minimum 32 characters) used to sign webhook payloads. Generate one using: openssl rand -hex 32',
|
|
104
106
|
displayOptions: {
|
|
105
107
|
show: { resource: ['webhook'], operation: ['create'] },
|
|
106
108
|
},
|
|
@@ -140,7 +142,8 @@ exports.webhookFields = [
|
|
|
140
142
|
name: 'url',
|
|
141
143
|
type: 'string',
|
|
142
144
|
default: '',
|
|
143
|
-
|
|
145
|
+
placeholder: 'https://your-app.com/webhooks/lemonsqueezy',
|
|
146
|
+
description: 'The publicly accessible HTTPS URL to receive webhook events',
|
|
144
147
|
},
|
|
145
148
|
{
|
|
146
149
|
displayName: 'Events',
|
|
@@ -148,7 +151,7 @@ exports.webhookFields = [
|
|
|
148
151
|
type: 'multiOptions',
|
|
149
152
|
options: constants_1.WEBHOOK_EVENTS,
|
|
150
153
|
default: [],
|
|
151
|
-
description: '
|
|
154
|
+
description: 'Select which events should trigger this webhook. See <a href="https://docs.lemonsqueezy.com/api/webhooks#event-types" target="_blank">Lemon Squeezy docs</a> for details.',
|
|
152
155
|
},
|
|
153
156
|
{
|
|
154
157
|
displayName: 'Secret',
|
|
@@ -156,7 +159,8 @@ exports.webhookFields = [
|
|
|
156
159
|
type: 'string',
|
|
157
160
|
typeOptions: { password: true },
|
|
158
161
|
default: '',
|
|
159
|
-
|
|
162
|
+
placeholder: 'e.g., a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6',
|
|
163
|
+
description: 'A secure random string (minimum 32 characters) used to sign webhook payloads. Generate one using: openssl rand -hex 32',
|
|
160
164
|
},
|
|
161
165
|
],
|
|
162
166
|
},
|