linkedin-secret-sauce 0.4.0 → 0.5.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/index.d.ts +43 -0
- package/dist/enrichment/index.js +231 -0
- package/dist/enrichment/orchestrator.d.ts +31 -0
- package/dist/enrichment/orchestrator.js +218 -0
- package/dist/enrichment/providers/apollo.d.ts +11 -0
- package/dist/enrichment/providers/apollo.js +136 -0
- package/dist/enrichment/providers/construct.d.ts +11 -0
- package/dist/enrichment/providers/construct.js +107 -0
- package/dist/enrichment/providers/dropcontact.d.ts +16 -0
- package/dist/enrichment/providers/dropcontact.js +37 -0
- package/dist/enrichment/providers/hunter.d.ts +11 -0
- package/dist/enrichment/providers/hunter.js +162 -0
- package/dist/enrichment/providers/index.d.ts +9 -0
- package/dist/enrichment/providers/index.js +18 -0
- package/dist/enrichment/providers/ldd.d.ts +11 -0
- package/dist/enrichment/providers/ldd.js +110 -0
- package/dist/enrichment/providers/smartprospect.d.ts +11 -0
- package/dist/enrichment/providers/smartprospect.js +249 -0
- package/dist/enrichment/types.d.ts +329 -0
- package/dist/enrichment/types.js +31 -0
- package/dist/enrichment/utils/disposable-domains.d.ts +24 -0
- package/dist/enrichment/utils/disposable-domains.js +1011 -0
- package/dist/enrichment/utils/index.d.ts +6 -0
- package/dist/enrichment/utils/index.js +22 -0
- package/dist/enrichment/utils/personal-domains.d.ts +31 -0
- package/dist/enrichment/utils/personal-domains.js +95 -0
- package/dist/enrichment/utils/validation.d.ts +42 -0
- package/dist/enrichment/utils/validation.js +130 -0
- package/dist/enrichment/verification/index.d.ts +4 -0
- package/dist/enrichment/verification/index.js +8 -0
- package/dist/enrichment/verification/mx.d.ts +16 -0
- package/dist/enrichment/verification/mx.js +168 -0
- package/dist/index.d.ts +17 -14
- package/dist/index.js +20 -1
- package/package.json +1 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Email Construction Provider
|
|
4
|
+
*
|
|
5
|
+
* Generates email pattern candidates based on name + domain, then verifies via MX check.
|
|
6
|
+
* This is a FREE provider that doesn't require any API keys.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.createConstructProvider = createConstructProvider;
|
|
10
|
+
const mx_1 = require("../verification/mx");
|
|
11
|
+
const personal_domains_1 = require("../utils/personal-domains");
|
|
12
|
+
const validation_1 = require("../utils/validation");
|
|
13
|
+
/**
|
|
14
|
+
* Build all email pattern candidates for a person
|
|
15
|
+
*/
|
|
16
|
+
function buildCandidates(input) {
|
|
17
|
+
const domain = String(input.domain || '').toLowerCase();
|
|
18
|
+
const first = (0, validation_1.cleanNamePart)(input.first || '');
|
|
19
|
+
const last = (0, validation_1.cleanNamePart)(input.last || '');
|
|
20
|
+
const fl = first ? first[0] : '';
|
|
21
|
+
const locals = [];
|
|
22
|
+
// Patterns with both first and last name
|
|
23
|
+
if (first && last) {
|
|
24
|
+
locals.push(`${first}.${last}`); // john.doe
|
|
25
|
+
locals.push(`${fl}.${last}`); // j.doe
|
|
26
|
+
locals.push(`${first}${last[0] ?? ''}`); // johnd
|
|
27
|
+
locals.push(`${fl}${last}`); // jdoe
|
|
28
|
+
locals.push(`${first}_${last}`); // john_doe
|
|
29
|
+
locals.push(`${last}.${first}`); // doe.john
|
|
30
|
+
}
|
|
31
|
+
// Single name patterns
|
|
32
|
+
if (first)
|
|
33
|
+
locals.push(first); // john
|
|
34
|
+
if (last)
|
|
35
|
+
locals.push(last); // doe
|
|
36
|
+
// Deduplicate while preserving order
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
const emails = [];
|
|
39
|
+
for (const local of locals) {
|
|
40
|
+
if (!local)
|
|
41
|
+
continue;
|
|
42
|
+
const email = `${local}@${domain}`;
|
|
43
|
+
if (!seen.has(email)) {
|
|
44
|
+
seen.add(email);
|
|
45
|
+
emails.push(email);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return emails;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Extract name components from candidate
|
|
52
|
+
*/
|
|
53
|
+
function extractNames(candidate) {
|
|
54
|
+
const firstName = candidate.firstName ||
|
|
55
|
+
candidate.first_name ||
|
|
56
|
+
candidate.first ||
|
|
57
|
+
candidate.name?.split(' ')?.[0] ||
|
|
58
|
+
'';
|
|
59
|
+
const lastName = candidate.lastName ||
|
|
60
|
+
candidate.last_name ||
|
|
61
|
+
candidate.last ||
|
|
62
|
+
candidate.name?.split(' ')?.slice(1).join(' ') ||
|
|
63
|
+
'';
|
|
64
|
+
return { first: firstName, last: lastName };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Extract domain from candidate
|
|
68
|
+
*/
|
|
69
|
+
function extractDomain(candidate) {
|
|
70
|
+
return candidate.domain || candidate.companyDomain || candidate.company_domain || '';
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Create the construct provider function
|
|
74
|
+
*/
|
|
75
|
+
function createConstructProvider(config) {
|
|
76
|
+
const maxAttempts = config?.maxAttempts ?? 8;
|
|
77
|
+
const timeoutMs = config?.timeoutMs ?? 5000;
|
|
78
|
+
async function fetchEmail(candidate) {
|
|
79
|
+
const { first, last } = extractNames(candidate);
|
|
80
|
+
const domain = extractDomain(candidate);
|
|
81
|
+
// Skip if missing required fields
|
|
82
|
+
if (!first || !domain) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
// Skip personal domains
|
|
86
|
+
if ((0, personal_domains_1.isPersonalDomain)(domain)) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const candidates = buildCandidates({ first, last, domain });
|
|
90
|
+
const max = Math.min(candidates.length, maxAttempts);
|
|
91
|
+
for (let i = 0; i < max; i++) {
|
|
92
|
+
const email = candidates[i];
|
|
93
|
+
const verification = await (0, mx_1.verifyEmailMx)(email, { timeoutMs });
|
|
94
|
+
if (verification.valid === true && verification.confidence >= 50) {
|
|
95
|
+
return {
|
|
96
|
+
email,
|
|
97
|
+
verified: true,
|
|
98
|
+
score: verification.confidence,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
// Mark provider name for orchestrator
|
|
105
|
+
fetchEmail.__name = 'construct';
|
|
106
|
+
return fetchEmail;
|
|
107
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dropcontact Provider
|
|
3
|
+
*
|
|
4
|
+
* Dropcontact API for email finding.
|
|
5
|
+
* This is a placeholder implementation - full implementation would call the Dropcontact API.
|
|
6
|
+
*/
|
|
7
|
+
import type { EnrichmentCandidate, ProviderResult, DropcontactConfig } from '../types';
|
|
8
|
+
/**
|
|
9
|
+
* Create the Dropcontact provider function
|
|
10
|
+
*
|
|
11
|
+
* Note: This is a placeholder. Full implementation would:
|
|
12
|
+
* 1. POST to https://api.dropcontact.io/batch with contacts
|
|
13
|
+
* 2. Poll for results
|
|
14
|
+
* 3. Return enriched email with verification status
|
|
15
|
+
*/
|
|
16
|
+
export declare function createDropcontactProvider(config: DropcontactConfig): (_candidate: EnrichmentCandidate) => Promise<ProviderResult | null>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Dropcontact Provider
|
|
4
|
+
*
|
|
5
|
+
* Dropcontact API for email finding.
|
|
6
|
+
* This is a placeholder implementation - full implementation would call the Dropcontact API.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.createDropcontactProvider = createDropcontactProvider;
|
|
10
|
+
/**
|
|
11
|
+
* Create the Dropcontact provider function
|
|
12
|
+
*
|
|
13
|
+
* Note: This is a placeholder. Full implementation would:
|
|
14
|
+
* 1. POST to https://api.dropcontact.io/batch with contacts
|
|
15
|
+
* 2. Poll for results
|
|
16
|
+
* 3. Return enriched email with verification status
|
|
17
|
+
*/
|
|
18
|
+
function createDropcontactProvider(config) {
|
|
19
|
+
const { apiKey } = config;
|
|
20
|
+
if (!apiKey) {
|
|
21
|
+
// Return a no-op provider if not configured
|
|
22
|
+
const noopProvider = async () => null;
|
|
23
|
+
noopProvider.__name = 'dropcontact';
|
|
24
|
+
return noopProvider;
|
|
25
|
+
}
|
|
26
|
+
async function fetchEmail(_candidate) {
|
|
27
|
+
// TODO: Implement Dropcontact API integration
|
|
28
|
+
// API: POST https://api.dropcontact.io/batch
|
|
29
|
+
// Headers: X-Access-Token: {apiKey}
|
|
30
|
+
// Body: { data: [{ first_name, last_name, company, website }], siren: false, language: "en" }
|
|
31
|
+
// For now, return null (skip this provider)
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
// Mark provider name for orchestrator
|
|
35
|
+
fetchEmail.__name = 'dropcontact';
|
|
36
|
+
return fetchEmail;
|
|
37
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hunter.io Provider
|
|
3
|
+
*
|
|
4
|
+
* Hunter.io public API for email finding.
|
|
5
|
+
* Uses email-finder endpoint with name+domain, falls back to domain-search.
|
|
6
|
+
*/
|
|
7
|
+
import type { EnrichmentCandidate, ProviderResult, HunterConfig } from "../types";
|
|
8
|
+
/**
|
|
9
|
+
* Create the Hunter provider function
|
|
10
|
+
*/
|
|
11
|
+
export declare function createHunterProvider(config: HunterConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | null>;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Hunter.io Provider
|
|
4
|
+
*
|
|
5
|
+
* Hunter.io public API for email finding.
|
|
6
|
+
* Uses email-finder endpoint with name+domain, falls back to domain-search.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.createHunterProvider = createHunterProvider;
|
|
10
|
+
const API_BASE = "https://api.hunter.io/v2";
|
|
11
|
+
/**
|
|
12
|
+
* Delay helper for retry logic
|
|
13
|
+
*/
|
|
14
|
+
async function delay(ms) {
|
|
15
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Check if value is truthy
|
|
19
|
+
*/
|
|
20
|
+
function truthy(v) {
|
|
21
|
+
return v !== undefined && v !== null && String(v).length > 0;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Map Hunter verification status to boolean
|
|
25
|
+
*/
|
|
26
|
+
function mapVerified(status) {
|
|
27
|
+
if (!status)
|
|
28
|
+
return undefined;
|
|
29
|
+
const s = String(status).toLowerCase();
|
|
30
|
+
if (s === "valid" || s === "deliverable")
|
|
31
|
+
return true;
|
|
32
|
+
if (s === "invalid" || s === "undeliverable")
|
|
33
|
+
return false;
|
|
34
|
+
return undefined; // catch-all/unknown/webmail -> leave undefined
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* HTTP request with retry on 429 rate limit
|
|
38
|
+
*/
|
|
39
|
+
async function requestWithRetry(url, retries = 1, backoffMs = 200) {
|
|
40
|
+
let lastErr;
|
|
41
|
+
for (let i = 0; i <= retries; i++) {
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetch(url);
|
|
44
|
+
// Retry on rate limit
|
|
45
|
+
if (res?.status === 429 && i < retries) {
|
|
46
|
+
await delay(backoffMs * Math.pow(2, i));
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (!res || res.status >= 400) {
|
|
50
|
+
lastErr = new Error(`hunter_http_${res?.status ?? "error"}`);
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
const json = (await res.json());
|
|
54
|
+
return json;
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
lastErr = err;
|
|
58
|
+
if (i < retries) {
|
|
59
|
+
await delay(backoffMs * Math.pow(2, i));
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
throw lastErr ?? new Error("hunter_http_error");
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Extract name and domain from candidate
|
|
68
|
+
*/
|
|
69
|
+
function extractInputs(candidate) {
|
|
70
|
+
const name = candidate.name || candidate.fullName || candidate.full_name;
|
|
71
|
+
const first = candidate.first ||
|
|
72
|
+
candidate.first_name ||
|
|
73
|
+
candidate.firstName ||
|
|
74
|
+
name?.split?.(" ")?.[0];
|
|
75
|
+
const last = candidate.last ||
|
|
76
|
+
candidate.last_name ||
|
|
77
|
+
candidate.lastName ||
|
|
78
|
+
name?.split?.(" ")?.slice(1).join(" ") ||
|
|
79
|
+
undefined;
|
|
80
|
+
const domain = candidate.domain ||
|
|
81
|
+
candidate.companyDomain ||
|
|
82
|
+
candidate.company_domain ||
|
|
83
|
+
undefined;
|
|
84
|
+
return { first, last, domain };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Create the Hunter provider function
|
|
88
|
+
*/
|
|
89
|
+
function createHunterProvider(config) {
|
|
90
|
+
const { apiKey } = config;
|
|
91
|
+
if (!apiKey) {
|
|
92
|
+
// Return a no-op provider if not configured
|
|
93
|
+
const noopProvider = async () => null;
|
|
94
|
+
noopProvider.__name = "hunter";
|
|
95
|
+
return noopProvider;
|
|
96
|
+
}
|
|
97
|
+
async function fetchEmail(candidate) {
|
|
98
|
+
const { first, last, domain } = extractInputs(candidate);
|
|
99
|
+
let url = null;
|
|
100
|
+
// Use email-finder if we have name components
|
|
101
|
+
if (truthy(first) && truthy(last) && truthy(domain)) {
|
|
102
|
+
const qs = new URLSearchParams({
|
|
103
|
+
api_key: String(apiKey),
|
|
104
|
+
domain: String(domain),
|
|
105
|
+
first_name: String(first),
|
|
106
|
+
last_name: String(last),
|
|
107
|
+
});
|
|
108
|
+
url = `${API_BASE}/email-finder?${qs.toString()}`;
|
|
109
|
+
}
|
|
110
|
+
// Fall back to domain-search if only domain available
|
|
111
|
+
else if (truthy(domain)) {
|
|
112
|
+
const qs = new URLSearchParams({
|
|
113
|
+
api_key: String(apiKey),
|
|
114
|
+
domain: String(domain),
|
|
115
|
+
limit: "1",
|
|
116
|
+
});
|
|
117
|
+
url = `${API_BASE}/domain-search?${qs.toString()}`;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
return null; // Can't search without domain
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const json = await requestWithRetry(url, 1, 100);
|
|
124
|
+
// Parse email-finder response shape
|
|
125
|
+
const ef = (json && (json.data || json.result));
|
|
126
|
+
if (ef && (ef.email || ef.score !== undefined)) {
|
|
127
|
+
const email = ef.email ?? null;
|
|
128
|
+
const score = typeof ef.score === "number"
|
|
129
|
+
? ef.score
|
|
130
|
+
: Number(ef.score ?? 0) || undefined;
|
|
131
|
+
const verified = mapVerified(ef?.verification?.status ?? ef?.status);
|
|
132
|
+
if (!email)
|
|
133
|
+
return null;
|
|
134
|
+
return { email, verified, score };
|
|
135
|
+
}
|
|
136
|
+
// Parse domain-search response shape
|
|
137
|
+
const ds = (json &&
|
|
138
|
+
json.data &&
|
|
139
|
+
Array.isArray(json.data.emails)
|
|
140
|
+
? json.data.emails
|
|
141
|
+
: null);
|
|
142
|
+
if (ds && ds.length > 0) {
|
|
143
|
+
const firstHit = ds[0];
|
|
144
|
+
const email = firstHit?.value || firstHit?.email || null;
|
|
145
|
+
const score = typeof firstHit?.confidence === "number"
|
|
146
|
+
? firstHit.confidence
|
|
147
|
+
: Number(firstHit?.confidence ?? 0) || undefined;
|
|
148
|
+
const verified = mapVerified(firstHit?.verification?.status ?? firstHit?.status);
|
|
149
|
+
if (!email)
|
|
150
|
+
return null;
|
|
151
|
+
return { email, verified, score };
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Mark provider name for orchestrator
|
|
160
|
+
fetchEmail.__name = "hunter";
|
|
161
|
+
return fetchEmail;
|
|
162
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email Enrichment Providers
|
|
3
|
+
*/
|
|
4
|
+
export { createConstructProvider } from './construct';
|
|
5
|
+
export { createLddProvider } from './ldd';
|
|
6
|
+
export { createSmartProspectProvider } from './smartprospect';
|
|
7
|
+
export { createHunterProvider } from './hunter';
|
|
8
|
+
export { createApolloProvider } from './apollo';
|
|
9
|
+
export { createDropcontactProvider } from './dropcontact';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Email Enrichment Providers
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createDropcontactProvider = exports.createApolloProvider = exports.createHunterProvider = exports.createSmartProspectProvider = exports.createLddProvider = exports.createConstructProvider = void 0;
|
|
7
|
+
var construct_1 = require("./construct");
|
|
8
|
+
Object.defineProperty(exports, "createConstructProvider", { enumerable: true, get: function () { return construct_1.createConstructProvider; } });
|
|
9
|
+
var ldd_1 = require("./ldd");
|
|
10
|
+
Object.defineProperty(exports, "createLddProvider", { enumerable: true, get: function () { return ldd_1.createLddProvider; } });
|
|
11
|
+
var smartprospect_1 = require("./smartprospect");
|
|
12
|
+
Object.defineProperty(exports, "createSmartProspectProvider", { enumerable: true, get: function () { return smartprospect_1.createSmartProspectProvider; } });
|
|
13
|
+
var hunter_1 = require("./hunter");
|
|
14
|
+
Object.defineProperty(exports, "createHunterProvider", { enumerable: true, get: function () { return hunter_1.createHunterProvider; } });
|
|
15
|
+
var apollo_1 = require("./apollo");
|
|
16
|
+
Object.defineProperty(exports, "createApolloProvider", { enumerable: true, get: function () { return apollo_1.createApolloProvider; } });
|
|
17
|
+
var dropcontact_1 = require("./dropcontact");
|
|
18
|
+
Object.defineProperty(exports, "createDropcontactProvider", { enumerable: true, get: function () { return dropcontact_1.createDropcontactProvider; } });
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LinkedIn Data Dump (LDD) Provider
|
|
3
|
+
*
|
|
4
|
+
* YOUR private database of ~500M scraped LinkedIn profiles with emails.
|
|
5
|
+
* This is FREE and unlimited - it's your own service.
|
|
6
|
+
*/
|
|
7
|
+
import type { EnrichmentCandidate, ProviderResult, LddConfig } from "../types";
|
|
8
|
+
/**
|
|
9
|
+
* Create the LDD provider function
|
|
10
|
+
*/
|
|
11
|
+
export declare function createLddProvider(config: LddConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | null>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* LinkedIn Data Dump (LDD) Provider
|
|
4
|
+
*
|
|
5
|
+
* YOUR private database of ~500M scraped LinkedIn profiles with emails.
|
|
6
|
+
* This is FREE and unlimited - it's your own service.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.createLddProvider = createLddProvider;
|
|
10
|
+
const validation_1 = require("../utils/validation");
|
|
11
|
+
/**
|
|
12
|
+
* Delay helper for retry logic
|
|
13
|
+
*/
|
|
14
|
+
async function delay(ms) {
|
|
15
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* HTTP request with retry on 429 rate limit
|
|
19
|
+
*/
|
|
20
|
+
async function requestWithRetry(url, token, retries = 1, backoffMs = 200) {
|
|
21
|
+
let lastErr;
|
|
22
|
+
for (let i = 0; i <= retries; i++) {
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch(url, {
|
|
25
|
+
method: "GET",
|
|
26
|
+
headers: {
|
|
27
|
+
Authorization: `Bearer ${token}`,
|
|
28
|
+
"Content-Type": "application/json",
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
// Retry on rate limit
|
|
32
|
+
if (res?.status === 429 && i < retries) {
|
|
33
|
+
await delay(backoffMs * Math.pow(2, i));
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
return res;
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
lastErr = err;
|
|
40
|
+
if (i < retries) {
|
|
41
|
+
await delay(backoffMs * Math.pow(2, i));
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
throw lastErr ?? new Error("ldd_http_error");
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Extract LinkedIn username from candidate
|
|
50
|
+
*/
|
|
51
|
+
function extractUsername(candidate) {
|
|
52
|
+
// Direct username
|
|
53
|
+
const directUsername = candidate.linkedinUsername || candidate.linkedin_username;
|
|
54
|
+
if (directUsername)
|
|
55
|
+
return directUsername;
|
|
56
|
+
// Extract from URL
|
|
57
|
+
const url = candidate.linkedinUrl || candidate.linkedin_url;
|
|
58
|
+
if (url) {
|
|
59
|
+
const extracted = (0, validation_1.extractLinkedInUsername)(url);
|
|
60
|
+
if (extracted)
|
|
61
|
+
return extracted;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Create the LDD provider function
|
|
67
|
+
*/
|
|
68
|
+
function createLddProvider(config) {
|
|
69
|
+
const { apiUrl, apiToken } = config;
|
|
70
|
+
if (!apiUrl || !apiToken) {
|
|
71
|
+
// Return a no-op provider if not configured
|
|
72
|
+
const noopProvider = async () => null;
|
|
73
|
+
noopProvider.__name = "ldd";
|
|
74
|
+
return noopProvider;
|
|
75
|
+
}
|
|
76
|
+
async function fetchEmail(candidate) {
|
|
77
|
+
const username = extractUsername(candidate);
|
|
78
|
+
if (!username) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const endpoint = `${apiUrl}/api/v1/profiles/by-username/${encodeURIComponent(username)}`;
|
|
83
|
+
const response = await requestWithRetry(endpoint, apiToken, 1, 100);
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const data = (await response.json());
|
|
88
|
+
if (!data.success || !data.data) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
// Find first valid email
|
|
92
|
+
const emails = data.data.emails || [];
|
|
93
|
+
const validEmail = emails.find((e) => e.email_address && e.email_address.includes("@"));
|
|
94
|
+
if (!validEmail) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
email: validEmail.email_address,
|
|
99
|
+
verified: true, // LDD data is pre-verified
|
|
100
|
+
score: 90, // High confidence from your own database
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Mark provider name for orchestrator
|
|
108
|
+
fetchEmail.__name = "ldd";
|
|
109
|
+
return fetchEmail;
|
|
110
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SmartProspect/Smartlead Provider
|
|
3
|
+
*
|
|
4
|
+
* Smartlead's prospect database - a REVERSE-ENGINEERED private API.
|
|
5
|
+
* Two-phase process: search (free) then fetch (costs credits).
|
|
6
|
+
*/
|
|
7
|
+
import type { EnrichmentCandidate, ProviderResult, SmartProspectConfig } from '../types';
|
|
8
|
+
/**
|
|
9
|
+
* Create the SmartProspect provider function
|
|
10
|
+
*/
|
|
11
|
+
export declare function createSmartProspectProvider(config: SmartProspectConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | null>;
|