linkedin-secret-sauce 0.4.0 → 0.5.1

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 (39) hide show
  1. package/dist/enrichment/auth/index.d.ts +4 -0
  2. package/dist/enrichment/auth/index.js +12 -0
  3. package/dist/enrichment/auth/smartlead-auth.d.ts +50 -0
  4. package/dist/enrichment/auth/smartlead-auth.js +156 -0
  5. package/dist/enrichment/index.d.ts +44 -0
  6. package/dist/enrichment/index.js +244 -0
  7. package/dist/enrichment/orchestrator.d.ts +31 -0
  8. package/dist/enrichment/orchestrator.js +218 -0
  9. package/dist/enrichment/providers/apollo.d.ts +11 -0
  10. package/dist/enrichment/providers/apollo.js +136 -0
  11. package/dist/enrichment/providers/construct.d.ts +11 -0
  12. package/dist/enrichment/providers/construct.js +107 -0
  13. package/dist/enrichment/providers/dropcontact.d.ts +16 -0
  14. package/dist/enrichment/providers/dropcontact.js +37 -0
  15. package/dist/enrichment/providers/hunter.d.ts +11 -0
  16. package/dist/enrichment/providers/hunter.js +162 -0
  17. package/dist/enrichment/providers/index.d.ts +9 -0
  18. package/dist/enrichment/providers/index.js +18 -0
  19. package/dist/enrichment/providers/ldd.d.ts +11 -0
  20. package/dist/enrichment/providers/ldd.js +110 -0
  21. package/dist/enrichment/providers/smartprospect.d.ts +19 -0
  22. package/dist/enrichment/providers/smartprospect.js +333 -0
  23. package/dist/enrichment/types.d.ts +343 -0
  24. package/dist/enrichment/types.js +31 -0
  25. package/dist/enrichment/utils/disposable-domains.d.ts +24 -0
  26. package/dist/enrichment/utils/disposable-domains.js +1011 -0
  27. package/dist/enrichment/utils/index.d.ts +6 -0
  28. package/dist/enrichment/utils/index.js +22 -0
  29. package/dist/enrichment/utils/personal-domains.d.ts +31 -0
  30. package/dist/enrichment/utils/personal-domains.js +95 -0
  31. package/dist/enrichment/utils/validation.d.ts +42 -0
  32. package/dist/enrichment/utils/validation.js +130 -0
  33. package/dist/enrichment/verification/index.d.ts +4 -0
  34. package/dist/enrichment/verification/index.js +8 -0
  35. package/dist/enrichment/verification/mx.d.ts +16 -0
  36. package/dist/enrichment/verification/mx.js +168 -0
  37. package/dist/index.d.ts +18 -14
  38. package/dist/index.js +26 -1
  39. package/package.json +15 -16
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+ /**
3
+ * Email Enrichment Waterfall Orchestrator
4
+ *
5
+ * Runs providers in sequence until a verified email is found that meets
6
+ * the confidence threshold. Supports budget tracking and cost callbacks.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.enrichBusinessEmail = enrichBusinessEmail;
10
+ exports.enrichBatch = enrichBatch;
11
+ const personal_domains_1 = require("./utils/personal-domains");
12
+ const disposable_domains_1 = require("./utils/disposable-domains");
13
+ /**
14
+ * Default provider costs in USD per lookup
15
+ */
16
+ const DEFAULT_PROVIDER_COSTS = {
17
+ construct: 0,
18
+ ldd: 0,
19
+ smartprospect: 0.01,
20
+ hunter: 0.005,
21
+ apollo: 0,
22
+ dropcontact: 0.01,
23
+ };
24
+ /**
25
+ * Normalize provider result to canonical format
26
+ */
27
+ function normalizeProviderResult(providerName, raw) {
28
+ const iso = new Date().toISOString();
29
+ // No result from provider
30
+ if (!raw || !raw.email) {
31
+ return {
32
+ business_email: null,
33
+ business_email_source: providerName,
34
+ business_email_verified: false,
35
+ last_checked_at: iso,
36
+ };
37
+ }
38
+ const email = String(raw.email);
39
+ const confidence = typeof raw.score === 'number'
40
+ ? raw.score
41
+ : typeof raw.confidence === 'number'
42
+ ? raw.confidence
43
+ : undefined;
44
+ const verified = Boolean(raw.verified);
45
+ return {
46
+ business_email: email,
47
+ business_email_source: providerName,
48
+ business_email_verified: verified,
49
+ business_email_confidence: confidence,
50
+ last_checked_at: iso,
51
+ };
52
+ }
53
+ /**
54
+ * Get provider name from function
55
+ */
56
+ function getProviderName(provider, index) {
57
+ return provider.__name ?? `provider-${index}`;
58
+ }
59
+ /**
60
+ * Get provider cost
61
+ */
62
+ function getProviderCost(providerName) {
63
+ return DEFAULT_PROVIDER_COSTS[providerName] ?? 0;
64
+ }
65
+ /**
66
+ * Enrich a single candidate with business email
67
+ */
68
+ async function enrichBusinessEmail(candidate, options) {
69
+ const { providers, maxCostPerEmail = Infinity, confidenceThreshold = 0, retryMs = 200, onCost, logger, } = options;
70
+ // Track remaining budget
71
+ let remaining = Number.isFinite(maxCostPerEmail) && maxCostPerEmail >= 0 ? maxCostPerEmail : Infinity;
72
+ logger?.debug?.('enrichment.start', {
73
+ providers: providers.length,
74
+ confidenceThreshold,
75
+ maxCostPerEmail,
76
+ });
77
+ for (let i = 0; i < providers.length; i++) {
78
+ const provider = providers[i];
79
+ const providerName = getProviderName(provider, i);
80
+ const stepCost = getProviderCost(providerName);
81
+ // Skip if cost exceeds remaining budget
82
+ if (stepCost > 0 && remaining < stepCost) {
83
+ logger?.debug?.('enrichment.skip_budget', {
84
+ provider: providerName,
85
+ cost: stepCost,
86
+ remaining,
87
+ });
88
+ continue;
89
+ }
90
+ let raw = null;
91
+ try {
92
+ logger?.debug?.('enrichment.provider.start', { provider: providerName });
93
+ raw = await provider(candidate);
94
+ logger?.debug?.('enrichment.provider.done', {
95
+ provider: providerName,
96
+ hasResult: !!raw?.email,
97
+ });
98
+ }
99
+ catch (error) {
100
+ logger?.warn?.('enrichment.provider.error', {
101
+ provider: providerName,
102
+ error: error instanceof Error ? error.message : String(error),
103
+ });
104
+ // Retry once after delay on transient errors
105
+ if (retryMs > 0) {
106
+ await new Promise((r) => setTimeout(r, retryMs));
107
+ try {
108
+ raw = await provider(candidate);
109
+ logger?.debug?.('enrichment.provider.retry.done', {
110
+ provider: providerName,
111
+ hasResult: !!raw?.email,
112
+ });
113
+ }
114
+ catch {
115
+ raw = null;
116
+ }
117
+ }
118
+ }
119
+ // Deduct cost (even on failure - API was called)
120
+ if (stepCost > 0) {
121
+ remaining = Math.max(0, remaining - stepCost);
122
+ if (onCost) {
123
+ try {
124
+ await onCost(providerName, stepCost);
125
+ }
126
+ catch {
127
+ // Ignore cost callback errors
128
+ }
129
+ }
130
+ logger?.debug?.('enrichment.cost.debit', {
131
+ provider: providerName,
132
+ cost: stepCost,
133
+ remaining,
134
+ });
135
+ }
136
+ // Normalize result
137
+ const normalized = normalizeProviderResult(providerName, raw);
138
+ // Skip if no email found
139
+ if (!normalized.business_email) {
140
+ continue;
141
+ }
142
+ // Filter out personal emails
143
+ if ((0, personal_domains_1.isPersonalEmail)(normalized.business_email)) {
144
+ logger?.debug?.('enrichment.skip_personal', {
145
+ provider: providerName,
146
+ email: normalized.business_email,
147
+ });
148
+ continue;
149
+ }
150
+ // Filter out disposable emails
151
+ if ((0, disposable_domains_1.isDisposableEmail)(normalized.business_email)) {
152
+ logger?.debug?.('enrichment.skip_disposable', {
153
+ provider: providerName,
154
+ email: normalized.business_email,
155
+ });
156
+ continue;
157
+ }
158
+ // Check confidence threshold
159
+ const score = normalized.business_email_confidence;
160
+ if (confidenceThreshold > 0 && score !== undefined && score < confidenceThreshold) {
161
+ logger?.debug?.('enrichment.skip_low_confidence', {
162
+ provider: providerName,
163
+ score,
164
+ threshold: confidenceThreshold,
165
+ });
166
+ continue;
167
+ }
168
+ // Check if verified
169
+ if (!normalized.business_email_verified) {
170
+ logger?.debug?.('enrichment.skip_unverified', {
171
+ provider: providerName,
172
+ email: normalized.business_email,
173
+ });
174
+ continue;
175
+ }
176
+ // Success!
177
+ logger?.info?.('enrichment.success', {
178
+ provider: providerName,
179
+ email: normalized.business_email,
180
+ confidence: normalized.business_email_confidence,
181
+ });
182
+ return normalized;
183
+ }
184
+ // No provider found a valid email
185
+ logger?.debug?.('enrichment.not_found', {
186
+ candidateHasName: !!(candidate.firstName || candidate.name),
187
+ candidateHasDomain: !!(candidate.domain || candidate.company),
188
+ });
189
+ return {
190
+ business_email: null,
191
+ business_email_source: null,
192
+ business_email_verified: false,
193
+ last_checked_at: new Date().toISOString(),
194
+ status: 'not_found',
195
+ };
196
+ }
197
+ /**
198
+ * Enrich multiple candidates in batches
199
+ */
200
+ async function enrichBatch(candidates, options) {
201
+ const results = [];
202
+ const batchSize = options.batchSize ?? 50;
203
+ const delayMs = options.delayMs ?? 200;
204
+ for (let i = 0; i < candidates.length; i += batchSize) {
205
+ const chunk = candidates.slice(i, i + batchSize);
206
+ // Process batch in parallel
207
+ const batchResults = await Promise.all(chunk.map((candidate) => enrichBusinessEmail(candidate, options)));
208
+ // Combine results with candidates
209
+ batchResults.forEach((result, idx) => {
210
+ results.push({ candidate: chunk[idx], ...result });
211
+ });
212
+ // Delay between batches to avoid rate limiting
213
+ if (i + batchSize < candidates.length && delayMs > 0) {
214
+ await new Promise((r) => setTimeout(r, delayMs));
215
+ }
216
+ }
217
+ return results;
218
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Apollo.io Provider
3
+ *
4
+ * Apollo.io public API for email finding.
5
+ * Uses mixed_people/search endpoint.
6
+ */
7
+ import type { EnrichmentCandidate, ProviderResult, ApolloConfig } from "../types";
8
+ /**
9
+ * Create the Apollo provider function
10
+ */
11
+ export declare function createApolloProvider(config: ApolloConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | null>;
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ /**
3
+ * Apollo.io Provider
4
+ *
5
+ * Apollo.io public API for email finding.
6
+ * Uses mixed_people/search endpoint.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.createApolloProvider = createApolloProvider;
10
+ const API_BASE = "https://api.apollo.io/v1";
11
+ /**
12
+ * Delay helper for retry logic
13
+ */
14
+ async function delay(ms) {
15
+ return new Promise((r) => setTimeout(r, ms));
16
+ }
17
+ /**
18
+ * Check if value is truthy
19
+ */
20
+ function truthy(v) {
21
+ return v !== undefined && v !== null && String(v).length > 0;
22
+ }
23
+ /**
24
+ * Map Apollo verification status to boolean
25
+ */
26
+ function mapVerified(status) {
27
+ if (!status)
28
+ return undefined;
29
+ const s = String(status).toLowerCase();
30
+ if (s === "verified" || s === "deliverable")
31
+ return true;
32
+ if (s === "invalid" || s === "undeliverable")
33
+ return false;
34
+ return undefined;
35
+ }
36
+ /**
37
+ * HTTP request with retry on 429 rate limit
38
+ */
39
+ async function requestWithRetry(url, init, retries = 1, backoffMs = 200) {
40
+ let lastErr;
41
+ for (let i = 0; i <= retries; i++) {
42
+ try {
43
+ const res = await fetch(url, init);
44
+ // Retry on rate limit
45
+ if (res?.status === 429 && i < retries) {
46
+ await delay(backoffMs * Math.pow(2, i));
47
+ continue;
48
+ }
49
+ if (!res || res.status >= 400) {
50
+ lastErr = new Error(`apollo_http_${res?.status ?? "error"}`);
51
+ break;
52
+ }
53
+ const json = (await res.json());
54
+ return json;
55
+ }
56
+ catch (err) {
57
+ lastErr = err;
58
+ if (i < retries) {
59
+ await delay(backoffMs * Math.pow(2, i));
60
+ continue;
61
+ }
62
+ }
63
+ }
64
+ throw lastErr ?? new Error("apollo_http_error");
65
+ }
66
+ /**
67
+ * Extract inputs from candidate
68
+ */
69
+ function extractInputs(candidate) {
70
+ const name = candidate.name || candidate.full_name || candidate.fullName || undefined;
71
+ const org = candidate.company || candidate.organization || candidate.org || undefined;
72
+ const domain = candidate.domain ||
73
+ candidate.companyDomain ||
74
+ candidate.company_domain ||
75
+ undefined;
76
+ return { name, org, domain };
77
+ }
78
+ /**
79
+ * Create the Apollo provider function
80
+ */
81
+ function createApolloProvider(config) {
82
+ const { apiKey } = config;
83
+ if (!apiKey) {
84
+ // Return a no-op provider if not configured
85
+ const noopProvider = async () => null;
86
+ noopProvider.__name = "apollo";
87
+ return noopProvider;
88
+ }
89
+ async function fetchEmail(candidate) {
90
+ const { name, org, domain } = extractInputs(candidate);
91
+ // Build request body
92
+ const body = { page: 1, per_page: 1 };
93
+ if (truthy(name))
94
+ body["q_person_name"] = String(name);
95
+ if (truthy(domain))
96
+ body["organization_domains"] = [String(domain)];
97
+ else if (truthy(org))
98
+ body["q_organization_name"] = String(org);
99
+ const url = `${API_BASE}/mixed_people/search`;
100
+ const init = {
101
+ method: "POST",
102
+ headers: {
103
+ "content-type": "application/json",
104
+ "X-Api-Key": String(apiKey),
105
+ },
106
+ body: JSON.stringify(body),
107
+ };
108
+ try {
109
+ const json = await requestWithRetry(url, init, 1, 100);
110
+ // Parse response - handle multiple possible shapes
111
+ const people = (json &&
112
+ (json.people || json.matches || json.data?.people));
113
+ if (!Array.isArray(people) || people.length === 0)
114
+ return null;
115
+ const p = people[0] || {};
116
+ const email = p.email ??
117
+ p.primary_email ??
118
+ p.emails?.[0]?.email ??
119
+ null;
120
+ if (!email)
121
+ return null;
122
+ const score = typeof p.email_confidence === "number"
123
+ ? p.email_confidence
124
+ : Number(p.email_confidence ?? 0) || undefined;
125
+ const verified = mapVerified(p.email_status ??
126
+ p.emails?.[0]?.status);
127
+ return { email, verified, score };
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ }
133
+ // Mark provider name for orchestrator
134
+ fetchEmail.__name = "apollo";
135
+ return fetchEmail;
136
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Email Construction Provider
3
+ *
4
+ * Generates email pattern candidates based on name + domain, then verifies via MX check.
5
+ * This is a FREE provider that doesn't require any API keys.
6
+ */
7
+ import type { EnrichmentCandidate, ProviderResult, ConstructConfig } from '../types';
8
+ /**
9
+ * Create the construct provider function
10
+ */
11
+ export declare function createConstructProvider(config?: ConstructConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | null>;
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ /**
3
+ * Email Construction Provider
4
+ *
5
+ * Generates email pattern candidates based on name + domain, then verifies via MX check.
6
+ * This is a FREE provider that doesn't require any API keys.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.createConstructProvider = createConstructProvider;
10
+ const mx_1 = require("../verification/mx");
11
+ const personal_domains_1 = require("../utils/personal-domains");
12
+ const validation_1 = require("../utils/validation");
13
+ /**
14
+ * Build all email pattern candidates for a person
15
+ */
16
+ function buildCandidates(input) {
17
+ const domain = String(input.domain || '').toLowerCase();
18
+ const first = (0, validation_1.cleanNamePart)(input.first || '');
19
+ const last = (0, validation_1.cleanNamePart)(input.last || '');
20
+ const fl = first ? first[0] : '';
21
+ const locals = [];
22
+ // Patterns with both first and last name
23
+ if (first && last) {
24
+ locals.push(`${first}.${last}`); // john.doe
25
+ locals.push(`${fl}.${last}`); // j.doe
26
+ locals.push(`${first}${last[0] ?? ''}`); // johnd
27
+ locals.push(`${fl}${last}`); // jdoe
28
+ locals.push(`${first}_${last}`); // john_doe
29
+ locals.push(`${last}.${first}`); // doe.john
30
+ }
31
+ // Single name patterns
32
+ if (first)
33
+ locals.push(first); // john
34
+ if (last)
35
+ locals.push(last); // doe
36
+ // Deduplicate while preserving order
37
+ const seen = new Set();
38
+ const emails = [];
39
+ for (const local of locals) {
40
+ if (!local)
41
+ continue;
42
+ const email = `${local}@${domain}`;
43
+ if (!seen.has(email)) {
44
+ seen.add(email);
45
+ emails.push(email);
46
+ }
47
+ }
48
+ return emails;
49
+ }
50
+ /**
51
+ * Extract name components from candidate
52
+ */
53
+ function extractNames(candidate) {
54
+ const firstName = candidate.firstName ||
55
+ candidate.first_name ||
56
+ candidate.first ||
57
+ candidate.name?.split(' ')?.[0] ||
58
+ '';
59
+ const lastName = candidate.lastName ||
60
+ candidate.last_name ||
61
+ candidate.last ||
62
+ candidate.name?.split(' ')?.slice(1).join(' ') ||
63
+ '';
64
+ return { first: firstName, last: lastName };
65
+ }
66
+ /**
67
+ * Extract domain from candidate
68
+ */
69
+ function extractDomain(candidate) {
70
+ return candidate.domain || candidate.companyDomain || candidate.company_domain || '';
71
+ }
72
+ /**
73
+ * Create the construct provider function
74
+ */
75
+ function createConstructProvider(config) {
76
+ const maxAttempts = config?.maxAttempts ?? 8;
77
+ const timeoutMs = config?.timeoutMs ?? 5000;
78
+ async function fetchEmail(candidate) {
79
+ const { first, last } = extractNames(candidate);
80
+ const domain = extractDomain(candidate);
81
+ // Skip if missing required fields
82
+ if (!first || !domain) {
83
+ return null;
84
+ }
85
+ // Skip personal domains
86
+ if ((0, personal_domains_1.isPersonalDomain)(domain)) {
87
+ return null;
88
+ }
89
+ const candidates = buildCandidates({ first, last, domain });
90
+ const max = Math.min(candidates.length, maxAttempts);
91
+ for (let i = 0; i < max; i++) {
92
+ const email = candidates[i];
93
+ const verification = await (0, mx_1.verifyEmailMx)(email, { timeoutMs });
94
+ if (verification.valid === true && verification.confidence >= 50) {
95
+ return {
96
+ email,
97
+ verified: true,
98
+ score: verification.confidence,
99
+ };
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+ // Mark provider name for orchestrator
105
+ fetchEmail.__name = 'construct';
106
+ return fetchEmail;
107
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Dropcontact Provider
3
+ *
4
+ * Dropcontact API for email finding.
5
+ * This is a placeholder implementation - full implementation would call the Dropcontact API.
6
+ */
7
+ import type { EnrichmentCandidate, ProviderResult, DropcontactConfig } from '../types';
8
+ /**
9
+ * 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
+ */
16
+ export declare function createDropcontactProvider(config: DropcontactConfig): (_candidate: EnrichmentCandidate) => Promise<ProviderResult | null>;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ /**
3
+ * Dropcontact Provider
4
+ *
5
+ * Dropcontact API for email finding.
6
+ * This is a placeholder implementation - full implementation would call the Dropcontact API.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.createDropcontactProvider = createDropcontactProvider;
10
+ /**
11
+ * 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
+ */
18
+ function createDropcontactProvider(config) {
19
+ const { apiKey } = config;
20
+ if (!apiKey) {
21
+ // Return a no-op provider if not configured
22
+ const noopProvider = async () => null;
23
+ noopProvider.__name = 'dropcontact';
24
+ return noopProvider;
25
+ }
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;
33
+ }
34
+ // Mark provider name for orchestrator
35
+ fetchEmail.__name = 'dropcontact';
36
+ return fetchEmail;
37
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Hunter.io Provider
3
+ *
4
+ * Hunter.io public API for email finding.
5
+ * Uses email-finder endpoint with name+domain, falls back to domain-search.
6
+ */
7
+ import type { EnrichmentCandidate, ProviderResult, HunterConfig } from "../types";
8
+ /**
9
+ * Create the Hunter provider function
10
+ */
11
+ export declare function createHunterProvider(config: HunterConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | null>;