linkedin-secret-sauce 0.7.2 → 0.9.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, type LinkedInContact, type MatchResult, type MatchOptions, type LinkedInEnrichmentResult, type LinkedInEnrichmentOptions, } from "./matching";
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";
@@ -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.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.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");
@@ -287,3 +287,6 @@ Object.defineProperty(exports, "parseLinkedInSearchResponse", { enumerable: true
287
287
  Object.defineProperty(exports, "enrichLinkedInContact", { enumerable: true, get: function () { return matching_1.enrichLinkedInContact; } });
288
288
  Object.defineProperty(exports, "enrichLinkedInContactsBatch", { enumerable: true, get: function () { return matching_1.enrichLinkedInContactsBatch; } });
289
289
  Object.defineProperty(exports, "createLinkedInEnricher", { enumerable: true, get: function () { return matching_1.createLinkedInEnricher; } });
290
+ // UNIFIED email lookup (recommended for most use cases)
291
+ Object.defineProperty(exports, "getEmailsForLinkedInContact", { enumerable: true, get: function () { return matching_1.getEmailsForLinkedInContact; } });
292
+ Object.defineProperty(exports, "getEmailsForLinkedInContactsBatch", { enumerable: true, get: function () { return matching_1.getEmailsForLinkedInContactsBatch; } });
@@ -4,7 +4,7 @@
4
4
  * Matches contacts between LinkedIn Sales Navigator and SmartProspect
5
5
  * to find the same person across both platforms.
6
6
  */
7
- import type { SmartProspectContact, SmartProspectConfig, SmartProspectFetchResponse } from './types';
7
+ import type { SmartProspectContact, SmartProspectConfig, SmartProspectFetchResponse, LddConfig } from './types';
8
8
  import { type SmartProspectClient } from './providers/smartprospect';
9
9
  /**
10
10
  * LinkedIn Sales Navigator contact (simplified from API response)
@@ -239,3 +239,140 @@ export declare function createLinkedInEnricher(smartProspectConfig: SmartProspec
239
239
  /** Fetch email for a specific SmartProspect contact ID (COSTS CREDITS) */
240
240
  fetchEmail: (contactId: string) => Promise<SmartProspectFetchResponse>;
241
241
  } | null;
242
+ /**
243
+ * Email source - where the email was found
244
+ */
245
+ export type EmailSource = 'ldd' | 'smartprospect' | 'pattern' | 'hunter' | 'apollo';
246
+ /**
247
+ * Email result from unified lookup
248
+ */
249
+ export interface EmailResult {
250
+ /** The email address */
251
+ email: string;
252
+ /** Which provider found this email */
253
+ source: EmailSource;
254
+ /** Confidence score (0-100) */
255
+ confidence: number;
256
+ /** Email type classification */
257
+ type: 'business' | 'personal' | 'unknown';
258
+ /** Whether the email was verified */
259
+ verified: boolean;
260
+ /** Email deliverability score (0-1) for SmartProspect emails */
261
+ deliverability?: number;
262
+ /** Whether this is a catch-all domain */
263
+ isCatchAll?: boolean;
264
+ /** Additional metadata */
265
+ metadata?: Record<string, unknown>;
266
+ }
267
+ /**
268
+ * Result from unified email lookup
269
+ */
270
+ export interface GetEmailsResult {
271
+ /** Whether lookup was successful (found at least one email) */
272
+ success: boolean;
273
+ /** All found emails, sorted by confidence (highest first) */
274
+ emails: EmailResult[];
275
+ /** Numeric LinkedIn ID extracted from objectUrn */
276
+ numericLinkedInId: string | null;
277
+ /** Which providers were queried */
278
+ providersQueried: EmailSource[];
279
+ /** Error message if any provider failed */
280
+ errors?: string[];
281
+ }
282
+ /**
283
+ * Configuration for unified email lookup
284
+ */
285
+ export interface GetEmailsConfig {
286
+ /** LDD configuration (FREE - your ~500M database) */
287
+ ldd?: LddConfig;
288
+ /** SmartProspect configuration (FREE for FlexIQ - already paying monthly) */
289
+ smartprospect?: SmartProspectConfig;
290
+ /** Company domain for email pattern guessing (optional) */
291
+ companyDomain?: string;
292
+ /** Hunter configuration (PAID - last resort) */
293
+ hunter?: {
294
+ apiKey: string;
295
+ };
296
+ /** Apollo configuration (PAID - last resort) */
297
+ apollo?: {
298
+ apiKey: string;
299
+ };
300
+ }
301
+ /**
302
+ * Options for unified email lookup
303
+ */
304
+ export interface GetEmailsOptions {
305
+ /** Skip LDD lookup (default: false) */
306
+ skipLdd?: boolean;
307
+ /** Skip SmartProspect lookup (default: false) */
308
+ skipSmartProspect?: boolean;
309
+ /** Skip email pattern guessing (default: false) */
310
+ skipPatternGuessing?: boolean;
311
+ /** Skip paid providers Hunter/Apollo (default: false) */
312
+ skipPaidProviders?: boolean;
313
+ /** Minimum match confidence for SmartProspect (default: 60) */
314
+ minMatchConfidence?: number;
315
+ /** Minimum confidence to skip paid providers (default: 80) */
316
+ paidProviderThreshold?: number;
317
+ /** Include company in SmartProspect search (default: true) */
318
+ includeCompany?: boolean;
319
+ }
320
+ /**
321
+ * Get emails for a LinkedIn contact - UNIFIED FUNCTION
322
+ *
323
+ * Strategy (optimized for FlexIQ):
324
+ * 1. Query LDD + SmartProspect in PARALLEL (both FREE for you)
325
+ * 2. Try email pattern guessing with MX verification (FREE)
326
+ * 3. Only use Hunter/Apollo if confidence is below threshold (PAID - last resort)
327
+ *
328
+ * @example
329
+ * ```ts
330
+ * import { getEmailsForLinkedInContact } from 'linkedin-secret-sauce';
331
+ *
332
+ * // From your LinkedIn Sales Nav search result
333
+ * const contact = {
334
+ * objectUrn: 'urn:li:member:307567', // Stable ID for LDD
335
+ * firstName: 'Jim',
336
+ * lastName: 'DeMaio',
337
+ * currentPositions: [{
338
+ * companyName: 'Acme Corp',
339
+ * companyUrnResolutionResult: { website: 'acme.com' }
340
+ * }]
341
+ * };
342
+ *
343
+ * const result = await getEmailsForLinkedInContact(contact, {
344
+ * ldd: {
345
+ * apiUrl: process.env.LDD_API_URL,
346
+ * apiToken: process.env.LDD_API_TOKEN,
347
+ * },
348
+ * smartprospect: {
349
+ * email: process.env.SMARTLEAD_EMAIL,
350
+ * password: process.env.SMARTLEAD_PASSWORD,
351
+ * },
352
+ * // companyDomain extracted automatically from currentPositions
353
+ * });
354
+ *
355
+ * if (result.success) {
356
+ * console.log('Providers queried:', result.providersQueried);
357
+ * console.log('Found emails:', result.emails);
358
+ * // [
359
+ * // { email: 'jim@acme.com', source: 'ldd', confidence: 95, type: 'business' },
360
+ * // { email: 'j.demaio@acme.com', source: 'smartprospect', confidence: 85, type: 'business' },
361
+ * // ]
362
+ * }
363
+ * ```
364
+ */
365
+ export declare function getEmailsForLinkedInContact(contact: LinkedInContact, config: GetEmailsConfig, options?: GetEmailsOptions): Promise<GetEmailsResult>;
366
+ /**
367
+ * Get emails for multiple LinkedIn contacts in batch
368
+ *
369
+ * Processes contacts with controlled concurrency to balance speed and rate limits.
370
+ */
371
+ export declare function getEmailsForLinkedInContactsBatch(contacts: LinkedInContact[], config: GetEmailsConfig, options?: GetEmailsOptions & {
372
+ /** Delay between requests in ms (default: 100) */
373
+ delayMs?: number;
374
+ /** Max concurrent lookups (default: 3) */
375
+ concurrency?: number;
376
+ /** Callback for progress updates */
377
+ onProgress?: (completed: number, total: number, result: GetEmailsResult) => void;
378
+ }): Promise<GetEmailsResult[]>;
@@ -5,6 +5,39 @@
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 });
9
42
  exports.calculateMatchConfidence = calculateMatchConfidence;
10
43
  exports.classifyMatchQuality = classifyMatchQuality;
@@ -15,6 +48,8 @@ exports.parseLinkedInSearchResponse = parseLinkedInSearchResponse;
15
48
  exports.enrichLinkedInContact = enrichLinkedInContact;
16
49
  exports.enrichLinkedInContactsBatch = enrichLinkedInContactsBatch;
17
50
  exports.createLinkedInEnricher = createLinkedInEnricher;
51
+ exports.getEmailsForLinkedInContact = getEmailsForLinkedInContact;
52
+ exports.getEmailsForLinkedInContactsBatch = getEmailsForLinkedInContactsBatch;
18
53
  const smartprospect_1 = require("./providers/smartprospect");
19
54
  const ldd_1 = require("./providers/ldd");
20
55
  // =============================================================================
@@ -624,3 +659,320 @@ function createLinkedInEnricher(smartProspectConfig, defaultOptions = {}) {
624
659
  },
625
660
  };
626
661
  }
662
+ /**
663
+ * Get emails for a LinkedIn contact - UNIFIED FUNCTION
664
+ *
665
+ * Strategy (optimized for FlexIQ):
666
+ * 1. Query LDD + SmartProspect in PARALLEL (both FREE for you)
667
+ * 2. Try email pattern guessing with MX verification (FREE)
668
+ * 3. Only use Hunter/Apollo if confidence is below threshold (PAID - last resort)
669
+ *
670
+ * @example
671
+ * ```ts
672
+ * import { getEmailsForLinkedInContact } from 'linkedin-secret-sauce';
673
+ *
674
+ * // From your LinkedIn Sales Nav search result
675
+ * const contact = {
676
+ * objectUrn: 'urn:li:member:307567', // Stable ID for LDD
677
+ * firstName: 'Jim',
678
+ * lastName: 'DeMaio',
679
+ * currentPositions: [{
680
+ * companyName: 'Acme Corp',
681
+ * companyUrnResolutionResult: { website: 'acme.com' }
682
+ * }]
683
+ * };
684
+ *
685
+ * const result = await getEmailsForLinkedInContact(contact, {
686
+ * ldd: {
687
+ * apiUrl: process.env.LDD_API_URL,
688
+ * apiToken: process.env.LDD_API_TOKEN,
689
+ * },
690
+ * smartprospect: {
691
+ * email: process.env.SMARTLEAD_EMAIL,
692
+ * password: process.env.SMARTLEAD_PASSWORD,
693
+ * },
694
+ * // companyDomain extracted automatically from currentPositions
695
+ * });
696
+ *
697
+ * if (result.success) {
698
+ * console.log('Providers queried:', result.providersQueried);
699
+ * console.log('Found emails:', result.emails);
700
+ * // [
701
+ * // { email: 'jim@acme.com', source: 'ldd', confidence: 95, type: 'business' },
702
+ * // { email: 'j.demaio@acme.com', source: 'smartprospect', confidence: 85, type: 'business' },
703
+ * // ]
704
+ * }
705
+ * ```
706
+ */
707
+ async function getEmailsForLinkedInContact(contact, config, options = {}) {
708
+ const { skipLdd = false, skipSmartProspect = false, skipPatternGuessing = false, skipPaidProviders = false, minMatchConfidence = 60, paidProviderThreshold = 80, includeCompany = true, } = options;
709
+ // Extract numeric ID from objectUrn
710
+ const numericLinkedInId = (0, ldd_1.extractNumericLinkedInId)(contact.objectUrn);
711
+ // Extract company domain from contact
712
+ const companyDomain = config.companyDomain || extractCompanyDomain(contact);
713
+ const result = {
714
+ success: false,
715
+ emails: [],
716
+ numericLinkedInId,
717
+ providersQueried: [],
718
+ errors: [],
719
+ };
720
+ // Track seen emails to deduplicate
721
+ const seenEmails = new Set();
722
+ const addEmail = (emailResult) => {
723
+ const lower = emailResult.email.toLowerCase();
724
+ if (!seenEmails.has(lower)) {
725
+ seenEmails.add(lower);
726
+ result.emails.push(emailResult);
727
+ }
728
+ };
729
+ // ==========================================================================
730
+ // Phase 1: FREE providers in PARALLEL (LDD + SmartProspect)
731
+ // ==========================================================================
732
+ const freeProviderPromises = [];
733
+ // LDD lookup (FREE)
734
+ if (!skipLdd && config.ldd?.apiUrl && config.ldd?.apiToken) {
735
+ result.providersQueried.push('ldd');
736
+ freeProviderPromises.push(queryLdd(contact, config.ldd, numericLinkedInId, addEmail, result));
737
+ }
738
+ // SmartProspect lookup (FREE for FlexIQ)
739
+ if (!skipSmartProspect && config.smartprospect) {
740
+ result.providersQueried.push('smartprospect');
741
+ freeProviderPromises.push(querySmartProspect(contact, config.smartprospect, minMatchConfidence, includeCompany, addEmail, result));
742
+ }
743
+ // Wait for both free providers
744
+ await Promise.all(freeProviderPromises);
745
+ // Check if we have good enough results
746
+ const bestConfidence = result.emails.length > 0
747
+ ? Math.max(...result.emails.map(e => e.confidence))
748
+ : 0;
749
+ // ==========================================================================
750
+ // Phase 2: Email pattern guessing with MX verification (FREE)
751
+ // ==========================================================================
752
+ if (!skipPatternGuessing && companyDomain && bestConfidence < paidProviderThreshold) {
753
+ result.providersQueried.push('pattern');
754
+ await queryPatternGuessing(contact, companyDomain, addEmail, result);
755
+ }
756
+ // Recalculate best confidence
757
+ const finalBestConfidence = result.emails.length > 0
758
+ ? Math.max(...result.emails.map(e => e.confidence))
759
+ : 0;
760
+ // ==========================================================================
761
+ // Phase 3: PAID providers as last resort (Hunter/Apollo)
762
+ // ==========================================================================
763
+ if (!skipPaidProviders && finalBestConfidence < paidProviderThreshold) {
764
+ // Only use paid providers if we have low confidence or no results
765
+ // TODO: Implement Hunter and Apollo providers when needed
766
+ // For now, just mark that we would have queried them
767
+ if (config.hunter?.apiKey || config.apollo?.apiKey) {
768
+ // result.providersQueried.push('hunter');
769
+ // result.providersQueried.push('apollo');
770
+ // await queryPaidProviders(contact, config, addEmail, result);
771
+ }
772
+ }
773
+ // Sort emails by confidence (highest first), then by source priority
774
+ const sourcePriority = {
775
+ ldd: 0, // Highest priority - your own data
776
+ smartprospect: 1,
777
+ pattern: 2,
778
+ hunter: 3,
779
+ apollo: 4,
780
+ };
781
+ result.emails.sort((a, b) => {
782
+ if (b.confidence !== a.confidence) {
783
+ return b.confidence - a.confidence;
784
+ }
785
+ return sourcePriority[a.source] - sourcePriority[b.source];
786
+ });
787
+ result.success = result.emails.length > 0;
788
+ // Clean up errors array if empty
789
+ if (result.errors && result.errors.length === 0) {
790
+ delete result.errors;
791
+ }
792
+ return result;
793
+ }
794
+ /**
795
+ * Extract company domain from LinkedIn contact
796
+ */
797
+ function extractCompanyDomain(contact) {
798
+ const pos = contact.currentPositions?.[0];
799
+ if (!pos)
800
+ return null;
801
+ // Try to get website from company resolution
802
+ const website = pos.companyUrnResolutionResult?.website;
803
+ 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
+ }
813
+ }
814
+ return null;
815
+ }
816
+ /**
817
+ * Query LDD provider
818
+ */
819
+ async function queryLdd(contact, lddConfig, numericLinkedInId, addEmail, result) {
820
+ try {
821
+ const lddProvider = (0, ldd_1.createLddProvider)(lddConfig);
822
+ const lddResult = await lddProvider({
823
+ numericLinkedInId: numericLinkedInId || undefined,
824
+ firstName: contact.firstName,
825
+ lastName: contact.lastName,
826
+ });
827
+ if (lddResult && 'emails' in lddResult && lddResult.emails.length > 0) {
828
+ for (const emailData of lddResult.emails) {
829
+ addEmail({
830
+ email: emailData.email,
831
+ source: 'ldd',
832
+ confidence: emailData.confidence ?? 90,
833
+ type: emailData.metadata?.emailTypeClassified || 'unknown',
834
+ verified: emailData.verified ?? true,
835
+ metadata: emailData.metadata,
836
+ });
837
+ }
838
+ }
839
+ }
840
+ catch (err) {
841
+ result.errors?.push(`LDD: ${err instanceof Error ? err.message : 'Unknown error'}`);
842
+ }
843
+ }
844
+ /**
845
+ * Query SmartProspect provider
846
+ */
847
+ async function querySmartProspect(contact, smartProspectConfig, minMatchConfidence, includeCompany, addEmail, result) {
848
+ try {
849
+ const client = (0, smartprospect_1.createSmartProspectClient)(smartProspectConfig);
850
+ if (!client) {
851
+ result.errors?.push('SmartProspect: Failed to create client');
852
+ return;
853
+ }
854
+ // Build search filters
855
+ const filters = buildSmartProspectFiltersFromLinkedIn(contact);
856
+ const searchFilters = {
857
+ firstName: filters.firstName,
858
+ lastName: filters.lastName,
859
+ limit: 25,
860
+ };
861
+ if (includeCompany && filters.companyName) {
862
+ searchFilters.companyName = filters.companyName;
863
+ }
864
+ // Search for matching contacts
865
+ let searchResponse = await client.search(searchFilters);
866
+ // Try broader search if no results
867
+ if (!searchResponse.success || searchResponse.data.list.length === 0) {
868
+ if (includeCompany && filters.companyName) {
869
+ const broaderResponse = await client.search({
870
+ firstName: filters.firstName,
871
+ lastName: filters.lastName,
872
+ limit: 25,
873
+ });
874
+ if (broaderResponse.success && broaderResponse.data.list.length > 0) {
875
+ searchResponse = broaderResponse;
876
+ }
877
+ }
878
+ }
879
+ if (searchResponse.data.list.length === 0) {
880
+ return; // No candidates found, but not an error
881
+ }
882
+ // Find best match
883
+ const matchResult = findBestMatch(contact, searchResponse.data.list, {
884
+ minConfidence: minMatchConfidence,
885
+ fuzzyNames: true,
886
+ fuzzyCompany: true,
887
+ });
888
+ if (!matchResult) {
889
+ return; // No good match, but not an error
890
+ }
891
+ // Fetch email for matched contact
892
+ const fetchResponse = await client.fetch([matchResult.smartProspectContact.id]);
893
+ if (fetchResponse.success && fetchResponse.data.list.length > 0) {
894
+ const enrichedContact = fetchResponse.data.list[0];
895
+ if (enrichedContact.email) {
896
+ addEmail({
897
+ email: enrichedContact.email,
898
+ source: 'smartprospect',
899
+ confidence: matchResult.confidence,
900
+ type: 'business',
901
+ verified: enrichedContact.verificationStatus === 'verified',
902
+ deliverability: enrichedContact.emailDeliverability,
903
+ metadata: {
904
+ matchQuality: matchResult.quality,
905
+ matchedFields: matchResult.matchedFields,
906
+ smartProspectId: enrichedContact.id,
907
+ company: enrichedContact.company?.name,
908
+ title: enrichedContact.title,
909
+ },
910
+ });
911
+ }
912
+ }
913
+ }
914
+ catch (err) {
915
+ result.errors?.push(`SmartProspect: ${err instanceof Error ? err.message : 'Unknown error'}`);
916
+ }
917
+ }
918
+ /**
919
+ * Query pattern guessing with MX verification
920
+ */
921
+ async function queryPatternGuessing(contact, companyDomain, addEmail, result) {
922
+ try {
923
+ // Import construct provider dynamically to avoid circular deps
924
+ const { createConstructProvider } = await Promise.resolve().then(() => __importStar(require('./providers/construct')));
925
+ const constructProvider = createConstructProvider({ maxAttempts: 6, timeoutMs: 3000 });
926
+ const constructResult = await constructProvider({
927
+ firstName: contact.firstName,
928
+ lastName: contact.lastName,
929
+ domain: companyDomain,
930
+ });
931
+ if (constructResult && 'emails' in constructResult && constructResult.emails.length > 0) {
932
+ for (const emailData of constructResult.emails) {
933
+ addEmail({
934
+ email: emailData.email,
935
+ source: 'pattern',
936
+ confidence: emailData.confidence ?? 50,
937
+ type: 'business',
938
+ verified: emailData.verified ?? false,
939
+ isCatchAll: emailData.isCatchAll,
940
+ metadata: emailData.metadata,
941
+ });
942
+ }
943
+ }
944
+ }
945
+ catch (err) {
946
+ result.errors?.push(`Pattern: ${err instanceof Error ? err.message : 'Unknown error'}`);
947
+ }
948
+ }
949
+ /**
950
+ * Get emails for multiple LinkedIn contacts in batch
951
+ *
952
+ * Processes contacts with controlled concurrency to balance speed and rate limits.
953
+ */
954
+ async function getEmailsForLinkedInContactsBatch(contacts, config, options = {}) {
955
+ const { delayMs = 100, concurrency = 3, onProgress, ...lookupOptions } = options;
956
+ const results = new Array(contacts.length);
957
+ let completed = 0;
958
+ // 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);
963
+ const globalIndex = i + batchIndex;
964
+ results[globalIndex] = result;
965
+ completed++;
966
+ if (onProgress) {
967
+ onProgress(completed, contacts.length, result);
968
+ }
969
+ return result;
970
+ });
971
+ await Promise.all(batchPromises);
972
+ // Delay between batches (except for last batch)
973
+ if (i + concurrency < contacts.length && delayMs > 0) {
974
+ await new Promise((r) => setTimeout(r, delayMs));
975
+ }
976
+ }
977
+ return results;
978
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linkedin-secret-sauce",
3
- "version": "0.7.2",
3
+ "version": "0.9.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",