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.
- package/README.md +512 -232
- package/dist/enrichment/auth/smartlead-auth.d.ts +3 -3
- package/dist/enrichment/auth/smartlead-auth.js +25 -25
- package/dist/enrichment/index.d.ts +6 -4
- package/dist/enrichment/index.js +45 -13
- package/dist/enrichment/matching.d.ts +8 -3
- package/dist/enrichment/matching.js +7 -5
- package/dist/enrichment/orchestrator.js +48 -9
- package/dist/enrichment/providers/bouncer.d.ts +67 -0
- package/dist/enrichment/providers/bouncer.js +233 -0
- package/dist/enrichment/providers/construct.js +72 -14
- package/dist/enrichment/providers/hunter.js +6 -60
- package/dist/enrichment/providers/index.d.ts +2 -1
- package/dist/enrichment/providers/index.js +11 -3
- package/dist/enrichment/providers/ldd.js +5 -47
- package/dist/enrichment/providers/smartprospect.js +9 -14
- package/dist/enrichment/providers/snovio.d.ts +58 -0
- package/dist/enrichment/providers/snovio.js +286 -0
- package/dist/enrichment/types.d.ts +133 -10
- package/dist/enrichment/types.js +28 -6
- package/dist/enrichment/utils/http-retry.d.ts +96 -0
- package/dist/enrichment/utils/http-retry.js +162 -0
- package/dist/enrichment/verification/index.d.ts +1 -1
- package/dist/enrichment/verification/index.js +3 -1
- package/dist/enrichment/verification/mx.d.ts +33 -0
- package/dist/enrichment/verification/mx.js +367 -7
- package/dist/index.d.ts +196 -6
- package/dist/index.js +159 -12
- package/dist/parsers/search-parser.js +7 -3
- package/package.json +10 -3
|
@@ -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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
14
|
-
*
|
|
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,
|
|
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";
|
package/dist/enrichment/index.js
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
*
|
|
12
12
|
* const enricher = createEnrichmentClient({
|
|
13
13
|
* providers: {
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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.
|
|
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' | '
|
|
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
|
-
/**
|
|
323
|
-
|
|
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/
|
|
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
|
|
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.
|
|
872
|
+
if (config.hunter?.apiKey || config.bouncer?.apiKey || config.snovio?.userId) {
|
|
873
873
|
// result.providersQueried.push('hunter');
|
|
874
|
-
// result.providersQueried.push('
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
428
|
-
results
|
|
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>>;
|