linkedin-secret-sauce 0.12.0 → 0.12.2

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 (44) hide show
  1. package/README.md +50 -21
  2. package/dist/cosiall-client.d.ts +1 -1
  3. package/dist/cosiall-client.js +1 -1
  4. package/dist/enrichment/index.d.ts +3 -3
  5. package/dist/enrichment/index.js +19 -2
  6. package/dist/enrichment/matching.d.ts +29 -9
  7. package/dist/enrichment/matching.js +545 -142
  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 +27 -0
  15. package/dist/enrichment/providers/cosiall.js +109 -0
  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 +10 -7
  21. package/dist/enrichment/providers/index.js +12 -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 +234 -17
  30. package/dist/enrichment/types.js +60 -48
  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/package.json +25 -26
@@ -10,6 +10,7 @@ exports.createConstructProvider = createConstructProvider;
10
10
  const mx_1 = require("../verification/mx");
11
11
  const personal_domains_1 = require("../utils/personal-domains");
12
12
  const validation_1 = require("../utils/validation");
13
+ const candidate_parser_1 = require("../utils/candidate-parser");
13
14
  /**
14
15
  * Build all email pattern candidates for a person
15
16
  *
@@ -19,11 +20,11 @@ const validation_1 = require("../utils/validation");
19
20
  * 3. f.lastname, flastname, etc.
20
21
  */
21
22
  function buildCandidates(input) {
22
- const domain = String(input.domain || '').toLowerCase();
23
- const first = (0, validation_1.cleanNamePart)(input.first || '');
24
- const last = (0, validation_1.cleanNamePart)(input.last || '');
25
- const fl = first ? first[0] : '';
26
- const ll = last ? last[0] : '';
23
+ const domain = String(input.domain || "").toLowerCase();
24
+ const first = (0, validation_1.cleanNamePart)(input.first || "");
25
+ const last = (0, validation_1.cleanNamePart)(input.last || "");
26
+ const fl = first ? first[0] : "";
27
+ const ll = last ? last[0] : "";
27
28
  const locals = [];
28
29
  // Simple first name pattern (very common, especially for small companies/startups)
29
30
  if (first)
@@ -59,28 +60,6 @@ function buildCandidates(input) {
59
60
  }
60
61
  return emails;
61
62
  }
62
- /**
63
- * Extract name components from candidate
64
- */
65
- function extractNames(candidate) {
66
- const firstName = candidate.firstName ||
67
- candidate.first_name ||
68
- candidate.first ||
69
- candidate.name?.split(' ')?.[0] ||
70
- '';
71
- const lastName = candidate.lastName ||
72
- candidate.last_name ||
73
- candidate.last ||
74
- candidate.name?.split(' ')?.slice(1).join(' ') ||
75
- '';
76
- return { first: firstName, last: lastName };
77
- }
78
- /**
79
- * Extract domain from candidate
80
- */
81
- function extractDomain(candidate) {
82
- return candidate.domain || candidate.companyDomain || candidate.company_domain || '';
83
- }
84
63
  /**
85
64
  * Create the construct provider function
86
65
  */
@@ -89,8 +68,8 @@ function createConstructProvider(config) {
89
68
  const timeoutMs = config?.timeoutMs ?? 5000;
90
69
  const smtpVerifyDelayMs = config?.smtpVerifyDelayMs ?? 2000; // Delay between SMTP checks
91
70
  async function fetchEmail(candidate) {
92
- const { first, last } = extractNames(candidate);
93
- const domain = extractDomain(candidate);
71
+ const { firstName: first, lastName: last } = (0, candidate_parser_1.extractName)(candidate);
72
+ const { domain } = (0, candidate_parser_1.extractCompany)(candidate);
94
73
  // Skip if missing required fields
95
74
  if (!first || !domain) {
96
75
  return null;
@@ -102,7 +81,9 @@ function createConstructProvider(config) {
102
81
  const candidates = buildCandidates({ first, last, domain });
103
82
  const max = Math.min(candidates.length, maxAttempts);
104
83
  // First, check if domain is catch-all
105
- const catchAllResult = await (0, mx_1.checkDomainCatchAll)(domain, { timeoutMs: 10000 });
84
+ const catchAllResult = await (0, mx_1.checkDomainCatchAll)(domain, {
85
+ timeoutMs: 10000,
86
+ });
106
87
  const isCatchAll = catchAllResult.isCatchAll;
107
88
  // Collect ALL valid email patterns (not just first match)
108
89
  const validEmails = [];
@@ -116,7 +97,7 @@ function createConstructProvider(config) {
116
97
  const email = emailsToVerify[i];
117
98
  // If we already found a valid email, skip the rest
118
99
  if (validEmails.length > 0) {
119
- attemptedPatterns.push({ email, status: 'skipped' });
100
+ attemptedPatterns.push({ email, status: "skipped" });
120
101
  continue;
121
102
  }
122
103
  // Add delay between checks (except first one)
@@ -124,18 +105,21 @@ function createConstructProvider(config) {
124
105
  await new Promise((resolve) => setTimeout(resolve, smtpVerifyDelayMs));
125
106
  }
126
107
  // Verify single email
127
- const results = await (0, mx_1.verifyEmailsExist)([email], { delayMs: 0, timeoutMs });
108
+ const results = await (0, mx_1.verifyEmailsExist)([email], {
109
+ delayMs: 0,
110
+ timeoutMs,
111
+ });
128
112
  const result = results[0];
129
113
  if (result.exists === true) {
130
114
  // Email confirmed to exist!
131
- attemptedPatterns.push({ email, status: 'exists' });
115
+ attemptedPatterns.push({ email, status: "exists" });
132
116
  validEmails.push({
133
117
  email: result.email,
134
118
  verified: true,
135
119
  confidence: 95, // High confidence - SMTP verified
136
120
  isCatchAll: false,
137
121
  metadata: {
138
- pattern: result.email.split('@')[0],
122
+ pattern: result.email.split("@")[0],
139
123
  mxRecords: catchAllResult.mxRecords,
140
124
  smtpVerified: true,
141
125
  attemptedPatterns, // Include what was tried
@@ -145,10 +129,10 @@ function createConstructProvider(config) {
145
129
  break;
146
130
  }
147
131
  else if (result.exists === false) {
148
- attemptedPatterns.push({ email, status: 'not_found' });
132
+ attemptedPatterns.push({ email, status: "not_found" });
149
133
  }
150
134
  else {
151
- attemptedPatterns.push({ email, status: 'unknown' });
135
+ attemptedPatterns.push({ email, status: "unknown" });
152
136
  }
153
137
  }
154
138
  // If no valid email found, include attempted patterns in metadata
@@ -168,7 +152,7 @@ function createConstructProvider(config) {
168
152
  confidence: verification.confidence,
169
153
  isCatchAll: isCatchAll ?? undefined,
170
154
  metadata: {
171
- pattern: email.split('@')[0],
155
+ pattern: email.split("@")[0],
172
156
  mxRecords: verification.mxRecords,
173
157
  smtpVerified: false,
174
158
  },
@@ -184,6 +168,6 @@ function createConstructProvider(config) {
184
168
  return { emails: validEmails };
185
169
  }
186
170
  // Mark provider name for orchestrator
187
- fetchEmail.__name = 'construct';
171
+ fetchEmail.__name = "construct";
188
172
  return fetchEmail;
189
173
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Cosiall Profile Emails Provider
3
+ *
4
+ * Lookups against the Cosiall FlexIQ Profile Emails API.
5
+ * This is FREE and returns all known emails for a LinkedIn profile.
6
+ *
7
+ * Lookup priority:
8
+ * 1. objectUrn (most precise - "urn:li:member:129147375")
9
+ * 2. linkedInUrl (URL like "https://www.linkedin.com/in/john-doe/")
10
+ * 3. vanity (username extracted from URL or direct field)
11
+ */
12
+ import type { EnrichmentCandidate, ProviderMultiResult, ProviderResult } from "../types";
13
+ /**
14
+ * Cosiall provider configuration
15
+ * No configuration needed - uses Cosiall API credentials from global config
16
+ */
17
+ export interface CosiallConfig {
18
+ /** Whether to enable the provider (default: true) */
19
+ enabled?: boolean;
20
+ }
21
+ /**
22
+ * Create the Cosiall provider function
23
+ *
24
+ * Returns all emails found for a LinkedIn profile.
25
+ * Since this is a free service, it should always be executed.
26
+ */
27
+ export declare function createCosiallProvider(config?: CosiallConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | ProviderMultiResult | null>;
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ /**
3
+ * Cosiall Profile Emails Provider
4
+ *
5
+ * Lookups against the Cosiall FlexIQ Profile Emails API.
6
+ * This is FREE and returns all known emails for a LinkedIn profile.
7
+ *
8
+ * Lookup priority:
9
+ * 1. objectUrn (most precise - "urn:li:member:129147375")
10
+ * 2. linkedInUrl (URL like "https://www.linkedin.com/in/john-doe/")
11
+ * 3. vanity (username extracted from URL or direct field)
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.createCosiallProvider = createCosiallProvider;
15
+ const cosiall_client_1 = require("../../cosiall-client");
16
+ const validation_1 = require("../utils/validation");
17
+ const noop_provider_1 = require("../utils/noop-provider");
18
+ /**
19
+ * Extract objectUrn from candidate
20
+ */
21
+ function extractObjectUrn(candidate) {
22
+ const objectUrn = candidate.objectUrn || candidate.object_urn;
23
+ if (objectUrn && objectUrn.startsWith("urn:li:")) {
24
+ return objectUrn;
25
+ }
26
+ return null;
27
+ }
28
+ /**
29
+ * Extract LinkedIn URL from candidate
30
+ */
31
+ function extractLinkedInUrl(candidate) {
32
+ const url = candidate.linkedinUrl || candidate.linkedin_url;
33
+ if (url && url.includes("linkedin.com")) {
34
+ return url;
35
+ }
36
+ return null;
37
+ }
38
+ /**
39
+ * Extract vanity (username) from candidate
40
+ */
41
+ function extractVanity(candidate) {
42
+ // Direct username field
43
+ const directUsername = candidate.linkedinUsername || candidate.linkedin_username;
44
+ if (directUsername) {
45
+ return directUsername;
46
+ }
47
+ // Extract from URL
48
+ const url = candidate.linkedinUrl || candidate.linkedin_url;
49
+ if (url) {
50
+ const extracted = (0, validation_1.extractLinkedInUsername)(url);
51
+ if (extracted) {
52
+ return extracted;
53
+ }
54
+ }
55
+ return null;
56
+ }
57
+ /**
58
+ * Create the Cosiall provider function
59
+ *
60
+ * Returns all emails found for a LinkedIn profile.
61
+ * Since this is a free service, it should always be executed.
62
+ */
63
+ function createCosiallProvider(config) {
64
+ // Check if explicitly disabled
65
+ if (config?.enabled === false) {
66
+ return (0, noop_provider_1.createNoOpProvider)("cosiall");
67
+ }
68
+ async function fetchEmail(candidate) {
69
+ // Extract lookup parameters in priority order
70
+ const objectUrn = extractObjectUrn(candidate);
71
+ const linkedInUrl = extractLinkedInUrl(candidate);
72
+ const vanity = extractVanity(candidate);
73
+ // Must have at least one lookup parameter
74
+ if (!objectUrn && !linkedInUrl && !vanity) {
75
+ return null;
76
+ }
77
+ try {
78
+ const result = await (0, cosiall_client_1.fetchProfileEmailsFromCosiall)({
79
+ objectUrn: objectUrn || undefined,
80
+ linkedInUrl: linkedInUrl || undefined,
81
+ vanity: vanity || undefined,
82
+ });
83
+ // No emails found
84
+ if (!result.emails || result.emails.length === 0) {
85
+ return null;
86
+ }
87
+ // Build multi-result with all emails
88
+ const emails = result.emails.map((email) => ({
89
+ email,
90
+ verified: true, // Cosiall data is from LinkedIn profiles
91
+ confidence: 85, // Good confidence - these are profile-associated emails
92
+ metadata: {
93
+ profileId: result.profileId,
94
+ objectUrn: result.objectUrn,
95
+ linkedInUrl: result.linkedInUrl,
96
+ source: "cosiall",
97
+ },
98
+ }));
99
+ return { emails };
100
+ }
101
+ catch {
102
+ // Silently fail - provider failures shouldn't stop the enrichment flow
103
+ return null;
104
+ }
105
+ }
106
+ // Mark provider name for orchestrator
107
+ fetchEmail.__name = "cosiall";
108
+ return fetchEmail;
109
+ }
@@ -1,16 +1,22 @@
1
1
  /**
2
2
  * Dropcontact Provider
3
3
  *
4
- * Dropcontact API for email finding.
5
- * This is a placeholder implementation - full implementation would call the Dropcontact API.
4
+ * Dropcontact API for email finding and enrichment.
5
+ * Uses the batch enrichment API with polling for results.
6
+ *
7
+ * API Flow:
8
+ * 1. POST /batch to submit contacts for enrichment
9
+ * 2. GET /batch/{request_id} to poll for results
10
+ *
11
+ * Features:
12
+ * - Email finding from name + company/website
13
+ * - Email verification (deliverable/undeliverable)
14
+ * - GDPR compliant (EU-based)
15
+ *
16
+ * @see https://developer.dropcontact.com/
6
17
  */
7
- import type { EnrichmentCandidate, ProviderResult, DropcontactConfig } from '../types';
18
+ import type { EnrichmentCandidate, ProviderResult, DropcontactConfig } from "../types";
8
19
  /**
9
20
  * Create the Dropcontact provider function
10
- *
11
- * Note: This is a placeholder. Full implementation would:
12
- * 1. POST to https://api.dropcontact.io/batch with contacts
13
- * 2. Poll for results
14
- * 3. Return enriched email with verification status
15
21
  */
16
- export declare function createDropcontactProvider(config: DropcontactConfig): (_candidate: EnrichmentCandidate) => Promise<ProviderResult | null>;
22
+ export declare function createDropcontactProvider(config: DropcontactConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | null>;
@@ -2,36 +2,205 @@
2
2
  /**
3
3
  * Dropcontact Provider
4
4
  *
5
- * Dropcontact API for email finding.
6
- * This is a placeholder implementation - full implementation would call the Dropcontact API.
5
+ * Dropcontact API for email finding and enrichment.
6
+ * Uses the batch enrichment API with polling for results.
7
+ *
8
+ * API Flow:
9
+ * 1. POST /batch to submit contacts for enrichment
10
+ * 2. GET /batch/{request_id} to poll for results
11
+ *
12
+ * Features:
13
+ * - Email finding from name + company/website
14
+ * - Email verification (deliverable/undeliverable)
15
+ * - GDPR compliant (EU-based)
16
+ *
17
+ * @see https://developer.dropcontact.com/
7
18
  */
8
19
  Object.defineProperty(exports, "__esModule", { value: true });
9
20
  exports.createDropcontactProvider = createDropcontactProvider;
21
+ const http_retry_1 = require("../utils/http-retry");
22
+ const noop_provider_1 = require("../utils/noop-provider");
23
+ const API_BASE = "https://api.dropcontact.io";
24
+ /**
25
+ * Extract inputs from candidate for Dropcontact API
26
+ */
27
+ function extractInputs(candidate) {
28
+ const name = candidate.name || candidate.fullName || candidate.full_name;
29
+ const first = candidate.first ||
30
+ candidate.first_name ||
31
+ candidate.firstName ||
32
+ name?.split?.(" ")?.[0];
33
+ const last = candidate.last ||
34
+ candidate.last_name ||
35
+ candidate.lastName ||
36
+ name?.split?.(" ")?.slice(1).join(" ") ||
37
+ undefined;
38
+ const domain = candidate.domain ||
39
+ candidate.companyDomain ||
40
+ candidate.company_domain ||
41
+ undefined;
42
+ const company = candidate.company ||
43
+ candidate.currentCompany ||
44
+ candidate.organization ||
45
+ candidate.org ||
46
+ undefined;
47
+ const linkedin = candidate.linkedinUrl ||
48
+ candidate.linkedin_url ||
49
+ (candidate.linkedinHandle
50
+ ? `https://www.linkedin.com/in/${candidate.linkedinHandle}`
51
+ : undefined) ||
52
+ (candidate.linkedin_handle
53
+ ? `https://www.linkedin.com/in/${candidate.linkedin_handle}`
54
+ : undefined) ||
55
+ undefined;
56
+ return {
57
+ first_name: first,
58
+ last_name: last,
59
+ full_name: name,
60
+ company: company,
61
+ website: domain,
62
+ linkedin: linkedin,
63
+ };
64
+ }
10
65
  /**
11
66
  * Create the Dropcontact provider function
12
- *
13
- * Note: This is a placeholder. Full implementation would:
14
- * 1. POST to https://api.dropcontact.io/batch with contacts
15
- * 2. Poll for results
16
- * 3. Return enriched email with verification status
17
67
  */
18
68
  function createDropcontactProvider(config) {
19
69
  const { apiKey } = config;
20
70
  if (!apiKey) {
21
- // Return a no-op provider if not configured
22
- const noopProvider = async () => null;
23
- noopProvider.__name = 'dropcontact';
24
- return noopProvider;
71
+ return (0, noop_provider_1.createNoOpProvider)("dropcontact");
72
+ }
73
+ /**
74
+ * Submit a batch request to Dropcontact
75
+ */
76
+ async function submitBatch(contacts) {
77
+ const body = {
78
+ data: contacts,
79
+ siren: false, // We don't need French company registration numbers
80
+ language: "en",
81
+ };
82
+ try {
83
+ const response = await (0, http_retry_1.postWithRetry)(`${API_BASE}/batch`, body, { "X-Access-Token": apiKey }, { retries: 2, backoffMs: 500, timeoutMs: 30000 });
84
+ if (response.error) {
85
+ return null;
86
+ }
87
+ return response.request_id || null;
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ }
93
+ /**
94
+ * Poll for batch results with exponential backoff
95
+ */
96
+ async function pollResults(requestId, maxAttempts = 10, initialDelayMs = 2000) {
97
+ let delayMs = initialDelayMs;
98
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
99
+ await (0, http_retry_1.delay)(delayMs);
100
+ try {
101
+ const response = await (0, http_retry_1.getWithRetry)(`${API_BASE}/batch/${requestId}`, { "X-Access-Token": apiKey }, { retries: 1, backoffMs: 500, timeoutMs: 30000 });
102
+ // Check if processing is complete
103
+ if (response.error) {
104
+ return null;
105
+ }
106
+ // If we have data and status is finished (or we got data back), return it
107
+ if (response.data && response.data.length > 0) {
108
+ // Check if still processing
109
+ if (response.status === "pending") {
110
+ // Increase delay for next poll (max 10 seconds)
111
+ delayMs = Math.min(delayMs * 1.5, 10000);
112
+ continue;
113
+ }
114
+ return response.data;
115
+ }
116
+ // If explicitly finished but no data
117
+ if (response.status === "finished") {
118
+ return response.data || null;
119
+ }
120
+ // Still processing, increase delay
121
+ delayMs = Math.min(delayMs * 1.5, 10000);
122
+ }
123
+ catch {
124
+ // Network error, retry with backoff
125
+ delayMs = Math.min(delayMs * 2, 10000);
126
+ }
127
+ }
128
+ return null; // Timeout after max attempts
25
129
  }
26
- async function fetchEmail(_candidate) {
27
- // TODO: Implement Dropcontact API integration
28
- // API: POST https://api.dropcontact.io/batch
29
- // Headers: X-Access-Token: {apiKey}
30
- // Body: { data: [{ first_name, last_name, company, website }], siren: false, language: "en" }
31
- // For now, return null (skip this provider)
32
- return null;
130
+ /**
131
+ * Fetch email for a candidate
132
+ */
133
+ async function fetchEmail(candidate) {
134
+ const contact = extractInputs(candidate);
135
+ // Need at least a name and company/website to search
136
+ const hasName = (0, http_retry_1.truthy)(contact.first_name) || (0, http_retry_1.truthy)(contact.full_name);
137
+ const hasCompanyInfo = (0, http_retry_1.truthy)(contact.company) || (0, http_retry_1.truthy)(contact.website);
138
+ const hasLinkedIn = (0, http_retry_1.truthy)(contact.linkedin);
139
+ if (!hasName && !hasLinkedIn) {
140
+ return null; // Need at least name or LinkedIn URL
141
+ }
142
+ if (!hasCompanyInfo && !hasLinkedIn) {
143
+ return null; // Need company info or LinkedIn URL
144
+ }
145
+ // Submit single contact as batch
146
+ const requestId = await submitBatch([contact]);
147
+ if (!requestId) {
148
+ return null;
149
+ }
150
+ // Poll for results
151
+ const results = await pollResults(requestId);
152
+ if (!results || results.length === 0) {
153
+ return null;
154
+ }
155
+ // Extract email from first result
156
+ const enriched = results[0];
157
+ if (!enriched.email || enriched.email.length === 0) {
158
+ return null;
159
+ }
160
+ // Find the best email (prefer valid ones)
161
+ let bestEmail = null;
162
+ for (const emailObj of enriched.email) {
163
+ if (!emailObj.email)
164
+ continue;
165
+ // Prefer valid emails
166
+ if (emailObj.qualification === "valid") {
167
+ bestEmail = emailObj;
168
+ break;
169
+ }
170
+ // Take catch-all if no valid found yet
171
+ if (!bestEmail ||
172
+ (bestEmail.qualification !== "valid" &&
173
+ emailObj.qualification === "catch_all")) {
174
+ bestEmail = emailObj;
175
+ }
176
+ // Take any email if nothing else
177
+ if (!bestEmail) {
178
+ bestEmail = emailObj;
179
+ }
180
+ }
181
+ if (!bestEmail || !bestEmail.email) {
182
+ return null;
183
+ }
184
+ // Map qualification to verified status
185
+ const verified = (0, http_retry_1.mapVerifiedStatus)(bestEmail.qualification);
186
+ // Confidence based on qualification
187
+ let score = 50;
188
+ if (bestEmail.qualification === "valid") {
189
+ score = 95;
190
+ }
191
+ else if (bestEmail.qualification === "catch_all") {
192
+ score = 60;
193
+ }
194
+ else if (bestEmail.qualification === "invalid") {
195
+ score = 10;
196
+ }
197
+ return {
198
+ email: bestEmail.email,
199
+ verified,
200
+ score,
201
+ };
33
202
  }
34
203
  // Mark provider name for orchestrator
35
- fetchEmail.__name = 'dropcontact';
204
+ fetchEmail.__name = "dropcontact";
36
205
  return fetchEmail;
37
206
  }
@@ -2,7 +2,14 @@
2
2
  * Hunter.io Provider
3
3
  *
4
4
  * Hunter.io public API for email finding.
5
- * Uses email-finder endpoint with name+domain, falls back to domain-search.
5
+ *
6
+ * Supports three modes (in order of preference):
7
+ * 1. linkedin_handle - Most accurate, uses LinkedIn vanity/handle
8
+ * 2. domain + first_name + last_name - Standard email finder
9
+ * 3. company + first_name + last_name - When domain is unknown
10
+ * 4. domain-search fallback - Returns multiple emails for a domain
11
+ *
12
+ * @see https://hunter.io/api-documentation/v2#email-finder
6
13
  */
7
14
  import type { EnrichmentCandidate, ProviderResult, ProviderMultiResult, HunterConfig } from "../types";
8
15
  /**
@@ -3,31 +3,35 @@
3
3
  * Hunter.io Provider
4
4
  *
5
5
  * Hunter.io public API for email finding.
6
- * Uses email-finder endpoint with name+domain, falls back to domain-search.
6
+ *
7
+ * Supports three modes (in order of preference):
8
+ * 1. linkedin_handle - Most accurate, uses LinkedIn vanity/handle
9
+ * 2. domain + first_name + last_name - Standard email finder
10
+ * 3. company + first_name + last_name - When domain is unknown
11
+ * 4. domain-search fallback - Returns multiple emails for a domain
12
+ *
13
+ * @see https://hunter.io/api-documentation/v2#email-finder
7
14
  */
8
15
  Object.defineProperty(exports, "__esModule", { value: true });
9
16
  exports.createHunterProvider = createHunterProvider;
10
17
  const http_retry_1 = require("../utils/http-retry");
18
+ const candidate_parser_1 = require("../utils/candidate-parser");
19
+ const noop_provider_1 = require("../utils/noop-provider");
11
20
  const API_BASE = "https://api.hunter.io/v2";
12
21
  /**
13
- * Extract name and domain from candidate
22
+ * Extract Hunter-specific inputs from candidate using shared parser
14
23
  */
15
- function extractInputs(candidate) {
16
- const name = candidate.name || candidate.fullName || candidate.full_name;
17
- const first = candidate.first ||
18
- candidate.first_name ||
19
- candidate.firstName ||
20
- name?.split?.(" ")?.[0];
21
- const last = candidate.last ||
22
- candidate.last_name ||
23
- candidate.lastName ||
24
- name?.split?.(" ")?.slice(1).join(" ") ||
25
- undefined;
26
- const domain = candidate.domain ||
27
- candidate.companyDomain ||
28
- candidate.company_domain ||
29
- undefined;
30
- return { first, last, domain };
24
+ function extractHunterInputs(candidate) {
25
+ const { firstName, lastName } = (0, candidate_parser_1.extractName)(candidate);
26
+ const { company, domain } = (0, candidate_parser_1.extractCompany)(candidate);
27
+ const { username: linkedinHandle } = (0, candidate_parser_1.extractLinkedIn)(candidate);
28
+ return {
29
+ first: firstName || undefined,
30
+ last: lastName || undefined,
31
+ domain: domain ?? undefined,
32
+ company: company ?? undefined,
33
+ linkedinHandle: linkedinHandle ?? undefined,
34
+ };
31
35
  }
32
36
  /**
33
37
  * Create the Hunter provider function
@@ -35,17 +39,23 @@ function extractInputs(candidate) {
35
39
  function createHunterProvider(config) {
36
40
  const { apiKey } = config;
37
41
  if (!apiKey) {
38
- // Return a no-op provider if not configured
39
- const noopProvider = async () => null;
40
- noopProvider.__name = "hunter";
41
- return noopProvider;
42
+ return (0, noop_provider_1.createNoOpProvider)("hunter");
42
43
  }
43
44
  async function fetchEmail(candidate) {
44
- const { first, last, domain } = extractInputs(candidate);
45
+ const { first, last, domain, company, linkedinHandle } = extractHunterInputs(candidate);
45
46
  let url = null;
46
47
  let isEmailFinder = false;
47
- // Use email-finder if we have name components
48
- if ((0, http_retry_1.truthy)(first) && (0, http_retry_1.truthy)(last) && (0, http_retry_1.truthy)(domain)) {
48
+ // Priority 1: Use linkedin_handle if available (most accurate)
49
+ if ((0, http_retry_1.truthy)(linkedinHandle)) {
50
+ const qs = new URLSearchParams({
51
+ api_key: String(apiKey),
52
+ linkedin_handle: String(linkedinHandle),
53
+ });
54
+ url = `${API_BASE}/email-finder?${qs.toString()}`;
55
+ isEmailFinder = true;
56
+ }
57
+ // Priority 2: Use domain + name (standard email finder)
58
+ else if ((0, http_retry_1.truthy)(first) && (0, http_retry_1.truthy)(last) && (0, http_retry_1.truthy)(domain)) {
49
59
  const qs = new URLSearchParams({
50
60
  api_key: String(apiKey),
51
61
  domain: String(domain),
@@ -55,7 +65,18 @@ function createHunterProvider(config) {
55
65
  url = `${API_BASE}/email-finder?${qs.toString()}`;
56
66
  isEmailFinder = true;
57
67
  }
58
- // Fall back to domain-search if only domain available (can return multiple)
68
+ // Priority 3: Use company + name (when domain unknown)
69
+ else if ((0, http_retry_1.truthy)(first) && (0, http_retry_1.truthy)(last) && (0, http_retry_1.truthy)(company)) {
70
+ const qs = new URLSearchParams({
71
+ api_key: String(apiKey),
72
+ company: String(company),
73
+ first_name: String(first),
74
+ last_name: String(last),
75
+ });
76
+ url = `${API_BASE}/email-finder?${qs.toString()}`;
77
+ isEmailFinder = true;
78
+ }
79
+ // Priority 4: Fall back to domain-search if only domain available (can return multiple)
59
80
  else if ((0, http_retry_1.truthy)(domain)) {
60
81
  const qs = new URLSearchParams({
61
82
  api_key: String(apiKey),
@@ -65,10 +86,13 @@ function createHunterProvider(config) {
65
86
  url = `${API_BASE}/domain-search?${qs.toString()}`;
66
87
  }
67
88
  else {
68
- return null; // Can't search without domain
89
+ return null; // Can't search without domain, company, or linkedin_handle
69
90
  }
70
91
  try {
71
- const json = await (0, http_retry_1.getWithRetry)(url, undefined, { retries: 1, backoffMs: 100 });
92
+ const json = await (0, http_retry_1.getWithRetry)(url, undefined, {
93
+ retries: 1,
94
+ backoffMs: 100,
95
+ });
72
96
  // Parse email-finder response shape (single result)
73
97
  if (isEmailFinder) {
74
98
  const ef = (json && (json.data || json.result));