linkedin-secret-sauce 0.12.3 → 0.12.5
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 +6 -49
- package/dist/enrichment/index.d.ts +3 -5
- package/dist/enrichment/index.js +4 -20
- package/dist/enrichment/matching.d.ts +4 -8
- package/dist/enrichment/matching.js +165 -84
- package/dist/enrichment/orchestrator.d.ts +1 -1
- package/dist/enrichment/orchestrator.js +51 -44
- package/dist/enrichment/providers/bounceban.js +19 -10
- package/dist/enrichment/providers/construct.js +97 -89
- package/dist/enrichment/providers/cosiall.js +2 -2
- package/dist/enrichment/providers/hunter.js +2 -1
- package/dist/enrichment/providers/index.d.ts +0 -2
- package/dist/enrichment/providers/index.js +1 -8
- package/dist/enrichment/providers/ldd.js +4 -2
- package/dist/enrichment/providers/smartprospect.d.ts +5 -2
- package/dist/enrichment/providers/smartprospect.js +64 -27
- package/dist/enrichment/providers/snovio.d.ts +4 -4
- package/dist/enrichment/providers/snovio.js +21 -17
- package/dist/enrichment/providers/trykitt.d.ts +2 -2
- package/dist/enrichment/providers/trykitt.js +86 -21
- package/dist/enrichment/types.d.ts +8 -71
- package/dist/enrichment/types.js +3 -8
- package/dist/enrichment/utils/rate-limiter.js +0 -2
- package/dist/index.d.ts +2 -4
- package/dist/index.js +2 -2
- package/docs/ENRICHMENT.md +3 -45
- package/docs/INTEGRATION.md +1 -3
- package/docs/PLAYGROUND.md +4 -9
- package/docs/api/assets/hierarchy.js +1 -1
- package/docs/api/assets/navigation.js +1 -1
- package/docs/api/assets/search.js +1 -1
- package/docs/api/classes/LinkedInClientError.html +4 -4
- package/docs/api/functions/_testGetAccountCookies.html +2 -2
- package/docs/api/functions/_testGetAccountEntry.html +2 -2
- package/docs/api/functions/_testGetAllAccountIds.html +2 -2
- package/docs/api/functions/_testGetPoolState.html +2 -2
- package/docs/api/functions/adminResetAccount.html +1 -1
- package/docs/api/functions/adminSetCooldown.html +1 -1
- package/docs/api/functions/buildCookieHeader.html +1 -1
- package/docs/api/functions/clearAllSmartLeadTokens.html +2 -2
- package/docs/api/functions/clearRequestHistory.html +1 -1
- package/docs/api/functions/clearSessionAccount.html +1 -1
- package/docs/api/functions/clearSmartLeadToken.html +2 -2
- package/docs/api/functions/createEnrichmentClient.html +3 -3
- package/docs/api/functions/extractCsrfToken.html +1 -1
- package/docs/api/functions/extractLinkedInHandle.html +2 -2
- package/docs/api/functions/fetchCookiesFromCosiall.html +2 -2
- package/docs/api/functions/fetchProfileEmailsFromCosiall.html +2 -2
- package/docs/api/functions/forceRefreshCookies.html +1 -1
- package/docs/api/functions/getAccountForSession.html +1 -1
- package/docs/api/functions/getAccountsSummary.html +1 -1
- package/docs/api/functions/getCompaniesBatch.html +2 -2
- package/docs/api/functions/getCompanyById.html +2 -2
- package/docs/api/functions/getCompanyByUrl.html +1 -1
- package/docs/api/functions/getConfig.html +1 -1
- package/docs/api/functions/getCookiePoolHealth.html +1 -1
- package/docs/api/functions/getProfileByUrn.html +2 -2
- package/docs/api/functions/getProfileByVanity.html +2 -2
- package/docs/api/functions/getProfilesBatch.html +1 -1
- package/docs/api/functions/getRequestHistory.html +1 -1
- package/docs/api/functions/getSalesNavigatorProfileDetails.html +1 -1
- package/docs/api/functions/getSalesNavigatorProfileFull.html +2 -2
- package/docs/api/functions/getSmartLeadToken.html +1 -1
- package/docs/api/functions/getSmartLeadTokenCacheStats.html +2 -2
- package/docs/api/functions/getSmartLeadUser.html +2 -2
- package/docs/api/functions/getSnapshot.html +1 -1
- package/docs/api/functions/getYearsAtCompanyOptions.html +2 -2
- package/docs/api/functions/getYearsInPositionOptions.html +2 -2
- package/docs/api/functions/getYearsOfExperienceOptions.html +2 -2
- package/docs/api/functions/incrementMetric.html +1 -1
- package/docs/api/functions/initializeCookiePool.html +1 -1
- package/docs/api/functions/initializeLinkedInClient.html +1 -1
- package/docs/api/functions/isBusinessEmail.html +2 -2
- package/docs/api/functions/isDisposableDomain.html +2 -2
- package/docs/api/functions/isDisposableEmail.html +2 -2
- package/docs/api/functions/isPersonalDomain.html +2 -2
- package/docs/api/functions/isPersonalEmail.html +2 -2
- package/docs/api/functions/isRoleAccount.html +2 -2
- package/docs/api/functions/isValidEmailSyntax.html +2 -2
- package/docs/api/functions/parseFullProfile.html +2 -2
- package/docs/api/functions/parseSalesSearchResults.html +1 -1
- package/docs/api/functions/reportAccountFailure.html +1 -1
- package/docs/api/functions/reportAccountSuccess.html +1 -1
- package/docs/api/functions/resolveCompanyUniversalName.html +1 -1
- package/docs/api/functions/searchSalesLeads.html +2 -2
- package/docs/api/functions/selectAccountForRequest.html +1 -1
- package/docs/api/functions/setAccountForSession.html +1 -1
- package/docs/api/functions/typeahead.html +1 -1
- package/docs/api/functions/verifyEmailMx.html +1 -1
- package/docs/api/hierarchy.html +1 -1
- package/docs/api/index.html +3 -3
- package/docs/api/interfaces/AccountCookies.html +2 -2
- package/docs/api/interfaces/BatchEnrichmentOptions.html +8 -8
- package/docs/api/interfaces/CacheAdapter.html +4 -4
- package/docs/api/interfaces/CanonicalEmail.html +8 -8
- package/docs/api/interfaces/Company.html +2 -2
- package/docs/api/interfaces/ConstructConfig.html +5 -5
- package/docs/api/interfaces/CosiallProfileEmailsResponse.html +6 -6
- package/docs/api/interfaces/EnrichmentCandidate.html +4 -4
- package/docs/api/interfaces/EnrichmentClient.html +6 -6
- package/docs/api/interfaces/EnrichmentClientConfig.html +7 -7
- package/docs/api/interfaces/EnrichmentLogger.html +3 -3
- package/docs/api/interfaces/EnrichmentOptions.html +6 -6
- package/docs/api/interfaces/HunterConfig.html +3 -3
- package/docs/api/interfaces/LddConfig.html +3 -3
- package/docs/api/interfaces/LddProfileData.html +2 -2
- package/docs/api/interfaces/LinkedInClientConfig.html +2 -2
- package/docs/api/interfaces/LinkedInCookie.html +2 -2
- package/docs/api/interfaces/LinkedInPosition.html +2 -2
- package/docs/api/interfaces/LinkedInProfile.html +2 -2
- package/docs/api/interfaces/LinkedInSpotlightBadge.html +2 -2
- package/docs/api/interfaces/LinkedInTenure.html +2 -2
- package/docs/api/interfaces/Metrics.html +2 -2
- package/docs/api/interfaces/MetricsSnapshot.html +2 -2
- package/docs/api/interfaces/ProfileEducation.html +2 -2
- package/docs/api/interfaces/ProfileEmailsLookupOptions.html +5 -5
- package/docs/api/interfaces/ProfilePosition.html +2 -2
- package/docs/api/interfaces/ProfileSkill.html +2 -2
- package/docs/api/interfaces/ProviderResult.html +6 -6
- package/docs/api/interfaces/ProvidersConfig.html +6 -9
- package/docs/api/interfaces/RequestHistoryEntry.html +2 -2
- package/docs/api/interfaces/SalesLeadSearchResult.html +2 -2
- package/docs/api/interfaces/SalesNavigatorContactInfo.html +2 -2
- package/docs/api/interfaces/SalesNavigatorPosition.html +2 -2
- package/docs/api/interfaces/SalesNavigatorProfile.html +2 -2
- package/docs/api/interfaces/SalesNavigatorProfileFull.html +4 -4
- package/docs/api/interfaces/SearchSalesResult.html +2 -2
- package/docs/api/interfaces/SmartLeadAuthConfig.html +4 -4
- package/docs/api/interfaces/SmartLeadCredentials.html +2 -2
- package/docs/api/interfaces/SmartLeadLoginResponse.html +2 -2
- package/docs/api/interfaces/SmartLeadUser.html +2 -2
- package/docs/api/interfaces/SmartProspectConfig.html +8 -8
- package/docs/api/interfaces/SmartProspectContact.html +2 -2
- package/docs/api/interfaces/SmartProspectSearchFilters.html +21 -21
- package/docs/api/interfaces/TypeaheadItem.html +2 -2
- package/docs/api/interfaces/TypeaheadResult.html +2 -2
- package/docs/api/interfaces/VerificationResult.html +9 -9
- package/docs/api/types/CostCallback.html +2 -2
- package/docs/api/types/Geo.html +2 -2
- package/docs/api/types/LddApiResponse.html +1 -1
- package/docs/api/types/ProviderFunc.html +2 -2
- package/docs/api/types/ProviderName.html +2 -2
- package/docs/api/types/SalesSearchFilters.html +2 -2
- package/docs/api/types/TypeaheadType.html +1 -1
- package/docs/api/variables/COMPANY_SIZE_OPTIONS.html +1 -1
- package/docs/api/variables/DEFAULT_PROVIDER_ORDER.html +3 -4
- package/docs/api/variables/DISPOSABLE_DOMAINS.html +2 -2
- package/docs/api/variables/FUNCTION_OPTIONS.html +1 -1
- package/docs/api/variables/INDUSTRY_OPTIONS.html +1 -1
- package/docs/api/variables/LANGUAGE_OPTIONS.html +1 -1
- package/docs/api/variables/PERSONAL_DOMAINS.html +2 -2
- package/docs/api/variables/PROVIDER_COSTS.html +3 -5
- package/docs/api/variables/REGION_OPTIONS.html +1 -1
- package/docs/api/variables/SENIORITY_OPTIONS.html +2 -2
- package/docs/api/variables/YEARS_OPTIONS.html +1 -1
- package/package.json +1 -1
- package/dist/enrichment/providers/apollo.d.ts +0 -11
- package/dist/enrichment/providers/apollo.js +0 -181
- package/dist/enrichment/providers/bouncer.d.ts +0 -67
- package/dist/enrichment/providers/bouncer.js +0 -231
- package/dist/enrichment/providers/dropcontact.d.ts +0 -22
- package/dist/enrichment/providers/dropcontact.js +0 -206
- package/docs/api/interfaces/DropcontactConfig.html +0 -3
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Email Enrichment Providers
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.clearSnovioTokenCache = exports.verifyEmailWithSnovio = exports.findEmailsWithSnovio = exports.createSnovioProvider = exports.checkCatchAllWithBounceBan = exports.verifyEmailsBatchWithBounceBan = exports.verifyEmailWithBounceBan = exports.createBounceBanProvider = exports.
|
|
6
|
+
exports.clearSnovioTokenCache = exports.verifyEmailWithSnovio = exports.findEmailsWithSnovio = exports.createSnovioProvider = exports.checkCatchAllWithBounceBan = exports.verifyEmailsBatchWithBounceBan = exports.verifyEmailWithBounceBan = exports.createBounceBanProvider = exports.createHunterProvider = exports.verifyEmailWithTryKitt = exports.findEmailWithTryKitt = exports.createTryKittProvider = exports.createCosiallProvider = exports.createSmartProspectProvider = exports.createLddProvider = exports.createConstructProvider = void 0;
|
|
7
7
|
var construct_1 = require("./construct");
|
|
8
8
|
Object.defineProperty(exports, "createConstructProvider", { enumerable: true, get: function () { return construct_1.createConstructProvider; } });
|
|
9
9
|
var ldd_1 = require("./ldd");
|
|
@@ -18,13 +18,6 @@ Object.defineProperty(exports, "findEmailWithTryKitt", { enumerable: true, get:
|
|
|
18
18
|
Object.defineProperty(exports, "verifyEmailWithTryKitt", { enumerable: true, get: function () { return trykitt_1.verifyEmailWithTryKitt; } });
|
|
19
19
|
var hunter_1 = require("./hunter");
|
|
20
20
|
Object.defineProperty(exports, "createHunterProvider", { enumerable: true, get: function () { return hunter_1.createHunterProvider; } });
|
|
21
|
-
var dropcontact_1 = require("./dropcontact");
|
|
22
|
-
Object.defineProperty(exports, "createDropcontactProvider", { enumerable: true, get: function () { return dropcontact_1.createDropcontactProvider; } });
|
|
23
|
-
var bouncer_1 = require("./bouncer");
|
|
24
|
-
Object.defineProperty(exports, "createBouncerProvider", { enumerable: true, get: function () { return bouncer_1.createBouncerProvider; } });
|
|
25
|
-
Object.defineProperty(exports, "verifyEmailWithBouncer", { enumerable: true, get: function () { return bouncer_1.verifyEmailWithBouncer; } });
|
|
26
|
-
Object.defineProperty(exports, "checkCatchAllDomain", { enumerable: true, get: function () { return bouncer_1.checkCatchAllDomain; } });
|
|
27
|
-
Object.defineProperty(exports, "verifyEmailsBatch", { enumerable: true, get: function () { return bouncer_1.verifyEmailsBatch; } });
|
|
28
21
|
var bounceban_1 = require("./bounceban");
|
|
29
22
|
Object.defineProperty(exports, "createBounceBanProvider", { enumerable: true, get: function () { return bounceban_1.createBounceBanProvider; } });
|
|
30
23
|
Object.defineProperty(exports, "verifyEmailWithBounceBan", { enumerable: true, get: function () { return bounceban_1.verifyEmailWithBounceBan; } });
|
|
@@ -52,7 +52,8 @@ function createLddProvider(config) {
|
|
|
52
52
|
const response = await (0, http_retry_1.getWithRetry)(endpoint, { Authorization: `Bearer ${apiToken}` }, { retries: 1, backoffMs: 100 });
|
|
53
53
|
return parseResponse(response);
|
|
54
54
|
}
|
|
55
|
-
catch {
|
|
55
|
+
catch (err) {
|
|
56
|
+
console.error("[LDD] Lookup by numeric ID failed:", err instanceof Error ? err.message : err);
|
|
56
57
|
return null;
|
|
57
58
|
}
|
|
58
59
|
}
|
|
@@ -62,7 +63,8 @@ function createLddProvider(config) {
|
|
|
62
63
|
const response = await (0, http_retry_1.getWithRetry)(endpoint, { Authorization: `Bearer ${apiToken}` }, { retries: 1, backoffMs: 100 });
|
|
63
64
|
return parseResponse(response);
|
|
64
65
|
}
|
|
65
|
-
catch {
|
|
66
|
+
catch (err) {
|
|
67
|
+
console.error("[LDD] Lookup by username failed:", err instanceof Error ? err.message : err);
|
|
66
68
|
return null;
|
|
67
69
|
}
|
|
68
70
|
}
|
|
@@ -27,8 +27,11 @@ export interface SmartProspectLocationOptions {
|
|
|
27
27
|
export interface SmartProspectClient {
|
|
28
28
|
/** Search for contacts (FREE - no credits used) */
|
|
29
29
|
search: (filters: SmartProspectSearchFilters) => Promise<SmartProspectSearchResponse>;
|
|
30
|
-
/** Fetch/enrich emails for specific contact IDs (COSTS CREDITS)
|
|
31
|
-
|
|
30
|
+
/** Fetch/enrich emails for specific contact IDs (COSTS CREDITS)
|
|
31
|
+
* @param contactIds - Array of contact IDs to fetch
|
|
32
|
+
* @param searchFilterId - Filter ID from the search response (required by API)
|
|
33
|
+
*/
|
|
34
|
+
fetch: (contactIds: string[], searchFilterId?: number) => Promise<SmartProspectFetchResponse>;
|
|
32
35
|
/** Search and immediately fetch all results (COSTS CREDITS for fetched contacts) */
|
|
33
36
|
searchAndFetch: (filters: SmartProspectSearchFilters) => Promise<{
|
|
34
37
|
searchResponse: SmartProspectSearchResponse;
|
|
@@ -22,10 +22,10 @@ const candidate_parser_1 = require("../utils/candidate-parser");
|
|
|
22
22
|
const noop_provider_1 = require("../utils/noop-provider");
|
|
23
23
|
const DEFAULT_API_URL = "https://prospect-api.smartlead.ai/api/search-email-leads";
|
|
24
24
|
const DEFAULT_POLLING_CONFIG = {
|
|
25
|
-
initialDelay:
|
|
26
|
-
pollInterval:
|
|
27
|
-
maxAttempts:
|
|
28
|
-
maxWaitTime:
|
|
25
|
+
initialDelay: 1000,
|
|
26
|
+
pollInterval: 2000,
|
|
27
|
+
maxAttempts: 45,
|
|
28
|
+
maxWaitTime: 90000,
|
|
29
29
|
};
|
|
30
30
|
/**
|
|
31
31
|
* HTTP request with retry on 429 rate limit
|
|
@@ -37,12 +37,15 @@ async function requestWithRetry(url, options, retries = 2, backoffMs = 300) {
|
|
|
37
37
|
const res = await fetch(url, options);
|
|
38
38
|
// Retry on rate limit
|
|
39
39
|
if (res.status === 429 && i < retries) {
|
|
40
|
+
console.error("[SmartProspect] Rate limited, retrying...");
|
|
40
41
|
await (0, http_retry_1.delay)(backoffMs * Math.pow(2, i));
|
|
41
42
|
continue;
|
|
42
43
|
}
|
|
43
44
|
if (!res.ok) {
|
|
44
45
|
const errorText = await res.text().catch(() => "");
|
|
45
|
-
|
|
46
|
+
const error = new Error(`SmartProspect API error: ${res.status} - ${errorText}`);
|
|
47
|
+
console.error("[SmartProspect] API error:", error.message);
|
|
48
|
+
throw error;
|
|
46
49
|
}
|
|
47
50
|
return (await res.json());
|
|
48
51
|
}
|
|
@@ -54,6 +57,7 @@ async function requestWithRetry(url, options, retries = 2, backoffMs = 300) {
|
|
|
54
57
|
}
|
|
55
58
|
}
|
|
56
59
|
}
|
|
60
|
+
console.error("[SmartProspect] Request failed after retries:", lastErr instanceof Error ? lastErr.message : lastErr);
|
|
57
61
|
throw lastErr ?? new Error("smartprospect_http_error");
|
|
58
62
|
}
|
|
59
63
|
/**
|
|
@@ -155,7 +159,7 @@ function createSmartProspectProvider(config) {
|
|
|
155
159
|
},
|
|
156
160
|
body: JSON.stringify({
|
|
157
161
|
...filters,
|
|
158
|
-
dontDisplayOwnedContact: filters.dontDisplayOwnedContact ??
|
|
162
|
+
dontDisplayOwnedContact: filters.dontDisplayOwnedContact ?? false,
|
|
159
163
|
limit: filters.limit ?? 10,
|
|
160
164
|
titleExactMatch: filters.titleExactMatch ?? false,
|
|
161
165
|
}),
|
|
@@ -264,9 +268,20 @@ function createSmartProspectProvider(config) {
|
|
|
264
268
|
* 1. Call fetch-contacts (returns immediately with status: "pending")
|
|
265
269
|
* 2. Poll get-contacts until completed
|
|
266
270
|
* 3. Return enriched contacts with emails
|
|
271
|
+
*
|
|
272
|
+
* @param contactIds - Array of contact IDs to fetch
|
|
273
|
+
* @param searchFilterId - Filter ID from the search response (required by API)
|
|
267
274
|
*/
|
|
268
|
-
async function fetchContacts(contactIds) {
|
|
275
|
+
async function fetchContacts(contactIds, searchFilterId) {
|
|
269
276
|
const makeFetchRequest = async (token) => {
|
|
277
|
+
// API expects: { filter_id, limit, visual_limit, id: [...] }
|
|
278
|
+
// filter_id comes from search response and is required for polling
|
|
279
|
+
const fetchPayload = {
|
|
280
|
+
filter_id: searchFilterId,
|
|
281
|
+
limit: contactIds.length,
|
|
282
|
+
visual_limit: contactIds.length,
|
|
283
|
+
id: contactIds,
|
|
284
|
+
};
|
|
270
285
|
return requestWithRetry(`${apiUrl}/fetch-contacts`, {
|
|
271
286
|
method: "POST",
|
|
272
287
|
headers: {
|
|
@@ -274,17 +289,17 @@ function createSmartProspectProvider(config) {
|
|
|
274
289
|
"Content-Type": "application/json",
|
|
275
290
|
Accept: "application/json",
|
|
276
291
|
},
|
|
277
|
-
body: JSON.stringify(
|
|
292
|
+
body: JSON.stringify(fetchPayload),
|
|
278
293
|
});
|
|
279
294
|
};
|
|
280
295
|
try {
|
|
281
296
|
const token = await getAuthToken();
|
|
282
297
|
const fetchResult = await makeFetchRequest(token);
|
|
283
|
-
// If fetch-contacts returns pending status
|
|
284
|
-
const filterId = fetchResult.data?.filter_id;
|
|
298
|
+
// If fetch-contacts returns pending status, poll for results using the filter_id we sent
|
|
285
299
|
const status = fetchResult.data?.status;
|
|
286
|
-
|
|
287
|
-
|
|
300
|
+
// Use the searchFilterId we passed in (from search response), not from fetch response
|
|
301
|
+
if (searchFilterId && status === "pending") {
|
|
302
|
+
const pollResult = await pollForContactResults(searchFilterId, contactIds.length);
|
|
288
303
|
if (pollResult.success && pollResult.data.list.length > 0) {
|
|
289
304
|
return {
|
|
290
305
|
success: true,
|
|
@@ -296,7 +311,7 @@ function createSmartProspectProvider(config) {
|
|
|
296
311
|
leads_found: pollResult.data.metrics.totalContacts,
|
|
297
312
|
email_fetched: pollResult.data.metrics.totalEmails,
|
|
298
313
|
verification_status_list: [],
|
|
299
|
-
filter_id:
|
|
314
|
+
filter_id: String(searchFilterId),
|
|
300
315
|
status: "completed",
|
|
301
316
|
},
|
|
302
317
|
};
|
|
@@ -394,7 +409,8 @@ function createSmartProspectProvider(config) {
|
|
|
394
409
|
// Step 2: Fetch emails for ALL matching contacts (COSTS CREDITS)
|
|
395
410
|
// Note: This costs more credits but gives us all available emails
|
|
396
411
|
const contactIds = scoredMatches.map((m) => m.contact.id);
|
|
397
|
-
const
|
|
412
|
+
const searchFilterId = searchResult.data.filter_id;
|
|
413
|
+
const fetchResult = await fetchContacts(contactIds, searchFilterId);
|
|
398
414
|
if (!fetchResult.success || fetchResult.data.list.length === 0) {
|
|
399
415
|
return null;
|
|
400
416
|
}
|
|
@@ -549,7 +565,7 @@ function createSmartProspectClient(config) {
|
|
|
549
565
|
if (filters.city?.length)
|
|
550
566
|
payload.city = filters.city;
|
|
551
567
|
// Workflow options
|
|
552
|
-
payload.dontDisplayOwnedContact = filters.dontDisplayOwnedContact ??
|
|
568
|
+
payload.dontDisplayOwnedContact = filters.dontDisplayOwnedContact ?? false;
|
|
553
569
|
// Pagination
|
|
554
570
|
payload.limit = filters.limit ?? 25;
|
|
555
571
|
if (filters.scroll_id)
|
|
@@ -628,11 +644,18 @@ function createSmartProspectClient(config) {
|
|
|
628
644
|
const token = await getAuthToken();
|
|
629
645
|
const result = await makeRequest(token);
|
|
630
646
|
if (result.success && result.data.metrics) {
|
|
631
|
-
const { completed, totalContacts } = result.data.metrics;
|
|
632
|
-
|
|
633
|
-
|
|
647
|
+
const { completed, totalContacts, totalEmails } = result.data.metrics;
|
|
648
|
+
const contact = result.data.list[0];
|
|
649
|
+
const email = contact?.email;
|
|
650
|
+
console.log(`[SmartProspect] poll ${attempt + 1}/${maxAttempts}: completed=${completed}/${totalContacts}, emails=${totalEmails}, email=${email || "(none)"}`);
|
|
651
|
+
// Check if all contacts have been processed AND we have an email
|
|
652
|
+
if ((completed >= totalContacts || completed >= expectedCount) &&
|
|
653
|
+
email) {
|
|
634
654
|
return result;
|
|
635
655
|
}
|
|
656
|
+
// NOTE: Don't exit early on noEmailFound - it may be a temporary state
|
|
657
|
+
// The API sometimes reports noEmailFound before the email is ready
|
|
658
|
+
// Keep polling until we get an email or hit the timeout
|
|
636
659
|
}
|
|
637
660
|
// Wait before next poll
|
|
638
661
|
await (0, http_retry_1.delay)(pollInterval);
|
|
@@ -680,8 +703,11 @@ function createSmartProspectClient(config) {
|
|
|
680
703
|
* 1. Call fetch-contacts to initiate lookup (returns immediately with status: "pending")
|
|
681
704
|
* 2. Poll get-contacts until metrics.completed >= expected count
|
|
682
705
|
* 3. Return the final enriched contacts with emails
|
|
706
|
+
*
|
|
707
|
+
* @param contactIds - Array of contact IDs to fetch
|
|
708
|
+
* @param searchFilterId - Filter ID from the search response (required by API)
|
|
683
709
|
*/
|
|
684
|
-
async function fetch(contactIds) {
|
|
710
|
+
async function fetch(contactIds, searchFilterId) {
|
|
685
711
|
if (!contactIds.length) {
|
|
686
712
|
return {
|
|
687
713
|
success: true,
|
|
@@ -705,6 +731,13 @@ function createSmartProspectClient(config) {
|
|
|
705
731
|
};
|
|
706
732
|
}
|
|
707
733
|
const makeFetchRequest = async (token) => {
|
|
734
|
+
// API expects: { filter_id, limit, visual_limit, id: [...] }
|
|
735
|
+
const fetchPayload = {
|
|
736
|
+
filter_id: searchFilterId,
|
|
737
|
+
limit: contactIds.length,
|
|
738
|
+
visual_limit: contactIds.length,
|
|
739
|
+
id: contactIds,
|
|
740
|
+
};
|
|
708
741
|
return requestWithRetry(`${apiUrl}/fetch-contacts`, {
|
|
709
742
|
method: "POST",
|
|
710
743
|
headers: {
|
|
@@ -712,18 +745,21 @@ function createSmartProspectClient(config) {
|
|
|
712
745
|
"Content-Type": "application/json",
|
|
713
746
|
Accept: "application/json",
|
|
714
747
|
},
|
|
715
|
-
body: JSON.stringify(
|
|
748
|
+
body: JSON.stringify(fetchPayload),
|
|
716
749
|
});
|
|
717
750
|
};
|
|
718
751
|
try {
|
|
719
752
|
const token = await getAuthToken();
|
|
720
753
|
const fetchResult = await makeFetchRequest(token);
|
|
721
|
-
// If fetch-contacts returns
|
|
722
|
-
|
|
723
|
-
const status = fetchResult.data?.status;
|
|
724
|
-
|
|
754
|
+
// If fetch-contacts returns pending status OR no email found, poll for results
|
|
755
|
+
// Note: status can be in data.status OR data.list[0].status depending on API response
|
|
756
|
+
const status = fetchResult.data?.status || fetchResult.data?.list?.[0]?.status;
|
|
757
|
+
const hasEmail = fetchResult.data?.list?.[0]?.email;
|
|
758
|
+
// Use the searchFilterId we passed in (from search response), not from fetch response
|
|
759
|
+
// Poll if: status is pending, OR we have a filter_id but no email yet
|
|
760
|
+
if (searchFilterId && (status === "pending" || !hasEmail)) {
|
|
725
761
|
// Poll get-contacts for final results
|
|
726
|
-
const pollResult = await pollForResults(
|
|
762
|
+
const pollResult = await pollForResults(searchFilterId, contactIds.length);
|
|
727
763
|
if (pollResult.success && pollResult.data.list.length > 0) {
|
|
728
764
|
// Convert poll result to fetch response format
|
|
729
765
|
return {
|
|
@@ -736,7 +772,7 @@ function createSmartProspectClient(config) {
|
|
|
736
772
|
leads_found: pollResult.data.metrics.totalContacts,
|
|
737
773
|
email_fetched: pollResult.data.metrics.totalEmails,
|
|
738
774
|
verification_status_list: [],
|
|
739
|
-
filter_id:
|
|
775
|
+
filter_id: String(searchFilterId),
|
|
740
776
|
status: "completed",
|
|
741
777
|
},
|
|
742
778
|
};
|
|
@@ -793,7 +829,8 @@ function createSmartProspectClient(config) {
|
|
|
793
829
|
};
|
|
794
830
|
}
|
|
795
831
|
const contactIds = searchResponse.data.list.map((c) => c.id);
|
|
796
|
-
const
|
|
832
|
+
const searchFilterId = searchResponse.data.filter_id;
|
|
833
|
+
const fetchResponse = await fetch(contactIds, searchFilterId);
|
|
797
834
|
return {
|
|
798
835
|
searchResponse,
|
|
799
836
|
fetchResponse,
|
|
@@ -30,8 +30,8 @@ export declare function createSnovioProvider(config: SnovioConfig): (candidate:
|
|
|
30
30
|
* 'Doe',
|
|
31
31
|
* 'example.com',
|
|
32
32
|
* {
|
|
33
|
-
*
|
|
34
|
-
*
|
|
33
|
+
* clientId: process.env.SNOVIO_CLIENT_ID,
|
|
34
|
+
* clientSecret: process.env.SNOVIO_CLIENT_SECRET,
|
|
35
35
|
* }
|
|
36
36
|
* );
|
|
37
37
|
* ```
|
|
@@ -43,8 +43,8 @@ export declare function findEmailsWithSnovio(firstName: string, lastName: string
|
|
|
43
43
|
* @example
|
|
44
44
|
* ```typescript
|
|
45
45
|
* const result = await verifyEmailWithSnovio('test@example.com', {
|
|
46
|
-
*
|
|
47
|
-
*
|
|
46
|
+
* clientId: process.env.SNOVIO_CLIENT_ID,
|
|
47
|
+
* clientSecret: process.env.SNOVIO_CLIENT_SECRET,
|
|
48
48
|
* });
|
|
49
49
|
* ```
|
|
50
50
|
*/
|
|
@@ -68,7 +68,7 @@ function extractNameAndDomain(candidate) {
|
|
|
68
68
|
/**
|
|
69
69
|
* Get OAuth access token from Snov.io
|
|
70
70
|
*/
|
|
71
|
-
async function getAccessToken(
|
|
71
|
+
async function getAccessToken(clientId, clientSecret, apiUrl, timeoutMs) {
|
|
72
72
|
// Return cached token if valid
|
|
73
73
|
if (cachedAccessToken && Date.now() < tokenExpiresAt) {
|
|
74
74
|
return cachedAccessToken;
|
|
@@ -83,8 +83,8 @@ async function getAccessToken(userId, apiSecret, apiUrl, timeoutMs) {
|
|
|
83
83
|
},
|
|
84
84
|
body: JSON.stringify({
|
|
85
85
|
grant_type: "client_credentials",
|
|
86
|
-
client_id:
|
|
87
|
-
client_secret:
|
|
86
|
+
client_id: clientId,
|
|
87
|
+
client_secret: clientSecret,
|
|
88
88
|
}),
|
|
89
89
|
signal: controller.signal,
|
|
90
90
|
});
|
|
@@ -97,7 +97,8 @@ async function getAccessToken(userId, apiSecret, apiUrl, timeoutMs) {
|
|
|
97
97
|
tokenExpiresAt = Date.now() + (data.expires_in - 300) * 1000;
|
|
98
98
|
return data.access_token;
|
|
99
99
|
}
|
|
100
|
-
catch {
|
|
100
|
+
catch (err) {
|
|
101
|
+
console.error("[Snov.io] Failed to get access token:", err instanceof Error ? err.message : err);
|
|
101
102
|
return null;
|
|
102
103
|
}
|
|
103
104
|
finally {
|
|
@@ -136,8 +137,10 @@ async function findEmailsByName(firstName, lastName, domain, accessToken, apiUrl
|
|
|
136
137
|
}
|
|
137
138
|
catch (error) {
|
|
138
139
|
if (error instanceof Error && error.name === "AbortError") {
|
|
139
|
-
|
|
140
|
+
console.error("[Snov.io] Request timed out");
|
|
141
|
+
return null;
|
|
140
142
|
}
|
|
143
|
+
console.error("[Snov.io] Email lookup failed:", error instanceof Error ? error.message : error);
|
|
141
144
|
throw error;
|
|
142
145
|
}
|
|
143
146
|
finally {
|
|
@@ -151,8 +154,8 @@ async function findEmailsByName(firstName, lastName, domain, accessToken, apiUrl
|
|
|
151
154
|
* when pattern guessing fails or for catch-all domains.
|
|
152
155
|
*/
|
|
153
156
|
function createSnovioProvider(config) {
|
|
154
|
-
const {
|
|
155
|
-
if (!
|
|
157
|
+
const { clientId, clientSecret, apiUrl = DEFAULT_API_URL, timeoutMs = DEFAULT_TIMEOUT_MS, } = config;
|
|
158
|
+
if (!clientId || !clientSecret) {
|
|
156
159
|
return (0, noop_provider_1.createNoOpProvider)("snovio");
|
|
157
160
|
}
|
|
158
161
|
async function findEmails(candidate) {
|
|
@@ -162,7 +165,7 @@ function createSnovioProvider(config) {
|
|
|
162
165
|
}
|
|
163
166
|
const { firstName, lastName, domain } = nameAndDomain;
|
|
164
167
|
// Get access token
|
|
165
|
-
const accessToken = await getAccessToken(
|
|
168
|
+
const accessToken = await getAccessToken(clientId, clientSecret, apiUrl, timeoutMs);
|
|
166
169
|
if (!accessToken) {
|
|
167
170
|
return null;
|
|
168
171
|
}
|
|
@@ -210,15 +213,15 @@ function createSnovioProvider(config) {
|
|
|
210
213
|
* 'Doe',
|
|
211
214
|
* 'example.com',
|
|
212
215
|
* {
|
|
213
|
-
*
|
|
214
|
-
*
|
|
216
|
+
* clientId: process.env.SNOVIO_CLIENT_ID,
|
|
217
|
+
* clientSecret: process.env.SNOVIO_CLIENT_SECRET,
|
|
215
218
|
* }
|
|
216
219
|
* );
|
|
217
220
|
* ```
|
|
218
221
|
*/
|
|
219
222
|
async function findEmailsWithSnovio(firstName, lastName, domain, config) {
|
|
220
|
-
const {
|
|
221
|
-
const accessToken = await getAccessToken(
|
|
223
|
+
const { clientId, clientSecret, apiUrl = DEFAULT_API_URL, timeoutMs = DEFAULT_TIMEOUT_MS, } = config;
|
|
224
|
+
const accessToken = await getAccessToken(clientId, clientSecret, apiUrl, timeoutMs);
|
|
222
225
|
if (!accessToken) {
|
|
223
226
|
return null;
|
|
224
227
|
}
|
|
@@ -230,14 +233,14 @@ async function findEmailsWithSnovio(firstName, lastName, domain, config) {
|
|
|
230
233
|
* @example
|
|
231
234
|
* ```typescript
|
|
232
235
|
* const result = await verifyEmailWithSnovio('test@example.com', {
|
|
233
|
-
*
|
|
234
|
-
*
|
|
236
|
+
* clientId: process.env.SNOVIO_CLIENT_ID,
|
|
237
|
+
* clientSecret: process.env.SNOVIO_CLIENT_SECRET,
|
|
235
238
|
* });
|
|
236
239
|
* ```
|
|
237
240
|
*/
|
|
238
241
|
async function verifyEmailWithSnovio(email, config) {
|
|
239
|
-
const {
|
|
240
|
-
const accessToken = await getAccessToken(
|
|
242
|
+
const { clientId, clientSecret, apiUrl = DEFAULT_API_URL, timeoutMs = DEFAULT_TIMEOUT_MS, } = config;
|
|
243
|
+
const accessToken = await getAccessToken(clientId, clientSecret, apiUrl, timeoutMs);
|
|
241
244
|
if (!accessToken) {
|
|
242
245
|
return null;
|
|
243
246
|
}
|
|
@@ -268,7 +271,8 @@ async function verifyEmailWithSnovio(email, config) {
|
|
|
268
271
|
status: result.result,
|
|
269
272
|
};
|
|
270
273
|
}
|
|
271
|
-
catch {
|
|
274
|
+
catch (err) {
|
|
275
|
+
console.error("[Snov.io] Email verification failed:", err instanceof Error ? err.message : err);
|
|
272
276
|
return null;
|
|
273
277
|
}
|
|
274
278
|
finally {
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* - 2-5x faster than traditional SMTP verification
|
|
12
12
|
*
|
|
13
13
|
* API Endpoints:
|
|
14
|
-
* - POST /job/
|
|
15
|
-
* - POST /job/
|
|
14
|
+
* - POST /job/find_email - Find email by name + domain
|
|
15
|
+
* - POST /job/verify_email - Verify existing email
|
|
16
16
|
*
|
|
17
17
|
* Rate Limits (Free tier):
|
|
18
18
|
* - 2 requests/second
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* - 2-5x faster than traditional SMTP verification
|
|
13
13
|
*
|
|
14
14
|
* API Endpoints:
|
|
15
|
-
* - POST /job/
|
|
16
|
-
* - POST /job/
|
|
15
|
+
* - POST /job/find_email - Find email by name + domain
|
|
16
|
+
* - POST /job/verify_email - Verify existing email
|
|
17
17
|
*
|
|
18
18
|
* Rate Limits (Free tier):
|
|
19
19
|
* - 2 requests/second
|
|
@@ -31,6 +31,58 @@ const candidate_parser_1 = require("../utils/candidate-parser");
|
|
|
31
31
|
const noop_provider_1 = require("../utils/noop-provider");
|
|
32
32
|
const DEFAULT_API_URL = "https://api.trykitt.ai";
|
|
33
33
|
const DEFAULT_TIMEOUT_MS = 30000;
|
|
34
|
+
const JOB_POLL_INTERVAL_MS = 1000;
|
|
35
|
+
const JOB_MAX_POLLS = 30; // Max 30 seconds waiting
|
|
36
|
+
/**
|
|
37
|
+
* Poll for job completion
|
|
38
|
+
*/
|
|
39
|
+
async function pollJobResult(apiUrl, apiKey, jobId, timeoutMs) {
|
|
40
|
+
for (let i = 0; i < JOB_MAX_POLLS; i++) {
|
|
41
|
+
try {
|
|
42
|
+
const response = await (0, http_retry_1.getWithRetry)(`${apiUrl}/job?id=${jobId}`, {
|
|
43
|
+
"x-api-key": apiKey,
|
|
44
|
+
}, {
|
|
45
|
+
retries: 1,
|
|
46
|
+
backoffMs: 500,
|
|
47
|
+
timeoutMs,
|
|
48
|
+
});
|
|
49
|
+
// API returns an array - get first element
|
|
50
|
+
const jobData = Array.isArray(response)
|
|
51
|
+
? response[0]
|
|
52
|
+
: response;
|
|
53
|
+
if (!jobData) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const jobObj = jobData;
|
|
57
|
+
const results = jobObj.results;
|
|
58
|
+
if (jobObj.status === "completed") {
|
|
59
|
+
// Return in expected format, mapping 'results' to 'result'
|
|
60
|
+
return {
|
|
61
|
+
status: jobObj.status,
|
|
62
|
+
result: results
|
|
63
|
+
? {
|
|
64
|
+
email: results.email !== "no-results-found"
|
|
65
|
+
? results.email
|
|
66
|
+
: null,
|
|
67
|
+
confidence: results.confidence,
|
|
68
|
+
confidence_score: results.confidence_score,
|
|
69
|
+
}
|
|
70
|
+
: null,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (jobObj.status === "failed" || jobObj.status === "error") {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
// Wait before next poll
|
|
77
|
+
await new Promise((resolve) => setTimeout(resolve, JOB_POLL_INTERVAL_MS));
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
console.error("[TryKitt] Poll error:", err instanceof Error ? err.message : err);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
34
86
|
/**
|
|
35
87
|
* Extract TryKitt-specific inputs from candidate using shared parser
|
|
36
88
|
*/
|
|
@@ -82,25 +134,32 @@ function createTryKittProvider(config) {
|
|
|
82
134
|
return null;
|
|
83
135
|
}
|
|
84
136
|
try {
|
|
85
|
-
// Build request body
|
|
137
|
+
// Build request body - API expects camelCase parameters
|
|
138
|
+
// callbackType: "here" forces synchronous/realtime processing
|
|
86
139
|
const body = {
|
|
87
|
-
|
|
88
|
-
|
|
140
|
+
fullName: fullName,
|
|
141
|
+
domainOrWebsite: domain,
|
|
142
|
+
callbackType: "here",
|
|
143
|
+
callbackURL: "",
|
|
89
144
|
};
|
|
90
145
|
// Add LinkedIn URL if available (improves accuracy)
|
|
91
146
|
if (linkedinUrl) {
|
|
92
|
-
body.
|
|
147
|
+
body.linkedinStandardProfileURL = linkedinUrl;
|
|
93
148
|
}
|
|
94
|
-
const
|
|
95
|
-
|
|
149
|
+
const submitResponse = await (0, http_retry_1.postWithRetry)(`${apiUrl}/job/find_email`, body, {
|
|
150
|
+
"x-api-key": apiKey,
|
|
96
151
|
}, {
|
|
97
152
|
retries: 2,
|
|
98
153
|
backoffMs: 500,
|
|
99
154
|
timeoutMs,
|
|
100
155
|
retryOnStatus: [429, 500, 502, 503, 504],
|
|
101
156
|
});
|
|
102
|
-
|
|
103
|
-
|
|
157
|
+
if (!submitResponse.job_id) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
// Poll for job completion
|
|
161
|
+
const response = await pollJobResult(apiUrl, apiKey, submitResponse.job_id, timeoutMs);
|
|
162
|
+
if (!response) {
|
|
104
163
|
return null;
|
|
105
164
|
}
|
|
106
165
|
// Check if email was found
|
|
@@ -121,8 +180,8 @@ function createTryKittProvider(config) {
|
|
|
121
180
|
score: confidence,
|
|
122
181
|
};
|
|
123
182
|
}
|
|
124
|
-
catch {
|
|
125
|
-
|
|
183
|
+
catch (err) {
|
|
184
|
+
console.error("[TryKitt] Email lookup failed:", err instanceof Error ? err.message : err);
|
|
126
185
|
return null;
|
|
127
186
|
}
|
|
128
187
|
}
|
|
@@ -147,15 +206,19 @@ async function findEmailWithTryKitt(fullName, domain, config, linkedinUrl) {
|
|
|
147
206
|
return null;
|
|
148
207
|
}
|
|
149
208
|
try {
|
|
209
|
+
// API expects camelCase parameters
|
|
210
|
+
// callbackType: "here" forces synchronous/realtime processing
|
|
150
211
|
const body = {
|
|
151
|
-
|
|
152
|
-
|
|
212
|
+
fullName: fullName,
|
|
213
|
+
domainOrWebsite: domain,
|
|
214
|
+
callbackType: "here",
|
|
215
|
+
callbackURL: "",
|
|
153
216
|
};
|
|
154
217
|
if (linkedinUrl) {
|
|
155
|
-
body.
|
|
218
|
+
body.linkedinStandardProfileURL = linkedinUrl;
|
|
156
219
|
}
|
|
157
|
-
const response = await (0, http_retry_1.postWithRetry)(`${apiUrl}/job/
|
|
158
|
-
|
|
220
|
+
const response = await (0, http_retry_1.postWithRetry)(`${apiUrl}/job/find_email`, body, {
|
|
221
|
+
"x-api-key": apiKey,
|
|
159
222
|
}, {
|
|
160
223
|
retries: 2,
|
|
161
224
|
backoffMs: 500,
|
|
@@ -163,7 +226,8 @@ async function findEmailWithTryKitt(fullName, domain, config, linkedinUrl) {
|
|
|
163
226
|
});
|
|
164
227
|
return response;
|
|
165
228
|
}
|
|
166
|
-
catch {
|
|
229
|
+
catch (err) {
|
|
230
|
+
console.error("[TryKitt] findEmailWithTryKitt failed:", err instanceof Error ? err.message : err);
|
|
167
231
|
return null;
|
|
168
232
|
}
|
|
169
233
|
}
|
|
@@ -185,8 +249,8 @@ async function verifyEmailWithTryKitt(email, config) {
|
|
|
185
249
|
return null;
|
|
186
250
|
}
|
|
187
251
|
try {
|
|
188
|
-
const response = await (0, http_retry_1.postWithRetry)(`${apiUrl}/job/
|
|
189
|
-
|
|
252
|
+
const response = await (0, http_retry_1.postWithRetry)(`${apiUrl}/job/verify_email`, { email }, {
|
|
253
|
+
"x-api-key": apiKey,
|
|
190
254
|
}, {
|
|
191
255
|
retries: 2,
|
|
192
256
|
backoffMs: 500,
|
|
@@ -204,7 +268,8 @@ async function verifyEmailWithTryKitt(email, config) {
|
|
|
204
268
|
isRoleAccount: response.result.is_role_account ?? false,
|
|
205
269
|
};
|
|
206
270
|
}
|
|
207
|
-
catch {
|
|
271
|
+
catch (err) {
|
|
272
|
+
console.error("[TryKitt] Email verification failed:", err instanceof Error ? err.message : err);
|
|
208
273
|
return null;
|
|
209
274
|
}
|
|
210
275
|
}
|