linkedin-secret-sauce 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/enrichment/auth/index.d.ts +4 -0
- package/dist/enrichment/auth/index.js +17 -0
- package/dist/enrichment/auth/smartlead-auth.d.ts +82 -0
- package/dist/enrichment/auth/smartlead-auth.js +321 -0
- package/dist/enrichment/index.d.ts +11 -8
- package/dist/enrichment/index.js +75 -20
- package/dist/enrichment/matching.d.ts +239 -0
- package/dist/enrichment/matching.js +619 -0
- package/dist/enrichment/orchestrator.d.ts +13 -1
- package/dist/enrichment/orchestrator.js +220 -3
- 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 +2 -2
- package/dist/enrichment/providers/ldd.js +16 -8
- package/dist/enrichment/providers/smartprospect.d.ts +72 -2
- package/dist/enrichment/providers/smartprospect.js +512 -72
- package/dist/enrichment/types.d.ts +156 -14
- package/dist/enrichment/types.js +56 -7
- package/dist/index.d.ts +2 -1
- package/dist/index.js +7 -1
- package/package.json +1 -1
|
@@ -4,10 +4,20 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Smartlead's prospect database - a REVERSE-ENGINEERED private API.
|
|
6
6
|
* Two-phase process: search (free) then fetch (costs credits).
|
|
7
|
+
*
|
|
8
|
+
* Supports two authentication methods:
|
|
9
|
+
* 1. Direct token: Pass `apiToken` directly (for pre-authenticated scenarios)
|
|
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
|
|
7
15
|
*/
|
|
8
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
17
|
exports.createSmartProspectProvider = createSmartProspectProvider;
|
|
10
|
-
|
|
18
|
+
exports.createSmartProspectClient = createSmartProspectClient;
|
|
19
|
+
const smartlead_auth_1 = require("../auth/smartlead-auth");
|
|
20
|
+
const DEFAULT_API_URL = "https://prospect-api.smartlead.ai/api/search-email-leads";
|
|
11
21
|
/**
|
|
12
22
|
* Delay helper for retry logic
|
|
13
23
|
*/
|
|
@@ -28,7 +38,7 @@ async function requestWithRetry(url, options, retries = 2, backoffMs = 300) {
|
|
|
28
38
|
continue;
|
|
29
39
|
}
|
|
30
40
|
if (!res.ok) {
|
|
31
|
-
const errorText = await res.text().catch(() =>
|
|
41
|
+
const errorText = await res.text().catch(() => "");
|
|
32
42
|
throw new Error(`SmartProspect API error: ${res.status} - ${errorText}`);
|
|
33
43
|
}
|
|
34
44
|
return (await res.json());
|
|
@@ -41,7 +51,7 @@ async function requestWithRetry(url, options, retries = 2, backoffMs = 300) {
|
|
|
41
51
|
}
|
|
42
52
|
}
|
|
43
53
|
}
|
|
44
|
-
throw lastErr ?? new Error(
|
|
54
|
+
throw lastErr ?? new Error("smartprospect_http_error");
|
|
45
55
|
}
|
|
46
56
|
/**
|
|
47
57
|
* Calculate match score between search input and contact result
|
|
@@ -49,30 +59,33 @@ async function requestWithRetry(url, options, retries = 2, backoffMs = 300) {
|
|
|
49
59
|
function calculateMatchScore(contact, searchName, company, title) {
|
|
50
60
|
let score = 0;
|
|
51
61
|
// Name match scoring (0-50 points)
|
|
52
|
-
const contactName = contact.fullName?.toLowerCase() ||
|
|
62
|
+
const contactName = contact.fullName?.toLowerCase() || "";
|
|
53
63
|
const searchNameLower = searchName.toLowerCase();
|
|
54
64
|
if (contactName === searchNameLower) {
|
|
55
65
|
score += 50; // Exact match
|
|
56
66
|
}
|
|
57
|
-
else if (contactName.includes(searchNameLower) ||
|
|
67
|
+
else if (contactName.includes(searchNameLower) ||
|
|
68
|
+
searchNameLower.includes(contactName)) {
|
|
58
69
|
score += 30; // Partial match
|
|
59
70
|
}
|
|
60
71
|
// Company match scoring (0-30 points)
|
|
61
72
|
if (company) {
|
|
62
|
-
const contactCompany = contact.company?.name?.toLowerCase() ||
|
|
73
|
+
const contactCompany = contact.company?.name?.toLowerCase() || "";
|
|
63
74
|
const searchCompany = company.toLowerCase();
|
|
64
75
|
if (contactCompany === searchCompany) {
|
|
65
76
|
score += 30; // Exact match
|
|
66
77
|
}
|
|
67
|
-
else if (contactCompany.includes(searchCompany) ||
|
|
78
|
+
else if (contactCompany.includes(searchCompany) ||
|
|
79
|
+
searchCompany.includes(contactCompany)) {
|
|
68
80
|
score += 15; // Partial match
|
|
69
81
|
}
|
|
70
82
|
}
|
|
71
83
|
// Title match scoring (0-10 points)
|
|
72
84
|
if (title) {
|
|
73
|
-
const contactTitle = contact.title?.toLowerCase() ||
|
|
85
|
+
const contactTitle = contact.title?.toLowerCase() || "";
|
|
74
86
|
const searchTitle = title.toLowerCase();
|
|
75
|
-
if (contactTitle.includes(searchTitle) ||
|
|
87
|
+
if (contactTitle.includes(searchTitle) ||
|
|
88
|
+
searchTitle.includes(contactTitle)) {
|
|
76
89
|
score += 10;
|
|
77
90
|
}
|
|
78
91
|
}
|
|
@@ -89,37 +102,72 @@ function extractNames(candidate) {
|
|
|
89
102
|
const firstName = candidate.firstName ||
|
|
90
103
|
candidate.first_name ||
|
|
91
104
|
candidate.first ||
|
|
92
|
-
candidate.name?.split(
|
|
93
|
-
|
|
105
|
+
candidate.name?.split(" ")?.[0] ||
|
|
106
|
+
"";
|
|
94
107
|
const lastName = candidate.lastName ||
|
|
95
108
|
candidate.last_name ||
|
|
96
109
|
candidate.last ||
|
|
97
|
-
candidate.name?.split(
|
|
98
|
-
|
|
110
|
+
candidate.name?.split(" ")?.slice(1).join(" ") ||
|
|
111
|
+
"";
|
|
99
112
|
return { firstName, lastName };
|
|
100
113
|
}
|
|
101
114
|
/**
|
|
102
115
|
* Create the SmartProspect provider function
|
|
116
|
+
*
|
|
117
|
+
* Supports two auth methods:
|
|
118
|
+
* 1. Direct token: Pass `apiToken` in config
|
|
119
|
+
* 2. Credentials: Pass `email` and `password` in config for auto-login
|
|
103
120
|
*/
|
|
104
121
|
function createSmartProspectProvider(config) {
|
|
105
|
-
const { apiToken, apiUrl = DEFAULT_API_URL } = config;
|
|
106
|
-
if
|
|
122
|
+
const { apiToken, email, password, apiUrl = DEFAULT_API_URL, loginUrl, eagerInit = true, } = config;
|
|
123
|
+
// Check if we have valid auth config
|
|
124
|
+
const hasDirectToken = !!apiToken;
|
|
125
|
+
const hasCredentials = !!email && !!password;
|
|
126
|
+
if (!hasDirectToken && !hasCredentials) {
|
|
107
127
|
// Return a no-op provider if not configured
|
|
108
128
|
const noopProvider = async () => null;
|
|
109
|
-
noopProvider.__name =
|
|
129
|
+
noopProvider.__name = "smartprospect";
|
|
110
130
|
return noopProvider;
|
|
111
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Get the current auth token (either direct or via login)
|
|
134
|
+
*/
|
|
135
|
+
async function getAuthToken() {
|
|
136
|
+
if (hasDirectToken) {
|
|
137
|
+
return apiToken;
|
|
138
|
+
}
|
|
139
|
+
// Use credentials-based auth with token caching
|
|
140
|
+
return (0, smartlead_auth_1.getSmartLeadToken)({
|
|
141
|
+
credentials: { email: email, password: password },
|
|
142
|
+
loginUrl,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
// Eager initialization: pre-fetch token so it's ready for first request
|
|
146
|
+
if (eagerInit && hasCredentials) {
|
|
147
|
+
// Fire and forget - don't block provider creation
|
|
148
|
+
getAuthToken().catch(() => {
|
|
149
|
+
// Silently ignore errors during eager init - will retry on first request
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Handle 401 errors by clearing cached token
|
|
154
|
+
*/
|
|
155
|
+
async function handleAuthError() {
|
|
156
|
+
if (hasCredentials) {
|
|
157
|
+
(0, smartlead_auth_1.clearSmartLeadToken)(email);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
112
160
|
/**
|
|
113
161
|
* Search for contacts matching filters (FREE - no credits used)
|
|
114
162
|
*/
|
|
115
163
|
async function searchContacts(filters) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
method:
|
|
164
|
+
const makeRequest = async (token) => {
|
|
165
|
+
return requestWithRetry(`${apiUrl}/search-contacts`, {
|
|
166
|
+
method: "POST",
|
|
119
167
|
headers: {
|
|
120
|
-
Authorization: `Bearer ${
|
|
121
|
-
|
|
122
|
-
Accept:
|
|
168
|
+
Authorization: `Bearer ${token}`,
|
|
169
|
+
"Content-Type": "application/json",
|
|
170
|
+
Accept: "application/json",
|
|
123
171
|
},
|
|
124
172
|
body: JSON.stringify({
|
|
125
173
|
...filters,
|
|
@@ -128,12 +176,28 @@ function createSmartProspectProvider(config) {
|
|
|
128
176
|
titleExactMatch: filters.titleExactMatch ?? false,
|
|
129
177
|
}),
|
|
130
178
|
});
|
|
131
|
-
|
|
179
|
+
};
|
|
180
|
+
try {
|
|
181
|
+
const token = await getAuthToken();
|
|
182
|
+
return await makeRequest(token);
|
|
132
183
|
}
|
|
133
|
-
catch {
|
|
184
|
+
catch (err) {
|
|
185
|
+
// On 401, clear token and retry once with fresh token
|
|
186
|
+
if (err instanceof Error &&
|
|
187
|
+
err.message.includes("401") &&
|
|
188
|
+
hasCredentials) {
|
|
189
|
+
await handleAuthError();
|
|
190
|
+
try {
|
|
191
|
+
const freshToken = await getAuthToken();
|
|
192
|
+
return await makeRequest(freshToken);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// Retry failed too
|
|
196
|
+
}
|
|
197
|
+
}
|
|
134
198
|
return {
|
|
135
199
|
success: false,
|
|
136
|
-
message:
|
|
200
|
+
message: "Search failed",
|
|
137
201
|
data: { list: [], total_count: 0 },
|
|
138
202
|
};
|
|
139
203
|
}
|
|
@@ -142,22 +206,38 @@ function createSmartProspectProvider(config) {
|
|
|
142
206
|
* Fetch/enrich emails for specific contact IDs (COSTS CREDITS)
|
|
143
207
|
*/
|
|
144
208
|
async function fetchContacts(contactIds) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
method:
|
|
209
|
+
const makeRequest = async (token) => {
|
|
210
|
+
return requestWithRetry(`${apiUrl}/fetch-contacts`, {
|
|
211
|
+
method: "POST",
|
|
148
212
|
headers: {
|
|
149
|
-
Authorization: `Bearer ${
|
|
150
|
-
|
|
151
|
-
Accept:
|
|
213
|
+
Authorization: `Bearer ${token}`,
|
|
214
|
+
"Content-Type": "application/json",
|
|
215
|
+
Accept: "application/json",
|
|
152
216
|
},
|
|
153
217
|
body: JSON.stringify({ contactIds }),
|
|
154
218
|
});
|
|
155
|
-
|
|
219
|
+
};
|
|
220
|
+
try {
|
|
221
|
+
const token = await getAuthToken();
|
|
222
|
+
return await makeRequest(token);
|
|
156
223
|
}
|
|
157
|
-
catch {
|
|
224
|
+
catch (err) {
|
|
225
|
+
// On 401, clear token and retry once with fresh token
|
|
226
|
+
if (err instanceof Error &&
|
|
227
|
+
err.message.includes("401") &&
|
|
228
|
+
hasCredentials) {
|
|
229
|
+
await handleAuthError();
|
|
230
|
+
try {
|
|
231
|
+
const freshToken = await getAuthToken();
|
|
232
|
+
return await makeRequest(freshToken);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// Retry failed too
|
|
236
|
+
}
|
|
237
|
+
}
|
|
158
238
|
return {
|
|
159
239
|
success: false,
|
|
160
|
-
message:
|
|
240
|
+
message: "Fetch failed",
|
|
161
241
|
data: {
|
|
162
242
|
list: [],
|
|
163
243
|
total_count: 0,
|
|
@@ -177,73 +257,433 @@ function createSmartProspectProvider(config) {
|
|
|
177
257
|
};
|
|
178
258
|
}
|
|
179
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* Calculate confidence score for a contact
|
|
262
|
+
*/
|
|
263
|
+
function calculateConfidence(contact) {
|
|
264
|
+
const isVerified = contact.verificationStatus === "valid";
|
|
265
|
+
const isCatchAll = contact.verificationStatus === "catch_all";
|
|
266
|
+
const deliverability = contact.emailDeliverability || 0;
|
|
267
|
+
let confidence = deliverability * 100;
|
|
268
|
+
if (isVerified) {
|
|
269
|
+
confidence = Math.max(confidence, 90);
|
|
270
|
+
}
|
|
271
|
+
else if (isCatchAll) {
|
|
272
|
+
confidence = Math.min(confidence, 75);
|
|
273
|
+
}
|
|
274
|
+
return confidence;
|
|
275
|
+
}
|
|
180
276
|
async function fetchEmail(candidate) {
|
|
181
277
|
const { firstName, lastName } = extractNames(candidate);
|
|
182
278
|
if (!firstName) {
|
|
183
279
|
return null; // Minimum requirement
|
|
184
280
|
}
|
|
185
281
|
const fullName = `${firstName} ${lastName}`.trim();
|
|
186
|
-
const company = candidate.company ||
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
282
|
+
const company = candidate.company ||
|
|
283
|
+
candidate.currentCompany ||
|
|
284
|
+
candidate.organization ||
|
|
285
|
+
undefined;
|
|
286
|
+
const title = candidate.title ||
|
|
287
|
+
candidate.currentRole ||
|
|
288
|
+
candidate.current_role ||
|
|
289
|
+
undefined;
|
|
290
|
+
const domain = candidate.domain ||
|
|
291
|
+
candidate.companyDomain ||
|
|
292
|
+
candidate.company_domain ||
|
|
293
|
+
undefined;
|
|
294
|
+
// Build search filters using correct API field names
|
|
190
295
|
const filters = {
|
|
191
296
|
name: [fullName],
|
|
192
|
-
limit:
|
|
297
|
+
limit: 10, // Get more results to return multiple emails
|
|
193
298
|
};
|
|
194
299
|
if (company) {
|
|
195
|
-
filters.
|
|
300
|
+
filters.companyName = [company];
|
|
196
301
|
}
|
|
197
302
|
if (domain) {
|
|
198
|
-
filters.
|
|
303
|
+
filters.companyDomain = [domain];
|
|
199
304
|
}
|
|
200
305
|
// Step 1: Search for contacts (FREE)
|
|
201
306
|
const searchResult = await searchContacts(filters);
|
|
202
307
|
if (!searchResult.success || searchResult.data.list.length === 0) {
|
|
203
308
|
return null;
|
|
204
309
|
}
|
|
205
|
-
//
|
|
310
|
+
// Score and sort all matches
|
|
206
311
|
const matches = searchResult.data.list;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
if (!bestMatch) {
|
|
312
|
+
const scoredMatches = matches
|
|
313
|
+
.map((contact) => ({
|
|
314
|
+
contact,
|
|
315
|
+
score: calculateMatchScore(contact, fullName, company, title),
|
|
316
|
+
}))
|
|
317
|
+
.filter((m) => m.score > 0) // Only include matches with some relevance
|
|
318
|
+
.sort((a, b) => b.score - a.score);
|
|
319
|
+
if (scoredMatches.length === 0) {
|
|
217
320
|
return null;
|
|
218
321
|
}
|
|
219
|
-
// Step 2: Fetch
|
|
220
|
-
|
|
322
|
+
// Step 2: Fetch emails for ALL matching contacts (COSTS CREDITS)
|
|
323
|
+
// Note: This costs more credits but gives us all available emails
|
|
324
|
+
const contactIds = scoredMatches.map((m) => m.contact.id);
|
|
325
|
+
const fetchResult = await fetchContacts(contactIds);
|
|
221
326
|
if (!fetchResult.success || fetchResult.data.list.length === 0) {
|
|
222
327
|
return null;
|
|
223
328
|
}
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
329
|
+
// Build multi-result with all found emails
|
|
330
|
+
const emails = [];
|
|
331
|
+
const seenEmails = new Set();
|
|
332
|
+
for (const enrichedContact of fetchResult.data.list) {
|
|
333
|
+
const email = enrichedContact?.email;
|
|
334
|
+
if (!email)
|
|
335
|
+
continue;
|
|
336
|
+
const emailLower = email.toLowerCase();
|
|
337
|
+
if (seenEmails.has(emailLower))
|
|
338
|
+
continue;
|
|
339
|
+
seenEmails.add(emailLower);
|
|
340
|
+
const isVerified = enrichedContact.verificationStatus === "valid";
|
|
341
|
+
const isCatchAll = enrichedContact.verificationStatus === "catch_all";
|
|
342
|
+
emails.push({
|
|
343
|
+
email,
|
|
344
|
+
verified: isVerified || isCatchAll,
|
|
345
|
+
confidence: calculateConfidence(enrichedContact),
|
|
346
|
+
isCatchAll,
|
|
347
|
+
metadata: {
|
|
348
|
+
fullName: enrichedContact.fullName,
|
|
349
|
+
title: enrichedContact.title,
|
|
350
|
+
company: enrichedContact.company?.name,
|
|
351
|
+
linkedin: enrichedContact.linkedin,
|
|
352
|
+
verificationStatus: enrichedContact.verificationStatus,
|
|
353
|
+
matchScore: scoredMatches.find((m) => m.contact.id === enrichedContact.id)?.score,
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
if (emails.length === 0) {
|
|
227
358
|
return null;
|
|
228
359
|
}
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
360
|
+
// Sort by confidence (highest first)
|
|
361
|
+
emails.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0));
|
|
362
|
+
// Return multi-result format
|
|
363
|
+
return { emails };
|
|
364
|
+
}
|
|
365
|
+
// Mark provider name for orchestrator
|
|
366
|
+
fetchEmail.__name = "smartprospect";
|
|
367
|
+
return fetchEmail;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Create a SmartProspect client for direct API access
|
|
371
|
+
*
|
|
372
|
+
* This provides access to the full SmartProspect API for:
|
|
373
|
+
* - Searching contacts with comprehensive filters (FREE)
|
|
374
|
+
* - Fetching/enriching contact emails (COSTS CREDITS)
|
|
375
|
+
* - Combined search + fetch operations
|
|
376
|
+
*
|
|
377
|
+
* @example
|
|
378
|
+
* ```ts
|
|
379
|
+
* const client = createSmartProspectClient({
|
|
380
|
+
* email: 'user@example.com',
|
|
381
|
+
* password: 'password123'
|
|
382
|
+
* });
|
|
383
|
+
*
|
|
384
|
+
* // Search only (FREE)
|
|
385
|
+
* const results = await client.search({
|
|
386
|
+
* title: ['CEO', 'CTO'],
|
|
387
|
+
* company: ['Google', 'Microsoft'],
|
|
388
|
+
* level: ['C-Level'],
|
|
389
|
+
* companyHeadCount: ['1001-5000', '5001-10000'],
|
|
390
|
+
* limit: 25
|
|
391
|
+
* });
|
|
392
|
+
*
|
|
393
|
+
* // Fetch specific contacts (COSTS CREDITS)
|
|
394
|
+
* const enriched = await client.fetch(['contact-id-1', 'contact-id-2']);
|
|
395
|
+
* ```
|
|
396
|
+
*/
|
|
397
|
+
function createSmartProspectClient(config) {
|
|
398
|
+
const { apiToken, email, password, apiUrl = DEFAULT_API_URL, loginUrl, eagerInit = true, } = config;
|
|
399
|
+
// Check if we have valid auth config
|
|
400
|
+
const hasDirectToken = !!apiToken;
|
|
401
|
+
const hasCredentials = !!email && !!password;
|
|
402
|
+
if (!hasDirectToken && !hasCredentials) {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Get the current auth token
|
|
407
|
+
*/
|
|
408
|
+
async function getAuthToken() {
|
|
409
|
+
if (hasDirectToken) {
|
|
410
|
+
return apiToken;
|
|
236
411
|
}
|
|
237
|
-
|
|
238
|
-
|
|
412
|
+
return (0, smartlead_auth_1.getSmartLeadToken)({
|
|
413
|
+
credentials: { email: email, password: password },
|
|
414
|
+
loginUrl,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Handle 401 errors
|
|
419
|
+
*/
|
|
420
|
+
async function handleAuthError() {
|
|
421
|
+
if (hasCredentials) {
|
|
422
|
+
(0, smartlead_auth_1.clearSmartLeadToken)(email);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// Eager initialization
|
|
426
|
+
if (eagerInit && hasCredentials) {
|
|
427
|
+
getAuthToken().catch(() => { });
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Build the request payload with proper defaults
|
|
431
|
+
* Field names match the actual SmartProspect API
|
|
432
|
+
*/
|
|
433
|
+
function buildSearchPayload(filters) {
|
|
434
|
+
const payload = {};
|
|
435
|
+
// Contact-based filters
|
|
436
|
+
if (filters.name?.length)
|
|
437
|
+
payload.name = filters.name;
|
|
438
|
+
if (filters.firstName?.length)
|
|
439
|
+
payload.firstName = filters.firstName;
|
|
440
|
+
if (filters.lastName?.length)
|
|
441
|
+
payload.lastName = filters.lastName;
|
|
442
|
+
// Title filters
|
|
443
|
+
if (filters.title?.length)
|
|
444
|
+
payload.title = filters.title;
|
|
445
|
+
payload.titleExactMatch = filters.titleExactMatch ?? false;
|
|
446
|
+
// Department & Level
|
|
447
|
+
if (filters.department?.length)
|
|
448
|
+
payload.department = filters.department;
|
|
449
|
+
if (filters.level?.length)
|
|
450
|
+
payload.level = filters.level;
|
|
451
|
+
// Company filters (note: API uses companyName and companyDomain)
|
|
452
|
+
if (filters.companyName?.length)
|
|
453
|
+
payload.companyName = filters.companyName;
|
|
454
|
+
if (filters.companyDomain?.length)
|
|
455
|
+
payload.companyDomain = filters.companyDomain;
|
|
456
|
+
// Industry filters (note: API uses companyIndustry and companySubIndustry)
|
|
457
|
+
if (filters.companyIndustry?.length)
|
|
458
|
+
payload.companyIndustry = filters.companyIndustry;
|
|
459
|
+
if (filters.companySubIndustry?.length)
|
|
460
|
+
payload.companySubIndustry = filters.companySubIndustry;
|
|
461
|
+
// Company size filters
|
|
462
|
+
if (filters.companyHeadCount?.length)
|
|
463
|
+
payload.companyHeadCount = filters.companyHeadCount;
|
|
464
|
+
if (filters.companyRevenue?.length)
|
|
465
|
+
payload.companyRevenue = filters.companyRevenue;
|
|
466
|
+
// Location filters
|
|
467
|
+
if (filters.country?.length)
|
|
468
|
+
payload.country = filters.country;
|
|
469
|
+
if (filters.state?.length)
|
|
470
|
+
payload.state = filters.state;
|
|
471
|
+
if (filters.city?.length)
|
|
472
|
+
payload.city = filters.city;
|
|
473
|
+
// Workflow options
|
|
474
|
+
payload.dontDisplayOwnedContact = filters.dontDisplayOwnedContact ?? true;
|
|
475
|
+
// Pagination
|
|
476
|
+
payload.limit = filters.limit ?? 25;
|
|
477
|
+
if (filters.scroll_id)
|
|
478
|
+
payload.scroll_id = filters.scroll_id;
|
|
479
|
+
return payload;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Search for contacts (FREE - no credits)
|
|
483
|
+
*/
|
|
484
|
+
async function search(filters) {
|
|
485
|
+
const payload = buildSearchPayload(filters);
|
|
486
|
+
const makeRequest = async (token) => {
|
|
487
|
+
return requestWithRetry(`${apiUrl}/search-contacts`, {
|
|
488
|
+
method: "POST",
|
|
489
|
+
headers: {
|
|
490
|
+
Authorization: `Bearer ${token}`,
|
|
491
|
+
"Content-Type": "application/json",
|
|
492
|
+
Accept: "application/json",
|
|
493
|
+
},
|
|
494
|
+
body: JSON.stringify(payload),
|
|
495
|
+
});
|
|
496
|
+
};
|
|
497
|
+
try {
|
|
498
|
+
const token = await getAuthToken();
|
|
499
|
+
return await makeRequest(token);
|
|
500
|
+
}
|
|
501
|
+
catch (err) {
|
|
502
|
+
if (err instanceof Error &&
|
|
503
|
+
err.message.includes("401") &&
|
|
504
|
+
hasCredentials) {
|
|
505
|
+
await handleAuthError();
|
|
506
|
+
try {
|
|
507
|
+
const freshToken = await getAuthToken();
|
|
508
|
+
return await makeRequest(freshToken);
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
// Retry failed
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
success: false,
|
|
516
|
+
message: err instanceof Error ? err.message : "Search failed",
|
|
517
|
+
data: { list: [], total_count: 0 },
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Fetch/enrich emails for specific contact IDs (COSTS CREDITS)
|
|
523
|
+
*/
|
|
524
|
+
async function fetch(contactIds) {
|
|
525
|
+
if (!contactIds.length) {
|
|
526
|
+
return {
|
|
527
|
+
success: true,
|
|
528
|
+
message: "No contacts to fetch",
|
|
529
|
+
data: {
|
|
530
|
+
list: [],
|
|
531
|
+
total_count: 0,
|
|
532
|
+
metrics: {
|
|
533
|
+
totalContacts: 0,
|
|
534
|
+
totalEmails: 0,
|
|
535
|
+
noEmailFound: 0,
|
|
536
|
+
invalidEmails: 0,
|
|
537
|
+
catchAllEmails: 0,
|
|
538
|
+
verifiedEmails: 0,
|
|
539
|
+
completed: 0,
|
|
540
|
+
},
|
|
541
|
+
leads_found: 0,
|
|
542
|
+
email_fetched: 0,
|
|
543
|
+
verification_status_list: [],
|
|
544
|
+
},
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
const makeRequest = async (token) => {
|
|
548
|
+
return requestWithRetry(`${apiUrl}/fetch-contacts`, {
|
|
549
|
+
method: "POST",
|
|
550
|
+
headers: {
|
|
551
|
+
Authorization: `Bearer ${token}`,
|
|
552
|
+
"Content-Type": "application/json",
|
|
553
|
+
Accept: "application/json",
|
|
554
|
+
},
|
|
555
|
+
body: JSON.stringify({ contactIds }),
|
|
556
|
+
});
|
|
557
|
+
};
|
|
558
|
+
try {
|
|
559
|
+
const token = await getAuthToken();
|
|
560
|
+
return await makeRequest(token);
|
|
561
|
+
}
|
|
562
|
+
catch (err) {
|
|
563
|
+
if (err instanceof Error &&
|
|
564
|
+
err.message.includes("401") &&
|
|
565
|
+
hasCredentials) {
|
|
566
|
+
await handleAuthError();
|
|
567
|
+
try {
|
|
568
|
+
const freshToken = await getAuthToken();
|
|
569
|
+
return await makeRequest(freshToken);
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
// Retry failed
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return {
|
|
576
|
+
success: false,
|
|
577
|
+
message: err instanceof Error ? err.message : "Fetch failed",
|
|
578
|
+
data: {
|
|
579
|
+
list: [],
|
|
580
|
+
total_count: 0,
|
|
581
|
+
metrics: {
|
|
582
|
+
totalContacts: 0,
|
|
583
|
+
totalEmails: 0,
|
|
584
|
+
noEmailFound: 0,
|
|
585
|
+
invalidEmails: 0,
|
|
586
|
+
catchAllEmails: 0,
|
|
587
|
+
verifiedEmails: 0,
|
|
588
|
+
completed: 0,
|
|
589
|
+
},
|
|
590
|
+
leads_found: 0,
|
|
591
|
+
email_fetched: 0,
|
|
592
|
+
verification_status_list: [],
|
|
593
|
+
},
|
|
594
|
+
};
|
|
239
595
|
}
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Search and immediately fetch all results
|
|
599
|
+
*/
|
|
600
|
+
async function searchAndFetch(filters) {
|
|
601
|
+
const searchResponse = await search(filters);
|
|
602
|
+
if (!searchResponse.success || searchResponse.data.list.length === 0) {
|
|
603
|
+
return {
|
|
604
|
+
searchResponse,
|
|
605
|
+
fetchResponse: null,
|
|
606
|
+
contacts: [],
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
const contactIds = searchResponse.data.list.map((c) => c.id);
|
|
610
|
+
const fetchResponse = await fetch(contactIds);
|
|
240
611
|
return {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
612
|
+
searchResponse,
|
|
613
|
+
fetchResponse,
|
|
614
|
+
contacts: fetchResponse.success ? fetchResponse.data.list : [],
|
|
244
615
|
};
|
|
245
616
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
617
|
+
/**
|
|
618
|
+
* Generic location lookup (countries, states, cities)
|
|
619
|
+
*/
|
|
620
|
+
async function getLocations(type, options = {}) {
|
|
621
|
+
const { search: searchQuery, limit = 20, offset = 0 } = options;
|
|
622
|
+
const params = new URLSearchParams();
|
|
623
|
+
params.set('limit', String(limit));
|
|
624
|
+
if (offset > 0)
|
|
625
|
+
params.set('offset', String(offset));
|
|
626
|
+
if (searchQuery)
|
|
627
|
+
params.set('search', searchQuery);
|
|
628
|
+
const makeRequest = async (token) => {
|
|
629
|
+
return requestWithRetry(`${apiUrl}/${type}?${params.toString()}`, {
|
|
630
|
+
method: "GET",
|
|
631
|
+
headers: {
|
|
632
|
+
Authorization: `Bearer ${token}`,
|
|
633
|
+
Accept: "application/json",
|
|
634
|
+
},
|
|
635
|
+
});
|
|
636
|
+
};
|
|
637
|
+
try {
|
|
638
|
+
const token = await getAuthToken();
|
|
639
|
+
return await makeRequest(token);
|
|
640
|
+
}
|
|
641
|
+
catch (err) {
|
|
642
|
+
if (err instanceof Error &&
|
|
643
|
+
err.message.includes("401") &&
|
|
644
|
+
hasCredentials) {
|
|
645
|
+
await handleAuthError();
|
|
646
|
+
try {
|
|
647
|
+
const freshToken = await getAuthToken();
|
|
648
|
+
return await makeRequest(freshToken);
|
|
649
|
+
}
|
|
650
|
+
catch {
|
|
651
|
+
// Retry failed
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
success: false,
|
|
656
|
+
message: err instanceof Error ? err.message : "Lookup failed",
|
|
657
|
+
data: [],
|
|
658
|
+
pagination: { limit, offset, page: 1, count: 0 },
|
|
659
|
+
search: searchQuery || null,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Lookup countries (for typeahead)
|
|
665
|
+
*/
|
|
666
|
+
async function getCountries(options) {
|
|
667
|
+
return getLocations('countries', options);
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Lookup states (for typeahead)
|
|
671
|
+
*/
|
|
672
|
+
async function getStates(options) {
|
|
673
|
+
return getLocations('states', options);
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Lookup cities (for typeahead)
|
|
677
|
+
*/
|
|
678
|
+
async function getCities(options) {
|
|
679
|
+
return getLocations('cities', options);
|
|
680
|
+
}
|
|
681
|
+
return {
|
|
682
|
+
search,
|
|
683
|
+
fetch,
|
|
684
|
+
searchAndFetch,
|
|
685
|
+
getCountries,
|
|
686
|
+
getStates,
|
|
687
|
+
getCities,
|
|
688
|
+
};
|
|
249
689
|
}
|