linkedin-secret-sauce 0.11.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,12 +3,52 @@
3
3
  * MX Record Resolution and Email Verification
4
4
  *
5
5
  * Provides MX record resolution with timeout support and confidence scoring.
6
+ * Includes SMTP-based catch-all detection using RCPT TO verification.
6
7
  */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
7
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.verifyEmailsExist = verifyEmailsExist;
43
+ exports.checkDomainCatchAll = checkDomainCatchAll;
8
44
  exports.verifyEmailMx = verifyEmailMx;
9
45
  const promises_1 = require("node:dns/promises");
46
+ const net = __importStar(require("node:net"));
10
47
  const disposable_domains_1 = require("../utils/disposable-domains");
11
48
  const validation_1 = require("../utils/validation");
49
+ // Cache for catch-all results (domain -> isCatchAll)
50
+ const catchAllCache = new Map();
51
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
12
52
  /**
13
53
  * Resolve MX records with timeout
14
54
  *
@@ -30,6 +70,273 @@ async function resolveMxWithTimeout(domain, timeoutMs) {
30
70
  clearTimeout(timeoutId);
31
71
  }
32
72
  }
73
+ /**
74
+ * Generate a random string for catch-all testing
75
+ */
76
+ function generateRandomLocalPart() {
77
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
78
+ let result = 'catchalltest_';
79
+ for (let i = 0; i < 16; i++) {
80
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
81
+ }
82
+ return result;
83
+ }
84
+ /**
85
+ * Detect if a domain is a catch-all by testing with a random email address
86
+ * Uses SMTP RCPT TO verification
87
+ *
88
+ * @param domain - Domain to test
89
+ * @param mxHost - MX server hostname
90
+ * @param timeoutMs - Timeout in milliseconds
91
+ * @returns true if catch-all, false if not, null if unable to determine
92
+ */
93
+ async function detectCatchAll(domain, mxHost, timeoutMs = 10000) {
94
+ // Check cache first
95
+ const cached = catchAllCache.get(domain);
96
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
97
+ return cached.isCatchAll;
98
+ }
99
+ return new Promise((resolve) => {
100
+ const randomEmail = `${generateRandomLocalPart()}@${domain}`;
101
+ const socket = new net.Socket();
102
+ let step = 0;
103
+ let responseBuffer = '';
104
+ const cleanup = () => {
105
+ socket.removeAllListeners();
106
+ socket.destroy();
107
+ };
108
+ const timeout = setTimeout(() => {
109
+ cleanup();
110
+ resolve(null); // Unable to determine
111
+ }, timeoutMs);
112
+ socket.on('error', () => {
113
+ clearTimeout(timeout);
114
+ cleanup();
115
+ resolve(null); // Unable to determine
116
+ });
117
+ socket.on('close', () => {
118
+ clearTimeout(timeout);
119
+ cleanup();
120
+ });
121
+ socket.on('data', (data) => {
122
+ responseBuffer += data.toString();
123
+ // Process complete lines
124
+ const lines = responseBuffer.split('\r\n');
125
+ responseBuffer = lines.pop() || ''; // Keep incomplete line in buffer
126
+ for (const line of lines) {
127
+ if (!line)
128
+ continue;
129
+ const code = parseInt(line.substring(0, 3), 10);
130
+ switch (step) {
131
+ case 0: // Waiting for greeting
132
+ if (code === 220) {
133
+ step = 1;
134
+ socket.write(`HELO verify.local\r\n`);
135
+ }
136
+ else {
137
+ cleanup();
138
+ resolve(null);
139
+ }
140
+ break;
141
+ case 1: // Waiting for HELO response
142
+ if (code === 250) {
143
+ step = 2;
144
+ socket.write(`MAIL FROM:<test@verify.local>\r\n`);
145
+ }
146
+ else {
147
+ cleanup();
148
+ resolve(null);
149
+ }
150
+ break;
151
+ case 2: // Waiting for MAIL FROM response
152
+ if (code === 250) {
153
+ step = 3;
154
+ socket.write(`RCPT TO:<${randomEmail}>\r\n`);
155
+ }
156
+ else {
157
+ cleanup();
158
+ resolve(null);
159
+ }
160
+ break;
161
+ case 3: // Waiting for RCPT TO response - this is the key!
162
+ clearTimeout(timeout);
163
+ socket.write('QUIT\r\n');
164
+ // 250 = accepted (catch-all)
165
+ // 550, 551, 552, 553 = rejected (not catch-all)
166
+ // Other codes = unable to determine
167
+ const isCatchAll = code === 250;
168
+ const isRejected = code >= 550 && code <= 559;
169
+ if (isCatchAll || isRejected) {
170
+ // Cache the result
171
+ catchAllCache.set(domain, {
172
+ isCatchAll,
173
+ timestamp: Date.now(),
174
+ });
175
+ cleanup();
176
+ resolve(isCatchAll);
177
+ }
178
+ else {
179
+ cleanup();
180
+ resolve(null);
181
+ }
182
+ break;
183
+ }
184
+ }
185
+ });
186
+ // Connect to MX server on port 25
187
+ socket.connect(25, mxHost);
188
+ });
189
+ }
190
+ /**
191
+ * Verify if a specific email address exists via SMTP RCPT TO
192
+ *
193
+ * @param email - Email address to verify
194
+ * @param mxHost - MX server hostname
195
+ * @param timeoutMs - Timeout in milliseconds
196
+ * @returns true if exists, false if not, null if unable to determine
197
+ */
198
+ async function verifyEmailExists(email, mxHost, timeoutMs = 10000) {
199
+ return new Promise((resolve) => {
200
+ const socket = new net.Socket();
201
+ let step = 0;
202
+ let responseBuffer = '';
203
+ const cleanup = () => {
204
+ socket.removeAllListeners();
205
+ socket.destroy();
206
+ };
207
+ const timeout = setTimeout(() => {
208
+ cleanup();
209
+ resolve(null);
210
+ }, timeoutMs);
211
+ socket.on('error', () => {
212
+ clearTimeout(timeout);
213
+ cleanup();
214
+ resolve(null);
215
+ });
216
+ socket.on('close', () => {
217
+ clearTimeout(timeout);
218
+ cleanup();
219
+ });
220
+ socket.on('data', (data) => {
221
+ responseBuffer += data.toString();
222
+ const lines = responseBuffer.split('\r\n');
223
+ responseBuffer = lines.pop() || '';
224
+ for (const line of lines) {
225
+ if (!line)
226
+ continue;
227
+ const code = parseInt(line.substring(0, 3), 10);
228
+ switch (step) {
229
+ case 0: // Waiting for greeting
230
+ if (code === 220) {
231
+ step = 1;
232
+ socket.write(`HELO verify.local\r\n`);
233
+ }
234
+ else {
235
+ cleanup();
236
+ resolve(null);
237
+ }
238
+ break;
239
+ case 1: // Waiting for HELO response
240
+ if (code === 250) {
241
+ step = 2;
242
+ socket.write(`MAIL FROM:<test@verify.local>\r\n`);
243
+ }
244
+ else {
245
+ cleanup();
246
+ resolve(null);
247
+ }
248
+ break;
249
+ case 2: // Waiting for MAIL FROM response
250
+ if (code === 250) {
251
+ step = 3;
252
+ socket.write(`RCPT TO:<${email}>\r\n`);
253
+ }
254
+ else {
255
+ cleanup();
256
+ resolve(null);
257
+ }
258
+ break;
259
+ case 3: // Waiting for RCPT TO response
260
+ clearTimeout(timeout);
261
+ socket.write('QUIT\r\n');
262
+ // 250 = accepted (email exists or catch-all)
263
+ // 550, 551, 552, 553 = rejected (email doesn't exist)
264
+ const exists = code === 250;
265
+ const rejected = code >= 550 && code <= 559;
266
+ if (exists || rejected) {
267
+ cleanup();
268
+ resolve(exists);
269
+ }
270
+ else {
271
+ cleanup();
272
+ resolve(null);
273
+ }
274
+ break;
275
+ }
276
+ }
277
+ });
278
+ socket.connect(25, mxHost);
279
+ });
280
+ }
281
+ /**
282
+ * Verify multiple email addresses with rate limiting
283
+ *
284
+ * @param emails - Array of email addresses to verify
285
+ * @param options - Options including delay between checks
286
+ * @returns Array of verification results
287
+ */
288
+ async function verifyEmailsExist(emails, options) {
289
+ const delayMs = options?.delayMs ?? 3000; // 3 second delay between checks
290
+ const timeoutMs = options?.timeoutMs ?? 10000;
291
+ const results = [];
292
+ // Group emails by domain to reuse MX lookups
293
+ const emailsByDomain = new Map();
294
+ for (const email of emails) {
295
+ const domain = email.split('@')[1]?.toLowerCase();
296
+ if (domain) {
297
+ const existing = emailsByDomain.get(domain) || [];
298
+ existing.push(email);
299
+ emailsByDomain.set(domain, existing);
300
+ }
301
+ else {
302
+ results.push({ email, exists: null, mxHost: null, error: 'Invalid email format' });
303
+ }
304
+ }
305
+ // Process each domain
306
+ for (const [domain, domainEmails] of emailsByDomain) {
307
+ // Get MX records for domain
308
+ const mx = await resolveMxWithTimeout(domain, 5000);
309
+ if (!mx || mx.length === 0) {
310
+ for (const email of domainEmails) {
311
+ results.push({ email, exists: null, mxHost: null, error: 'No MX records found' });
312
+ }
313
+ continue;
314
+ }
315
+ const sortedMx = mx.sort((a, b) => a.priority - b.priority);
316
+ const mxHost = sortedMx[0].exchange;
317
+ // Verify each email with delay
318
+ for (let i = 0; i < domainEmails.length; i++) {
319
+ const email = domainEmails[i];
320
+ // Add delay between checks (except for first one)
321
+ if (i > 0) {
322
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
323
+ }
324
+ try {
325
+ const exists = await verifyEmailExists(email, mxHost, timeoutMs);
326
+ results.push({ email, exists, mxHost });
327
+ }
328
+ catch (err) {
329
+ results.push({
330
+ email,
331
+ exists: null,
332
+ mxHost,
333
+ error: err instanceof Error ? err.message : 'Verification failed',
334
+ });
335
+ }
336
+ }
337
+ }
338
+ return results;
339
+ }
33
340
  /**
34
341
  * Calculate confidence score based on verification factors
35
342
  */
@@ -43,9 +350,14 @@ function calculateConfidence(params) {
43
350
  score += 20;
44
351
  else if (mxCount === 1)
45
352
  score += 10;
46
- // Non-catch-all bonus (we assume catch-all if MX exists but can't verify)
47
- if (!isCatchAll)
353
+ // Catch-all scoring:
354
+ // - Not catch-all (false): +30 bonus
355
+ // - Is catch-all (true): no bonus
356
+ // - Unknown (null): +15 (partial bonus)
357
+ if (isCatchAll === false)
48
358
  score += 30;
359
+ else if (isCatchAll === null)
360
+ score += 15;
49
361
  // Non-role account bonus
50
362
  if (!isRoleAccount)
51
363
  score += 20;
@@ -61,8 +373,50 @@ function calculateConfidence(params) {
61
373
  * @param options - Verification options
62
374
  * @returns Verification result with confidence score
63
375
  */
376
+ /**
377
+ * Check if a domain is a catch-all (accepts all email addresses)
378
+ *
379
+ * @param domain - Domain to check
380
+ * @param options - Options including timeout
381
+ * @returns Object with isCatchAll status and MX records
382
+ */
383
+ async function checkDomainCatchAll(domain, options) {
384
+ const timeoutMs = options?.timeoutMs ?? 10000;
385
+ try {
386
+ // Get MX records
387
+ const mx = await resolveMxWithTimeout(domain, 5000);
388
+ if (!mx || mx.length === 0) {
389
+ return {
390
+ isCatchAll: null,
391
+ mxRecords: [],
392
+ mxHost: null,
393
+ error: 'No MX records found',
394
+ };
395
+ }
396
+ // Sort by priority and get primary
397
+ const sortedMx = mx.sort((a, b) => a.priority - b.priority);
398
+ const primaryMxHost = sortedMx[0].exchange;
399
+ const mxRecords = sortedMx.map((m) => m.exchange);
400
+ // Detect catch-all
401
+ const isCatchAll = await detectCatchAll(domain, primaryMxHost, timeoutMs);
402
+ return {
403
+ isCatchAll,
404
+ mxRecords,
405
+ mxHost: primaryMxHost,
406
+ };
407
+ }
408
+ catch (err) {
409
+ return {
410
+ isCatchAll: null,
411
+ mxRecords: [],
412
+ mxHost: null,
413
+ error: err instanceof Error ? err.message : 'Unknown error',
414
+ };
415
+ }
416
+ }
64
417
  async function verifyEmailMx(email, options) {
65
418
  const timeoutMs = options?.timeoutMs ?? 5000;
419
+ const checkCatchAll = options?.checkCatchAll ?? false;
66
420
  // Step 1: Syntax validation
67
421
  if (!(0, validation_1.isValidEmailSyntax)(email)) {
68
422
  return {
@@ -113,8 +467,14 @@ async function verifyEmailMx(email, options) {
113
467
  mxRecords: [],
114
468
  };
115
469
  }
116
- // Conservative: assume catch-all if MX exists (full detection requires SMTP handshake)
117
- const catchAll = true;
470
+ // Sort MX records by priority (lowest first)
471
+ const sortedMx = mx.sort((a, b) => a.priority - b.priority);
472
+ const primaryMxHost = sortedMx[0].exchange;
473
+ // Detect catch-all if requested (uses SMTP RCPT TO verification)
474
+ let catchAll = null;
475
+ if (checkCatchAll) {
476
+ catchAll = await detectCatchAll(domain, primaryMxHost, timeoutMs);
477
+ }
118
478
  // Calculate confidence
119
479
  const confidence = calculateConfidence({
120
480
  mxValid: true,
@@ -127,17 +487,17 @@ async function verifyEmailMx(email, options) {
127
487
  // Determine reason
128
488
  let reason = 'valid';
129
489
  if (!valid) {
130
- if (catchAll)
490
+ if (catchAll === true)
131
491
  reason = 'catch_all';
132
492
  else if (roleAccount)
133
493
  reason = 'role_account';
134
494
  }
135
- const mxRecords = mx.map((m) => m.exchange);
495
+ const mxRecords = sortedMx.map((m) => m.exchange);
136
496
  return {
137
497
  valid,
138
498
  confidence,
139
499
  reason,
140
- isCatchAll: catchAll,
500
+ isCatchAll: catchAll ?? false, // Default to false if unknown
141
501
  isRoleAccount: roleAccount,
142
502
  isDisposable: false,
143
503
  mxRecords,
package/dist/index.d.ts CHANGED
@@ -1,18 +1,208 @@
1
+ /**
2
+ * LinkedIn Secret Sauce
3
+ *
4
+ * A complete LinkedIn Sales Navigator client + Email Enrichment library.
5
+ *
6
+ * ## Two Main Modules
7
+ *
8
+ * ### 1. LinkedIn API
9
+ * Server-side LinkedIn Sales Navigator client with automatic cookie management,
10
+ * retries, caching, and parsing.
11
+ *
12
+ * ```typescript
13
+ * import { initializeLinkedInClient, searchSalesLeads, getProfileByVanity } from 'linkedin-secret-sauce';
14
+ *
15
+ * initializeLinkedInClient({
16
+ * cosiallApiUrl: process.env.COSIALL_API_URL,
17
+ * cosiallApiKey: process.env.COSIALL_API_KEY,
18
+ * });
19
+ *
20
+ * const profile = await getProfileByVanity('john-doe');
21
+ * const leads = await searchSalesLeads('cto fintech', { count: 25 });
22
+ * ```
23
+ *
24
+ * ### 2. Email Enrichment
25
+ * Find business emails using multiple providers with waterfall/parallel strategies.
26
+ *
27
+ * ```typescript
28
+ * import { createEnrichmentClient } from 'linkedin-secret-sauce';
29
+ *
30
+ * const enrichment = createEnrichmentClient({
31
+ * providers: {
32
+ * hunter: { apiKey: process.env.HUNTER_API_KEY },
33
+ * bouncer: { apiKey: process.env.BOUNCER_API_KEY },
34
+ * },
35
+ * });
36
+ *
37
+ * const result = await enrichment.enrich({
38
+ * firstName: 'John',
39
+ * lastName: 'Doe',
40
+ * domain: 'acme.com',
41
+ * });
42
+ * console.log(result.business_email); // john.doe@acme.com
43
+ * ```
44
+ *
45
+ * @packageDocumentation
46
+ */
1
47
  export { initializeLinkedInClient, getConfig } from "./config";
2
48
  export type { LinkedInClientConfig } from "./config";
49
+ export * from "./linkedin-api";
50
+ export * from "./types";
51
+ export type { LinkedInTenure, LinkedInPosition, LinkedInSpotlightBadge, SalesLeadSearchResult, } from "./types";
3
52
  export { LinkedInClientError } from "./utils/errors";
4
53
  export * from "./cosiall-client";
5
54
  export * from "./cookie-pool";
6
55
  export { parseFullProfile } from "./parsers/profile-parser";
7
56
  export { parseSalesSearchResults } from "./parsers/search-parser";
8
- export * from "./linkedin-api";
9
- export * from "./types";
10
- export type { LinkedInTenure, LinkedInPosition, LinkedInSpotlightBadge, SalesLeadSearchResult, } from "./types";
11
57
  export * from "./utils/metrics";
12
58
  export { getRequestHistory, clearRequestHistory, } from "./utils/request-history";
13
59
  export type { RequestHistoryEntry } from "./utils/request-history";
14
60
  export * from "./constants";
61
+ /**
62
+ * Create an enrichment client for finding business emails
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * const enrichment = createEnrichmentClient({
67
+ * providers: {
68
+ * construct: {}, // FREE pattern matching
69
+ * hunter: { apiKey: 'xxx' },
70
+ * bouncer: { apiKey: 'xxx' },
71
+ * },
72
+ * onCost: (provider, cost) => console.log(`${provider}: $${cost}`),
73
+ * });
74
+ *
75
+ * const result = await enrichment.enrich({
76
+ * firstName: 'John',
77
+ * lastName: 'Doe',
78
+ * domain: 'acme.com',
79
+ * });
80
+ * ```
81
+ */
15
82
  export { createEnrichmentClient } from "./enrichment";
16
- export type { CanonicalEmail, EnrichmentCandidate, ProviderResult, ProviderFunc, ProviderName, EnrichmentClientConfig, EnrichmentClient, ProvidersConfig, EnrichmentOptions, BatchEnrichmentOptions, HunterConfig, ApolloConfig, SmartProspectConfig, LddConfig, DropcontactConfig, ConstructConfig, CacheAdapter, CostCallback, EnrichmentLogger, VerificationResult, SmartProspectContact, SmartProspectSearchFilters, LddProfileData, LddApiResponse, } from "./enrichment";
17
- export { isPersonalEmail, isBusinessEmail, isPersonalDomain, isDisposableEmail, isDisposableDomain, isValidEmailSyntax, isRoleAccount, verifyEmailMx, PERSONAL_DOMAINS, DISPOSABLE_DOMAINS, DEFAULT_PROVIDER_ORDER, PROVIDER_COSTS, getSmartLeadToken, getSmartLeadUser, clearSmartLeadToken, clearAllSmartLeadTokens, getSmartLeadTokenCacheStats, } from "./enrichment";
18
- export type { SmartLeadCredentials, SmartLeadAuthConfig, SmartLeadUser, SmartLeadLoginResponse, } from "./enrichment";
83
+ export type {
84
+ /** Result returned by enrichment operations */
85
+ CanonicalEmail,
86
+ /** Input candidate for email lookup */
87
+ EnrichmentCandidate,
88
+ /** Raw provider result */
89
+ ProviderResult,
90
+ /** Provider function signature */
91
+ ProviderFunc,
92
+ /** Supported provider names */
93
+ ProviderName,
94
+ /** Full client configuration */
95
+ EnrichmentClientConfig,
96
+ /** Enrichment client interface */
97
+ EnrichmentClient,
98
+ /** Provider configuration map */
99
+ ProvidersConfig,
100
+ /** Enrichment options */
101
+ EnrichmentOptions,
102
+ /** Batch processing options */
103
+ BatchEnrichmentOptions,
104
+ /** Hunter.io configuration */
105
+ HunterConfig,
106
+ /** SmartProspect/SmartLead configuration */
107
+ SmartProspectConfig,
108
+ /** LinkedIn Data Dump configuration */
109
+ LddConfig,
110
+ /** Dropcontact configuration */
111
+ DropcontactConfig,
112
+ /** Construct provider configuration */
113
+ ConstructConfig,
114
+ /** Cache adapter interface */
115
+ CacheAdapter,
116
+ /** Cost tracking callback */
117
+ CostCallback,
118
+ /** Logger interface */
119
+ EnrichmentLogger,
120
+ /** Email verification result */
121
+ VerificationResult,
122
+ /** SmartProspect contact */
123
+ SmartProspectContact,
124
+ /** SmartProspect search filters */
125
+ SmartProspectSearchFilters,
126
+ /** LDD profile data */
127
+ LddProfileData,
128
+ /** LDD API response */
129
+ LddApiResponse, } from "./enrichment";
130
+ export {
131
+ /**
132
+ * Check if email is from a personal domain (Gmail, Yahoo, etc.)
133
+ * @example isPersonalEmail('john@gmail.com') // true
134
+ */
135
+ isPersonalEmail,
136
+ /**
137
+ * Check if email is from a business domain
138
+ * @example isBusinessEmail('john@acme.com') // true
139
+ */
140
+ isBusinessEmail,
141
+ /**
142
+ * Check if domain is a personal email provider
143
+ * @example isPersonalDomain('gmail.com') // true
144
+ */
145
+ isPersonalDomain,
146
+ /**
147
+ * Check if email is from a disposable/temporary email service
148
+ * @example isDisposableEmail('test@mailinator.com') // true
149
+ */
150
+ isDisposableEmail,
151
+ /**
152
+ * Check if domain is a disposable email provider
153
+ * @example isDisposableDomain('guerrillamail.com') // true
154
+ */
155
+ isDisposableDomain,
156
+ /**
157
+ * Validate email syntax
158
+ * @example isValidEmailSyntax('john@example.com') // true
159
+ */
160
+ isValidEmailSyntax,
161
+ /**
162
+ * Check if email is a role account (info@, support@, etc.)
163
+ * @example isRoleAccount('info@company.com') // true
164
+ */
165
+ isRoleAccount,
166
+ /**
167
+ * Verify email via MX record lookup
168
+ */
169
+ verifyEmailMx,
170
+ /** Set of known personal email domains */
171
+ PERSONAL_DOMAINS,
172
+ /** Set of known disposable email domains */
173
+ DISPOSABLE_DOMAINS,
174
+ /** Default provider execution order */
175
+ DEFAULT_PROVIDER_ORDER,
176
+ /** Cost per lookup by provider (USD) */
177
+ PROVIDER_COSTS, } from "./enrichment";
178
+ export {
179
+ /**
180
+ * Get JWT token for SmartLead API (cached, auto-refreshes)
181
+ * @example const token = await getSmartLeadToken({ credentials: { email, password } });
182
+ */
183
+ getSmartLeadToken,
184
+ /**
185
+ * Get SmartLead user info from cached login
186
+ */
187
+ getSmartLeadUser,
188
+ /**
189
+ * Clear cached token for an email (e.g., on 401 error)
190
+ */
191
+ clearSmartLeadToken,
192
+ /**
193
+ * Clear all cached SmartLead tokens
194
+ */
195
+ clearAllSmartLeadTokens,
196
+ /**
197
+ * Get token cache statistics for debugging
198
+ */
199
+ getSmartLeadTokenCacheStats, } from "./enrichment";
200
+ export type {
201
+ /** SmartLead login credentials */
202
+ SmartLeadCredentials,
203
+ /** SmartLead authentication configuration */
204
+ SmartLeadAuthConfig,
205
+ /** SmartLead user info */
206
+ SmartLeadUser,
207
+ /** SmartLead login response */
208
+ SmartLeadLoginResponse, } from "./enrichment";