linkedin-secret-sauce 0.8.0 → 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, 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, type LinkedInContact, type MatchResult, type MatchOptions, type LinkedInEnrichmentResult, type LinkedInEnrichmentOptions, type EmailSource, type EmailResult, type GetEmailsResult, type GetEmailsConfig, type GetEmailsOptions, } from "./matching";
@@ -239,6 +239,10 @@ 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';
242
246
  /**
243
247
  * Email result from unified lookup
244
248
  */
@@ -246,7 +250,7 @@ export interface EmailResult {
246
250
  /** The email address */
247
251
  email: string;
248
252
  /** Which provider found this email */
249
- source: 'ldd' | 'smartprospect';
253
+ source: EmailSource;
250
254
  /** Confidence score (0-100) */
251
255
  confidence: number;
252
256
  /** Email type classification */
@@ -255,6 +259,8 @@ export interface EmailResult {
255
259
  verified: boolean;
256
260
  /** Email deliverability score (0-1) for SmartProspect emails */
257
261
  deliverability?: number;
262
+ /** Whether this is a catch-all domain */
263
+ isCatchAll?: boolean;
258
264
  /** Additional metadata */
259
265
  metadata?: Record<string, unknown>;
260
266
  }
@@ -269,18 +275,28 @@ export interface GetEmailsResult {
269
275
  /** Numeric LinkedIn ID extracted from objectUrn */
270
276
  numericLinkedInId: string | null;
271
277
  /** Which providers were queried */
272
- providersQueried: ('ldd' | 'smartprospect')[];
273
- /** Error message if failed */
274
- error?: string;
278
+ providersQueried: EmailSource[];
279
+ /** Error message if any provider failed */
280
+ errors?: string[];
275
281
  }
276
282
  /**
277
283
  * Configuration for unified email lookup
278
284
  */
279
285
  export interface GetEmailsConfig {
280
- /** LDD configuration (optional but recommended - FREE) */
286
+ /** LDD configuration (FREE - your ~500M database) */
281
287
  ldd?: LddConfig;
282
- /** SmartProspect configuration (optional - PAID fallback) */
288
+ /** SmartProspect configuration (FREE for FlexIQ - already paying monthly) */
283
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
+ };
284
300
  }
285
301
  /**
286
302
  * Options for unified email lookup
@@ -290,19 +306,24 @@ export interface GetEmailsOptions {
290
306
  skipLdd?: boolean;
291
307
  /** Skip SmartProspect lookup (default: false) */
292
308
  skipSmartProspect?: boolean;
309
+ /** Skip email pattern guessing (default: false) */
310
+ skipPatternGuessing?: boolean;
311
+ /** Skip paid providers Hunter/Apollo (default: false) */
312
+ skipPaidProviders?: boolean;
293
313
  /** Minimum match confidence for SmartProspect (default: 60) */
294
314
  minMatchConfidence?: number;
315
+ /** Minimum confidence to skip paid providers (default: 80) */
316
+ paidProviderThreshold?: number;
295
317
  /** Include company in SmartProspect search (default: true) */
296
318
  includeCompany?: boolean;
297
319
  }
298
320
  /**
299
321
  * Get emails for a LinkedIn contact - UNIFIED FUNCTION
300
322
  *
301
- * This is the main function consumers should use. It:
302
- * 1. Extracts the numeric LinkedIn ID from objectUrn (stable identifier)
303
- * 2. Tries LDD first (FREE, uses numeric ID for reliable lookup)
304
- * 3. Falls back to SmartProspect if LDD has no results (PAID)
305
- * 4. Returns ALL emails with unified scoring
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)
306
327
  *
307
328
  * @example
308
329
  * ```ts
@@ -310,10 +331,13 @@ export interface GetEmailsOptions {
310
331
  *
311
332
  * // From your LinkedIn Sales Nav search result
312
333
  * const contact = {
313
- * objectUrn: 'urn:li:member:307567', // IMPORTANT: Include this!
334
+ * objectUrn: 'urn:li:member:307567', // Stable ID for LDD
314
335
  * firstName: 'Jim',
315
336
  * lastName: 'DeMaio',
316
- * currentPositions: [{ companyName: 'Acme Corp', title: 'CEO' }]
337
+ * currentPositions: [{
338
+ * companyName: 'Acme Corp',
339
+ * companyUrnResolutionResult: { website: 'acme.com' }
340
+ * }]
317
341
  * };
318
342
  *
319
343
  * const result = await getEmailsForLinkedInContact(contact, {
@@ -325,13 +349,15 @@ export interface GetEmailsOptions {
325
349
  * email: process.env.SMARTLEAD_EMAIL,
326
350
  * password: process.env.SMARTLEAD_PASSWORD,
327
351
  * },
352
+ * // companyDomain extracted automatically from currentPositions
328
353
  * });
329
354
  *
330
355
  * if (result.success) {
356
+ * console.log('Providers queried:', result.providersQueried);
331
357
  * console.log('Found emails:', result.emails);
332
358
  * // [
333
359
  * // { email: 'jim@acme.com', source: 'ldd', confidence: 95, type: 'business' },
334
- * // { email: 'jim@gmail.com', source: 'ldd', confidence: 70, type: 'personal' }
360
+ * // { email: 'j.demaio@acme.com', source: 'smartprospect', confidence: 85, type: 'business' },
335
361
  * // ]
336
362
  * }
337
363
  * ```
@@ -340,11 +366,13 @@ export declare function getEmailsForLinkedInContact(contact: LinkedInContact, co
340
366
  /**
341
367
  * Get emails for multiple LinkedIn contacts in batch
342
368
  *
343
- * Processes contacts sequentially to avoid rate limits.
369
+ * Processes contacts with controlled concurrency to balance speed and rate limits.
344
370
  */
345
371
  export declare function getEmailsForLinkedInContactsBatch(contacts: LinkedInContact[], config: GetEmailsConfig, options?: GetEmailsOptions & {
346
- /** Delay between requests in ms (default: 200) */
372
+ /** Delay between requests in ms (default: 100) */
347
373
  delayMs?: number;
374
+ /** Max concurrent lookups (default: 3) */
375
+ concurrency?: number;
348
376
  /** Callback for progress updates */
349
377
  onProgress?: (completed: number, total: number, result: GetEmailsResult) => void;
350
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;
@@ -629,11 +662,10 @@ function createLinkedInEnricher(smartProspectConfig, defaultOptions = {}) {
629
662
  /**
630
663
  * Get emails for a LinkedIn contact - UNIFIED FUNCTION
631
664
  *
632
- * This is the main function consumers should use. It:
633
- * 1. Extracts the numeric LinkedIn ID from objectUrn (stable identifier)
634
- * 2. Tries LDD first (FREE, uses numeric ID for reliable lookup)
635
- * 3. Falls back to SmartProspect if LDD has no results (PAID)
636
- * 4. Returns ALL emails with unified scoring
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)
637
669
  *
638
670
  * @example
639
671
  * ```ts
@@ -641,10 +673,13 @@ function createLinkedInEnricher(smartProspectConfig, defaultOptions = {}) {
641
673
  *
642
674
  * // From your LinkedIn Sales Nav search result
643
675
  * const contact = {
644
- * objectUrn: 'urn:li:member:307567', // IMPORTANT: Include this!
676
+ * objectUrn: 'urn:li:member:307567', // Stable ID for LDD
645
677
  * firstName: 'Jim',
646
678
  * lastName: 'DeMaio',
647
- * currentPositions: [{ companyName: 'Acme Corp', title: 'CEO' }]
679
+ * currentPositions: [{
680
+ * companyName: 'Acme Corp',
681
+ * companyUrnResolutionResult: { website: 'acme.com' }
682
+ * }]
648
683
  * };
649
684
  *
650
685
  * const result = await getEmailsForLinkedInContact(contact, {
@@ -656,164 +691,286 @@ function createLinkedInEnricher(smartProspectConfig, defaultOptions = {}) {
656
691
  * email: process.env.SMARTLEAD_EMAIL,
657
692
  * password: process.env.SMARTLEAD_PASSWORD,
658
693
  * },
694
+ * // companyDomain extracted automatically from currentPositions
659
695
  * });
660
696
  *
661
697
  * if (result.success) {
698
+ * console.log('Providers queried:', result.providersQueried);
662
699
  * console.log('Found emails:', result.emails);
663
700
  * // [
664
701
  * // { email: 'jim@acme.com', source: 'ldd', confidence: 95, type: 'business' },
665
- * // { email: 'jim@gmail.com', source: 'ldd', confidence: 70, type: 'personal' }
702
+ * // { email: 'j.demaio@acme.com', source: 'smartprospect', confidence: 85, type: 'business' },
666
703
  * // ]
667
704
  * }
668
705
  * ```
669
706
  */
670
707
  async function getEmailsForLinkedInContact(contact, config, options = {}) {
671
- const { skipLdd = false, skipSmartProspect = false, minMatchConfidence = 60, includeCompany = true, } = options;
708
+ const { skipLdd = false, skipSmartProspect = false, skipPatternGuessing = false, skipPaidProviders = false, minMatchConfidence = 60, paidProviderThreshold = 80, includeCompany = true, } = options;
672
709
  // Extract numeric ID from objectUrn
673
710
  const numericLinkedInId = (0, ldd_1.extractNumericLinkedInId)(contact.objectUrn);
711
+ // Extract company domain from contact
712
+ const companyDomain = config.companyDomain || extractCompanyDomain(contact);
674
713
  const result = {
675
714
  success: false,
676
715
  emails: [],
677
716
  numericLinkedInId,
678
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
+ }
679
728
  };
680
729
  // ==========================================================================
681
- // Step 1: Try LDD first (FREE)
730
+ // Phase 1: FREE providers in PARALLEL (LDD + SmartProspect)
682
731
  // ==========================================================================
732
+ const freeProviderPromises = [];
733
+ // LDD lookup (FREE)
683
734
  if (!skipLdd && config.ldd?.apiUrl && config.ldd?.apiToken) {
684
735
  result.providersQueried.push('ldd');
685
- try {
686
- const lddProvider = (0, ldd_1.createLddProvider)(config.ldd);
687
- const lddResult = await lddProvider({
688
- numericLinkedInId: numericLinkedInId || undefined,
689
- linkedinUsername: undefined, // Let LDD provider extract from contact if needed
690
- firstName: contact.firstName,
691
- lastName: contact.lastName,
692
- });
693
- if (lddResult && 'emails' in lddResult && lddResult.emails.length > 0) {
694
- // LDD found emails!
695
- for (const emailData of lddResult.emails) {
696
- result.emails.push({
697
- email: emailData.email,
698
- source: 'ldd',
699
- confidence: emailData.confidence ?? 90,
700
- type: emailData.metadata?.emailTypeClassified || 'unknown',
701
- verified: emailData.verified ?? true,
702
- metadata: emailData.metadata,
703
- });
704
- }
705
- result.success = true;
706
- }
707
- }
708
- catch (err) {
709
- // LDD failed, continue to SmartProspect
710
- result.error = `LDD error: ${err instanceof Error ? err.message : 'Unknown error'}`;
711
- }
736
+ freeProviderPromises.push(queryLdd(contact, config.ldd, numericLinkedInId, addEmail, result));
712
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;
713
749
  // ==========================================================================
714
- // Step 2: Fall back to SmartProspect if LDD didn't find emails (PAID)
750
+ // Phase 2: Email pattern guessing with MX verification (FREE)
715
751
  // ==========================================================================
716
- if (!result.success && !skipSmartProspect && config.smartprospect) {
717
- result.providersQueried.push('smartprospect');
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
718
805
  try {
719
- const client = (0, smartprospect_1.createSmartProspectClient)(config.smartprospect);
720
- if (!client) {
721
- result.error = 'Failed to create SmartProspect client';
722
- return result;
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
+ });
723
837
  }
724
- // Build search filters
725
- const filters = buildSmartProspectFiltersFromLinkedIn(contact);
726
- const searchFilters = {
727
- firstName: filters.firstName,
728
- lastName: filters.lastName,
729
- limit: 25,
730
- };
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) {
731
868
  if (includeCompany && filters.companyName) {
732
- searchFilters.companyName = filters.companyName;
733
- }
734
- // Search for matching contacts
735
- const searchResponse = await client.search(searchFilters);
736
- if (!searchResponse.success || searchResponse.data.list.length === 0) {
737
- // Try broader search without company
738
- if (includeCompany && filters.companyName) {
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
- }
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;
747
876
  }
748
877
  }
749
- if (searchResponse.data.list.length === 0) {
750
- result.error = result.error
751
- ? `${result.error}; SmartProspect: No candidates found`
752
- : 'SmartProspect: No candidates found';
753
- return result;
754
- }
755
- // Find best match
756
- const matchResult = findBestMatch(contact, searchResponse.data.list, {
757
- minConfidence: minMatchConfidence,
758
- fuzzyNames: true,
759
- fuzzyCompany: true,
760
- });
761
- if (!matchResult) {
762
- result.error = result.error
763
- ? `${result.error}; SmartProspect: No match above ${minMatchConfidence}% confidence`
764
- : `SmartProspect: No match above ${minMatchConfidence}% confidence`;
765
- return result;
766
- }
767
- // Fetch email for matched contact (COSTS CREDITS)
768
- const fetchResponse = await client.fetch([matchResult.smartProspectContact.id]);
769
- if (fetchResponse.success && fetchResponse.data.list.length > 0) {
770
- const enrichedContact = fetchResponse.data.list[0];
771
- if (enrichedContact.email) {
772
- result.emails.push({
773
- email: enrichedContact.email,
774
- source: 'smartprospect',
775
- confidence: matchResult.confidence,
776
- type: 'business', // SmartProspect returns business emails
777
- verified: enrichedContact.verificationStatus === 'verified',
778
- deliverability: enrichedContact.emailDeliverability,
779
- metadata: {
780
- matchQuality: matchResult.quality,
781
- matchedFields: matchResult.matchedFields,
782
- smartProspectId: enrichedContact.id,
783
- company: enrichedContact.company?.name,
784
- title: enrichedContact.title,
785
- },
786
- });
787
- result.success = true;
788
- }
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
+ });
789
911
  }
790
912
  }
791
- catch (err) {
792
- result.error = result.error
793
- ? `${result.error}; SmartProspect error: ${err instanceof Error ? err.message : 'Unknown error'}`
794
- : `SmartProspect error: ${err instanceof Error ? err.message : 'Unknown error'}`;
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
+ }
795
943
  }
796
944
  }
797
- // Sort emails by confidence (highest first)
798
- result.emails.sort((a, b) => b.confidence - a.confidence);
799
- return result;
945
+ catch (err) {
946
+ result.errors?.push(`Pattern: ${err instanceof Error ? err.message : 'Unknown error'}`);
947
+ }
800
948
  }
801
949
  /**
802
950
  * Get emails for multiple LinkedIn contacts in batch
803
951
  *
804
- * Processes contacts sequentially to avoid rate limits.
952
+ * Processes contacts with controlled concurrency to balance speed and rate limits.
805
953
  */
806
954
  async function getEmailsForLinkedInContactsBatch(contacts, config, options = {}) {
807
- const { delayMs = 200, onProgress, ...lookupOptions } = options;
808
- const results = [];
809
- for (let i = 0; i < contacts.length; i++) {
810
- const result = await getEmailsForLinkedInContact(contacts[i], config, lookupOptions);
811
- results.push(result);
812
- if (onProgress) {
813
- onProgress(i + 1, contacts.length, result);
814
- }
815
- // Delay between requests (except for last one)
816
- if (i < contacts.length - 1 && delayMs > 0) {
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) {
817
974
  await new Promise((r) => setTimeout(r, delayMs));
818
975
  }
819
976
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linkedin-secret-sauce",
3
- "version": "0.8.0",
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",