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.
@@ -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>;
@@ -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
- if (fs.existsSync(filePath)) {
225
- const content = fs.readFileSync(filePath, 'utf8');
226
- const data = JSON.parse(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);
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.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
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
- if (fs.existsSync(filePath)) {
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
- saveFileCache();
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
- * hunter: { apiKey: process.env.HUNTER_API_KEY },
14
- * apollo: { apiKey: process.env.APOLLO_API_KEY },
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, createApolloProvider, createDropcontactProvider, createBouncerProvider, createSnovioProvider, verifyEmailWithBouncer, checkCatchAllDomain, verifyEmailsBatch, findEmailsWithSnovio, verifyEmailWithSnovio, clearSnovioTokenCache, } from "./providers";
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";
@@ -11,8 +11,9 @@
11
11
  *
12
12
  * const enricher = createEnrichmentClient({
13
13
  * providers: {
14
- * hunter: { apiKey: process.env.HUNTER_API_KEY },
15
- * apollo: { apiKey: process.env.APOLLO_API_KEY },
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.enrichLinkedInContact = exports.parseLinkedInSearchResponse = exports.buildSmartProspectFiltersFromLinkedIn = 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.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.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
- * Strategy:
61
- * 1. construct - FREE pattern guessing with MX check
62
- * 2. bouncer - SMTP verification of construct results ($0.006/email)
63
- * 3. ldd - FREE LinkedIn data dump lookup
64
- * 4. smartprospect - Paid SmartLead lookup ($0.01/email)
65
- * 5. snovio - Email finder for catch-all domains ($0.02/email)
66
- * 6. hunter - Hunter.io API ($0.005/email)
67
- * 7. apollo - FREE Apollo.io lookup
68
- * 8. dropcontact - Dropcontact API ($0.01/email)
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' | 'apollo';
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
- /** Apollo configuration (PAID - last resort) */
323
- apollo?: {
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/Apollo)
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 and Apollo providers when needed
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.apollo?.apiKey) {
872
+ if (config.hunter?.apiKey || config.bouncer?.apiKey || config.snovio?.userId) {
873
873
  // result.providersQueried.push('hunter');
874
- // result.providersQueried.push('apollo');
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
- apollo: 5,
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
- * - Bouncer: $0.006/email at scale (best accuracy 99%+)
21
- * - Snov.io: $0.02/email (email finding + verification)
22
- * - Hunter: $0.005/email
23
- * - SmartProspect: $0.01/email
24
- * - Dropcontact: $0.01/email
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.01,
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.all(chunk.map((candidate) => enrichBusinessEmail(candidate, options)));
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
- results.push({ candidate: chunk[idx], ...result });
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.all(chunk.map((candidate) => enrichAllEmails(candidate, options)));
437
- results.push(...batchResults);
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));