linkedin-secret-sauce 0.5.1 → 0.7.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/dist/config.js +1 -1
- package/dist/cosiall-client.js +2 -1
- package/dist/enrichment/auth/index.d.ts +1 -1
- package/dist/enrichment/auth/index.js +6 -1
- package/dist/enrichment/auth/smartlead-auth.d.ts +32 -0
- package/dist/enrichment/auth/smartlead-auth.js +163 -0
- package/dist/enrichment/index.d.ts +5 -2
- package/dist/enrichment/index.js +46 -1
- package/dist/enrichment/matching.d.ts +241 -0
- package/dist/enrichment/matching.js +626 -0
- package/dist/enrichment/orchestrator.d.ts +13 -1
- package/dist/enrichment/orchestrator.js +222 -5
- package/dist/enrichment/providers/apollo.d.ts +2 -2
- package/dist/enrichment/providers/apollo.js +59 -14
- package/dist/enrichment/providers/construct.d.ts +2 -2
- package/dist/enrichment/providers/construct.js +16 -4
- package/dist/enrichment/providers/hunter.d.ts +2 -2
- package/dist/enrichment/providers/hunter.js +48 -22
- package/dist/enrichment/providers/ldd.d.ts +20 -2
- package/dist/enrichment/providers/ldd.js +122 -16
- package/dist/enrichment/providers/smartprospect.d.ts +64 -2
- package/dist/enrichment/providers/smartprospect.js +605 -38
- package/dist/enrichment/types.d.ts +167 -11
- package/dist/enrichment/types.js +50 -1
- package/dist/http-client.js +1 -1
- package/dist/linkedin-api.d.ts +1 -2
- package/dist/parsers/profile-parser.js +4 -2
- package/dist/utils/linkedin-config.js +1 -1
- package/dist/utils/search-encoder.js +8 -9
- package/package.json +22 -15
|
@@ -8,12 +8,15 @@
|
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.enrichBusinessEmail = enrichBusinessEmail;
|
|
10
10
|
exports.enrichBatch = enrichBatch;
|
|
11
|
+
exports.enrichAllEmails = enrichAllEmails;
|
|
12
|
+
exports.enrichAllBatch = enrichAllBatch;
|
|
11
13
|
const personal_domains_1 = require("./utils/personal-domains");
|
|
12
14
|
const disposable_domains_1 = require("./utils/disposable-domains");
|
|
15
|
+
const validation_1 = require("./utils/validation");
|
|
13
16
|
/**
|
|
14
17
|
* Default provider costs in USD per lookup
|
|
15
18
|
*/
|
|
16
|
-
const
|
|
19
|
+
const _PROVIDER_COSTS = {
|
|
17
20
|
construct: 0,
|
|
18
21
|
ldd: 0,
|
|
19
22
|
smartprospect: 0.01,
|
|
@@ -60,7 +63,13 @@ function getProviderName(provider, index) {
|
|
|
60
63
|
* Get provider cost
|
|
61
64
|
*/
|
|
62
65
|
function getProviderCost(providerName) {
|
|
63
|
-
return
|
|
66
|
+
return _PROVIDER_COSTS[providerName] ?? 0;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Check if a provider result is a multi-result (returns multiple emails)
|
|
70
|
+
*/
|
|
71
|
+
function isMultiResult(result) {
|
|
72
|
+
return result !== null && 'emails' in result && Array.isArray(result.emails);
|
|
64
73
|
}
|
|
65
74
|
/**
|
|
66
75
|
* Enrich a single candidate with business email
|
|
@@ -93,7 +102,7 @@ async function enrichBusinessEmail(candidate, options) {
|
|
|
93
102
|
raw = await provider(candidate);
|
|
94
103
|
logger?.debug?.('enrichment.provider.done', {
|
|
95
104
|
provider: providerName,
|
|
96
|
-
hasResult: !!raw
|
|
105
|
+
hasResult: !!(raw && (isMultiResult(raw) ? raw.emails.length > 0 : raw.email)),
|
|
97
106
|
});
|
|
98
107
|
}
|
|
99
108
|
catch (error) {
|
|
@@ -108,7 +117,7 @@ async function enrichBusinessEmail(candidate, options) {
|
|
|
108
117
|
raw = await provider(candidate);
|
|
109
118
|
logger?.debug?.('enrichment.provider.retry.done', {
|
|
110
119
|
provider: providerName,
|
|
111
|
-
hasResult: !!raw
|
|
120
|
+
hasResult: !!(raw && (isMultiResult(raw) ? raw.emails.length > 0 : raw.email)),
|
|
112
121
|
});
|
|
113
122
|
}
|
|
114
123
|
catch {
|
|
@@ -116,6 +125,20 @@ async function enrichBusinessEmail(candidate, options) {
|
|
|
116
125
|
}
|
|
117
126
|
}
|
|
118
127
|
}
|
|
128
|
+
// Convert multi-result to single result (take best match for backwards compatibility)
|
|
129
|
+
let singleResult = null;
|
|
130
|
+
if (isMultiResult(raw) && raw.emails.length > 0) {
|
|
131
|
+
// Take the first (highest confidence) email
|
|
132
|
+
const best = raw.emails[0];
|
|
133
|
+
singleResult = {
|
|
134
|
+
email: best.email,
|
|
135
|
+
verified: best.verified,
|
|
136
|
+
confidence: best.confidence,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
else if (raw && !isMultiResult(raw)) {
|
|
140
|
+
singleResult = raw;
|
|
141
|
+
}
|
|
119
142
|
// Deduct cost (even on failure - API was called)
|
|
120
143
|
if (stepCost > 0) {
|
|
121
144
|
remaining = Math.max(0, remaining - stepCost);
|
|
@@ -134,7 +157,7 @@ async function enrichBusinessEmail(candidate, options) {
|
|
|
134
157
|
});
|
|
135
158
|
}
|
|
136
159
|
// Normalize result
|
|
137
|
-
const normalized = normalizeProviderResult(providerName,
|
|
160
|
+
const normalized = normalizeProviderResult(providerName, singleResult);
|
|
138
161
|
// Skip if no email found
|
|
139
162
|
if (!normalized.business_email) {
|
|
140
163
|
continue;
|
|
@@ -216,3 +239,197 @@ async function enrichBatch(candidates, options) {
|
|
|
216
239
|
}
|
|
217
240
|
return results;
|
|
218
241
|
}
|
|
242
|
+
// =============================================================================
|
|
243
|
+
// Multi-Email Enrichment (Collect All)
|
|
244
|
+
// =============================================================================
|
|
245
|
+
/**
|
|
246
|
+
* Classify an email's type based on domain characteristics
|
|
247
|
+
*/
|
|
248
|
+
function classifyEmailType(email) {
|
|
249
|
+
if ((0, disposable_domains_1.isDisposableEmail)(email)) {
|
|
250
|
+
return 'disposable';
|
|
251
|
+
}
|
|
252
|
+
if ((0, personal_domains_1.isPersonalEmail)(email)) {
|
|
253
|
+
return 'personal';
|
|
254
|
+
}
|
|
255
|
+
if ((0, validation_1.isRoleAccount)(email)) {
|
|
256
|
+
return 'role';
|
|
257
|
+
}
|
|
258
|
+
// If it's not personal, disposable, or role, it's likely business
|
|
259
|
+
return 'business';
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Enrich a candidate and collect ALL emails from ALL providers
|
|
263
|
+
*
|
|
264
|
+
* Unlike enrichBusinessEmail which stops at the first valid match,
|
|
265
|
+
* this function queries all providers (respecting budget) and returns
|
|
266
|
+
* all found emails with their scores and classifications.
|
|
267
|
+
*/
|
|
268
|
+
async function enrichAllEmails(candidate, options) {
|
|
269
|
+
const { providers, maxCostPerEmail = Infinity, retryMs = 200, onCost, logger, } = options;
|
|
270
|
+
// Track results
|
|
271
|
+
const allEmails = [];
|
|
272
|
+
const providersQueried = [];
|
|
273
|
+
let totalCost = 0;
|
|
274
|
+
// Track remaining budget
|
|
275
|
+
let remaining = Number.isFinite(maxCostPerEmail) && maxCostPerEmail >= 0 ? maxCostPerEmail : Infinity;
|
|
276
|
+
// Track seen emails to avoid duplicates
|
|
277
|
+
const seenEmails = new Set();
|
|
278
|
+
logger?.debug?.('enrichment.all.start', {
|
|
279
|
+
providers: providers.length,
|
|
280
|
+
maxCostPerEmail,
|
|
281
|
+
});
|
|
282
|
+
for (let i = 0; i < providers.length; i++) {
|
|
283
|
+
const provider = providers[i];
|
|
284
|
+
const providerName = getProviderName(provider, i);
|
|
285
|
+
const stepCost = getProviderCost(providerName);
|
|
286
|
+
// Skip if cost exceeds remaining budget
|
|
287
|
+
if (stepCost > 0 && remaining < stepCost) {
|
|
288
|
+
logger?.debug?.('enrichment.all.skip_budget', {
|
|
289
|
+
provider: providerName,
|
|
290
|
+
cost: stepCost,
|
|
291
|
+
remaining,
|
|
292
|
+
});
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
providersQueried.push(providerName);
|
|
296
|
+
let raw = null;
|
|
297
|
+
try {
|
|
298
|
+
logger?.debug?.('enrichment.all.provider.start', { provider: providerName });
|
|
299
|
+
raw = await provider(candidate);
|
|
300
|
+
logger?.debug?.('enrichment.all.provider.done', {
|
|
301
|
+
provider: providerName,
|
|
302
|
+
hasResult: !!raw,
|
|
303
|
+
isMulti: isMultiResult(raw),
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
logger?.warn?.('enrichment.all.provider.error', {
|
|
308
|
+
provider: providerName,
|
|
309
|
+
error: error instanceof Error ? error.message : String(error),
|
|
310
|
+
});
|
|
311
|
+
// Retry once after delay on transient errors
|
|
312
|
+
if (retryMs > 0) {
|
|
313
|
+
await new Promise((r) => setTimeout(r, retryMs));
|
|
314
|
+
try {
|
|
315
|
+
raw = await provider(candidate);
|
|
316
|
+
logger?.debug?.('enrichment.all.provider.retry.done', {
|
|
317
|
+
provider: providerName,
|
|
318
|
+
hasResult: !!raw,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
raw = null;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// Deduct cost (even on failure - API was called)
|
|
327
|
+
if (stepCost > 0) {
|
|
328
|
+
remaining = Math.max(0, remaining - stepCost);
|
|
329
|
+
totalCost += stepCost;
|
|
330
|
+
if (onCost) {
|
|
331
|
+
try {
|
|
332
|
+
await onCost(providerName, stepCost);
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
// Ignore cost callback errors
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
logger?.debug?.('enrichment.all.cost.debit', {
|
|
339
|
+
provider: providerName,
|
|
340
|
+
cost: stepCost,
|
|
341
|
+
remaining,
|
|
342
|
+
totalCost,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
// Process results
|
|
346
|
+
const now = new Date().toISOString();
|
|
347
|
+
if (isMultiResult(raw)) {
|
|
348
|
+
// Provider returned multiple emails
|
|
349
|
+
for (const emailResult of raw.emails) {
|
|
350
|
+
const emailLower = emailResult.email.toLowerCase();
|
|
351
|
+
if (seenEmails.has(emailLower)) {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
seenEmails.add(emailLower);
|
|
355
|
+
allEmails.push({
|
|
356
|
+
email: emailResult.email,
|
|
357
|
+
source: providerName,
|
|
358
|
+
confidence: emailResult.confidence ?? 50,
|
|
359
|
+
verified: emailResult.verified ?? false,
|
|
360
|
+
type: classifyEmailType(emailResult.email),
|
|
361
|
+
isCatchAll: emailResult.isCatchAll,
|
|
362
|
+
foundAt: now,
|
|
363
|
+
metadata: emailResult.metadata,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
else if (raw?.email) {
|
|
368
|
+
// Provider returned single email
|
|
369
|
+
const emailLower = raw.email.toLowerCase();
|
|
370
|
+
if (!seenEmails.has(emailLower)) {
|
|
371
|
+
seenEmails.add(emailLower);
|
|
372
|
+
const confidence = typeof raw.score === 'number'
|
|
373
|
+
? raw.score
|
|
374
|
+
: typeof raw.confidence === 'number'
|
|
375
|
+
? raw.confidence
|
|
376
|
+
: 50;
|
|
377
|
+
allEmails.push({
|
|
378
|
+
email: raw.email,
|
|
379
|
+
source: providerName,
|
|
380
|
+
confidence,
|
|
381
|
+
verified: raw.verified ?? false,
|
|
382
|
+
type: classifyEmailType(raw.email),
|
|
383
|
+
foundAt: now,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// Sort by confidence (highest first), then by type (business first)
|
|
389
|
+
const typeOrder = {
|
|
390
|
+
business: 0,
|
|
391
|
+
unknown: 1,
|
|
392
|
+
role: 2,
|
|
393
|
+
personal: 3,
|
|
394
|
+
disposable: 4,
|
|
395
|
+
};
|
|
396
|
+
allEmails.sort((a, b) => {
|
|
397
|
+
// First by confidence (descending)
|
|
398
|
+
if (b.confidence !== a.confidence) {
|
|
399
|
+
return b.confidence - a.confidence;
|
|
400
|
+
}
|
|
401
|
+
// Then by type priority
|
|
402
|
+
return typeOrder[a.type] - typeOrder[b.type];
|
|
403
|
+
});
|
|
404
|
+
logger?.info?.('enrichment.all.complete', {
|
|
405
|
+
emailsFound: allEmails.length,
|
|
406
|
+
providersQueried: providersQueried.length,
|
|
407
|
+
totalCost,
|
|
408
|
+
});
|
|
409
|
+
return {
|
|
410
|
+
emails: allEmails,
|
|
411
|
+
candidate,
|
|
412
|
+
totalCost,
|
|
413
|
+
providersQueried,
|
|
414
|
+
completedAt: new Date().toISOString(),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Enrich multiple candidates and collect all emails (batch version)
|
|
419
|
+
*/
|
|
420
|
+
async function enrichAllBatch(candidates, options) {
|
|
421
|
+
const results = [];
|
|
422
|
+
const batchSize = options.batchSize ?? 50;
|
|
423
|
+
const delayMs = options.delayMs ?? 200;
|
|
424
|
+
for (let i = 0; i < candidates.length; i += batchSize) {
|
|
425
|
+
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);
|
|
429
|
+
// Delay between batches to avoid rate limiting
|
|
430
|
+
if (i + batchSize < candidates.length && delayMs > 0) {
|
|
431
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return results;
|
|
435
|
+
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Apollo.io public API for email finding.
|
|
5
5
|
* Uses mixed_people/search endpoint.
|
|
6
6
|
*/
|
|
7
|
-
import type { EnrichmentCandidate, ProviderResult, ApolloConfig } from "../types";
|
|
7
|
+
import type { EnrichmentCandidate, ProviderResult, ProviderMultiResult, ApolloConfig } from "../types";
|
|
8
8
|
/**
|
|
9
9
|
* Create the Apollo provider function
|
|
10
10
|
*/
|
|
11
|
-
export declare function createApolloProvider(config: ApolloConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | null>;
|
|
11
|
+
export declare function createApolloProvider(config: ApolloConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | ProviderMultiResult | null>;
|
|
@@ -88,8 +88,8 @@ function createApolloProvider(config) {
|
|
|
88
88
|
}
|
|
89
89
|
async function fetchEmail(candidate) {
|
|
90
90
|
const { name, org, domain } = extractInputs(candidate);
|
|
91
|
-
// Build request body
|
|
92
|
-
const body = { page: 1, per_page:
|
|
91
|
+
// Build request body - get more results for multi-email support
|
|
92
|
+
const body = { page: 1, per_page: 10 };
|
|
93
93
|
if (truthy(name))
|
|
94
94
|
body["q_person_name"] = String(name);
|
|
95
95
|
if (truthy(domain))
|
|
@@ -112,19 +112,64 @@ function createApolloProvider(config) {
|
|
|
112
112
|
(json.people || json.matches || json.data?.people));
|
|
113
113
|
if (!Array.isArray(people) || people.length === 0)
|
|
114
114
|
return null;
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
115
|
+
// Collect all emails from all people
|
|
116
|
+
const emails = [];
|
|
117
|
+
const seenEmails = new Set();
|
|
118
|
+
for (const person of people) {
|
|
119
|
+
const p = person || {};
|
|
120
|
+
// Get primary email
|
|
121
|
+
const primaryEmail = p.email ??
|
|
122
|
+
p.primary_email ??
|
|
123
|
+
null;
|
|
124
|
+
if (primaryEmail) {
|
|
125
|
+
const emailLower = primaryEmail.toLowerCase();
|
|
126
|
+
if (!seenEmails.has(emailLower)) {
|
|
127
|
+
seenEmails.add(emailLower);
|
|
128
|
+
const score = typeof p.email_confidence === "number"
|
|
129
|
+
? p.email_confidence
|
|
130
|
+
: Number(p.email_confidence ?? 0) || 50;
|
|
131
|
+
const verified = mapVerified(p.email_status);
|
|
132
|
+
emails.push({
|
|
133
|
+
email: primaryEmail,
|
|
134
|
+
verified,
|
|
135
|
+
confidence: score,
|
|
136
|
+
metadata: {
|
|
137
|
+
name: p.name,
|
|
138
|
+
title: p.title,
|
|
139
|
+
company: p.organization?.name,
|
|
140
|
+
linkedin: p.linkedin_url,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Also check emails array if present
|
|
146
|
+
const personEmails = p.emails;
|
|
147
|
+
if (Array.isArray(personEmails)) {
|
|
148
|
+
for (const e of personEmails) {
|
|
149
|
+
if (!e.email)
|
|
150
|
+
continue;
|
|
151
|
+
const emailLower = e.email.toLowerCase();
|
|
152
|
+
if (seenEmails.has(emailLower))
|
|
153
|
+
continue;
|
|
154
|
+
seenEmails.add(emailLower);
|
|
155
|
+
emails.push({
|
|
156
|
+
email: e.email,
|
|
157
|
+
verified: mapVerified(e.status),
|
|
158
|
+
confidence: 50, // Secondary emails get lower confidence
|
|
159
|
+
metadata: {
|
|
160
|
+
name: p.name,
|
|
161
|
+
title: p.title,
|
|
162
|
+
company: p.organization?.name,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (emails.length === 0)
|
|
121
169
|
return null;
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const verified = mapVerified(p.email_status ??
|
|
126
|
-
p.emails?.[0]?.status);
|
|
127
|
-
return { email, verified, score };
|
|
170
|
+
// Sort by confidence
|
|
171
|
+
emails.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0));
|
|
172
|
+
return { emails };
|
|
128
173
|
}
|
|
129
174
|
catch {
|
|
130
175
|
return null;
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Generates email pattern candidates based on name + domain, then verifies via MX check.
|
|
5
5
|
* This is a FREE provider that doesn't require any API keys.
|
|
6
6
|
*/
|
|
7
|
-
import type { EnrichmentCandidate, ProviderResult, ConstructConfig } from '../types';
|
|
7
|
+
import type { EnrichmentCandidate, ProviderResult, ProviderMultiResult, ConstructConfig } from '../types';
|
|
8
8
|
/**
|
|
9
9
|
* Create the construct provider function
|
|
10
10
|
*/
|
|
11
|
-
export declare function createConstructProvider(config?: ConstructConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | null>;
|
|
11
|
+
export declare function createConstructProvider(config?: ConstructConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | ProviderMultiResult | null>;
|
|
@@ -88,18 +88,30 @@ function createConstructProvider(config) {
|
|
|
88
88
|
}
|
|
89
89
|
const candidates = buildCandidates({ first, last, domain });
|
|
90
90
|
const max = Math.min(candidates.length, maxAttempts);
|
|
91
|
+
// Collect ALL valid email patterns (not just first match)
|
|
92
|
+
const validEmails = [];
|
|
91
93
|
for (let i = 0; i < max; i++) {
|
|
92
94
|
const email = candidates[i];
|
|
93
95
|
const verification = await (0, mx_1.verifyEmailMx)(email, { timeoutMs });
|
|
94
96
|
if (verification.valid === true && verification.confidence >= 50) {
|
|
95
|
-
|
|
97
|
+
validEmails.push({
|
|
96
98
|
email,
|
|
97
99
|
verified: true,
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
confidence: verification.confidence,
|
|
101
|
+
isCatchAll: verification.isCatchAll,
|
|
102
|
+
metadata: {
|
|
103
|
+
pattern: email.split('@')[0], // The local part pattern used
|
|
104
|
+
mxRecords: verification.mxRecords,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
100
107
|
}
|
|
101
108
|
}
|
|
102
|
-
|
|
109
|
+
if (validEmails.length === 0) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
// Sort by confidence
|
|
113
|
+
validEmails.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0));
|
|
114
|
+
return { emails: validEmails };
|
|
103
115
|
}
|
|
104
116
|
// Mark provider name for orchestrator
|
|
105
117
|
fetchEmail.__name = 'construct';
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Hunter.io public API for email finding.
|
|
5
5
|
* Uses email-finder endpoint with name+domain, falls back to domain-search.
|
|
6
6
|
*/
|
|
7
|
-
import type { EnrichmentCandidate, ProviderResult, HunterConfig } from "../types";
|
|
7
|
+
import type { EnrichmentCandidate, ProviderResult, ProviderMultiResult, HunterConfig } from "../types";
|
|
8
8
|
/**
|
|
9
9
|
* Create the Hunter provider function
|
|
10
10
|
*/
|
|
11
|
-
export declare function createHunterProvider(config: HunterConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | null>;
|
|
11
|
+
export declare function createHunterProvider(config: HunterConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | ProviderMultiResult | null>;
|
|
@@ -97,6 +97,7 @@ function createHunterProvider(config) {
|
|
|
97
97
|
async function fetchEmail(candidate) {
|
|
98
98
|
const { first, last, domain } = extractInputs(candidate);
|
|
99
99
|
let url = null;
|
|
100
|
+
let isEmailFinder = false;
|
|
100
101
|
// Use email-finder if we have name components
|
|
101
102
|
if (truthy(first) && truthy(last) && truthy(domain)) {
|
|
102
103
|
const qs = new URLSearchParams({
|
|
@@ -106,13 +107,14 @@ function createHunterProvider(config) {
|
|
|
106
107
|
last_name: String(last),
|
|
107
108
|
});
|
|
108
109
|
url = `${API_BASE}/email-finder?${qs.toString()}`;
|
|
110
|
+
isEmailFinder = true;
|
|
109
111
|
}
|
|
110
|
-
// Fall back to domain-search if only domain available
|
|
112
|
+
// Fall back to domain-search if only domain available (can return multiple)
|
|
111
113
|
else if (truthy(domain)) {
|
|
112
114
|
const qs = new URLSearchParams({
|
|
113
115
|
api_key: String(apiKey),
|
|
114
116
|
domain: String(domain),
|
|
115
|
-
limit: "
|
|
117
|
+
limit: "10", // Get more results
|
|
116
118
|
});
|
|
117
119
|
url = `${API_BASE}/domain-search?${qs.toString()}`;
|
|
118
120
|
}
|
|
@@ -121,34 +123,58 @@ function createHunterProvider(config) {
|
|
|
121
123
|
}
|
|
122
124
|
try {
|
|
123
125
|
const json = await requestWithRetry(url, 1, 100);
|
|
124
|
-
// Parse email-finder response shape
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
126
|
+
// Parse email-finder response shape (single result)
|
|
127
|
+
if (isEmailFinder) {
|
|
128
|
+
const ef = (json && (json.data || json.result));
|
|
129
|
+
if (ef && (ef.email || ef.score !== undefined)) {
|
|
130
|
+
const email = ef.email ?? null;
|
|
131
|
+
const score = typeof ef.score === "number"
|
|
132
|
+
? ef.score
|
|
133
|
+
: Number(ef.score ?? 0) || undefined;
|
|
134
|
+
const verified = mapVerified(ef?.verification?.status ?? ef?.status);
|
|
135
|
+
if (!email)
|
|
136
|
+
return null;
|
|
137
|
+
return { email, verified, score };
|
|
138
|
+
}
|
|
135
139
|
}
|
|
136
|
-
// Parse domain-search response shape
|
|
140
|
+
// Parse domain-search response shape (can have multiple emails)
|
|
137
141
|
const ds = (json &&
|
|
138
142
|
json.data &&
|
|
139
143
|
Array.isArray(json.data.emails)
|
|
140
144
|
? json.data.emails
|
|
141
145
|
: null);
|
|
142
146
|
if (ds && ds.length > 0) {
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
// Return all emails as multi-result
|
|
148
|
+
const emails = [];
|
|
149
|
+
const seenEmails = new Set();
|
|
150
|
+
for (const hit of ds) {
|
|
151
|
+
const email = hit?.value || hit?.email;
|
|
152
|
+
if (!email)
|
|
153
|
+
continue;
|
|
154
|
+
const emailLower = email.toLowerCase();
|
|
155
|
+
if (seenEmails.has(emailLower))
|
|
156
|
+
continue;
|
|
157
|
+
seenEmails.add(emailLower);
|
|
158
|
+
const score = typeof hit?.confidence === "number"
|
|
159
|
+
? hit.confidence
|
|
160
|
+
: Number(hit?.confidence ?? 0) || 50;
|
|
161
|
+
const verified = mapVerified(hit?.verification?.status ?? hit?.status);
|
|
162
|
+
emails.push({
|
|
163
|
+
email,
|
|
164
|
+
verified,
|
|
165
|
+
confidence: score,
|
|
166
|
+
metadata: {
|
|
167
|
+
firstName: hit.first_name,
|
|
168
|
+
lastName: hit.last_name,
|
|
169
|
+
position: hit.position,
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
if (emails.length === 0)
|
|
150
174
|
return null;
|
|
151
|
-
|
|
175
|
+
// Sort by confidence
|
|
176
|
+
emails.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0));
|
|
177
|
+
return { emails };
|
|
152
178
|
}
|
|
153
179
|
return null;
|
|
154
180
|
}
|
|
@@ -3,9 +3,27 @@
|
|
|
3
3
|
*
|
|
4
4
|
* YOUR private database of ~500M scraped LinkedIn profiles with emails.
|
|
5
5
|
* This is FREE and unlimited - it's your own service.
|
|
6
|
+
*
|
|
7
|
+
* IMPORTANT: Prefer numeric LinkedIn ID over username for lookups.
|
|
8
|
+
* The numeric ID (from objectUrn like "urn:li:member:307567") is STABLE and never changes,
|
|
9
|
+
* while usernames can be changed by users at any time.
|
|
6
10
|
*/
|
|
7
|
-
import type { EnrichmentCandidate, ProviderResult, LddConfig } from "../types";
|
|
11
|
+
import type { EnrichmentCandidate, ProviderResult, ProviderMultiResult, LddConfig } from "../types";
|
|
12
|
+
/**
|
|
13
|
+
* Extract numeric LinkedIn ID from various formats:
|
|
14
|
+
* - Direct number: "307567"
|
|
15
|
+
* - URN format: "urn:li:member:307567"
|
|
16
|
+
* - Full URN: "urn:li:fs_salesProfile:(ACwAAAAEsW8B...,NAME_SEARCH,PJOV)"
|
|
17
|
+
* - objectUrn: "urn:li:member:307567"
|
|
18
|
+
*
|
|
19
|
+
* Returns the numeric ID as a string, or null if not found.
|
|
20
|
+
*/
|
|
21
|
+
export declare function extractNumericLinkedInId(input: string | undefined | null): string | null;
|
|
8
22
|
/**
|
|
9
23
|
* Create the LDD provider function
|
|
24
|
+
*
|
|
25
|
+
* Lookup priority:
|
|
26
|
+
* 1. Numeric LinkedIn ID (PREFERRED - stable, never changes)
|
|
27
|
+
* 2. Username (FALLBACK - can change over time)
|
|
10
28
|
*/
|
|
11
|
-
export declare function createLddProvider(config: LddConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | null>;
|
|
29
|
+
export declare function createLddProvider(config: LddConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | ProviderMultiResult | null>;
|