linkedin-secret-sauce 0.12.4 → 0.13.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.
Files changed (161) hide show
  1. package/dist/enrichment/matching.js +161 -81
  2. package/dist/enrichment/providers/bounceban.js +19 -10
  3. package/dist/enrichment/providers/construct.js +97 -89
  4. package/dist/enrichment/providers/cosiall.js +2 -2
  5. package/dist/enrichment/providers/hunter.js +2 -1
  6. package/dist/enrichment/providers/ldd.js +4 -2
  7. package/dist/enrichment/providers/smartprospect.d.ts +5 -2
  8. package/dist/enrichment/providers/smartprospect.js +64 -27
  9. package/dist/enrichment/providers/snovio.js +7 -3
  10. package/dist/enrichment/providers/trykitt.d.ts +2 -2
  11. package/dist/enrichment/providers/trykitt.js +86 -21
  12. package/dist/index.d.ts +1 -1
  13. package/dist/linkedin-api.d.ts +113 -1
  14. package/dist/linkedin-api.js +836 -3
  15. package/dist/parsers/profile-parser.js +1 -0
  16. package/dist/types.d.ts +164 -0
  17. package/docs/api/assets/hierarchy.js +1 -1
  18. package/docs/api/assets/navigation.js +1 -1
  19. package/docs/api/assets/search.js +1 -1
  20. package/docs/api/classes/LinkedInClientError.html +4 -4
  21. package/docs/api/functions/_testGetAccountCookies.html +2 -2
  22. package/docs/api/functions/_testGetAccountEntry.html +2 -2
  23. package/docs/api/functions/_testGetAllAccountIds.html +2 -2
  24. package/docs/api/functions/_testGetPoolState.html +2 -2
  25. package/docs/api/functions/adminResetAccount.html +1 -1
  26. package/docs/api/functions/adminSetCooldown.html +1 -1
  27. package/docs/api/functions/analyzeProfilePosts.html +14 -0
  28. package/docs/api/functions/buildCookieHeader.html +1 -1
  29. package/docs/api/functions/clearAllSmartLeadTokens.html +2 -2
  30. package/docs/api/functions/clearRequestHistory.html +1 -1
  31. package/docs/api/functions/clearSessionAccount.html +1 -1
  32. package/docs/api/functions/clearSmartLeadToken.html +2 -2
  33. package/docs/api/functions/createEnrichmentClient.html +2 -2
  34. package/docs/api/functions/extractCsrfToken.html +1 -1
  35. package/docs/api/functions/extractLinkedInHandle.html +2 -2
  36. package/docs/api/functions/fetchCookiesFromCosiall.html +2 -2
  37. package/docs/api/functions/fetchProfileEmailsFromCosiall.html +2 -2
  38. package/docs/api/functions/forceRefreshCookies.html +1 -1
  39. package/docs/api/functions/getAccountForSession.html +1 -1
  40. package/docs/api/functions/getAccountsSummary.html +1 -1
  41. package/docs/api/functions/getCompaniesBatch.html +2 -2
  42. package/docs/api/functions/getCompanyById.html +2 -2
  43. package/docs/api/functions/getCompanyByUrl.html +1 -1
  44. package/docs/api/functions/getConfig.html +1 -1
  45. package/docs/api/functions/getCookiePoolHealth.html +1 -1
  46. package/docs/api/functions/getPostComments.html +11 -0
  47. package/docs/api/functions/getPostHistory.html +12 -0
  48. package/docs/api/functions/getPostReactions.html +12 -0
  49. package/docs/api/functions/getProfileByUrn.html +2 -2
  50. package/docs/api/functions/getProfileByVanity.html +2 -2
  51. package/docs/api/functions/getProfilesBatch.html +1 -1
  52. package/docs/api/functions/getRequestHistory.html +1 -1
  53. package/docs/api/functions/getSalesNavigatorProfileDetails.html +1 -1
  54. package/docs/api/functions/getSalesNavigatorProfileFull.html +2 -2
  55. package/docs/api/functions/getSmartLeadToken.html +1 -1
  56. package/docs/api/functions/getSmartLeadTokenCacheStats.html +2 -2
  57. package/docs/api/functions/getSmartLeadUser.html +2 -2
  58. package/docs/api/functions/getSnapshot.html +1 -1
  59. package/docs/api/functions/getYearsAtCompanyOptions.html +2 -2
  60. package/docs/api/functions/getYearsInPositionOptions.html +2 -2
  61. package/docs/api/functions/getYearsOfExperienceOptions.html +2 -2
  62. package/docs/api/functions/incrementMetric.html +1 -1
  63. package/docs/api/functions/initializeCookiePool.html +1 -1
  64. package/docs/api/functions/initializeLinkedInClient.html +1 -1
  65. package/docs/api/functions/isBusinessEmail.html +2 -2
  66. package/docs/api/functions/isDisposableDomain.html +2 -2
  67. package/docs/api/functions/isDisposableEmail.html +2 -2
  68. package/docs/api/functions/isPersonalDomain.html +2 -2
  69. package/docs/api/functions/isPersonalEmail.html +2 -2
  70. package/docs/api/functions/isRoleAccount.html +2 -2
  71. package/docs/api/functions/isValidEmailSyntax.html +2 -2
  72. package/docs/api/functions/parseFullProfile.html +2 -2
  73. package/docs/api/functions/parseSalesSearchResults.html +1 -1
  74. package/docs/api/functions/reportAccountFailure.html +1 -1
  75. package/docs/api/functions/reportAccountSuccess.html +1 -1
  76. package/docs/api/functions/resolveCompanyUniversalName.html +1 -1
  77. package/docs/api/functions/searchSalesLeads.html +2 -2
  78. package/docs/api/functions/selectAccountForRequest.html +1 -1
  79. package/docs/api/functions/setAccountForSession.html +1 -1
  80. package/docs/api/functions/typeahead.html +1 -1
  81. package/docs/api/functions/verifyEmailMx.html +1 -1
  82. package/docs/api/hierarchy.html +1 -1
  83. package/docs/api/index.html +2 -2
  84. package/docs/api/interfaces/AccountCookies.html +2 -2
  85. package/docs/api/interfaces/AnalyzedPost.html +8 -0
  86. package/docs/api/interfaces/BatchEnrichmentOptions.html +8 -8
  87. package/docs/api/interfaces/CacheAdapter.html +4 -4
  88. package/docs/api/interfaces/CanonicalEmail.html +8 -8
  89. package/docs/api/interfaces/CommentAuthor.html +8 -0
  90. package/docs/api/interfaces/Company.html +2 -2
  91. package/docs/api/interfaces/ConstructConfig.html +5 -5
  92. package/docs/api/interfaces/CosiallProfileEmailsResponse.html +6 -6
  93. package/docs/api/interfaces/EnrichmentCandidate.html +4 -4
  94. package/docs/api/interfaces/EnrichmentClient.html +6 -6
  95. package/docs/api/interfaces/EnrichmentClientConfig.html +7 -7
  96. package/docs/api/interfaces/EnrichmentLogger.html +3 -3
  97. package/docs/api/interfaces/EnrichmentOptions.html +6 -6
  98. package/docs/api/interfaces/HunterConfig.html +3 -3
  99. package/docs/api/interfaces/LddConfig.html +3 -3
  100. package/docs/api/interfaces/LddProfileData.html +2 -2
  101. package/docs/api/interfaces/LinkedInClientConfig.html +2 -2
  102. package/docs/api/interfaces/LinkedInCookie.html +2 -2
  103. package/docs/api/interfaces/LinkedInPosition.html +2 -2
  104. package/docs/api/interfaces/LinkedInProfile.html +2 -2
  105. package/docs/api/interfaces/LinkedInSpotlightBadge.html +2 -2
  106. package/docs/api/interfaces/LinkedInTenure.html +2 -2
  107. package/docs/api/interfaces/Metrics.html +2 -2
  108. package/docs/api/interfaces/MetricsSnapshot.html +2 -2
  109. package/docs/api/interfaces/PostAnalytics.html +11 -0
  110. package/docs/api/interfaces/PostComment.html +8 -0
  111. package/docs/api/interfaces/PostCommentsResult.html +7 -0
  112. package/docs/api/interfaces/PostHistoryResult.html +4 -0
  113. package/docs/api/interfaces/PostReaction.html +5 -0
  114. package/docs/api/interfaces/PostReactionsResult.html +5 -0
  115. package/docs/api/interfaces/ProfileAnalysisOptions.html +14 -0
  116. package/docs/api/interfaces/ProfileAnalysisResult.html +9 -0
  117. package/docs/api/interfaces/ProfileEducation.html +2 -2
  118. package/docs/api/interfaces/ProfileEmailsLookupOptions.html +5 -5
  119. package/docs/api/interfaces/ProfilePosition.html +2 -2
  120. package/docs/api/interfaces/ProfilePost.html +14 -0
  121. package/docs/api/interfaces/ProfileSkill.html +2 -2
  122. package/docs/api/interfaces/ProviderResult.html +6 -6
  123. package/docs/api/interfaces/ProvidersConfig.html +6 -6
  124. package/docs/api/interfaces/ReactionActor.html +9 -0
  125. package/docs/api/interfaces/RequestHistoryEntry.html +2 -2
  126. package/docs/api/interfaces/SalesLeadSearchResult.html +2 -2
  127. package/docs/api/interfaces/SalesNavigatorContactInfo.html +2 -2
  128. package/docs/api/interfaces/SalesNavigatorPosition.html +2 -2
  129. package/docs/api/interfaces/SalesNavigatorProfile.html +2 -2
  130. package/docs/api/interfaces/SalesNavigatorProfileFull.html +4 -4
  131. package/docs/api/interfaces/SearchSalesResult.html +2 -2
  132. package/docs/api/interfaces/SmartLeadAuthConfig.html +4 -4
  133. package/docs/api/interfaces/SmartLeadCredentials.html +2 -2
  134. package/docs/api/interfaces/SmartLeadLoginResponse.html +2 -2
  135. package/docs/api/interfaces/SmartLeadUser.html +2 -2
  136. package/docs/api/interfaces/SmartProspectConfig.html +8 -8
  137. package/docs/api/interfaces/SmartProspectContact.html +2 -2
  138. package/docs/api/interfaces/SmartProspectSearchFilters.html +21 -21
  139. package/docs/api/interfaces/TypeaheadItem.html +2 -2
  140. package/docs/api/interfaces/TypeaheadResult.html +2 -2
  141. package/docs/api/interfaces/VerificationResult.html +9 -9
  142. package/docs/api/types/CostCallback.html +2 -2
  143. package/docs/api/types/Geo.html +2 -2
  144. package/docs/api/types/LddApiResponse.html +1 -1
  145. package/docs/api/types/LinkedInReactionType.html +2 -0
  146. package/docs/api/types/ProviderFunc.html +2 -2
  147. package/docs/api/types/ProviderName.html +2 -2
  148. package/docs/api/types/SalesSearchFilters.html +2 -2
  149. package/docs/api/types/TypeaheadType.html +1 -1
  150. package/docs/api/variables/COMPANY_SIZE_OPTIONS.html +1 -1
  151. package/docs/api/variables/DEFAULT_PROVIDER_ORDER.html +2 -2
  152. package/docs/api/variables/DISPOSABLE_DOMAINS.html +2 -2
  153. package/docs/api/variables/FUNCTION_OPTIONS.html +1 -1
  154. package/docs/api/variables/INDUSTRY_OPTIONS.html +1 -1
  155. package/docs/api/variables/LANGUAGE_OPTIONS.html +1 -1
  156. package/docs/api/variables/PERSONAL_DOMAINS.html +2 -2
  157. package/docs/api/variables/PROVIDER_COSTS.html +2 -2
  158. package/docs/api/variables/REGION_OPTIONS.html +1 -1
  159. package/docs/api/variables/SENIORITY_OPTIONS.html +2 -2
  160. package/docs/api/variables/YEARS_OPTIONS.html +1 -1
  161. package/package.json +1 -1
@@ -813,12 +813,41 @@ async function getEmailsForLinkedInContact(contactOrLead, config, options = {})
813
813
  * Merge emails and boost confidence when found from multiple sources
814
814
  * - 2 sources: boost to at least 95% confidence
815
815
  * - 3+ sources: boost to 100% confidence
816
+ *
817
+ * IMPORTANT: When multiple sources find the same email, we pick the
818
+ * most authoritative source (not just highest confidence), then boost.
819
+ * Source priority: ldd > smartprospect > cosiall > trykitt > bounceban > hunter > snovio > pattern
816
820
  */
817
821
  const mergeAndBoostEmails = () => {
818
822
  const mergedEmails = [];
823
+ // Source priority for merging (lower = higher priority)
824
+ const mergePriority = {
825
+ ldd: 0, // Highest priority - your own data
826
+ smartprospect: 1, // Very reliable - verified database
827
+ cosiall: 2, // LinkedIn profile data
828
+ trykitt: 3, // AI-powered finder
829
+ bounceban: 4, // Verification provider
830
+ hunter: 5, // Paid finder
831
+ snovio: 6, // Paid finder
832
+ linkedin: 7, // Company lookup only
833
+ pattern: 8, // Pattern guessing - lowest priority
834
+ };
819
835
  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);
836
+ // Find the best result by SOURCE PRIORITY first, then confidence
837
+ // This ensures SmartProspect/LDD are preferred over pattern guessing
838
+ const bestResult = results.reduce((best, current) => {
839
+ const bestPriority = mergePriority[best.source] ?? 99;
840
+ const currentPriority = mergePriority[current.source] ?? 99;
841
+ // Pick by source priority first
842
+ if (currentPriority < bestPriority) {
843
+ return current;
844
+ }
845
+ if (currentPriority > bestPriority) {
846
+ return best;
847
+ }
848
+ // Same source priority - pick higher confidence
849
+ return current.confidence > best.confidence ? current : best;
850
+ });
822
851
  // Boost confidence based on number of sources
823
852
  let boostedConfidence = bestResult.confidence;
824
853
  if (sources.size >= 3) {
@@ -874,90 +903,110 @@ async function getEmailsForLinkedInContact(contactOrLead, config, options = {})
874
903
  result.providersQueried.push("cosiall");
875
904
  freeProviderPromises.push(queryCosiall(contact, addEmail, result));
876
905
  }
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
- }
906
+ // TryKitt.ai is moved to Phase 2 (after domain discovery)
907
+ // It needs name + domain to find emails
883
908
  // Wait for all Phase 1 free providers
884
909
  await Promise.all(freeProviderPromises);
885
- // Merge emails and calculate best confidence after Phase 1
910
+ // Merge emails after Phase 1
886
911
  result.emails = mergeAndBoostEmails();
887
- const phase1BestConfidence = result.emails.length > 0
888
- ? Math.max(...result.emails.map((e) => e.confidence))
889
- : 0;
890
912
  // ==========================================================================
891
- // Phase 2: Domain Discovery + Pattern Guessing
892
- // ONLY if Phase 1 confidence < threshold (to minimize LinkedIn API calls)
913
+ // Phase 2: Domain Discovery + FREE Email Finding (TryKitt, Pattern Guessing)
914
+ // FREE providers ALWAYS run - no reason to skip them
893
915
  // ==========================================================================
894
916
  let companyDomain = linkedInCompanyDomain || discoveredCompanyDomain;
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
- }
917
+ // Try LinkedIn Company Lookup for domain discovery
918
+ // Only if we don't have a domain yet
919
+ if (!companyDomain &&
920
+ !skipPatternGuessing &&
921
+ !options.skipLinkedInCompanyLookup &&
922
+ config.linkedInCompanyLookup) {
923
+ const companyUrn = contact.currentPositions?.[0]?.companyUrn;
924
+ if (companyUrn) {
925
+ const companyId = extractCompanyIdFromUrn(companyUrn);
926
+ if (companyId) {
927
+ result.providersQueried.push("linkedin");
928
+ try {
929
+ const company = await config.linkedInCompanyLookup(companyId);
930
+ if (company?.websiteUrl) {
931
+ companyDomain = extractDomainFromUrl(company.websiteUrl);
932
+ if (companyDomain) {
933
+ discoveredCompanyDomain = companyDomain;
915
934
  }
916
935
  }
917
- catch (err) {
918
- result.errors?.push(`LinkedIn: ${err instanceof Error ? err.message : "Company lookup failed"}`);
919
- }
936
+ }
937
+ catch (err) {
938
+ result.errors?.push(`LinkedIn: ${err instanceof Error ? err.message : "Company lookup failed"}`);
920
939
  }
921
940
  }
922
941
  }
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
942
+ }
943
+ // Store discovered domain in result for visibility
944
+ if (discoveredCompanyDomain && !linkedInCompanyDomain) {
945
+ result.discoveredCompanyDomain = discoveredCompanyDomain;
946
+ }
947
+ // TryKitt.ai lookup (FREE for individuals - unlimited)
948
+ // AI-powered email finder - runs after domain discovery
949
+ if (!skipTryKitt && config.trykitt?.apiKey && companyDomain) {
950
+ result.providersQueried.push("trykitt");
951
+ await queryTryKitt(contact, config.trykitt, companyDomain, addEmail, result);
952
+ // Re-merge after TryKitt
953
+ result.emails = mergeAndBoostEmails();
954
+ }
955
+ // Run pattern guessing if we have a domain (from LinkedIn contact, SmartProspect, or LinkedIn API)
956
+ if (!skipPatternGuessing && companyDomain) {
957
+ result.providersQueried.push("pattern");
958
+ await queryPatternGuessing(contact, companyDomain, addEmail, result);
959
+ // Re-merge after pattern guessing
960
+ result.emails = mergeAndBoostEmails();
961
+ }
962
+ // ==========================================================================
963
+ // Check if we already have a verified BUSINESS email from a trusted source
964
+ // If so, skip BounceBan and paid providers entirely
965
+ //
966
+ // NOTE: LDD and Cosiall can return personal emails (Gmail, Yahoo, etc.)
967
+ // Don't stop searching if we only have personal emails - keep looking for business.
968
+ // ==========================================================================
969
+ const hasVerifiedBusinessEmail = result.emails.some((e) => e.verified &&
970
+ e.type === "business" &&
971
+ (e.source === "smartprospect" ||
972
+ e.source === "ldd" ||
973
+ e.source === "cosiall" ||
974
+ e.source === "trykitt"));
975
+ const phase2BestConfidence = result.emails.length > 0
976
+ ? Math.max(...result.emails.map((e) => e.confidence))
977
+ : 0;
978
+ // BounceBan catch-all verification (costs $0.008/lookup)
979
+ // SKIP if we already have a verified BUSINESS email from SmartProspect/Cosiall/TryKitt
980
+ // Only run to verify pattern-guessed emails when no trusted source found a business email
981
+ // NOTE: LDD personal emails don't count - we still want to find the business email
982
+ if (!skipBounceBan &&
983
+ config.bounceban?.apiKey &&
984
+ result.emails.length > 0 &&
985
+ !hasVerifiedBusinessEmail && // Skip if we have a verified BUSINESS email
986
+ phase2BestConfidence < paidProviderThreshold) {
987
+ // Only verify pattern-guessed emails (the ones we're uncertain about)
988
+ const emailsToVerify = result.emails.filter((e) => e.source === "pattern" && !e.verified);
989
+ if (emailsToVerify.length > 0) {
990
+ result.providersQueried.push("bounceban");
991
+ await queryBounceBan(emailsToVerify, config.bounceban, addEmail, result);
992
+ // Re-merge after BounceBan verification
932
993
  result.emails = mergeAndBoostEmails();
933
994
  }
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
- }
950
995
  }
951
996
  // Calculate final best confidence
952
997
  const finalBestConfidence = result.emails.length > 0
953
998
  ? Math.max(...result.emails.map((e) => e.confidence))
954
999
  : 0;
955
1000
  // ==========================================================================
956
- // Phase 3: PAID providers as last resort (Hunter/Bouncer/Snovio)
1001
+ // Phase 3: PAID providers as LAST RESORT (Hunter then Snovio)
1002
+ // SKIP if we already have a verified BUSINESS email
1003
+ // NOTE: LDD personal emails don't count - we still want to find the business email
957
1004
  // ==========================================================================
958
- if (!skipPaidProviders && finalBestConfidence < paidProviderThreshold) {
959
- // Only use paid providers if we have low confidence or no results
960
- // Hunter.io - Email Finder ($0.015/lookup)
1005
+ const hasAnyVerifiedBusinessEmail = result.emails.some((e) => e.verified && e.type === "business");
1006
+ if (!skipPaidProviders &&
1007
+ !hasAnyVerifiedBusinessEmail && // Skip if we have a verified BUSINESS email
1008
+ finalBestConfidence < paidProviderThreshold) {
1009
+ // Hunter.io - Email Finder ($0.005/lookup) - try first
961
1010
  if (config.hunter?.apiKey) {
962
1011
  result.providersQueried.push("hunter");
963
1012
  // Extract linkedin handle from contact if available
@@ -980,10 +1029,15 @@ async function getEmailsForLinkedInContact(contactOrLead, config, options = {})
980
1029
  // Re-merge after Hunter
981
1030
  result.emails = mergeAndBoostEmails();
982
1031
  }
983
- // Snovio - Email Finder ($0.02/lookup) - best for catch-all domains
1032
+ // Check if Hunter found a verified email - if so, skip Snovio
1033
+ const hunterFoundVerified = result.emails.some((e) => e.source === "hunter" && e.verified);
1034
+ // Snovio - Email Finder ($0.02/lookup) - ONLY if Hunter didn't find anything
1035
+ // This is the absolute last resort since it's the most expensive
984
1036
  if (config.snovio?.clientId &&
985
1037
  config.snovio?.clientSecret &&
986
- companyDomain) {
1038
+ companyDomain &&
1039
+ !hunterFoundVerified // Skip if Hunter already found a verified email
1040
+ ) {
987
1041
  result.providersQueried.push("snovio");
988
1042
  await querySnovio(contact, config.snovio, companyDomain, addEmail, result);
989
1043
  // Re-merge after Snovio
@@ -1217,20 +1271,26 @@ async function queryBounceBan(emailsToVerify, bouncebanConfig, addEmail, result)
1217
1271
  useWaterfall: bouncebanConfig.useWaterfall ?? true,
1218
1272
  });
1219
1273
  if (verification && verification.status === "success") {
1220
- // Map BounceBan result to confidence
1221
- let confidence = verification.score;
1274
+ // BounceBan specializes in catch-all verification with 97%+ accuracy
1275
+ // Trust the score they return - don't artificially cap it
1276
+ const confidence = verification.score;
1222
1277
  const verified = verification.result === "deliverable";
1223
- // Reduce confidence for catch-all domains even if deliverable
1224
- if (verification.is_accept_all && verified) {
1225
- confidence = Math.min(confidence, 75);
1226
- }
1278
+ // For catch-all domains:
1279
+ // - "deliverable" = BounceBan's algorithm determined this specific email exists (97%+ accuracy)
1280
+ // - "risky" = Could not determine with high confidence
1281
+ // - "undeliverable" = Email definitely doesn't exist
1227
1282
  // Only add if it's potentially deliverable
1228
1283
  if (verification.result === "deliverable" ||
1229
1284
  verification.result === "risky") {
1285
+ // For "deliverable" on catch-all, BounceBan has verified this is the REAL email
1286
+ // For "risky", we keep lower confidence as BounceBan couldn't determine
1287
+ const finalConfidence = verification.result === "deliverable"
1288
+ ? Math.max(confidence, 85) // Deliverable = high confidence (BounceBan verified it)
1289
+ : Math.min(confidence, 60); // Risky = lower confidence
1230
1290
  addEmail({
1231
1291
  email: verification.email,
1232
1292
  source: "bounceban",
1233
- confidence,
1293
+ confidence: finalConfidence,
1234
1294
  type: "business",
1235
1295
  verified,
1236
1296
  isCatchAll: verification.is_accept_all,
@@ -1309,20 +1369,39 @@ async function querySmartProspect(contact, smartProspectConfig, minMatchConfiden
1309
1369
  .split("/")[0]
1310
1370
  .toLowerCase();
1311
1371
  }
1312
- // Fetch email for matched contact
1313
- const fetchResponse = await client.fetch([
1314
- matchResult.smartProspectContact.id,
1315
- ]);
1372
+ // Fetch email for matched contact (pass filter_id from search for polling)
1373
+ const searchFilterId = searchResponse.data.filter_id;
1374
+ const fetchResponse = await client.fetch([matchResult.smartProspectContact.id], searchFilterId);
1316
1375
  if (fetchResponse.success && fetchResponse.data.list.length > 0) {
1317
1376
  const enrichedContact = fetchResponse.data.list[0];
1318
1377
  if (enrichedContact.email) {
1378
+ // Calculate email confidence based on verification status AND match quality
1379
+ // SmartProspect verification status: "valid", "catch_all", "invalid", etc.
1380
+ const isVerified = enrichedContact.verificationStatus === "valid" ||
1381
+ enrichedContact.verificationStatus === "verified";
1382
+ const isCatchAll = enrichedContact.verificationStatus === "catch_all";
1383
+ // Base confidence from match quality (60-100%)
1384
+ let emailConfidence = matchResult.confidence;
1385
+ // Boost confidence for verified emails from SmartProspect
1386
+ // A verified email from SmartProspect is more reliable than pattern guessing
1387
+ if (isVerified) {
1388
+ emailConfidence = Math.max(emailConfidence, 90); // Verified = at least 90%
1389
+ }
1390
+ else if (isCatchAll) {
1391
+ emailConfidence = Math.max(emailConfidence, 80); // Catch-all = at least 80%
1392
+ }
1393
+ else if (enrichedContact.emailDeliverability > 0) {
1394
+ // Use deliverability score if available
1395
+ emailConfidence = Math.max(emailConfidence, enrichedContact.emailDeliverability * 100);
1396
+ }
1319
1397
  addEmail({
1320
1398
  email: enrichedContact.email,
1321
1399
  source: "smartprospect",
1322
- confidence: matchResult.confidence,
1400
+ confidence: emailConfidence,
1323
1401
  type: "business",
1324
- verified: enrichedContact.verificationStatus === "verified",
1402
+ verified: isVerified || isCatchAll,
1325
1403
  deliverability: enrichedContact.emailDeliverability,
1404
+ isCatchAll,
1326
1405
  metadata: {
1327
1406
  matchQuality: matchResult.quality,
1328
1407
  matchedFields: matchResult.matchedFields,
@@ -1330,6 +1409,7 @@ async function querySmartProspect(contact, smartProspectConfig, minMatchConfiden
1330
1409
  company: enrichedContact.company?.name,
1331
1410
  companyWebsite: companyWebsite,
1332
1411
  title: enrichedContact.title,
1412
+ verificationStatus: enrichedContact.verificationStatus,
1333
1413
  },
1334
1414
  });
1335
1415
  }
@@ -99,14 +99,16 @@ function extractEmailsToVerify(candidate) {
99
99
  */
100
100
  async function verifyEmailStandard(email, apiKey, apiUrl, timeoutMs, useDeepVerify) {
101
101
  const params = new URLSearchParams({
102
- api_key: apiKey,
103
102
  email: email,
104
103
  });
105
104
  if (useDeepVerify) {
106
105
  params.set("mode", "deepverify");
107
106
  }
107
+ const url = `${apiUrl}/v1/verify/single?${params.toString()}`;
108
108
  try {
109
- const response = await (0, http_retry_1.getWithRetry)(`${apiUrl}/v1/verify/single?${params.toString()}`, undefined, {
109
+ const response = await (0, http_retry_1.getWithRetry)(url, {
110
+ Authorization: apiKey,
111
+ }, {
110
112
  retries: 1,
111
113
  backoffMs: 500,
112
114
  timeoutMs,
@@ -114,7 +116,8 @@ async function verifyEmailStandard(email, apiKey, apiUrl, timeoutMs, useDeepVeri
114
116
  });
115
117
  return response;
116
118
  }
117
- catch {
119
+ catch (err) {
120
+ console.error("[BounceBan] Standard verification failed:", err instanceof Error ? err.message : err);
118
121
  return null;
119
122
  }
120
123
  }
@@ -128,15 +131,17 @@ async function verifyEmailStandard(email, apiKey, apiUrl, timeoutMs, useDeepVeri
128
131
  */
129
132
  async function verifyEmailWaterfall(email, apiKey, waterfallApiUrl, timeoutMs, useDeepVerify) {
130
133
  const params = new URLSearchParams({
131
- api_key: apiKey,
132
134
  email: email,
133
135
  timeout: String(Math.floor(timeoutMs / 1000)), // Convert to seconds
134
136
  });
135
137
  if (useDeepVerify) {
136
138
  params.set("mode", "deepverify");
137
139
  }
140
+ const url = `${waterfallApiUrl}/v1/verify/single?${params.toString()}`;
138
141
  try {
139
- const response = await (0, http_retry_1.getWithRetry)(`${waterfallApiUrl}/v1/verify/single?${params.toString()}`, undefined, {
142
+ const response = await (0, http_retry_1.getWithRetry)(url, {
143
+ Authorization: apiKey,
144
+ }, {
140
145
  retries: 2, // Waterfall allows free retries within 30 mins
141
146
  backoffMs: 5000, // Wait 5s between retries as recommended
142
147
  timeoutMs: timeoutMs + 10000, // Add buffer for network
@@ -144,7 +149,8 @@ async function verifyEmailWaterfall(email, apiKey, waterfallApiUrl, timeoutMs, u
144
149
  });
145
150
  return response;
146
151
  }
147
- catch {
152
+ catch (err) {
153
+ console.error("[BounceBan] Waterfall verification failed:", err instanceof Error ? err.message : err);
148
154
  return null;
149
155
  }
150
156
  }
@@ -256,9 +262,9 @@ async function submitBulkVerification(emails, apiKey, apiUrl, useDeepVerify) {
256
262
  method: "POST",
257
263
  headers: {
258
264
  "Content-Type": "application/json",
265
+ Authorization: apiKey,
259
266
  },
260
267
  body: JSON.stringify({
261
- api_key: apiKey,
262
268
  emails: emails,
263
269
  mode: useDeepVerify ? "deepverify" : "regular",
264
270
  }),
@@ -279,10 +285,13 @@ async function submitBulkVerification(emails, apiKey, apiUrl, useDeepVerify) {
279
285
  async function checkBulkStatus(taskId, apiKey, apiUrl) {
280
286
  try {
281
287
  const params = new URLSearchParams({
282
- api_key: apiKey,
283
288
  id: taskId,
284
289
  });
285
- const response = await fetch(`${apiUrl}/v1/verify/bulk/status?${params.toString()}`);
290
+ const response = await fetch(`${apiUrl}/v1/verify/bulk/status?${params.toString()}`, {
291
+ headers: {
292
+ Authorization: apiKey,
293
+ },
294
+ });
286
295
  if (!response.ok) {
287
296
  return null;
288
297
  }
@@ -301,9 +310,9 @@ async function fetchBulkResults(taskId, apiKey, apiUrl, page = 1, perPage = 100)
301
310
  method: "POST",
302
311
  headers: {
303
312
  "Content-Type": "application/json",
313
+ Authorization: apiKey,
304
314
  },
305
315
  body: JSON.stringify({
306
- api_key: apiKey,
307
316
  id: taskId,
308
317
  page: page,
309
318
  per_page: perPage,
@@ -68,104 +68,112 @@ function createConstructProvider(config) {
68
68
  const timeoutMs = config?.timeoutMs ?? 5000;
69
69
  const smtpVerifyDelayMs = config?.smtpVerifyDelayMs ?? 2000; // Delay between SMTP checks
70
70
  async function fetchEmail(candidate) {
71
- const { firstName: first, lastName: last } = (0, candidate_parser_1.extractName)(candidate);
72
- const { domain } = (0, candidate_parser_1.extractCompany)(candidate);
73
- // Skip if missing required fields
74
- if (!first || !domain) {
75
- return null;
76
- }
77
- // Skip personal domains
78
- if ((0, personal_domains_1.isPersonalDomain)(domain)) {
79
- return null;
80
- }
81
- const candidates = buildCandidates({ first, last, domain });
82
- const max = Math.min(candidates.length, maxAttempts);
83
- // First, check if domain is catch-all
84
- const catchAllResult = await (0, mx_1.checkDomainCatchAll)(domain, {
85
- timeoutMs: 10000,
86
- });
87
- const isCatchAll = catchAllResult.isCatchAll;
88
- // Collect ALL valid email patterns (not just first match)
89
- const validEmails = [];
90
- // Track all attempted patterns for debugging
91
- const attemptedPatterns = [];
92
- // If NOT catch-all, we can verify each email via SMTP
93
- if (isCatchAll === false) {
94
- // Verify emails one by one, stop when we find a valid one
95
- const emailsToVerify = candidates.slice(0, max);
96
- for (let i = 0; i < emailsToVerify.length; i++) {
97
- const email = emailsToVerify[i];
98
- // If we already found a valid email, skip the rest
99
- if (validEmails.length > 0) {
100
- attemptedPatterns.push({ email, status: "skipped" });
101
- continue;
102
- }
103
- // Add delay between checks (except first one)
104
- if (i > 0) {
105
- await new Promise((resolve) => setTimeout(resolve, smtpVerifyDelayMs));
106
- }
107
- // Verify single email
108
- const results = await (0, mx_1.verifyEmailsExist)([email], {
109
- delayMs: 0,
110
- timeoutMs,
111
- });
112
- const result = results[0];
113
- if (result.exists === true) {
114
- // Email confirmed to exist!
115
- attemptedPatterns.push({ email, status: "exists" });
116
- validEmails.push({
117
- email: result.email,
118
- verified: true,
119
- confidence: 95, // High confidence - SMTP verified
120
- isCatchAll: false,
121
- metadata: {
122
- pattern: result.email.split("@")[0],
123
- mxRecords: catchAllResult.mxRecords,
124
- smtpVerified: true,
125
- attemptedPatterns, // Include what was tried
126
- },
71
+ try {
72
+ const { firstName: first, lastName: last } = (0, candidate_parser_1.extractName)(candidate);
73
+ const { domain } = (0, candidate_parser_1.extractCompany)(candidate);
74
+ // Skip if missing required fields
75
+ if (!first || !domain) {
76
+ return null;
77
+ }
78
+ // Skip personal domains
79
+ if ((0, personal_domains_1.isPersonalDomain)(domain)) {
80
+ return null;
81
+ }
82
+ const candidates = buildCandidates({ first, last, domain });
83
+ const max = Math.min(candidates.length, maxAttempts);
84
+ // First, check if domain is catch-all
85
+ const catchAllResult = await (0, mx_1.checkDomainCatchAll)(domain, {
86
+ timeoutMs: 10000,
87
+ });
88
+ const isCatchAll = catchAllResult.isCatchAll;
89
+ // Collect ALL valid email patterns (not just first match)
90
+ const validEmails = [];
91
+ // Track all attempted patterns for debugging
92
+ const attemptedPatterns = [];
93
+ // If NOT catch-all, we can verify each email via SMTP
94
+ if (isCatchAll === false) {
95
+ // Verify emails one by one, stop when we find a valid one
96
+ const emailsToVerify = candidates.slice(0, max);
97
+ for (let i = 0; i < emailsToVerify.length; i++) {
98
+ const email = emailsToVerify[i];
99
+ // If we already found a valid email, skip the rest
100
+ if (validEmails.length > 0) {
101
+ attemptedPatterns.push({ email, status: "skipped" });
102
+ continue;
103
+ }
104
+ // Add delay between checks (except first one)
105
+ if (i > 0) {
106
+ await new Promise((resolve) => setTimeout(resolve, smtpVerifyDelayMs));
107
+ }
108
+ // Verify single email
109
+ const results = await (0, mx_1.verifyEmailsExist)([email], {
110
+ delayMs: 0,
111
+ timeoutMs,
127
112
  });
128
- // Found one! Stop checking more
129
- break;
130
- }
131
- else if (result.exists === false) {
132
- attemptedPatterns.push({ email, status: "not_found" });
113
+ const result = results[0];
114
+ if (result.exists === true) {
115
+ // Email confirmed to exist!
116
+ attemptedPatterns.push({ email, status: "exists" });
117
+ validEmails.push({
118
+ email: result.email,
119
+ verified: true,
120
+ confidence: 80, // Good confidence - SMTP verified pattern guess
121
+ // Note: 80% is appropriate for pattern guessing with SMTP verification
122
+ // Higher-priority sources like SmartProspect (90%) should rank above
123
+ isCatchAll: false,
124
+ metadata: {
125
+ pattern: result.email.split("@")[0],
126
+ mxRecords: catchAllResult.mxRecords,
127
+ smtpVerified: true,
128
+ attemptedPatterns, // Include what was tried
129
+ },
130
+ });
131
+ // Found one! Stop checking more
132
+ break;
133
+ }
134
+ else if (result.exists === false) {
135
+ attemptedPatterns.push({ email, status: "not_found" });
136
+ }
137
+ else {
138
+ attemptedPatterns.push({ email, status: "unknown" });
139
+ }
133
140
  }
134
- else {
135
- attemptedPatterns.push({ email, status: "unknown" });
141
+ // If no valid email found, include attempted patterns in metadata
142
+ if (validEmails.length === 0 && attemptedPatterns.length > 0) {
143
+ // Return null but could add metadata about attempts
136
144
  }
137
145
  }
138
- // If no valid email found, include attempted patterns in metadata
139
- if (validEmails.length === 0 && attemptedPatterns.length > 0) {
140
- // Return null but could add metadata about attempts
141
- }
142
- }
143
- else {
144
- // Catch-all or unknown - fall back to MX verification only
145
- for (let i = 0; i < max; i++) {
146
- const email = candidates[i];
147
- const verification = await (0, mx_1.verifyEmailMx)(email, { timeoutMs });
148
- if (verification.valid === true && verification.confidence >= 50) {
149
- validEmails.push({
150
- email,
151
- verified: true,
152
- confidence: verification.confidence,
153
- isCatchAll: isCatchAll ?? undefined,
154
- metadata: {
155
- pattern: email.split("@")[0],
156
- mxRecords: verification.mxRecords,
157
- smtpVerified: false,
158
- },
159
- });
146
+ else {
147
+ // Catch-all or unknown - fall back to MX verification only
148
+ for (let i = 0; i < max; i++) {
149
+ const email = candidates[i];
150
+ const verification = await (0, mx_1.verifyEmailMx)(email, { timeoutMs });
151
+ if (verification.valid === true && verification.confidence >= 50) {
152
+ validEmails.push({
153
+ email,
154
+ verified: true,
155
+ confidence: verification.confidence,
156
+ isCatchAll: isCatchAll ?? undefined,
157
+ metadata: {
158
+ pattern: email.split("@")[0],
159
+ mxRecords: verification.mxRecords,
160
+ smtpVerified: false,
161
+ },
162
+ });
163
+ }
160
164
  }
161
165
  }
166
+ if (validEmails.length === 0) {
167
+ return null;
168
+ }
169
+ // Sort by confidence
170
+ validEmails.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0));
171
+ return { emails: validEmails };
162
172
  }
163
- if (validEmails.length === 0) {
173
+ catch (err) {
174
+ console.error("[Construct] Email pattern generation failed:", err instanceof Error ? err.message : err);
164
175
  return null;
165
176
  }
166
- // Sort by confidence
167
- validEmails.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0));
168
- return { emails: validEmails };
169
177
  }
170
178
  // Mark provider name for orchestrator
171
179
  fetchEmail.__name = "construct";
@@ -98,8 +98,8 @@ function createCosiallProvider(config) {
98
98
  }));
99
99
  return { emails };
100
100
  }
101
- catch {
102
- // Silently fail - provider failures shouldn't stop the enrichment flow
101
+ catch (err) {
102
+ console.error("[Cosiall] Profile email lookup failed:", err instanceof Error ? err.message : err);
103
103
  return null;
104
104
  }
105
105
  }