linkedin-secret-sauce 0.9.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 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";
@@ -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(contact: LinkedInContact, config: GetEmailsConfig, options?: GetEmailsOptions): Promise<GetEmailsResult>;
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(contacts: LinkedInContact[], config: GetEmailsConfig, options?: GetEmailsOptions & {
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(contact, config, options = {}) {
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 companyDomain = config.companyDomain || extractCompanyDomain(contact);
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 bestConfidence = result.emails.length > 0
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: Email pattern guessing with MX verification (FREE)
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 && bestConfidence < paidProviderThreshold) {
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
- pattern: 2,
778
- hunter: 3,
779
- apollo: 4,
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
- // Extract domain from URL
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
  /**
@@ -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(contacts, config, options = {}) {
1093
+ async function getEmailsForLinkedInContactsBatch(contactsOrLeads, config, options = {}) {
955
1094
  const { delayMs = 100, concurrency = 3, onProgress, ...lookupOptions } = options;
956
- const results = new Array(contacts.length);
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 < contacts.length; i += concurrency) {
960
- const batch = contacts.slice(i, i + concurrency);
961
- const batchPromises = batch.map(async (contact, batchIndex) => {
962
- const result = await getEmailsForLinkedInContact(contact, config, lookupOptions);
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, contacts.length, result);
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 < contacts.length && delayMs > 0) {
1112
+ if (i + concurrency < contactsOrLeads.length && delayMs > 0) {
974
1113
  await new Promise((r) => setTimeout(r, delayMs));
975
1114
  }
976
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
@@ -119,6 +119,9 @@ export interface LinkedInSpotlightBadge {
119
119
  }>;
120
120
  }
121
121
  export interface SalesLeadSearchResult {
122
+ firstName?: string;
123
+ lastName?: string;
124
+ fullName?: string;
122
125
  name?: string;
123
126
  headline?: string;
124
127
  imageUrl?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linkedin-secret-sauce",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Private LinkedIn Sales Navigator client with automatic cookie management",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",