linkedin-secret-sauce 0.10.1 → 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, } 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,25 +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.salesLeadToContact = exports.getEmailsForLinkedInContactsBatch = exports.getEmailsForLinkedInContact = exports.createLinkedInEnricher = exports.enrichLinkedInContactsBatch = 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.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.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;
46
48
  exports.createEnrichmentClient = createEnrichmentClient;
47
49
  const orchestrator_1 = require("./orchestrator");
48
50
  const construct_1 = require("./providers/construct");
49
51
  const ldd_1 = require("./providers/ldd");
50
52
  const smartprospect_1 = require("./providers/smartprospect");
51
53
  const hunter_1 = require("./providers/hunter");
52
- const apollo_1 = require("./providers/apollo");
53
54
  const dropcontact_1 = require("./providers/dropcontact");
55
+ const bouncer_1 = require("./providers/bouncer");
56
+ const snovio_1 = require("./providers/snovio");
54
57
  /**
55
- * Default provider order
58
+ * Default provider order - 2-Phase Strategy
59
+ *
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
56
71
  */
57
72
  const DEFAULT_ORDER = [
58
- "construct",
59
73
  "ldd",
60
74
  "smartprospect",
75
+ "construct",
76
+ "bouncer",
77
+ "snovio",
61
78
  "hunter",
62
- "apollo",
63
- "dropcontact",
64
79
  ];
65
80
  /**
66
81
  * Create an enrichment client with the given configuration
@@ -84,12 +99,15 @@ function createEnrichmentClient(config) {
84
99
  if (providerConfigs.hunter) {
85
100
  providerFuncs.set("hunter", (0, hunter_1.createHunterProvider)(providerConfigs.hunter));
86
101
  }
87
- if (providerConfigs.apollo) {
88
- providerFuncs.set("apollo", (0, apollo_1.createApolloProvider)(providerConfigs.apollo));
89
- }
90
102
  if (providerConfigs.dropcontact) {
91
103
  providerFuncs.set("dropcontact", (0, dropcontact_1.createDropcontactProvider)(providerConfigs.dropcontact));
92
104
  }
105
+ if (providerConfigs.bouncer) {
106
+ providerFuncs.set("bouncer", (0, bouncer_1.createBouncerProvider)(providerConfigs.bouncer));
107
+ }
108
+ if (providerConfigs.snovio) {
109
+ providerFuncs.set("snovio", (0, snovio_1.createSnovioProvider)(providerConfigs.snovio));
110
+ }
93
111
  // Build ordered provider list
94
112
  const providerOrder = options.providerOrder ?? DEFAULT_ORDER;
95
113
  const orderedProviders = [];
@@ -220,8 +238,11 @@ function buildCacheKey(candidate) {
220
238
  }
221
239
  return parts.join("|");
222
240
  }
223
- // Re-export types
241
+ // Re-export types and constants
224
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; } });
225
246
  // Re-export utilities
226
247
  var personal_domains_1 = require("./utils/personal-domains");
227
248
  Object.defineProperty(exports, "isPersonalEmail", { enumerable: true, get: function () { return personal_domains_1.isPersonalEmail; } });
@@ -242,14 +263,25 @@ Object.defineProperty(exports, "extractLinkedInUsername", { enumerable: true, ge
242
263
  // Re-export verification
243
264
  var mx_1 = require("./verification/mx");
244
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; } });
245
268
  // Re-export providers (for advanced usage)
246
269
  var providers_1 = require("./providers");
247
270
  Object.defineProperty(exports, "createConstructProvider", { enumerable: true, get: function () { return providers_1.createConstructProvider; } });
248
271
  Object.defineProperty(exports, "createLddProvider", { enumerable: true, get: function () { return providers_1.createLddProvider; } });
249
272
  Object.defineProperty(exports, "createSmartProspectProvider", { enumerable: true, get: function () { return providers_1.createSmartProspectProvider; } });
250
273
  Object.defineProperty(exports, "createHunterProvider", { enumerable: true, get: function () { return providers_1.createHunterProvider; } });
251
- Object.defineProperty(exports, "createApolloProvider", { enumerable: true, get: function () { return providers_1.createApolloProvider; } });
252
274
  Object.defineProperty(exports, "createDropcontactProvider", { enumerable: true, get: function () { return providers_1.createDropcontactProvider; } });
275
+ Object.defineProperty(exports, "createBouncerProvider", { enumerable: true, get: function () { return providers_1.createBouncerProvider; } });
276
+ Object.defineProperty(exports, "createSnovioProvider", { enumerable: true, get: function () { return providers_1.createSnovioProvider; } });
277
+ // Bouncer utilities
278
+ Object.defineProperty(exports, "verifyEmailWithBouncer", { enumerable: true, get: function () { return providers_1.verifyEmailWithBouncer; } });
279
+ Object.defineProperty(exports, "checkCatchAllDomain", { enumerable: true, get: function () { return providers_1.checkCatchAllDomain; } });
280
+ Object.defineProperty(exports, "verifyEmailsBatch", { enumerable: true, get: function () { return providers_1.verifyEmailsBatch; } });
281
+ // Snov.io utilities
282
+ Object.defineProperty(exports, "findEmailsWithSnovio", { enumerable: true, get: function () { return providers_1.findEmailsWithSnovio; } });
283
+ Object.defineProperty(exports, "verifyEmailWithSnovio", { enumerable: true, get: function () { return providers_1.verifyEmailWithSnovio; } });
284
+ Object.defineProperty(exports, "clearSnovioTokenCache", { enumerable: true, get: function () { return providers_1.clearSnovioTokenCache; } });
253
285
  // Re-export LDD utilities
254
286
  var ldd_2 = require("./providers/ldd");
255
287
  Object.defineProperty(exports, "extractNumericLinkedInId", { enumerable: true, get: function () { return ldd_2.extractNumericLinkedInId; } });
@@ -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) {
@@ -15,14 +15,24 @@ const disposable_domains_1 = require("./utils/disposable-domains");
15
15
  const validation_1 = require("./utils/validation");
16
16
  /**
17
17
  * Default provider costs in USD per lookup
18
+ *
19
+ * Costs based on 2025 pricing:
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)
18
27
  */
19
28
  const _PROVIDER_COSTS = {
20
29
  construct: 0,
21
30
  ldd: 0,
22
- smartprospect: 0.01,
31
+ smartprospect: 0,
23
32
  hunter: 0.005,
24
- apollo: 0,
25
33
  dropcontact: 0.01,
34
+ bouncer: 0.006,
35
+ snovio: 0.02,
26
36
  };
27
37
  /**
28
38
  * Normalize provider result to canonical format
@@ -226,11 +236,25 @@ async function enrichBatch(candidates, options) {
226
236
  const delayMs = options.delayMs ?? 200;
227
237
  for (let i = 0; i < candidates.length; i += batchSize) {
228
238
  const chunk = candidates.slice(i, i + batchSize);
229
- // Process batch in parallel
230
- const batchResults = await Promise.all(chunk.map((candidate) => enrichBusinessEmail(candidate, options)));
231
- // 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)
232
242
  batchResults.forEach((result, idx) => {
233
- 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
+ }
234
258
  });
235
259
  // Delay between batches to avoid rate limiting
236
260
  if (i + batchSize < candidates.length && delayMs > 0) {
@@ -423,9 +447,24 @@ async function enrichAllBatch(candidates, options) {
423
447
  const delayMs = options.delayMs ?? 200;
424
448
  for (let i = 0; i < candidates.length; i += batchSize) {
425
449
  const chunk = candidates.slice(i, i + batchSize);
426
- // Process batch in parallel
427
- const batchResults = await Promise.all(chunk.map((candidate) => enrichAllEmails(candidate, options)));
428
- 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
+ });
429
468
  // Delay between batches to avoid rate limiting
430
469
  if (i + batchSize < candidates.length && delayMs > 0) {
431
470
  await new Promise((r) => setTimeout(r, delayMs));
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Bouncer.io Email Verification Provider
3
+ *
4
+ * Provides SMTP-level email verification with 99%+ accuracy.
5
+ * Best for verifying pattern-guessed emails on non-catch-all domains.
6
+ *
7
+ * Features:
8
+ * - SMTP verification (checks if mailbox exists)
9
+ * - Catch-all domain detection
10
+ * - Disposable email detection
11
+ * - Role account detection
12
+ * - Toxicity scoring (0-5)
13
+ *
14
+ * @see https://docs.usebouncer.com
15
+ */
16
+ import type { EnrichmentCandidate, ProviderResult, ProviderMultiResult, BouncerConfig, BouncerVerifyResponse } from '../types';
17
+ /**
18
+ * Create the Bouncer verification provider
19
+ *
20
+ * NOTE: This provider is a VERIFIER, not a FINDER. It verifies emails
21
+ * that were generated by the construct provider or passed in the candidate.
22
+ *
23
+ * Usage in the enrichment flow:
24
+ * 1. construct provider generates email patterns
25
+ * 2. bouncer provider verifies which patterns are deliverable
26
+ * 3. Only verified emails are returned
27
+ */
28
+ export declare function createBouncerProvider(config: BouncerConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | ProviderMultiResult | null>;
29
+ /**
30
+ * Standalone function to verify a single email via Bouncer
31
+ *
32
+ * Useful for ad-hoc verification outside the enrichment flow.
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * const result = await verifyEmailWithBouncer('test@example.com', {
37
+ * apiKey: process.env.BOUNCER_API_KEY,
38
+ * });
39
+ * console.log(result.status); // 'deliverable' | 'undeliverable' | 'risky' | 'unknown'
40
+ * ```
41
+ */
42
+ export declare function verifyEmailWithBouncer(email: string, config: BouncerConfig): Promise<BouncerVerifyResponse | null>;
43
+ /**
44
+ * Check if a domain is catch-all via Bouncer
45
+ *
46
+ * Sends a verification request for a random email and checks acceptAll flag.
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * const isCatchAll = await checkCatchAllDomain('example.com', {
51
+ * apiKey: process.env.BOUNCER_API_KEY,
52
+ * });
53
+ * ```
54
+ */
55
+ export declare function checkCatchAllDomain(domain: string, config: BouncerConfig): Promise<boolean | null>;
56
+ /**
57
+ * Verify multiple emails in batch via Bouncer
58
+ *
59
+ * @example
60
+ * ```typescript
61
+ * const results = await verifyEmailsBatch(
62
+ * ['john@example.com', 'jane@example.com'],
63
+ * { apiKey: process.env.BOUNCER_API_KEY }
64
+ * );
65
+ * ```
66
+ */
67
+ export declare function verifyEmailsBatch(emails: string[], config: BouncerConfig): Promise<Map<string, BouncerVerifyResponse | null>>;