linkedin-secret-sauce 0.5.1 → 0.7.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.
- package/dist/config.js +1 -1
- package/dist/cosiall-client.js +2 -1
- package/dist/enrichment/auth/index.d.ts +1 -1
- package/dist/enrichment/auth/index.js +6 -1
- package/dist/enrichment/auth/smartlead-auth.d.ts +32 -0
- package/dist/enrichment/auth/smartlead-auth.js +163 -0
- package/dist/enrichment/index.d.ts +5 -2
- package/dist/enrichment/index.js +46 -1
- package/dist/enrichment/matching.d.ts +241 -0
- package/dist/enrichment/matching.js +626 -0
- package/dist/enrichment/orchestrator.d.ts +13 -1
- package/dist/enrichment/orchestrator.js +222 -5
- package/dist/enrichment/providers/apollo.d.ts +2 -2
- package/dist/enrichment/providers/apollo.js +59 -14
- package/dist/enrichment/providers/construct.d.ts +2 -2
- package/dist/enrichment/providers/construct.js +16 -4
- package/dist/enrichment/providers/hunter.d.ts +2 -2
- package/dist/enrichment/providers/hunter.js +48 -22
- package/dist/enrichment/providers/ldd.d.ts +20 -2
- package/dist/enrichment/providers/ldd.js +122 -16
- package/dist/enrichment/providers/smartprospect.d.ts +64 -2
- package/dist/enrichment/providers/smartprospect.js +605 -38
- package/dist/enrichment/types.d.ts +167 -11
- package/dist/enrichment/types.js +50 -1
- package/dist/http-client.js +1 -1
- package/dist/linkedin-api.d.ts +1 -2
- package/dist/parsers/profile-parser.js +4 -2
- package/dist/utils/linkedin-config.js +1 -1
- package/dist/utils/search-encoder.js +8 -9
- package/package.json +22 -15
|
@@ -8,9 +8,14 @@
|
|
|
8
8
|
* Supports two authentication methods:
|
|
9
9
|
* 1. Direct token: Pass `apiToken` directly (for pre-authenticated scenarios)
|
|
10
10
|
* 2. Credentials: Pass `email` and `password` for automatic login with token caching
|
|
11
|
+
*
|
|
12
|
+
* Exports:
|
|
13
|
+
* - createSmartProspectProvider: For email enrichment (waterfall pattern)
|
|
14
|
+
* - createSmartProspectClient: Full client with search/fetch capabilities
|
|
11
15
|
*/
|
|
12
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
17
|
exports.createSmartProspectProvider = createSmartProspectProvider;
|
|
18
|
+
exports.createSmartProspectClient = createSmartProspectClient;
|
|
14
19
|
const smartlead_auth_1 = require("../auth/smartlead-auth");
|
|
15
20
|
const DEFAULT_API_URL = "https://prospect-api.smartlead.ai/api/search-email-leads";
|
|
16
21
|
/**
|
|
@@ -19,6 +24,12 @@ const DEFAULT_API_URL = "https://prospect-api.smartlead.ai/api/search-email-lead
|
|
|
19
24
|
async function delay(ms) {
|
|
20
25
|
return new Promise((r) => setTimeout(r, ms));
|
|
21
26
|
}
|
|
27
|
+
const DEFAULT_POLLING_CONFIG = {
|
|
28
|
+
initialDelay: 500,
|
|
29
|
+
pollInterval: 1000,
|
|
30
|
+
maxAttempts: 10,
|
|
31
|
+
maxWaitTime: 15000,
|
|
32
|
+
};
|
|
22
33
|
/**
|
|
23
34
|
* HTTP request with retry on 429 rate limit
|
|
24
35
|
*/
|
|
@@ -197,11 +208,85 @@ function createSmartProspectProvider(config) {
|
|
|
197
208
|
};
|
|
198
209
|
}
|
|
199
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Poll get-contacts until email lookup is complete (provider version)
|
|
213
|
+
*/
|
|
214
|
+
async function pollForContactResults(filterId, expectedCount) {
|
|
215
|
+
const { initialDelay, pollInterval, maxAttempts, maxWaitTime } = DEFAULT_POLLING_CONFIG;
|
|
216
|
+
const startTime = Date.now();
|
|
217
|
+
await delay(initialDelay);
|
|
218
|
+
const makeRequest = async (token) => {
|
|
219
|
+
return requestWithRetry(`${apiUrl}/get-contacts`, {
|
|
220
|
+
method: "POST",
|
|
221
|
+
headers: {
|
|
222
|
+
Authorization: `Bearer ${token}`,
|
|
223
|
+
"Content-Type": "application/json",
|
|
224
|
+
Accept: "application/json",
|
|
225
|
+
},
|
|
226
|
+
body: JSON.stringify({
|
|
227
|
+
filter_id: filterId,
|
|
228
|
+
limit: expectedCount,
|
|
229
|
+
offset: 0,
|
|
230
|
+
}),
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
234
|
+
if (Date.now() - startTime > maxWaitTime) {
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const token = await getAuthToken();
|
|
239
|
+
const result = await makeRequest(token);
|
|
240
|
+
if (result.success && result.data.metrics) {
|
|
241
|
+
const { completed, totalContacts } = result.data.metrics;
|
|
242
|
+
if (completed >= totalContacts || completed >= expectedCount) {
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
await delay(pollInterval);
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
if (err instanceof Error && err.message.includes("401") && hasCredentials) {
|
|
250
|
+
await handleAuthError();
|
|
251
|
+
}
|
|
252
|
+
await delay(pollInterval);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Return last result
|
|
256
|
+
try {
|
|
257
|
+
const token = await getAuthToken();
|
|
258
|
+
return await makeRequest(token);
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return {
|
|
262
|
+
success: false,
|
|
263
|
+
message: "Polling timed out",
|
|
264
|
+
data: {
|
|
265
|
+
list: [],
|
|
266
|
+
total_count: 0,
|
|
267
|
+
metrics: {
|
|
268
|
+
totalContacts: 0,
|
|
269
|
+
totalEmails: 0,
|
|
270
|
+
noEmailFound: 0,
|
|
271
|
+
invalidEmails: 0,
|
|
272
|
+
catchAllEmails: 0,
|
|
273
|
+
verifiedEmails: 0,
|
|
274
|
+
completed: 0,
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
200
280
|
/**
|
|
201
281
|
* Fetch/enrich emails for specific contact IDs (COSTS CREDITS)
|
|
282
|
+
*
|
|
283
|
+
* Uses async polling pattern:
|
|
284
|
+
* 1. Call fetch-contacts (returns immediately with status: "pending")
|
|
285
|
+
* 2. Poll get-contacts until completed
|
|
286
|
+
* 3. Return enriched contacts with emails
|
|
202
287
|
*/
|
|
203
288
|
async function fetchContacts(contactIds) {
|
|
204
|
-
const
|
|
289
|
+
const makeFetchRequest = async (token) => {
|
|
205
290
|
return requestWithRetry(`${apiUrl}/fetch-contacts`, {
|
|
206
291
|
method: "POST",
|
|
207
292
|
headers: {
|
|
@@ -214,7 +299,30 @@ function createSmartProspectProvider(config) {
|
|
|
214
299
|
};
|
|
215
300
|
try {
|
|
216
301
|
const token = await getAuthToken();
|
|
217
|
-
|
|
302
|
+
const fetchResult = await makeFetchRequest(token);
|
|
303
|
+
// If fetch-contacts returns pending status with filter_id, poll for results
|
|
304
|
+
const filterId = fetchResult.data?.filter_id;
|
|
305
|
+
const status = fetchResult.data?.status;
|
|
306
|
+
if (filterId && status === "pending") {
|
|
307
|
+
const pollResult = await pollForContactResults(filterId, contactIds.length);
|
|
308
|
+
if (pollResult.success && pollResult.data.list.length > 0) {
|
|
309
|
+
return {
|
|
310
|
+
success: true,
|
|
311
|
+
message: "Fetch completed",
|
|
312
|
+
data: {
|
|
313
|
+
list: pollResult.data.list,
|
|
314
|
+
total_count: pollResult.data.total_count,
|
|
315
|
+
metrics: pollResult.data.metrics,
|
|
316
|
+
leads_found: pollResult.data.metrics.totalContacts,
|
|
317
|
+
email_fetched: pollResult.data.metrics.totalEmails,
|
|
318
|
+
verification_status_list: [],
|
|
319
|
+
filter_id: filterId,
|
|
320
|
+
status: "completed",
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return fetchResult;
|
|
218
326
|
}
|
|
219
327
|
catch (err) {
|
|
220
328
|
// On 401, clear token and retry once with fresh token
|
|
@@ -224,7 +332,7 @@ function createSmartProspectProvider(config) {
|
|
|
224
332
|
await handleAuthError();
|
|
225
333
|
try {
|
|
226
334
|
const freshToken = await getAuthToken();
|
|
227
|
-
return await
|
|
335
|
+
return await makeFetchRequest(freshToken);
|
|
228
336
|
}
|
|
229
337
|
catch {
|
|
230
338
|
// Retry failed too
|
|
@@ -252,6 +360,22 @@ function createSmartProspectProvider(config) {
|
|
|
252
360
|
};
|
|
253
361
|
}
|
|
254
362
|
}
|
|
363
|
+
/**
|
|
364
|
+
* Calculate confidence score for a contact
|
|
365
|
+
*/
|
|
366
|
+
function calculateConfidence(contact) {
|
|
367
|
+
const isVerified = contact.verificationStatus === "valid";
|
|
368
|
+
const isCatchAll = contact.verificationStatus === "catch_all";
|
|
369
|
+
const deliverability = contact.emailDeliverability || 0;
|
|
370
|
+
let confidence = deliverability * 100;
|
|
371
|
+
if (isVerified) {
|
|
372
|
+
confidence = Math.max(confidence, 90);
|
|
373
|
+
}
|
|
374
|
+
else if (isCatchAll) {
|
|
375
|
+
confidence = Math.min(confidence, 75);
|
|
376
|
+
}
|
|
377
|
+
return confidence;
|
|
378
|
+
}
|
|
255
379
|
async function fetchEmail(candidate) {
|
|
256
380
|
const { firstName, lastName } = extractNames(candidate);
|
|
257
381
|
if (!firstName) {
|
|
@@ -270,64 +394,507 @@ function createSmartProspectProvider(config) {
|
|
|
270
394
|
candidate.companyDomain ||
|
|
271
395
|
candidate.company_domain ||
|
|
272
396
|
undefined;
|
|
273
|
-
// Build search filters
|
|
397
|
+
// Build search filters using correct API field names
|
|
274
398
|
const filters = {
|
|
275
399
|
name: [fullName],
|
|
276
|
-
limit:
|
|
400
|
+
limit: 10, // Get more results to return multiple emails
|
|
277
401
|
};
|
|
278
402
|
if (company) {
|
|
279
|
-
filters.
|
|
403
|
+
filters.companyName = [company];
|
|
280
404
|
}
|
|
281
405
|
if (domain) {
|
|
282
|
-
filters.
|
|
406
|
+
filters.companyDomain = [domain];
|
|
283
407
|
}
|
|
284
408
|
// Step 1: Search for contacts (FREE)
|
|
285
409
|
const searchResult = await searchContacts(filters);
|
|
286
410
|
if (!searchResult.success || searchResult.data.list.length === 0) {
|
|
287
411
|
return null;
|
|
288
412
|
}
|
|
289
|
-
//
|
|
413
|
+
// Score and sort all matches
|
|
290
414
|
const matches = searchResult.data.list;
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
300
|
-
if (!bestMatch) {
|
|
415
|
+
const scoredMatches = matches
|
|
416
|
+
.map((contact) => ({
|
|
417
|
+
contact,
|
|
418
|
+
score: calculateMatchScore(contact, fullName, company, title),
|
|
419
|
+
}))
|
|
420
|
+
.filter((m) => m.score > 0) // Only include matches with some relevance
|
|
421
|
+
.sort((a, b) => b.score - a.score);
|
|
422
|
+
if (scoredMatches.length === 0) {
|
|
301
423
|
return null;
|
|
302
424
|
}
|
|
303
|
-
// Step 2: Fetch
|
|
304
|
-
|
|
425
|
+
// Step 2: Fetch emails for ALL matching contacts (COSTS CREDITS)
|
|
426
|
+
// Note: This costs more credits but gives us all available emails
|
|
427
|
+
const contactIds = scoredMatches.map((m) => m.contact.id);
|
|
428
|
+
const fetchResult = await fetchContacts(contactIds);
|
|
305
429
|
if (!fetchResult.success || fetchResult.data.list.length === 0) {
|
|
306
430
|
return null;
|
|
307
431
|
}
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
|
|
432
|
+
// Build multi-result with all found emails
|
|
433
|
+
const emails = [];
|
|
434
|
+
const seenEmails = new Set();
|
|
435
|
+
for (const enrichedContact of fetchResult.data.list) {
|
|
436
|
+
const email = enrichedContact?.email;
|
|
437
|
+
if (!email)
|
|
438
|
+
continue;
|
|
439
|
+
const emailLower = email.toLowerCase();
|
|
440
|
+
if (seenEmails.has(emailLower))
|
|
441
|
+
continue;
|
|
442
|
+
seenEmails.add(emailLower);
|
|
443
|
+
const isVerified = enrichedContact.verificationStatus === "valid";
|
|
444
|
+
const isCatchAll = enrichedContact.verificationStatus === "catch_all";
|
|
445
|
+
emails.push({
|
|
446
|
+
email,
|
|
447
|
+
verified: isVerified || isCatchAll,
|
|
448
|
+
confidence: calculateConfidence(enrichedContact),
|
|
449
|
+
isCatchAll,
|
|
450
|
+
metadata: {
|
|
451
|
+
fullName: enrichedContact.fullName,
|
|
452
|
+
title: enrichedContact.title,
|
|
453
|
+
company: enrichedContact.company?.name,
|
|
454
|
+
linkedin: enrichedContact.linkedin,
|
|
455
|
+
verificationStatus: enrichedContact.verificationStatus,
|
|
456
|
+
matchScore: scoredMatches.find((m) => m.contact.id === enrichedContact.id)?.score,
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
if (emails.length === 0) {
|
|
311
461
|
return null;
|
|
312
462
|
}
|
|
313
|
-
//
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
463
|
+
// Sort by confidence (highest first)
|
|
464
|
+
emails.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0));
|
|
465
|
+
// Return multi-result format
|
|
466
|
+
return { emails };
|
|
467
|
+
}
|
|
468
|
+
// Mark provider name for orchestrator
|
|
469
|
+
fetchEmail.__name = "smartprospect";
|
|
470
|
+
return fetchEmail;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Create a SmartProspect client for direct API access
|
|
474
|
+
*
|
|
475
|
+
* This provides access to the full SmartProspect API for:
|
|
476
|
+
* - Searching contacts with comprehensive filters (FREE)
|
|
477
|
+
* - Fetching/enriching contact emails (COSTS CREDITS)
|
|
478
|
+
* - Combined search + fetch operations
|
|
479
|
+
*
|
|
480
|
+
* @example
|
|
481
|
+
* ```ts
|
|
482
|
+
* const client = createSmartProspectClient({
|
|
483
|
+
* email: 'user@example.com',
|
|
484
|
+
* password: 'password123'
|
|
485
|
+
* });
|
|
486
|
+
*
|
|
487
|
+
* // Search only (FREE)
|
|
488
|
+
* const results = await client.search({
|
|
489
|
+
* title: ['CEO', 'CTO'],
|
|
490
|
+
* company: ['Google', 'Microsoft'],
|
|
491
|
+
* level: ['C-Level'],
|
|
492
|
+
* companyHeadCount: ['1001-5000', '5001-10000'],
|
|
493
|
+
* limit: 25
|
|
494
|
+
* });
|
|
495
|
+
*
|
|
496
|
+
* // Fetch specific contacts (COSTS CREDITS)
|
|
497
|
+
* const enriched = await client.fetch(['contact-id-1', 'contact-id-2']);
|
|
498
|
+
* ```
|
|
499
|
+
*/
|
|
500
|
+
function createSmartProspectClient(config) {
|
|
501
|
+
const { apiToken, email, password, apiUrl = DEFAULT_API_URL, loginUrl, eagerInit = true, } = config;
|
|
502
|
+
// Check if we have valid auth config
|
|
503
|
+
const hasDirectToken = !!apiToken;
|
|
504
|
+
const hasCredentials = !!email && !!password;
|
|
505
|
+
if (!hasDirectToken && !hasCredentials) {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Get the current auth token
|
|
510
|
+
*/
|
|
511
|
+
async function getAuthToken() {
|
|
512
|
+
if (hasDirectToken) {
|
|
513
|
+
return apiToken;
|
|
320
514
|
}
|
|
321
|
-
|
|
322
|
-
|
|
515
|
+
return (0, smartlead_auth_1.getSmartLeadToken)({
|
|
516
|
+
credentials: { email: email, password: password },
|
|
517
|
+
loginUrl,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Handle 401 errors
|
|
522
|
+
*/
|
|
523
|
+
async function handleAuthError() {
|
|
524
|
+
if (hasCredentials) {
|
|
525
|
+
(0, smartlead_auth_1.clearSmartLeadToken)(email);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
// Eager initialization
|
|
529
|
+
if (eagerInit && hasCredentials) {
|
|
530
|
+
getAuthToken().catch(() => { });
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Build the request payload with proper defaults
|
|
534
|
+
* Field names match the actual SmartProspect API
|
|
535
|
+
*/
|
|
536
|
+
function buildSearchPayload(filters) {
|
|
537
|
+
const payload = {};
|
|
538
|
+
// Contact-based filters
|
|
539
|
+
if (filters.name?.length)
|
|
540
|
+
payload.name = filters.name;
|
|
541
|
+
if (filters.firstName?.length)
|
|
542
|
+
payload.firstName = filters.firstName;
|
|
543
|
+
if (filters.lastName?.length)
|
|
544
|
+
payload.lastName = filters.lastName;
|
|
545
|
+
// Title filters
|
|
546
|
+
if (filters.title?.length)
|
|
547
|
+
payload.title = filters.title;
|
|
548
|
+
payload.titleExactMatch = filters.titleExactMatch ?? false;
|
|
549
|
+
// Department & Level
|
|
550
|
+
if (filters.department?.length)
|
|
551
|
+
payload.department = filters.department;
|
|
552
|
+
if (filters.level?.length)
|
|
553
|
+
payload.level = filters.level;
|
|
554
|
+
// Company filters (note: API uses companyName and companyDomain)
|
|
555
|
+
if (filters.companyName?.length)
|
|
556
|
+
payload.companyName = filters.companyName;
|
|
557
|
+
if (filters.companyDomain?.length)
|
|
558
|
+
payload.companyDomain = filters.companyDomain;
|
|
559
|
+
// Industry filters (note: API uses companyIndustry and companySubIndustry)
|
|
560
|
+
if (filters.companyIndustry?.length)
|
|
561
|
+
payload.companyIndustry = filters.companyIndustry;
|
|
562
|
+
if (filters.companySubIndustry?.length)
|
|
563
|
+
payload.companySubIndustry = filters.companySubIndustry;
|
|
564
|
+
// Company size filters
|
|
565
|
+
if (filters.companyHeadCount?.length)
|
|
566
|
+
payload.companyHeadCount = filters.companyHeadCount;
|
|
567
|
+
if (filters.companyRevenue?.length)
|
|
568
|
+
payload.companyRevenue = filters.companyRevenue;
|
|
569
|
+
// Location filters
|
|
570
|
+
if (filters.country?.length)
|
|
571
|
+
payload.country = filters.country;
|
|
572
|
+
if (filters.state?.length)
|
|
573
|
+
payload.state = filters.state;
|
|
574
|
+
if (filters.city?.length)
|
|
575
|
+
payload.city = filters.city;
|
|
576
|
+
// Workflow options
|
|
577
|
+
payload.dontDisplayOwnedContact = filters.dontDisplayOwnedContact ?? true;
|
|
578
|
+
// Pagination
|
|
579
|
+
payload.limit = filters.limit ?? 25;
|
|
580
|
+
if (filters.scroll_id)
|
|
581
|
+
payload.scroll_id = filters.scroll_id;
|
|
582
|
+
return payload;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Search for contacts (FREE - no credits)
|
|
586
|
+
*/
|
|
587
|
+
async function search(filters) {
|
|
588
|
+
const payload = buildSearchPayload(filters);
|
|
589
|
+
const makeRequest = async (token) => {
|
|
590
|
+
return requestWithRetry(`${apiUrl}/search-contacts`, {
|
|
591
|
+
method: "POST",
|
|
592
|
+
headers: {
|
|
593
|
+
Authorization: `Bearer ${token}`,
|
|
594
|
+
"Content-Type": "application/json",
|
|
595
|
+
Accept: "application/json",
|
|
596
|
+
},
|
|
597
|
+
body: JSON.stringify(payload),
|
|
598
|
+
});
|
|
599
|
+
};
|
|
600
|
+
try {
|
|
601
|
+
const token = await getAuthToken();
|
|
602
|
+
return await makeRequest(token);
|
|
603
|
+
}
|
|
604
|
+
catch (err) {
|
|
605
|
+
if (err instanceof Error &&
|
|
606
|
+
err.message.includes("401") &&
|
|
607
|
+
hasCredentials) {
|
|
608
|
+
await handleAuthError();
|
|
609
|
+
try {
|
|
610
|
+
const freshToken = await getAuthToken();
|
|
611
|
+
return await makeRequest(freshToken);
|
|
612
|
+
}
|
|
613
|
+
catch {
|
|
614
|
+
// Retry failed
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return {
|
|
618
|
+
success: false,
|
|
619
|
+
message: err instanceof Error ? err.message : "Search failed",
|
|
620
|
+
data: { list: [], total_count: 0 },
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Poll get-contacts until email lookup is complete
|
|
626
|
+
*/
|
|
627
|
+
async function pollForResults(filterId, expectedCount, pollingConfig = DEFAULT_POLLING_CONFIG) {
|
|
628
|
+
const { initialDelay, pollInterval, maxAttempts, maxWaitTime } = pollingConfig;
|
|
629
|
+
const startTime = Date.now();
|
|
630
|
+
// Wait initial delay before first poll
|
|
631
|
+
await delay(initialDelay);
|
|
632
|
+
const makeRequest = async (token) => {
|
|
633
|
+
return requestWithRetry(`${apiUrl}/get-contacts`, {
|
|
634
|
+
method: "POST",
|
|
635
|
+
headers: {
|
|
636
|
+
Authorization: `Bearer ${token}`,
|
|
637
|
+
"Content-Type": "application/json",
|
|
638
|
+
Accept: "application/json",
|
|
639
|
+
},
|
|
640
|
+
body: JSON.stringify({
|
|
641
|
+
filter_id: filterId,
|
|
642
|
+
limit: expectedCount,
|
|
643
|
+
offset: 0,
|
|
644
|
+
}),
|
|
645
|
+
});
|
|
646
|
+
};
|
|
647
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
648
|
+
// Check if we've exceeded max wait time
|
|
649
|
+
if (Date.now() - startTime > maxWaitTime) {
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
const token = await getAuthToken();
|
|
654
|
+
const result = await makeRequest(token);
|
|
655
|
+
if (result.success && result.data.metrics) {
|
|
656
|
+
const { completed, totalContacts } = result.data.metrics;
|
|
657
|
+
// Check if all contacts have been processed
|
|
658
|
+
if (completed >= totalContacts || completed >= expectedCount) {
|
|
659
|
+
return result;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
// Wait before next poll
|
|
663
|
+
await delay(pollInterval);
|
|
664
|
+
}
|
|
665
|
+
catch (err) {
|
|
666
|
+
// On 401, clear token and retry
|
|
667
|
+
if (err instanceof Error &&
|
|
668
|
+
err.message.includes("401") &&
|
|
669
|
+
hasCredentials) {
|
|
670
|
+
await handleAuthError();
|
|
671
|
+
}
|
|
672
|
+
// Continue polling on errors
|
|
673
|
+
await delay(pollInterval);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
// Return last result even if not complete
|
|
677
|
+
try {
|
|
678
|
+
const token = await getAuthToken();
|
|
679
|
+
return await makeRequest(token);
|
|
680
|
+
}
|
|
681
|
+
catch {
|
|
682
|
+
return {
|
|
683
|
+
success: false,
|
|
684
|
+
message: "Polling timed out",
|
|
685
|
+
data: {
|
|
686
|
+
list: [],
|
|
687
|
+
total_count: 0,
|
|
688
|
+
metrics: {
|
|
689
|
+
totalContacts: 0,
|
|
690
|
+
totalEmails: 0,
|
|
691
|
+
noEmailFound: 0,
|
|
692
|
+
invalidEmails: 0,
|
|
693
|
+
catchAllEmails: 0,
|
|
694
|
+
verifiedEmails: 0,
|
|
695
|
+
completed: 0,
|
|
696
|
+
},
|
|
697
|
+
},
|
|
698
|
+
};
|
|
323
699
|
}
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Fetch/enrich emails for specific contact IDs (COSTS CREDITS)
|
|
703
|
+
*
|
|
704
|
+
* SmartProspect uses async email lookup:
|
|
705
|
+
* 1. Call fetch-contacts to initiate lookup (returns immediately with status: "pending")
|
|
706
|
+
* 2. Poll get-contacts until metrics.completed >= expected count
|
|
707
|
+
* 3. Return the final enriched contacts with emails
|
|
708
|
+
*/
|
|
709
|
+
async function fetch(contactIds) {
|
|
710
|
+
if (!contactIds.length) {
|
|
711
|
+
return {
|
|
712
|
+
success: true,
|
|
713
|
+
message: "No contacts to fetch",
|
|
714
|
+
data: {
|
|
715
|
+
list: [],
|
|
716
|
+
total_count: 0,
|
|
717
|
+
metrics: {
|
|
718
|
+
totalContacts: 0,
|
|
719
|
+
totalEmails: 0,
|
|
720
|
+
noEmailFound: 0,
|
|
721
|
+
invalidEmails: 0,
|
|
722
|
+
catchAllEmails: 0,
|
|
723
|
+
verifiedEmails: 0,
|
|
724
|
+
completed: 0,
|
|
725
|
+
},
|
|
726
|
+
leads_found: 0,
|
|
727
|
+
email_fetched: 0,
|
|
728
|
+
verification_status_list: [],
|
|
729
|
+
},
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
const makeFetchRequest = async (token) => {
|
|
733
|
+
return requestWithRetry(`${apiUrl}/fetch-contacts`, {
|
|
734
|
+
method: "POST",
|
|
735
|
+
headers: {
|
|
736
|
+
Authorization: `Bearer ${token}`,
|
|
737
|
+
"Content-Type": "application/json",
|
|
738
|
+
Accept: "application/json",
|
|
739
|
+
},
|
|
740
|
+
body: JSON.stringify({ contactIds }),
|
|
741
|
+
});
|
|
742
|
+
};
|
|
743
|
+
try {
|
|
744
|
+
const token = await getAuthToken();
|
|
745
|
+
const fetchResult = await makeFetchRequest(token);
|
|
746
|
+
// If fetch-contacts returns a filter_id with pending status, we need to poll
|
|
747
|
+
const filterId = fetchResult.data?.filter_id;
|
|
748
|
+
const status = fetchResult.data?.status;
|
|
749
|
+
if (filterId && status === "pending") {
|
|
750
|
+
// Poll get-contacts for final results
|
|
751
|
+
const pollResult = await pollForResults(filterId, contactIds.length);
|
|
752
|
+
if (pollResult.success && pollResult.data.list.length > 0) {
|
|
753
|
+
// Convert poll result to fetch response format
|
|
754
|
+
return {
|
|
755
|
+
success: true,
|
|
756
|
+
message: "Fetch completed",
|
|
757
|
+
data: {
|
|
758
|
+
list: pollResult.data.list,
|
|
759
|
+
total_count: pollResult.data.total_count,
|
|
760
|
+
metrics: pollResult.data.metrics,
|
|
761
|
+
leads_found: pollResult.data.metrics.totalContacts,
|
|
762
|
+
email_fetched: pollResult.data.metrics.totalEmails,
|
|
763
|
+
verification_status_list: [],
|
|
764
|
+
filter_id: filterId,
|
|
765
|
+
status: "completed",
|
|
766
|
+
},
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
// If no polling needed or polling failed, return original result
|
|
771
|
+
return fetchResult;
|
|
772
|
+
}
|
|
773
|
+
catch (err) {
|
|
774
|
+
if (err instanceof Error &&
|
|
775
|
+
err.message.includes("401") &&
|
|
776
|
+
hasCredentials) {
|
|
777
|
+
await handleAuthError();
|
|
778
|
+
try {
|
|
779
|
+
const freshToken = await getAuthToken();
|
|
780
|
+
return await makeFetchRequest(freshToken);
|
|
781
|
+
}
|
|
782
|
+
catch {
|
|
783
|
+
// Retry failed
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return {
|
|
787
|
+
success: false,
|
|
788
|
+
message: err instanceof Error ? err.message : "Fetch failed",
|
|
789
|
+
data: {
|
|
790
|
+
list: [],
|
|
791
|
+
total_count: 0,
|
|
792
|
+
metrics: {
|
|
793
|
+
totalContacts: 0,
|
|
794
|
+
totalEmails: 0,
|
|
795
|
+
noEmailFound: 0,
|
|
796
|
+
invalidEmails: 0,
|
|
797
|
+
catchAllEmails: 0,
|
|
798
|
+
verifiedEmails: 0,
|
|
799
|
+
completed: 0,
|
|
800
|
+
},
|
|
801
|
+
leads_found: 0,
|
|
802
|
+
email_fetched: 0,
|
|
803
|
+
verification_status_list: [],
|
|
804
|
+
},
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Search and immediately fetch all results
|
|
810
|
+
*/
|
|
811
|
+
async function searchAndFetch(filters) {
|
|
812
|
+
const searchResponse = await search(filters);
|
|
813
|
+
if (!searchResponse.success || searchResponse.data.list.length === 0) {
|
|
814
|
+
return {
|
|
815
|
+
searchResponse,
|
|
816
|
+
fetchResponse: null,
|
|
817
|
+
contacts: [],
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
const contactIds = searchResponse.data.list.map((c) => c.id);
|
|
821
|
+
const fetchResponse = await fetch(contactIds);
|
|
324
822
|
return {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
823
|
+
searchResponse,
|
|
824
|
+
fetchResponse,
|
|
825
|
+
contacts: fetchResponse.success ? fetchResponse.data.list : [],
|
|
328
826
|
};
|
|
329
827
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
828
|
+
/**
|
|
829
|
+
* Generic location lookup (countries, states, cities)
|
|
830
|
+
*/
|
|
831
|
+
async function getLocations(type, options = {}) {
|
|
832
|
+
const { search: searchQuery, limit = 20, offset = 0 } = options;
|
|
833
|
+
const params = new URLSearchParams();
|
|
834
|
+
params.set('limit', String(limit));
|
|
835
|
+
if (offset > 0)
|
|
836
|
+
params.set('offset', String(offset));
|
|
837
|
+
if (searchQuery)
|
|
838
|
+
params.set('search', searchQuery);
|
|
839
|
+
const makeRequest = async (token) => {
|
|
840
|
+
return requestWithRetry(`${apiUrl}/${type}?${params.toString()}`, {
|
|
841
|
+
method: "GET",
|
|
842
|
+
headers: {
|
|
843
|
+
Authorization: `Bearer ${token}`,
|
|
844
|
+
Accept: "application/json",
|
|
845
|
+
},
|
|
846
|
+
});
|
|
847
|
+
};
|
|
848
|
+
try {
|
|
849
|
+
const token = await getAuthToken();
|
|
850
|
+
return await makeRequest(token);
|
|
851
|
+
}
|
|
852
|
+
catch (err) {
|
|
853
|
+
if (err instanceof Error &&
|
|
854
|
+
err.message.includes("401") &&
|
|
855
|
+
hasCredentials) {
|
|
856
|
+
await handleAuthError();
|
|
857
|
+
try {
|
|
858
|
+
const freshToken = await getAuthToken();
|
|
859
|
+
return await makeRequest(freshToken);
|
|
860
|
+
}
|
|
861
|
+
catch {
|
|
862
|
+
// Retry failed
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return {
|
|
866
|
+
success: false,
|
|
867
|
+
message: err instanceof Error ? err.message : "Lookup failed",
|
|
868
|
+
data: [],
|
|
869
|
+
pagination: { limit, offset, page: 1, count: 0 },
|
|
870
|
+
search: searchQuery || null,
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Lookup countries (for typeahead)
|
|
876
|
+
*/
|
|
877
|
+
async function getCountries(options) {
|
|
878
|
+
return getLocations('countries', options);
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Lookup states (for typeahead)
|
|
882
|
+
*/
|
|
883
|
+
async function getStates(options) {
|
|
884
|
+
return getLocations('states', options);
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Lookup cities (for typeahead)
|
|
888
|
+
*/
|
|
889
|
+
async function getCities(options) {
|
|
890
|
+
return getLocations('cities', options);
|
|
891
|
+
}
|
|
892
|
+
return {
|
|
893
|
+
search,
|
|
894
|
+
fetch,
|
|
895
|
+
searchAndFetch,
|
|
896
|
+
getCountries,
|
|
897
|
+
getStates,
|
|
898
|
+
getCities,
|
|
899
|
+
};
|
|
333
900
|
}
|