linkedin-secret-sauce 0.4.0 → 0.5.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 (35) hide show
  1. package/dist/enrichment/index.d.ts +43 -0
  2. package/dist/enrichment/index.js +231 -0
  3. package/dist/enrichment/orchestrator.d.ts +31 -0
  4. package/dist/enrichment/orchestrator.js +218 -0
  5. package/dist/enrichment/providers/apollo.d.ts +11 -0
  6. package/dist/enrichment/providers/apollo.js +136 -0
  7. package/dist/enrichment/providers/construct.d.ts +11 -0
  8. package/dist/enrichment/providers/construct.js +107 -0
  9. package/dist/enrichment/providers/dropcontact.d.ts +16 -0
  10. package/dist/enrichment/providers/dropcontact.js +37 -0
  11. package/dist/enrichment/providers/hunter.d.ts +11 -0
  12. package/dist/enrichment/providers/hunter.js +162 -0
  13. package/dist/enrichment/providers/index.d.ts +9 -0
  14. package/dist/enrichment/providers/index.js +18 -0
  15. package/dist/enrichment/providers/ldd.d.ts +11 -0
  16. package/dist/enrichment/providers/ldd.js +110 -0
  17. package/dist/enrichment/providers/smartprospect.d.ts +11 -0
  18. package/dist/enrichment/providers/smartprospect.js +249 -0
  19. package/dist/enrichment/types.d.ts +329 -0
  20. package/dist/enrichment/types.js +31 -0
  21. package/dist/enrichment/utils/disposable-domains.d.ts +24 -0
  22. package/dist/enrichment/utils/disposable-domains.js +1011 -0
  23. package/dist/enrichment/utils/index.d.ts +6 -0
  24. package/dist/enrichment/utils/index.js +22 -0
  25. package/dist/enrichment/utils/personal-domains.d.ts +31 -0
  26. package/dist/enrichment/utils/personal-domains.js +95 -0
  27. package/dist/enrichment/utils/validation.d.ts +42 -0
  28. package/dist/enrichment/utils/validation.js +130 -0
  29. package/dist/enrichment/verification/index.d.ts +4 -0
  30. package/dist/enrichment/verification/index.js +8 -0
  31. package/dist/enrichment/verification/mx.d.ts +16 -0
  32. package/dist/enrichment/verification/mx.js +168 -0
  33. package/dist/index.d.ts +17 -14
  34. package/dist/index.js +20 -1
  35. package/package.json +1 -1
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Email Enrichment Module
3
+ *
4
+ * Provides email enrichment capabilities with multiple provider support,
5
+ * waterfall orchestration, and configurable options.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { createEnrichmentClient } from 'linkedin-secret-sauce';
10
+ *
11
+ * const enricher = createEnrichmentClient({
12
+ * providers: {
13
+ * hunter: { apiKey: process.env.HUNTER_API_KEY },
14
+ * apollo: { apiKey: process.env.APOLLO_API_KEY },
15
+ * },
16
+ * options: {
17
+ * maxCostPerEmail: 0.05,
18
+ * confidenceThreshold: 50,
19
+ * },
20
+ * });
21
+ *
22
+ * const result = await enricher.enrich({
23
+ * firstName: 'John',
24
+ * lastName: 'Doe',
25
+ * company: 'Acme Corp',
26
+ * });
27
+ * ```
28
+ */
29
+ import type { EnrichmentClientConfig, EnrichmentClient } from './types';
30
+ /**
31
+ * Create an enrichment client with the given configuration
32
+ *
33
+ * @param config - Configuration for the enrichment client
34
+ * @returns An enrichment client with enrich and enrichBatch methods
35
+ */
36
+ export declare function createEnrichmentClient(config: EnrichmentClientConfig): EnrichmentClient;
37
+ export * from './types';
38
+ export { isPersonalEmail, isBusinessEmail, isPersonalDomain, PERSONAL_DOMAINS, } from './utils/personal-domains';
39
+ export { isDisposableEmail, isDisposableDomain, DISPOSABLE_DOMAINS, } from './utils/disposable-domains';
40
+ export { isValidEmailSyntax, isRoleAccount, asciiFold, cleanNamePart, hostnameFromUrl, extractLinkedInUsername, } from './utils/validation';
41
+ export { verifyEmailMx } from './verification/mx';
42
+ export { createConstructProvider, createLddProvider, createSmartProspectProvider, createHunterProvider, createApolloProvider, createDropcontactProvider, } from './providers';
43
+ export { enrichBusinessEmail, enrichBatch } from './orchestrator';
@@ -0,0 +1,231 @@
1
+ "use strict";
2
+ /**
3
+ * Email Enrichment Module
4
+ *
5
+ * Provides email enrichment capabilities with multiple provider support,
6
+ * waterfall orchestration, and configurable options.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { createEnrichmentClient } from 'linkedin-secret-sauce';
11
+ *
12
+ * const enricher = createEnrichmentClient({
13
+ * providers: {
14
+ * hunter: { apiKey: process.env.HUNTER_API_KEY },
15
+ * apollo: { apiKey: process.env.APOLLO_API_KEY },
16
+ * },
17
+ * options: {
18
+ * maxCostPerEmail: 0.05,
19
+ * confidenceThreshold: 50,
20
+ * },
21
+ * });
22
+ *
23
+ * const result = await enricher.enrich({
24
+ * firstName: 'John',
25
+ * lastName: 'Doe',
26
+ * company: 'Acme Corp',
27
+ * });
28
+ * ```
29
+ */
30
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
31
+ if (k2 === undefined) k2 = k;
32
+ var desc = Object.getOwnPropertyDescriptor(m, k);
33
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
34
+ desc = { enumerable: true, get: function() { return m[k]; } };
35
+ }
36
+ Object.defineProperty(o, k2, desc);
37
+ }) : (function(o, m, k, k2) {
38
+ if (k2 === undefined) k2 = k;
39
+ o[k2] = m[k];
40
+ }));
41
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
42
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
43
+ };
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.enrichBatch = exports.enrichBusinessEmail = exports.createDropcontactProvider = exports.createApolloProvider = exports.createHunterProvider = exports.createSmartProspectProvider = exports.createLddProvider = exports.createConstructProvider = exports.verifyEmailMx = exports.extractLinkedInUsername = exports.hostnameFromUrl = exports.cleanNamePart = exports.asciiFold = exports.isRoleAccount = exports.isValidEmailSyntax = exports.DISPOSABLE_DOMAINS = exports.isDisposableDomain = exports.isDisposableEmail = exports.PERSONAL_DOMAINS = exports.isPersonalDomain = exports.isBusinessEmail = exports.isPersonalEmail = void 0;
46
+ exports.createEnrichmentClient = createEnrichmentClient;
47
+ const orchestrator_1 = require("./orchestrator");
48
+ const construct_1 = require("./providers/construct");
49
+ const ldd_1 = require("./providers/ldd");
50
+ const smartprospect_1 = require("./providers/smartprospect");
51
+ const hunter_1 = require("./providers/hunter");
52
+ const apollo_1 = require("./providers/apollo");
53
+ const dropcontact_1 = require("./providers/dropcontact");
54
+ /**
55
+ * Default provider order
56
+ */
57
+ const DEFAULT_ORDER = [
58
+ 'construct',
59
+ 'ldd',
60
+ 'smartprospect',
61
+ 'hunter',
62
+ 'apollo',
63
+ 'dropcontact',
64
+ ];
65
+ /**
66
+ * Create an enrichment client with the given configuration
67
+ *
68
+ * @param config - Configuration for the enrichment client
69
+ * @returns An enrichment client with enrich and enrichBatch methods
70
+ */
71
+ function createEnrichmentClient(config) {
72
+ const { providers: providerConfigs, options = {}, cache, onCost, logger } = config;
73
+ // Build provider functions based on configuration
74
+ const providerFuncs = new Map();
75
+ // Always create construct provider (free, no API key needed)
76
+ providerFuncs.set('construct', (0, construct_1.createConstructProvider)(providerConfigs.construct));
77
+ // Create other providers if configured
78
+ if (providerConfigs.ldd) {
79
+ providerFuncs.set('ldd', (0, ldd_1.createLddProvider)(providerConfigs.ldd));
80
+ }
81
+ if (providerConfigs.smartprospect) {
82
+ providerFuncs.set('smartprospect', (0, smartprospect_1.createSmartProspectProvider)(providerConfigs.smartprospect));
83
+ }
84
+ if (providerConfigs.hunter) {
85
+ providerFuncs.set('hunter', (0, hunter_1.createHunterProvider)(providerConfigs.hunter));
86
+ }
87
+ if (providerConfigs.apollo) {
88
+ providerFuncs.set('apollo', (0, apollo_1.createApolloProvider)(providerConfigs.apollo));
89
+ }
90
+ if (providerConfigs.dropcontact) {
91
+ providerFuncs.set('dropcontact', (0, dropcontact_1.createDropcontactProvider)(providerConfigs.dropcontact));
92
+ }
93
+ // Build ordered provider list
94
+ const providerOrder = options.providerOrder ?? DEFAULT_ORDER;
95
+ const orderedProviders = [];
96
+ for (const name of providerOrder) {
97
+ const provider = providerFuncs.get(name);
98
+ if (provider) {
99
+ orderedProviders.push(provider);
100
+ }
101
+ }
102
+ // Orchestrator options
103
+ const orchestratorOptions = {
104
+ providers: orderedProviders,
105
+ maxCostPerEmail: options.maxCostPerEmail,
106
+ confidenceThreshold: options.confidenceThreshold,
107
+ retryMs: options.retryMs,
108
+ onCost,
109
+ logger,
110
+ };
111
+ return {
112
+ /**
113
+ * Enrich a single candidate with business email
114
+ */
115
+ async enrich(candidate) {
116
+ // Check cache first
117
+ if (cache) {
118
+ const cacheKey = buildCacheKey(candidate);
119
+ if (cacheKey) {
120
+ try {
121
+ const cached = await cache.get(cacheKey);
122
+ if (cached) {
123
+ logger?.debug?.('enrichment.cache.hit', { cacheKey });
124
+ return cached;
125
+ }
126
+ }
127
+ catch {
128
+ // Ignore cache errors
129
+ }
130
+ }
131
+ }
132
+ // Run enrichment
133
+ const result = await (0, orchestrator_1.enrichBusinessEmail)(candidate, orchestratorOptions);
134
+ // Store in cache
135
+ if (cache && result.business_email) {
136
+ const cacheKey = buildCacheKey(candidate);
137
+ if (cacheKey) {
138
+ try {
139
+ await cache.set(cacheKey, result, 30 * 24 * 60 * 60 * 1000); // 30 days TTL
140
+ }
141
+ catch {
142
+ // Ignore cache errors
143
+ }
144
+ }
145
+ }
146
+ return result;
147
+ },
148
+ /**
149
+ * Enrich multiple candidates in batches
150
+ */
151
+ async enrichBatch(candidates, batchOptions) {
152
+ return (0, orchestrator_1.enrichBatch)(candidates, {
153
+ ...orchestratorOptions,
154
+ ...batchOptions,
155
+ });
156
+ },
157
+ };
158
+ }
159
+ /**
160
+ * Build a cache key from candidate data
161
+ */
162
+ function buildCacheKey(candidate) {
163
+ const parts = [];
164
+ // Use LinkedIn URL/username as primary key if available
165
+ const linkedinUrl = candidate.linkedinUrl || candidate.linkedin_url;
166
+ const linkedinUsername = candidate.linkedinUsername || candidate.linkedin_username;
167
+ const linkedinId = candidate.linkedinId || candidate.linkedin_id;
168
+ if (linkedinUrl) {
169
+ parts.push(`li:${linkedinUrl}`);
170
+ }
171
+ else if (linkedinUsername) {
172
+ parts.push(`li:${linkedinUsername}`);
173
+ }
174
+ else if (linkedinId) {
175
+ parts.push(`li:${linkedinId}`);
176
+ }
177
+ else {
178
+ // Fall back to name + domain/company
179
+ const firstName = candidate.firstName || candidate.first_name || candidate.first || '';
180
+ const lastName = candidate.lastName || candidate.last_name || candidate.last || '';
181
+ const domain = candidate.domain || candidate.companyDomain || candidate.company_domain || '';
182
+ const company = candidate.company || candidate.currentCompany || candidate.organization || '';
183
+ if (firstName && (domain || company)) {
184
+ parts.push(`name:${firstName.toLowerCase()}`);
185
+ if (lastName)
186
+ parts.push(lastName.toLowerCase());
187
+ if (domain)
188
+ parts.push(`dom:${domain.toLowerCase()}`);
189
+ else if (company)
190
+ parts.push(`co:${company.toLowerCase()}`);
191
+ }
192
+ }
193
+ if (parts.length === 0) {
194
+ return null;
195
+ }
196
+ return parts.join('|');
197
+ }
198
+ // Re-export types
199
+ __exportStar(require("./types"), exports);
200
+ // Re-export utilities
201
+ var personal_domains_1 = require("./utils/personal-domains");
202
+ Object.defineProperty(exports, "isPersonalEmail", { enumerable: true, get: function () { return personal_domains_1.isPersonalEmail; } });
203
+ Object.defineProperty(exports, "isBusinessEmail", { enumerable: true, get: function () { return personal_domains_1.isBusinessEmail; } });
204
+ Object.defineProperty(exports, "isPersonalDomain", { enumerable: true, get: function () { return personal_domains_1.isPersonalDomain; } });
205
+ Object.defineProperty(exports, "PERSONAL_DOMAINS", { enumerable: true, get: function () { return personal_domains_1.PERSONAL_DOMAINS; } });
206
+ var disposable_domains_1 = require("./utils/disposable-domains");
207
+ Object.defineProperty(exports, "isDisposableEmail", { enumerable: true, get: function () { return disposable_domains_1.isDisposableEmail; } });
208
+ Object.defineProperty(exports, "isDisposableDomain", { enumerable: true, get: function () { return disposable_domains_1.isDisposableDomain; } });
209
+ Object.defineProperty(exports, "DISPOSABLE_DOMAINS", { enumerable: true, get: function () { return disposable_domains_1.DISPOSABLE_DOMAINS; } });
210
+ var validation_1 = require("./utils/validation");
211
+ Object.defineProperty(exports, "isValidEmailSyntax", { enumerable: true, get: function () { return validation_1.isValidEmailSyntax; } });
212
+ Object.defineProperty(exports, "isRoleAccount", { enumerable: true, get: function () { return validation_1.isRoleAccount; } });
213
+ Object.defineProperty(exports, "asciiFold", { enumerable: true, get: function () { return validation_1.asciiFold; } });
214
+ Object.defineProperty(exports, "cleanNamePart", { enumerable: true, get: function () { return validation_1.cleanNamePart; } });
215
+ Object.defineProperty(exports, "hostnameFromUrl", { enumerable: true, get: function () { return validation_1.hostnameFromUrl; } });
216
+ Object.defineProperty(exports, "extractLinkedInUsername", { enumerable: true, get: function () { return validation_1.extractLinkedInUsername; } });
217
+ // Re-export verification
218
+ var mx_1 = require("./verification/mx");
219
+ Object.defineProperty(exports, "verifyEmailMx", { enumerable: true, get: function () { return mx_1.verifyEmailMx; } });
220
+ // Re-export providers (for advanced usage)
221
+ var providers_1 = require("./providers");
222
+ Object.defineProperty(exports, "createConstructProvider", { enumerable: true, get: function () { return providers_1.createConstructProvider; } });
223
+ Object.defineProperty(exports, "createLddProvider", { enumerable: true, get: function () { return providers_1.createLddProvider; } });
224
+ Object.defineProperty(exports, "createSmartProspectProvider", { enumerable: true, get: function () { return providers_1.createSmartProspectProvider; } });
225
+ Object.defineProperty(exports, "createHunterProvider", { enumerable: true, get: function () { return providers_1.createHunterProvider; } });
226
+ Object.defineProperty(exports, "createApolloProvider", { enumerable: true, get: function () { return providers_1.createApolloProvider; } });
227
+ Object.defineProperty(exports, "createDropcontactProvider", { enumerable: true, get: function () { return providers_1.createDropcontactProvider; } });
228
+ // Re-export orchestrator (for advanced usage)
229
+ var orchestrator_2 = require("./orchestrator");
230
+ Object.defineProperty(exports, "enrichBusinessEmail", { enumerable: true, get: function () { return orchestrator_2.enrichBusinessEmail; } });
231
+ Object.defineProperty(exports, "enrichBatch", { enumerable: true, get: function () { return orchestrator_2.enrichBatch; } });
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Email Enrichment Waterfall Orchestrator
3
+ *
4
+ * Runs providers in sequence until a verified email is found that meets
5
+ * the confidence threshold. Supports budget tracking and cost callbacks.
6
+ */
7
+ import type { CanonicalEmail, EnrichmentCandidate, ProviderFunc, BatchEnrichmentOptions, CostCallback, EnrichmentLogger } from './types';
8
+ export interface OrchestratorOptions {
9
+ /** Provider functions in order */
10
+ providers: ProviderFunc[];
11
+ /** Maximum cost per email in USD */
12
+ maxCostPerEmail?: number;
13
+ /** Minimum confidence threshold (0-100) */
14
+ confidenceThreshold?: number;
15
+ /** Retry delay in ms */
16
+ retryMs?: number;
17
+ /** Cost callback */
18
+ onCost?: CostCallback;
19
+ /** Logger */
20
+ logger?: EnrichmentLogger;
21
+ }
22
+ /**
23
+ * Enrich a single candidate with business email
24
+ */
25
+ export declare function enrichBusinessEmail(candidate: EnrichmentCandidate, options: OrchestratorOptions): Promise<CanonicalEmail>;
26
+ /**
27
+ * Enrich multiple candidates in batches
28
+ */
29
+ export declare function enrichBatch(candidates: EnrichmentCandidate[], options: OrchestratorOptions & BatchEnrichmentOptions): Promise<Array<{
30
+ candidate: EnrichmentCandidate;
31
+ } & CanonicalEmail>>;
@@ -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>;