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.
- package/dist/enrichment/index.d.ts +43 -0
- package/dist/enrichment/index.js +231 -0
- package/dist/enrichment/orchestrator.d.ts +31 -0
- package/dist/enrichment/orchestrator.js +218 -0
- package/dist/enrichment/providers/apollo.d.ts +11 -0
- package/dist/enrichment/providers/apollo.js +136 -0
- package/dist/enrichment/providers/construct.d.ts +11 -0
- package/dist/enrichment/providers/construct.js +107 -0
- package/dist/enrichment/providers/dropcontact.d.ts +16 -0
- package/dist/enrichment/providers/dropcontact.js +37 -0
- package/dist/enrichment/providers/hunter.d.ts +11 -0
- package/dist/enrichment/providers/hunter.js +162 -0
- package/dist/enrichment/providers/index.d.ts +9 -0
- package/dist/enrichment/providers/index.js +18 -0
- package/dist/enrichment/providers/ldd.d.ts +11 -0
- package/dist/enrichment/providers/ldd.js +110 -0
- package/dist/enrichment/providers/smartprospect.d.ts +11 -0
- package/dist/enrichment/providers/smartprospect.js +249 -0
- package/dist/enrichment/types.d.ts +329 -0
- package/dist/enrichment/types.js +31 -0
- package/dist/enrichment/utils/disposable-domains.d.ts +24 -0
- package/dist/enrichment/utils/disposable-domains.js +1011 -0
- package/dist/enrichment/utils/index.d.ts +6 -0
- package/dist/enrichment/utils/index.js +22 -0
- package/dist/enrichment/utils/personal-domains.d.ts +31 -0
- package/dist/enrichment/utils/personal-domains.js +95 -0
- package/dist/enrichment/utils/validation.d.ts +42 -0
- package/dist/enrichment/utils/validation.js +130 -0
- package/dist/enrichment/verification/index.d.ts +4 -0
- package/dist/enrichment/verification/index.js +8 -0
- package/dist/enrichment/verification/mx.d.ts +16 -0
- package/dist/enrichment/verification/mx.js +168 -0
- package/dist/index.d.ts +17 -14
- package/dist/index.js +20 -1
- 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>;
|