linkedin-secret-sauce 0.11.1 → 0.12.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/cosiall-client.d.ts +40 -1
- package/dist/cosiall-client.js +142 -3
- package/dist/enrichment/index.d.ts +3 -3
- package/dist/enrichment/index.js +10 -2
- package/dist/enrichment/matching.d.ts +14 -8
- package/dist/enrichment/matching.js +175 -94
- package/dist/enrichment/providers/cosiall.d.ts +27 -0
- package/dist/enrichment/providers/cosiall.js +110 -0
- package/dist/enrichment/providers/index.d.ts +8 -7
- package/dist/enrichment/providers/index.js +3 -1
- package/dist/enrichment/types.d.ts +25 -11
- package/dist/enrichment/types.js +44 -40
- package/dist/index.d.ts +1 -0
- package/dist/types.d.ts +29 -3
- package/dist/utils/metrics.d.ts +3 -0
- package/dist/utils/metrics.js +3 -0
- package/package.json +1 -1
|
@@ -70,8 +70,8 @@ const ldd_1 = require("./providers/ldd");
|
|
|
70
70
|
function salesLeadToContact(lead) {
|
|
71
71
|
// Use firstName/lastName directly from Sales Nav - they're already separate fields
|
|
72
72
|
// This preserves exact names as shown on LinkedIn (important for SmartProspect matching)
|
|
73
|
-
const firstName = lead.firstName ||
|
|
74
|
-
const lastName = lead.lastName ||
|
|
73
|
+
const firstName = lead.firstName || "";
|
|
74
|
+
const lastName = lead.lastName || "";
|
|
75
75
|
return {
|
|
76
76
|
objectUrn: lead.objectUrn,
|
|
77
77
|
entityUrn: lead.salesProfileUrn,
|
|
@@ -100,7 +100,7 @@ function salesLeadToContact(lead) {
|
|
|
100
100
|
*/
|
|
101
101
|
function isSalesLeadSearchResult(input) {
|
|
102
102
|
// SalesLeadSearchResult has 'name' but not 'firstName'
|
|
103
|
-
return
|
|
103
|
+
return "name" in input && !("firstName" in input);
|
|
104
104
|
}
|
|
105
105
|
/**
|
|
106
106
|
* Normalize input to LinkedInContact - accepts either format
|
|
@@ -119,8 +119,8 @@ function normalizeToContact(input) {
|
|
|
119
119
|
*/
|
|
120
120
|
function normalize(str) {
|
|
121
121
|
if (!str)
|
|
122
|
-
return
|
|
123
|
-
return str.toLowerCase().trim().replace(/\s+/g,
|
|
122
|
+
return "";
|
|
123
|
+
return str.toLowerCase().trim().replace(/\s+/g, " ");
|
|
124
124
|
}
|
|
125
125
|
/**
|
|
126
126
|
* Extract company name from LinkedIn position
|
|
@@ -128,15 +128,15 @@ function normalize(str) {
|
|
|
128
128
|
function getLinkedInCompany(contact) {
|
|
129
129
|
const pos = contact.currentPositions?.[0];
|
|
130
130
|
if (!pos)
|
|
131
|
-
return
|
|
132
|
-
return pos.companyUrnResolutionResult?.name || pos.companyName ||
|
|
131
|
+
return "";
|
|
132
|
+
return pos.companyUrnResolutionResult?.name || pos.companyName || "";
|
|
133
133
|
}
|
|
134
134
|
/**
|
|
135
135
|
* Extract job title from LinkedIn position
|
|
136
136
|
*/
|
|
137
137
|
function getLinkedInTitle(contact) {
|
|
138
138
|
const pos = contact.currentPositions?.[0];
|
|
139
|
-
return pos?.title ||
|
|
139
|
+
return pos?.title || "";
|
|
140
140
|
}
|
|
141
141
|
/**
|
|
142
142
|
* Extract location parts from LinkedIn geoRegion
|
|
@@ -144,8 +144,8 @@ function getLinkedInTitle(contact) {
|
|
|
144
144
|
*/
|
|
145
145
|
function parseLinkedInLocation(geoRegion) {
|
|
146
146
|
if (!geoRegion)
|
|
147
|
-
return { city:
|
|
148
|
-
const parts = geoRegion.split(
|
|
147
|
+
return { city: "", state: "", country: "" };
|
|
148
|
+
const parts = geoRegion.split(",").map((p) => p.trim());
|
|
149
149
|
if (parts.length >= 3) {
|
|
150
150
|
return {
|
|
151
151
|
city: parts[0],
|
|
@@ -156,15 +156,15 @@ function parseLinkedInLocation(geoRegion) {
|
|
|
156
156
|
else if (parts.length === 2) {
|
|
157
157
|
return {
|
|
158
158
|
city: parts[0],
|
|
159
|
-
state:
|
|
159
|
+
state: "",
|
|
160
160
|
country: parts[1],
|
|
161
161
|
};
|
|
162
162
|
}
|
|
163
163
|
else {
|
|
164
164
|
return {
|
|
165
|
-
city:
|
|
166
|
-
state:
|
|
167
|
-
country: parts[0] ||
|
|
165
|
+
city: "",
|
|
166
|
+
state: "",
|
|
167
|
+
country: parts[0] || "",
|
|
168
168
|
};
|
|
169
169
|
}
|
|
170
170
|
}
|
|
@@ -209,7 +209,7 @@ function stringSimilarity(a, b) {
|
|
|
209
209
|
* Check if strings match exactly (normalized)
|
|
210
210
|
*/
|
|
211
211
|
function exactMatch(a, b) {
|
|
212
|
-
return normalize(a) === normalize(b) && normalize(a) !==
|
|
212
|
+
return normalize(a) === normalize(b) && normalize(a) !== "";
|
|
213
213
|
}
|
|
214
214
|
/**
|
|
215
215
|
* Check if strings match with fuzzy tolerance
|
|
@@ -245,50 +245,52 @@ function calculateMatchConfidence(linkedin, smartprospect, options = {}) {
|
|
|
245
245
|
// First name match
|
|
246
246
|
if (exactMatch(linkedin.firstName, smartprospect.firstName)) {
|
|
247
247
|
score += 20;
|
|
248
|
-
matchedFields.push(
|
|
248
|
+
matchedFields.push("firstName:exact");
|
|
249
249
|
}
|
|
250
|
-
else if (fuzzyNames &&
|
|
250
|
+
else if (fuzzyNames &&
|
|
251
|
+
fuzzyMatch(linkedin.firstName, smartprospect.firstName)) {
|
|
251
252
|
score += 15;
|
|
252
|
-
matchedFields.push(
|
|
253
|
+
matchedFields.push("firstName:fuzzy");
|
|
253
254
|
}
|
|
254
255
|
// Last name match
|
|
255
256
|
if (exactMatch(linkedin.lastName, smartprospect.lastName)) {
|
|
256
257
|
score += 20;
|
|
257
|
-
matchedFields.push(
|
|
258
|
+
matchedFields.push("lastName:exact");
|
|
258
259
|
}
|
|
259
|
-
else if (fuzzyNames &&
|
|
260
|
+
else if (fuzzyNames &&
|
|
261
|
+
fuzzyMatch(linkedin.lastName, smartprospect.lastName)) {
|
|
260
262
|
score += 15;
|
|
261
|
-
matchedFields.push(
|
|
263
|
+
matchedFields.push("lastName:fuzzy");
|
|
262
264
|
}
|
|
263
265
|
// === Company matching (up to 30 points) ===
|
|
264
266
|
const liCompany = getLinkedInCompany(linkedin);
|
|
265
|
-
const spCompany = smartprospect.company?.name ||
|
|
267
|
+
const spCompany = smartprospect.company?.name || "";
|
|
266
268
|
if (exactMatch(liCompany, spCompany)) {
|
|
267
269
|
score += 30;
|
|
268
|
-
matchedFields.push(
|
|
270
|
+
matchedFields.push("company:exact");
|
|
269
271
|
}
|
|
270
272
|
else if (fuzzyCompany && fuzzyMatch(liCompany, spCompany, 0.8)) {
|
|
271
273
|
score += 25;
|
|
272
|
-
matchedFields.push(
|
|
274
|
+
matchedFields.push("company:fuzzy");
|
|
273
275
|
}
|
|
274
276
|
else if (containsMatch(liCompany, spCompany)) {
|
|
275
277
|
score += 15;
|
|
276
|
-
matchedFields.push(
|
|
278
|
+
matchedFields.push("company:contains");
|
|
277
279
|
}
|
|
278
280
|
// === Title matching (up to 15 points) ===
|
|
279
281
|
const liTitle = getLinkedInTitle(linkedin);
|
|
280
|
-
const spTitle = smartprospect.title ||
|
|
282
|
+
const spTitle = smartprospect.title || "";
|
|
281
283
|
if (exactMatch(liTitle, spTitle)) {
|
|
282
284
|
score += 15;
|
|
283
|
-
matchedFields.push(
|
|
285
|
+
matchedFields.push("title:exact");
|
|
284
286
|
}
|
|
285
287
|
else if (fuzzyMatch(liTitle, spTitle, 0.75)) {
|
|
286
288
|
score += 12;
|
|
287
|
-
matchedFields.push(
|
|
289
|
+
matchedFields.push("title:fuzzy");
|
|
288
290
|
}
|
|
289
291
|
else if (containsMatch(liTitle, spTitle)) {
|
|
290
292
|
score += 8;
|
|
291
|
-
matchedFields.push(
|
|
293
|
+
matchedFields.push("title:contains");
|
|
292
294
|
}
|
|
293
295
|
// === Location matching (up to 15 points) ===
|
|
294
296
|
const liLocation = parseLinkedInLocation(linkedin.geoRegion);
|
|
@@ -298,17 +300,17 @@ function calculateMatchConfidence(linkedin, smartprospect, options = {}) {
|
|
|
298
300
|
// Country match
|
|
299
301
|
if (spCountry && exactMatch(liLocation.country, smartprospect.country)) {
|
|
300
302
|
score += 5;
|
|
301
|
-
matchedFields.push(
|
|
303
|
+
matchedFields.push("country:exact");
|
|
302
304
|
}
|
|
303
305
|
// State match
|
|
304
306
|
if (spState && exactMatch(liLocation.state, smartprospect.state)) {
|
|
305
307
|
score += 5;
|
|
306
|
-
matchedFields.push(
|
|
308
|
+
matchedFields.push("state:exact");
|
|
307
309
|
}
|
|
308
310
|
// City match
|
|
309
311
|
if (spCity && exactMatch(liLocation.city, smartprospect.city)) {
|
|
310
312
|
score += 5;
|
|
311
|
-
matchedFields.push(
|
|
313
|
+
matchedFields.push("city:exact");
|
|
312
314
|
}
|
|
313
315
|
return { confidence: Math.min(100, score), matchedFields };
|
|
314
316
|
}
|
|
@@ -316,22 +318,22 @@ function calculateMatchConfidence(linkedin, smartprospect, options = {}) {
|
|
|
316
318
|
* Classify match quality based on confidence and matched fields
|
|
317
319
|
*/
|
|
318
320
|
function classifyMatchQuality(confidence, matchedFields) {
|
|
319
|
-
const hasNameMatch = matchedFields.some((f) => f.startsWith(
|
|
320
|
-
matchedFields.some((f) => f.startsWith(
|
|
321
|
-
const hasCompanyMatch = matchedFields.some((f) => f.startsWith(
|
|
321
|
+
const hasNameMatch = matchedFields.some((f) => f.startsWith("firstName:exact")) &&
|
|
322
|
+
matchedFields.some((f) => f.startsWith("lastName:exact"));
|
|
323
|
+
const hasCompanyMatch = matchedFields.some((f) => f.startsWith("company:"));
|
|
322
324
|
if (confidence >= 85 && hasNameMatch && hasCompanyMatch) {
|
|
323
|
-
return
|
|
325
|
+
return "exact";
|
|
324
326
|
}
|
|
325
327
|
else if (confidence >= 70 && hasNameMatch) {
|
|
326
|
-
return
|
|
328
|
+
return "high";
|
|
327
329
|
}
|
|
328
330
|
else if (confidence >= 50) {
|
|
329
|
-
return
|
|
331
|
+
return "medium";
|
|
330
332
|
}
|
|
331
333
|
else if (confidence >= 30) {
|
|
332
|
-
return
|
|
334
|
+
return "low";
|
|
333
335
|
}
|
|
334
|
-
return
|
|
336
|
+
return "none";
|
|
335
337
|
}
|
|
336
338
|
/**
|
|
337
339
|
* Find the best matching SmartProspect contact for a LinkedIn contact
|
|
@@ -399,8 +401,8 @@ function parseLinkedInSearchResponse(elements) {
|
|
|
399
401
|
return elements.map((el) => ({
|
|
400
402
|
objectUrn: el.objectUrn,
|
|
401
403
|
entityUrn: el.entityUrn,
|
|
402
|
-
firstName: el.firstName ||
|
|
403
|
-
lastName: el.lastName ||
|
|
404
|
+
firstName: el.firstName || "",
|
|
405
|
+
lastName: el.lastName || "",
|
|
404
406
|
fullName: el.fullName || undefined,
|
|
405
407
|
geoRegion: el.geoRegion || undefined,
|
|
406
408
|
currentPositions: el.currentPositions,
|
|
@@ -457,7 +459,7 @@ async function enrichLinkedInContact(linkedInContact, smartProspectConfig, optio
|
|
|
457
459
|
numericLinkedInId,
|
|
458
460
|
matchedContact: null,
|
|
459
461
|
matchConfidence: 0,
|
|
460
|
-
matchQuality:
|
|
462
|
+
matchQuality: "none",
|
|
461
463
|
matchedFields: [],
|
|
462
464
|
email: null,
|
|
463
465
|
emailDeliverability: 0,
|
|
@@ -468,7 +470,7 @@ async function enrichLinkedInContact(linkedInContact, smartProspectConfig, optio
|
|
|
468
470
|
// Create SmartProspect client
|
|
469
471
|
const client = (0, smartprospect_1.createSmartProspectClient)(smartProspectConfig);
|
|
470
472
|
if (!client) {
|
|
471
|
-
result.error =
|
|
473
|
+
result.error = "Failed to create SmartProspect client - check credentials";
|
|
472
474
|
return result;
|
|
473
475
|
}
|
|
474
476
|
try {
|
|
@@ -509,12 +511,16 @@ async function enrichLinkedInContact(linkedInContact, smartProspectConfig, optio
|
|
|
509
511
|
}
|
|
510
512
|
}
|
|
511
513
|
if (result.allCandidates.length === 0) {
|
|
512
|
-
result.error =
|
|
514
|
+
result.error = "No candidates found in SmartProspect";
|
|
513
515
|
return result;
|
|
514
516
|
}
|
|
515
517
|
}
|
|
516
518
|
// Step 2: Find best match using intelligent matching
|
|
517
|
-
const matchResult = findBestMatch(linkedInContact, result.allCandidates, {
|
|
519
|
+
const matchResult = findBestMatch(linkedInContact, result.allCandidates, {
|
|
520
|
+
minConfidence,
|
|
521
|
+
fuzzyNames,
|
|
522
|
+
fuzzyCompany,
|
|
523
|
+
});
|
|
518
524
|
if (!matchResult) {
|
|
519
525
|
result.error = `No match above ${minConfidence}% confidence threshold`;
|
|
520
526
|
return result;
|
|
@@ -525,7 +531,9 @@ async function enrichLinkedInContact(linkedInContact, smartProspectConfig, optio
|
|
|
525
531
|
result.matchedFields = matchResult.matchedFields;
|
|
526
532
|
// Step 3: Fetch email if auto-fetch enabled and good match (COSTS CREDITS)
|
|
527
533
|
if (autoFetch && matchResult.confidence >= minConfidence) {
|
|
528
|
-
const fetchResponse = await client.fetch([
|
|
534
|
+
const fetchResponse = await client.fetch([
|
|
535
|
+
matchResult.smartProspectContact.id,
|
|
536
|
+
]);
|
|
529
537
|
if (fetchResponse.success && fetchResponse.data.list.length > 0) {
|
|
530
538
|
const enrichedContact = fetchResponse.data.list[0];
|
|
531
539
|
result.email = enrichedContact.email || null;
|
|
@@ -539,7 +547,7 @@ async function enrichLinkedInContact(linkedInContact, smartProspectConfig, optio
|
|
|
539
547
|
return result;
|
|
540
548
|
}
|
|
541
549
|
catch (err) {
|
|
542
|
-
result.error = err instanceof Error ? err.message :
|
|
550
|
+
result.error = err instanceof Error ? err.message : "Unknown error";
|
|
543
551
|
return result;
|
|
544
552
|
}
|
|
545
553
|
}
|
|
@@ -604,7 +612,7 @@ function createLinkedInEnricher(smartProspectConfig, defaultOptions = {}) {
|
|
|
604
612
|
numericLinkedInId,
|
|
605
613
|
matchedContact: null,
|
|
606
614
|
matchConfidence: 0,
|
|
607
|
-
matchQuality:
|
|
615
|
+
matchQuality: "none",
|
|
608
616
|
matchedFields: [],
|
|
609
617
|
email: null,
|
|
610
618
|
emailDeliverability: 0,
|
|
@@ -634,7 +642,9 @@ function createLinkedInEnricher(smartProspectConfig, defaultOptions = {}) {
|
|
|
634
642
|
result.allCandidates = searchResponse.data.list;
|
|
635
643
|
result.totalCandidatesFound = searchResponse.data.total_count;
|
|
636
644
|
// Broader search fallback
|
|
637
|
-
if (searchResponse.data.list.length === 0 &&
|
|
645
|
+
if (searchResponse.data.list.length === 0 &&
|
|
646
|
+
includeCompany &&
|
|
647
|
+
filters.companyName) {
|
|
638
648
|
const broaderResponse = await client.search({
|
|
639
649
|
firstName: filters.firstName,
|
|
640
650
|
lastName: filters.lastName,
|
|
@@ -646,7 +656,7 @@ function createLinkedInEnricher(smartProspectConfig, defaultOptions = {}) {
|
|
|
646
656
|
}
|
|
647
657
|
}
|
|
648
658
|
if (result.allCandidates.length === 0) {
|
|
649
|
-
result.error =
|
|
659
|
+
result.error = "No candidates found in SmartProspect";
|
|
650
660
|
return result;
|
|
651
661
|
}
|
|
652
662
|
const matchResult = findBestMatch(contact, result.allCandidates, {
|
|
@@ -663,12 +673,16 @@ function createLinkedInEnricher(smartProspectConfig, defaultOptions = {}) {
|
|
|
663
673
|
result.matchQuality = matchResult.quality;
|
|
664
674
|
result.matchedFields = matchResult.matchedFields;
|
|
665
675
|
if (autoFetch && matchResult.confidence >= minConfidence) {
|
|
666
|
-
const fetchResponse = await client.fetch([
|
|
676
|
+
const fetchResponse = await client.fetch([
|
|
677
|
+
matchResult.smartProspectContact.id,
|
|
678
|
+
]);
|
|
667
679
|
if (fetchResponse.success && fetchResponse.data.list.length > 0) {
|
|
668
680
|
const enrichedContact = fetchResponse.data.list[0];
|
|
669
681
|
result.email = enrichedContact.email || null;
|
|
670
|
-
result.emailDeliverability =
|
|
671
|
-
|
|
682
|
+
result.emailDeliverability =
|
|
683
|
+
enrichedContact.emailDeliverability || 0;
|
|
684
|
+
result.verificationStatus =
|
|
685
|
+
enrichedContact.verificationStatus || null;
|
|
672
686
|
result.matchedContact = enrichedContact;
|
|
673
687
|
}
|
|
674
688
|
}
|
|
@@ -676,7 +690,7 @@ function createLinkedInEnricher(smartProspectConfig, defaultOptions = {}) {
|
|
|
676
690
|
return result;
|
|
677
691
|
}
|
|
678
692
|
catch (err) {
|
|
679
|
-
result.error = err instanceof Error ? err.message :
|
|
693
|
+
result.error = err instanceof Error ? err.message : "Unknown error";
|
|
680
694
|
return result;
|
|
681
695
|
}
|
|
682
696
|
},
|
|
@@ -766,7 +780,7 @@ function createLinkedInEnricher(smartProspectConfig, defaultOptions = {}) {
|
|
|
766
780
|
async function getEmailsForLinkedInContact(contactOrLead, config, options = {}) {
|
|
767
781
|
// Normalize input - accept either LinkedInContact or raw SalesLeadSearchResult
|
|
768
782
|
const contact = normalizeToContact(contactOrLead);
|
|
769
|
-
const { skipLdd = false, skipSmartProspect = false, skipPatternGuessing = false, skipPaidProviders = false, minMatchConfidence = 60, paidProviderThreshold = 80, includeCompany = true, } = options;
|
|
783
|
+
const { skipLdd = false, skipSmartProspect = false, skipCosiall = false, skipPatternGuessing = false, skipPaidProviders = false, minMatchConfidence = 60, paidProviderThreshold = 80, includeCompany = true, } = options;
|
|
770
784
|
// Extract numeric ID from objectUrn
|
|
771
785
|
const numericLinkedInId = (0, ldd_1.extractNumericLinkedInId)(contact.objectUrn);
|
|
772
786
|
// Extract company domain from contact (LinkedIn data - often missing)
|
|
@@ -790,30 +804,34 @@ async function getEmailsForLinkedInContact(contactOrLead, config, options = {})
|
|
|
790
804
|
// Track company domain discovered from SmartProspect (since LinkedIn doesn't provide it)
|
|
791
805
|
let discoveredCompanyDomain = null;
|
|
792
806
|
// ==========================================================================
|
|
793
|
-
// Phase 1: FREE providers in PARALLEL (LDD + SmartProspect)
|
|
807
|
+
// Phase 1: FREE providers in PARALLEL (LDD + SmartProspect + Cosiall)
|
|
794
808
|
// ==========================================================================
|
|
795
809
|
const freeProviderPromises = [];
|
|
796
810
|
// LDD lookup (FREE)
|
|
797
811
|
if (!skipLdd && config.ldd?.apiUrl && config.ldd?.apiToken) {
|
|
798
|
-
result.providersQueried.push(
|
|
812
|
+
result.providersQueried.push("ldd");
|
|
799
813
|
freeProviderPromises.push(queryLdd(contact, config.ldd, numericLinkedInId, addEmail, result));
|
|
800
814
|
}
|
|
801
815
|
// SmartProspect lookup (FREE for FlexIQ)
|
|
802
816
|
// This also extracts company domain for pattern guessing
|
|
803
817
|
if (!skipSmartProspect && config.smartprospect) {
|
|
804
|
-
result.providersQueried.push(
|
|
805
|
-
freeProviderPromises.push(querySmartProspect(contact, config.smartprospect, minMatchConfidence, includeCompany, addEmail, result)
|
|
806
|
-
.then((domain) => {
|
|
818
|
+
result.providersQueried.push("smartprospect");
|
|
819
|
+
freeProviderPromises.push(querySmartProspect(contact, config.smartprospect, minMatchConfidence, includeCompany, addEmail, result).then((domain) => {
|
|
807
820
|
if (domain) {
|
|
808
821
|
discoveredCompanyDomain = domain;
|
|
809
822
|
}
|
|
810
823
|
}));
|
|
811
824
|
}
|
|
825
|
+
// Cosiall Profile Emails lookup (FREE)
|
|
826
|
+
if (!skipCosiall && config.cosiall?.enabled !== false) {
|
|
827
|
+
result.providersQueried.push("cosiall");
|
|
828
|
+
freeProviderPromises.push(queryCosiall(contact, addEmail, result));
|
|
829
|
+
}
|
|
812
830
|
// Wait for both free providers
|
|
813
831
|
await Promise.all(freeProviderPromises);
|
|
814
832
|
// Check if we have good enough results already
|
|
815
833
|
const bestConfidenceAfterFreeProviders = result.emails.length > 0
|
|
816
|
-
? Math.max(...result.emails.map(e => e.confidence))
|
|
834
|
+
? Math.max(...result.emails.map((e) => e.confidence))
|
|
817
835
|
: 0;
|
|
818
836
|
// ==========================================================================
|
|
819
837
|
// Phase 2: Domain Discovery (if needed for pattern guessing)
|
|
@@ -831,7 +849,7 @@ async function getEmailsForLinkedInContact(contactOrLead, config, options = {})
|
|
|
831
849
|
if (companyUrn) {
|
|
832
850
|
const companyId = extractCompanyIdFromUrn(companyUrn);
|
|
833
851
|
if (companyId) {
|
|
834
|
-
result.providersQueried.push(
|
|
852
|
+
result.providersQueried.push("linkedin");
|
|
835
853
|
try {
|
|
836
854
|
const company = await config.linkedInCompanyLookup(companyId);
|
|
837
855
|
if (company?.websiteUrl) {
|
|
@@ -842,7 +860,7 @@ async function getEmailsForLinkedInContact(contactOrLead, config, options = {})
|
|
|
842
860
|
}
|
|
843
861
|
}
|
|
844
862
|
catch (err) {
|
|
845
|
-
result.errors?.push(`LinkedIn: ${err instanceof Error ? err.message :
|
|
863
|
+
result.errors?.push(`LinkedIn: ${err instanceof Error ? err.message : "Company lookup failed"}`);
|
|
846
864
|
}
|
|
847
865
|
}
|
|
848
866
|
}
|
|
@@ -854,13 +872,15 @@ async function getEmailsForLinkedInContact(contactOrLead, config, options = {})
|
|
|
854
872
|
// ==========================================================================
|
|
855
873
|
// Phase 3: Email pattern guessing with MX verification (FREE)
|
|
856
874
|
// ==========================================================================
|
|
857
|
-
if (!skipPatternGuessing &&
|
|
858
|
-
|
|
875
|
+
if (!skipPatternGuessing &&
|
|
876
|
+
companyDomain &&
|
|
877
|
+
bestConfidenceAfterFreeProviders < paidProviderThreshold) {
|
|
878
|
+
result.providersQueried.push("pattern");
|
|
859
879
|
await queryPatternGuessing(contact, companyDomain, addEmail, result);
|
|
860
880
|
}
|
|
861
881
|
// Recalculate best confidence
|
|
862
882
|
const finalBestConfidence = result.emails.length > 0
|
|
863
|
-
? Math.max(...result.emails.map(e => e.confidence))
|
|
883
|
+
? Math.max(...result.emails.map((e) => e.confidence))
|
|
864
884
|
: 0;
|
|
865
885
|
// ==========================================================================
|
|
866
886
|
// Phase 3: PAID providers as last resort (Hunter/Bouncer/Snovio)
|
|
@@ -869,7 +889,9 @@ async function getEmailsForLinkedInContact(contactOrLead, config, options = {})
|
|
|
869
889
|
// Only use paid providers if we have low confidence or no results
|
|
870
890
|
// TODO: Implement Hunter, Bouncer, Snovio providers when needed
|
|
871
891
|
// For now, just mark that we would have queried them
|
|
872
|
-
if (config.hunter?.apiKey ||
|
|
892
|
+
if (config.hunter?.apiKey ||
|
|
893
|
+
config.bouncer?.apiKey ||
|
|
894
|
+
config.snovio?.userId) {
|
|
873
895
|
// result.providersQueried.push('hunter');
|
|
874
896
|
// result.providersQueried.push('bouncer');
|
|
875
897
|
// result.providersQueried.push('snovio');
|
|
@@ -880,11 +902,12 @@ async function getEmailsForLinkedInContact(contactOrLead, config, options = {})
|
|
|
880
902
|
const sourcePriority = {
|
|
881
903
|
ldd: 0, // Highest priority - your own data
|
|
882
904
|
smartprospect: 1,
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
905
|
+
cosiall: 2, // Cosiall Profile Emails (FREE)
|
|
906
|
+
linkedin: 3, // LinkedIn company lookup (for domain discovery, doesn't provide emails)
|
|
907
|
+
pattern: 4,
|
|
908
|
+
hunter: 5,
|
|
909
|
+
bouncer: 6,
|
|
910
|
+
snovio: 7,
|
|
888
911
|
};
|
|
889
912
|
result.emails.sort((a, b) => {
|
|
890
913
|
if (b.confidence !== a.confidence) {
|
|
@@ -920,12 +943,15 @@ function extractDomainFromUrl(url) {
|
|
|
920
943
|
if (!url)
|
|
921
944
|
return null;
|
|
922
945
|
try {
|
|
923
|
-
const fullUrl = url.startsWith(
|
|
924
|
-
return new URL(fullUrl).hostname.replace(/^www\./,
|
|
946
|
+
const fullUrl = url.startsWith("http") ? url : `https://${url}`;
|
|
947
|
+
return new URL(fullUrl).hostname.replace(/^www\./, "").toLowerCase();
|
|
925
948
|
}
|
|
926
949
|
catch {
|
|
927
950
|
// If URL parsing fails, try simple extraction
|
|
928
|
-
return url
|
|
951
|
+
return (url
|
|
952
|
+
.replace(/^(https?:\/\/)?(www\.)?/, "")
|
|
953
|
+
.split("/")[0]
|
|
954
|
+
.toLowerCase() || null);
|
|
929
955
|
}
|
|
930
956
|
}
|
|
931
957
|
/**
|
|
@@ -950,13 +976,13 @@ async function queryLdd(contact, lddConfig, numericLinkedInId, addEmail, result)
|
|
|
950
976
|
firstName: contact.firstName,
|
|
951
977
|
lastName: contact.lastName,
|
|
952
978
|
});
|
|
953
|
-
if (lddResult &&
|
|
979
|
+
if (lddResult && "emails" in lddResult && lddResult.emails.length > 0) {
|
|
954
980
|
for (const emailData of lddResult.emails) {
|
|
955
981
|
addEmail({
|
|
956
982
|
email: emailData.email,
|
|
957
|
-
source:
|
|
983
|
+
source: "ldd",
|
|
958
984
|
confidence: emailData.confidence ?? 90,
|
|
959
|
-
type: emailData.metadata?.emailTypeClassified ||
|
|
985
|
+
type: emailData.metadata?.emailTypeClassified || "unknown",
|
|
960
986
|
verified: emailData.verified ?? true,
|
|
961
987
|
metadata: emailData.metadata,
|
|
962
988
|
});
|
|
@@ -964,7 +990,55 @@ async function queryLdd(contact, lddConfig, numericLinkedInId, addEmail, result)
|
|
|
964
990
|
}
|
|
965
991
|
}
|
|
966
992
|
catch (err) {
|
|
967
|
-
result.errors?.push(`LDD: ${err instanceof Error ? err.message :
|
|
993
|
+
result.errors?.push(`LDD: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Query Cosiall Profile Emails provider
|
|
998
|
+
*/
|
|
999
|
+
async function queryCosiall(contact, addEmail, result) {
|
|
1000
|
+
try {
|
|
1001
|
+
// Import dynamically to avoid circular deps
|
|
1002
|
+
const { fetchProfileEmailsFromCosiall } = await Promise.resolve().then(() => __importStar(require("../cosiall-client")));
|
|
1003
|
+
// Build lookup parameters from contact
|
|
1004
|
+
const objectUrn = contact.objectUrn;
|
|
1005
|
+
const linkedInUrl = contact.entityUrn
|
|
1006
|
+
? undefined // entityUrn is not a URL
|
|
1007
|
+
: undefined;
|
|
1008
|
+
// Extract vanity from contact if available
|
|
1009
|
+
// (LinkedInContact doesn't have direct username field, but could be derived)
|
|
1010
|
+
// Must have at least objectUrn to query Cosiall
|
|
1011
|
+
if (!objectUrn) {
|
|
1012
|
+
return; // Silently skip - no identifier available
|
|
1013
|
+
}
|
|
1014
|
+
const cosiallResult = await fetchProfileEmailsFromCosiall({
|
|
1015
|
+
objectUrn,
|
|
1016
|
+
linkedInUrl,
|
|
1017
|
+
});
|
|
1018
|
+
if (cosiallResult.emails && cosiallResult.emails.length > 0) {
|
|
1019
|
+
for (const email of cosiallResult.emails) {
|
|
1020
|
+
addEmail({
|
|
1021
|
+
email,
|
|
1022
|
+
source: "cosiall",
|
|
1023
|
+
confidence: 85, // Good confidence - profile-associated emails
|
|
1024
|
+
type: "unknown", // Cosiall doesn't classify email type
|
|
1025
|
+
verified: true, // These are from LinkedIn profiles
|
|
1026
|
+
metadata: {
|
|
1027
|
+
profileId: cosiallResult.profileId,
|
|
1028
|
+
objectUrn: cosiallResult.objectUrn,
|
|
1029
|
+
linkedInUrl: cosiallResult.linkedInUrl,
|
|
1030
|
+
},
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
catch (err) {
|
|
1036
|
+
// Silently fail for NOT_FOUND - just means profile not in database
|
|
1037
|
+
const errorMessage = err instanceof Error ? err.message : "Unknown error";
|
|
1038
|
+
if (!errorMessage.includes("not found") &&
|
|
1039
|
+
!errorMessage.includes("NOT_FOUND")) {
|
|
1040
|
+
result.errors?.push(`Cosiall: ${errorMessage}`);
|
|
1041
|
+
}
|
|
968
1042
|
}
|
|
969
1043
|
}
|
|
970
1044
|
/**
|
|
@@ -975,7 +1049,7 @@ async function querySmartProspect(contact, smartProspectConfig, minMatchConfiden
|
|
|
975
1049
|
try {
|
|
976
1050
|
const client = (0, smartprospect_1.createSmartProspectClient)(smartProspectConfig);
|
|
977
1051
|
if (!client) {
|
|
978
|
-
result.errors?.push(
|
|
1052
|
+
result.errors?.push("SmartProspect: Failed to create client");
|
|
979
1053
|
return null;
|
|
980
1054
|
}
|
|
981
1055
|
// Build search filters
|
|
@@ -1021,21 +1095,23 @@ async function querySmartProspect(contact, smartProspectConfig, minMatchConfiden
|
|
|
1021
1095
|
if (companyWebsite) {
|
|
1022
1096
|
// Clean up the domain (remove protocol, www, trailing slashes)
|
|
1023
1097
|
discoveredDomain = companyWebsite
|
|
1024
|
-
.replace(/^(https?:\/\/)?(www\.)?/,
|
|
1025
|
-
.split(
|
|
1098
|
+
.replace(/^(https?:\/\/)?(www\.)?/, "")
|
|
1099
|
+
.split("/")[0]
|
|
1026
1100
|
.toLowerCase();
|
|
1027
1101
|
}
|
|
1028
1102
|
// Fetch email for matched contact
|
|
1029
|
-
const fetchResponse = await client.fetch([
|
|
1103
|
+
const fetchResponse = await client.fetch([
|
|
1104
|
+
matchResult.smartProspectContact.id,
|
|
1105
|
+
]);
|
|
1030
1106
|
if (fetchResponse.success && fetchResponse.data.list.length > 0) {
|
|
1031
1107
|
const enrichedContact = fetchResponse.data.list[0];
|
|
1032
1108
|
if (enrichedContact.email) {
|
|
1033
1109
|
addEmail({
|
|
1034
1110
|
email: enrichedContact.email,
|
|
1035
|
-
source:
|
|
1111
|
+
source: "smartprospect",
|
|
1036
1112
|
confidence: matchResult.confidence,
|
|
1037
|
-
type:
|
|
1038
|
-
verified: enrichedContact.verificationStatus ===
|
|
1113
|
+
type: "business",
|
|
1114
|
+
verified: enrichedContact.verificationStatus === "verified",
|
|
1039
1115
|
deliverability: enrichedContact.emailDeliverability,
|
|
1040
1116
|
metadata: {
|
|
1041
1117
|
matchQuality: matchResult.quality,
|
|
@@ -1051,7 +1127,7 @@ async function querySmartProspect(contact, smartProspectConfig, minMatchConfiden
|
|
|
1051
1127
|
return discoveredDomain;
|
|
1052
1128
|
}
|
|
1053
1129
|
catch (err) {
|
|
1054
|
-
result.errors?.push(`SmartProspect: ${err instanceof Error ? err.message :
|
|
1130
|
+
result.errors?.push(`SmartProspect: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
1055
1131
|
return null;
|
|
1056
1132
|
}
|
|
1057
1133
|
}
|
|
@@ -1061,20 +1137,25 @@ async function querySmartProspect(contact, smartProspectConfig, minMatchConfiden
|
|
|
1061
1137
|
async function queryPatternGuessing(contact, companyDomain, addEmail, result) {
|
|
1062
1138
|
try {
|
|
1063
1139
|
// Import construct provider dynamically to avoid circular deps
|
|
1064
|
-
const { createConstructProvider } = await Promise.resolve().then(() => __importStar(require(
|
|
1065
|
-
const constructProvider = createConstructProvider({
|
|
1140
|
+
const { createConstructProvider } = await Promise.resolve().then(() => __importStar(require("./providers/construct")));
|
|
1141
|
+
const constructProvider = createConstructProvider({
|
|
1142
|
+
maxAttempts: 12,
|
|
1143
|
+
timeoutMs: 3000,
|
|
1144
|
+
});
|
|
1066
1145
|
const constructResult = await constructProvider({
|
|
1067
1146
|
firstName: contact.firstName,
|
|
1068
1147
|
lastName: contact.lastName,
|
|
1069
1148
|
domain: companyDomain,
|
|
1070
1149
|
});
|
|
1071
|
-
if (constructResult &&
|
|
1150
|
+
if (constructResult &&
|
|
1151
|
+
"emails" in constructResult &&
|
|
1152
|
+
constructResult.emails.length > 0) {
|
|
1072
1153
|
for (const emailData of constructResult.emails) {
|
|
1073
1154
|
addEmail({
|
|
1074
1155
|
email: emailData.email,
|
|
1075
|
-
source:
|
|
1156
|
+
source: "pattern",
|
|
1076
1157
|
confidence: emailData.confidence ?? 50,
|
|
1077
|
-
type:
|
|
1158
|
+
type: "business",
|
|
1078
1159
|
verified: emailData.verified ?? false,
|
|
1079
1160
|
isCatchAll: emailData.isCatchAll,
|
|
1080
1161
|
metadata: emailData.metadata,
|
|
@@ -1083,7 +1164,7 @@ async function queryPatternGuessing(contact, companyDomain, addEmail, result) {
|
|
|
1083
1164
|
}
|
|
1084
1165
|
}
|
|
1085
1166
|
catch (err) {
|
|
1086
|
-
result.errors?.push(`Pattern: ${err instanceof Error ? err.message :
|
|
1167
|
+
result.errors?.push(`Pattern: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
1087
1168
|
}
|
|
1088
1169
|
}
|
|
1089
1170
|
/**
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cosiall Profile Emails Provider
|
|
3
|
+
*
|
|
4
|
+
* Lookups against the Cosiall FlexIQ Profile Emails API.
|
|
5
|
+
* This is FREE and returns all known emails for a LinkedIn profile.
|
|
6
|
+
*
|
|
7
|
+
* Lookup priority:
|
|
8
|
+
* 1. objectUrn (most precise - "urn:li:fsd_profile:ACoAABcdEfG")
|
|
9
|
+
* 2. linkedInUrl (URL like "https://www.linkedin.com/in/john-doe/")
|
|
10
|
+
* 3. vanity (username extracted from URL or direct field)
|
|
11
|
+
*/
|
|
12
|
+
import type { EnrichmentCandidate, ProviderMultiResult, ProviderResult } from "../types";
|
|
13
|
+
/**
|
|
14
|
+
* Cosiall provider configuration
|
|
15
|
+
* No configuration needed - uses Cosiall API credentials from global config
|
|
16
|
+
*/
|
|
17
|
+
export interface CosiallConfig {
|
|
18
|
+
/** Whether to enable the provider (default: true) */
|
|
19
|
+
enabled?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Create the Cosiall provider function
|
|
23
|
+
*
|
|
24
|
+
* Returns all emails found for a LinkedIn profile.
|
|
25
|
+
* Since this is a free service, it should always be executed.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createCosiallProvider(config?: CosiallConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | ProviderMultiResult | null>;
|