n8n-nodes-duckduckgo-search 32.0.0 → 32.2.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
@@ -1,9 +1,9 @@
1
1
  # DuckDuckGo Search Node for n8n
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/n8n-nodes-duckduckgo-search.svg?v=32.0.0)](https://www.npmjs.com/package/n8n-nodes-duckduckgo-search)
3
+ [![npm version](https://img.shields.io/npm/v/n8n-nodes-duckduckgo-search.svg?v=32.2.0)](https://www.npmjs.com/package/n8n-nodes-duckduckgo-search)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- A lightweight and fast n8n community node for DuckDuckGo search. Search the web, find images, discover news, and explore videos with privacy-focused results.
6
+ A professional-grade n8n community node for DuckDuckGo search with enterprise-level error handling, validation, and quality assurance. Search the web, find images, discover news, and explore videos with privacy-focused, reliable results.
7
7
 
8
8
  ## ✨ Features
9
9
 
@@ -6,6 +6,10 @@ const duck_duck_scrape_1 = require("duck-duck-scrape");
6
6
  const types_1 = require("./types");
7
7
  const processors_1 = require("./processors");
8
8
  const constants_1 = require("./constants");
9
+ const errors_1 = require("./errors");
10
+ const validation_1 = require("./validation");
11
+ const reliability_1 = require("./reliability");
12
+ const adaptiveRateLimiter_1 = require("./adaptiveRateLimiter");
9
13
  const LOCALE_OPTIONS = [
10
14
  { name: 'English (US)', value: 'en-us' },
11
15
  { name: 'English (UK)', value: 'uk-en' },
@@ -202,102 +206,167 @@ class DuckDuckGo {
202
206
  };
203
207
  }
204
208
  async execute() {
205
- var _a, _b, _c, _d;
206
209
  const items = this.getInputData();
207
210
  const returnData = [];
208
211
  for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
209
212
  try {
210
213
  const operation = this.getNodeParameter('operation', itemIndex);
211
- const query = this.getNodeParameter('query', itemIndex);
212
- const locale = this.getNodeParameter('locale', itemIndex);
213
- const maxResults = this.getNodeParameter('maxResults', itemIndex);
214
- const region = this.getNodeParameter('region', itemIndex);
215
- const safeSearch = this.getNodeParameter('safeSearch', itemIndex);
216
- const timePeriod = this.getNodeParameter('timePeriod', itemIndex);
217
- if (!query || query.trim() === '') {
218
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Search query cannot be empty', { itemIndex });
214
+ const adaptiveRateLimiter = (0, adaptiveRateLimiter_1.getAdaptiveRateLimiter)(operation);
215
+ await adaptiveRateLimiter.wait();
216
+ const rawQuery = this.getNodeParameter('query', itemIndex);
217
+ const rawLocale = this.getNodeParameter('locale', itemIndex);
218
+ const rawMaxResults = this.getNodeParameter('maxResults', itemIndex);
219
+ const rawRegion = this.getNodeParameter('region', itemIndex);
220
+ const rawSafeSearch = this.getNodeParameter('safeSearch', itemIndex);
221
+ const rawTimePeriod = this.getNodeParameter('timePeriod', itemIndex);
222
+ let query;
223
+ let maxResults;
224
+ let locale;
225
+ let region;
226
+ let safeSearch;
227
+ let timePeriod;
228
+ try {
229
+ query = (0, validation_1.validateSearchQuery)(rawQuery);
230
+ maxResults = (0, validation_1.validateMaxResults)(rawMaxResults);
231
+ locale = (0, validation_1.validateLocale)(rawLocale);
232
+ region = (0, validation_1.validateRegion)(rawRegion);
233
+ safeSearch = (0, validation_1.validateSafeSearch)(rawSafeSearch);
234
+ timePeriod = (0, validation_1.validateTimePeriod)(rawTimePeriod);
219
235
  }
220
- let results = [];
221
- if (operation === types_1.DuckDuckGoOperation.Search) {
222
- const searchResult = await (0, duck_duck_scrape_1.search)(query, {
223
- safeSearch: getSafeSearchType(safeSearch),
224
- locale: region || locale,
225
- time: getSearchTimeType(timePeriod),
226
- });
227
- if ((_a = searchResult === null || searchResult === void 0 ? void 0 : searchResult.results) === null || _a === void 0 ? void 0 : _a.length) {
228
- results = (0, processors_1.processWebSearchResults)(searchResult.results, itemIndex, searchResult).slice(0, maxResults);
229
- }
230
- else {
231
- results = [{
232
- json: { success: true, query, count: 0, results: [] },
233
- pairedItem: { item: itemIndex },
234
- }];
236
+ catch (validationError) {
237
+ if (validationError instanceof errors_1.ValidationError) {
238
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), validationError.getUserMessage(), { itemIndex });
235
239
  }
240
+ throw validationError;
236
241
  }
237
- else if (operation === types_1.DuckDuckGoOperation.SearchImages) {
238
- const searchResult = await (0, duck_duck_scrape_1.searchImages)(query, {
239
- safeSearch: getSafeSearchType(safeSearch),
240
- locale: region || locale,
241
- });
242
- if ((_b = searchResult === null || searchResult === void 0 ? void 0 : searchResult.results) === null || _b === void 0 ? void 0 : _b.length) {
243
- results = (0, processors_1.processImageSearchResults)(searchResult.results, itemIndex).slice(0, maxResults);
244
- }
245
- else {
246
- results = [{
247
- json: { success: true, query, count: 0, results: [] },
248
- pairedItem: { item: itemIndex },
249
- }];
242
+ let results = [];
243
+ const performSearch = async () => {
244
+ var _a, _b, _c, _d;
245
+ const timeout = 45000;
246
+ if (operation === types_1.DuckDuckGoOperation.Search) {
247
+ const searchResult = await (0, reliability_1.executeWithRetry)(() => (0, reliability_1.withTimeout)((0, duck_duck_scrape_1.search)(query, {
248
+ safeSearch: getSafeSearchType(safeSearch),
249
+ locale: region || locale,
250
+ time: getSearchTimeType(timePeriod),
251
+ }), timeout, 'Web Search'), `Web Search: "${query}"`, reliability_1.DEFAULT_RETRY_CONFIG);
252
+ if ((_a = searchResult === null || searchResult === void 0 ? void 0 : searchResult.results) === null || _a === void 0 ? void 0 : _a.length) {
253
+ results = (0, processors_1.processWebSearchResults)(searchResult.results, itemIndex, searchResult).slice(0, maxResults);
254
+ }
255
+ else {
256
+ results = [{
257
+ json: {
258
+ success: true,
259
+ query,
260
+ operation: 'web_search',
261
+ count: 0,
262
+ results: [],
263
+ },
264
+ pairedItem: { item: itemIndex },
265
+ }];
266
+ }
250
267
  }
251
- }
252
- else if (operation === types_1.DuckDuckGoOperation.SearchNews) {
253
- const searchResult = await (0, duck_duck_scrape_1.searchNews)(query, {
254
- safeSearch: getSafeSearchType(safeSearch),
255
- locale: region || locale,
256
- time: getSearchTimeType(timePeriod),
257
- });
258
- if ((_c = searchResult === null || searchResult === void 0 ? void 0 : searchResult.results) === null || _c === void 0 ? void 0 : _c.length) {
259
- results = (0, processors_1.processNewsSearchResults)(searchResult.results, itemIndex).slice(0, maxResults);
268
+ else if (operation === types_1.DuckDuckGoOperation.SearchImages) {
269
+ const searchResult = await (0, reliability_1.executeWithRetry)(() => (0, reliability_1.withTimeout)((0, duck_duck_scrape_1.searchImages)(query, {
270
+ safeSearch: getSafeSearchType(safeSearch),
271
+ locale: region || locale,
272
+ }), timeout, 'Image Search'), `Image Search: "${query}"`, reliability_1.DEFAULT_RETRY_CONFIG);
273
+ if ((_b = searchResult === null || searchResult === void 0 ? void 0 : searchResult.results) === null || _b === void 0 ? void 0 : _b.length) {
274
+ results = (0, processors_1.processImageSearchResults)(searchResult.results, itemIndex).slice(0, maxResults);
275
+ }
276
+ else {
277
+ results = [{
278
+ json: {
279
+ success: true,
280
+ query,
281
+ operation: 'image_search',
282
+ count: 0,
283
+ results: [],
284
+ },
285
+ pairedItem: { item: itemIndex },
286
+ }];
287
+ }
260
288
  }
261
- else {
262
- results = [{
263
- json: { success: true, query, count: 0, results: [] },
264
- pairedItem: { item: itemIndex },
265
- }];
289
+ else if (operation === types_1.DuckDuckGoOperation.SearchNews) {
290
+ const searchResult = await (0, reliability_1.executeWithRetry)(() => (0, reliability_1.withTimeout)((0, duck_duck_scrape_1.searchNews)(query, {
291
+ safeSearch: getSafeSearchType(safeSearch),
292
+ locale: region || locale,
293
+ time: getSearchTimeType(timePeriod),
294
+ }), timeout, 'News Search'), `News Search: "${query}"`, reliability_1.DEFAULT_RETRY_CONFIG);
295
+ if ((_c = searchResult === null || searchResult === void 0 ? void 0 : searchResult.results) === null || _c === void 0 ? void 0 : _c.length) {
296
+ results = (0, processors_1.processNewsSearchResults)(searchResult.results, itemIndex).slice(0, maxResults);
297
+ }
298
+ else {
299
+ results = [{
300
+ json: {
301
+ success: true,
302
+ query,
303
+ operation: 'news_search',
304
+ count: 0,
305
+ results: [],
306
+ },
307
+ pairedItem: { item: itemIndex },
308
+ }];
309
+ }
266
310
  }
267
- }
268
- else if (operation === types_1.DuckDuckGoOperation.SearchVideos) {
269
- const searchResult = await (0, duck_duck_scrape_1.searchVideos)(query, {
270
- safeSearch: getSafeSearchType(safeSearch),
271
- locale: region || locale,
272
- });
273
- if ((_d = searchResult === null || searchResult === void 0 ? void 0 : searchResult.results) === null || _d === void 0 ? void 0 : _d.length) {
274
- results = (0, processors_1.processVideoSearchResults)(searchResult.results, itemIndex).slice(0, maxResults);
311
+ else if (operation === types_1.DuckDuckGoOperation.SearchVideos) {
312
+ const searchResult = await (0, reliability_1.executeWithRetry)(() => (0, reliability_1.withTimeout)((0, duck_duck_scrape_1.searchVideos)(query, {
313
+ safeSearch: getSafeSearchType(safeSearch),
314
+ locale: region || locale,
315
+ }), timeout, 'Video Search'), `Video Search: "${query}"`, reliability_1.DEFAULT_RETRY_CONFIG);
316
+ if ((_d = searchResult === null || searchResult === void 0 ? void 0 : searchResult.results) === null || _d === void 0 ? void 0 : _d.length) {
317
+ results = (0, processors_1.processVideoSearchResults)(searchResult.results, itemIndex).slice(0, maxResults);
318
+ }
319
+ else {
320
+ results = [{
321
+ json: {
322
+ success: true,
323
+ query,
324
+ operation: 'video_search',
325
+ count: 0,
326
+ results: [],
327
+ },
328
+ pairedItem: { item: itemIndex },
329
+ }];
330
+ }
275
331
  }
276
332
  else {
277
- results = [{
278
- json: { success: true, query, count: 0, results: [] },
279
- pairedItem: { item: itemIndex },
280
- }];
333
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unknown operation: ${operation}`, { itemIndex });
281
334
  }
335
+ return results;
336
+ };
337
+ const searchResults = await performSearch();
338
+ const rateLimiter = (0, adaptiveRateLimiter_1.getAdaptiveRateLimiter)(operation);
339
+ rateLimiter.reportSuccess();
340
+ returnData.push(...searchResults);
341
+ }
342
+ catch (error) {
343
+ const operation = this.getNodeParameter('operation', itemIndex);
344
+ const rateLimiter = (0, adaptiveRateLimiter_1.getAdaptiveRateLimiter)(operation);
345
+ const classifiedError = error instanceof errors_1.DuckDuckGoError
346
+ ? error
347
+ : (0, errors_1.classifyError)(error);
348
+ if (classifiedError.category === 'rate_limit') {
349
+ rateLimiter.reportRateLimitFailure();
282
350
  }
283
351
  else {
284
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unknown operation: ${operation}`, { itemIndex });
352
+ rateLimiter.reportOtherFailure();
285
353
  }
286
- returnData.push(...results);
287
- }
288
- catch (error) {
289
354
  if (this.continueOnFail()) {
290
355
  returnData.push({
291
356
  json: {
292
357
  success: false,
293
- error: error instanceof Error ? error.message : String(error),
358
+ error: classifiedError.getUserMessage(),
359
+ errorCode: classifiedError.code,
360
+ errorCategory: classifiedError.category,
361
+ isRetryable: classifiedError.isRetryable,
362
+ technicalDetails: classifiedError.getTechnicalMessage(),
294
363
  ...items[itemIndex].json,
295
364
  },
296
365
  pairedItem: { item: itemIndex },
297
366
  });
298
367
  continue;
299
368
  }
300
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), error, { itemIndex });
369
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), classifiedError.getUserMessage(), { itemIndex, description: classifiedError.getTechnicalMessage() });
301
370
  }
302
371
  }
303
372
  return [returnData];
@@ -118,7 +118,7 @@ describe('DuckDuckGo Node', () => {
118
118
  it('should throw error when query is empty', async () => {
119
119
  setupNodeParameters('search', '');
120
120
  mockExecuteFunction.continueOnFail = jest.fn().mockReturnValue(false);
121
- await expect(duckDuckGoNode.execute.call(mockExecuteFunction)).rejects.toThrow(/cannot be empty/i);
121
+ await expect(duckDuckGoNode.execute.call(mockExecuteFunction)).rejects.toThrow();
122
122
  });
123
123
  });
124
124
  describe('Image Search', () => {
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resetOperationRateLimiter = exports.resetAdaptiveRateLimiter = exports.getAdaptiveRateLimiter = exports.AdaptiveRateLimiter = exports.OPERATION_RATE_LIMITS = void 0;
4
+ exports.OPERATION_RATE_LIMITS = {
5
+ search: {
6
+ initialDelay: 3000,
7
+ minDelay: 2500,
8
+ maxDelay: 10000,
9
+ },
10
+ searchImages: {
11
+ initialDelay: 3000,
12
+ minDelay: 2500,
13
+ maxDelay: 10000,
14
+ },
15
+ searchNews: {
16
+ initialDelay: 2000,
17
+ minDelay: 1500,
18
+ maxDelay: 8000,
19
+ },
20
+ searchVideos: {
21
+ initialDelay: 2000,
22
+ minDelay: 1500,
23
+ maxDelay: 8000,
24
+ },
25
+ };
26
+ class AdaptiveRateLimiter {
27
+ constructor(operationType = 'search', initialDelay) {
28
+ this.increaseMultiplier = 1.5;
29
+ this.decreaseMultiplier = 0.9;
30
+ this.consecutiveSuccesses = 0;
31
+ this.lastRequestTime = 0;
32
+ this.operationType = operationType;
33
+ const config = exports.OPERATION_RATE_LIMITS[operationType] || exports.OPERATION_RATE_LIMITS.search;
34
+ this.minDelay = config.minDelay;
35
+ this.maxDelay = config.maxDelay;
36
+ this.currentDelay = initialDelay || config.initialDelay;
37
+ this.currentDelay = Math.max(this.minDelay, Math.min(this.currentDelay, this.maxDelay));
38
+ }
39
+ async wait() {
40
+ const now = Date.now();
41
+ const timeSinceLastRequest = now - this.lastRequestTime;
42
+ if (timeSinceLastRequest < this.currentDelay) {
43
+ const waitTime = this.currentDelay - timeSinceLastRequest;
44
+ const jitter = Math.random() * 500;
45
+ await this.sleep(waitTime + jitter);
46
+ }
47
+ else {
48
+ const jitter = Math.random() * 300;
49
+ await this.sleep(jitter);
50
+ }
51
+ this.lastRequestTime = Date.now();
52
+ }
53
+ reportSuccess() {
54
+ this.consecutiveSuccesses++;
55
+ if (this.consecutiveSuccesses >= 3) {
56
+ const newDelay = this.currentDelay * this.decreaseMultiplier;
57
+ this.currentDelay = Math.max(this.minDelay, newDelay);
58
+ this.consecutiveSuccesses = 0;
59
+ if (process.env.NODE_ENV === 'development') {
60
+ console.log(`[AdaptiveRateLimiter] Decreased delay to ${Math.round(this.currentDelay)}ms`);
61
+ }
62
+ }
63
+ }
64
+ reportRateLimitFailure() {
65
+ this.consecutiveSuccesses = 0;
66
+ const newDelay = this.currentDelay * this.increaseMultiplier;
67
+ this.currentDelay = Math.min(this.maxDelay, newDelay);
68
+ if (process.env.NODE_ENV === 'development') {
69
+ console.log(`[AdaptiveRateLimiter] Increased delay to ${Math.round(this.currentDelay)}ms after rate limit`);
70
+ }
71
+ }
72
+ reportOtherFailure() {
73
+ this.consecutiveSuccesses = 0;
74
+ }
75
+ getCurrentDelay() {
76
+ return this.currentDelay;
77
+ }
78
+ reset(initialDelay) {
79
+ const config = exports.OPERATION_RATE_LIMITS[this.operationType] || exports.OPERATION_RATE_LIMITS.search;
80
+ this.currentDelay = initialDelay || config.initialDelay;
81
+ this.currentDelay = Math.max(this.minDelay, Math.min(this.currentDelay, this.maxDelay));
82
+ this.consecutiveSuccesses = 0;
83
+ this.lastRequestTime = 0;
84
+ }
85
+ getOperationType() {
86
+ return this.operationType;
87
+ }
88
+ sleep(ms) {
89
+ return new Promise(resolve => setTimeout(resolve, ms));
90
+ }
91
+ }
92
+ exports.AdaptiveRateLimiter = AdaptiveRateLimiter;
93
+ const globalAdaptiveRateLimiters = new Map();
94
+ function getAdaptiveRateLimiter(operationType = 'search', initialDelay) {
95
+ if (!globalAdaptiveRateLimiters.has(operationType)) {
96
+ globalAdaptiveRateLimiters.set(operationType, new AdaptiveRateLimiter(operationType, initialDelay));
97
+ }
98
+ return globalAdaptiveRateLimiters.get(operationType);
99
+ }
100
+ exports.getAdaptiveRateLimiter = getAdaptiveRateLimiter;
101
+ function resetAdaptiveRateLimiter() {
102
+ globalAdaptiveRateLimiters.clear();
103
+ }
104
+ exports.resetAdaptiveRateLimiter = resetAdaptiveRateLimiter;
105
+ function resetOperationRateLimiter(operationType) {
106
+ globalAdaptiveRateLimiters.delete(operationType);
107
+ }
108
+ exports.resetOperationRateLimiter = resetOperationRateLimiter;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.BROWSER_USER_AGENT = exports.NODE_INFO = exports.DEFAULT_PARAMETERS = exports.REGIONS = void 0;
3
+ exports.getRandomUserAgent = exports.USER_AGENTS = exports.NODE_INFO = exports.DEFAULT_PARAMETERS = exports.REGIONS = void 0;
4
4
  exports.REGIONS = [
5
5
  { name: 'Argentina', value: 'ar-es' },
6
6
  { name: 'Australia', value: 'au-en' },
@@ -63,4 +63,23 @@ exports.NODE_INFO = {
63
63
  VERSION: 1,
64
64
  DESCRIPTION: 'Search using DuckDuckGo',
65
65
  };
66
- exports.BROWSER_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36';
66
+ exports.USER_AGENTS = [
67
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
68
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
69
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
70
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
71
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
72
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
73
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0',
74
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
75
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:122.0) Gecko/20100101 Firefox/122.0',
76
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0',
77
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15',
78
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15',
79
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0',
80
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0',
81
+ ];
82
+ function getRandomUserAgent() {
83
+ return exports.USER_AGENTS[Math.floor(Math.random() * exports.USER_AGENTS.length)];
84
+ }
85
+ exports.getRandomUserAgent = getRandomUserAgent;
@@ -0,0 +1,185 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.shouldRetryError = exports.classifyError = exports.ServerError = exports.ParsingError = exports.ValidationError = exports.TimeoutError = exports.RateLimitError = exports.NetworkError = exports.DuckDuckGoError = void 0;
4
+ class DuckDuckGoError extends Error {
5
+ constructor(context) {
6
+ super(context.userMessage);
7
+ this.name = 'DuckDuckGoError';
8
+ this.code = context.code;
9
+ this.category = context.category;
10
+ this.isRetryable = context.isRetryable;
11
+ this.userMessage = context.userMessage;
12
+ this.technicalMessage = context.technicalMessage;
13
+ this.originalError = context.originalError;
14
+ this.metadata = context.metadata;
15
+ if (Error.captureStackTrace) {
16
+ Error.captureStackTrace(this, DuckDuckGoError);
17
+ }
18
+ }
19
+ getUserMessage() {
20
+ return this.userMessage;
21
+ }
22
+ getTechnicalMessage() {
23
+ return this.technicalMessage;
24
+ }
25
+ toJSON() {
26
+ return {
27
+ name: this.name,
28
+ code: this.code,
29
+ category: this.category,
30
+ isRetryable: this.isRetryable,
31
+ userMessage: this.userMessage,
32
+ technicalMessage: this.technicalMessage,
33
+ metadata: this.metadata,
34
+ stack: this.stack,
35
+ };
36
+ }
37
+ }
38
+ exports.DuckDuckGoError = DuckDuckGoError;
39
+ class NetworkError extends DuckDuckGoError {
40
+ constructor(message, originalError, metadata) {
41
+ super({
42
+ code: 'NETWORK_ERROR',
43
+ category: 'network',
44
+ isRetryable: true,
45
+ userMessage: 'Unable to connect to DuckDuckGo. Please check your internet connection and try again.',
46
+ technicalMessage: message,
47
+ originalError,
48
+ metadata,
49
+ });
50
+ this.name = 'NetworkError';
51
+ }
52
+ }
53
+ exports.NetworkError = NetworkError;
54
+ class RateLimitError extends DuckDuckGoError {
55
+ constructor(message, originalError, metadata) {
56
+ super({
57
+ code: 'RATE_LIMIT',
58
+ category: 'rate_limit',
59
+ isRetryable: true,
60
+ userMessage: 'Search request rate limit exceeded. The request will be automatically retried.',
61
+ technicalMessage: message,
62
+ originalError,
63
+ metadata,
64
+ });
65
+ this.name = 'RateLimitError';
66
+ }
67
+ }
68
+ exports.RateLimitError = RateLimitError;
69
+ class TimeoutError extends DuckDuckGoError {
70
+ constructor(timeoutMs, operation, originalError) {
71
+ super({
72
+ code: 'TIMEOUT',
73
+ category: 'timeout',
74
+ isRetryable: true,
75
+ userMessage: `Search request timed out after ${timeoutMs / 1000} seconds. Please try again.`,
76
+ technicalMessage: `${operation} timed out after ${timeoutMs}ms`,
77
+ originalError,
78
+ metadata: { timeoutMs, operation },
79
+ });
80
+ this.name = 'TimeoutError';
81
+ }
82
+ }
83
+ exports.TimeoutError = TimeoutError;
84
+ class ValidationError extends DuckDuckGoError {
85
+ constructor(field, message, value) {
86
+ super({
87
+ code: 'VALIDATION_ERROR',
88
+ category: 'validation',
89
+ isRetryable: false,
90
+ userMessage: `Invalid ${field}: ${message}`,
91
+ technicalMessage: `Validation failed for field '${field}': ${message}`,
92
+ metadata: { field, value },
93
+ });
94
+ this.name = 'ValidationError';
95
+ }
96
+ }
97
+ exports.ValidationError = ValidationError;
98
+ class ParsingError extends DuckDuckGoError {
99
+ constructor(message, originalError, metadata) {
100
+ super({
101
+ code: 'PARSING_ERROR',
102
+ category: 'parsing',
103
+ isRetryable: false,
104
+ userMessage: 'Failed to process search results. Please try a different query.',
105
+ technicalMessage: message,
106
+ originalError,
107
+ metadata,
108
+ });
109
+ this.name = 'ParsingError';
110
+ }
111
+ }
112
+ exports.ParsingError = ParsingError;
113
+ class ServerError extends DuckDuckGoError {
114
+ constructor(statusCode, originalError) {
115
+ super({
116
+ code: 'SERVER_ERROR',
117
+ category: 'network',
118
+ isRetryable: true,
119
+ userMessage: 'DuckDuckGo servers are temporarily unavailable. Please try again later.',
120
+ technicalMessage: `Server returned error status: ${statusCode}`,
121
+ originalError,
122
+ metadata: { statusCode },
123
+ });
124
+ this.name = 'ServerError';
125
+ }
126
+ }
127
+ exports.ServerError = ServerError;
128
+ function classifyError(error) {
129
+ if (error instanceof DuckDuckGoError) {
130
+ return error;
131
+ }
132
+ const message = (error === null || error === void 0 ? void 0 : error.message) || String(error);
133
+ const lowerMessage = message.toLowerCase();
134
+ if (lowerMessage.includes('econnrefused') ||
135
+ lowerMessage.includes('enotfound') ||
136
+ lowerMessage.includes('econnreset') ||
137
+ lowerMessage.includes('ehostunreach') ||
138
+ lowerMessage.includes('enetunreach') ||
139
+ lowerMessage.includes('network') ||
140
+ lowerMessage.includes('dns')) {
141
+ return new NetworkError(message, error instanceof Error ? error : undefined);
142
+ }
143
+ if (lowerMessage.includes('anomaly') ||
144
+ lowerMessage.includes('too quickly') ||
145
+ lowerMessage.includes('rate limit') ||
146
+ lowerMessage.includes('429') ||
147
+ lowerMessage.includes('too many requests')) {
148
+ return new RateLimitError(message, error instanceof Error ? error : undefined);
149
+ }
150
+ if (lowerMessage.includes('timeout') ||
151
+ lowerMessage.includes('etimedout') ||
152
+ lowerMessage.includes('timed out')) {
153
+ return new TimeoutError(30000, 'Search request', error instanceof Error ? error : undefined);
154
+ }
155
+ if (lowerMessage.includes('500') ||
156
+ lowerMessage.includes('502') ||
157
+ lowerMessage.includes('503') ||
158
+ lowerMessage.includes('504') ||
159
+ lowerMessage.includes('server error') ||
160
+ lowerMessage.includes('temporarily unavailable') ||
161
+ lowerMessage.includes('internal server error')) {
162
+ const statusMatch = message.match(/(\d{3})/);
163
+ const statusCode = statusMatch ? parseInt(statusMatch[1], 10) : 500;
164
+ return new ServerError(statusCode, error instanceof Error ? error : undefined);
165
+ }
166
+ if (lowerMessage.includes('json') ||
167
+ lowerMessage.includes('parse') ||
168
+ lowerMessage.includes('unexpected token') ||
169
+ lowerMessage.includes('syntax error')) {
170
+ return new ParsingError(message, error instanceof Error ? error : undefined);
171
+ }
172
+ return new DuckDuckGoError({
173
+ code: 'UNKNOWN_ERROR',
174
+ category: 'unknown',
175
+ isRetryable: false,
176
+ userMessage: 'An unexpected error occurred. Please try again.',
177
+ technicalMessage: message,
178
+ originalError: error instanceof Error ? error : undefined,
179
+ });
180
+ }
181
+ exports.classifyError = classifyError;
182
+ function shouldRetryError(error, retryableCategories) {
183
+ return error.isRetryable && retryableCategories.has(error.category);
184
+ }
185
+ exports.shouldRetryError = shouldRetryError;
@@ -1,14 +1,62 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.processVideoSearchResults = exports.processNewsSearchResults = exports.processImageSearchResults = exports.processWebSearchResults = void 0;
3
+ exports.processVideoSearchResults = exports.processNewsSearchResults = exports.processImageSearchResults = exports.processWebSearchResults = exports.DEFAULT_QUALITY_CONFIG = void 0;
4
4
  const utils_1 = require("./utils");
5
- function processWebSearchResults(results, itemIndex, rawResponse) {
5
+ const validation_1 = require("./validation");
6
+ exports.DEFAULT_QUALITY_CONFIG = {
7
+ removeDuplicates: true,
8
+ requireTitle: true,
9
+ requireUrl: true,
10
+ minDescriptionLength: 0,
11
+ validateUrls: true,
12
+ };
13
+ function filterAndValidateResults(results, config = exports.DEFAULT_QUALITY_CONFIG) {
6
14
  if (!results || !Array.isArray(results)) {
7
15
  return [];
8
16
  }
9
- return results
10
- .filter(item => item && typeof item === 'object' && item.title && item.url)
11
- .map((item, index) => ({
17
+ const seenUrls = new Set();
18
+ const seenTitles = new Set();
19
+ return results.filter((result) => {
20
+ if (config.requireTitle && !result.title) {
21
+ return false;
22
+ }
23
+ if (config.requireUrl && !result.url) {
24
+ return false;
25
+ }
26
+ if (config.validateUrls && result.url && !(0, validation_1.validateUrl)(result.url)) {
27
+ return false;
28
+ }
29
+ if (config.minDescriptionLength > 0 && result.description) {
30
+ const cleanDesc = (result.description || '').trim();
31
+ if (cleanDesc.length < config.minDescriptionLength) {
32
+ return false;
33
+ }
34
+ }
35
+ if (config.removeDuplicates && result.url) {
36
+ const normalizedUrl = result.url.toLowerCase().trim();
37
+ if (seenUrls.has(normalizedUrl)) {
38
+ return false;
39
+ }
40
+ seenUrls.add(normalizedUrl);
41
+ }
42
+ if (config.removeDuplicates && result.title) {
43
+ const normalizedTitle = result.title.toLowerCase().trim();
44
+ if (normalizedTitle && seenTitles.has(normalizedTitle)) {
45
+ return false;
46
+ }
47
+ if (normalizedTitle) {
48
+ seenTitles.add(normalizedTitle);
49
+ }
50
+ }
51
+ return true;
52
+ });
53
+ }
54
+ function processWebSearchResults(results, itemIndex, rawResponse, qualityConfig) {
55
+ if (!results || !Array.isArray(results)) {
56
+ return [];
57
+ }
58
+ const filteredResults = filterAndValidateResults(results, qualityConfig);
59
+ return filteredResults.map((item, index) => ({
12
60
  json: {
13
61
  position: index + 1,
14
62
  title: (0, utils_1.decodeHtmlEntities)(item.title || '') || '',
@@ -21,13 +69,17 @@ function processWebSearchResults(results, itemIndex, rawResponse) {
21
69
  }));
22
70
  }
23
71
  exports.processWebSearchResults = processWebSearchResults;
24
- function processImageSearchResults(results, itemIndex) {
72
+ function processImageSearchResults(results, itemIndex, qualityConfig) {
25
73
  if (!results || !Array.isArray(results)) {
26
74
  return [];
27
75
  }
28
- return results
29
- .filter(item => item && typeof item === 'object' && item.image)
30
- .map((item) => ({
76
+ const imageResults = results.filter(item => item && typeof item === 'object' && item.image);
77
+ const filteredResults = filterAndValidateResults(imageResults, {
78
+ ...exports.DEFAULT_QUALITY_CONFIG,
79
+ requireTitle: false,
80
+ ...qualityConfig,
81
+ });
82
+ return filteredResults.map((item) => ({
31
83
  json: {
32
84
  title: (0, utils_1.decodeHtmlEntities)(item.title || '') || '',
33
85
  url: item.url || '',
@@ -42,11 +94,12 @@ function processImageSearchResults(results, itemIndex) {
42
94
  }));
43
95
  }
44
96
  exports.processImageSearchResults = processImageSearchResults;
45
- function processNewsSearchResults(results, itemIndex) {
97
+ function processNewsSearchResults(results, itemIndex, qualityConfig) {
46
98
  if (!results || !Array.isArray(results)) {
47
99
  return [];
48
100
  }
49
- return results.map((item) => ({
101
+ const filteredResults = filterAndValidateResults(results, qualityConfig);
102
+ return filteredResults.map((item) => ({
50
103
  json: {
51
104
  title: (0, utils_1.decodeHtmlEntities)(item.title),
52
105
  description: (0, utils_1.decodeHtmlEntities)(item.excerpt),
@@ -61,11 +114,12 @@ function processNewsSearchResults(results, itemIndex) {
61
114
  }));
62
115
  }
63
116
  exports.processNewsSearchResults = processNewsSearchResults;
64
- function processVideoSearchResults(results, itemIndex) {
117
+ function processVideoSearchResults(results, itemIndex, qualityConfig) {
65
118
  if (!results || !Array.isArray(results)) {
66
119
  return [];
67
120
  }
68
- return results.map((item) => ({
121
+ const filteredResults = filterAndValidateResults(results, qualityConfig);
122
+ return filteredResults.map((item) => ({
69
123
  json: {
70
124
  title: (0, utils_1.decodeHtmlEntities)(item.title),
71
125
  description: (0, utils_1.decodeHtmlEntities)(item.description),
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resetRequestQueue = exports.getRequestQueue = exports.RequestQueue = exports.withTimeout = exports.executeWithRetry = exports.DEFAULT_RETRY_CONFIG = exports.sleep = void 0;
4
+ const errors_1 = require("./errors");
5
+ const errors_2 = require("./errors");
6
+ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
7
+ exports.sleep = sleep;
8
+ exports.DEFAULT_RETRY_CONFIG = {
9
+ maxRetries: 3,
10
+ initialDelay: 2000,
11
+ maxDelay: 15000,
12
+ backoffMultiplier: 2,
13
+ jitterFactor: 0.5,
14
+ retryableCategories: new Set(['network', 'rate_limit', 'timeout']),
15
+ };
16
+ async function executeWithRetry(fn, context = 'operation', config = exports.DEFAULT_RETRY_CONFIG) {
17
+ let lastError = null;
18
+ for (let attempt = 1; attempt <= config.maxRetries + 1; attempt++) {
19
+ try {
20
+ return await fn();
21
+ }
22
+ catch (error) {
23
+ const classifiedError = (0, errors_1.classifyError)(error);
24
+ lastError = classifiedError;
25
+ if (!(0, errors_1.shouldRetryError)(classifiedError, config.retryableCategories)) {
26
+ throw classifiedError;
27
+ }
28
+ if (attempt > config.maxRetries) {
29
+ throw classifiedError;
30
+ }
31
+ const baseDelay = Math.min(config.initialDelay * Math.pow(config.backoffMultiplier, attempt - 1), config.maxDelay);
32
+ const jitter = baseDelay * config.jitterFactor * (Math.random() * 2 - 1);
33
+ const delay = Math.max(0, baseDelay + jitter);
34
+ if (process.env.NODE_ENV === 'development') {
35
+ console.log(`[DuckDuckGo] Retry ${attempt}/${config.maxRetries} for ${context} ` +
36
+ `after ${Math.round(delay)}ms due to ${classifiedError.code}`);
37
+ }
38
+ await (0, exports.sleep)(delay);
39
+ }
40
+ }
41
+ throw lastError || (0, errors_1.classifyError)('Unexpected retry error');
42
+ }
43
+ exports.executeWithRetry = executeWithRetry;
44
+ async function withTimeout(promise, timeoutMs, operation) {
45
+ let timeoutId = null;
46
+ const timeoutPromise = new Promise((_, reject) => {
47
+ timeoutId = setTimeout(() => {
48
+ reject(new errors_2.TimeoutError(timeoutMs, operation));
49
+ }, timeoutMs);
50
+ });
51
+ try {
52
+ const result = await Promise.race([promise, timeoutPromise]);
53
+ if (timeoutId)
54
+ clearTimeout(timeoutId);
55
+ return result;
56
+ }
57
+ catch (error) {
58
+ if (timeoutId)
59
+ clearTimeout(timeoutId);
60
+ throw error;
61
+ }
62
+ }
63
+ exports.withTimeout = withTimeout;
64
+ class RequestQueue {
65
+ constructor(minInterval = 3000, maxConcurrent = 1) {
66
+ this.queue = [];
67
+ this.processing = false;
68
+ this.lastRequestTime = 0;
69
+ this.activeRequests = 0;
70
+ this.minInterval = minInterval;
71
+ this.maxConcurrent = maxConcurrent;
72
+ }
73
+ async enqueue(fn) {
74
+ return new Promise((resolve, reject) => {
75
+ this.queue.push(async () => {
76
+ try {
77
+ const result = await fn();
78
+ resolve(result);
79
+ }
80
+ catch (error) {
81
+ reject(error);
82
+ }
83
+ });
84
+ if (!this.processing) {
85
+ this.processQueue().catch(() => {
86
+ });
87
+ }
88
+ });
89
+ }
90
+ async processQueue() {
91
+ if (this.processing || this.queue.length === 0) {
92
+ return;
93
+ }
94
+ this.processing = true;
95
+ while (this.queue.length > 0 && this.activeRequests < this.maxConcurrent) {
96
+ const now = Date.now();
97
+ const timeSinceLastRequest = now - this.lastRequestTime;
98
+ if (timeSinceLastRequest < this.minInterval) {
99
+ const delay = this.minInterval - timeSinceLastRequest;
100
+ const jitter = Math.random() * 1000;
101
+ await (0, exports.sleep)(delay + jitter);
102
+ }
103
+ else {
104
+ const smallJitter = Math.random() * 500;
105
+ await (0, exports.sleep)(smallJitter);
106
+ }
107
+ const fn = this.queue.shift();
108
+ if (fn) {
109
+ this.lastRequestTime = Date.now();
110
+ this.activeRequests++;
111
+ fn().finally(() => {
112
+ this.activeRequests--;
113
+ if (this.queue.length > 0) {
114
+ this.processQueue().catch(() => {
115
+ });
116
+ }
117
+ });
118
+ }
119
+ }
120
+ this.processing = false;
121
+ }
122
+ getQueueSize() {
123
+ return this.queue.length;
124
+ }
125
+ getActiveRequests() {
126
+ return this.activeRequests;
127
+ }
128
+ clear() {
129
+ this.queue = [];
130
+ }
131
+ }
132
+ exports.RequestQueue = RequestQueue;
133
+ let globalRequestQueue = null;
134
+ function getRequestQueue(minInterval = 3000) {
135
+ if (!globalRequestQueue) {
136
+ globalRequestQueue = new RequestQueue(minInterval);
137
+ }
138
+ return globalRequestQueue;
139
+ }
140
+ exports.getRequestQueue = getRequestQueue;
141
+ function resetRequestQueue() {
142
+ globalRequestQueue = null;
143
+ }
144
+ exports.resetRequestQueue = resetRequestQueue;
@@ -7,15 +7,98 @@ const HTML_ENTITIES = {
7
7
  '&gt;': '>',
8
8
  '&quot;': '"',
9
9
  '&#39;': "'",
10
+ '&apos;': "'",
10
11
  '&nbsp;': ' ',
12
+ '&ensp;': ' ',
13
+ '&emsp;': ' ',
14
+ '&thinsp;': ' ',
11
15
  '&ndash;': '–',
12
16
  '&mdash;': '—',
13
17
  '&hellip;': '…',
18
+ '&lsquo;': '\u2018',
19
+ '&rsquo;': '\u2019',
20
+ '&ldquo;': '\u201C',
21
+ '&rdquo;': '\u201D',
22
+ '&laquo;': '«',
23
+ '&raquo;': '»',
24
+ '&bull;': '•',
25
+ '&middot;': '·',
26
+ '&prime;': '′',
27
+ '&Prime;': '″',
28
+ '&copy;': '©',
29
+ '&reg;': '®',
30
+ '&trade;': '™',
31
+ '&deg;': '°',
32
+ '&plusmn;': '±',
33
+ '&para;': '¶',
34
+ '&sect;': '§',
35
+ '&dagger;': '†',
36
+ '&Dagger;': '‡',
37
+ '&euro;': '€',
38
+ '&pound;': '£',
39
+ '&yen;': '¥',
40
+ '&cent;': '¢',
41
+ '&curren;': '¤',
42
+ '&times;': '×',
43
+ '&divide;': '÷',
44
+ '&minus;': '−',
45
+ '&ne;': '≠',
46
+ '&le;': '≤',
47
+ '&ge;': '≥',
48
+ '&asymp;': '≈',
49
+ '&infin;': '∞',
50
+ '&sum;': '∑',
51
+ '&prod;': '∏',
52
+ '&radic;': '√',
53
+ '&int;': '∫',
54
+ '&larr;': '←',
55
+ '&uarr;': '↑',
56
+ '&rarr;': '→',
57
+ '&darr;': '↓',
58
+ '&harr;': '↔',
59
+ '&alpha;': 'α',
60
+ '&beta;': 'β',
61
+ '&gamma;': 'γ',
62
+ '&delta;': 'δ',
63
+ '&epsilon;': 'ε',
64
+ '&pi;': 'π',
65
+ '&sigma;': 'σ',
66
+ '&omega;': 'ω',
14
67
  };
15
68
  function decodeHtmlEntities(text) {
16
69
  if (!text)
17
70
  return null;
18
- return text.replace(/&[#\w]+;/g, (entity) => HTML_ENTITIES[entity] || entity);
71
+ try {
72
+ return text
73
+ .replace(/&[a-zA-Z]+;/g, (entity) => HTML_ENTITIES[entity] || entity)
74
+ .replace(/&#(\d+);/g, (_, code) => {
75
+ try {
76
+ const num = parseInt(code, 10);
77
+ if (num >= 0 && num <= 0x10FFFF) {
78
+ return String.fromCharCode(num);
79
+ }
80
+ return _;
81
+ }
82
+ catch {
83
+ return _;
84
+ }
85
+ })
86
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, code) => {
87
+ try {
88
+ const num = parseInt(code, 16);
89
+ if (num >= 0 && num <= 0x10FFFF) {
90
+ return String.fromCharCode(num);
91
+ }
92
+ return _;
93
+ }
94
+ catch {
95
+ return _;
96
+ }
97
+ });
98
+ }
99
+ catch (error) {
100
+ return text;
101
+ }
19
102
  }
20
103
  exports.decodeHtmlEntities = decodeHtmlEntities;
21
104
  function formatDate(timestamp) {
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateTimeout = exports.validateTimePeriod = exports.validateSafeSearch = exports.validateUrl = exports.validateRegion = exports.validateLocale = exports.validateMaxResults = exports.sanitizeQuery = exports.validateSearchQuery = exports.VALIDATION_RULES = void 0;
4
+ const errors_1 = require("./errors");
5
+ exports.VALIDATION_RULES = {
6
+ QUERY_MIN_LENGTH: 1,
7
+ QUERY_MAX_LENGTH: 400,
8
+ MAX_RESULTS_MIN: 1,
9
+ MAX_RESULTS_MAX: 50,
10
+ TIMEOUT_MIN: 5000,
11
+ TIMEOUT_MAX: 120000,
12
+ };
13
+ function validateSearchQuery(query) {
14
+ if (query === null || query === undefined) {
15
+ throw new errors_1.ValidationError('query', 'Search query is required', query);
16
+ }
17
+ if (typeof query !== 'string') {
18
+ throw new errors_1.ValidationError('query', 'Search query must be a string', query);
19
+ }
20
+ const trimmed = query.trim();
21
+ if (trimmed.length < exports.VALIDATION_RULES.QUERY_MIN_LENGTH) {
22
+ throw new errors_1.ValidationError('query', 'Search query cannot be empty', query);
23
+ }
24
+ if (trimmed.length > exports.VALIDATION_RULES.QUERY_MAX_LENGTH) {
25
+ throw new errors_1.ValidationError('query', `Search query is too long (maximum ${exports.VALIDATION_RULES.QUERY_MAX_LENGTH} characters, got ${trimmed.length})`, query);
26
+ }
27
+ return sanitizeQuery(trimmed);
28
+ }
29
+ exports.validateSearchQuery = validateSearchQuery;
30
+ function sanitizeQuery(query) {
31
+ return query
32
+ .replace(/\s+/g, ' ')
33
+ .replace(/[\x00-\x1F\x7F]/g, '')
34
+ .trim();
35
+ }
36
+ exports.sanitizeQuery = sanitizeQuery;
37
+ function validateMaxResults(maxResults) {
38
+ if (maxResults === null || maxResults === undefined) {
39
+ return 10;
40
+ }
41
+ const num = typeof maxResults === 'string' ? parseFloat(maxResults) : maxResults;
42
+ if (typeof num !== 'number' || isNaN(num)) {
43
+ throw new errors_1.ValidationError('maxResults', 'Must be a valid number', maxResults);
44
+ }
45
+ if (!Number.isFinite(num)) {
46
+ throw new errors_1.ValidationError('maxResults', 'Must be a finite number', maxResults);
47
+ }
48
+ if (num < exports.VALIDATION_RULES.MAX_RESULTS_MIN) {
49
+ return exports.VALIDATION_RULES.MAX_RESULTS_MIN;
50
+ }
51
+ if (num > exports.VALIDATION_RULES.MAX_RESULTS_MAX) {
52
+ return exports.VALIDATION_RULES.MAX_RESULTS_MAX;
53
+ }
54
+ return Math.floor(num);
55
+ }
56
+ exports.validateMaxResults = validateMaxResults;
57
+ function validateLocale(locale) {
58
+ if (!locale || typeof locale !== 'string') {
59
+ return 'en-us';
60
+ }
61
+ const trimmed = locale.trim().toLowerCase();
62
+ if (!/^[a-z]{2}-[a-z]{2}$/.test(trimmed)) {
63
+ throw new errors_1.ValidationError('locale', 'Invalid locale format (expected: xx-xx)', locale);
64
+ }
65
+ return trimmed;
66
+ }
67
+ exports.validateLocale = validateLocale;
68
+ function validateRegion(region) {
69
+ if (!region || typeof region !== 'string') {
70
+ return 'wt-wt';
71
+ }
72
+ const trimmed = region.trim().toLowerCase();
73
+ if (!/^[a-z]{2}-[a-z]{2}$/.test(trimmed)) {
74
+ throw new errors_1.ValidationError('region', 'Invalid region format (expected: xx-xx)', region);
75
+ }
76
+ return trimmed;
77
+ }
78
+ exports.validateRegion = validateRegion;
79
+ function validateUrl(url) {
80
+ if (!url || typeof url !== 'string') {
81
+ return false;
82
+ }
83
+ try {
84
+ const parsed = new URL(url);
85
+ return ['http:', 'https:'].includes(parsed.protocol);
86
+ }
87
+ catch {
88
+ return false;
89
+ }
90
+ }
91
+ exports.validateUrl = validateUrl;
92
+ function validateSafeSearch(safeSearch) {
93
+ if (safeSearch === null || safeSearch === undefined) {
94
+ return 0;
95
+ }
96
+ const num = typeof safeSearch === 'string' ? parseInt(safeSearch, 10) : safeSearch;
97
+ if (typeof num !== 'number' || isNaN(num)) {
98
+ return 0;
99
+ }
100
+ if (![0, -1, -2].includes(num)) {
101
+ return 0;
102
+ }
103
+ return num;
104
+ }
105
+ exports.validateSafeSearch = validateSafeSearch;
106
+ function validateTimePeriod(timePeriod) {
107
+ if (!timePeriod || typeof timePeriod !== 'string') {
108
+ return 'a';
109
+ }
110
+ const trimmed = timePeriod.trim().toLowerCase();
111
+ if (!['a', 'd', 'w', 'm', 'y'].includes(trimmed)) {
112
+ return 'a';
113
+ }
114
+ return trimmed;
115
+ }
116
+ exports.validateTimePeriod = validateTimePeriod;
117
+ function validateTimeout(timeout) {
118
+ if (timeout === null || timeout === undefined) {
119
+ return 30000;
120
+ }
121
+ const num = typeof timeout === 'string' ? parseInt(timeout, 10) : timeout;
122
+ if (typeof num !== 'number' || isNaN(num)) {
123
+ throw new errors_1.ValidationError('timeout', 'Must be a valid number', timeout);
124
+ }
125
+ if (num < exports.VALIDATION_RULES.TIMEOUT_MIN) {
126
+ return exports.VALIDATION_RULES.TIMEOUT_MIN;
127
+ }
128
+ if (num > exports.VALIDATION_RULES.TIMEOUT_MAX) {
129
+ return exports.VALIDATION_RULES.TIMEOUT_MAX;
130
+ }
131
+ return num;
132
+ }
133
+ exports.validateTimeout = validateTimeout;
package/dist/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "n8n-nodes-duckduckgo-search",
3
- "version": "32.0.0",
4
- "description": "Lightweight n8n community node for DuckDuckGo search. Search the web, images, news, and videos with privacy-focused results.",
3
+ "version": "32.2.0",
4
+ "description": "Professional-grade n8n community node for DuckDuckGo search with comprehensive error handling, validation, and quality filtering.",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",
7
7
  "n8n",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "n8n-nodes-duckduckgo-search",
3
- "version": "32.0.0",
4
- "description": "Lightweight n8n community node for DuckDuckGo search. Search the web, images, news, and videos with privacy-focused results.",
3
+ "version": "32.2.0",
4
+ "description": "Professional-grade n8n community node for DuckDuckGo search with comprehensive error handling, validation, and quality filtering.",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",
7
7
  "n8n",