linkedin-secret-sauce 0.12.1 → 0.12.3

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.
Files changed (184) hide show
  1. package/README.md +339 -31
  2. package/dist/cosiall-client.d.ts +1 -1
  3. package/dist/cosiall-client.js +1 -1
  4. package/dist/enrichment/index.d.ts +23 -2
  5. package/dist/enrichment/index.js +38 -22
  6. package/dist/enrichment/matching.d.ts +16 -2
  7. package/dist/enrichment/matching.js +387 -65
  8. package/dist/enrichment/providers/bounceban.d.ts +82 -0
  9. package/dist/enrichment/providers/bounceban.js +447 -0
  10. package/dist/enrichment/providers/bouncer.d.ts +1 -1
  11. package/dist/enrichment/providers/bouncer.js +19 -21
  12. package/dist/enrichment/providers/construct.d.ts +1 -1
  13. package/dist/enrichment/providers/construct.js +22 -38
  14. package/dist/enrichment/providers/cosiall.d.ts +1 -1
  15. package/dist/enrichment/providers/cosiall.js +3 -4
  16. package/dist/enrichment/providers/dropcontact.d.ts +15 -9
  17. package/dist/enrichment/providers/dropcontact.js +188 -19
  18. package/dist/enrichment/providers/hunter.d.ts +8 -1
  19. package/dist/enrichment/providers/hunter.js +52 -28
  20. package/dist/enrichment/providers/index.d.ts +2 -0
  21. package/dist/enrichment/providers/index.js +10 -1
  22. package/dist/enrichment/providers/ldd.d.ts +1 -10
  23. package/dist/enrichment/providers/ldd.js +20 -97
  24. package/dist/enrichment/providers/smartprospect.js +28 -48
  25. package/dist/enrichment/providers/snovio.d.ts +1 -1
  26. package/dist/enrichment/providers/snovio.js +29 -31
  27. package/dist/enrichment/providers/trykitt.d.ts +63 -0
  28. package/dist/enrichment/providers/trykitt.js +210 -0
  29. package/dist/enrichment/types.d.ts +220 -7
  30. package/dist/enrichment/types.js +16 -8
  31. package/dist/enrichment/utils/candidate-parser.d.ts +107 -0
  32. package/dist/enrichment/utils/candidate-parser.js +173 -0
  33. package/dist/enrichment/utils/noop-provider.d.ts +39 -0
  34. package/dist/enrichment/utils/noop-provider.js +37 -0
  35. package/dist/enrichment/utils/rate-limiter.d.ts +103 -0
  36. package/dist/enrichment/utils/rate-limiter.js +204 -0
  37. package/dist/enrichment/utils/validation.d.ts +75 -3
  38. package/dist/enrichment/utils/validation.js +164 -11
  39. package/dist/linkedin-api.d.ts +40 -1
  40. package/dist/linkedin-api.js +160 -27
  41. package/dist/types.d.ts +50 -1
  42. package/dist/utils/lru-cache.d.ts +105 -0
  43. package/dist/utils/lru-cache.js +175 -0
  44. package/docs/COSIALL_PROFILE_EMAILS.md +342 -0
  45. package/docs/ENRICHMENT.md +622 -0
  46. package/docs/INTEGRATION.md +405 -0
  47. package/docs/PLAYGROUND.md +558 -0
  48. package/docs/SALES_SEARCH.md +171 -0
  49. package/docs/api/.nojekyll +1 -0
  50. package/docs/api/assets/hierarchy.js +1 -0
  51. package/docs/api/assets/highlight.css +92 -0
  52. package/docs/api/assets/icons.js +18 -0
  53. package/docs/api/assets/icons.svg +1 -0
  54. package/docs/api/assets/main.js +60 -0
  55. package/docs/api/assets/navigation.js +1 -0
  56. package/docs/api/assets/search.js +1 -0
  57. package/docs/api/assets/style.css +1633 -0
  58. package/docs/api/classes/LinkedInClientError.html +37 -0
  59. package/docs/api/functions/_testGetAccountCookies.html +4 -0
  60. package/docs/api/functions/_testGetAccountEntry.html +4 -0
  61. package/docs/api/functions/_testGetAllAccountIds.html +3 -0
  62. package/docs/api/functions/_testGetPoolState.html +3 -0
  63. package/docs/api/functions/adminResetAccount.html +1 -0
  64. package/docs/api/functions/adminSetCooldown.html +1 -0
  65. package/docs/api/functions/buildCookieHeader.html +1 -0
  66. package/docs/api/functions/clearAllSmartLeadTokens.html +2 -0
  67. package/docs/api/functions/clearRequestHistory.html +1 -0
  68. package/docs/api/functions/clearSessionAccount.html +1 -0
  69. package/docs/api/functions/clearSmartLeadToken.html +2 -0
  70. package/docs/api/functions/createEnrichmentClient.html +8 -0
  71. package/docs/api/functions/extractCsrfToken.html +1 -0
  72. package/docs/api/functions/extractLinkedInHandle.html +7 -0
  73. package/docs/api/functions/fetchCookiesFromCosiall.html +14 -0
  74. package/docs/api/functions/fetchProfileEmailsFromCosiall.html +18 -0
  75. package/docs/api/functions/forceRefreshCookies.html +1 -0
  76. package/docs/api/functions/getAccountForSession.html +1 -0
  77. package/docs/api/functions/getAccountsSummary.html +1 -0
  78. package/docs/api/functions/getCompaniesBatch.html +5 -0
  79. package/docs/api/functions/getCompanyById.html +9 -0
  80. package/docs/api/functions/getCompanyByUrl.html +1 -0
  81. package/docs/api/functions/getConfig.html +1 -0
  82. package/docs/api/functions/getCookiePoolHealth.html +1 -0
  83. package/docs/api/functions/getProfileByUrn.html +17 -0
  84. package/docs/api/functions/getProfileByVanity.html +10 -0
  85. package/docs/api/functions/getProfilesBatch.html +1 -0
  86. package/docs/api/functions/getRequestHistory.html +1 -0
  87. package/docs/api/functions/getSalesNavigatorProfileDetails.html +1 -0
  88. package/docs/api/functions/getSalesNavigatorProfileFull.html +16 -0
  89. package/docs/api/functions/getSmartLeadToken.html +1 -0
  90. package/docs/api/functions/getSmartLeadTokenCacheStats.html +2 -0
  91. package/docs/api/functions/getSmartLeadUser.html +2 -0
  92. package/docs/api/functions/getSnapshot.html +1 -0
  93. package/docs/api/functions/getYearsAtCompanyOptions.html +2 -0
  94. package/docs/api/functions/getYearsInPositionOptions.html +2 -0
  95. package/docs/api/functions/getYearsOfExperienceOptions.html +2 -0
  96. package/docs/api/functions/incrementMetric.html +1 -0
  97. package/docs/api/functions/initializeCookiePool.html +1 -0
  98. package/docs/api/functions/initializeLinkedInClient.html +1 -0
  99. package/docs/api/functions/isBusinessEmail.html +4 -0
  100. package/docs/api/functions/isDisposableDomain.html +4 -0
  101. package/docs/api/functions/isDisposableEmail.html +4 -0
  102. package/docs/api/functions/isPersonalDomain.html +4 -0
  103. package/docs/api/functions/isPersonalEmail.html +4 -0
  104. package/docs/api/functions/isRoleAccount.html +4 -0
  105. package/docs/api/functions/isValidEmailSyntax.html +4 -0
  106. package/docs/api/functions/parseFullProfile.html +15 -0
  107. package/docs/api/functions/parseSalesSearchResults.html +1 -0
  108. package/docs/api/functions/reportAccountFailure.html +1 -0
  109. package/docs/api/functions/reportAccountSuccess.html +1 -0
  110. package/docs/api/functions/resolveCompanyUniversalName.html +1 -0
  111. package/docs/api/functions/searchSalesLeads.html +16 -0
  112. package/docs/api/functions/selectAccountForRequest.html +1 -0
  113. package/docs/api/functions/setAccountForSession.html +1 -0
  114. package/docs/api/functions/typeahead.html +1 -0
  115. package/docs/api/functions/verifyEmailMx.html +1 -0
  116. package/docs/api/hierarchy.html +1 -0
  117. package/docs/api/index.html +12 -0
  118. package/docs/api/interfaces/AccountCookies.html +4 -0
  119. package/docs/api/interfaces/BatchEnrichmentOptions.html +14 -0
  120. package/docs/api/interfaces/CacheAdapter.html +6 -0
  121. package/docs/api/interfaces/CanonicalEmail.html +14 -0
  122. package/docs/api/interfaces/Company.html +17 -0
  123. package/docs/api/interfaces/ConstructConfig.html +8 -0
  124. package/docs/api/interfaces/CosiallProfileEmailsResponse.html +11 -0
  125. package/docs/api/interfaces/DropcontactConfig.html +3 -0
  126. package/docs/api/interfaces/EnrichmentCandidate.html +34 -0
  127. package/docs/api/interfaces/EnrichmentClient.html +10 -0
  128. package/docs/api/interfaces/EnrichmentClientConfig.html +12 -0
  129. package/docs/api/interfaces/EnrichmentLogger.html +6 -0
  130. package/docs/api/interfaces/EnrichmentOptions.html +10 -0
  131. package/docs/api/interfaces/HunterConfig.html +3 -0
  132. package/docs/api/interfaces/LddConfig.html +4 -0
  133. package/docs/api/interfaces/LddProfileData.html +6 -0
  134. package/docs/api/interfaces/LinkedInClientConfig.html +20 -0
  135. package/docs/api/interfaces/LinkedInCookie.html +9 -0
  136. package/docs/api/interfaces/LinkedInPosition.html +14 -0
  137. package/docs/api/interfaces/LinkedInProfile.html +21 -0
  138. package/docs/api/interfaces/LinkedInSpotlightBadge.html +5 -0
  139. package/docs/api/interfaces/LinkedInTenure.html +3 -0
  140. package/docs/api/interfaces/Metrics.html +22 -0
  141. package/docs/api/interfaces/MetricsSnapshot.html +23 -0
  142. package/docs/api/interfaces/ProfileEducation.html +8 -0
  143. package/docs/api/interfaces/ProfileEmailsLookupOptions.html +9 -0
  144. package/docs/api/interfaces/ProfilePosition.html +12 -0
  145. package/docs/api/interfaces/ProfileSkill.html +3 -0
  146. package/docs/api/interfaces/ProviderResult.html +11 -0
  147. package/docs/api/interfaces/ProvidersConfig.html +17 -0
  148. package/docs/api/interfaces/RequestHistoryEntry.html +8 -0
  149. package/docs/api/interfaces/SalesLeadSearchResult.html +31 -0
  150. package/docs/api/interfaces/SalesNavigatorContactInfo.html +5 -0
  151. package/docs/api/interfaces/SalesNavigatorPosition.html +11 -0
  152. package/docs/api/interfaces/SalesNavigatorProfile.html +9 -0
  153. package/docs/api/interfaces/SalesNavigatorProfileFull.html +24 -0
  154. package/docs/api/interfaces/SearchSalesResult.html +5 -0
  155. package/docs/api/interfaces/SmartLeadAuthConfig.html +6 -0
  156. package/docs/api/interfaces/SmartLeadCredentials.html +3 -0
  157. package/docs/api/interfaces/SmartLeadLoginResponse.html +3 -0
  158. package/docs/api/interfaces/SmartLeadUser.html +8 -0
  159. package/docs/api/interfaces/SmartProspectConfig.html +19 -0
  160. package/docs/api/interfaces/SmartProspectContact.html +24 -0
  161. package/docs/api/interfaces/SmartProspectSearchFilters.html +42 -0
  162. package/docs/api/interfaces/TypeaheadItem.html +3 -0
  163. package/docs/api/interfaces/TypeaheadResult.html +3 -0
  164. package/docs/api/interfaces/VerificationResult.html +16 -0
  165. package/docs/api/types/CostCallback.html +2 -0
  166. package/docs/api/types/Geo.html +4 -0
  167. package/docs/api/types/LddApiResponse.html +1 -0
  168. package/docs/api/types/ProviderFunc.html +2 -0
  169. package/docs/api/types/ProviderName.html +2 -0
  170. package/docs/api/types/SalesSearchFilters.html +7 -0
  171. package/docs/api/types/TypeaheadType.html +1 -0
  172. package/docs/api/variables/COMPANY_SIZE_OPTIONS.html +1 -0
  173. package/docs/api/variables/DEFAULT_PROVIDER_ORDER.html +20 -0
  174. package/docs/api/variables/DISPOSABLE_DOMAINS.html +2 -0
  175. package/docs/api/variables/FUNCTION_OPTIONS.html +1 -0
  176. package/docs/api/variables/INDUSTRY_OPTIONS.html +1 -0
  177. package/docs/api/variables/LANGUAGE_OPTIONS.html +1 -0
  178. package/docs/api/variables/PERSONAL_DOMAINS.html +2 -0
  179. package/docs/api/variables/PROVIDER_COSTS.html +15 -0
  180. package/docs/api/variables/REGION_OPTIONS.html +1 -0
  181. package/docs/api/variables/SENIORITY_OPTIONS.html +3 -0
  182. package/docs/api/variables/YEARS_OPTIONS.html +1 -0
  183. package/docs/index.html +98 -0
  184. package/package.json +16 -5
@@ -258,7 +258,7 @@ export declare function createLinkedInEnricher(smartProspectConfig: SmartProspec
258
258
  /**
259
259
  * Email source - where the email was found
260
260
  */
261
- export type EmailSource = "ldd" | "smartprospect" | "cosiall" | "linkedin" | "pattern" | "hunter" | "bouncer" | "snovio";
261
+ export type EmailSource = "ldd" | "smartprospect" | "cosiall" | "trykitt" | "linkedin" | "pattern" | "hunter" | "bouncer" | "bounceban" | "snovio";
262
262
  /**
263
263
  * Email result from unified lookup
264
264
  */
@@ -309,6 +309,10 @@ export interface GetEmailsConfig {
309
309
  cosiall?: {
310
310
  enabled?: boolean;
311
311
  };
312
+ /** TryKitt.ai configuration (FREE for individuals - unlimited) */
313
+ trykitt?: {
314
+ apiKey: string;
315
+ };
312
316
  /** Company domain for email pattern guessing (optional - if not provided, will try to discover) */
313
317
  companyDomain?: string;
314
318
  /**
@@ -327,6 +331,12 @@ export interface GetEmailsConfig {
327
331
  bouncer?: {
328
332
  apiKey: string;
329
333
  };
334
+ /** BounceBan configuration (FREE single / catch-all specialist) */
335
+ bounceban?: {
336
+ apiKey: string;
337
+ useDeepVerify?: boolean;
338
+ useWaterfall?: boolean;
339
+ };
330
340
  /** Snov.io configuration (PAID - email finder) */
331
341
  snovio?: {
332
342
  userId: string;
@@ -343,11 +353,15 @@ export interface GetEmailsOptions {
343
353
  skipSmartProspect?: boolean;
344
354
  /** Skip Cosiall Profile Emails lookup (default: false) */
345
355
  skipCosiall?: boolean;
356
+ /** Skip TryKitt.ai lookup (default: false) */
357
+ skipTryKitt?: boolean;
346
358
  /** Skip email pattern guessing (default: false) */
347
359
  skipPatternGuessing?: boolean;
360
+ /** Skip BounceBan catch-all verification (default: false) */
361
+ skipBounceBan?: boolean;
348
362
  /** Skip LinkedIn company lookup for domain discovery (default: false) */
349
363
  skipLinkedInCompanyLookup?: boolean;
350
- /** Skip paid providers Hunter/Apollo (default: false) */
364
+ /** Skip paid providers Hunter/Snovio (default: false) */
351
365
  skipPaidProviders?: boolean;
352
366
  /** Minimum match confidence for SmartProspect (default: 60) */
353
367
  minMatchConfidence?: number;
@@ -780,7 +780,7 @@ function createLinkedInEnricher(smartProspectConfig, defaultOptions = {}) {
780
780
  async function getEmailsForLinkedInContact(contactOrLead, config, options = {}) {
781
781
  // Normalize input - accept either LinkedInContact or raw SalesLeadSearchResult
782
782
  const contact = normalizeToContact(contactOrLead);
783
- const { skipLdd = false, skipSmartProspect = false, skipCosiall = false, skipPatternGuessing = false, skipPaidProviders = false, minMatchConfidence = 60, paidProviderThreshold = 80, includeCompany = true, } = options;
783
+ const { skipLdd = false, skipSmartProspect = false, skipCosiall = false, skipTryKitt = false, skipPatternGuessing = false, skipBounceBan = false, skipPaidProviders = false, minMatchConfidence = 60, paidProviderThreshold = 80, includeCompany = true, } = options;
784
784
  // Extract numeric ID from objectUrn
785
785
  const numericLinkedInId = (0, ldd_1.extractNumericLinkedInId)(contact.objectUrn);
786
786
  // Extract company domain from contact (LinkedIn data - often missing)
@@ -792,28 +792,75 @@ async function getEmailsForLinkedInContact(contactOrLead, config, options = {})
792
792
  providersQueried: [],
793
793
  errors: [],
794
794
  };
795
- // Track seen emails to deduplicate
796
- const seenEmails = new Set();
795
+ // Track all emails with their sources for confidence boosting
796
+ const emailsByAddress = new Map();
797
797
  const addEmail = (emailResult) => {
798
798
  const lower = emailResult.email.toLowerCase();
799
- if (!seenEmails.has(lower)) {
800
- seenEmails.add(lower);
801
- result.emails.push(emailResult);
799
+ const existing = emailsByAddress.get(lower);
800
+ if (existing) {
801
+ // Email already found from another source - track for confidence boosting
802
+ existing.results.push(emailResult);
803
+ existing.sources.add(emailResult.source);
802
804
  }
805
+ else {
806
+ emailsByAddress.set(lower, {
807
+ results: [emailResult],
808
+ sources: new Set([emailResult.source]),
809
+ });
810
+ }
811
+ };
812
+ /**
813
+ * Merge emails and boost confidence when found from multiple sources
814
+ * - 2 sources: boost to at least 95% confidence
815
+ * - 3+ sources: boost to 100% confidence
816
+ */
817
+ const mergeAndBoostEmails = () => {
818
+ const mergedEmails = [];
819
+ for (const [_email, { results, sources }] of emailsByAddress) {
820
+ // Find the best result (highest confidence)
821
+ const bestResult = results.reduce((best, current) => current.confidence > best.confidence ? current : best);
822
+ // Boost confidence based on number of sources
823
+ let boostedConfidence = bestResult.confidence;
824
+ if (sources.size >= 3) {
825
+ boostedConfidence = 100;
826
+ }
827
+ else if (sources.size === 2) {
828
+ boostedConfidence = Math.max(boostedConfidence, 95);
829
+ }
830
+ // Create merged result
831
+ const mergedResult = {
832
+ ...bestResult,
833
+ confidence: boostedConfidence,
834
+ metadata: {
835
+ ...bestResult.metadata,
836
+ foundBySources: Array.from(sources),
837
+ sourceCount: sources.size,
838
+ confidenceBoosted: sources.size > 1,
839
+ originalConfidence: bestResult.confidence,
840
+ },
841
+ };
842
+ // If verified by any source, mark as verified
843
+ if (results.some((r) => r.verified)) {
844
+ mergedResult.verified = true;
845
+ }
846
+ mergedEmails.push(mergedResult);
847
+ }
848
+ return mergedEmails;
803
849
  };
804
850
  // Track company domain discovered from SmartProspect (since LinkedIn doesn't provide it)
805
851
  let discoveredCompanyDomain = null;
806
852
  // ==========================================================================
807
- // Phase 1: FREE providers in PARALLEL (LDD + SmartProspect + Cosiall)
853
+ // Phase 1: FREE database providers in PARALLEL (no LinkedIn API calls)
854
+ // LDD + SmartProspect + Cosiall - these are your own databases or already paid
808
855
  // ==========================================================================
809
856
  const freeProviderPromises = [];
810
- // LDD lookup (FREE)
857
+ // LDD lookup (FREE - your ~500M database)
811
858
  if (!skipLdd && config.ldd?.apiUrl && config.ldd?.apiToken) {
812
859
  result.providersQueried.push("ldd");
813
860
  freeProviderPromises.push(queryLdd(contact, config.ldd, numericLinkedInId, addEmail, result));
814
861
  }
815
- // SmartProspect lookup (FREE for FlexIQ)
816
- // This also extracts company domain for pattern guessing
862
+ // SmartProspect lookup (FREE for FlexIQ - already paying monthly)
863
+ // This also extracts company domain for pattern guessing (no extra API call)
817
864
  if (!skipSmartProspect && config.smartprospect) {
818
865
  result.providersQueried.push("smartprospect");
819
866
  freeProviderPromises.push(querySmartProspect(contact, config.smartprospect, minMatchConfidence, includeCompany, addEmail, result).then((domain) => {
@@ -822,63 +869,86 @@ async function getEmailsForLinkedInContact(contactOrLead, config, options = {})
822
869
  }
823
870
  }));
824
871
  }
825
- // Cosiall Profile Emails lookup (FREE)
872
+ // Cosiall Profile Emails lookup (FREE - your database)
826
873
  if (!skipCosiall && config.cosiall?.enabled !== false) {
827
874
  result.providersQueried.push("cosiall");
828
875
  freeProviderPromises.push(queryCosiall(contact, addEmail, result));
829
876
  }
830
- // Wait for both free providers
877
+ // TryKitt.ai lookup (FREE for individuals - unlimited)
878
+ // AI-powered email finder with enterprise identity server catch-all verification
879
+ if (!skipTryKitt && config.trykitt?.apiKey) {
880
+ result.providersQueried.push("trykitt");
881
+ freeProviderPromises.push(queryTryKitt(contact, config.trykitt, linkedInCompanyDomain || discoveredCompanyDomain, addEmail, result));
882
+ }
883
+ // Wait for all Phase 1 free providers
831
884
  await Promise.all(freeProviderPromises);
832
- // Check if we have good enough results already
833
- const bestConfidenceAfterFreeProviders = result.emails.length > 0
885
+ // Merge emails and calculate best confidence after Phase 1
886
+ result.emails = mergeAndBoostEmails();
887
+ const phase1BestConfidence = result.emails.length > 0
834
888
  ? Math.max(...result.emails.map((e) => e.confidence))
835
889
  : 0;
836
890
  // ==========================================================================
837
- // Phase 2: Domain Discovery (if needed for pattern guessing)
891
+ // Phase 2: Domain Discovery + Pattern Guessing
892
+ // ONLY if Phase 1 confidence < threshold (to minimize LinkedIn API calls)
838
893
  // ==========================================================================
839
- // Priority: LinkedIn contact data > SmartProspect > LinkedIn Company API
840
894
  let companyDomain = linkedInCompanyDomain || discoveredCompanyDomain;
841
- // If no domain yet and pattern guessing is enabled, try LinkedIn company lookup
842
- if (!companyDomain &&
843
- !skipPatternGuessing &&
844
- !options.skipLinkedInCompanyLookup &&
845
- config.linkedInCompanyLookup &&
846
- bestConfidenceAfterFreeProviders < paidProviderThreshold) {
847
- // Extract company ID from companyUrn
848
- const companyUrn = contact.currentPositions?.[0]?.companyUrn;
849
- if (companyUrn) {
850
- const companyId = extractCompanyIdFromUrn(companyUrn);
851
- if (companyId) {
852
- result.providersQueried.push("linkedin");
853
- try {
854
- const company = await config.linkedInCompanyLookup(companyId);
855
- if (company?.websiteUrl) {
856
- companyDomain = extractDomainFromUrl(company.websiteUrl);
857
- if (companyDomain) {
858
- discoveredCompanyDomain = companyDomain;
895
+ // Skip Phase 2 entirely if we already have high confidence emails
896
+ if (phase1BestConfidence < paidProviderThreshold) {
897
+ // Try LinkedIn Company Lookup for domain discovery (EXPENSIVE - uses LinkedIn API)
898
+ // Only if we don't have a domain yet
899
+ if (!companyDomain &&
900
+ !skipPatternGuessing &&
901
+ !options.skipLinkedInCompanyLookup &&
902
+ config.linkedInCompanyLookup) {
903
+ const companyUrn = contact.currentPositions?.[0]?.companyUrn;
904
+ if (companyUrn) {
905
+ const companyId = extractCompanyIdFromUrn(companyUrn);
906
+ if (companyId) {
907
+ result.providersQueried.push("linkedin");
908
+ try {
909
+ const company = await config.linkedInCompanyLookup(companyId);
910
+ if (company?.websiteUrl) {
911
+ companyDomain = extractDomainFromUrl(company.websiteUrl);
912
+ if (companyDomain) {
913
+ discoveredCompanyDomain = companyDomain;
914
+ }
859
915
  }
860
916
  }
861
- }
862
- catch (err) {
863
- result.errors?.push(`LinkedIn: ${err instanceof Error ? err.message : "Company lookup failed"}`);
917
+ catch (err) {
918
+ result.errors?.push(`LinkedIn: ${err instanceof Error ? err.message : "Company lookup failed"}`);
919
+ }
864
920
  }
865
921
  }
866
922
  }
923
+ // Store discovered domain in result for visibility
924
+ if (discoveredCompanyDomain && !linkedInCompanyDomain) {
925
+ result.discoveredCompanyDomain = discoveredCompanyDomain;
926
+ }
927
+ // Run pattern guessing if we have a domain (from LinkedIn contact, SmartProspect, or LinkedIn API)
928
+ if (!skipPatternGuessing && companyDomain) {
929
+ result.providersQueried.push("pattern");
930
+ await queryPatternGuessing(contact, companyDomain, addEmail, result);
931
+ // Re-merge after pattern guessing
932
+ result.emails = mergeAndBoostEmails();
933
+ }
934
+ // BounceBan catch-all verification (FREE single emails)
935
+ // Use BounceBan to verify pattern-guessed emails, especially on catch-all domains
936
+ if (!skipBounceBan &&
937
+ config.bounceban?.apiKey &&
938
+ result.emails.length > 0) {
939
+ // Only verify emails that are from pattern guessing or have low confidence
940
+ const emailsToVerify = result.emails
941
+ .filter((e) => e.source === "pattern" || (e.confidence < 80 && !e.verified))
942
+ .slice(0, 3); // Verify top 3 to save credits
943
+ if (emailsToVerify.length > 0) {
944
+ result.providersQueried.push("bounceban");
945
+ await queryBounceBan(emailsToVerify, config.bounceban, addEmail, result);
946
+ // Re-merge after BounceBan verification
947
+ result.emails = mergeAndBoostEmails();
948
+ }
949
+ }
867
950
  }
868
- // Store discovered domain in result for visibility
869
- if (discoveredCompanyDomain && !linkedInCompanyDomain) {
870
- result.discoveredCompanyDomain = discoveredCompanyDomain;
871
- }
872
- // ==========================================================================
873
- // Phase 3: Email pattern guessing with MX verification (FREE)
874
- // ==========================================================================
875
- if (!skipPatternGuessing &&
876
- companyDomain &&
877
- bestConfidenceAfterFreeProviders < paidProviderThreshold) {
878
- result.providersQueried.push("pattern");
879
- await queryPatternGuessing(contact, companyDomain, addEmail, result);
880
- }
881
- // Recalculate best confidence
951
+ // Calculate final best confidence
882
952
  const finalBestConfidence = result.emails.length > 0
883
953
  ? Math.max(...result.emails.map((e) => e.confidence))
884
954
  : 0;
@@ -887,27 +957,55 @@ async function getEmailsForLinkedInContact(contactOrLead, config, options = {})
887
957
  // ==========================================================================
888
958
  if (!skipPaidProviders && finalBestConfidence < paidProviderThreshold) {
889
959
  // Only use paid providers if we have low confidence or no results
890
- // TODO: Implement Hunter, Bouncer, Snovio providers when needed
891
- // For now, just mark that we would have queried them
892
- if (config.hunter?.apiKey ||
893
- config.bouncer?.apiKey ||
894
- config.snovio?.userId) {
895
- // result.providersQueried.push('hunter');
896
- // result.providersQueried.push('bouncer');
897
- // result.providersQueried.push('snovio');
898
- // await queryPaidProviders(contact, config, addEmail, result);
960
+ // Hunter.io - Email Finder ($0.005/lookup)
961
+ if (config.hunter?.apiKey) {
962
+ result.providersQueried.push("hunter");
963
+ // Extract linkedin handle from contact if available
964
+ // Note: For best results, caller should fetch profile with getSalesNavigatorProfileFull
965
+ // and extract handle using extractLinkedInHandle(profile.flagshipProfileUrl)
966
+ let linkedinHandle = null;
967
+ // Try to extract from entityUrn or any linkedin URL fields
968
+ // (These won't typically have the vanity, but check anyway)
969
+ const contactAny = contact;
970
+ if (contactAny.linkedinHandle) {
971
+ linkedinHandle = String(contactAny.linkedinHandle);
972
+ }
973
+ else if (contactAny.flagshipProfileUrl) {
974
+ // Extract handle from flagship URL
975
+ const match = String(contactAny.flagshipProfileUrl).match(/linkedin\.com\/in\/([^\/\?]+)/i);
976
+ if (match)
977
+ linkedinHandle = match[1];
978
+ }
979
+ await queryHunter(contact, config.hunter, companyDomain || null, linkedinHandle, addEmail, result);
980
+ // Re-merge after Hunter
981
+ result.emails = mergeAndBoostEmails();
982
+ }
983
+ // Snovio - Email Finder ($0.02/lookup) - best for catch-all domains
984
+ if (config.snovio?.userId && config.snovio?.apiSecret && companyDomain) {
985
+ result.providersQueried.push("snovio");
986
+ await querySnovio(contact, config.snovio, companyDomain, addEmail, result);
987
+ // Re-merge after Snovio
988
+ result.emails = mergeAndBoostEmails();
899
989
  }
990
+ // Note: Bouncer is a VERIFICATION provider (verifies existing emails).
991
+ // It's automatically used by the construct provider for SMTP verification.
992
+ // No need to call it separately here since pattern guessing already uses
993
+ // the built-in mx verification which is similar to Bouncer.
900
994
  }
995
+ // Final merge (in case paid providers added emails) and sort
996
+ result.emails = mergeAndBoostEmails();
901
997
  // Sort emails by confidence (highest first), then by source priority
902
998
  const sourcePriority = {
903
999
  ldd: 0, // Highest priority - your own data
904
1000
  smartprospect: 1,
905
1001
  cosiall: 2, // Cosiall Profile Emails (FREE)
906
- linkedin: 3, // LinkedIn company lookup (for domain discovery, doesn't provide emails)
907
- pattern: 4,
908
- hunter: 5,
909
- bouncer: 6,
910
- snovio: 7,
1002
+ trykitt: 3, // TryKitt.ai (FREE for individuals)
1003
+ linkedin: 4, // LinkedIn company lookup (for domain discovery, doesn't provide emails)
1004
+ pattern: 5,
1005
+ bounceban: 6, // BounceBan catch-all verification (FREE single)
1006
+ hunter: 7,
1007
+ bouncer: 8,
1008
+ snovio: 9,
911
1009
  };
912
1010
  result.emails.sort((a, b) => {
913
1011
  if (b.confidence !== a.confidence) {
@@ -1041,6 +1139,117 @@ async function queryCosiall(contact, addEmail, result) {
1041
1139
  }
1042
1140
  }
1043
1141
  }
1142
+ /**
1143
+ * Query TryKitt.ai provider (FREE for individuals)
1144
+ * AI-powered email finder with enterprise identity server catch-all verification
1145
+ */
1146
+ async function queryTryKitt(contact, trykittConfig, companyDomain, addEmail, result) {
1147
+ try {
1148
+ // Import TryKitt provider dynamically
1149
+ const { createTryKittProvider } = await Promise.resolve().then(() => __importStar(require("./providers/trykitt")));
1150
+ const trykittProvider = createTryKittProvider(trykittConfig);
1151
+ // Build candidate with available data
1152
+ const fullName = contact.fullName || `${contact.firstName} ${contact.lastName}`.trim();
1153
+ // Need both name and domain to search
1154
+ if (!fullName || !companyDomain) {
1155
+ return; // Silently skip - missing required data
1156
+ }
1157
+ const candidate = {
1158
+ fullName,
1159
+ firstName: contact.firstName,
1160
+ lastName: contact.lastName,
1161
+ domain: companyDomain,
1162
+ };
1163
+ // Add LinkedIn URL if available (improves TryKitt accuracy)
1164
+ if (contact.entityUrn) {
1165
+ // entityUrn is like "urn:li:fs_salesProfile:..." - not useful for TryKitt
1166
+ }
1167
+ const trykittResult = await trykittProvider(candidate);
1168
+ if (trykittResult) {
1169
+ // Single result
1170
+ if ("email" in trykittResult && trykittResult.email) {
1171
+ addEmail({
1172
+ email: trykittResult.email,
1173
+ source: "trykitt",
1174
+ confidence: trykittResult.score ?? 80,
1175
+ type: "business",
1176
+ verified: trykittResult.verified ?? false,
1177
+ metadata: {
1178
+ provider: "trykitt",
1179
+ aiPowered: true,
1180
+ },
1181
+ });
1182
+ }
1183
+ // Multi result
1184
+ else if ("emails" in trykittResult) {
1185
+ const multiResult = trykittResult;
1186
+ for (const emailData of multiResult.emails) {
1187
+ addEmail({
1188
+ email: emailData.email,
1189
+ source: "trykitt",
1190
+ confidence: emailData.confidence ?? 70,
1191
+ type: "business",
1192
+ verified: emailData.verified ?? false,
1193
+ isCatchAll: emailData.isCatchAll,
1194
+ metadata: emailData.metadata,
1195
+ });
1196
+ }
1197
+ }
1198
+ }
1199
+ }
1200
+ catch (err) {
1201
+ result.errors?.push(`TryKitt: ${err instanceof Error ? err.message : "Unknown error"}`);
1202
+ }
1203
+ }
1204
+ /**
1205
+ * Query BounceBan for catch-all verification
1206
+ * Verifies pattern-guessed emails with 85-95% accuracy on catch-all domains
1207
+ */
1208
+ async function queryBounceBan(emailsToVerify, bouncebanConfig, addEmail, result) {
1209
+ try {
1210
+ // Import BounceBan provider dynamically
1211
+ const { verifyEmailWithBounceBan } = await Promise.resolve().then(() => __importStar(require("./providers/bounceban")));
1212
+ for (const emailResult of emailsToVerify) {
1213
+ const verification = await verifyEmailWithBounceBan(emailResult.email, {
1214
+ apiKey: bouncebanConfig.apiKey,
1215
+ useDeepVerify: bouncebanConfig.useDeepVerify ?? false,
1216
+ useWaterfall: bouncebanConfig.useWaterfall ?? true,
1217
+ });
1218
+ if (verification && verification.status === "success") {
1219
+ // Map BounceBan result to confidence
1220
+ let confidence = verification.score;
1221
+ const verified = verification.result === "deliverable";
1222
+ // Reduce confidence for catch-all domains even if deliverable
1223
+ if (verification.is_accept_all && verified) {
1224
+ confidence = Math.min(confidence, 75);
1225
+ }
1226
+ // Only add if it's potentially deliverable
1227
+ if (verification.result === "deliverable" ||
1228
+ verification.result === "risky") {
1229
+ addEmail({
1230
+ email: verification.email,
1231
+ source: "bounceban",
1232
+ confidence,
1233
+ type: "business",
1234
+ verified,
1235
+ isCatchAll: verification.is_accept_all,
1236
+ metadata: {
1237
+ bounceBanResult: verification.result,
1238
+ bounceBanScore: verification.score,
1239
+ smtpProvider: verification.smtp_provider,
1240
+ isDisposable: verification.is_disposable,
1241
+ isRole: verification.is_role,
1242
+ originalSource: emailResult.source,
1243
+ },
1244
+ });
1245
+ }
1246
+ }
1247
+ }
1248
+ }
1249
+ catch (err) {
1250
+ result.errors?.push(`BounceBan: ${err instanceof Error ? err.message : "Unknown error"}`);
1251
+ }
1252
+ }
1044
1253
  /**
1045
1254
  * Query SmartProspect provider
1046
1255
  * Returns the company domain if found (for pattern guessing fallback)
@@ -1167,6 +1376,119 @@ async function queryPatternGuessing(contact, companyDomain, addEmail, result) {
1167
1376
  result.errors?.push(`Pattern: ${err instanceof Error ? err.message : "Unknown error"}`);
1168
1377
  }
1169
1378
  }
1379
+ /**
1380
+ * Query Hunter.io Email Finder (PAID provider)
1381
+ *
1382
+ * Supports multiple modes:
1383
+ * 1. linkedin_handle - Most accurate (requires getSalesNavigatorProfileFull to get handle)
1384
+ * 2. domain + first_name + last_name
1385
+ * 3. company + first_name + last_name
1386
+ */
1387
+ async function queryHunter(contact, hunterConfig, companyDomain, linkedinHandle, addEmail, result) {
1388
+ try {
1389
+ // Import Hunter provider dynamically
1390
+ const { createHunterProvider } = await Promise.resolve().then(() => __importStar(require("./providers/hunter")));
1391
+ const hunterProvider = createHunterProvider(hunterConfig);
1392
+ // Build candidate with all available data
1393
+ const candidate = {
1394
+ firstName: contact.firstName,
1395
+ lastName: contact.lastName,
1396
+ };
1397
+ // Add linkedin_handle if available (best for Hunter)
1398
+ if (linkedinHandle) {
1399
+ candidate.linkedinHandle = linkedinHandle;
1400
+ }
1401
+ // Add domain if available
1402
+ if (companyDomain) {
1403
+ candidate.domain = companyDomain;
1404
+ }
1405
+ // Add company name if available
1406
+ const companyName = contact.currentPositions?.[0]?.companyName;
1407
+ if (companyName) {
1408
+ candidate.company = companyName;
1409
+ }
1410
+ const hunterResult = await hunterProvider(candidate);
1411
+ if (hunterResult) {
1412
+ // Single result
1413
+ if ("email" in hunterResult && hunterResult.email) {
1414
+ addEmail({
1415
+ email: hunterResult.email,
1416
+ source: "hunter",
1417
+ confidence: hunterResult.score ?? 70,
1418
+ type: "business",
1419
+ verified: hunterResult.verified ?? false,
1420
+ metadata: {
1421
+ usedLinkedInHandle: !!linkedinHandle,
1422
+ usedDomain: !!companyDomain,
1423
+ usedCompany: !!companyName && !companyDomain && !linkedinHandle,
1424
+ },
1425
+ });
1426
+ }
1427
+ // Multi result (from domain-search)
1428
+ else if ("emails" in hunterResult && hunterResult.emails.length > 0) {
1429
+ for (const emailData of hunterResult.emails) {
1430
+ addEmail({
1431
+ email: emailData.email,
1432
+ source: "hunter",
1433
+ confidence: emailData.confidence ?? 60,
1434
+ type: "business",
1435
+ verified: emailData.verified ?? false,
1436
+ metadata: emailData.metadata,
1437
+ });
1438
+ }
1439
+ }
1440
+ }
1441
+ }
1442
+ catch (err) {
1443
+ result.errors?.push(`Hunter: ${err instanceof Error ? err.message : "Unknown error"}`);
1444
+ }
1445
+ }
1446
+ /**
1447
+ * Query Snovio for email addresses
1448
+ */
1449
+ async function querySnovio(contact, snovioConfig, companyDomain, addEmail, result) {
1450
+ try {
1451
+ // Import Snovio provider dynamically
1452
+ const { createSnovioProvider } = await Promise.resolve().then(() => __importStar(require("./providers/snovio")));
1453
+ const snovioProvider = createSnovioProvider(snovioConfig);
1454
+ // Build candidate with available data
1455
+ const candidate = {
1456
+ firstName: contact.firstName,
1457
+ lastName: contact.lastName,
1458
+ domain: companyDomain,
1459
+ };
1460
+ const snovioResult = await snovioProvider(candidate);
1461
+ if (snovioResult) {
1462
+ // Single result
1463
+ if ("email" in snovioResult && snovioResult.email) {
1464
+ addEmail({
1465
+ email: snovioResult.email,
1466
+ source: "snovio",
1467
+ confidence: snovioResult.score ?? 70,
1468
+ type: "business",
1469
+ verified: snovioResult.verified ?? false,
1470
+ });
1471
+ }
1472
+ // Multi result
1473
+ else if ("emails" in snovioResult && snovioResult.emails.length > 0) {
1474
+ for (const emailData of snovioResult.emails) {
1475
+ addEmail({
1476
+ email: emailData.email,
1477
+ source: "snovio",
1478
+ confidence: emailData.confidence ?? 60,
1479
+ type: "business",
1480
+ verified: emailData.verified ?? false,
1481
+ isCatchAll: emailData.isCatchAll,
1482
+ metadata: emailData.metadata,
1483
+ });
1484
+ }
1485
+ }
1486
+ }
1487
+ }
1488
+ catch (err) {
1489
+ result.errors?.push(`Snovio: ${err instanceof Error ? err.message : "Unknown error"}`);
1490
+ }
1491
+ }
1170
1492
  /**
1171
1493
  * Get emails for multiple LinkedIn contacts in batch
1172
1494
  *
@@ -0,0 +1,82 @@
1
+ /**
2
+ * BounceBan Email Verification Provider
3
+ *
4
+ * Specializes in catch-all email verification with 85-95% accuracy
5
+ * using proprietary algorithms WITHOUT sending actual emails.
6
+ *
7
+ * Features:
8
+ * - Catch-all domain verification (industry-leading)
9
+ * - DeepVerify mode for pattern-guessed emails (+15-25% accuracy)
10
+ * - Waterfall endpoint with FREE 30-min retry window
11
+ * - Single email verification is FREE
12
+ * - GDPR compliant (no emails sent)
13
+ *
14
+ * API Endpoints:
15
+ * - GET /v1/verify/single - Standard single verification
16
+ * - GET https://api-waterfall.bounceban.com/v1/verify/single - Waterfall (recommended)
17
+ * - POST /v1/verify/bulk - Bulk verification
18
+ *
19
+ * Result values:
20
+ * - deliverable: Email verified (score 70-100)
21
+ * - risky: May work, often catch-all (score 30-70)
22
+ * - undeliverable: Email doesn't exist (score 0-30)
23
+ * - unknown: Could not determine
24
+ *
25
+ * @see https://bounceban.com
26
+ */
27
+ import type { EnrichmentCandidate, ProviderResult, ProviderMultiResult, BounceBanConfig, BounceBanVerifyResponse } from "../types";
28
+ /**
29
+ * Create the BounceBan verification provider
30
+ *
31
+ * This provider specializes in verifying emails on catch-all domains
32
+ * where traditional SMTP verification fails.
33
+ *
34
+ * Best used after construct provider generates email patterns.
35
+ */
36
+ export declare function createBounceBanProvider(config: BounceBanConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | ProviderMultiResult | null>;
37
+ /**
38
+ * Standalone function to verify a single email via BounceBan
39
+ *
40
+ * Uses the waterfall endpoint by default for best catch-all handling.
41
+ *
42
+ * @example
43
+ * ```typescript
44
+ * const result = await verifyEmailWithBounceBan('john@catchall-domain.com', {
45
+ * apiKey: process.env.BOUNCEBAN_API_KEY,
46
+ * });
47
+ * console.log(result.result); // "deliverable" | "risky" | "undeliverable" | "unknown"
48
+ * console.log(result.score); // 0-100
49
+ * console.log(result.is_accept_all); // true (catch-all domain)
50
+ * ```
51
+ */
52
+ export declare function verifyEmailWithBounceBan(email: string, config: BounceBanConfig): Promise<BounceBanVerifyResponse | null>;
53
+ /**
54
+ * Verify multiple emails in batch via BounceBan Bulk API
55
+ *
56
+ * More cost-effective for large volumes ($0.003/email vs single free).
57
+ * Uses async bulk API with polling for results.
58
+ *
59
+ * For small batches (< 5 emails), falls back to sequential single verification.
60
+ *
61
+ * @example
62
+ * ```typescript
63
+ * const results = await verifyEmailsBatchWithBounceBan(
64
+ * ['john@example.com', 'jane@example.com'],
65
+ * { apiKey: process.env.BOUNCEBAN_API_KEY }
66
+ * );
67
+ * ```
68
+ */
69
+ export declare function verifyEmailsBatchWithBounceBan(emails: string[], config: BounceBanConfig): Promise<Map<string, BounceBanVerifyResponse | null>>;
70
+ /**
71
+ * Check if a domain is catch-all via BounceBan
72
+ *
73
+ * Verifies a random non-existent email to detect catch-all behavior.
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * const isCatchAll = await checkCatchAllWithBounceBan('example.com', {
78
+ * apiKey: process.env.BOUNCEBAN_API_KEY,
79
+ * });
80
+ * ```
81
+ */
82
+ export declare function checkCatchAllWithBounceBan(domain: string, config: BounceBanConfig): Promise<boolean | null>;