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.
@@ -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[]>;