linkedin-secret-sauce 0.8.0 → 0.10.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.
|
@@ -44,4 +44,4 @@ export { extractNumericLinkedInId } from "./providers/ldd";
|
|
|
44
44
|
export { createSmartProspectClient, type SmartProspectClient, type SmartProspectLocationOptions, } from "./providers/smartprospect";
|
|
45
45
|
export { enrichBusinessEmail, enrichBatch, enrichAllEmails, enrichAllBatch } from "./orchestrator";
|
|
46
46
|
export { getSmartLeadToken, getSmartLeadUser, clearSmartLeadToken, clearAllSmartLeadTokens, getSmartLeadTokenCacheStats, enableFileCache, disableFileCache, isFileCacheEnabled, clearFileCache, type SmartLeadCredentials, type SmartLeadAuthConfig, type SmartLeadUser, type SmartLeadLoginResponse, } from "./auth";
|
|
47
|
-
export { calculateMatchConfidence, classifyMatchQuality, findBestMatch, matchContacts, buildSmartProspectFiltersFromLinkedIn, parseLinkedInSearchResponse, enrichLinkedInContact, enrichLinkedInContactsBatch, createLinkedInEnricher, getEmailsForLinkedInContact, getEmailsForLinkedInContactsBatch, type LinkedInContact, type MatchResult, type MatchOptions, type LinkedInEnrichmentResult, type LinkedInEnrichmentOptions, type EmailResult, type GetEmailsResult, type GetEmailsConfig, type GetEmailsOptions, } from "./matching";
|
|
47
|
+
export { calculateMatchConfidence, classifyMatchQuality, findBestMatch, matchContacts, buildSmartProspectFiltersFromLinkedIn, parseLinkedInSearchResponse, enrichLinkedInContact, enrichLinkedInContactsBatch, createLinkedInEnricher, getEmailsForLinkedInContact, getEmailsForLinkedInContactsBatch, salesLeadToContact, type LinkedInContact, type MatchResult, type MatchOptions, type LinkedInEnrichmentResult, type LinkedInEnrichmentOptions, type EmailSource, type EmailResult, type GetEmailsResult, type GetEmailsConfig, type GetEmailsOptions, } from "./matching";
|
package/dist/enrichment/index.js
CHANGED
|
@@ -42,7 +42,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
42
42
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
43
43
|
};
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
-
exports.getEmailsForLinkedInContactsBatch = exports.getEmailsForLinkedInContact = exports.createLinkedInEnricher = exports.enrichLinkedInContactsBatch = exports.enrichLinkedInContact = exports.parseLinkedInSearchResponse = exports.buildSmartProspectFiltersFromLinkedIn = exports.matchContacts = exports.findBestMatch = exports.classifyMatchQuality = exports.calculateMatchConfidence = exports.clearFileCache = exports.isFileCacheEnabled = exports.disableFileCache = exports.enableFileCache = exports.getSmartLeadTokenCacheStats = exports.clearAllSmartLeadTokens = exports.clearSmartLeadToken = exports.getSmartLeadUser = exports.getSmartLeadToken = exports.enrichAllBatch = exports.enrichAllEmails = exports.enrichBatch = exports.enrichBusinessEmail = exports.createSmartProspectClient = exports.extractNumericLinkedInId = exports.createDropcontactProvider = exports.createApolloProvider = exports.createHunterProvider = exports.createSmartProspectProvider = exports.createLddProvider = exports.createConstructProvider = exports.verifyEmailMx = exports.extractLinkedInUsername = exports.hostnameFromUrl = exports.cleanNamePart = exports.asciiFold = exports.isRoleAccount = exports.isValidEmailSyntax = exports.DISPOSABLE_DOMAINS = exports.isDisposableDomain = exports.isDisposableEmail = exports.PERSONAL_DOMAINS = exports.isPersonalDomain = exports.isBusinessEmail = exports.isPersonalEmail = void 0;
|
|
45
|
+
exports.salesLeadToContact = exports.getEmailsForLinkedInContactsBatch = exports.getEmailsForLinkedInContact = exports.createLinkedInEnricher = exports.enrichLinkedInContactsBatch = exports.enrichLinkedInContact = exports.parseLinkedInSearchResponse = exports.buildSmartProspectFiltersFromLinkedIn = exports.matchContacts = exports.findBestMatch = exports.classifyMatchQuality = exports.calculateMatchConfidence = exports.clearFileCache = exports.isFileCacheEnabled = exports.disableFileCache = exports.enableFileCache = exports.getSmartLeadTokenCacheStats = exports.clearAllSmartLeadTokens = exports.clearSmartLeadToken = exports.getSmartLeadUser = exports.getSmartLeadToken = exports.enrichAllBatch = exports.enrichAllEmails = exports.enrichBatch = exports.enrichBusinessEmail = exports.createSmartProspectClient = exports.extractNumericLinkedInId = exports.createDropcontactProvider = exports.createApolloProvider = exports.createHunterProvider = exports.createSmartProspectProvider = exports.createLddProvider = exports.createConstructProvider = exports.verifyEmailMx = exports.extractLinkedInUsername = exports.hostnameFromUrl = exports.cleanNamePart = exports.asciiFold = exports.isRoleAccount = exports.isValidEmailSyntax = exports.DISPOSABLE_DOMAINS = exports.isDisposableDomain = exports.isDisposableEmail = exports.PERSONAL_DOMAINS = exports.isPersonalDomain = exports.isBusinessEmail = exports.isPersonalEmail = void 0;
|
|
46
46
|
exports.createEnrichmentClient = createEnrichmentClient;
|
|
47
47
|
const orchestrator_1 = require("./orchestrator");
|
|
48
48
|
const construct_1 = require("./providers/construct");
|
|
@@ -290,3 +290,5 @@ Object.defineProperty(exports, "createLinkedInEnricher", { enumerable: true, get
|
|
|
290
290
|
// UNIFIED email lookup (recommended for most use cases)
|
|
291
291
|
Object.defineProperty(exports, "getEmailsForLinkedInContact", { enumerable: true, get: function () { return matching_1.getEmailsForLinkedInContact; } });
|
|
292
292
|
Object.defineProperty(exports, "getEmailsForLinkedInContactsBatch", { enumerable: true, get: function () { return matching_1.getEmailsForLinkedInContactsBatch; } });
|
|
293
|
+
// Conversion utility for Sales Nav entities
|
|
294
|
+
Object.defineProperty(exports, "salesLeadToContact", { enumerable: true, get: function () { return matching_1.salesLeadToContact; } });
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* to find the same person across both platforms.
|
|
6
6
|
*/
|
|
7
7
|
import type { SmartProspectContact, SmartProspectConfig, SmartProspectFetchResponse, LddConfig } from './types';
|
|
8
|
+
import type { SalesLeadSearchResult } from '../types';
|
|
8
9
|
import { type SmartProspectClient } from './providers/smartprospect';
|
|
9
10
|
/**
|
|
10
11
|
* LinkedIn Sales Navigator contact (simplified from API response)
|
|
@@ -39,6 +40,21 @@ export interface LinkedInContact {
|
|
|
39
40
|
}>;
|
|
40
41
|
};
|
|
41
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Convert a Sales Navigator search result to LinkedInContact format.
|
|
45
|
+
* This allows consumers to pass raw Sales Nav entities directly.
|
|
46
|
+
*
|
|
47
|
+
* @param lead - Raw Sales Navigator lead search result
|
|
48
|
+
* @returns LinkedInContact compatible with email lookup functions
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* const searchResults = await searchSalesLeads('CEO', { filters });
|
|
53
|
+
* const contact = salesLeadToContact(searchResults.items[0]);
|
|
54
|
+
* const emails = await getEmailsForLinkedInContact(contact, config);
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export declare function salesLeadToContact(lead: SalesLeadSearchResult): LinkedInContact;
|
|
42
58
|
/**
|
|
43
59
|
* Match result with confidence score
|
|
44
60
|
*/
|
|
@@ -239,6 +255,10 @@ export declare function createLinkedInEnricher(smartProspectConfig: SmartProspec
|
|
|
239
255
|
/** Fetch email for a specific SmartProspect contact ID (COSTS CREDITS) */
|
|
240
256
|
fetchEmail: (contactId: string) => Promise<SmartProspectFetchResponse>;
|
|
241
257
|
} | null;
|
|
258
|
+
/**
|
|
259
|
+
* Email source - where the email was found
|
|
260
|
+
*/
|
|
261
|
+
export type EmailSource = 'ldd' | 'smartprospect' | 'linkedin' | 'pattern' | 'hunter' | 'apollo';
|
|
242
262
|
/**
|
|
243
263
|
* Email result from unified lookup
|
|
244
264
|
*/
|
|
@@ -246,7 +266,7 @@ export interface EmailResult {
|
|
|
246
266
|
/** The email address */
|
|
247
267
|
email: string;
|
|
248
268
|
/** Which provider found this email */
|
|
249
|
-
source:
|
|
269
|
+
source: EmailSource;
|
|
250
270
|
/** Confidence score (0-100) */
|
|
251
271
|
confidence: number;
|
|
252
272
|
/** Email type classification */
|
|
@@ -255,6 +275,8 @@ export interface EmailResult {
|
|
|
255
275
|
verified: boolean;
|
|
256
276
|
/** Email deliverability score (0-1) for SmartProspect emails */
|
|
257
277
|
deliverability?: number;
|
|
278
|
+
/** Whether this is a catch-all domain */
|
|
279
|
+
isCatchAll?: boolean;
|
|
258
280
|
/** Additional metadata */
|
|
259
281
|
metadata?: Record<string, unknown>;
|
|
260
282
|
}
|
|
@@ -269,18 +291,38 @@ export interface GetEmailsResult {
|
|
|
269
291
|
/** Numeric LinkedIn ID extracted from objectUrn */
|
|
270
292
|
numericLinkedInId: string | null;
|
|
271
293
|
/** Which providers were queried */
|
|
272
|
-
providersQueried:
|
|
273
|
-
/**
|
|
274
|
-
|
|
294
|
+
providersQueried: EmailSource[];
|
|
295
|
+
/** Company domain discovered during lookup (from SmartProspect if not in LinkedIn data) */
|
|
296
|
+
discoveredCompanyDomain?: string;
|
|
297
|
+
/** Error message if any provider failed */
|
|
298
|
+
errors?: string[];
|
|
275
299
|
}
|
|
276
300
|
/**
|
|
277
301
|
* Configuration for unified email lookup
|
|
278
302
|
*/
|
|
279
303
|
export interface GetEmailsConfig {
|
|
280
|
-
/** LDD configuration (
|
|
304
|
+
/** LDD configuration (FREE - your ~500M database) */
|
|
281
305
|
ldd?: LddConfig;
|
|
282
|
-
/** SmartProspect configuration (
|
|
306
|
+
/** SmartProspect configuration (FREE for FlexIQ - already paying monthly) */
|
|
283
307
|
smartprospect?: SmartProspectConfig;
|
|
308
|
+
/** Company domain for email pattern guessing (optional - if not provided, will try to discover) */
|
|
309
|
+
companyDomain?: string;
|
|
310
|
+
/**
|
|
311
|
+
* LinkedIn company lookup function (for fetching company website when not available).
|
|
312
|
+
* Pass getCompanyById from linkedin-api to enable this.
|
|
313
|
+
* This is a LAST RESORT since it uses LinkedIn API quota.
|
|
314
|
+
*/
|
|
315
|
+
linkedInCompanyLookup?: (companyId: string) => Promise<{
|
|
316
|
+
websiteUrl?: string;
|
|
317
|
+
} | null>;
|
|
318
|
+
/** Hunter configuration (PAID - last resort) */
|
|
319
|
+
hunter?: {
|
|
320
|
+
apiKey: string;
|
|
321
|
+
};
|
|
322
|
+
/** Apollo configuration (PAID - last resort) */
|
|
323
|
+
apollo?: {
|
|
324
|
+
apiKey: string;
|
|
325
|
+
};
|
|
284
326
|
}
|
|
285
327
|
/**
|
|
286
328
|
* Options for unified email lookup
|
|
@@ -290,19 +332,26 @@ export interface GetEmailsOptions {
|
|
|
290
332
|
skipLdd?: boolean;
|
|
291
333
|
/** Skip SmartProspect lookup (default: false) */
|
|
292
334
|
skipSmartProspect?: boolean;
|
|
335
|
+
/** Skip email pattern guessing (default: false) */
|
|
336
|
+
skipPatternGuessing?: boolean;
|
|
337
|
+
/** Skip LinkedIn company lookup for domain discovery (default: false) */
|
|
338
|
+
skipLinkedInCompanyLookup?: boolean;
|
|
339
|
+
/** Skip paid providers Hunter/Apollo (default: false) */
|
|
340
|
+
skipPaidProviders?: boolean;
|
|
293
341
|
/** Minimum match confidence for SmartProspect (default: 60) */
|
|
294
342
|
minMatchConfidence?: number;
|
|
343
|
+
/** Minimum confidence to skip paid providers (default: 80) */
|
|
344
|
+
paidProviderThreshold?: number;
|
|
295
345
|
/** Include company in SmartProspect search (default: true) */
|
|
296
346
|
includeCompany?: boolean;
|
|
297
347
|
}
|
|
298
348
|
/**
|
|
299
349
|
* Get emails for a LinkedIn contact - UNIFIED FUNCTION
|
|
300
350
|
*
|
|
301
|
-
*
|
|
302
|
-
* 1.
|
|
303
|
-
* 2.
|
|
304
|
-
* 3.
|
|
305
|
-
* 4. Returns ALL emails with unified scoring
|
|
351
|
+
* Strategy (optimized for FlexIQ):
|
|
352
|
+
* 1. Query LDD + SmartProspect in PARALLEL (both FREE for you)
|
|
353
|
+
* 2. Try email pattern guessing with MX verification (FREE)
|
|
354
|
+
* 3. Only use Hunter/Apollo if confidence is below threshold (PAID - last resort)
|
|
306
355
|
*
|
|
307
356
|
* @example
|
|
308
357
|
* ```ts
|
|
@@ -310,10 +359,13 @@ export interface GetEmailsOptions {
|
|
|
310
359
|
*
|
|
311
360
|
* // From your LinkedIn Sales Nav search result
|
|
312
361
|
* const contact = {
|
|
313
|
-
* objectUrn: 'urn:li:member:307567', //
|
|
362
|
+
* objectUrn: 'urn:li:member:307567', // Stable ID for LDD
|
|
314
363
|
* firstName: 'Jim',
|
|
315
364
|
* lastName: 'DeMaio',
|
|
316
|
-
* currentPositions: [{
|
|
365
|
+
* currentPositions: [{
|
|
366
|
+
* companyName: 'Acme Corp',
|
|
367
|
+
* companyUrnResolutionResult: { website: 'acme.com' }
|
|
368
|
+
* }]
|
|
317
369
|
* };
|
|
318
370
|
*
|
|
319
371
|
* const result = await getEmailsForLinkedInContact(contact, {
|
|
@@ -325,26 +377,31 @@ export interface GetEmailsOptions {
|
|
|
325
377
|
* email: process.env.SMARTLEAD_EMAIL,
|
|
326
378
|
* password: process.env.SMARTLEAD_PASSWORD,
|
|
327
379
|
* },
|
|
380
|
+
* // companyDomain extracted automatically from currentPositions
|
|
328
381
|
* });
|
|
329
382
|
*
|
|
330
383
|
* if (result.success) {
|
|
384
|
+
* console.log('Providers queried:', result.providersQueried);
|
|
331
385
|
* console.log('Found emails:', result.emails);
|
|
332
386
|
* // [
|
|
333
387
|
* // { email: 'jim@acme.com', source: 'ldd', confidence: 95, type: 'business' },
|
|
334
|
-
* // { email: '
|
|
388
|
+
* // { email: 'j.demaio@acme.com', source: 'smartprospect', confidence: 85, type: 'business' },
|
|
335
389
|
* // ]
|
|
336
390
|
* }
|
|
337
391
|
* ```
|
|
338
392
|
*/
|
|
339
|
-
export declare function getEmailsForLinkedInContact(
|
|
393
|
+
export declare function getEmailsForLinkedInContact(contactOrLead: LinkedInContact | SalesLeadSearchResult, config: GetEmailsConfig, options?: GetEmailsOptions): Promise<GetEmailsResult>;
|
|
340
394
|
/**
|
|
341
395
|
* Get emails for multiple LinkedIn contacts in batch
|
|
342
396
|
*
|
|
343
|
-
* Processes contacts
|
|
397
|
+
* Processes contacts with controlled concurrency to balance speed and rate limits.
|
|
398
|
+
* Accepts either LinkedInContact[] or SalesLeadSearchResult[] directly from Sales Nav search.
|
|
344
399
|
*/
|
|
345
|
-
export declare function getEmailsForLinkedInContactsBatch(
|
|
346
|
-
/** Delay between requests in ms (default:
|
|
400
|
+
export declare function getEmailsForLinkedInContactsBatch(contactsOrLeads: (LinkedInContact | SalesLeadSearchResult)[], config: GetEmailsConfig, options?: GetEmailsOptions & {
|
|
401
|
+
/** Delay between requests in ms (default: 100) */
|
|
347
402
|
delayMs?: number;
|
|
403
|
+
/** Max concurrent lookups (default: 3) */
|
|
404
|
+
concurrency?: number;
|
|
348
405
|
/** Callback for progress updates */
|
|
349
406
|
onProgress?: (completed: number, total: number, result: GetEmailsResult) => void;
|
|
350
407
|
}): Promise<GetEmailsResult[]>;
|
|
@@ -5,7 +5,41 @@
|
|
|
5
5
|
* Matches contacts between LinkedIn Sales Navigator and SmartProspect
|
|
6
6
|
* to find the same person across both platforms.
|
|
7
7
|
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
8
41
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.salesLeadToContact = salesLeadToContact;
|
|
9
43
|
exports.calculateMatchConfidence = calculateMatchConfidence;
|
|
10
44
|
exports.classifyMatchQuality = classifyMatchQuality;
|
|
11
45
|
exports.findBestMatch = findBestMatch;
|
|
@@ -19,6 +53,64 @@ exports.getEmailsForLinkedInContact = getEmailsForLinkedInContact;
|
|
|
19
53
|
exports.getEmailsForLinkedInContactsBatch = getEmailsForLinkedInContactsBatch;
|
|
20
54
|
const smartprospect_1 = require("./providers/smartprospect");
|
|
21
55
|
const ldd_1 = require("./providers/ldd");
|
|
56
|
+
/**
|
|
57
|
+
* Convert a Sales Navigator search result to LinkedInContact format.
|
|
58
|
+
* This allows consumers to pass raw Sales Nav entities directly.
|
|
59
|
+
*
|
|
60
|
+
* @param lead - Raw Sales Navigator lead search result
|
|
61
|
+
* @returns LinkedInContact compatible with email lookup functions
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* const searchResults = await searchSalesLeads('CEO', { filters });
|
|
66
|
+
* const contact = salesLeadToContact(searchResults.items[0]);
|
|
67
|
+
* const emails = await getEmailsForLinkedInContact(contact, config);
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
function salesLeadToContact(lead) {
|
|
71
|
+
// Use firstName/lastName directly from Sales Nav - they're already separate fields
|
|
72
|
+
// This preserves exact names as shown on LinkedIn (important for SmartProspect matching)
|
|
73
|
+
const firstName = lead.firstName || '';
|
|
74
|
+
const lastName = lead.lastName || '';
|
|
75
|
+
return {
|
|
76
|
+
objectUrn: lead.objectUrn,
|
|
77
|
+
entityUrn: lead.salesProfileUrn,
|
|
78
|
+
firstName,
|
|
79
|
+
lastName,
|
|
80
|
+
fullName: lead.fullName || lead.name,
|
|
81
|
+
geoRegion: lead.geoRegion || lead.locationText,
|
|
82
|
+
currentPositions: lead.currentPositions?.map((pos) => ({
|
|
83
|
+
companyName: pos.companyName,
|
|
84
|
+
title: pos.title,
|
|
85
|
+
companyUrn: pos.companyUrn,
|
|
86
|
+
// Note: Sales Nav search does NOT return website in companyUrnResolutionResult
|
|
87
|
+
// We use LinkedIn Company Lookup to get website from companyUrn
|
|
88
|
+
companyUrnResolutionResult: pos.companyUrnResolutionResult
|
|
89
|
+
? {
|
|
90
|
+
name: pos.companyUrnResolutionResult.name,
|
|
91
|
+
industry: pos.companyUrnResolutionResult.industry,
|
|
92
|
+
location: pos.companyUrnResolutionResult.location,
|
|
93
|
+
}
|
|
94
|
+
: undefined,
|
|
95
|
+
})),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Type guard to check if input is a SalesLeadSearchResult
|
|
100
|
+
*/
|
|
101
|
+
function isSalesLeadSearchResult(input) {
|
|
102
|
+
// SalesLeadSearchResult has 'name' but not 'firstName'
|
|
103
|
+
return 'name' in input && !('firstName' in input);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Normalize input to LinkedInContact - accepts either format
|
|
107
|
+
*/
|
|
108
|
+
function normalizeToContact(input) {
|
|
109
|
+
if (isSalesLeadSearchResult(input)) {
|
|
110
|
+
return salesLeadToContact(input);
|
|
111
|
+
}
|
|
112
|
+
return input;
|
|
113
|
+
}
|
|
22
114
|
// =============================================================================
|
|
23
115
|
// Utility Functions
|
|
24
116
|
// =============================================================================
|
|
@@ -629,11 +721,10 @@ function createLinkedInEnricher(smartProspectConfig, defaultOptions = {}) {
|
|
|
629
721
|
/**
|
|
630
722
|
* Get emails for a LinkedIn contact - UNIFIED FUNCTION
|
|
631
723
|
*
|
|
632
|
-
*
|
|
633
|
-
* 1.
|
|
634
|
-
* 2.
|
|
635
|
-
* 3.
|
|
636
|
-
* 4. Returns ALL emails with unified scoring
|
|
724
|
+
* Strategy (optimized for FlexIQ):
|
|
725
|
+
* 1. Query LDD + SmartProspect in PARALLEL (both FREE for you)
|
|
726
|
+
* 2. Try email pattern guessing with MX verification (FREE)
|
|
727
|
+
* 3. Only use Hunter/Apollo if confidence is below threshold (PAID - last resort)
|
|
637
728
|
*
|
|
638
729
|
* @example
|
|
639
730
|
* ```ts
|
|
@@ -641,10 +732,13 @@ function createLinkedInEnricher(smartProspectConfig, defaultOptions = {}) {
|
|
|
641
732
|
*
|
|
642
733
|
* // From your LinkedIn Sales Nav search result
|
|
643
734
|
* const contact = {
|
|
644
|
-
* objectUrn: 'urn:li:member:307567', //
|
|
735
|
+
* objectUrn: 'urn:li:member:307567', // Stable ID for LDD
|
|
645
736
|
* firstName: 'Jim',
|
|
646
737
|
* lastName: 'DeMaio',
|
|
647
|
-
* currentPositions: [{
|
|
738
|
+
* currentPositions: [{
|
|
739
|
+
* companyName: 'Acme Corp',
|
|
740
|
+
* companyUrnResolutionResult: { website: 'acme.com' }
|
|
741
|
+
* }]
|
|
648
742
|
* };
|
|
649
743
|
*
|
|
650
744
|
* const result = await getEmailsForLinkedInContact(contact, {
|
|
@@ -656,164 +750,366 @@ function createLinkedInEnricher(smartProspectConfig, defaultOptions = {}) {
|
|
|
656
750
|
* email: process.env.SMARTLEAD_EMAIL,
|
|
657
751
|
* password: process.env.SMARTLEAD_PASSWORD,
|
|
658
752
|
* },
|
|
753
|
+
* // companyDomain extracted automatically from currentPositions
|
|
659
754
|
* });
|
|
660
755
|
*
|
|
661
756
|
* if (result.success) {
|
|
757
|
+
* console.log('Providers queried:', result.providersQueried);
|
|
662
758
|
* console.log('Found emails:', result.emails);
|
|
663
759
|
* // [
|
|
664
760
|
* // { email: 'jim@acme.com', source: 'ldd', confidence: 95, type: 'business' },
|
|
665
|
-
* // { email: '
|
|
761
|
+
* // { email: 'j.demaio@acme.com', source: 'smartprospect', confidence: 85, type: 'business' },
|
|
666
762
|
* // ]
|
|
667
763
|
* }
|
|
668
764
|
* ```
|
|
669
765
|
*/
|
|
670
|
-
async function getEmailsForLinkedInContact(
|
|
671
|
-
|
|
766
|
+
async function getEmailsForLinkedInContact(contactOrLead, config, options = {}) {
|
|
767
|
+
// Normalize input - accept either LinkedInContact or raw SalesLeadSearchResult
|
|
768
|
+
const contact = normalizeToContact(contactOrLead);
|
|
769
|
+
const { skipLdd = false, skipSmartProspect = false, skipPatternGuessing = false, skipPaidProviders = false, minMatchConfidence = 60, paidProviderThreshold = 80, includeCompany = true, } = options;
|
|
672
770
|
// Extract numeric ID from objectUrn
|
|
673
771
|
const numericLinkedInId = (0, ldd_1.extractNumericLinkedInId)(contact.objectUrn);
|
|
772
|
+
// Extract company domain from contact (LinkedIn data - often missing)
|
|
773
|
+
const linkedInCompanyDomain = config.companyDomain || extractCompanyDomain(contact);
|
|
674
774
|
const result = {
|
|
675
775
|
success: false,
|
|
676
776
|
emails: [],
|
|
677
777
|
numericLinkedInId,
|
|
678
778
|
providersQueried: [],
|
|
779
|
+
errors: [],
|
|
780
|
+
};
|
|
781
|
+
// Track seen emails to deduplicate
|
|
782
|
+
const seenEmails = new Set();
|
|
783
|
+
const addEmail = (emailResult) => {
|
|
784
|
+
const lower = emailResult.email.toLowerCase();
|
|
785
|
+
if (!seenEmails.has(lower)) {
|
|
786
|
+
seenEmails.add(lower);
|
|
787
|
+
result.emails.push(emailResult);
|
|
788
|
+
}
|
|
679
789
|
};
|
|
790
|
+
// Track company domain discovered from SmartProspect (since LinkedIn doesn't provide it)
|
|
791
|
+
let discoveredCompanyDomain = null;
|
|
680
792
|
// ==========================================================================
|
|
681
|
-
//
|
|
793
|
+
// Phase 1: FREE providers in PARALLEL (LDD + SmartProspect)
|
|
682
794
|
// ==========================================================================
|
|
795
|
+
const freeProviderPromises = [];
|
|
796
|
+
// LDD lookup (FREE)
|
|
683
797
|
if (!skipLdd && config.ldd?.apiUrl && config.ldd?.apiToken) {
|
|
684
798
|
result.providersQueried.push('ldd');
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
if (
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
799
|
+
freeProviderPromises.push(queryLdd(contact, config.ldd, numericLinkedInId, addEmail, result));
|
|
800
|
+
}
|
|
801
|
+
// SmartProspect lookup (FREE for FlexIQ)
|
|
802
|
+
// This also extracts company domain for pattern guessing
|
|
803
|
+
if (!skipSmartProspect && config.smartprospect) {
|
|
804
|
+
result.providersQueried.push('smartprospect');
|
|
805
|
+
freeProviderPromises.push(querySmartProspect(contact, config.smartprospect, minMatchConfidence, includeCompany, addEmail, result)
|
|
806
|
+
.then((domain) => {
|
|
807
|
+
if (domain) {
|
|
808
|
+
discoveredCompanyDomain = domain;
|
|
809
|
+
}
|
|
810
|
+
}));
|
|
811
|
+
}
|
|
812
|
+
// Wait for both free providers
|
|
813
|
+
await Promise.all(freeProviderPromises);
|
|
814
|
+
// Check if we have good enough results already
|
|
815
|
+
const bestConfidenceAfterFreeProviders = result.emails.length > 0
|
|
816
|
+
? Math.max(...result.emails.map(e => e.confidence))
|
|
817
|
+
: 0;
|
|
818
|
+
// ==========================================================================
|
|
819
|
+
// Phase 2: Domain Discovery (if needed for pattern guessing)
|
|
820
|
+
// ==========================================================================
|
|
821
|
+
// Priority: LinkedIn contact data > SmartProspect > LinkedIn Company API
|
|
822
|
+
let companyDomain = linkedInCompanyDomain || discoveredCompanyDomain;
|
|
823
|
+
// If no domain yet and pattern guessing is enabled, try LinkedIn company lookup
|
|
824
|
+
if (!companyDomain &&
|
|
825
|
+
!skipPatternGuessing &&
|
|
826
|
+
!options.skipLinkedInCompanyLookup &&
|
|
827
|
+
config.linkedInCompanyLookup &&
|
|
828
|
+
bestConfidenceAfterFreeProviders < paidProviderThreshold) {
|
|
829
|
+
// Extract company ID from companyUrn
|
|
830
|
+
const companyUrn = contact.currentPositions?.[0]?.companyUrn;
|
|
831
|
+
if (companyUrn) {
|
|
832
|
+
const companyId = extractCompanyIdFromUrn(companyUrn);
|
|
833
|
+
if (companyId) {
|
|
834
|
+
result.providersQueried.push('linkedin');
|
|
835
|
+
try {
|
|
836
|
+
const company = await config.linkedInCompanyLookup(companyId);
|
|
837
|
+
if (company?.websiteUrl) {
|
|
838
|
+
companyDomain = extractDomainFromUrl(company.websiteUrl);
|
|
839
|
+
if (companyDomain) {
|
|
840
|
+
discoveredCompanyDomain = companyDomain;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
catch (err) {
|
|
845
|
+
result.errors?.push(`LinkedIn: ${err instanceof Error ? err.message : 'Company lookup failed'}`);
|
|
704
846
|
}
|
|
705
|
-
result.success = true;
|
|
706
847
|
}
|
|
707
848
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
849
|
+
}
|
|
850
|
+
// Store discovered domain in result for visibility
|
|
851
|
+
if (discoveredCompanyDomain && !linkedInCompanyDomain) {
|
|
852
|
+
result.discoveredCompanyDomain = discoveredCompanyDomain;
|
|
712
853
|
}
|
|
713
854
|
// ==========================================================================
|
|
714
|
-
//
|
|
855
|
+
// Phase 3: Email pattern guessing with MX verification (FREE)
|
|
715
856
|
// ==========================================================================
|
|
716
|
-
if (!
|
|
717
|
-
result.providersQueried.push('
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
857
|
+
if (!skipPatternGuessing && companyDomain && bestConfidenceAfterFreeProviders < paidProviderThreshold) {
|
|
858
|
+
result.providersQueried.push('pattern');
|
|
859
|
+
await queryPatternGuessing(contact, companyDomain, addEmail, result);
|
|
860
|
+
}
|
|
861
|
+
// Recalculate best confidence
|
|
862
|
+
const finalBestConfidence = result.emails.length > 0
|
|
863
|
+
? Math.max(...result.emails.map(e => e.confidence))
|
|
864
|
+
: 0;
|
|
865
|
+
// ==========================================================================
|
|
866
|
+
// Phase 3: PAID providers as last resort (Hunter/Apollo)
|
|
867
|
+
// ==========================================================================
|
|
868
|
+
if (!skipPaidProviders && finalBestConfidence < paidProviderThreshold) {
|
|
869
|
+
// Only use paid providers if we have low confidence or no results
|
|
870
|
+
// TODO: Implement Hunter and Apollo providers when needed
|
|
871
|
+
// For now, just mark that we would have queried them
|
|
872
|
+
if (config.hunter?.apiKey || config.apollo?.apiKey) {
|
|
873
|
+
// result.providersQueried.push('hunter');
|
|
874
|
+
// result.providersQueried.push('apollo');
|
|
875
|
+
// await queryPaidProviders(contact, config, addEmail, result);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
// Sort emails by confidence (highest first), then by source priority
|
|
879
|
+
const sourcePriority = {
|
|
880
|
+
ldd: 0, // Highest priority - your own data
|
|
881
|
+
smartprospect: 1,
|
|
882
|
+
linkedin: 2, // LinkedIn company lookup (for domain discovery, doesn't provide emails)
|
|
883
|
+
pattern: 3,
|
|
884
|
+
hunter: 4,
|
|
885
|
+
apollo: 5,
|
|
886
|
+
};
|
|
887
|
+
result.emails.sort((a, b) => {
|
|
888
|
+
if (b.confidence !== a.confidence) {
|
|
889
|
+
return b.confidence - a.confidence;
|
|
890
|
+
}
|
|
891
|
+
return sourcePriority[a.source] - sourcePriority[b.source];
|
|
892
|
+
});
|
|
893
|
+
result.success = result.emails.length > 0;
|
|
894
|
+
// Clean up errors array if empty
|
|
895
|
+
if (result.errors && result.errors.length === 0) {
|
|
896
|
+
delete result.errors;
|
|
897
|
+
}
|
|
898
|
+
return result;
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Extract company domain from LinkedIn contact
|
|
902
|
+
*/
|
|
903
|
+
function extractCompanyDomain(contact) {
|
|
904
|
+
const pos = contact.currentPositions?.[0];
|
|
905
|
+
if (!pos)
|
|
906
|
+
return null;
|
|
907
|
+
// Try to get website from company resolution
|
|
908
|
+
const website = pos.companyUrnResolutionResult?.website;
|
|
909
|
+
if (website) {
|
|
910
|
+
return extractDomainFromUrl(website);
|
|
911
|
+
}
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Extract domain from a URL string
|
|
916
|
+
*/
|
|
917
|
+
function extractDomainFromUrl(url) {
|
|
918
|
+
if (!url)
|
|
919
|
+
return null;
|
|
920
|
+
try {
|
|
921
|
+
const fullUrl = url.startsWith('http') ? url : `https://${url}`;
|
|
922
|
+
return new URL(fullUrl).hostname.replace(/^www\./, '').toLowerCase();
|
|
923
|
+
}
|
|
924
|
+
catch {
|
|
925
|
+
// If URL parsing fails, try simple extraction
|
|
926
|
+
return url.replace(/^(https?:\/\/)?(www\.)?/, '').split('/')[0].toLowerCase() || null;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Extract company ID from a Sales Navigator company URN
|
|
931
|
+
* e.g., "urn:li:fs_salesCompany:108063139" → "108063139"
|
|
932
|
+
*/
|
|
933
|
+
function extractCompanyIdFromUrn(companyUrn) {
|
|
934
|
+
if (!companyUrn)
|
|
935
|
+
return null;
|
|
936
|
+
// Handle various URN formats
|
|
937
|
+
const match = companyUrn.match(/(\d+)$/);
|
|
938
|
+
return match ? match[1] : null;
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Query LDD provider
|
|
942
|
+
*/
|
|
943
|
+
async function queryLdd(contact, lddConfig, numericLinkedInId, addEmail, result) {
|
|
944
|
+
try {
|
|
945
|
+
const lddProvider = (0, ldd_1.createLddProvider)(lddConfig);
|
|
946
|
+
const lddResult = await lddProvider({
|
|
947
|
+
numericLinkedInId: numericLinkedInId || undefined,
|
|
948
|
+
firstName: contact.firstName,
|
|
949
|
+
lastName: contact.lastName,
|
|
950
|
+
});
|
|
951
|
+
if (lddResult && 'emails' in lddResult && lddResult.emails.length > 0) {
|
|
952
|
+
for (const emailData of lddResult.emails) {
|
|
953
|
+
addEmail({
|
|
954
|
+
email: emailData.email,
|
|
955
|
+
source: 'ldd',
|
|
956
|
+
confidence: emailData.confidence ?? 90,
|
|
957
|
+
type: emailData.metadata?.emailTypeClassified || 'unknown',
|
|
958
|
+
verified: emailData.verified ?? true,
|
|
959
|
+
metadata: emailData.metadata,
|
|
960
|
+
});
|
|
723
961
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
catch (err) {
|
|
965
|
+
result.errors?.push(`LDD: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Query SmartProspect provider
|
|
970
|
+
* Returns the company domain if found (for pattern guessing fallback)
|
|
971
|
+
*/
|
|
972
|
+
async function querySmartProspect(contact, smartProspectConfig, minMatchConfidence, includeCompany, addEmail, result) {
|
|
973
|
+
try {
|
|
974
|
+
const client = (0, smartprospect_1.createSmartProspectClient)(smartProspectConfig);
|
|
975
|
+
if (!client) {
|
|
976
|
+
result.errors?.push('SmartProspect: Failed to create client');
|
|
977
|
+
return null;
|
|
978
|
+
}
|
|
979
|
+
// Build search filters
|
|
980
|
+
const filters = buildSmartProspectFiltersFromLinkedIn(contact);
|
|
981
|
+
const searchFilters = {
|
|
982
|
+
firstName: filters.firstName,
|
|
983
|
+
lastName: filters.lastName,
|
|
984
|
+
limit: 25,
|
|
985
|
+
};
|
|
986
|
+
if (includeCompany && filters.companyName) {
|
|
987
|
+
searchFilters.companyName = filters.companyName;
|
|
988
|
+
}
|
|
989
|
+
// Search for matching contacts
|
|
990
|
+
let searchResponse = await client.search(searchFilters);
|
|
991
|
+
// Try broader search if no results
|
|
992
|
+
if (!searchResponse.success || searchResponse.data.list.length === 0) {
|
|
731
993
|
if (includeCompany && filters.companyName) {
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
const broaderResponse = await client.search({
|
|
740
|
-
firstName: filters.firstName,
|
|
741
|
-
lastName: filters.lastName,
|
|
742
|
-
limit: 25,
|
|
743
|
-
});
|
|
744
|
-
if (broaderResponse.success && broaderResponse.data.list.length > 0) {
|
|
745
|
-
searchResponse.data.list = broaderResponse.data.list;
|
|
746
|
-
}
|
|
994
|
+
const broaderResponse = await client.search({
|
|
995
|
+
firstName: filters.firstName,
|
|
996
|
+
lastName: filters.lastName,
|
|
997
|
+
limit: 25,
|
|
998
|
+
});
|
|
999
|
+
if (broaderResponse.success && broaderResponse.data.list.length > 0) {
|
|
1000
|
+
searchResponse = broaderResponse;
|
|
747
1001
|
}
|
|
748
1002
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
1003
|
+
}
|
|
1004
|
+
if (searchResponse.data.list.length === 0) {
|
|
1005
|
+
return null; // No candidates found, but not an error
|
|
1006
|
+
}
|
|
1007
|
+
// Find best match
|
|
1008
|
+
const matchResult = findBestMatch(contact, searchResponse.data.list, {
|
|
1009
|
+
minConfidence: minMatchConfidence,
|
|
1010
|
+
fuzzyNames: true,
|
|
1011
|
+
fuzzyCompany: true,
|
|
1012
|
+
});
|
|
1013
|
+
if (!matchResult) {
|
|
1014
|
+
return null; // No good match, but not an error
|
|
1015
|
+
}
|
|
1016
|
+
// Extract company domain from matched contact (SmartProspect provides this!)
|
|
1017
|
+
const companyWebsite = matchResult.smartProspectContact.company?.website;
|
|
1018
|
+
let discoveredDomain = null;
|
|
1019
|
+
if (companyWebsite) {
|
|
1020
|
+
// Clean up the domain (remove protocol, www, trailing slashes)
|
|
1021
|
+
discoveredDomain = companyWebsite
|
|
1022
|
+
.replace(/^(https?:\/\/)?(www\.)?/, '')
|
|
1023
|
+
.split('/')[0]
|
|
1024
|
+
.toLowerCase();
|
|
1025
|
+
}
|
|
1026
|
+
// Fetch email for matched contact
|
|
1027
|
+
const fetchResponse = await client.fetch([matchResult.smartProspectContact.id]);
|
|
1028
|
+
if (fetchResponse.success && fetchResponse.data.list.length > 0) {
|
|
1029
|
+
const enrichedContact = fetchResponse.data.list[0];
|
|
1030
|
+
if (enrichedContact.email) {
|
|
1031
|
+
addEmail({
|
|
1032
|
+
email: enrichedContact.email,
|
|
1033
|
+
source: 'smartprospect',
|
|
1034
|
+
confidence: matchResult.confidence,
|
|
1035
|
+
type: 'business',
|
|
1036
|
+
verified: enrichedContact.verificationStatus === 'verified',
|
|
1037
|
+
deliverability: enrichedContact.emailDeliverability,
|
|
1038
|
+
metadata: {
|
|
1039
|
+
matchQuality: matchResult.quality,
|
|
1040
|
+
matchedFields: matchResult.matchedFields,
|
|
1041
|
+
smartProspectId: enrichedContact.id,
|
|
1042
|
+
company: enrichedContact.company?.name,
|
|
1043
|
+
companyWebsite: companyWebsite,
|
|
1044
|
+
title: enrichedContact.title,
|
|
1045
|
+
},
|
|
1046
|
+
});
|
|
789
1047
|
}
|
|
790
1048
|
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
1049
|
+
return discoveredDomain;
|
|
1050
|
+
}
|
|
1051
|
+
catch (err) {
|
|
1052
|
+
result.errors?.push(`SmartProspect: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
1053
|
+
return null;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Query pattern guessing with MX verification
|
|
1058
|
+
*/
|
|
1059
|
+
async function queryPatternGuessing(contact, companyDomain, addEmail, result) {
|
|
1060
|
+
try {
|
|
1061
|
+
// Import construct provider dynamically to avoid circular deps
|
|
1062
|
+
const { createConstructProvider } = await Promise.resolve().then(() => __importStar(require('./providers/construct')));
|
|
1063
|
+
const constructProvider = createConstructProvider({ maxAttempts: 6, timeoutMs: 3000 });
|
|
1064
|
+
const constructResult = await constructProvider({
|
|
1065
|
+
firstName: contact.firstName,
|
|
1066
|
+
lastName: contact.lastName,
|
|
1067
|
+
domain: companyDomain,
|
|
1068
|
+
});
|
|
1069
|
+
if (constructResult && 'emails' in constructResult && constructResult.emails.length > 0) {
|
|
1070
|
+
for (const emailData of constructResult.emails) {
|
|
1071
|
+
addEmail({
|
|
1072
|
+
email: emailData.email,
|
|
1073
|
+
source: 'pattern',
|
|
1074
|
+
confidence: emailData.confidence ?? 50,
|
|
1075
|
+
type: 'business',
|
|
1076
|
+
verified: emailData.verified ?? false,
|
|
1077
|
+
isCatchAll: emailData.isCatchAll,
|
|
1078
|
+
metadata: emailData.metadata,
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
795
1081
|
}
|
|
796
1082
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
1083
|
+
catch (err) {
|
|
1084
|
+
result.errors?.push(`Pattern: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
1085
|
+
}
|
|
800
1086
|
}
|
|
801
1087
|
/**
|
|
802
1088
|
* Get emails for multiple LinkedIn contacts in batch
|
|
803
1089
|
*
|
|
804
|
-
* Processes contacts
|
|
1090
|
+
* Processes contacts with controlled concurrency to balance speed and rate limits.
|
|
1091
|
+
* Accepts either LinkedInContact[] or SalesLeadSearchResult[] directly from Sales Nav search.
|
|
805
1092
|
*/
|
|
806
|
-
async function getEmailsForLinkedInContactsBatch(
|
|
807
|
-
const { delayMs =
|
|
808
|
-
const results =
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
1093
|
+
async function getEmailsForLinkedInContactsBatch(contactsOrLeads, config, options = {}) {
|
|
1094
|
+
const { delayMs = 100, concurrency = 3, onProgress, ...lookupOptions } = options;
|
|
1095
|
+
const results = new Array(contactsOrLeads.length);
|
|
1096
|
+
let completed = 0;
|
|
1097
|
+
// Process in batches with concurrency limit
|
|
1098
|
+
for (let i = 0; i < contactsOrLeads.length; i += concurrency) {
|
|
1099
|
+
const batch = contactsOrLeads.slice(i, i + concurrency);
|
|
1100
|
+
const batchPromises = batch.map(async (contactOrLead, batchIndex) => {
|
|
1101
|
+
const result = await getEmailsForLinkedInContact(contactOrLead, config, lookupOptions);
|
|
1102
|
+
const globalIndex = i + batchIndex;
|
|
1103
|
+
results[globalIndex] = result;
|
|
1104
|
+
completed++;
|
|
1105
|
+
if (onProgress) {
|
|
1106
|
+
onProgress(completed, contactsOrLeads.length, result);
|
|
1107
|
+
}
|
|
1108
|
+
return result;
|
|
1109
|
+
});
|
|
1110
|
+
await Promise.all(batchPromises);
|
|
1111
|
+
// Delay between batches (except for last batch)
|
|
1112
|
+
if (i + concurrency < contactsOrLeads.length && delayMs > 0) {
|
|
817
1113
|
await new Promise((r) => setTimeout(r, delayMs));
|
|
818
1114
|
}
|
|
819
1115
|
}
|
|
@@ -42,7 +42,7 @@ function parseCompany(raw) {
|
|
|
42
42
|
data?.universalName ||
|
|
43
43
|
r?.universalName),
|
|
44
44
|
name: (miniCompany?.name || data?.name || r?.name),
|
|
45
|
-
websiteUrl: (data?.websiteUrl || r?.websiteUrl),
|
|
45
|
+
websiteUrl: (data?.websiteUrl || data?.website || r?.websiteUrl || r?.website),
|
|
46
46
|
description: (data?.description || r?.description),
|
|
47
47
|
sizeLabel: (typeof data?.employeeCountRange === "string"
|
|
48
48
|
? data.employeeCountRange
|
package/dist/types.d.ts
CHANGED