n8n-nodes-lemonsqueezy 0.8.0 → 0.9.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
@@ -65,7 +65,7 @@ The main node for interacting with the Lemon Squeezy API.
65
65
  | **Product** | Get, Get Many |
66
66
  | **Store** | Get, Get Many |
67
67
  | **Subscription** | Get, Get Many, Update, Cancel, Resume |
68
- | **Subscription Invoice** | Get, Get Many |
68
+ | **Subscription Invoice** | Get, Get Many, Generate, Refund |
69
69
  | **Usage Record** | Create, Get, Get Many |
70
70
  | **User** | Get Current |
71
71
  | **Variant** | Get, Get Many |
@@ -304,6 +304,21 @@ Contributions are welcome! Please feel free to submit a Pull Request.
304
304
 
305
305
  ## Changelog
306
306
 
307
+ ### v0.9.0
308
+
309
+ **New Features:**
310
+ - Added **Subscription Invoice Generate operation** for generating invoices on subscriptions with outstanding balances
311
+ - Added **Subscription Invoice Refund operation** for issuing full or partial refunds
312
+ - Added custom data payload size validation (max 10KB) for checkout creation
313
+ - Added Retry-After header extraction for smarter rate limit handling
314
+ - Added resource validation guard to catch unknown resources early
315
+
316
+ **Improvements:**
317
+ - Added JSDoc documentation to all resource files
318
+ - Improved field descriptions with units (cents), ranges (0-100), and examples
319
+ - Added maxValue (100) to all limit fields for API compliance
320
+ - 189 tests with comprehensive coverage
321
+
307
322
  ### v0.8.0
308
323
 
309
324
  **New Features:**
@@ -95,6 +95,8 @@ async function handleCreate(ctx, resource, itemIndex) {
95
95
  }
96
96
  if (additionalOptions.customData !== undefined && additionalOptions.customData !== null) {
97
97
  const customDataValue = additionalOptions.customData;
98
+ // Validate payload size before processing (max 10KB)
99
+ (0, helpers_1.validateCustomDataSize)(customDataValue);
98
100
  if (typeof customDataValue === 'string') {
99
101
  try {
100
102
  const parsed = JSON.parse(customDataValue);
@@ -356,6 +358,10 @@ class LemonSqueezy {
356
358
  try {
357
359
  let responseData;
358
360
  const endpoint = constants_1.RESOURCE_ENDPOINTS[resource];
361
+ // Validate that the resource exists in our endpoints mapping
362
+ if (!endpoint && !['user'].includes(resource)) {
363
+ throw new Error(`Unknown resource: ${resource}`);
364
+ }
359
365
  if (operation === 'get') {
360
366
  const idParam = constants_1.RESOURCE_ID_PARAMS[resource];
361
367
  const id = this.getNodeParameter(idParam, i);
@@ -444,6 +450,21 @@ class LemonSqueezy {
444
450
  else if (resource === 'user' && operation === 'getCurrent') {
445
451
  responseData = await helpers_1.lemonSqueezyApiRequest.call(this, 'GET', '/users/me');
446
452
  }
453
+ else if (resource === 'subscriptionInvoice') {
454
+ if (operation === 'generate') {
455
+ const subscriptionId = this.getNodeParameter('generateSubscriptionId', i);
456
+ const body = (0, helpers_1.buildJsonApiBody)('subscription-invoices', {}, { subscription: { type: 'subscriptions', id: subscriptionId } });
457
+ responseData = await helpers_1.lemonSqueezyApiRequest.call(this, 'POST', '/subscription-invoices', body);
458
+ }
459
+ else if (operation === 'refund') {
460
+ const invoiceId = this.getNodeParameter('subscriptionInvoiceId', i);
461
+ const refundAmount = this.getNodeParameter('refundAmount', i, 0);
462
+ const body = refundAmount > 0
463
+ ? { data: { type: 'subscription-invoices', attributes: { amount: refundAmount } } }
464
+ : {};
465
+ responseData = await helpers_1.lemonSqueezyApiRequest.call(this, 'POST', `/subscription-invoices/${invoiceId}/refund`, body);
466
+ }
467
+ }
447
468
  const executionData = this.helpers.constructExecutionMetaData(this.helpers.returnJsonArray(responseData), { itemData: { item: i } });
448
469
  returnData.push(...executionData);
449
470
  }
@@ -139,6 +139,34 @@ export declare function safeJsonParse<T = unknown>(jsonString: string, fieldName
139
139
  * validateDiscountAmount(-100, 'fixed') // throws "Fixed discount amount must be a positive integer"
140
140
  */
141
141
  export declare function validateDiscountAmount(amount: number, amountType: string): void;
142
+ /**
143
+ * Validates that a custom data payload doesn't exceed the maximum size.
144
+ *
145
+ * This prevents memory issues and potential abuse from extremely large payloads.
146
+ *
147
+ * @param data - The custom data object or string to validate
148
+ * @param maxSizeBytes - Maximum allowed size in bytes (default: 10KB)
149
+ * @throws Error if the payload exceeds the maximum size
150
+ *
151
+ * @example
152
+ * validateCustomDataSize({ key: 'value' }) // passes
153
+ * validateCustomDataSize(veryLargeObject) // throws if > 10KB
154
+ */
155
+ export declare function validateCustomDataSize(data: unknown, maxSizeBytes?: number): void;
156
+ /**
157
+ * Extracts the Retry-After header value from an error response.
158
+ *
159
+ * The Retry-After header indicates how long to wait before retrying
160
+ * a rate-limited or temporarily unavailable request.
161
+ *
162
+ * @param error - The error object that may contain Retry-After header
163
+ * @returns Number of seconds to wait, or undefined if not present
164
+ *
165
+ * @example
166
+ * getRetryAfterSeconds({ response: { headers: { 'retry-after': '60' } } }) // 60
167
+ * getRetryAfterSeconds({ response: { headers: {} } }) // undefined
168
+ */
169
+ export declare function getRetryAfterSeconds(error: unknown): number | undefined;
142
170
  /**
143
171
  * Checks if an error is a rate limit error (HTTP 429).
144
172
  *
@@ -52,6 +52,8 @@ exports.isPositiveInteger = isPositiveInteger;
52
52
  exports.validateField = validateField;
53
53
  exports.safeJsonParse = safeJsonParse;
54
54
  exports.validateDiscountAmount = validateDiscountAmount;
55
+ exports.validateCustomDataSize = validateCustomDataSize;
56
+ exports.getRetryAfterSeconds = getRetryAfterSeconds;
55
57
  exports.isRateLimitError = isRateLimitError;
56
58
  exports.isRetryableError = isRetryableError;
57
59
  exports.lemonSqueezyApiRequest = lemonSqueezyApiRequest;
@@ -311,6 +313,57 @@ function validateDiscountAmount(amount, amountType) {
311
313
  }
312
314
  }
313
315
  }
316
+ /** Maximum payload size for custom data (10KB) */
317
+ const MAX_CUSTOM_DATA_SIZE_BYTES = 10 * 1024;
318
+ /**
319
+ * Validates that a custom data payload doesn't exceed the maximum size.
320
+ *
321
+ * This prevents memory issues and potential abuse from extremely large payloads.
322
+ *
323
+ * @param data - The custom data object or string to validate
324
+ * @param maxSizeBytes - Maximum allowed size in bytes (default: 10KB)
325
+ * @throws Error if the payload exceeds the maximum size
326
+ *
327
+ * @example
328
+ * validateCustomDataSize({ key: 'value' }) // passes
329
+ * validateCustomDataSize(veryLargeObject) // throws if > 10KB
330
+ */
331
+ function validateCustomDataSize(data, maxSizeBytes = MAX_CUSTOM_DATA_SIZE_BYTES) {
332
+ const jsonString = typeof data === 'string' ? data : JSON.stringify(data);
333
+ const sizeBytes = Buffer.byteLength(jsonString, 'utf8');
334
+ if (sizeBytes > maxSizeBytes) {
335
+ const sizeKb = Math.round(sizeBytes / 1024);
336
+ const maxKb = Math.round(maxSizeBytes / 1024);
337
+ throw new Error(`Custom data exceeds maximum size (${sizeKb}KB > ${maxKb}KB). Reduce the payload size.`);
338
+ }
339
+ }
340
+ /**
341
+ * Extracts the Retry-After header value from an error response.
342
+ *
343
+ * The Retry-After header indicates how long to wait before retrying
344
+ * a rate-limited or temporarily unavailable request.
345
+ *
346
+ * @param error - The error object that may contain Retry-After header
347
+ * @returns Number of seconds to wait, or undefined if not present
348
+ *
349
+ * @example
350
+ * getRetryAfterSeconds({ response: { headers: { 'retry-after': '60' } } }) // 60
351
+ * getRetryAfterSeconds({ response: { headers: {} } }) // undefined
352
+ */
353
+ function getRetryAfterSeconds(error) {
354
+ var _a, _b;
355
+ if (error && typeof error === 'object') {
356
+ const err = error;
357
+ const retryAfter = (_b = (_a = err.response) === null || _a === void 0 ? void 0 : _a.headers) === null || _b === void 0 ? void 0 : _b['retry-after'];
358
+ if (retryAfter) {
359
+ const seconds = parseInt(retryAfter, 10);
360
+ if (!isNaN(seconds) && seconds > 0) {
361
+ return seconds;
362
+ }
363
+ }
364
+ }
365
+ return undefined;
366
+ }
314
367
  /**
315
368
  * Checks if an error is a rate limit error (HTTP 429).
316
369
  *
@@ -1,3 +1,17 @@
1
+ /**
2
+ * Customer Resource
3
+ *
4
+ * Provides operations for managing customers in Lemon Squeezy.
5
+ *
6
+ * Available operations:
7
+ * - Create: Create a new customer
8
+ * - Delete: Archive a customer
9
+ * - Get: Retrieve a single customer by ID
10
+ * - Get Many: Retrieve multiple customers with filtering
11
+ * - Update: Update customer information
12
+ *
13
+ * @see https://docs.lemonsqueezy.com/api/customers
14
+ */
1
15
  import type { INodeProperties } from 'n8n-workflow';
2
16
  export declare const customerOperations: INodeProperties;
3
17
  export declare const customerFields: INodeProperties[];
@@ -1,3 +1,17 @@
1
+ /**
2
+ * Discount Resource
3
+ *
4
+ * Provides operations for managing discount codes in Lemon Squeezy.
5
+ *
6
+ * Available operations:
7
+ * - Create: Create a new discount code
8
+ * - Delete: Delete an existing discount code
9
+ * - Get: Retrieve a single discount by ID
10
+ * - Get Many: Retrieve multiple discounts with filtering
11
+ * - Update: Update an existing discount code
12
+ *
13
+ * @see https://docs.lemonsqueezy.com/api/discounts
14
+ */
1
15
  import type { INodeProperties } from 'n8n-workflow';
2
16
  export declare const discountOperations: INodeProperties;
3
17
  export declare const discountFields: INodeProperties[];
@@ -98,7 +98,7 @@ exports.discountFields = [
98
98
  type: 'number',
99
99
  required: true,
100
100
  default: 0,
101
- description: 'Discount amount (percentage 0-100 or fixed amount in cents)',
101
+ description: 'Discount amount. For percent type: 0-100 (e.g., 25 = 25% off). For fixed type: amount in cents (e.g., 1000 = $10.00 off).',
102
102
  displayOptions: {
103
103
  show: { resource: ['discount'], operation: ['create'] },
104
104
  },
@@ -1,3 +1,16 @@
1
+ /**
2
+ * Order Resource
3
+ *
4
+ * Provides operations for managing orders in Lemon Squeezy.
5
+ * Orders are created when customers complete purchases.
6
+ *
7
+ * Available operations:
8
+ * - Get: Retrieve a single order by ID
9
+ * - Get Many: Retrieve multiple orders with filtering
10
+ * - Refund: Issue a full refund for an order
11
+ *
12
+ * @see https://docs.lemonsqueezy.com/api/orders
13
+ */
1
14
  import type { INodeProperties } from 'n8n-workflow';
2
15
  export declare const orderOperations: INodeProperties;
3
16
  export declare const orderFields: INodeProperties[];
@@ -62,8 +62,8 @@ exports.orderFields = [
62
62
  name: 'limit',
63
63
  type: 'number',
64
64
  default: 50,
65
- description: 'Max number of results to return',
66
- typeOptions: { minValue: 1 },
65
+ description: 'Max number of results to return (API maximum is 100 per page)',
66
+ typeOptions: { minValue: 1, maxValue: 100 },
67
67
  displayOptions: {
68
68
  show: { resource: ['order'], operation: ['getAll'], returnAll: [false] },
69
69
  },
@@ -1,3 +1,16 @@
1
+ /**
2
+ * Product Resource
3
+ *
4
+ * Provides read-only operations for products in Lemon Squeezy.
5
+ * Products are managed in the Lemon Squeezy dashboard and cannot be
6
+ * created, updated, or deleted via the API.
7
+ *
8
+ * Available operations:
9
+ * - Get: Retrieve a single product by ID
10
+ * - Get Many: Retrieve multiple products with filtering
11
+ *
12
+ * @see https://docs.lemonsqueezy.com/api/products
13
+ */
1
14
  import type { INodeProperties } from 'n8n-workflow';
2
15
  export declare const productOperations: INodeProperties;
3
16
  export declare const productFields: INodeProperties[];
@@ -56,8 +56,8 @@ exports.productFields = [
56
56
  name: 'limit',
57
57
  type: 'number',
58
58
  default: 50,
59
- description: 'Max number of results to return',
60
- typeOptions: { minValue: 1 },
59
+ description: 'Max number of results to return (API maximum is 100 per page)',
60
+ typeOptions: { minValue: 1, maxValue: 100 },
61
61
  displayOptions: {
62
62
  show: { resource: ['product'], operation: ['getAll'], returnAll: [false] },
63
63
  },
@@ -1,3 +1,16 @@
1
+ /**
2
+ * Subscription Invoice Resource
3
+ *
4
+ * Provides operations for managing subscription invoices in Lemon Squeezy.
5
+ *
6
+ * Available operations:
7
+ * - Get: Retrieve a single subscription invoice by ID
8
+ * - Get Many: Retrieve multiple subscription invoices with filtering
9
+ * - Generate: Generate a new invoice for an outstanding balance
10
+ * - Refund: Issue a refund for a subscription invoice
11
+ *
12
+ * @see https://docs.lemonsqueezy.com/api/subscription-invoices
13
+ */
1
14
  import type { INodeProperties } from 'n8n-workflow';
2
15
  export declare const subscriptionInvoiceOperations: INodeProperties[];
3
16
  export declare const subscriptionInvoiceFields: INodeProperties[];
@@ -13,6 +13,12 @@ exports.subscriptionInvoiceOperations = [
13
13
  },
14
14
  },
15
15
  options: [
16
+ {
17
+ name: 'Generate',
18
+ value: 'generate',
19
+ description: 'Generate an invoice for outstanding subscription balance',
20
+ action: 'Generate a subscription invoice',
21
+ },
16
22
  {
17
23
  name: 'Get',
18
24
  value: 'get',
@@ -25,12 +31,18 @@ exports.subscriptionInvoiceOperations = [
25
31
  description: 'Get many subscription invoices',
26
32
  action: 'Get many subscription invoices',
27
33
  },
34
+ {
35
+ name: 'Refund',
36
+ value: 'refund',
37
+ description: 'Issue a refund for a subscription invoice',
38
+ action: 'Refund a subscription invoice',
39
+ },
28
40
  ],
29
41
  default: 'getAll',
30
42
  },
31
43
  ];
32
44
  exports.subscriptionInvoiceFields = [
33
- // Get
45
+ // Subscription Invoice ID for Get and Refund operations
34
46
  {
35
47
  displayName: 'Subscription Invoice ID',
36
48
  name: 'subscriptionInvoiceId',
@@ -40,10 +52,43 @@ exports.subscriptionInvoiceFields = [
40
52
  displayOptions: {
41
53
  show: {
42
54
  resource: ['subscriptionInvoice'],
43
- operation: ['get'],
55
+ operation: ['get', 'refund'],
56
+ },
57
+ },
58
+ description: 'The ID of the subscription invoice (e.g., "123456")',
59
+ },
60
+ // Subscription ID for Generate operation
61
+ {
62
+ displayName: 'Subscription ID',
63
+ name: 'generateSubscriptionId',
64
+ type: 'string',
65
+ required: true,
66
+ default: '',
67
+ displayOptions: {
68
+ show: {
69
+ resource: ['subscriptionInvoice'],
70
+ operation: ['generate'],
71
+ },
72
+ },
73
+ description: 'The ID of the subscription to generate an invoice for. Only works if the subscription has an outstanding balance.',
74
+ },
75
+ // Refund Options
76
+ {
77
+ displayName: 'Refund Amount',
78
+ name: 'refundAmount',
79
+ type: 'number',
80
+ required: false,
81
+ default: 0,
82
+ displayOptions: {
83
+ show: {
84
+ resource: ['subscriptionInvoice'],
85
+ operation: ['refund'],
44
86
  },
45
87
  },
46
- description: 'The ID of the subscription invoice to retrieve',
88
+ description: 'Amount to refund in cents (e.g., 1000 = $10.00). Leave at 0 for full refund. Must not exceed the original invoice amount.',
89
+ typeOptions: {
90
+ minValue: 0,
91
+ },
47
92
  },
48
93
  // Get All
49
94
  {
@@ -1,3 +1,22 @@
1
+ /**
2
+ * Webhook Resource
3
+ *
4
+ * Provides operations for managing webhooks in Lemon Squeezy.
5
+ * Webhooks allow your application to receive real-time notifications
6
+ * about events in your store.
7
+ *
8
+ * Available operations:
9
+ * - Create: Create a new webhook endpoint
10
+ * - Delete: Delete an existing webhook
11
+ * - Get: Retrieve a single webhook by ID
12
+ * - Get Many: Retrieve multiple webhooks with filtering
13
+ * - Update: Update webhook URL, events, or secret
14
+ *
15
+ * Security: Webhook secrets must be at least 32 characters.
16
+ * Generate one using: openssl rand -hex 32
17
+ *
18
+ * @see https://docs.lemonsqueezy.com/api/webhooks
19
+ */
1
20
  import type { INodeProperties } from 'n8n-workflow';
2
21
  export declare const webhookOperations: INodeProperties;
3
22
  export declare const webhookFields: INodeProperties[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-lemonsqueezy",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "n8n community node for Lemon Squeezy - digital products and subscriptions platform",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",