linkedin-secret-sauce 0.11.0 → 0.12.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/README.md +512 -232
- package/dist/cosiall-client.d.ts +40 -1
- package/dist/cosiall-client.js +142 -3
- package/dist/enrichment/auth/smartlead-auth.d.ts +3 -3
- package/dist/enrichment/auth/smartlead-auth.js +25 -25
- package/dist/enrichment/index.d.ts +6 -4
- package/dist/enrichment/index.js +25 -24
- package/dist/enrichment/matching.d.ts +8 -3
- package/dist/enrichment/matching.js +7 -5
- package/dist/enrichment/orchestrator.js +44 -14
- package/dist/enrichment/providers/construct.js +72 -14
- package/dist/enrichment/providers/hunter.js +6 -60
- package/dist/enrichment/providers/index.d.ts +0 -1
- package/dist/enrichment/providers/index.js +1 -3
- package/dist/enrichment/providers/ldd.js +5 -47
- package/dist/enrichment/providers/smartprospect.js +9 -14
- package/dist/enrichment/types.d.ts +23 -24
- package/dist/enrichment/types.js +22 -21
- package/dist/enrichment/utils/http-retry.d.ts +96 -0
- package/dist/enrichment/utils/http-retry.js +162 -0
- package/dist/enrichment/verification/index.d.ts +1 -1
- package/dist/enrichment/verification/index.js +3 -1
- package/dist/enrichment/verification/mx.d.ts +33 -0
- package/dist/enrichment/verification/mx.js +367 -7
- package/dist/index.d.ts +197 -6
- package/dist/index.js +159 -12
- package/dist/parsers/search-parser.js +7 -3
- package/dist/types.d.ts +29 -3
- package/dist/utils/metrics.d.ts +3 -0
- package/dist/utils/metrics.js +3 -0
- package/package.json +30 -22
package/dist/cosiall-client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AccountCookies } from "./types";
|
|
1
|
+
import type { AccountCookies, CosiallProfileEmailsResponse, ProfileEmailsLookupOptions } from "./types";
|
|
2
2
|
/**
|
|
3
3
|
* Fetches LinkedIn cookies for all available accounts from the Cosiall API.
|
|
4
4
|
* These cookies are used for authenticating requests to LinkedIn's API.
|
|
@@ -19,3 +19,42 @@ import type { AccountCookies } from "./types";
|
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
21
|
export declare function fetchCookiesFromCosiall(): Promise<AccountCookies[]>;
|
|
22
|
+
/**
|
|
23
|
+
* Fetches email addresses associated with a LinkedIn profile from the Cosiall API.
|
|
24
|
+
*
|
|
25
|
+
* This endpoint provides flexible lookup by ObjectURN, LinkedIn URL, or vanity name.
|
|
26
|
+
* At least one lookup parameter must be provided.
|
|
27
|
+
*
|
|
28
|
+
* @param options - Lookup options (objectUrn, linkedInUrl, or vanity)
|
|
29
|
+
* @returns Profile emails response with profileId, objectUrn, linkedInUrl, and emails array
|
|
30
|
+
* @throws LinkedInClientError with code INVALID_REQUEST if no lookup parameters provided
|
|
31
|
+
* @throws LinkedInClientError with code NOT_FOUND if profile not found
|
|
32
|
+
* @throws LinkedInClientError with code AUTH_ERROR if API key is invalid
|
|
33
|
+
* @throws LinkedInClientError with code REQUEST_FAILED for other API errors
|
|
34
|
+
*
|
|
35
|
+
* @remarks
|
|
36
|
+
* - Lookup priority: objectUrn > linkedInUrl > vanity
|
|
37
|
+
* - Returns empty emails array if profile exists but has no emails
|
|
38
|
+
* - URL normalization is handled server-side (trailing slashes, https prefix)
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* // Lookup by ObjectURN (most precise)
|
|
43
|
+
* const result = await fetchProfileEmailsFromCosiall({
|
|
44
|
+
* objectUrn: 'urn:li:fsd_profile:ACoAABcdEfG'
|
|
45
|
+
* });
|
|
46
|
+
*
|
|
47
|
+
* // Lookup by LinkedIn URL
|
|
48
|
+
* const result = await fetchProfileEmailsFromCosiall({
|
|
49
|
+
* linkedInUrl: 'https://www.linkedin.com/in/john-doe/'
|
|
50
|
+
* });
|
|
51
|
+
*
|
|
52
|
+
* // Lookup by vanity name
|
|
53
|
+
* const result = await fetchProfileEmailsFromCosiall({
|
|
54
|
+
* vanity: 'john-doe'
|
|
55
|
+
* });
|
|
56
|
+
*
|
|
57
|
+
* console.log(`Found ${result.emails.length} emails:`, result.emails);
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export declare function fetchProfileEmailsFromCosiall(options: ProfileEmailsLookupOptions): Promise<CosiallProfileEmailsResponse>;
|
package/dist/cosiall-client.js
CHANGED
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.fetchCookiesFromCosiall = fetchCookiesFromCosiall;
|
|
37
|
+
exports.fetchProfileEmailsFromCosiall = fetchProfileEmailsFromCosiall;
|
|
37
38
|
const config_1 = require("./config");
|
|
38
39
|
const errors_1 = require("./utils/errors");
|
|
39
40
|
const logger_1 = require("./utils/logger");
|
|
@@ -120,11 +121,149 @@ async function fetchCookiesFromCosiall() {
|
|
|
120
121
|
}
|
|
121
122
|
return true;
|
|
122
123
|
}
|
|
123
|
-
return data
|
|
124
|
-
.filter(isItem)
|
|
125
|
-
.map((item) => ({
|
|
124
|
+
return data.filter(isItem).map((item) => ({
|
|
126
125
|
accountId: item.accountId,
|
|
127
126
|
cookies: item.cookies,
|
|
128
127
|
expiresAt: item.expiresAt,
|
|
129
128
|
}));
|
|
130
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Fetches email addresses associated with a LinkedIn profile from the Cosiall API.
|
|
132
|
+
*
|
|
133
|
+
* This endpoint provides flexible lookup by ObjectURN, LinkedIn URL, or vanity name.
|
|
134
|
+
* At least one lookup parameter must be provided.
|
|
135
|
+
*
|
|
136
|
+
* @param options - Lookup options (objectUrn, linkedInUrl, or vanity)
|
|
137
|
+
* @returns Profile emails response with profileId, objectUrn, linkedInUrl, and emails array
|
|
138
|
+
* @throws LinkedInClientError with code INVALID_REQUEST if no lookup parameters provided
|
|
139
|
+
* @throws LinkedInClientError with code NOT_FOUND if profile not found
|
|
140
|
+
* @throws LinkedInClientError with code AUTH_ERROR if API key is invalid
|
|
141
|
+
* @throws LinkedInClientError with code REQUEST_FAILED for other API errors
|
|
142
|
+
*
|
|
143
|
+
* @remarks
|
|
144
|
+
* - Lookup priority: objectUrn > linkedInUrl > vanity
|
|
145
|
+
* - Returns empty emails array if profile exists but has no emails
|
|
146
|
+
* - URL normalization is handled server-side (trailing slashes, https prefix)
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```typescript
|
|
150
|
+
* // Lookup by ObjectURN (most precise)
|
|
151
|
+
* const result = await fetchProfileEmailsFromCosiall({
|
|
152
|
+
* objectUrn: 'urn:li:fsd_profile:ACoAABcdEfG'
|
|
153
|
+
* });
|
|
154
|
+
*
|
|
155
|
+
* // Lookup by LinkedIn URL
|
|
156
|
+
* const result = await fetchProfileEmailsFromCosiall({
|
|
157
|
+
* linkedInUrl: 'https://www.linkedin.com/in/john-doe/'
|
|
158
|
+
* });
|
|
159
|
+
*
|
|
160
|
+
* // Lookup by vanity name
|
|
161
|
+
* const result = await fetchProfileEmailsFromCosiall({
|
|
162
|
+
* vanity: 'john-doe'
|
|
163
|
+
* });
|
|
164
|
+
*
|
|
165
|
+
* console.log(`Found ${result.emails.length} emails:`, result.emails);
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
async function fetchProfileEmailsFromCosiall(options) {
|
|
169
|
+
const { objectUrn, linkedInUrl, vanity } = options;
|
|
170
|
+
// Validate at least one parameter is provided
|
|
171
|
+
if (!objectUrn && !linkedInUrl && !vanity) {
|
|
172
|
+
throw new errors_1.LinkedInClientError("At least one of 'objectUrn', 'linkedInUrl', or 'vanity' must be provided", "INVALID_REQUEST", 400);
|
|
173
|
+
}
|
|
174
|
+
const { cosiallApiUrl, cosiallApiKey } = (0, config_1.getConfig)();
|
|
175
|
+
const base = cosiallApiUrl.replace(/\/+$/, "");
|
|
176
|
+
// Build query string
|
|
177
|
+
const params = new URLSearchParams();
|
|
178
|
+
if (objectUrn)
|
|
179
|
+
params.set("objectUrn", objectUrn);
|
|
180
|
+
if (linkedInUrl)
|
|
181
|
+
params.set("linkedInUrl", linkedInUrl);
|
|
182
|
+
if (vanity)
|
|
183
|
+
params.set("vanity", vanity);
|
|
184
|
+
const url = `${base}/api/flexiq/profile-emails?${params.toString()}`;
|
|
185
|
+
(0, logger_1.log)("info", "cosiall.profileEmails.start", {
|
|
186
|
+
objectUrn,
|
|
187
|
+
linkedInUrl,
|
|
188
|
+
vanity,
|
|
189
|
+
});
|
|
190
|
+
(0, metrics_1.incrementMetric)("cosiallProfileEmailsFetches");
|
|
191
|
+
const response = await fetch(url, {
|
|
192
|
+
method: "GET",
|
|
193
|
+
headers: {
|
|
194
|
+
"X-API-Key": cosiallApiKey,
|
|
195
|
+
Accept: "application/json",
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
// Handle error responses
|
|
199
|
+
if (!response.ok) {
|
|
200
|
+
const status = response.status;
|
|
201
|
+
let errorMessage = "Profile emails fetch failed";
|
|
202
|
+
let errorCode = "REQUEST_FAILED";
|
|
203
|
+
try {
|
|
204
|
+
const errorData = (await response.json());
|
|
205
|
+
errorMessage = errorData.error || errorMessage;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// Ignore JSON parse errors
|
|
209
|
+
}
|
|
210
|
+
if (status === 400) {
|
|
211
|
+
errorCode = "INVALID_REQUEST";
|
|
212
|
+
(0, logger_1.log)("warn", "cosiall.profileEmails.invalidRequest", {
|
|
213
|
+
status,
|
|
214
|
+
errorMessage,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
else if (status === 401) {
|
|
218
|
+
errorCode = "AUTH_ERROR";
|
|
219
|
+
(0, logger_1.log)("warn", "cosiall.profileEmails.authError", { status });
|
|
220
|
+
}
|
|
221
|
+
else if (status === 404) {
|
|
222
|
+
errorCode = "NOT_FOUND";
|
|
223
|
+
(0, logger_1.log)("info", "cosiall.profileEmails.notFound", {
|
|
224
|
+
objectUrn,
|
|
225
|
+
linkedInUrl,
|
|
226
|
+
vanity,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
(0, logger_1.log)("warn", "cosiall.profileEmails.error", { status, errorMessage });
|
|
231
|
+
(0, metrics_1.incrementMetric)("cosiallProfileEmailsFailures");
|
|
232
|
+
// Report unexpected errors to Sentry
|
|
233
|
+
try {
|
|
234
|
+
const { reportCriticalError } = await Promise.resolve().then(() => __importStar(require("./utils/sentry")));
|
|
235
|
+
reportCriticalError("Cosiall Profile Emails API failure", {
|
|
236
|
+
status,
|
|
237
|
+
errorMessage,
|
|
238
|
+
objectUrn,
|
|
239
|
+
linkedInUrl,
|
|
240
|
+
vanity,
|
|
241
|
+
tags: { component: "cosiall-client", endpoint: "profile-emails" },
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
catch { }
|
|
245
|
+
}
|
|
246
|
+
throw new errors_1.LinkedInClientError(errorMessage, errorCode, status);
|
|
247
|
+
}
|
|
248
|
+
const data = await response.json();
|
|
249
|
+
// Validate response structure
|
|
250
|
+
if (!data ||
|
|
251
|
+
typeof data !== "object" ||
|
|
252
|
+
!("profileId" in data) ||
|
|
253
|
+
!("objectUrn" in data) ||
|
|
254
|
+
!("linkedInUrl" in data) ||
|
|
255
|
+
!("emails" in data)) {
|
|
256
|
+
(0, logger_1.log)("error", "cosiall.profileEmails.invalidFormat", {
|
|
257
|
+
dataType: typeof data,
|
|
258
|
+
});
|
|
259
|
+
(0, metrics_1.incrementMetric)("cosiallProfileEmailsFailures");
|
|
260
|
+
throw new errors_1.LinkedInClientError("Invalid profile emails response format", "REQUEST_FAILED", 500);
|
|
261
|
+
}
|
|
262
|
+
const result = data;
|
|
263
|
+
(0, logger_1.log)("info", "cosiall.profileEmails.success", {
|
|
264
|
+
profileId: result.profileId,
|
|
265
|
+
emailCount: result.emails.length,
|
|
266
|
+
});
|
|
267
|
+
(0, metrics_1.incrementMetric)("cosiallProfileEmailsSuccess");
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
@@ -64,10 +64,10 @@ export declare function getSmartLeadTokenCacheStats(): {
|
|
|
64
64
|
* // In your test setup
|
|
65
65
|
* import { enableFileCache } from 'linkedin-secret-sauce/enrichment';
|
|
66
66
|
*
|
|
67
|
-
* enableFileCache(); // Now tokens persist between test runs
|
|
67
|
+
* await enableFileCache(); // Now tokens persist between test runs
|
|
68
68
|
* ```
|
|
69
69
|
*/
|
|
70
|
-
export declare function enableFileCache(): void
|
|
70
|
+
export declare function enableFileCache(): Promise<void>;
|
|
71
71
|
/**
|
|
72
72
|
* Disable file-based token caching
|
|
73
73
|
*/
|
|
@@ -79,4 +79,4 @@ export declare function isFileCacheEnabled(): boolean;
|
|
|
79
79
|
/**
|
|
80
80
|
* Clear the file cache
|
|
81
81
|
*/
|
|
82
|
-
export declare function clearFileCache(): void
|
|
82
|
+
export declare function clearFileCache(): Promise<void>;
|
|
@@ -51,9 +51,10 @@ exports.enableFileCache = enableFileCache;
|
|
|
51
51
|
exports.disableFileCache = disableFileCache;
|
|
52
52
|
exports.isFileCacheEnabled = isFileCacheEnabled;
|
|
53
53
|
exports.clearFileCache = clearFileCache;
|
|
54
|
-
const fs = __importStar(require("fs"));
|
|
54
|
+
const fs = __importStar(require("fs/promises"));
|
|
55
55
|
const path = __importStar(require("path"));
|
|
56
56
|
const os = __importStar(require("os"));
|
|
57
|
+
const http_retry_1 = require("../utils/http-retry");
|
|
57
58
|
const DEFAULT_LOGIN_URL = 'https://server.smartlead.ai/api/auth/login';
|
|
58
59
|
const DEFAULT_REFRESH_BUFFER_MS = 5 * 60 * 1000; // 5 minutes
|
|
59
60
|
const TOKEN_LIFETIME_MS = 24 * 60 * 60 * 1000; // Assume 24h JWT lifetime (conservative)
|
|
@@ -213,34 +214,32 @@ function getFileCachePath() {
|
|
|
213
214
|
return path.join(homeDir, FILE_CACHE_NAME);
|
|
214
215
|
}
|
|
215
216
|
/**
|
|
216
|
-
* Load tokens from file cache
|
|
217
|
+
* Load tokens from file cache (async)
|
|
217
218
|
*/
|
|
218
|
-
function loadFileCache() {
|
|
219
|
+
async function loadFileCache() {
|
|
219
220
|
const cache = new Map();
|
|
220
221
|
if (!fileCacheEnabled)
|
|
221
222
|
return cache;
|
|
222
223
|
try {
|
|
223
224
|
const filePath = getFileCachePath();
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
cache.set(key, value);
|
|
231
|
-
}
|
|
225
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
226
|
+
const data = (0, http_retry_1.safeJsonParse)(content, {});
|
|
227
|
+
for (const [key, value] of Object.entries(data)) {
|
|
228
|
+
// Validate structure
|
|
229
|
+
if (value && value.token && value.expiresAt && value.obtainedAt) {
|
|
230
|
+
cache.set(key, value);
|
|
232
231
|
}
|
|
233
232
|
}
|
|
234
233
|
}
|
|
235
234
|
catch {
|
|
236
|
-
// Ignore file read errors
|
|
235
|
+
// Ignore file read errors (file may not exist)
|
|
237
236
|
}
|
|
238
237
|
return cache;
|
|
239
238
|
}
|
|
240
239
|
/**
|
|
241
|
-
* Save tokens to file cache
|
|
240
|
+
* Save tokens to file cache (async, fire-and-forget)
|
|
242
241
|
*/
|
|
243
|
-
function saveFileCache() {
|
|
242
|
+
async function saveFileCache() {
|
|
244
243
|
if (!fileCacheEnabled)
|
|
245
244
|
return;
|
|
246
245
|
try {
|
|
@@ -249,7 +248,7 @@ function saveFileCache() {
|
|
|
249
248
|
for (const [key, value] of tokenCache.entries()) {
|
|
250
249
|
data[key] = value;
|
|
251
250
|
}
|
|
252
|
-
fs.
|
|
251
|
+
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
253
252
|
}
|
|
254
253
|
catch {
|
|
255
254
|
// Ignore file write errors
|
|
@@ -266,13 +265,13 @@ function saveFileCache() {
|
|
|
266
265
|
* // In your test setup
|
|
267
266
|
* import { enableFileCache } from 'linkedin-secret-sauce/enrichment';
|
|
268
267
|
*
|
|
269
|
-
* enableFileCache(); // Now tokens persist between test runs
|
|
268
|
+
* await enableFileCache(); // Now tokens persist between test runs
|
|
270
269
|
* ```
|
|
271
270
|
*/
|
|
272
|
-
function enableFileCache() {
|
|
271
|
+
async function enableFileCache() {
|
|
273
272
|
fileCacheEnabled = true;
|
|
274
273
|
// Load existing tokens from file into memory cache
|
|
275
|
-
const fileTokens = loadFileCache();
|
|
274
|
+
const fileTokens = await loadFileCache();
|
|
276
275
|
for (const [key, value] of fileTokens.entries()) {
|
|
277
276
|
// Only load if not already in memory and still valid
|
|
278
277
|
if (!tokenCache.has(key) && isTokenValid(value, DEFAULT_REFRESH_BUFFER_MS)) {
|
|
@@ -295,25 +294,26 @@ function isFileCacheEnabled() {
|
|
|
295
294
|
/**
|
|
296
295
|
* Clear the file cache
|
|
297
296
|
*/
|
|
298
|
-
function clearFileCache() {
|
|
297
|
+
async function clearFileCache() {
|
|
299
298
|
try {
|
|
300
299
|
const filePath = getFileCachePath();
|
|
301
|
-
|
|
302
|
-
fs.unlinkSync(filePath);
|
|
303
|
-
}
|
|
300
|
+
await fs.unlink(filePath);
|
|
304
301
|
}
|
|
305
302
|
catch {
|
|
306
|
-
// Ignore errors
|
|
303
|
+
// Ignore errors (file may not exist)
|
|
307
304
|
}
|
|
308
305
|
}
|
|
309
306
|
// We need to modify the token caching behavior to persist to file
|
|
310
307
|
// This is done by wrapping the cache set operation
|
|
311
|
-
// Override tokenCache.set to also persist to file
|
|
308
|
+
// Override tokenCache.set to also persist to file (fire-and-forget async)
|
|
312
309
|
const originalSet = tokenCache.set.bind(tokenCache);
|
|
313
310
|
tokenCache.set = function (key, value) {
|
|
314
311
|
const result = originalSet(key, value);
|
|
315
312
|
if (fileCacheEnabled) {
|
|
316
|
-
|
|
313
|
+
// Fire-and-forget: don't await, just let it run in background
|
|
314
|
+
saveFileCache().catch(() => {
|
|
315
|
+
// Ignore save errors silently
|
|
316
|
+
});
|
|
317
317
|
}
|
|
318
318
|
return result;
|
|
319
319
|
};
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
*
|
|
11
11
|
* const enricher = createEnrichmentClient({
|
|
12
12
|
* providers: {
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* ldd: { apiUrl: process.env.LDD_API_URL, apiToken: process.env.LDD_API_TOKEN },
|
|
14
|
+
* smartprospect: { email: process.env.SMARTLEAD_EMAIL, password: process.env.SMARTLEAD_PASSWORD },
|
|
15
|
+
* bouncer: { apiKey: process.env.BOUNCER_API_KEY },
|
|
15
16
|
* },
|
|
16
17
|
* options: {
|
|
17
18
|
* maxCostPerEmail: 0.05,
|
|
@@ -35,11 +36,12 @@ import type { EnrichmentClientConfig, EnrichmentClient } from "./types";
|
|
|
35
36
|
*/
|
|
36
37
|
export declare function createEnrichmentClient(config: EnrichmentClientConfig): EnrichmentClient;
|
|
37
38
|
export * from "./types";
|
|
39
|
+
export { PROVIDER_COSTS, DEFAULT_PROVIDER_ORDER } from "./types";
|
|
38
40
|
export { isPersonalEmail, isBusinessEmail, isPersonalDomain, PERSONAL_DOMAINS, } from "./utils/personal-domains";
|
|
39
41
|
export { isDisposableEmail, isDisposableDomain, DISPOSABLE_DOMAINS, } from "./utils/disposable-domains";
|
|
40
42
|
export { isValidEmailSyntax, isRoleAccount, asciiFold, cleanNamePart, hostnameFromUrl, extractLinkedInUsername, } from "./utils/validation";
|
|
41
|
-
export { verifyEmailMx } from "./verification/mx";
|
|
42
|
-
export { createConstructProvider, createLddProvider, createSmartProspectProvider, createHunterProvider,
|
|
43
|
+
export { verifyEmailMx, checkDomainCatchAll, verifyEmailsExist } from "./verification/mx";
|
|
44
|
+
export { createConstructProvider, createLddProvider, createSmartProspectProvider, createHunterProvider, createDropcontactProvider, createBouncerProvider, createSnovioProvider, verifyEmailWithBouncer, checkCatchAllDomain, verifyEmailsBatch, findEmailsWithSnovio, verifyEmailWithSnovio, clearSnovioTokenCache, } from "./providers";
|
|
43
45
|
export { extractNumericLinkedInId } from "./providers/ldd";
|
|
44
46
|
export { createSmartProspectClient, type SmartProspectClient, type SmartProspectLocationOptions, } from "./providers/smartprospect";
|
|
45
47
|
export { enrichBusinessEmail, enrichBatch, enrichAllEmails, enrichAllBatch } from "./orchestrator";
|
package/dist/enrichment/index.js
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
*
|
|
12
12
|
* const enricher = createEnrichmentClient({
|
|
13
13
|
* providers: {
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* ldd: { apiUrl: process.env.LDD_API_URL, apiToken: process.env.LDD_API_TOKEN },
|
|
15
|
+
* smartprospect: { email: process.env.SMARTLEAD_EMAIL, password: process.env.SMARTLEAD_PASSWORD },
|
|
16
|
+
* bouncer: { apiKey: process.env.BOUNCER_API_KEY },
|
|
16
17
|
* },
|
|
17
18
|
* options: {
|
|
18
19
|
* maxCostPerEmail: 0.05,
|
|
@@ -42,40 +43,39 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
42
43
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
43
44
|
};
|
|
44
45
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
-
exports.
|
|
46
|
-
exports.salesLeadToContact = exports.getEmailsForLinkedInContactsBatch = exports.getEmailsForLinkedInContact = exports.createLinkedInEnricher = exports.enrichLinkedInContactsBatch = void 0;
|
|
46
|
+
exports.matchContacts = exports.findBestMatch = exports.classifyMatchQuality = exports.calculateMatchConfidence = exports.clearFileCache = exports.isFileCacheEnabled = exports.disableFileCache = exports.enableFileCache = exports.getSmartLeadTokenCacheStats = exports.clearAllSmartLeadTokens = exports.clearSmartLeadToken = exports.getSmartLeadUser = exports.getSmartLeadToken = exports.enrichAllBatch = exports.enrichAllEmails = exports.enrichBatch = exports.enrichBusinessEmail = exports.createSmartProspectClient = exports.extractNumericLinkedInId = exports.clearSnovioTokenCache = exports.verifyEmailWithSnovio = exports.findEmailsWithSnovio = exports.verifyEmailsBatch = exports.checkCatchAllDomain = exports.verifyEmailWithBouncer = exports.createSnovioProvider = exports.createBouncerProvider = exports.createDropcontactProvider = exports.createHunterProvider = exports.createSmartProspectProvider = exports.createLddProvider = exports.createConstructProvider = exports.verifyEmailsExist = exports.checkDomainCatchAll = 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 = exports.DEFAULT_PROVIDER_ORDER = exports.PROVIDER_COSTS = void 0;
|
|
47
|
+
exports.salesLeadToContact = exports.getEmailsForLinkedInContactsBatch = exports.getEmailsForLinkedInContact = exports.createLinkedInEnricher = exports.enrichLinkedInContactsBatch = exports.enrichLinkedInContact = exports.parseLinkedInSearchResponse = exports.buildSmartProspectFiltersFromLinkedIn = void 0;
|
|
47
48
|
exports.createEnrichmentClient = createEnrichmentClient;
|
|
48
49
|
const orchestrator_1 = require("./orchestrator");
|
|
49
50
|
const construct_1 = require("./providers/construct");
|
|
50
51
|
const ldd_1 = require("./providers/ldd");
|
|
51
52
|
const smartprospect_1 = require("./providers/smartprospect");
|
|
52
53
|
const hunter_1 = require("./providers/hunter");
|
|
53
|
-
const apollo_1 = require("./providers/apollo");
|
|
54
54
|
const dropcontact_1 = require("./providers/dropcontact");
|
|
55
55
|
const bouncer_1 = require("./providers/bouncer");
|
|
56
56
|
const snovio_1 = require("./providers/snovio");
|
|
57
57
|
/**
|
|
58
|
-
* Default provider order
|
|
58
|
+
* Default provider order - 2-Phase Strategy
|
|
59
59
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
60
|
+
* PHASE 1 - Free lookups (real data):
|
|
61
|
+
* - ldd: LinkedIn Data Dump - real verified emails (FREE)
|
|
62
|
+
* - smartprospect: SmartLead API - real verified emails (FREE with subscription)
|
|
63
|
+
* - construct: Pattern guessing + MX check (FREE)
|
|
64
|
+
*
|
|
65
|
+
* PHASE 2 - Paid verification/finding (only if needed):
|
|
66
|
+
* - bouncer: SMTP verify constructed emails ($0.006/email)
|
|
67
|
+
* - snovio: Email finder for catch-all domains ($0.02/email)
|
|
68
|
+
* - hunter: Hunter.io fallback ($0.005/email)
|
|
69
|
+
*
|
|
70
|
+
* Note: dropcontact available but not in default order
|
|
69
71
|
*/
|
|
70
72
|
const DEFAULT_ORDER = [
|
|
71
|
-
"construct",
|
|
72
|
-
"bouncer",
|
|
73
73
|
"ldd",
|
|
74
74
|
"smartprospect",
|
|
75
|
+
"construct",
|
|
76
|
+
"bouncer",
|
|
75
77
|
"snovio",
|
|
76
78
|
"hunter",
|
|
77
|
-
"apollo",
|
|
78
|
-
"dropcontact",
|
|
79
79
|
];
|
|
80
80
|
/**
|
|
81
81
|
* Create an enrichment client with the given configuration
|
|
@@ -99,9 +99,6 @@ function createEnrichmentClient(config) {
|
|
|
99
99
|
if (providerConfigs.hunter) {
|
|
100
100
|
providerFuncs.set("hunter", (0, hunter_1.createHunterProvider)(providerConfigs.hunter));
|
|
101
101
|
}
|
|
102
|
-
if (providerConfigs.apollo) {
|
|
103
|
-
providerFuncs.set("apollo", (0, apollo_1.createApolloProvider)(providerConfigs.apollo));
|
|
104
|
-
}
|
|
105
102
|
if (providerConfigs.dropcontact) {
|
|
106
103
|
providerFuncs.set("dropcontact", (0, dropcontact_1.createDropcontactProvider)(providerConfigs.dropcontact));
|
|
107
104
|
}
|
|
@@ -241,8 +238,11 @@ function buildCacheKey(candidate) {
|
|
|
241
238
|
}
|
|
242
239
|
return parts.join("|");
|
|
243
240
|
}
|
|
244
|
-
// Re-export types
|
|
241
|
+
// Re-export types and constants
|
|
245
242
|
__exportStar(require("./types"), exports);
|
|
243
|
+
var types_1 = require("./types");
|
|
244
|
+
Object.defineProperty(exports, "PROVIDER_COSTS", { enumerable: true, get: function () { return types_1.PROVIDER_COSTS; } });
|
|
245
|
+
Object.defineProperty(exports, "DEFAULT_PROVIDER_ORDER", { enumerable: true, get: function () { return types_1.DEFAULT_PROVIDER_ORDER; } });
|
|
246
246
|
// Re-export utilities
|
|
247
247
|
var personal_domains_1 = require("./utils/personal-domains");
|
|
248
248
|
Object.defineProperty(exports, "isPersonalEmail", { enumerable: true, get: function () { return personal_domains_1.isPersonalEmail; } });
|
|
@@ -263,13 +263,14 @@ Object.defineProperty(exports, "extractLinkedInUsername", { enumerable: true, ge
|
|
|
263
263
|
// Re-export verification
|
|
264
264
|
var mx_1 = require("./verification/mx");
|
|
265
265
|
Object.defineProperty(exports, "verifyEmailMx", { enumerable: true, get: function () { return mx_1.verifyEmailMx; } });
|
|
266
|
+
Object.defineProperty(exports, "checkDomainCatchAll", { enumerable: true, get: function () { return mx_1.checkDomainCatchAll; } });
|
|
267
|
+
Object.defineProperty(exports, "verifyEmailsExist", { enumerable: true, get: function () { return mx_1.verifyEmailsExist; } });
|
|
266
268
|
// Re-export providers (for advanced usage)
|
|
267
269
|
var providers_1 = require("./providers");
|
|
268
270
|
Object.defineProperty(exports, "createConstructProvider", { enumerable: true, get: function () { return providers_1.createConstructProvider; } });
|
|
269
271
|
Object.defineProperty(exports, "createLddProvider", { enumerable: true, get: function () { return providers_1.createLddProvider; } });
|
|
270
272
|
Object.defineProperty(exports, "createSmartProspectProvider", { enumerable: true, get: function () { return providers_1.createSmartProspectProvider; } });
|
|
271
273
|
Object.defineProperty(exports, "createHunterProvider", { enumerable: true, get: function () { return providers_1.createHunterProvider; } });
|
|
272
|
-
Object.defineProperty(exports, "createApolloProvider", { enumerable: true, get: function () { return providers_1.createApolloProvider; } });
|
|
273
274
|
Object.defineProperty(exports, "createDropcontactProvider", { enumerable: true, get: function () { return providers_1.createDropcontactProvider; } });
|
|
274
275
|
Object.defineProperty(exports, "createBouncerProvider", { enumerable: true, get: function () { return providers_1.createBouncerProvider; } });
|
|
275
276
|
Object.defineProperty(exports, "createSnovioProvider", { enumerable: true, get: function () { return providers_1.createSnovioProvider; } });
|
|
@@ -258,7 +258,7 @@ export declare function createLinkedInEnricher(smartProspectConfig: SmartProspec
|
|
|
258
258
|
/**
|
|
259
259
|
* Email source - where the email was found
|
|
260
260
|
*/
|
|
261
|
-
export type EmailSource = 'ldd' | 'smartprospect' | 'linkedin' | 'pattern' | 'hunter' | '
|
|
261
|
+
export type EmailSource = 'ldd' | 'smartprospect' | 'linkedin' | 'pattern' | 'hunter' | 'bouncer' | 'snovio';
|
|
262
262
|
/**
|
|
263
263
|
* Email result from unified lookup
|
|
264
264
|
*/
|
|
@@ -319,10 +319,15 @@ export interface GetEmailsConfig {
|
|
|
319
319
|
hunter?: {
|
|
320
320
|
apiKey: string;
|
|
321
321
|
};
|
|
322
|
-
/**
|
|
323
|
-
|
|
322
|
+
/** Bouncer configuration (PAID - SMTP verification) */
|
|
323
|
+
bouncer?: {
|
|
324
324
|
apiKey: string;
|
|
325
325
|
};
|
|
326
|
+
/** Snov.io configuration (PAID - email finder) */
|
|
327
|
+
snovio?: {
|
|
328
|
+
userId: string;
|
|
329
|
+
apiSecret: string;
|
|
330
|
+
};
|
|
326
331
|
}
|
|
327
332
|
/**
|
|
328
333
|
* Options for unified email lookup
|
|
@@ -863,15 +863,16 @@ async function getEmailsForLinkedInContact(contactOrLead, config, options = {})
|
|
|
863
863
|
? Math.max(...result.emails.map(e => e.confidence))
|
|
864
864
|
: 0;
|
|
865
865
|
// ==========================================================================
|
|
866
|
-
// Phase 3: PAID providers as last resort (Hunter/
|
|
866
|
+
// Phase 3: PAID providers as last resort (Hunter/Bouncer/Snovio)
|
|
867
867
|
// ==========================================================================
|
|
868
868
|
if (!skipPaidProviders && finalBestConfidence < paidProviderThreshold) {
|
|
869
869
|
// Only use paid providers if we have low confidence or no results
|
|
870
|
-
// TODO: Implement Hunter
|
|
870
|
+
// TODO: Implement Hunter, Bouncer, Snovio providers when needed
|
|
871
871
|
// For now, just mark that we would have queried them
|
|
872
|
-
if (config.hunter?.apiKey || config.
|
|
872
|
+
if (config.hunter?.apiKey || config.bouncer?.apiKey || config.snovio?.userId) {
|
|
873
873
|
// result.providersQueried.push('hunter');
|
|
874
|
-
// result.providersQueried.push('
|
|
874
|
+
// result.providersQueried.push('bouncer');
|
|
875
|
+
// result.providersQueried.push('snovio');
|
|
875
876
|
// await queryPaidProviders(contact, config, addEmail, result);
|
|
876
877
|
}
|
|
877
878
|
}
|
|
@@ -882,7 +883,8 @@ async function getEmailsForLinkedInContact(contactOrLead, config, options = {})
|
|
|
882
883
|
linkedin: 2, // LinkedIn company lookup (for domain discovery, doesn't provide emails)
|
|
883
884
|
pattern: 3,
|
|
884
885
|
hunter: 4,
|
|
885
|
-
|
|
886
|
+
bouncer: 5,
|
|
887
|
+
snovio: 6,
|
|
886
888
|
};
|
|
887
889
|
result.emails.sort((a, b) => {
|
|
888
890
|
if (b.confidence !== a.confidence) {
|
|
@@ -17,18 +17,19 @@ const validation_1 = require("./utils/validation");
|
|
|
17
17
|
* Default provider costs in USD per lookup
|
|
18
18
|
*
|
|
19
19
|
* Costs based on 2025 pricing:
|
|
20
|
-
* -
|
|
21
|
-
* -
|
|
22
|
-
* -
|
|
23
|
-
* -
|
|
24
|
-
* -
|
|
20
|
+
* - ldd: FREE (subscription-based)
|
|
21
|
+
* - smartprospect: FREE (included in SmartLead subscription)
|
|
22
|
+
* - construct: FREE (pattern guessing + MX check)
|
|
23
|
+
* - bouncer: $0.006/email (SMTP verification, 99%+ accuracy)
|
|
24
|
+
* - snovio: $0.02/email (email finding + verification)
|
|
25
|
+
* - hunter: $0.005/email
|
|
26
|
+
* - dropcontact: $0.01/email (not in default order)
|
|
25
27
|
*/
|
|
26
28
|
const _PROVIDER_COSTS = {
|
|
27
29
|
construct: 0,
|
|
28
30
|
ldd: 0,
|
|
29
|
-
smartprospect: 0
|
|
31
|
+
smartprospect: 0,
|
|
30
32
|
hunter: 0.005,
|
|
31
|
-
apollo: 0,
|
|
32
33
|
dropcontact: 0.01,
|
|
33
34
|
bouncer: 0.006,
|
|
34
35
|
snovio: 0.02,
|
|
@@ -235,11 +236,25 @@ async function enrichBatch(candidates, options) {
|
|
|
235
236
|
const delayMs = options.delayMs ?? 200;
|
|
236
237
|
for (let i = 0; i < candidates.length; i += batchSize) {
|
|
237
238
|
const chunk = candidates.slice(i, i + batchSize);
|
|
238
|
-
// Process batch in parallel
|
|
239
|
-
const batchResults = await Promise.
|
|
240
|
-
// Combine results with candidates
|
|
239
|
+
// Process batch in parallel (use allSettled for resilience)
|
|
240
|
+
const batchResults = await Promise.allSettled(chunk.map((candidate) => enrichBusinessEmail(candidate, options)));
|
|
241
|
+
// Combine results with candidates (handle both fulfilled and rejected)
|
|
241
242
|
batchResults.forEach((result, idx) => {
|
|
242
|
-
|
|
243
|
+
const candidate = chunk[idx];
|
|
244
|
+
if (result.status === 'fulfilled') {
|
|
245
|
+
results.push({ candidate, ...result.value });
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
// On error, return empty result for this candidate
|
|
249
|
+
results.push({
|
|
250
|
+
candidate,
|
|
251
|
+
business_email: null,
|
|
252
|
+
business_email_source: null,
|
|
253
|
+
business_email_verified: false,
|
|
254
|
+
last_checked_at: new Date().toISOString(),
|
|
255
|
+
status: `error: ${result.reason instanceof Error ? result.reason.message : 'unknown'}`,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
243
258
|
});
|
|
244
259
|
// Delay between batches to avoid rate limiting
|
|
245
260
|
if (i + batchSize < candidates.length && delayMs > 0) {
|
|
@@ -432,9 +447,24 @@ async function enrichAllBatch(candidates, options) {
|
|
|
432
447
|
const delayMs = options.delayMs ?? 200;
|
|
433
448
|
for (let i = 0; i < candidates.length; i += batchSize) {
|
|
434
449
|
const chunk = candidates.slice(i, i + batchSize);
|
|
435
|
-
// Process batch in parallel
|
|
436
|
-
const batchResults = await Promise.
|
|
437
|
-
results
|
|
450
|
+
// Process batch in parallel (use allSettled for resilience)
|
|
451
|
+
const batchResults = await Promise.allSettled(chunk.map((candidate) => enrichAllEmails(candidate, options)));
|
|
452
|
+
// Handle both fulfilled and rejected results
|
|
453
|
+
batchResults.forEach((result, idx) => {
|
|
454
|
+
if (result.status === 'fulfilled') {
|
|
455
|
+
results.push(result.value);
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
// On error, return empty result for this candidate
|
|
459
|
+
results.push({
|
|
460
|
+
emails: [],
|
|
461
|
+
candidate: chunk[idx],
|
|
462
|
+
totalCost: 0,
|
|
463
|
+
providersQueried: [],
|
|
464
|
+
completedAt: new Date().toISOString(),
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
});
|
|
438
468
|
// Delay between batches to avoid rate limiting
|
|
439
469
|
if (i + batchSize < candidates.length && delayMs > 0) {
|
|
440
470
|
await new Promise((r) => setTimeout(r, delayMs));
|