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 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** - Email, URL, and date format 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
- try {
88
- checkoutData.custom = JSON.parse(additionalOptions.customData);
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
- catch {
91
- checkoutData.custom = additionalOptions.customData;
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: 'Verify Signature',
78
- name: 'verifySignature',
79
- type: 'boolean',
80
- default: true,
81
- description: 'Whether to verify the webhook signature (recommended)',
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
- // Webhook doesn't exist anymore
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, assume doesn't exist
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/${String(webhookData.webhookId)}`);
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
- // Webhook might already be deleted
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
- // Verify signature if enabled
181
- if (options.verifySignature !== false) {
182
- const signature = this.getHeaderData()['x-signature'];
183
- if (!signature) {
184
- return {
185
- webhookResponse: {
186
- status: 401,
187
- body: { error: 'Missing signature header' },
188
- },
189
- };
190
- }
191
- const bodyData = this.getBodyData();
192
- const rawBody = JSON.stringify(bodyData);
193
- const isValid = (0, helpers_1.verifyWebhookSignature)(rawBody, signature, webhookSecret);
194
- if (!isValid) {
195
- return {
196
- webhookResponse: {
197
- status: 401,
198
- body: { error: 'Invalid signature' },
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
  */