linkedin-secret-sauce 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/enrichment/index.js +3 -1
- package/dist/enrichment/matching.d.ts +33 -4
- package/dist/enrichment/matching.js +171 -32
- package/dist/enrichment/providers/construct.js +20 -8
- package/dist/parsers/company-parser.js +1 -1
- package/dist/types.d.ts +3 -0
- package/package.json +1 -1
|
@@ -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 EmailSource, 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
|
*/
|
|
@@ -242,7 +258,7 @@ export declare function createLinkedInEnricher(smartProspectConfig: SmartProspec
|
|
|
242
258
|
/**
|
|
243
259
|
* Email source - where the email was found
|
|
244
260
|
*/
|
|
245
|
-
export type EmailSource = 'ldd' | 'smartprospect' | 'pattern' | 'hunter' | 'apollo';
|
|
261
|
+
export type EmailSource = 'ldd' | 'smartprospect' | 'linkedin' | 'pattern' | 'hunter' | 'apollo';
|
|
246
262
|
/**
|
|
247
263
|
* Email result from unified lookup
|
|
248
264
|
*/
|
|
@@ -276,6 +292,8 @@ export interface GetEmailsResult {
|
|
|
276
292
|
numericLinkedInId: string | null;
|
|
277
293
|
/** Which providers were queried */
|
|
278
294
|
providersQueried: EmailSource[];
|
|
295
|
+
/** Company domain discovered during lookup (from SmartProspect if not in LinkedIn data) */
|
|
296
|
+
discoveredCompanyDomain?: string;
|
|
279
297
|
/** Error message if any provider failed */
|
|
280
298
|
errors?: string[];
|
|
281
299
|
}
|
|
@@ -287,8 +305,16 @@ export interface GetEmailsConfig {
|
|
|
287
305
|
ldd?: LddConfig;
|
|
288
306
|
/** SmartProspect configuration (FREE for FlexIQ - already paying monthly) */
|
|
289
307
|
smartprospect?: SmartProspectConfig;
|
|
290
|
-
/** Company domain for email pattern guessing (optional) */
|
|
308
|
+
/** Company domain for email pattern guessing (optional - if not provided, will try to discover) */
|
|
291
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>;
|
|
292
318
|
/** Hunter configuration (PAID - last resort) */
|
|
293
319
|
hunter?: {
|
|
294
320
|
apiKey: string;
|
|
@@ -308,6 +334,8 @@ export interface GetEmailsOptions {
|
|
|
308
334
|
skipSmartProspect?: boolean;
|
|
309
335
|
/** Skip email pattern guessing (default: false) */
|
|
310
336
|
skipPatternGuessing?: boolean;
|
|
337
|
+
/** Skip LinkedIn company lookup for domain discovery (default: false) */
|
|
338
|
+
skipLinkedInCompanyLookup?: boolean;
|
|
311
339
|
/** Skip paid providers Hunter/Apollo (default: false) */
|
|
312
340
|
skipPaidProviders?: boolean;
|
|
313
341
|
/** Minimum match confidence for SmartProspect (default: 60) */
|
|
@@ -362,13 +390,14 @@ export interface GetEmailsOptions {
|
|
|
362
390
|
* }
|
|
363
391
|
* ```
|
|
364
392
|
*/
|
|
365
|
-
export declare function getEmailsForLinkedInContact(
|
|
393
|
+
export declare function getEmailsForLinkedInContact(contactOrLead: LinkedInContact | SalesLeadSearchResult, config: GetEmailsConfig, options?: GetEmailsOptions): Promise<GetEmailsResult>;
|
|
366
394
|
/**
|
|
367
395
|
* Get emails for multiple LinkedIn contacts in batch
|
|
368
396
|
*
|
|
369
397
|
* Processes contacts with controlled concurrency to balance speed and rate limits.
|
|
398
|
+
* Accepts either LinkedInContact[] or SalesLeadSearchResult[] directly from Sales Nav search.
|
|
370
399
|
*/
|
|
371
|
-
export declare function getEmailsForLinkedInContactsBatch(
|
|
400
|
+
export declare function getEmailsForLinkedInContactsBatch(contactsOrLeads: (LinkedInContact | SalesLeadSearchResult)[], config: GetEmailsConfig, options?: GetEmailsOptions & {
|
|
372
401
|
/** Delay between requests in ms (default: 100) */
|
|
373
402
|
delayMs?: number;
|
|
374
403
|
/** Max concurrent lookups (default: 3) */
|
|
@@ -39,6 +39,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
39
39
|
};
|
|
40
40
|
})();
|
|
41
41
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.salesLeadToContact = salesLeadToContact;
|
|
42
43
|
exports.calculateMatchConfidence = calculateMatchConfidence;
|
|
43
44
|
exports.classifyMatchQuality = classifyMatchQuality;
|
|
44
45
|
exports.findBestMatch = findBestMatch;
|
|
@@ -52,6 +53,64 @@ exports.getEmailsForLinkedInContact = getEmailsForLinkedInContact;
|
|
|
52
53
|
exports.getEmailsForLinkedInContactsBatch = getEmailsForLinkedInContactsBatch;
|
|
53
54
|
const smartprospect_1 = require("./providers/smartprospect");
|
|
54
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
|
+
}
|
|
55
114
|
// =============================================================================
|
|
56
115
|
// Utility Functions
|
|
57
116
|
// =============================================================================
|
|
@@ -704,12 +763,14 @@ function createLinkedInEnricher(smartProspectConfig, defaultOptions = {}) {
|
|
|
704
763
|
* }
|
|
705
764
|
* ```
|
|
706
765
|
*/
|
|
707
|
-
async function getEmailsForLinkedInContact(
|
|
766
|
+
async function getEmailsForLinkedInContact(contactOrLead, config, options = {}) {
|
|
767
|
+
// Normalize input - accept either LinkedInContact or raw SalesLeadSearchResult
|
|
768
|
+
const contact = normalizeToContact(contactOrLead);
|
|
708
769
|
const { skipLdd = false, skipSmartProspect = false, skipPatternGuessing = false, skipPaidProviders = false, minMatchConfidence = 60, paidProviderThreshold = 80, includeCompany = true, } = options;
|
|
709
770
|
// Extract numeric ID from objectUrn
|
|
710
771
|
const numericLinkedInId = (0, ldd_1.extractNumericLinkedInId)(contact.objectUrn);
|
|
711
|
-
// Extract company domain from contact
|
|
712
|
-
const
|
|
772
|
+
// Extract company domain from contact (LinkedIn data - often missing)
|
|
773
|
+
const linkedInCompanyDomain = config.companyDomain || extractCompanyDomain(contact);
|
|
713
774
|
const result = {
|
|
714
775
|
success: false,
|
|
715
776
|
emails: [],
|
|
@@ -726,6 +787,8 @@ async function getEmailsForLinkedInContact(contact, config, options = {}) {
|
|
|
726
787
|
result.emails.push(emailResult);
|
|
727
788
|
}
|
|
728
789
|
};
|
|
790
|
+
// Track company domain discovered from SmartProspect (since LinkedIn doesn't provide it)
|
|
791
|
+
let discoveredCompanyDomain = null;
|
|
729
792
|
// ==========================================================================
|
|
730
793
|
// Phase 1: FREE providers in PARALLEL (LDD + SmartProspect)
|
|
731
794
|
// ==========================================================================
|
|
@@ -736,20 +799,62 @@ async function getEmailsForLinkedInContact(contact, config, options = {}) {
|
|
|
736
799
|
freeProviderPromises.push(queryLdd(contact, config.ldd, numericLinkedInId, addEmail, result));
|
|
737
800
|
}
|
|
738
801
|
// SmartProspect lookup (FREE for FlexIQ)
|
|
802
|
+
// This also extracts company domain for pattern guessing
|
|
739
803
|
if (!skipSmartProspect && config.smartprospect) {
|
|
740
804
|
result.providersQueried.push('smartprospect');
|
|
741
|
-
freeProviderPromises.push(querySmartProspect(contact, config.smartprospect, minMatchConfidence, includeCompany, addEmail, result)
|
|
805
|
+
freeProviderPromises.push(querySmartProspect(contact, config.smartprospect, minMatchConfidence, includeCompany, addEmail, result)
|
|
806
|
+
.then((domain) => {
|
|
807
|
+
if (domain) {
|
|
808
|
+
discoveredCompanyDomain = domain;
|
|
809
|
+
}
|
|
810
|
+
}));
|
|
742
811
|
}
|
|
743
812
|
// Wait for both free providers
|
|
744
813
|
await Promise.all(freeProviderPromises);
|
|
745
|
-
// Check if we have good enough results
|
|
746
|
-
const
|
|
814
|
+
// Check if we have good enough results already
|
|
815
|
+
const bestConfidenceAfterFreeProviders = result.emails.length > 0
|
|
747
816
|
? Math.max(...result.emails.map(e => e.confidence))
|
|
748
817
|
: 0;
|
|
749
818
|
// ==========================================================================
|
|
750
|
-
// Phase 2:
|
|
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'}`);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
// Store discovered domain in result for visibility
|
|
851
|
+
if (discoveredCompanyDomain && !linkedInCompanyDomain) {
|
|
852
|
+
result.discoveredCompanyDomain = discoveredCompanyDomain;
|
|
853
|
+
}
|
|
854
|
+
// ==========================================================================
|
|
855
|
+
// Phase 3: Email pattern guessing with MX verification (FREE)
|
|
751
856
|
// ==========================================================================
|
|
752
|
-
if (!skipPatternGuessing && companyDomain &&
|
|
857
|
+
if (!skipPatternGuessing && companyDomain && bestConfidenceAfterFreeProviders < paidProviderThreshold) {
|
|
753
858
|
result.providersQueried.push('pattern');
|
|
754
859
|
await queryPatternGuessing(contact, companyDomain, addEmail, result);
|
|
755
860
|
}
|
|
@@ -774,9 +879,10 @@ async function getEmailsForLinkedInContact(contact, config, options = {}) {
|
|
|
774
879
|
const sourcePriority = {
|
|
775
880
|
ldd: 0, // Highest priority - your own data
|
|
776
881
|
smartprospect: 1,
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
882
|
+
linkedin: 2, // LinkedIn company lookup (for domain discovery, doesn't provide emails)
|
|
883
|
+
pattern: 3,
|
|
884
|
+
hunter: 4,
|
|
885
|
+
apollo: 5,
|
|
780
886
|
};
|
|
781
887
|
result.emails.sort((a, b) => {
|
|
782
888
|
if (b.confidence !== a.confidence) {
|
|
@@ -801,18 +907,36 @@ function extractCompanyDomain(contact) {
|
|
|
801
907
|
// Try to get website from company resolution
|
|
802
908
|
const website = pos.companyUrnResolutionResult?.website;
|
|
803
909
|
if (website) {
|
|
804
|
-
|
|
805
|
-
try {
|
|
806
|
-
const url = website.startsWith('http') ? website : `https://${website}`;
|
|
807
|
-
return new URL(url).hostname.replace(/^www\./, '');
|
|
808
|
-
}
|
|
809
|
-
catch {
|
|
810
|
-
// If URL parsing fails, try as-is
|
|
811
|
-
return website.replace(/^(https?:\/\/)?(www\.)?/, '').split('/')[0];
|
|
812
|
-
}
|
|
910
|
+
return extractDomainFromUrl(website);
|
|
813
911
|
}
|
|
814
912
|
return null;
|
|
815
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
|
+
}
|
|
816
940
|
/**
|
|
817
941
|
* Query LDD provider
|
|
818
942
|
*/
|
|
@@ -843,13 +967,14 @@ async function queryLdd(contact, lddConfig, numericLinkedInId, addEmail, result)
|
|
|
843
967
|
}
|
|
844
968
|
/**
|
|
845
969
|
* Query SmartProspect provider
|
|
970
|
+
* Returns the company domain if found (for pattern guessing fallback)
|
|
846
971
|
*/
|
|
847
972
|
async function querySmartProspect(contact, smartProspectConfig, minMatchConfidence, includeCompany, addEmail, result) {
|
|
848
973
|
try {
|
|
849
974
|
const client = (0, smartprospect_1.createSmartProspectClient)(smartProspectConfig);
|
|
850
975
|
if (!client) {
|
|
851
976
|
result.errors?.push('SmartProspect: Failed to create client');
|
|
852
|
-
return;
|
|
977
|
+
return null;
|
|
853
978
|
}
|
|
854
979
|
// Build search filters
|
|
855
980
|
const filters = buildSmartProspectFiltersFromLinkedIn(contact);
|
|
@@ -877,7 +1002,7 @@ async function querySmartProspect(contact, smartProspectConfig, minMatchConfiden
|
|
|
877
1002
|
}
|
|
878
1003
|
}
|
|
879
1004
|
if (searchResponse.data.list.length === 0) {
|
|
880
|
-
return; // No candidates found, but not an error
|
|
1005
|
+
return null; // No candidates found, but not an error
|
|
881
1006
|
}
|
|
882
1007
|
// Find best match
|
|
883
1008
|
const matchResult = findBestMatch(contact, searchResponse.data.list, {
|
|
@@ -886,7 +1011,17 @@ async function querySmartProspect(contact, smartProspectConfig, minMatchConfiden
|
|
|
886
1011
|
fuzzyCompany: true,
|
|
887
1012
|
});
|
|
888
1013
|
if (!matchResult) {
|
|
889
|
-
return; // No good match, but not an error
|
|
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();
|
|
890
1025
|
}
|
|
891
1026
|
// Fetch email for matched contact
|
|
892
1027
|
const fetchResponse = await client.fetch([matchResult.smartProspectContact.id]);
|
|
@@ -905,14 +1040,17 @@ async function querySmartProspect(contact, smartProspectConfig, minMatchConfiden
|
|
|
905
1040
|
matchedFields: matchResult.matchedFields,
|
|
906
1041
|
smartProspectId: enrichedContact.id,
|
|
907
1042
|
company: enrichedContact.company?.name,
|
|
1043
|
+
companyWebsite: companyWebsite,
|
|
908
1044
|
title: enrichedContact.title,
|
|
909
1045
|
},
|
|
910
1046
|
});
|
|
911
1047
|
}
|
|
912
1048
|
}
|
|
1049
|
+
return discoveredDomain;
|
|
913
1050
|
}
|
|
914
1051
|
catch (err) {
|
|
915
1052
|
result.errors?.push(`SmartProspect: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
1053
|
+
return null;
|
|
916
1054
|
}
|
|
917
1055
|
}
|
|
918
1056
|
/**
|
|
@@ -922,7 +1060,7 @@ async function queryPatternGuessing(contact, companyDomain, addEmail, result) {
|
|
|
922
1060
|
try {
|
|
923
1061
|
// Import construct provider dynamically to avoid circular deps
|
|
924
1062
|
const { createConstructProvider } = await Promise.resolve().then(() => __importStar(require('./providers/construct')));
|
|
925
|
-
const constructProvider = createConstructProvider({ maxAttempts:
|
|
1063
|
+
const constructProvider = createConstructProvider({ maxAttempts: 12, timeoutMs: 3000 });
|
|
926
1064
|
const constructResult = await constructProvider({
|
|
927
1065
|
firstName: contact.firstName,
|
|
928
1066
|
lastName: contact.lastName,
|
|
@@ -950,27 +1088,28 @@ async function queryPatternGuessing(contact, companyDomain, addEmail, result) {
|
|
|
950
1088
|
* Get emails for multiple LinkedIn contacts in batch
|
|
951
1089
|
*
|
|
952
1090
|
* Processes contacts with controlled concurrency to balance speed and rate limits.
|
|
1091
|
+
* Accepts either LinkedInContact[] or SalesLeadSearchResult[] directly from Sales Nav search.
|
|
953
1092
|
*/
|
|
954
|
-
async function getEmailsForLinkedInContactsBatch(
|
|
1093
|
+
async function getEmailsForLinkedInContactsBatch(contactsOrLeads, config, options = {}) {
|
|
955
1094
|
const { delayMs = 100, concurrency = 3, onProgress, ...lookupOptions } = options;
|
|
956
|
-
const results = new Array(
|
|
1095
|
+
const results = new Array(contactsOrLeads.length);
|
|
957
1096
|
let completed = 0;
|
|
958
1097
|
// Process in batches with concurrency limit
|
|
959
|
-
for (let i = 0; i <
|
|
960
|
-
const batch =
|
|
961
|
-
const batchPromises = batch.map(async (
|
|
962
|
-
const result = await getEmailsForLinkedInContact(
|
|
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);
|
|
963
1102
|
const globalIndex = i + batchIndex;
|
|
964
1103
|
results[globalIndex] = result;
|
|
965
1104
|
completed++;
|
|
966
1105
|
if (onProgress) {
|
|
967
|
-
onProgress(completed,
|
|
1106
|
+
onProgress(completed, contactsOrLeads.length, result);
|
|
968
1107
|
}
|
|
969
1108
|
return result;
|
|
970
1109
|
});
|
|
971
1110
|
await Promise.all(batchPromises);
|
|
972
1111
|
// Delay between batches (except for last batch)
|
|
973
|
-
if (i + concurrency <
|
|
1112
|
+
if (i + concurrency < contactsOrLeads.length && delayMs > 0) {
|
|
974
1113
|
await new Promise((r) => setTimeout(r, delayMs));
|
|
975
1114
|
}
|
|
976
1115
|
}
|
|
@@ -12,27 +12,39 @@ const personal_domains_1 = require("../utils/personal-domains");
|
|
|
12
12
|
const validation_1 = require("../utils/validation");
|
|
13
13
|
/**
|
|
14
14
|
* Build all email pattern candidates for a person
|
|
15
|
+
*
|
|
16
|
+
* Patterns are ordered by commonality based on real-world email conventions:
|
|
17
|
+
* 1. firstname (most common for small companies)
|
|
18
|
+
* 2. firstname.lastname (most common for larger companies)
|
|
19
|
+
* 3. f.lastname, flastname, etc.
|
|
15
20
|
*/
|
|
16
21
|
function buildCandidates(input) {
|
|
17
22
|
const domain = String(input.domain || '').toLowerCase();
|
|
18
23
|
const first = (0, validation_1.cleanNamePart)(input.first || '');
|
|
19
24
|
const last = (0, validation_1.cleanNamePart)(input.last || '');
|
|
20
25
|
const fl = first ? first[0] : '';
|
|
26
|
+
const ll = last ? last[0] : '';
|
|
21
27
|
const locals = [];
|
|
22
|
-
//
|
|
28
|
+
// Simple first name pattern (very common, especially for small companies/startups)
|
|
29
|
+
if (first)
|
|
30
|
+
locals.push(first); // john
|
|
31
|
+
// Common combined patterns
|
|
23
32
|
if (first && last) {
|
|
24
|
-
locals.push(`${first}.${last}`); // john.doe
|
|
25
|
-
locals.push(`${fl}.${last}`); // j.doe
|
|
26
|
-
locals.push(`${first}${last[0] ?? ''}`); // johnd
|
|
33
|
+
locals.push(`${first}.${last}`); // john.doe (most common corporate pattern)
|
|
27
34
|
locals.push(`${fl}${last}`); // jdoe
|
|
35
|
+
locals.push(`${first}${ll}`); // johnd
|
|
36
|
+
locals.push(`${fl}.${last}`); // j.doe
|
|
37
|
+
locals.push(`${first}${last}`); // johndoe
|
|
28
38
|
locals.push(`${first}_${last}`); // john_doe
|
|
29
39
|
locals.push(`${last}.${first}`); // doe.john
|
|
40
|
+
locals.push(`${last}${first}`); // doejohn
|
|
41
|
+
locals.push(`${last}`); // doe (last name only)
|
|
42
|
+
locals.push(`${fl}${ll}`); // jd (initials)
|
|
30
43
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
locals.push(first); // john
|
|
34
|
-
if (last)
|
|
44
|
+
else if (last) {
|
|
45
|
+
// Only last name available
|
|
35
46
|
locals.push(last); // doe
|
|
47
|
+
}
|
|
36
48
|
// Deduplicate while preserving order
|
|
37
49
|
const seen = new Set();
|
|
38
50
|
const emails = [];
|
|
@@ -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