linkedin-secret-sauce 0.5.0 → 0.6.0
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/enrichment/auth/index.d.ts +4 -0
- package/dist/enrichment/auth/index.js +17 -0
- package/dist/enrichment/auth/smartlead-auth.d.ts +82 -0
- package/dist/enrichment/auth/smartlead-auth.js +321 -0
- package/dist/enrichment/index.d.ts +11 -8
- package/dist/enrichment/index.js +75 -20
- package/dist/enrichment/matching.d.ts +239 -0
- package/dist/enrichment/matching.js +619 -0
- package/dist/enrichment/orchestrator.d.ts +13 -1
- package/dist/enrichment/orchestrator.js +220 -3
- 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 +2 -2
- package/dist/enrichment/providers/ldd.js +16 -8
- package/dist/enrichment/providers/smartprospect.d.ts +72 -2
- package/dist/enrichment/providers/smartprospect.js +512 -72
- package/dist/enrichment/types.d.ts +156 -14
- package/dist/enrichment/types.js +56 -7
- package/dist/index.d.ts +2 -1
- package/dist/index.js +7 -1
- package/package.json +1 -1
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Contact Matching Utilities
|
|
4
|
+
*
|
|
5
|
+
* Matches contacts between LinkedIn Sales Navigator and SmartProspect
|
|
6
|
+
* to find the same person across both platforms.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.calculateMatchConfidence = calculateMatchConfidence;
|
|
10
|
+
exports.classifyMatchQuality = classifyMatchQuality;
|
|
11
|
+
exports.findBestMatch = findBestMatch;
|
|
12
|
+
exports.matchContacts = matchContacts;
|
|
13
|
+
exports.buildSmartProspectFiltersFromLinkedIn = buildSmartProspectFiltersFromLinkedIn;
|
|
14
|
+
exports.parseLinkedInSearchResponse = parseLinkedInSearchResponse;
|
|
15
|
+
exports.enrichLinkedInContact = enrichLinkedInContact;
|
|
16
|
+
exports.enrichLinkedInContactsBatch = enrichLinkedInContactsBatch;
|
|
17
|
+
exports.createLinkedInEnricher = createLinkedInEnricher;
|
|
18
|
+
const smartprospect_1 = require("./providers/smartprospect");
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Utility Functions
|
|
21
|
+
// =============================================================================
|
|
22
|
+
/**
|
|
23
|
+
* Normalize a string for comparison (lowercase, trim, remove extra spaces)
|
|
24
|
+
*/
|
|
25
|
+
function normalize(str) {
|
|
26
|
+
if (!str)
|
|
27
|
+
return '';
|
|
28
|
+
return str.toLowerCase().trim().replace(/\s+/g, ' ');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Extract company name from LinkedIn position
|
|
32
|
+
*/
|
|
33
|
+
function getLinkedInCompany(contact) {
|
|
34
|
+
const pos = contact.currentPositions?.[0];
|
|
35
|
+
if (!pos)
|
|
36
|
+
return '';
|
|
37
|
+
return pos.companyUrnResolutionResult?.name || pos.companyName || '';
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Extract job title from LinkedIn position
|
|
41
|
+
*/
|
|
42
|
+
function getLinkedInTitle(contact) {
|
|
43
|
+
const pos = contact.currentPositions?.[0];
|
|
44
|
+
return pos?.title || '';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Extract location parts from LinkedIn geoRegion
|
|
48
|
+
* Format: "City, State, Country" or "City, Country" or just "Country"
|
|
49
|
+
*/
|
|
50
|
+
function parseLinkedInLocation(geoRegion) {
|
|
51
|
+
if (!geoRegion)
|
|
52
|
+
return { city: '', state: '', country: '' };
|
|
53
|
+
const parts = geoRegion.split(',').map((p) => p.trim());
|
|
54
|
+
if (parts.length >= 3) {
|
|
55
|
+
return {
|
|
56
|
+
city: parts[0],
|
|
57
|
+
state: parts[1],
|
|
58
|
+
country: parts[parts.length - 1],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
else if (parts.length === 2) {
|
|
62
|
+
return {
|
|
63
|
+
city: parts[0],
|
|
64
|
+
state: '',
|
|
65
|
+
country: parts[1],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
return {
|
|
70
|
+
city: '',
|
|
71
|
+
state: '',
|
|
72
|
+
country: parts[0] || '',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Calculate Levenshtein distance between two strings
|
|
78
|
+
*/
|
|
79
|
+
function levenshteinDistance(a, b) {
|
|
80
|
+
const matrix = [];
|
|
81
|
+
for (let i = 0; i <= b.length; i++) {
|
|
82
|
+
matrix[i] = [i];
|
|
83
|
+
}
|
|
84
|
+
for (let j = 0; j <= a.length; j++) {
|
|
85
|
+
matrix[0][j] = j;
|
|
86
|
+
}
|
|
87
|
+
for (let i = 1; i <= b.length; i++) {
|
|
88
|
+
for (let j = 1; j <= a.length; j++) {
|
|
89
|
+
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
90
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return matrix[b.length][a.length];
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Calculate string similarity (0-1) using Levenshtein distance
|
|
101
|
+
*/
|
|
102
|
+
function stringSimilarity(a, b) {
|
|
103
|
+
const na = normalize(a);
|
|
104
|
+
const nb = normalize(b);
|
|
105
|
+
if (na === nb)
|
|
106
|
+
return 1;
|
|
107
|
+
if (!na || !nb)
|
|
108
|
+
return 0;
|
|
109
|
+
const maxLen = Math.max(na.length, nb.length);
|
|
110
|
+
const distance = levenshteinDistance(na, nb);
|
|
111
|
+
return 1 - distance / maxLen;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Check if strings match exactly (normalized)
|
|
115
|
+
*/
|
|
116
|
+
function exactMatch(a, b) {
|
|
117
|
+
return normalize(a) === normalize(b) && normalize(a) !== '';
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Check if strings match with fuzzy tolerance
|
|
121
|
+
*/
|
|
122
|
+
function fuzzyMatch(a, b, threshold = 0.85) {
|
|
123
|
+
return stringSimilarity(a, b) >= threshold;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Check if one string contains the other
|
|
127
|
+
*/
|
|
128
|
+
function containsMatch(a, b) {
|
|
129
|
+
const na = normalize(a);
|
|
130
|
+
const nb = normalize(b);
|
|
131
|
+
if (!na || !nb)
|
|
132
|
+
return false;
|
|
133
|
+
return na.includes(nb) || nb.includes(na);
|
|
134
|
+
}
|
|
135
|
+
// =============================================================================
|
|
136
|
+
// Main Matching Functions
|
|
137
|
+
// =============================================================================
|
|
138
|
+
/**
|
|
139
|
+
* Calculate match confidence between a LinkedIn contact and SmartProspect contact
|
|
140
|
+
*/
|
|
141
|
+
function calculateMatchConfidence(linkedin, smartprospect, options = {}) {
|
|
142
|
+
const { fuzzyNames = true, fuzzyCompany = true } = options;
|
|
143
|
+
const matchedFields = [];
|
|
144
|
+
let score = 0;
|
|
145
|
+
// === Name matching (up to 40 points) ===
|
|
146
|
+
const liFirstName = normalize(linkedin.firstName);
|
|
147
|
+
const liLastName = normalize(linkedin.lastName);
|
|
148
|
+
const spFirstName = normalize(smartprospect.firstName);
|
|
149
|
+
const spLastName = normalize(smartprospect.lastName);
|
|
150
|
+
// First name match
|
|
151
|
+
if (exactMatch(linkedin.firstName, smartprospect.firstName)) {
|
|
152
|
+
score += 20;
|
|
153
|
+
matchedFields.push('firstName:exact');
|
|
154
|
+
}
|
|
155
|
+
else if (fuzzyNames && fuzzyMatch(linkedin.firstName, smartprospect.firstName)) {
|
|
156
|
+
score += 15;
|
|
157
|
+
matchedFields.push('firstName:fuzzy');
|
|
158
|
+
}
|
|
159
|
+
// Last name match
|
|
160
|
+
if (exactMatch(linkedin.lastName, smartprospect.lastName)) {
|
|
161
|
+
score += 20;
|
|
162
|
+
matchedFields.push('lastName:exact');
|
|
163
|
+
}
|
|
164
|
+
else if (fuzzyNames && fuzzyMatch(linkedin.lastName, smartprospect.lastName)) {
|
|
165
|
+
score += 15;
|
|
166
|
+
matchedFields.push('lastName:fuzzy');
|
|
167
|
+
}
|
|
168
|
+
// === Company matching (up to 30 points) ===
|
|
169
|
+
const liCompany = getLinkedInCompany(linkedin);
|
|
170
|
+
const spCompany = smartprospect.company?.name || '';
|
|
171
|
+
if (exactMatch(liCompany, spCompany)) {
|
|
172
|
+
score += 30;
|
|
173
|
+
matchedFields.push('company:exact');
|
|
174
|
+
}
|
|
175
|
+
else if (fuzzyCompany && fuzzyMatch(liCompany, spCompany, 0.8)) {
|
|
176
|
+
score += 25;
|
|
177
|
+
matchedFields.push('company:fuzzy');
|
|
178
|
+
}
|
|
179
|
+
else if (containsMatch(liCompany, spCompany)) {
|
|
180
|
+
score += 15;
|
|
181
|
+
matchedFields.push('company:contains');
|
|
182
|
+
}
|
|
183
|
+
// === Title matching (up to 15 points) ===
|
|
184
|
+
const liTitle = getLinkedInTitle(linkedin);
|
|
185
|
+
const spTitle = smartprospect.title || '';
|
|
186
|
+
if (exactMatch(liTitle, spTitle)) {
|
|
187
|
+
score += 15;
|
|
188
|
+
matchedFields.push('title:exact');
|
|
189
|
+
}
|
|
190
|
+
else if (fuzzyMatch(liTitle, spTitle, 0.75)) {
|
|
191
|
+
score += 12;
|
|
192
|
+
matchedFields.push('title:fuzzy');
|
|
193
|
+
}
|
|
194
|
+
else if (containsMatch(liTitle, spTitle)) {
|
|
195
|
+
score += 8;
|
|
196
|
+
matchedFields.push('title:contains');
|
|
197
|
+
}
|
|
198
|
+
// === Location matching (up to 15 points) ===
|
|
199
|
+
const liLocation = parseLinkedInLocation(linkedin.geoRegion);
|
|
200
|
+
const spCountry = normalize(smartprospect.country);
|
|
201
|
+
const spState = normalize(smartprospect.state);
|
|
202
|
+
const spCity = normalize(smartprospect.city);
|
|
203
|
+
// Country match
|
|
204
|
+
if (spCountry && exactMatch(liLocation.country, smartprospect.country)) {
|
|
205
|
+
score += 5;
|
|
206
|
+
matchedFields.push('country:exact');
|
|
207
|
+
}
|
|
208
|
+
// State match
|
|
209
|
+
if (spState && exactMatch(liLocation.state, smartprospect.state)) {
|
|
210
|
+
score += 5;
|
|
211
|
+
matchedFields.push('state:exact');
|
|
212
|
+
}
|
|
213
|
+
// City match
|
|
214
|
+
if (spCity && exactMatch(liLocation.city, smartprospect.city)) {
|
|
215
|
+
score += 5;
|
|
216
|
+
matchedFields.push('city:exact');
|
|
217
|
+
}
|
|
218
|
+
return { confidence: Math.min(100, score), matchedFields };
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Classify match quality based on confidence and matched fields
|
|
222
|
+
*/
|
|
223
|
+
function classifyMatchQuality(confidence, matchedFields) {
|
|
224
|
+
const hasNameMatch = matchedFields.some((f) => f.startsWith('firstName:exact')) &&
|
|
225
|
+
matchedFields.some((f) => f.startsWith('lastName:exact'));
|
|
226
|
+
const hasCompanyMatch = matchedFields.some((f) => f.startsWith('company:'));
|
|
227
|
+
if (confidence >= 85 && hasNameMatch && hasCompanyMatch) {
|
|
228
|
+
return 'exact';
|
|
229
|
+
}
|
|
230
|
+
else if (confidence >= 70 && hasNameMatch) {
|
|
231
|
+
return 'high';
|
|
232
|
+
}
|
|
233
|
+
else if (confidence >= 50) {
|
|
234
|
+
return 'medium';
|
|
235
|
+
}
|
|
236
|
+
else if (confidence >= 30) {
|
|
237
|
+
return 'low';
|
|
238
|
+
}
|
|
239
|
+
return 'none';
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Find the best matching SmartProspect contact for a LinkedIn contact
|
|
243
|
+
*/
|
|
244
|
+
function findBestMatch(linkedInContact, smartProspectContacts, options = {}) {
|
|
245
|
+
const { minConfidence = 50 } = options;
|
|
246
|
+
let bestMatch = null;
|
|
247
|
+
for (const spContact of smartProspectContacts) {
|
|
248
|
+
const { confidence, matchedFields } = calculateMatchConfidence(linkedInContact, spContact, options);
|
|
249
|
+
if (confidence >= minConfidence) {
|
|
250
|
+
if (!bestMatch || confidence > bestMatch.confidence) {
|
|
251
|
+
bestMatch = {
|
|
252
|
+
smartProspectContact: spContact,
|
|
253
|
+
linkedInContact,
|
|
254
|
+
confidence,
|
|
255
|
+
matchedFields,
|
|
256
|
+
quality: classifyMatchQuality(confidence, matchedFields),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return bestMatch;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Match multiple LinkedIn contacts to SmartProspect contacts
|
|
265
|
+
*/
|
|
266
|
+
function matchContacts(linkedInContacts, smartProspectContacts, options = {}) {
|
|
267
|
+
const results = [];
|
|
268
|
+
for (const liContact of linkedInContacts) {
|
|
269
|
+
const match = findBestMatch(liContact, smartProspectContacts, options);
|
|
270
|
+
if (match) {
|
|
271
|
+
results.push(match);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// Sort by confidence descending
|
|
275
|
+
return results.sort((a, b) => b.confidence - a.confidence);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Build SmartProspect search filters from a LinkedIn contact
|
|
279
|
+
* This helps you search SmartProspect for a specific LinkedIn contact
|
|
280
|
+
*/
|
|
281
|
+
function buildSmartProspectFiltersFromLinkedIn(linkedInContact) {
|
|
282
|
+
const filters = {
|
|
283
|
+
firstName: [linkedInContact.firstName],
|
|
284
|
+
lastName: [linkedInContact.lastName],
|
|
285
|
+
};
|
|
286
|
+
const company = getLinkedInCompany(linkedInContact);
|
|
287
|
+
if (company) {
|
|
288
|
+
filters.companyName = [company];
|
|
289
|
+
}
|
|
290
|
+
const title = getLinkedInTitle(linkedInContact);
|
|
291
|
+
if (title) {
|
|
292
|
+
filters.title = [title];
|
|
293
|
+
}
|
|
294
|
+
const location = parseLinkedInLocation(linkedInContact.geoRegion);
|
|
295
|
+
if (location.country) {
|
|
296
|
+
filters.country = [location.country];
|
|
297
|
+
}
|
|
298
|
+
return filters;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Parse LinkedIn Sales Navigator API response elements into LinkedInContact array
|
|
302
|
+
*/
|
|
303
|
+
function parseLinkedInSearchResponse(elements) {
|
|
304
|
+
return elements.map((el) => ({
|
|
305
|
+
objectUrn: el.objectUrn,
|
|
306
|
+
entityUrn: el.entityUrn,
|
|
307
|
+
firstName: el.firstName || '',
|
|
308
|
+
lastName: el.lastName || '',
|
|
309
|
+
fullName: el.fullName || undefined,
|
|
310
|
+
geoRegion: el.geoRegion || undefined,
|
|
311
|
+
currentPositions: el.currentPositions,
|
|
312
|
+
profilePictureDisplayImage: el.profilePictureDisplayImage,
|
|
313
|
+
}));
|
|
314
|
+
}
|
|
315
|
+
// =============================================================================
|
|
316
|
+
// High-Level Enrichment Function
|
|
317
|
+
// =============================================================================
|
|
318
|
+
/**
|
|
319
|
+
* Enrich a LinkedIn Sales Navigator contact using SmartProspect
|
|
320
|
+
*
|
|
321
|
+
* This is the main function consumers should use. It:
|
|
322
|
+
* 1. Takes a LinkedIn contact from Sales Navigator search results
|
|
323
|
+
* 2. Searches SmartProspect for matching candidates
|
|
324
|
+
* 3. Uses intelligent matching to find the exact same person
|
|
325
|
+
* 4. Optionally fetches their verified email (costs credits)
|
|
326
|
+
*
|
|
327
|
+
* @example
|
|
328
|
+
* ```ts
|
|
329
|
+
* import { enrichLinkedInContact } from 'linkedin-secret-sauce';
|
|
330
|
+
*
|
|
331
|
+
* // From your LinkedIn Sales Nav search result
|
|
332
|
+
* const linkedInContact = {
|
|
333
|
+
* firstName: 'Valentin',
|
|
334
|
+
* lastName: 'Rangelov',
|
|
335
|
+
* geoRegion: 'Sofia, Sofia City, Bulgaria',
|
|
336
|
+
* currentPositions: [{
|
|
337
|
+
* companyName: 'Recruitica',
|
|
338
|
+
* title: 'Chief Technology Officer'
|
|
339
|
+
* }]
|
|
340
|
+
* };
|
|
341
|
+
*
|
|
342
|
+
* const result = await enrichLinkedInContact(linkedInContact, {
|
|
343
|
+
* email: process.env.SMARTLEAD_EMAIL,
|
|
344
|
+
* password: process.env.SMARTLEAD_PASSWORD
|
|
345
|
+
* });
|
|
346
|
+
*
|
|
347
|
+
* if (result.success && result.email) {
|
|
348
|
+
* console.log(`Found email: ${result.email}`);
|
|
349
|
+
* console.log(`Match confidence: ${result.matchConfidence}%`);
|
|
350
|
+
* console.log(`Deliverability: ${result.emailDeliverability * 100}%`);
|
|
351
|
+
* }
|
|
352
|
+
* ```
|
|
353
|
+
*/
|
|
354
|
+
async function enrichLinkedInContact(linkedInContact, smartProspectConfig, options = {}) {
|
|
355
|
+
const { minConfidence = 60, autoFetch = true, fuzzyNames = true, fuzzyCompany = true, searchLimit = 25, includeCompany = true, includeLocation = false, } = options;
|
|
356
|
+
// Initialize result
|
|
357
|
+
const result = {
|
|
358
|
+
success: false,
|
|
359
|
+
linkedInContact,
|
|
360
|
+
matchedContact: null,
|
|
361
|
+
matchConfidence: 0,
|
|
362
|
+
matchQuality: 'none',
|
|
363
|
+
matchedFields: [],
|
|
364
|
+
email: null,
|
|
365
|
+
emailDeliverability: 0,
|
|
366
|
+
verificationStatus: null,
|
|
367
|
+
allCandidates: [],
|
|
368
|
+
totalCandidatesFound: 0,
|
|
369
|
+
};
|
|
370
|
+
// Create SmartProspect client
|
|
371
|
+
const client = (0, smartprospect_1.createSmartProspectClient)(smartProspectConfig);
|
|
372
|
+
if (!client) {
|
|
373
|
+
result.error = 'Failed to create SmartProspect client - check credentials';
|
|
374
|
+
return result;
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
// Build search filters from LinkedIn contact
|
|
378
|
+
const filters = buildSmartProspectFiltersFromLinkedIn(linkedInContact);
|
|
379
|
+
// Optionally remove company filter for broader search
|
|
380
|
+
const searchFilters = {
|
|
381
|
+
firstName: filters.firstName,
|
|
382
|
+
lastName: filters.lastName,
|
|
383
|
+
limit: searchLimit,
|
|
384
|
+
};
|
|
385
|
+
if (includeCompany && filters.companyName) {
|
|
386
|
+
searchFilters.companyName = filters.companyName;
|
|
387
|
+
}
|
|
388
|
+
if (includeLocation && filters.country) {
|
|
389
|
+
searchFilters.country = filters.country;
|
|
390
|
+
}
|
|
391
|
+
// Step 1: Search SmartProspect (FREE)
|
|
392
|
+
const searchResponse = await client.search(searchFilters);
|
|
393
|
+
if (!searchResponse.success) {
|
|
394
|
+
result.error = `SmartProspect search failed: ${searchResponse.message}`;
|
|
395
|
+
return result;
|
|
396
|
+
}
|
|
397
|
+
result.allCandidates = searchResponse.data.list;
|
|
398
|
+
result.totalCandidatesFound = searchResponse.data.total_count;
|
|
399
|
+
if (searchResponse.data.list.length === 0) {
|
|
400
|
+
// Try broader search without company filter
|
|
401
|
+
if (includeCompany && filters.companyName) {
|
|
402
|
+
const broaderFilters = {
|
|
403
|
+
firstName: filters.firstName,
|
|
404
|
+
lastName: filters.lastName,
|
|
405
|
+
limit: searchLimit,
|
|
406
|
+
};
|
|
407
|
+
const broaderResponse = await client.search(broaderFilters);
|
|
408
|
+
if (broaderResponse.success && broaderResponse.data.list.length > 0) {
|
|
409
|
+
result.allCandidates = broaderResponse.data.list;
|
|
410
|
+
result.totalCandidatesFound = broaderResponse.data.total_count;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (result.allCandidates.length === 0) {
|
|
414
|
+
result.error = 'No candidates found in SmartProspect';
|
|
415
|
+
return result;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// Step 2: Find best match using intelligent matching
|
|
419
|
+
const matchResult = findBestMatch(linkedInContact, result.allCandidates, { minConfidence, fuzzyNames, fuzzyCompany });
|
|
420
|
+
if (!matchResult) {
|
|
421
|
+
result.error = `No match above ${minConfidence}% confidence threshold`;
|
|
422
|
+
return result;
|
|
423
|
+
}
|
|
424
|
+
result.matchedContact = matchResult.smartProspectContact;
|
|
425
|
+
result.matchConfidence = matchResult.confidence;
|
|
426
|
+
result.matchQuality = matchResult.quality;
|
|
427
|
+
result.matchedFields = matchResult.matchedFields;
|
|
428
|
+
// Step 3: Fetch email if auto-fetch enabled and good match (COSTS CREDITS)
|
|
429
|
+
if (autoFetch && matchResult.confidence >= minConfidence) {
|
|
430
|
+
const fetchResponse = await client.fetch([matchResult.smartProspectContact.id]);
|
|
431
|
+
if (fetchResponse.success && fetchResponse.data.list.length > 0) {
|
|
432
|
+
const enrichedContact = fetchResponse.data.list[0];
|
|
433
|
+
result.email = enrichedContact.email || null;
|
|
434
|
+
result.emailDeliverability = enrichedContact.emailDeliverability || 0;
|
|
435
|
+
result.verificationStatus = enrichedContact.verificationStatus || null;
|
|
436
|
+
// Update matched contact with enriched data
|
|
437
|
+
result.matchedContact = enrichedContact;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
result.success = true;
|
|
441
|
+
return result;
|
|
442
|
+
}
|
|
443
|
+
catch (err) {
|
|
444
|
+
result.error = err instanceof Error ? err.message : 'Unknown error';
|
|
445
|
+
return result;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Enrich multiple LinkedIn contacts in batch
|
|
450
|
+
*
|
|
451
|
+
* Processes contacts sequentially to avoid rate limits.
|
|
452
|
+
* For parallel processing, use Promise.all with rate limiting.
|
|
453
|
+
*/
|
|
454
|
+
async function enrichLinkedInContactsBatch(linkedInContacts, smartProspectConfig, options = {}) {
|
|
455
|
+
const { delayMs = 200, onProgress, ...enrichOptions } = options;
|
|
456
|
+
const results = [];
|
|
457
|
+
for (let i = 0; i < linkedInContacts.length; i++) {
|
|
458
|
+
const contact = linkedInContacts[i];
|
|
459
|
+
const result = await enrichLinkedInContact(contact, smartProspectConfig, enrichOptions);
|
|
460
|
+
results.push(result);
|
|
461
|
+
if (onProgress) {
|
|
462
|
+
onProgress(i + 1, linkedInContacts.length, result);
|
|
463
|
+
}
|
|
464
|
+
// Delay between requests (except for last one)
|
|
465
|
+
if (i < linkedInContacts.length - 1 && delayMs > 0) {
|
|
466
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return results;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Create a reusable enricher function with pre-configured SmartProspect client
|
|
473
|
+
*
|
|
474
|
+
* More efficient for multiple enrichments as it reuses the same client/token.
|
|
475
|
+
*
|
|
476
|
+
* @example
|
|
477
|
+
* ```ts
|
|
478
|
+
* const enricher = createLinkedInEnricher({
|
|
479
|
+
* email: process.env.SMARTLEAD_EMAIL,
|
|
480
|
+
* password: process.env.SMARTLEAD_PASSWORD
|
|
481
|
+
* });
|
|
482
|
+
*
|
|
483
|
+
* // Now use for multiple contacts
|
|
484
|
+
* const result1 = await enricher.enrich(contact1);
|
|
485
|
+
* const result2 = await enricher.enrich(contact2);
|
|
486
|
+
*
|
|
487
|
+
* // Or batch
|
|
488
|
+
* const results = await enricher.enrichBatch([contact1, contact2, contact3]);
|
|
489
|
+
* ```
|
|
490
|
+
*/
|
|
491
|
+
function createLinkedInEnricher(smartProspectConfig, defaultOptions = {}) {
|
|
492
|
+
const client = (0, smartprospect_1.createSmartProspectClient)(smartProspectConfig);
|
|
493
|
+
if (!client) {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
client,
|
|
498
|
+
async enrich(contact, options = {}) {
|
|
499
|
+
const mergedOptions = { ...defaultOptions, ...options };
|
|
500
|
+
// We reuse the existing client rather than creating a new one
|
|
501
|
+
const result = {
|
|
502
|
+
success: false,
|
|
503
|
+
linkedInContact: contact,
|
|
504
|
+
matchedContact: null,
|
|
505
|
+
matchConfidence: 0,
|
|
506
|
+
matchQuality: 'none',
|
|
507
|
+
matchedFields: [],
|
|
508
|
+
email: null,
|
|
509
|
+
emailDeliverability: 0,
|
|
510
|
+
verificationStatus: null,
|
|
511
|
+
allCandidates: [],
|
|
512
|
+
totalCandidatesFound: 0,
|
|
513
|
+
};
|
|
514
|
+
const { minConfidence = 60, autoFetch = true, fuzzyNames = true, fuzzyCompany = true, searchLimit = 25, includeCompany = true, includeLocation = false, } = mergedOptions;
|
|
515
|
+
try {
|
|
516
|
+
const filters = buildSmartProspectFiltersFromLinkedIn(contact);
|
|
517
|
+
const searchFilters = {
|
|
518
|
+
firstName: filters.firstName,
|
|
519
|
+
lastName: filters.lastName,
|
|
520
|
+
limit: searchLimit,
|
|
521
|
+
};
|
|
522
|
+
if (includeCompany && filters.companyName) {
|
|
523
|
+
searchFilters.companyName = filters.companyName;
|
|
524
|
+
}
|
|
525
|
+
if (includeLocation && filters.country) {
|
|
526
|
+
searchFilters.country = filters.country;
|
|
527
|
+
}
|
|
528
|
+
const searchResponse = await client.search(searchFilters);
|
|
529
|
+
if (!searchResponse.success) {
|
|
530
|
+
result.error = `SmartProspect search failed: ${searchResponse.message}`;
|
|
531
|
+
return result;
|
|
532
|
+
}
|
|
533
|
+
result.allCandidates = searchResponse.data.list;
|
|
534
|
+
result.totalCandidatesFound = searchResponse.data.total_count;
|
|
535
|
+
// Broader search fallback
|
|
536
|
+
if (searchResponse.data.list.length === 0 && includeCompany && filters.companyName) {
|
|
537
|
+
const broaderResponse = await client.search({
|
|
538
|
+
firstName: filters.firstName,
|
|
539
|
+
lastName: filters.lastName,
|
|
540
|
+
limit: searchLimit,
|
|
541
|
+
});
|
|
542
|
+
if (broaderResponse.success) {
|
|
543
|
+
result.allCandidates = broaderResponse.data.list;
|
|
544
|
+
result.totalCandidatesFound = broaderResponse.data.total_count;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (result.allCandidates.length === 0) {
|
|
548
|
+
result.error = 'No candidates found in SmartProspect';
|
|
549
|
+
return result;
|
|
550
|
+
}
|
|
551
|
+
const matchResult = findBestMatch(contact, result.allCandidates, {
|
|
552
|
+
minConfidence,
|
|
553
|
+
fuzzyNames,
|
|
554
|
+
fuzzyCompany,
|
|
555
|
+
});
|
|
556
|
+
if (!matchResult) {
|
|
557
|
+
result.error = `No match above ${minConfidence}% confidence threshold`;
|
|
558
|
+
return result;
|
|
559
|
+
}
|
|
560
|
+
result.matchedContact = matchResult.smartProspectContact;
|
|
561
|
+
result.matchConfidence = matchResult.confidence;
|
|
562
|
+
result.matchQuality = matchResult.quality;
|
|
563
|
+
result.matchedFields = matchResult.matchedFields;
|
|
564
|
+
if (autoFetch && matchResult.confidence >= minConfidence) {
|
|
565
|
+
const fetchResponse = await client.fetch([matchResult.smartProspectContact.id]);
|
|
566
|
+
if (fetchResponse.success && fetchResponse.data.list.length > 0) {
|
|
567
|
+
const enrichedContact = fetchResponse.data.list[0];
|
|
568
|
+
result.email = enrichedContact.email || null;
|
|
569
|
+
result.emailDeliverability = enrichedContact.emailDeliverability || 0;
|
|
570
|
+
result.verificationStatus = enrichedContact.verificationStatus || null;
|
|
571
|
+
result.matchedContact = enrichedContact;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
result.success = true;
|
|
575
|
+
return result;
|
|
576
|
+
}
|
|
577
|
+
catch (err) {
|
|
578
|
+
result.error = err instanceof Error ? err.message : 'Unknown error';
|
|
579
|
+
return result;
|
|
580
|
+
}
|
|
581
|
+
},
|
|
582
|
+
async enrichBatch(contacts, options = {}) {
|
|
583
|
+
const { delayMs = 200, onProgress, ...enrichOptions } = options;
|
|
584
|
+
const results = [];
|
|
585
|
+
for (let i = 0; i < contacts.length; i++) {
|
|
586
|
+
const result = await this.enrich(contacts[i], enrichOptions);
|
|
587
|
+
results.push(result);
|
|
588
|
+
if (onProgress) {
|
|
589
|
+
onProgress(i + 1, contacts.length, result);
|
|
590
|
+
}
|
|
591
|
+
if (i < contacts.length - 1 && delayMs > 0) {
|
|
592
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return results;
|
|
596
|
+
},
|
|
597
|
+
async searchOnly(contact, options = {}) {
|
|
598
|
+
const { searchLimit = 25, includeCompany = true } = options;
|
|
599
|
+
const filters = buildSmartProspectFiltersFromLinkedIn(contact);
|
|
600
|
+
const searchFilters = {
|
|
601
|
+
firstName: filters.firstName,
|
|
602
|
+
lastName: filters.lastName,
|
|
603
|
+
limit: searchLimit,
|
|
604
|
+
};
|
|
605
|
+
if (includeCompany && filters.companyName) {
|
|
606
|
+
searchFilters.companyName = filters.companyName;
|
|
607
|
+
}
|
|
608
|
+
const searchResponse = await client.search(searchFilters);
|
|
609
|
+
const candidates = searchResponse.success ? searchResponse.data.list : [];
|
|
610
|
+
const bestMatch = candidates.length > 0
|
|
611
|
+
? findBestMatch(contact, candidates, { minConfidence: 0 })
|
|
612
|
+
: null;
|
|
613
|
+
return { candidates, bestMatch };
|
|
614
|
+
},
|
|
615
|
+
async fetchEmail(contactId) {
|
|
616
|
+
return client.fetch([contactId]);
|
|
617
|
+
},
|
|
618
|
+
};
|
|
619
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Runs providers in sequence until a verified email is found that meets
|
|
5
5
|
* the confidence threshold. Supports budget tracking and cost callbacks.
|
|
6
6
|
*/
|
|
7
|
-
import type { CanonicalEmail, EnrichmentCandidate, ProviderFunc, BatchEnrichmentOptions, CostCallback, EnrichmentLogger } from './types';
|
|
7
|
+
import type { CanonicalEmail, EnrichmentCandidate, ProviderFunc, BatchEnrichmentOptions, CostCallback, EnrichmentLogger, MultiEmailResult } from './types';
|
|
8
8
|
export interface OrchestratorOptions {
|
|
9
9
|
/** Provider functions in order */
|
|
10
10
|
providers: ProviderFunc[];
|
|
@@ -29,3 +29,15 @@ export declare function enrichBusinessEmail(candidate: EnrichmentCandidate, opti
|
|
|
29
29
|
export declare function enrichBatch(candidates: EnrichmentCandidate[], options: OrchestratorOptions & BatchEnrichmentOptions): Promise<Array<{
|
|
30
30
|
candidate: EnrichmentCandidate;
|
|
31
31
|
} & CanonicalEmail>>;
|
|
32
|
+
/**
|
|
33
|
+
* Enrich a candidate and collect ALL emails from ALL providers
|
|
34
|
+
*
|
|
35
|
+
* Unlike enrichBusinessEmail which stops at the first valid match,
|
|
36
|
+
* this function queries all providers (respecting budget) and returns
|
|
37
|
+
* all found emails with their scores and classifications.
|
|
38
|
+
*/
|
|
39
|
+
export declare function enrichAllEmails(candidate: EnrichmentCandidate, options: OrchestratorOptions): Promise<MultiEmailResult>;
|
|
40
|
+
/**
|
|
41
|
+
* Enrich multiple candidates and collect all emails (batch version)
|
|
42
|
+
*/
|
|
43
|
+
export declare function enrichAllBatch(candidates: EnrichmentCandidate[], options: OrchestratorOptions & BatchEnrichmentOptions): Promise<MultiEmailResult[]>;
|