linkedin-secret-sauce 0.11.0 → 0.11.1

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.
@@ -64,10 +64,10 @@ export declare function getSmartLeadTokenCacheStats(): {
64
64
  * // In your test setup
65
65
  * import { enableFileCache } from 'linkedin-secret-sauce/enrichment';
66
66
  *
67
- * enableFileCache(); // Now tokens persist between test runs
67
+ * await enableFileCache(); // Now tokens persist between test runs
68
68
  * ```
69
69
  */
70
- export declare function enableFileCache(): void;
70
+ export declare function enableFileCache(): Promise<void>;
71
71
  /**
72
72
  * Disable file-based token caching
73
73
  */
@@ -79,4 +79,4 @@ export declare function isFileCacheEnabled(): boolean;
79
79
  /**
80
80
  * Clear the file cache
81
81
  */
82
- export declare function clearFileCache(): void;
82
+ export declare function clearFileCache(): Promise<void>;
@@ -51,9 +51,10 @@ exports.enableFileCache = enableFileCache;
51
51
  exports.disableFileCache = disableFileCache;
52
52
  exports.isFileCacheEnabled = isFileCacheEnabled;
53
53
  exports.clearFileCache = clearFileCache;
54
- const fs = __importStar(require("fs"));
54
+ const fs = __importStar(require("fs/promises"));
55
55
  const path = __importStar(require("path"));
56
56
  const os = __importStar(require("os"));
57
+ const http_retry_1 = require("../utils/http-retry");
57
58
  const DEFAULT_LOGIN_URL = 'https://server.smartlead.ai/api/auth/login';
58
59
  const DEFAULT_REFRESH_BUFFER_MS = 5 * 60 * 1000; // 5 minutes
59
60
  const TOKEN_LIFETIME_MS = 24 * 60 * 60 * 1000; // Assume 24h JWT lifetime (conservative)
@@ -213,34 +214,32 @@ function getFileCachePath() {
213
214
  return path.join(homeDir, FILE_CACHE_NAME);
214
215
  }
215
216
  /**
216
- * Load tokens from file cache
217
+ * Load tokens from file cache (async)
217
218
  */
218
- function loadFileCache() {
219
+ async function loadFileCache() {
219
220
  const cache = new Map();
220
221
  if (!fileCacheEnabled)
221
222
  return cache;
222
223
  try {
223
224
  const filePath = getFileCachePath();
224
- if (fs.existsSync(filePath)) {
225
- const content = fs.readFileSync(filePath, 'utf8');
226
- const data = JSON.parse(content);
227
- for (const [key, value] of Object.entries(data)) {
228
- // Validate structure
229
- if (value && value.token && value.expiresAt && value.obtainedAt) {
230
- cache.set(key, value);
231
- }
225
+ const content = await fs.readFile(filePath, 'utf8');
226
+ const data = (0, http_retry_1.safeJsonParse)(content, {});
227
+ for (const [key, value] of Object.entries(data)) {
228
+ // Validate structure
229
+ if (value && value.token && value.expiresAt && value.obtainedAt) {
230
+ cache.set(key, value);
232
231
  }
233
232
  }
234
233
  }
235
234
  catch {
236
- // Ignore file read errors
235
+ // Ignore file read errors (file may not exist)
237
236
  }
238
237
  return cache;
239
238
  }
240
239
  /**
241
- * Save tokens to file cache
240
+ * Save tokens to file cache (async, fire-and-forget)
242
241
  */
243
- function saveFileCache() {
242
+ async function saveFileCache() {
244
243
  if (!fileCacheEnabled)
245
244
  return;
246
245
  try {
@@ -249,7 +248,7 @@ function saveFileCache() {
249
248
  for (const [key, value] of tokenCache.entries()) {
250
249
  data[key] = value;
251
250
  }
252
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
251
+ await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf8');
253
252
  }
254
253
  catch {
255
254
  // Ignore file write errors
@@ -266,13 +265,13 @@ function saveFileCache() {
266
265
  * // In your test setup
267
266
  * import { enableFileCache } from 'linkedin-secret-sauce/enrichment';
268
267
  *
269
- * enableFileCache(); // Now tokens persist between test runs
268
+ * await enableFileCache(); // Now tokens persist between test runs
270
269
  * ```
271
270
  */
272
- function enableFileCache() {
271
+ async function enableFileCache() {
273
272
  fileCacheEnabled = true;
274
273
  // Load existing tokens from file into memory cache
275
- const fileTokens = loadFileCache();
274
+ const fileTokens = await loadFileCache();
276
275
  for (const [key, value] of fileTokens.entries()) {
277
276
  // Only load if not already in memory and still valid
278
277
  if (!tokenCache.has(key) && isTokenValid(value, DEFAULT_REFRESH_BUFFER_MS)) {
@@ -295,25 +294,26 @@ function isFileCacheEnabled() {
295
294
  /**
296
295
  * Clear the file cache
297
296
  */
298
- function clearFileCache() {
297
+ async function clearFileCache() {
299
298
  try {
300
299
  const filePath = getFileCachePath();
301
- if (fs.existsSync(filePath)) {
302
- fs.unlinkSync(filePath);
303
- }
300
+ await fs.unlink(filePath);
304
301
  }
305
302
  catch {
306
- // Ignore errors
303
+ // Ignore errors (file may not exist)
307
304
  }
308
305
  }
309
306
  // We need to modify the token caching behavior to persist to file
310
307
  // This is done by wrapping the cache set operation
311
- // Override tokenCache.set to also persist to file
308
+ // Override tokenCache.set to also persist to file (fire-and-forget async)
312
309
  const originalSet = tokenCache.set.bind(tokenCache);
313
310
  tokenCache.set = function (key, value) {
314
311
  const result = originalSet(key, value);
315
312
  if (fileCacheEnabled) {
316
- saveFileCache();
313
+ // Fire-and-forget: don't await, just let it run in background
314
+ saveFileCache().catch(() => {
315
+ // Ignore save errors silently
316
+ });
317
317
  }
318
318
  return result;
319
319
  };
@@ -10,8 +10,9 @@
10
10
  *
11
11
  * const enricher = createEnrichmentClient({
12
12
  * providers: {
13
- * hunter: { apiKey: process.env.HUNTER_API_KEY },
14
- * apollo: { apiKey: process.env.APOLLO_API_KEY },
13
+ * ldd: { apiUrl: process.env.LDD_API_URL, apiToken: process.env.LDD_API_TOKEN },
14
+ * smartprospect: { email: process.env.SMARTLEAD_EMAIL, password: process.env.SMARTLEAD_PASSWORD },
15
+ * bouncer: { apiKey: process.env.BOUNCER_API_KEY },
15
16
  * },
16
17
  * options: {
17
18
  * maxCostPerEmail: 0.05,
@@ -35,11 +36,12 @@ import type { EnrichmentClientConfig, EnrichmentClient } from "./types";
35
36
  */
36
37
  export declare function createEnrichmentClient(config: EnrichmentClientConfig): EnrichmentClient;
37
38
  export * from "./types";
39
+ export { PROVIDER_COSTS, DEFAULT_PROVIDER_ORDER } from "./types";
38
40
  export { isPersonalEmail, isBusinessEmail, isPersonalDomain, PERSONAL_DOMAINS, } from "./utils/personal-domains";
39
41
  export { isDisposableEmail, isDisposableDomain, DISPOSABLE_DOMAINS, } from "./utils/disposable-domains";
40
42
  export { isValidEmailSyntax, isRoleAccount, asciiFold, cleanNamePart, hostnameFromUrl, extractLinkedInUsername, } from "./utils/validation";
41
- export { verifyEmailMx } from "./verification/mx";
42
- export { createConstructProvider, createLddProvider, createSmartProspectProvider, createHunterProvider, createApolloProvider, createDropcontactProvider, createBouncerProvider, createSnovioProvider, verifyEmailWithBouncer, checkCatchAllDomain, verifyEmailsBatch, findEmailsWithSnovio, verifyEmailWithSnovio, clearSnovioTokenCache, } from "./providers";
43
+ export { verifyEmailMx, checkDomainCatchAll, verifyEmailsExist } from "./verification/mx";
44
+ export { createConstructProvider, createLddProvider, createSmartProspectProvider, createHunterProvider, createDropcontactProvider, createBouncerProvider, createSnovioProvider, verifyEmailWithBouncer, checkCatchAllDomain, verifyEmailsBatch, findEmailsWithSnovio, verifyEmailWithSnovio, clearSnovioTokenCache, } from "./providers";
43
45
  export { extractNumericLinkedInId } from "./providers/ldd";
44
46
  export { createSmartProspectClient, type SmartProspectClient, type SmartProspectLocationOptions, } from "./providers/smartprospect";
45
47
  export { enrichBusinessEmail, enrichBatch, enrichAllEmails, enrichAllBatch } from "./orchestrator";
@@ -11,8 +11,9 @@
11
11
  *
12
12
  * const enricher = createEnrichmentClient({
13
13
  * providers: {
14
- * hunter: { apiKey: process.env.HUNTER_API_KEY },
15
- * apollo: { apiKey: process.env.APOLLO_API_KEY },
14
+ * ldd: { apiUrl: process.env.LDD_API_URL, apiToken: process.env.LDD_API_TOKEN },
15
+ * smartprospect: { email: process.env.SMARTLEAD_EMAIL, password: process.env.SMARTLEAD_PASSWORD },
16
+ * bouncer: { apiKey: process.env.BOUNCER_API_KEY },
16
17
  * },
17
18
  * options: {
18
19
  * maxCostPerEmail: 0.05,
@@ -42,40 +43,39 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
42
43
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
43
44
  };
44
45
  Object.defineProperty(exports, "__esModule", { value: true });
45
- exports.enrichLinkedInContact = exports.parseLinkedInSearchResponse = exports.buildSmartProspectFiltersFromLinkedIn = exports.matchContacts = exports.findBestMatch = exports.classifyMatchQuality = exports.calculateMatchConfidence = exports.clearFileCache = exports.isFileCacheEnabled = exports.disableFileCache = exports.enableFileCache = exports.getSmartLeadTokenCacheStats = exports.clearAllSmartLeadTokens = exports.clearSmartLeadToken = exports.getSmartLeadUser = exports.getSmartLeadToken = exports.enrichAllBatch = exports.enrichAllEmails = exports.enrichBatch = exports.enrichBusinessEmail = exports.createSmartProspectClient = exports.extractNumericLinkedInId = exports.clearSnovioTokenCache = exports.verifyEmailWithSnovio = exports.findEmailsWithSnovio = exports.verifyEmailsBatch = exports.checkCatchAllDomain = exports.verifyEmailWithBouncer = exports.createSnovioProvider = exports.createBouncerProvider = exports.createDropcontactProvider = exports.createApolloProvider = exports.createHunterProvider = exports.createSmartProspectProvider = exports.createLddProvider = exports.createConstructProvider = exports.verifyEmailMx = exports.extractLinkedInUsername = exports.hostnameFromUrl = exports.cleanNamePart = exports.asciiFold = exports.isRoleAccount = exports.isValidEmailSyntax = exports.DISPOSABLE_DOMAINS = exports.isDisposableDomain = exports.isDisposableEmail = exports.PERSONAL_DOMAINS = exports.isPersonalDomain = exports.isBusinessEmail = exports.isPersonalEmail = void 0;
46
- exports.salesLeadToContact = exports.getEmailsForLinkedInContactsBatch = exports.getEmailsForLinkedInContact = exports.createLinkedInEnricher = exports.enrichLinkedInContactsBatch = void 0;
46
+ exports.matchContacts = exports.findBestMatch = exports.classifyMatchQuality = exports.calculateMatchConfidence = exports.clearFileCache = exports.isFileCacheEnabled = exports.disableFileCache = exports.enableFileCache = exports.getSmartLeadTokenCacheStats = exports.clearAllSmartLeadTokens = exports.clearSmartLeadToken = exports.getSmartLeadUser = exports.getSmartLeadToken = exports.enrichAllBatch = exports.enrichAllEmails = exports.enrichBatch = exports.enrichBusinessEmail = exports.createSmartProspectClient = exports.extractNumericLinkedInId = exports.clearSnovioTokenCache = exports.verifyEmailWithSnovio = exports.findEmailsWithSnovio = exports.verifyEmailsBatch = exports.checkCatchAllDomain = exports.verifyEmailWithBouncer = exports.createSnovioProvider = exports.createBouncerProvider = exports.createDropcontactProvider = exports.createHunterProvider = exports.createSmartProspectProvider = exports.createLddProvider = exports.createConstructProvider = exports.verifyEmailsExist = exports.checkDomainCatchAll = exports.verifyEmailMx = exports.extractLinkedInUsername = exports.hostnameFromUrl = exports.cleanNamePart = exports.asciiFold = exports.isRoleAccount = exports.isValidEmailSyntax = exports.DISPOSABLE_DOMAINS = exports.isDisposableDomain = exports.isDisposableEmail = exports.PERSONAL_DOMAINS = exports.isPersonalDomain = exports.isBusinessEmail = exports.isPersonalEmail = exports.DEFAULT_PROVIDER_ORDER = exports.PROVIDER_COSTS = void 0;
47
+ exports.salesLeadToContact = exports.getEmailsForLinkedInContactsBatch = exports.getEmailsForLinkedInContact = exports.createLinkedInEnricher = exports.enrichLinkedInContactsBatch = exports.enrichLinkedInContact = exports.parseLinkedInSearchResponse = exports.buildSmartProspectFiltersFromLinkedIn = void 0;
47
48
  exports.createEnrichmentClient = createEnrichmentClient;
48
49
  const orchestrator_1 = require("./orchestrator");
49
50
  const construct_1 = require("./providers/construct");
50
51
  const ldd_1 = require("./providers/ldd");
51
52
  const smartprospect_1 = require("./providers/smartprospect");
52
53
  const hunter_1 = require("./providers/hunter");
53
- const apollo_1 = require("./providers/apollo");
54
54
  const dropcontact_1 = require("./providers/dropcontact");
55
55
  const bouncer_1 = require("./providers/bouncer");
56
56
  const snovio_1 = require("./providers/snovio");
57
57
  /**
58
- * Default provider order
58
+ * Default provider order - 2-Phase Strategy
59
59
  *
60
- * Strategy:
61
- * 1. construct - FREE pattern guessing with MX check
62
- * 2. bouncer - SMTP verification of construct results ($0.006/email)
63
- * 3. ldd - FREE LinkedIn data dump lookup
64
- * 4. smartprospect - Paid SmartLead lookup ($0.01/email)
65
- * 5. snovio - Email finder for catch-all domains ($0.02/email)
66
- * 6. hunter - Hunter.io API ($0.005/email)
67
- * 7. apollo - FREE Apollo.io lookup
68
- * 8. dropcontact - Dropcontact API ($0.01/email)
60
+ * PHASE 1 - Free lookups (real data):
61
+ * - ldd: LinkedIn Data Dump - real verified emails (FREE)
62
+ * - smartprospect: SmartLead API - real verified emails (FREE with subscription)
63
+ * - construct: Pattern guessing + MX check (FREE)
64
+ *
65
+ * PHASE 2 - Paid verification/finding (only if needed):
66
+ * - bouncer: SMTP verify constructed emails ($0.006/email)
67
+ * - snovio: Email finder for catch-all domains ($0.02/email)
68
+ * - hunter: Hunter.io fallback ($0.005/email)
69
+ *
70
+ * Note: dropcontact available but not in default order
69
71
  */
70
72
  const DEFAULT_ORDER = [
71
- "construct",
72
- "bouncer",
73
73
  "ldd",
74
74
  "smartprospect",
75
+ "construct",
76
+ "bouncer",
75
77
  "snovio",
76
78
  "hunter",
77
- "apollo",
78
- "dropcontact",
79
79
  ];
80
80
  /**
81
81
  * Create an enrichment client with the given configuration
@@ -99,9 +99,6 @@ function createEnrichmentClient(config) {
99
99
  if (providerConfigs.hunter) {
100
100
  providerFuncs.set("hunter", (0, hunter_1.createHunterProvider)(providerConfigs.hunter));
101
101
  }
102
- if (providerConfigs.apollo) {
103
- providerFuncs.set("apollo", (0, apollo_1.createApolloProvider)(providerConfigs.apollo));
104
- }
105
102
  if (providerConfigs.dropcontact) {
106
103
  providerFuncs.set("dropcontact", (0, dropcontact_1.createDropcontactProvider)(providerConfigs.dropcontact));
107
104
  }
@@ -241,8 +238,11 @@ function buildCacheKey(candidate) {
241
238
  }
242
239
  return parts.join("|");
243
240
  }
244
- // Re-export types
241
+ // Re-export types and constants
245
242
  __exportStar(require("./types"), exports);
243
+ var types_1 = require("./types");
244
+ Object.defineProperty(exports, "PROVIDER_COSTS", { enumerable: true, get: function () { return types_1.PROVIDER_COSTS; } });
245
+ Object.defineProperty(exports, "DEFAULT_PROVIDER_ORDER", { enumerable: true, get: function () { return types_1.DEFAULT_PROVIDER_ORDER; } });
246
246
  // Re-export utilities
247
247
  var personal_domains_1 = require("./utils/personal-domains");
248
248
  Object.defineProperty(exports, "isPersonalEmail", { enumerable: true, get: function () { return personal_domains_1.isPersonalEmail; } });
@@ -263,13 +263,14 @@ Object.defineProperty(exports, "extractLinkedInUsername", { enumerable: true, ge
263
263
  // Re-export verification
264
264
  var mx_1 = require("./verification/mx");
265
265
  Object.defineProperty(exports, "verifyEmailMx", { enumerable: true, get: function () { return mx_1.verifyEmailMx; } });
266
+ Object.defineProperty(exports, "checkDomainCatchAll", { enumerable: true, get: function () { return mx_1.checkDomainCatchAll; } });
267
+ Object.defineProperty(exports, "verifyEmailsExist", { enumerable: true, get: function () { return mx_1.verifyEmailsExist; } });
266
268
  // Re-export providers (for advanced usage)
267
269
  var providers_1 = require("./providers");
268
270
  Object.defineProperty(exports, "createConstructProvider", { enumerable: true, get: function () { return providers_1.createConstructProvider; } });
269
271
  Object.defineProperty(exports, "createLddProvider", { enumerable: true, get: function () { return providers_1.createLddProvider; } });
270
272
  Object.defineProperty(exports, "createSmartProspectProvider", { enumerable: true, get: function () { return providers_1.createSmartProspectProvider; } });
271
273
  Object.defineProperty(exports, "createHunterProvider", { enumerable: true, get: function () { return providers_1.createHunterProvider; } });
272
- Object.defineProperty(exports, "createApolloProvider", { enumerable: true, get: function () { return providers_1.createApolloProvider; } });
273
274
  Object.defineProperty(exports, "createDropcontactProvider", { enumerable: true, get: function () { return providers_1.createDropcontactProvider; } });
274
275
  Object.defineProperty(exports, "createBouncerProvider", { enumerable: true, get: function () { return providers_1.createBouncerProvider; } });
275
276
  Object.defineProperty(exports, "createSnovioProvider", { enumerable: true, get: function () { return providers_1.createSnovioProvider; } });
@@ -258,7 +258,7 @@ export declare function createLinkedInEnricher(smartProspectConfig: SmartProspec
258
258
  /**
259
259
  * Email source - where the email was found
260
260
  */
261
- export type EmailSource = 'ldd' | 'smartprospect' | 'linkedin' | 'pattern' | 'hunter' | 'apollo';
261
+ export type EmailSource = 'ldd' | 'smartprospect' | 'linkedin' | 'pattern' | 'hunter' | 'bouncer' | 'snovio';
262
262
  /**
263
263
  * Email result from unified lookup
264
264
  */
@@ -319,10 +319,15 @@ export interface GetEmailsConfig {
319
319
  hunter?: {
320
320
  apiKey: string;
321
321
  };
322
- /** Apollo configuration (PAID - last resort) */
323
- apollo?: {
322
+ /** Bouncer configuration (PAID - SMTP verification) */
323
+ bouncer?: {
324
324
  apiKey: string;
325
325
  };
326
+ /** Snov.io configuration (PAID - email finder) */
327
+ snovio?: {
328
+ userId: string;
329
+ apiSecret: string;
330
+ };
326
331
  }
327
332
  /**
328
333
  * Options for unified email lookup
@@ -863,15 +863,16 @@ async function getEmailsForLinkedInContact(contactOrLead, config, options = {})
863
863
  ? Math.max(...result.emails.map(e => e.confidence))
864
864
  : 0;
865
865
  // ==========================================================================
866
- // Phase 3: PAID providers as last resort (Hunter/Apollo)
866
+ // Phase 3: PAID providers as last resort (Hunter/Bouncer/Snovio)
867
867
  // ==========================================================================
868
868
  if (!skipPaidProviders && finalBestConfidence < paidProviderThreshold) {
869
869
  // Only use paid providers if we have low confidence or no results
870
- // TODO: Implement Hunter and Apollo providers when needed
870
+ // TODO: Implement Hunter, Bouncer, Snovio providers when needed
871
871
  // For now, just mark that we would have queried them
872
- if (config.hunter?.apiKey || config.apollo?.apiKey) {
872
+ if (config.hunter?.apiKey || config.bouncer?.apiKey || config.snovio?.userId) {
873
873
  // result.providersQueried.push('hunter');
874
- // result.providersQueried.push('apollo');
874
+ // result.providersQueried.push('bouncer');
875
+ // result.providersQueried.push('snovio');
875
876
  // await queryPaidProviders(contact, config, addEmail, result);
876
877
  }
877
878
  }
@@ -882,7 +883,8 @@ async function getEmailsForLinkedInContact(contactOrLead, config, options = {})
882
883
  linkedin: 2, // LinkedIn company lookup (for domain discovery, doesn't provide emails)
883
884
  pattern: 3,
884
885
  hunter: 4,
885
- apollo: 5,
886
+ bouncer: 5,
887
+ snovio: 6,
886
888
  };
887
889
  result.emails.sort((a, b) => {
888
890
  if (b.confidence !== a.confidence) {
@@ -17,18 +17,19 @@ const validation_1 = require("./utils/validation");
17
17
  * Default provider costs in USD per lookup
18
18
  *
19
19
  * Costs based on 2025 pricing:
20
- * - Bouncer: $0.006/email at scale (best accuracy 99%+)
21
- * - Snov.io: $0.02/email (email finding + verification)
22
- * - Hunter: $0.005/email
23
- * - SmartProspect: $0.01/email
24
- * - Dropcontact: $0.01/email
20
+ * - ldd: FREE (subscription-based)
21
+ * - smartprospect: FREE (included in SmartLead subscription)
22
+ * - construct: FREE (pattern guessing + MX check)
23
+ * - bouncer: $0.006/email (SMTP verification, 99%+ accuracy)
24
+ * - snovio: $0.02/email (email finding + verification)
25
+ * - hunter: $0.005/email
26
+ * - dropcontact: $0.01/email (not in default order)
25
27
  */
26
28
  const _PROVIDER_COSTS = {
27
29
  construct: 0,
28
30
  ldd: 0,
29
- smartprospect: 0.01,
31
+ smartprospect: 0,
30
32
  hunter: 0.005,
31
- apollo: 0,
32
33
  dropcontact: 0.01,
33
34
  bouncer: 0.006,
34
35
  snovio: 0.02,
@@ -235,11 +236,25 @@ async function enrichBatch(candidates, options) {
235
236
  const delayMs = options.delayMs ?? 200;
236
237
  for (let i = 0; i < candidates.length; i += batchSize) {
237
238
  const chunk = candidates.slice(i, i + batchSize);
238
- // Process batch in parallel
239
- const batchResults = await Promise.all(chunk.map((candidate) => enrichBusinessEmail(candidate, options)));
240
- // Combine results with candidates
239
+ // Process batch in parallel (use allSettled for resilience)
240
+ const batchResults = await Promise.allSettled(chunk.map((candidate) => enrichBusinessEmail(candidate, options)));
241
+ // Combine results with candidates (handle both fulfilled and rejected)
241
242
  batchResults.forEach((result, idx) => {
242
- results.push({ candidate: chunk[idx], ...result });
243
+ const candidate = chunk[idx];
244
+ if (result.status === 'fulfilled') {
245
+ results.push({ candidate, ...result.value });
246
+ }
247
+ else {
248
+ // On error, return empty result for this candidate
249
+ results.push({
250
+ candidate,
251
+ business_email: null,
252
+ business_email_source: null,
253
+ business_email_verified: false,
254
+ last_checked_at: new Date().toISOString(),
255
+ status: `error: ${result.reason instanceof Error ? result.reason.message : 'unknown'}`,
256
+ });
257
+ }
243
258
  });
244
259
  // Delay between batches to avoid rate limiting
245
260
  if (i + batchSize < candidates.length && delayMs > 0) {
@@ -432,9 +447,24 @@ async function enrichAllBatch(candidates, options) {
432
447
  const delayMs = options.delayMs ?? 200;
433
448
  for (let i = 0; i < candidates.length; i += batchSize) {
434
449
  const chunk = candidates.slice(i, i + batchSize);
435
- // Process batch in parallel
436
- const batchResults = await Promise.all(chunk.map((candidate) => enrichAllEmails(candidate, options)));
437
- results.push(...batchResults);
450
+ // Process batch in parallel (use allSettled for resilience)
451
+ const batchResults = await Promise.allSettled(chunk.map((candidate) => enrichAllEmails(candidate, options)));
452
+ // Handle both fulfilled and rejected results
453
+ batchResults.forEach((result, idx) => {
454
+ if (result.status === 'fulfilled') {
455
+ results.push(result.value);
456
+ }
457
+ else {
458
+ // On error, return empty result for this candidate
459
+ results.push({
460
+ emails: [],
461
+ candidate: chunk[idx],
462
+ totalCost: 0,
463
+ providersQueried: [],
464
+ completedAt: new Date().toISOString(),
465
+ });
466
+ }
467
+ });
438
468
  // Delay between batches to avoid rate limiting
439
469
  if (i + batchSize < candidates.length && delayMs > 0) {
440
470
  await new Promise((r) => setTimeout(r, delayMs));
@@ -87,6 +87,7 @@ function extractDomain(candidate) {
87
87
  function createConstructProvider(config) {
88
88
  const maxAttempts = config?.maxAttempts ?? 8;
89
89
  const timeoutMs = config?.timeoutMs ?? 5000;
90
+ const smtpVerifyDelayMs = config?.smtpVerifyDelayMs ?? 2000; // Delay between SMTP checks
90
91
  async function fetchEmail(candidate) {
91
92
  const { first, last } = extractNames(candidate);
92
93
  const domain = extractDomain(candidate);
@@ -100,22 +101,79 @@ function createConstructProvider(config) {
100
101
  }
101
102
  const candidates = buildCandidates({ first, last, domain });
102
103
  const max = Math.min(candidates.length, maxAttempts);
104
+ // First, check if domain is catch-all
105
+ const catchAllResult = await (0, mx_1.checkDomainCatchAll)(domain, { timeoutMs: 10000 });
106
+ const isCatchAll = catchAllResult.isCatchAll;
103
107
  // Collect ALL valid email patterns (not just first match)
104
108
  const validEmails = [];
105
- for (let i = 0; i < max; i++) {
106
- const email = candidates[i];
107
- const verification = await (0, mx_1.verifyEmailMx)(email, { timeoutMs });
108
- if (verification.valid === true && verification.confidence >= 50) {
109
- validEmails.push({
110
- email,
111
- verified: true,
112
- confidence: verification.confidence,
113
- isCatchAll: verification.isCatchAll,
114
- metadata: {
115
- pattern: email.split('@')[0], // The local part pattern used
116
- mxRecords: verification.mxRecords,
117
- },
118
- });
109
+ // Track all attempted patterns for debugging
110
+ const attemptedPatterns = [];
111
+ // If NOT catch-all, we can verify each email via SMTP
112
+ if (isCatchAll === false) {
113
+ // Verify emails one by one, stop when we find a valid one
114
+ const emailsToVerify = candidates.slice(0, max);
115
+ for (let i = 0; i < emailsToVerify.length; i++) {
116
+ const email = emailsToVerify[i];
117
+ // If we already found a valid email, skip the rest
118
+ if (validEmails.length > 0) {
119
+ attemptedPatterns.push({ email, status: 'skipped' });
120
+ continue;
121
+ }
122
+ // Add delay between checks (except first one)
123
+ if (i > 0) {
124
+ await new Promise((resolve) => setTimeout(resolve, smtpVerifyDelayMs));
125
+ }
126
+ // Verify single email
127
+ const results = await (0, mx_1.verifyEmailsExist)([email], { delayMs: 0, timeoutMs });
128
+ const result = results[0];
129
+ if (result.exists === true) {
130
+ // Email confirmed to exist!
131
+ attemptedPatterns.push({ email, status: 'exists' });
132
+ validEmails.push({
133
+ email: result.email,
134
+ verified: true,
135
+ confidence: 95, // High confidence - SMTP verified
136
+ isCatchAll: false,
137
+ metadata: {
138
+ pattern: result.email.split('@')[0],
139
+ mxRecords: catchAllResult.mxRecords,
140
+ smtpVerified: true,
141
+ attemptedPatterns, // Include what was tried
142
+ },
143
+ });
144
+ // Found one! Stop checking more
145
+ break;
146
+ }
147
+ else if (result.exists === false) {
148
+ attemptedPatterns.push({ email, status: 'not_found' });
149
+ }
150
+ else {
151
+ attemptedPatterns.push({ email, status: 'unknown' });
152
+ }
153
+ }
154
+ // If no valid email found, include attempted patterns in metadata
155
+ if (validEmails.length === 0 && attemptedPatterns.length > 0) {
156
+ // Return null but could add metadata about attempts
157
+ }
158
+ }
159
+ else {
160
+ // Catch-all or unknown - fall back to MX verification only
161
+ for (let i = 0; i < max; i++) {
162
+ const email = candidates[i];
163
+ const verification = await (0, mx_1.verifyEmailMx)(email, { timeoutMs });
164
+ if (verification.valid === true && verification.confidence >= 50) {
165
+ validEmails.push({
166
+ email,
167
+ verified: true,
168
+ confidence: verification.confidence,
169
+ isCatchAll: isCatchAll ?? undefined,
170
+ metadata: {
171
+ pattern: email.split('@')[0],
172
+ mxRecords: verification.mxRecords,
173
+ smtpVerified: false,
174
+ },
175
+ });
176
+ }
119
177
  }
120
178
  }
121
179
  if (validEmails.length === 0) {
@@ -7,62 +7,8 @@
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.createHunterProvider = createHunterProvider;
10
+ const http_retry_1 = require("../utils/http-retry");
10
11
  const API_BASE = "https://api.hunter.io/v2";
11
- /**
12
- * Delay helper for retry logic
13
- */
14
- async function delay(ms) {
15
- return new Promise((r) => setTimeout(r, ms));
16
- }
17
- /**
18
- * Check if value is truthy
19
- */
20
- function truthy(v) {
21
- return v !== undefined && v !== null && String(v).length > 0;
22
- }
23
- /**
24
- * Map Hunter verification status to boolean
25
- */
26
- function mapVerified(status) {
27
- if (!status)
28
- return undefined;
29
- const s = String(status).toLowerCase();
30
- if (s === "valid" || s === "deliverable")
31
- return true;
32
- if (s === "invalid" || s === "undeliverable")
33
- return false;
34
- return undefined; // catch-all/unknown/webmail -> leave undefined
35
- }
36
- /**
37
- * HTTP request with retry on 429 rate limit
38
- */
39
- async function requestWithRetry(url, retries = 1, backoffMs = 200) {
40
- let lastErr;
41
- for (let i = 0; i <= retries; i++) {
42
- try {
43
- const res = await fetch(url);
44
- // Retry on rate limit
45
- if (res?.status === 429 && i < retries) {
46
- await delay(backoffMs * Math.pow(2, i));
47
- continue;
48
- }
49
- if (!res || res.status >= 400) {
50
- lastErr = new Error(`hunter_http_${res?.status ?? "error"}`);
51
- break;
52
- }
53
- const json = (await res.json());
54
- return json;
55
- }
56
- catch (err) {
57
- lastErr = err;
58
- if (i < retries) {
59
- await delay(backoffMs * Math.pow(2, i));
60
- continue;
61
- }
62
- }
63
- }
64
- throw lastErr ?? new Error("hunter_http_error");
65
- }
66
12
  /**
67
13
  * Extract name and domain from candidate
68
14
  */
@@ -99,7 +45,7 @@ function createHunterProvider(config) {
99
45
  let url = null;
100
46
  let isEmailFinder = false;
101
47
  // Use email-finder if we have name components
102
- if (truthy(first) && truthy(last) && truthy(domain)) {
48
+ if ((0, http_retry_1.truthy)(first) && (0, http_retry_1.truthy)(last) && (0, http_retry_1.truthy)(domain)) {
103
49
  const qs = new URLSearchParams({
104
50
  api_key: String(apiKey),
105
51
  domain: String(domain),
@@ -110,7 +56,7 @@ function createHunterProvider(config) {
110
56
  isEmailFinder = true;
111
57
  }
112
58
  // Fall back to domain-search if only domain available (can return multiple)
113
- else if (truthy(domain)) {
59
+ else if ((0, http_retry_1.truthy)(domain)) {
114
60
  const qs = new URLSearchParams({
115
61
  api_key: String(apiKey),
116
62
  domain: String(domain),
@@ -122,7 +68,7 @@ function createHunterProvider(config) {
122
68
  return null; // Can't search without domain
123
69
  }
124
70
  try {
125
- const json = await requestWithRetry(url, 1, 100);
71
+ const json = await (0, http_retry_1.getWithRetry)(url, undefined, { retries: 1, backoffMs: 100 });
126
72
  // Parse email-finder response shape (single result)
127
73
  if (isEmailFinder) {
128
74
  const ef = (json && (json.data || json.result));
@@ -131,7 +77,7 @@ function createHunterProvider(config) {
131
77
  const score = typeof ef.score === "number"
132
78
  ? ef.score
133
79
  : Number(ef.score ?? 0) || undefined;
134
- const verified = mapVerified(ef?.verification?.status ?? ef?.status);
80
+ const verified = (0, http_retry_1.mapVerifiedStatus)(ef?.verification?.status ?? ef?.status);
135
81
  if (!email)
136
82
  return null;
137
83
  return { email, verified, score };
@@ -158,7 +104,7 @@ function createHunterProvider(config) {
158
104
  const score = typeof hit?.confidence === "number"
159
105
  ? hit.confidence
160
106
  : Number(hit?.confidence ?? 0) || 50;
161
- const verified = mapVerified(hit?.verification?.status ?? hit?.status);
107
+ const verified = (0, http_retry_1.mapVerifiedStatus)(hit?.verification?.status ?? hit?.status);
162
108
  emails.push({
163
109
  email,
164
110
  verified,
@@ -5,7 +5,6 @@ export { createConstructProvider } from './construct';
5
5
  export { createLddProvider } from './ldd';
6
6
  export { createSmartProspectProvider } from './smartprospect';
7
7
  export { createHunterProvider } from './hunter';
8
- export { createApolloProvider } from './apollo';
9
8
  export { createDropcontactProvider } from './dropcontact';
10
9
  export { createBouncerProvider, verifyEmailWithBouncer, checkCatchAllDomain, verifyEmailsBatch } from './bouncer';
11
10
  export { createSnovioProvider, findEmailsWithSnovio, verifyEmailWithSnovio, clearSnovioTokenCache } from './snovio';