linkedin-secret-sauce 0.12.3 → 0.12.5
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 +6 -49
- package/dist/enrichment/index.d.ts +3 -5
- package/dist/enrichment/index.js +4 -20
- package/dist/enrichment/matching.d.ts +4 -8
- package/dist/enrichment/matching.js +165 -84
- package/dist/enrichment/orchestrator.d.ts +1 -1
- package/dist/enrichment/orchestrator.js +51 -44
- package/dist/enrichment/providers/bounceban.js +19 -10
- package/dist/enrichment/providers/construct.js +97 -89
- package/dist/enrichment/providers/cosiall.js +2 -2
- package/dist/enrichment/providers/hunter.js +2 -1
- package/dist/enrichment/providers/index.d.ts +0 -2
- package/dist/enrichment/providers/index.js +1 -8
- package/dist/enrichment/providers/ldd.js +4 -2
- package/dist/enrichment/providers/smartprospect.d.ts +5 -2
- package/dist/enrichment/providers/smartprospect.js +64 -27
- package/dist/enrichment/providers/snovio.d.ts +4 -4
- package/dist/enrichment/providers/snovio.js +21 -17
- package/dist/enrichment/providers/trykitt.d.ts +2 -2
- package/dist/enrichment/providers/trykitt.js +86 -21
- package/dist/enrichment/types.d.ts +8 -71
- package/dist/enrichment/types.js +3 -8
- package/dist/enrichment/utils/rate-limiter.js +0 -2
- package/dist/index.d.ts +2 -4
- package/dist/index.js +2 -2
- package/docs/ENRICHMENT.md +3 -45
- package/docs/INTEGRATION.md +1 -3
- package/docs/PLAYGROUND.md +4 -9
- package/docs/api/assets/hierarchy.js +1 -1
- package/docs/api/assets/navigation.js +1 -1
- package/docs/api/assets/search.js +1 -1
- package/docs/api/classes/LinkedInClientError.html +4 -4
- package/docs/api/functions/_testGetAccountCookies.html +2 -2
- package/docs/api/functions/_testGetAccountEntry.html +2 -2
- package/docs/api/functions/_testGetAllAccountIds.html +2 -2
- package/docs/api/functions/_testGetPoolState.html +2 -2
- package/docs/api/functions/adminResetAccount.html +1 -1
- package/docs/api/functions/adminSetCooldown.html +1 -1
- package/docs/api/functions/buildCookieHeader.html +1 -1
- package/docs/api/functions/clearAllSmartLeadTokens.html +2 -2
- package/docs/api/functions/clearRequestHistory.html +1 -1
- package/docs/api/functions/clearSessionAccount.html +1 -1
- package/docs/api/functions/clearSmartLeadToken.html +2 -2
- package/docs/api/functions/createEnrichmentClient.html +3 -3
- package/docs/api/functions/extractCsrfToken.html +1 -1
- package/docs/api/functions/extractLinkedInHandle.html +2 -2
- package/docs/api/functions/fetchCookiesFromCosiall.html +2 -2
- package/docs/api/functions/fetchProfileEmailsFromCosiall.html +2 -2
- package/docs/api/functions/forceRefreshCookies.html +1 -1
- package/docs/api/functions/getAccountForSession.html +1 -1
- package/docs/api/functions/getAccountsSummary.html +1 -1
- package/docs/api/functions/getCompaniesBatch.html +2 -2
- package/docs/api/functions/getCompanyById.html +2 -2
- package/docs/api/functions/getCompanyByUrl.html +1 -1
- package/docs/api/functions/getConfig.html +1 -1
- package/docs/api/functions/getCookiePoolHealth.html +1 -1
- package/docs/api/functions/getProfileByUrn.html +2 -2
- package/docs/api/functions/getProfileByVanity.html +2 -2
- package/docs/api/functions/getProfilesBatch.html +1 -1
- package/docs/api/functions/getRequestHistory.html +1 -1
- package/docs/api/functions/getSalesNavigatorProfileDetails.html +1 -1
- package/docs/api/functions/getSalesNavigatorProfileFull.html +2 -2
- package/docs/api/functions/getSmartLeadToken.html +1 -1
- package/docs/api/functions/getSmartLeadTokenCacheStats.html +2 -2
- package/docs/api/functions/getSmartLeadUser.html +2 -2
- package/docs/api/functions/getSnapshot.html +1 -1
- package/docs/api/functions/getYearsAtCompanyOptions.html +2 -2
- package/docs/api/functions/getYearsInPositionOptions.html +2 -2
- package/docs/api/functions/getYearsOfExperienceOptions.html +2 -2
- package/docs/api/functions/incrementMetric.html +1 -1
- package/docs/api/functions/initializeCookiePool.html +1 -1
- package/docs/api/functions/initializeLinkedInClient.html +1 -1
- package/docs/api/functions/isBusinessEmail.html +2 -2
- package/docs/api/functions/isDisposableDomain.html +2 -2
- package/docs/api/functions/isDisposableEmail.html +2 -2
- package/docs/api/functions/isPersonalDomain.html +2 -2
- package/docs/api/functions/isPersonalEmail.html +2 -2
- package/docs/api/functions/isRoleAccount.html +2 -2
- package/docs/api/functions/isValidEmailSyntax.html +2 -2
- package/docs/api/functions/parseFullProfile.html +2 -2
- package/docs/api/functions/parseSalesSearchResults.html +1 -1
- package/docs/api/functions/reportAccountFailure.html +1 -1
- package/docs/api/functions/reportAccountSuccess.html +1 -1
- package/docs/api/functions/resolveCompanyUniversalName.html +1 -1
- package/docs/api/functions/searchSalesLeads.html +2 -2
- package/docs/api/functions/selectAccountForRequest.html +1 -1
- package/docs/api/functions/setAccountForSession.html +1 -1
- package/docs/api/functions/typeahead.html +1 -1
- package/docs/api/functions/verifyEmailMx.html +1 -1
- package/docs/api/hierarchy.html +1 -1
- package/docs/api/index.html +3 -3
- package/docs/api/interfaces/AccountCookies.html +2 -2
- package/docs/api/interfaces/BatchEnrichmentOptions.html +8 -8
- package/docs/api/interfaces/CacheAdapter.html +4 -4
- package/docs/api/interfaces/CanonicalEmail.html +8 -8
- package/docs/api/interfaces/Company.html +2 -2
- package/docs/api/interfaces/ConstructConfig.html +5 -5
- package/docs/api/interfaces/CosiallProfileEmailsResponse.html +6 -6
- package/docs/api/interfaces/EnrichmentCandidate.html +4 -4
- package/docs/api/interfaces/EnrichmentClient.html +6 -6
- package/docs/api/interfaces/EnrichmentClientConfig.html +7 -7
- package/docs/api/interfaces/EnrichmentLogger.html +3 -3
- package/docs/api/interfaces/EnrichmentOptions.html +6 -6
- package/docs/api/interfaces/HunterConfig.html +3 -3
- package/docs/api/interfaces/LddConfig.html +3 -3
- package/docs/api/interfaces/LddProfileData.html +2 -2
- package/docs/api/interfaces/LinkedInClientConfig.html +2 -2
- package/docs/api/interfaces/LinkedInCookie.html +2 -2
- package/docs/api/interfaces/LinkedInPosition.html +2 -2
- package/docs/api/interfaces/LinkedInProfile.html +2 -2
- package/docs/api/interfaces/LinkedInSpotlightBadge.html +2 -2
- package/docs/api/interfaces/LinkedInTenure.html +2 -2
- package/docs/api/interfaces/Metrics.html +2 -2
- package/docs/api/interfaces/MetricsSnapshot.html +2 -2
- package/docs/api/interfaces/ProfileEducation.html +2 -2
- package/docs/api/interfaces/ProfileEmailsLookupOptions.html +5 -5
- package/docs/api/interfaces/ProfilePosition.html +2 -2
- package/docs/api/interfaces/ProfileSkill.html +2 -2
- package/docs/api/interfaces/ProviderResult.html +6 -6
- package/docs/api/interfaces/ProvidersConfig.html +6 -9
- package/docs/api/interfaces/RequestHistoryEntry.html +2 -2
- package/docs/api/interfaces/SalesLeadSearchResult.html +2 -2
- package/docs/api/interfaces/SalesNavigatorContactInfo.html +2 -2
- package/docs/api/interfaces/SalesNavigatorPosition.html +2 -2
- package/docs/api/interfaces/SalesNavigatorProfile.html +2 -2
- package/docs/api/interfaces/SalesNavigatorProfileFull.html +4 -4
- package/docs/api/interfaces/SearchSalesResult.html +2 -2
- package/docs/api/interfaces/SmartLeadAuthConfig.html +4 -4
- package/docs/api/interfaces/SmartLeadCredentials.html +2 -2
- package/docs/api/interfaces/SmartLeadLoginResponse.html +2 -2
- package/docs/api/interfaces/SmartLeadUser.html +2 -2
- package/docs/api/interfaces/SmartProspectConfig.html +8 -8
- package/docs/api/interfaces/SmartProspectContact.html +2 -2
- package/docs/api/interfaces/SmartProspectSearchFilters.html +21 -21
- package/docs/api/interfaces/TypeaheadItem.html +2 -2
- package/docs/api/interfaces/TypeaheadResult.html +2 -2
- package/docs/api/interfaces/VerificationResult.html +9 -9
- package/docs/api/types/CostCallback.html +2 -2
- package/docs/api/types/Geo.html +2 -2
- package/docs/api/types/LddApiResponse.html +1 -1
- package/docs/api/types/ProviderFunc.html +2 -2
- package/docs/api/types/ProviderName.html +2 -2
- package/docs/api/types/SalesSearchFilters.html +2 -2
- package/docs/api/types/TypeaheadType.html +1 -1
- package/docs/api/variables/COMPANY_SIZE_OPTIONS.html +1 -1
- package/docs/api/variables/DEFAULT_PROVIDER_ORDER.html +3 -4
- package/docs/api/variables/DISPOSABLE_DOMAINS.html +2 -2
- package/docs/api/variables/FUNCTION_OPTIONS.html +1 -1
- package/docs/api/variables/INDUSTRY_OPTIONS.html +1 -1
- package/docs/api/variables/LANGUAGE_OPTIONS.html +1 -1
- package/docs/api/variables/PERSONAL_DOMAINS.html +2 -2
- package/docs/api/variables/PROVIDER_COSTS.html +3 -5
- package/docs/api/variables/REGION_OPTIONS.html +1 -1
- package/docs/api/variables/SENIORITY_OPTIONS.html +2 -2
- package/docs/api/variables/YEARS_OPTIONS.html +1 -1
- package/package.json +1 -1
- package/dist/enrichment/providers/apollo.d.ts +0 -11
- package/dist/enrichment/providers/apollo.js +0 -181
- package/dist/enrichment/providers/bouncer.d.ts +0 -67
- package/dist/enrichment/providers/bouncer.js +0 -231
- package/dist/enrichment/providers/dropcontact.d.ts +0 -22
- package/dist/enrichment/providers/dropcontact.js +0 -206
- package/docs/api/interfaces/DropcontactConfig.html +0 -3
|
@@ -20,18 +20,16 @@ const validation_1 = require("./utils/validation");
|
|
|
20
20
|
* - ldd: FREE (subscription-based)
|
|
21
21
|
* - smartprospect: FREE (included in SmartLead subscription)
|
|
22
22
|
* - construct: FREE (pattern guessing + MX check)
|
|
23
|
-
* -
|
|
23
|
+
* - bounceban: FREE single / $0.003 bulk (catch-all specialist)
|
|
24
|
+
* - hunter: $0.015/email (Growth tier)
|
|
24
25
|
* - snovio: $0.02/email (email finding + verification)
|
|
25
|
-
* - hunter: $0.005/email
|
|
26
|
-
* - dropcontact: $0.01/email (not in default order)
|
|
27
26
|
*/
|
|
28
27
|
const _PROVIDER_COSTS = {
|
|
29
28
|
construct: 0,
|
|
30
29
|
ldd: 0,
|
|
31
30
|
smartprospect: 0,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
bouncer: 0.006,
|
|
31
|
+
bounceban: 0.003,
|
|
32
|
+
hunter: 0.015,
|
|
35
33
|
snovio: 0.02,
|
|
36
34
|
};
|
|
37
35
|
/**
|
|
@@ -49,9 +47,9 @@ function normalizeProviderResult(providerName, raw) {
|
|
|
49
47
|
};
|
|
50
48
|
}
|
|
51
49
|
const email = String(raw.email);
|
|
52
|
-
const confidence = typeof raw.score ===
|
|
50
|
+
const confidence = typeof raw.score === "number"
|
|
53
51
|
? raw.score
|
|
54
|
-
: typeof raw.confidence ===
|
|
52
|
+
: typeof raw.confidence === "number"
|
|
55
53
|
? raw.confidence
|
|
56
54
|
: undefined;
|
|
57
55
|
const verified = Boolean(raw.verified);
|
|
@@ -67,7 +65,8 @@ function normalizeProviderResult(providerName, raw) {
|
|
|
67
65
|
* Get provider name from function
|
|
68
66
|
*/
|
|
69
67
|
function getProviderName(provider, index) {
|
|
70
|
-
return provider.__name ??
|
|
68
|
+
return (provider.__name ??
|
|
69
|
+
`provider-${index}`);
|
|
71
70
|
}
|
|
72
71
|
/**
|
|
73
72
|
* Get provider cost
|
|
@@ -79,7 +78,7 @@ function getProviderCost(providerName) {
|
|
|
79
78
|
* Check if a provider result is a multi-result (returns multiple emails)
|
|
80
79
|
*/
|
|
81
80
|
function isMultiResult(result) {
|
|
82
|
-
return result !== null &&
|
|
81
|
+
return result !== null && "emails" in result && Array.isArray(result.emails);
|
|
83
82
|
}
|
|
84
83
|
/**
|
|
85
84
|
* Enrich a single candidate with business email
|
|
@@ -87,8 +86,10 @@ function isMultiResult(result) {
|
|
|
87
86
|
async function enrichBusinessEmail(candidate, options) {
|
|
88
87
|
const { providers, maxCostPerEmail = Infinity, confidenceThreshold = 0, retryMs = 200, onCost, logger, } = options;
|
|
89
88
|
// Track remaining budget
|
|
90
|
-
let remaining = Number.isFinite(maxCostPerEmail) && maxCostPerEmail >= 0
|
|
91
|
-
|
|
89
|
+
let remaining = Number.isFinite(maxCostPerEmail) && maxCostPerEmail >= 0
|
|
90
|
+
? maxCostPerEmail
|
|
91
|
+
: Infinity;
|
|
92
|
+
logger?.debug?.("enrichment.start", {
|
|
92
93
|
providers: providers.length,
|
|
93
94
|
confidenceThreshold,
|
|
94
95
|
maxCostPerEmail,
|
|
@@ -99,7 +100,7 @@ async function enrichBusinessEmail(candidate, options) {
|
|
|
99
100
|
const stepCost = getProviderCost(providerName);
|
|
100
101
|
// Skip if cost exceeds remaining budget
|
|
101
102
|
if (stepCost > 0 && remaining < stepCost) {
|
|
102
|
-
logger?.debug?.(
|
|
103
|
+
logger?.debug?.("enrichment.skip_budget", {
|
|
103
104
|
provider: providerName,
|
|
104
105
|
cost: stepCost,
|
|
105
106
|
remaining,
|
|
@@ -108,15 +109,15 @@ async function enrichBusinessEmail(candidate, options) {
|
|
|
108
109
|
}
|
|
109
110
|
let raw = null;
|
|
110
111
|
try {
|
|
111
|
-
logger?.debug?.(
|
|
112
|
+
logger?.debug?.("enrichment.provider.start", { provider: providerName });
|
|
112
113
|
raw = await provider(candidate);
|
|
113
|
-
logger?.debug?.(
|
|
114
|
+
logger?.debug?.("enrichment.provider.done", {
|
|
114
115
|
provider: providerName,
|
|
115
116
|
hasResult: !!(raw && (isMultiResult(raw) ? raw.emails.length > 0 : raw.email)),
|
|
116
117
|
});
|
|
117
118
|
}
|
|
118
119
|
catch (error) {
|
|
119
|
-
logger?.warn?.(
|
|
120
|
+
logger?.warn?.("enrichment.provider.error", {
|
|
120
121
|
provider: providerName,
|
|
121
122
|
error: error instanceof Error ? error.message : String(error),
|
|
122
123
|
});
|
|
@@ -125,7 +126,7 @@ async function enrichBusinessEmail(candidate, options) {
|
|
|
125
126
|
await new Promise((r) => setTimeout(r, retryMs));
|
|
126
127
|
try {
|
|
127
128
|
raw = await provider(candidate);
|
|
128
|
-
logger?.debug?.(
|
|
129
|
+
logger?.debug?.("enrichment.provider.retry.done", {
|
|
129
130
|
provider: providerName,
|
|
130
131
|
hasResult: !!(raw && (isMultiResult(raw) ? raw.emails.length > 0 : raw.email)),
|
|
131
132
|
});
|
|
@@ -160,7 +161,7 @@ async function enrichBusinessEmail(candidate, options) {
|
|
|
160
161
|
// Ignore cost callback errors
|
|
161
162
|
}
|
|
162
163
|
}
|
|
163
|
-
logger?.debug?.(
|
|
164
|
+
logger?.debug?.("enrichment.cost.debit", {
|
|
164
165
|
provider: providerName,
|
|
165
166
|
cost: stepCost,
|
|
166
167
|
remaining,
|
|
@@ -174,7 +175,7 @@ async function enrichBusinessEmail(candidate, options) {
|
|
|
174
175
|
}
|
|
175
176
|
// Filter out personal emails
|
|
176
177
|
if ((0, personal_domains_1.isPersonalEmail)(normalized.business_email)) {
|
|
177
|
-
logger?.debug?.(
|
|
178
|
+
logger?.debug?.("enrichment.skip_personal", {
|
|
178
179
|
provider: providerName,
|
|
179
180
|
email: normalized.business_email,
|
|
180
181
|
});
|
|
@@ -182,7 +183,7 @@ async function enrichBusinessEmail(candidate, options) {
|
|
|
182
183
|
}
|
|
183
184
|
// Filter out disposable emails
|
|
184
185
|
if ((0, disposable_domains_1.isDisposableEmail)(normalized.business_email)) {
|
|
185
|
-
logger?.debug?.(
|
|
186
|
+
logger?.debug?.("enrichment.skip_disposable", {
|
|
186
187
|
provider: providerName,
|
|
187
188
|
email: normalized.business_email,
|
|
188
189
|
});
|
|
@@ -190,8 +191,10 @@ async function enrichBusinessEmail(candidate, options) {
|
|
|
190
191
|
}
|
|
191
192
|
// Check confidence threshold
|
|
192
193
|
const score = normalized.business_email_confidence;
|
|
193
|
-
if (confidenceThreshold > 0 &&
|
|
194
|
-
|
|
194
|
+
if (confidenceThreshold > 0 &&
|
|
195
|
+
score !== undefined &&
|
|
196
|
+
score < confidenceThreshold) {
|
|
197
|
+
logger?.debug?.("enrichment.skip_low_confidence", {
|
|
195
198
|
provider: providerName,
|
|
196
199
|
score,
|
|
197
200
|
threshold: confidenceThreshold,
|
|
@@ -200,14 +203,14 @@ async function enrichBusinessEmail(candidate, options) {
|
|
|
200
203
|
}
|
|
201
204
|
// Check if verified
|
|
202
205
|
if (!normalized.business_email_verified) {
|
|
203
|
-
logger?.debug?.(
|
|
206
|
+
logger?.debug?.("enrichment.skip_unverified", {
|
|
204
207
|
provider: providerName,
|
|
205
208
|
email: normalized.business_email,
|
|
206
209
|
});
|
|
207
210
|
continue;
|
|
208
211
|
}
|
|
209
212
|
// Success!
|
|
210
|
-
logger?.info?.(
|
|
213
|
+
logger?.info?.("enrichment.success", {
|
|
211
214
|
provider: providerName,
|
|
212
215
|
email: normalized.business_email,
|
|
213
216
|
confidence: normalized.business_email_confidence,
|
|
@@ -215,7 +218,7 @@ async function enrichBusinessEmail(candidate, options) {
|
|
|
215
218
|
return normalized;
|
|
216
219
|
}
|
|
217
220
|
// No provider found a valid email
|
|
218
|
-
logger?.debug?.(
|
|
221
|
+
logger?.debug?.("enrichment.not_found", {
|
|
219
222
|
candidateHasName: !!(candidate.firstName || candidate.name),
|
|
220
223
|
candidateHasDomain: !!(candidate.domain || candidate.company),
|
|
221
224
|
});
|
|
@@ -224,7 +227,7 @@ async function enrichBusinessEmail(candidate, options) {
|
|
|
224
227
|
business_email_source: null,
|
|
225
228
|
business_email_verified: false,
|
|
226
229
|
last_checked_at: new Date().toISOString(),
|
|
227
|
-
status:
|
|
230
|
+
status: "not_found",
|
|
228
231
|
};
|
|
229
232
|
}
|
|
230
233
|
/**
|
|
@@ -241,7 +244,7 @@ async function enrichBatch(candidates, options) {
|
|
|
241
244
|
// Combine results with candidates (handle both fulfilled and rejected)
|
|
242
245
|
batchResults.forEach((result, idx) => {
|
|
243
246
|
const candidate = chunk[idx];
|
|
244
|
-
if (result.status ===
|
|
247
|
+
if (result.status === "fulfilled") {
|
|
245
248
|
results.push({ candidate, ...result.value });
|
|
246
249
|
}
|
|
247
250
|
else {
|
|
@@ -252,7 +255,7 @@ async function enrichBatch(candidates, options) {
|
|
|
252
255
|
business_email_source: null,
|
|
253
256
|
business_email_verified: false,
|
|
254
257
|
last_checked_at: new Date().toISOString(),
|
|
255
|
-
status: `error: ${result.reason instanceof Error ? result.reason.message :
|
|
258
|
+
status: `error: ${result.reason instanceof Error ? result.reason.message : "unknown"}`,
|
|
256
259
|
});
|
|
257
260
|
}
|
|
258
261
|
});
|
|
@@ -271,16 +274,16 @@ async function enrichBatch(candidates, options) {
|
|
|
271
274
|
*/
|
|
272
275
|
function classifyEmailType(email) {
|
|
273
276
|
if ((0, disposable_domains_1.isDisposableEmail)(email)) {
|
|
274
|
-
return
|
|
277
|
+
return "disposable";
|
|
275
278
|
}
|
|
276
279
|
if ((0, personal_domains_1.isPersonalEmail)(email)) {
|
|
277
|
-
return
|
|
280
|
+
return "personal";
|
|
278
281
|
}
|
|
279
282
|
if ((0, validation_1.isRoleAccount)(email)) {
|
|
280
|
-
return
|
|
283
|
+
return "role";
|
|
281
284
|
}
|
|
282
285
|
// If it's not personal, disposable, or role, it's likely business
|
|
283
|
-
return
|
|
286
|
+
return "business";
|
|
284
287
|
}
|
|
285
288
|
/**
|
|
286
289
|
* Enrich a candidate and collect ALL emails from ALL providers
|
|
@@ -296,10 +299,12 @@ async function enrichAllEmails(candidate, options) {
|
|
|
296
299
|
const providersQueried = [];
|
|
297
300
|
let totalCost = 0;
|
|
298
301
|
// Track remaining budget
|
|
299
|
-
let remaining = Number.isFinite(maxCostPerEmail) && maxCostPerEmail >= 0
|
|
302
|
+
let remaining = Number.isFinite(maxCostPerEmail) && maxCostPerEmail >= 0
|
|
303
|
+
? maxCostPerEmail
|
|
304
|
+
: Infinity;
|
|
300
305
|
// Track seen emails to avoid duplicates
|
|
301
306
|
const seenEmails = new Set();
|
|
302
|
-
logger?.debug?.(
|
|
307
|
+
logger?.debug?.("enrichment.all.start", {
|
|
303
308
|
providers: providers.length,
|
|
304
309
|
maxCostPerEmail,
|
|
305
310
|
});
|
|
@@ -309,7 +314,7 @@ async function enrichAllEmails(candidate, options) {
|
|
|
309
314
|
const stepCost = getProviderCost(providerName);
|
|
310
315
|
// Skip if cost exceeds remaining budget
|
|
311
316
|
if (stepCost > 0 && remaining < stepCost) {
|
|
312
|
-
logger?.debug?.(
|
|
317
|
+
logger?.debug?.("enrichment.all.skip_budget", {
|
|
313
318
|
provider: providerName,
|
|
314
319
|
cost: stepCost,
|
|
315
320
|
remaining,
|
|
@@ -319,16 +324,18 @@ async function enrichAllEmails(candidate, options) {
|
|
|
319
324
|
providersQueried.push(providerName);
|
|
320
325
|
let raw = null;
|
|
321
326
|
try {
|
|
322
|
-
logger?.debug?.(
|
|
327
|
+
logger?.debug?.("enrichment.all.provider.start", {
|
|
328
|
+
provider: providerName,
|
|
329
|
+
});
|
|
323
330
|
raw = await provider(candidate);
|
|
324
|
-
logger?.debug?.(
|
|
331
|
+
logger?.debug?.("enrichment.all.provider.done", {
|
|
325
332
|
provider: providerName,
|
|
326
333
|
hasResult: !!raw,
|
|
327
334
|
isMulti: isMultiResult(raw),
|
|
328
335
|
});
|
|
329
336
|
}
|
|
330
337
|
catch (error) {
|
|
331
|
-
logger?.warn?.(
|
|
338
|
+
logger?.warn?.("enrichment.all.provider.error", {
|
|
332
339
|
provider: providerName,
|
|
333
340
|
error: error instanceof Error ? error.message : String(error),
|
|
334
341
|
});
|
|
@@ -337,7 +344,7 @@ async function enrichAllEmails(candidate, options) {
|
|
|
337
344
|
await new Promise((r) => setTimeout(r, retryMs));
|
|
338
345
|
try {
|
|
339
346
|
raw = await provider(candidate);
|
|
340
|
-
logger?.debug?.(
|
|
347
|
+
logger?.debug?.("enrichment.all.provider.retry.done", {
|
|
341
348
|
provider: providerName,
|
|
342
349
|
hasResult: !!raw,
|
|
343
350
|
});
|
|
@@ -359,7 +366,7 @@ async function enrichAllEmails(candidate, options) {
|
|
|
359
366
|
// Ignore cost callback errors
|
|
360
367
|
}
|
|
361
368
|
}
|
|
362
|
-
logger?.debug?.(
|
|
369
|
+
logger?.debug?.("enrichment.all.cost.debit", {
|
|
363
370
|
provider: providerName,
|
|
364
371
|
cost: stepCost,
|
|
365
372
|
remaining,
|
|
@@ -393,9 +400,9 @@ async function enrichAllEmails(candidate, options) {
|
|
|
393
400
|
const emailLower = raw.email.toLowerCase();
|
|
394
401
|
if (!seenEmails.has(emailLower)) {
|
|
395
402
|
seenEmails.add(emailLower);
|
|
396
|
-
const confidence = typeof raw.score ===
|
|
403
|
+
const confidence = typeof raw.score === "number"
|
|
397
404
|
? raw.score
|
|
398
|
-
: typeof raw.confidence ===
|
|
405
|
+
: typeof raw.confidence === "number"
|
|
399
406
|
? raw.confidence
|
|
400
407
|
: 50;
|
|
401
408
|
allEmails.push({
|
|
@@ -425,7 +432,7 @@ async function enrichAllEmails(candidate, options) {
|
|
|
425
432
|
// Then by type priority
|
|
426
433
|
return typeOrder[a.type] - typeOrder[b.type];
|
|
427
434
|
});
|
|
428
|
-
logger?.info?.(
|
|
435
|
+
logger?.info?.("enrichment.all.complete", {
|
|
429
436
|
emailsFound: allEmails.length,
|
|
430
437
|
providersQueried: providersQueried.length,
|
|
431
438
|
totalCost,
|
|
@@ -451,7 +458,7 @@ async function enrichAllBatch(candidates, options) {
|
|
|
451
458
|
const batchResults = await Promise.allSettled(chunk.map((candidate) => enrichAllEmails(candidate, options)));
|
|
452
459
|
// Handle both fulfilled and rejected results
|
|
453
460
|
batchResults.forEach((result, idx) => {
|
|
454
|
-
if (result.status ===
|
|
461
|
+
if (result.status === "fulfilled") {
|
|
455
462
|
results.push(result.value);
|
|
456
463
|
}
|
|
457
464
|
else {
|
|
@@ -99,14 +99,16 @@ function extractEmailsToVerify(candidate) {
|
|
|
99
99
|
*/
|
|
100
100
|
async function verifyEmailStandard(email, apiKey, apiUrl, timeoutMs, useDeepVerify) {
|
|
101
101
|
const params = new URLSearchParams({
|
|
102
|
-
api_key: apiKey,
|
|
103
102
|
email: email,
|
|
104
103
|
});
|
|
105
104
|
if (useDeepVerify) {
|
|
106
105
|
params.set("mode", "deepverify");
|
|
107
106
|
}
|
|
107
|
+
const url = `${apiUrl}/v1/verify/single?${params.toString()}`;
|
|
108
108
|
try {
|
|
109
|
-
const response = await (0, http_retry_1.getWithRetry)(
|
|
109
|
+
const response = await (0, http_retry_1.getWithRetry)(url, {
|
|
110
|
+
Authorization: apiKey,
|
|
111
|
+
}, {
|
|
110
112
|
retries: 1,
|
|
111
113
|
backoffMs: 500,
|
|
112
114
|
timeoutMs,
|
|
@@ -114,7 +116,8 @@ async function verifyEmailStandard(email, apiKey, apiUrl, timeoutMs, useDeepVeri
|
|
|
114
116
|
});
|
|
115
117
|
return response;
|
|
116
118
|
}
|
|
117
|
-
catch {
|
|
119
|
+
catch (err) {
|
|
120
|
+
console.error("[BounceBan] Standard verification failed:", err instanceof Error ? err.message : err);
|
|
118
121
|
return null;
|
|
119
122
|
}
|
|
120
123
|
}
|
|
@@ -128,15 +131,17 @@ async function verifyEmailStandard(email, apiKey, apiUrl, timeoutMs, useDeepVeri
|
|
|
128
131
|
*/
|
|
129
132
|
async function verifyEmailWaterfall(email, apiKey, waterfallApiUrl, timeoutMs, useDeepVerify) {
|
|
130
133
|
const params = new URLSearchParams({
|
|
131
|
-
api_key: apiKey,
|
|
132
134
|
email: email,
|
|
133
135
|
timeout: String(Math.floor(timeoutMs / 1000)), // Convert to seconds
|
|
134
136
|
});
|
|
135
137
|
if (useDeepVerify) {
|
|
136
138
|
params.set("mode", "deepverify");
|
|
137
139
|
}
|
|
140
|
+
const url = `${waterfallApiUrl}/v1/verify/single?${params.toString()}`;
|
|
138
141
|
try {
|
|
139
|
-
const response = await (0, http_retry_1.getWithRetry)(
|
|
142
|
+
const response = await (0, http_retry_1.getWithRetry)(url, {
|
|
143
|
+
Authorization: apiKey,
|
|
144
|
+
}, {
|
|
140
145
|
retries: 2, // Waterfall allows free retries within 30 mins
|
|
141
146
|
backoffMs: 5000, // Wait 5s between retries as recommended
|
|
142
147
|
timeoutMs: timeoutMs + 10000, // Add buffer for network
|
|
@@ -144,7 +149,8 @@ async function verifyEmailWaterfall(email, apiKey, waterfallApiUrl, timeoutMs, u
|
|
|
144
149
|
});
|
|
145
150
|
return response;
|
|
146
151
|
}
|
|
147
|
-
catch {
|
|
152
|
+
catch (err) {
|
|
153
|
+
console.error("[BounceBan] Waterfall verification failed:", err instanceof Error ? err.message : err);
|
|
148
154
|
return null;
|
|
149
155
|
}
|
|
150
156
|
}
|
|
@@ -256,9 +262,9 @@ async function submitBulkVerification(emails, apiKey, apiUrl, useDeepVerify) {
|
|
|
256
262
|
method: "POST",
|
|
257
263
|
headers: {
|
|
258
264
|
"Content-Type": "application/json",
|
|
265
|
+
Authorization: apiKey,
|
|
259
266
|
},
|
|
260
267
|
body: JSON.stringify({
|
|
261
|
-
api_key: apiKey,
|
|
262
268
|
emails: emails,
|
|
263
269
|
mode: useDeepVerify ? "deepverify" : "regular",
|
|
264
270
|
}),
|
|
@@ -279,10 +285,13 @@ async function submitBulkVerification(emails, apiKey, apiUrl, useDeepVerify) {
|
|
|
279
285
|
async function checkBulkStatus(taskId, apiKey, apiUrl) {
|
|
280
286
|
try {
|
|
281
287
|
const params = new URLSearchParams({
|
|
282
|
-
api_key: apiKey,
|
|
283
288
|
id: taskId,
|
|
284
289
|
});
|
|
285
|
-
const response = await fetch(`${apiUrl}/v1/verify/bulk/status?${params.toString()}
|
|
290
|
+
const response = await fetch(`${apiUrl}/v1/verify/bulk/status?${params.toString()}`, {
|
|
291
|
+
headers: {
|
|
292
|
+
Authorization: apiKey,
|
|
293
|
+
},
|
|
294
|
+
});
|
|
286
295
|
if (!response.ok) {
|
|
287
296
|
return null;
|
|
288
297
|
}
|
|
@@ -301,9 +310,9 @@ async function fetchBulkResults(taskId, apiKey, apiUrl, page = 1, perPage = 100)
|
|
|
301
310
|
method: "POST",
|
|
302
311
|
headers: {
|
|
303
312
|
"Content-Type": "application/json",
|
|
313
|
+
Authorization: apiKey,
|
|
304
314
|
},
|
|
305
315
|
body: JSON.stringify({
|
|
306
|
-
api_key: apiKey,
|
|
307
316
|
id: taskId,
|
|
308
317
|
page: page,
|
|
309
318
|
per_page: perPage,
|
|
@@ -68,104 +68,112 @@ function createConstructProvider(config) {
|
|
|
68
68
|
const timeoutMs = config?.timeoutMs ?? 5000;
|
|
69
69
|
const smtpVerifyDelayMs = config?.smtpVerifyDelayMs ?? 2000; // Delay between SMTP checks
|
|
70
70
|
async function fetchEmail(candidate) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const result = results[0];
|
|
113
|
-
if (result.exists === true) {
|
|
114
|
-
// Email confirmed to exist!
|
|
115
|
-
attemptedPatterns.push({ email, status: "exists" });
|
|
116
|
-
validEmails.push({
|
|
117
|
-
email: result.email,
|
|
118
|
-
verified: true,
|
|
119
|
-
confidence: 95, // High confidence - SMTP verified
|
|
120
|
-
isCatchAll: false,
|
|
121
|
-
metadata: {
|
|
122
|
-
pattern: result.email.split("@")[0],
|
|
123
|
-
mxRecords: catchAllResult.mxRecords,
|
|
124
|
-
smtpVerified: true,
|
|
125
|
-
attemptedPatterns, // Include what was tried
|
|
126
|
-
},
|
|
71
|
+
try {
|
|
72
|
+
const { firstName: first, lastName: last } = (0, candidate_parser_1.extractName)(candidate);
|
|
73
|
+
const { domain } = (0, candidate_parser_1.extractCompany)(candidate);
|
|
74
|
+
// Skip if missing required fields
|
|
75
|
+
if (!first || !domain) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
// Skip personal domains
|
|
79
|
+
if ((0, personal_domains_1.isPersonalDomain)(domain)) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const candidates = buildCandidates({ first, last, domain });
|
|
83
|
+
const max = Math.min(candidates.length, maxAttempts);
|
|
84
|
+
// First, check if domain is catch-all
|
|
85
|
+
const catchAllResult = await (0, mx_1.checkDomainCatchAll)(domain, {
|
|
86
|
+
timeoutMs: 10000,
|
|
87
|
+
});
|
|
88
|
+
const isCatchAll = catchAllResult.isCatchAll;
|
|
89
|
+
// Collect ALL valid email patterns (not just first match)
|
|
90
|
+
const validEmails = [];
|
|
91
|
+
// Track all attempted patterns for debugging
|
|
92
|
+
const attemptedPatterns = [];
|
|
93
|
+
// If NOT catch-all, we can verify each email via SMTP
|
|
94
|
+
if (isCatchAll === false) {
|
|
95
|
+
// Verify emails one by one, stop when we find a valid one
|
|
96
|
+
const emailsToVerify = candidates.slice(0, max);
|
|
97
|
+
for (let i = 0; i < emailsToVerify.length; i++) {
|
|
98
|
+
const email = emailsToVerify[i];
|
|
99
|
+
// If we already found a valid email, skip the rest
|
|
100
|
+
if (validEmails.length > 0) {
|
|
101
|
+
attemptedPatterns.push({ email, status: "skipped" });
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
// Add delay between checks (except first one)
|
|
105
|
+
if (i > 0) {
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, smtpVerifyDelayMs));
|
|
107
|
+
}
|
|
108
|
+
// Verify single email
|
|
109
|
+
const results = await (0, mx_1.verifyEmailsExist)([email], {
|
|
110
|
+
delayMs: 0,
|
|
111
|
+
timeoutMs,
|
|
127
112
|
});
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
113
|
+
const result = results[0];
|
|
114
|
+
if (result.exists === true) {
|
|
115
|
+
// Email confirmed to exist!
|
|
116
|
+
attemptedPatterns.push({ email, status: "exists" });
|
|
117
|
+
validEmails.push({
|
|
118
|
+
email: result.email,
|
|
119
|
+
verified: true,
|
|
120
|
+
confidence: 80, // Good confidence - SMTP verified pattern guess
|
|
121
|
+
// Note: 80% is appropriate for pattern guessing with SMTP verification
|
|
122
|
+
// Higher-priority sources like SmartProspect (90%) should rank above
|
|
123
|
+
isCatchAll: false,
|
|
124
|
+
metadata: {
|
|
125
|
+
pattern: result.email.split("@")[0],
|
|
126
|
+
mxRecords: catchAllResult.mxRecords,
|
|
127
|
+
smtpVerified: true,
|
|
128
|
+
attemptedPatterns, // Include what was tried
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
// Found one! Stop checking more
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
else if (result.exists === false) {
|
|
135
|
+
attemptedPatterns.push({ email, status: "not_found" });
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
attemptedPatterns.push({ email, status: "unknown" });
|
|
139
|
+
}
|
|
133
140
|
}
|
|
134
|
-
|
|
135
|
-
|
|
141
|
+
// If no valid email found, include attempted patterns in metadata
|
|
142
|
+
if (validEmails.length === 0 && attemptedPatterns.length > 0) {
|
|
143
|
+
// Return null but could add metadata about attempts
|
|
136
144
|
}
|
|
137
145
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
mxRecords: verification.mxRecords,
|
|
157
|
-
smtpVerified: false,
|
|
158
|
-
},
|
|
159
|
-
});
|
|
146
|
+
else {
|
|
147
|
+
// Catch-all or unknown - fall back to MX verification only
|
|
148
|
+
for (let i = 0; i < max; i++) {
|
|
149
|
+
const email = candidates[i];
|
|
150
|
+
const verification = await (0, mx_1.verifyEmailMx)(email, { timeoutMs });
|
|
151
|
+
if (verification.valid === true && verification.confidence >= 50) {
|
|
152
|
+
validEmails.push({
|
|
153
|
+
email,
|
|
154
|
+
verified: true,
|
|
155
|
+
confidence: verification.confidence,
|
|
156
|
+
isCatchAll: isCatchAll ?? undefined,
|
|
157
|
+
metadata: {
|
|
158
|
+
pattern: email.split("@")[0],
|
|
159
|
+
mxRecords: verification.mxRecords,
|
|
160
|
+
smtpVerified: false,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
160
164
|
}
|
|
161
165
|
}
|
|
166
|
+
if (validEmails.length === 0) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
// Sort by confidence
|
|
170
|
+
validEmails.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0));
|
|
171
|
+
return { emails: validEmails };
|
|
162
172
|
}
|
|
163
|
-
|
|
173
|
+
catch (err) {
|
|
174
|
+
console.error("[Construct] Email pattern generation failed:", err instanceof Error ? err.message : err);
|
|
164
175
|
return null;
|
|
165
176
|
}
|
|
166
|
-
// Sort by confidence
|
|
167
|
-
validEmails.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0));
|
|
168
|
-
return { emails: validEmails };
|
|
169
177
|
}
|
|
170
178
|
// Mark provider name for orchestrator
|
|
171
179
|
fetchEmail.__name = "construct";
|
|
@@ -98,8 +98,8 @@ function createCosiallProvider(config) {
|
|
|
98
98
|
}));
|
|
99
99
|
return { emails };
|
|
100
100
|
}
|
|
101
|
-
catch {
|
|
102
|
-
|
|
101
|
+
catch (err) {
|
|
102
|
+
console.error("[Cosiall] Profile email lookup failed:", err instanceof Error ? err.message : err);
|
|
103
103
|
return null;
|
|
104
104
|
}
|
|
105
105
|
}
|
|
@@ -7,7 +7,5 @@ export { createSmartProspectProvider } from "./smartprospect";
|
|
|
7
7
|
export { createCosiallProvider } from "./cosiall";
|
|
8
8
|
export { createTryKittProvider, findEmailWithTryKitt, verifyEmailWithTryKitt, } from "./trykitt";
|
|
9
9
|
export { createHunterProvider } from "./hunter";
|
|
10
|
-
export { createDropcontactProvider } from "./dropcontact";
|
|
11
|
-
export { createBouncerProvider, verifyEmailWithBouncer, checkCatchAllDomain, verifyEmailsBatch, } from "./bouncer";
|
|
12
10
|
export { createBounceBanProvider, verifyEmailWithBounceBan, verifyEmailsBatchWithBounceBan, checkCatchAllWithBounceBan, } from "./bounceban";
|
|
13
11
|
export { createSnovioProvider, findEmailsWithSnovio, verifyEmailWithSnovio, clearSnovioTokenCache, } from "./snovio";
|