linkedin-secret-sauce 0.12.0 → 0.12.2
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 +50 -21
- package/dist/cosiall-client.d.ts +1 -1
- package/dist/cosiall-client.js +1 -1
- package/dist/enrichment/index.d.ts +3 -3
- package/dist/enrichment/index.js +19 -2
- package/dist/enrichment/matching.d.ts +29 -9
- package/dist/enrichment/matching.js +545 -142
- package/dist/enrichment/providers/bounceban.d.ts +82 -0
- package/dist/enrichment/providers/bounceban.js +447 -0
- package/dist/enrichment/providers/bouncer.d.ts +1 -1
- package/dist/enrichment/providers/bouncer.js +19 -21
- package/dist/enrichment/providers/construct.d.ts +1 -1
- package/dist/enrichment/providers/construct.js +22 -38
- package/dist/enrichment/providers/cosiall.d.ts +27 -0
- package/dist/enrichment/providers/cosiall.js +109 -0
- package/dist/enrichment/providers/dropcontact.d.ts +15 -9
- package/dist/enrichment/providers/dropcontact.js +188 -19
- package/dist/enrichment/providers/hunter.d.ts +8 -1
- package/dist/enrichment/providers/hunter.js +52 -28
- package/dist/enrichment/providers/index.d.ts +10 -7
- package/dist/enrichment/providers/index.js +12 -1
- package/dist/enrichment/providers/ldd.d.ts +1 -10
- package/dist/enrichment/providers/ldd.js +20 -97
- package/dist/enrichment/providers/smartprospect.js +28 -48
- package/dist/enrichment/providers/snovio.d.ts +1 -1
- package/dist/enrichment/providers/snovio.js +29 -31
- package/dist/enrichment/providers/trykitt.d.ts +63 -0
- package/dist/enrichment/providers/trykitt.js +210 -0
- package/dist/enrichment/types.d.ts +234 -17
- package/dist/enrichment/types.js +60 -48
- package/dist/enrichment/utils/candidate-parser.d.ts +107 -0
- package/dist/enrichment/utils/candidate-parser.js +173 -0
- package/dist/enrichment/utils/noop-provider.d.ts +39 -0
- package/dist/enrichment/utils/noop-provider.js +37 -0
- package/dist/enrichment/utils/rate-limiter.d.ts +103 -0
- package/dist/enrichment/utils/rate-limiter.js +204 -0
- package/dist/enrichment/utils/validation.d.ts +75 -3
- package/dist/enrichment/utils/validation.js +164 -11
- package/dist/linkedin-api.d.ts +40 -1
- package/dist/linkedin-api.js +160 -27
- package/dist/types.d.ts +50 -1
- package/dist/utils/lru-cache.d.ts +105 -0
- package/dist/utils/lru-cache.js +175 -0
- package/package.json +25 -26
package/dist/enrichment/types.js
CHANGED
|
@@ -8,27 +8,33 @@
|
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.SMARTPROSPECT_SUB_INDUSTRIES = exports.PROVIDER_COSTS = exports.DEFAULT_PROVIDER_ORDER = void 0;
|
|
10
10
|
/**
|
|
11
|
-
* Default provider order -
|
|
11
|
+
* Default provider order - 3-Phase Strategy
|
|
12
12
|
*
|
|
13
13
|
* PHASE 1 - Free lookups (run in parallel):
|
|
14
14
|
* - ldd: LinkedIn Data Dump - real verified emails (FREE with subscription)
|
|
15
15
|
* - smartprospect: SmartLead API - real verified emails (FREE with subscription)
|
|
16
|
+
* - cosiall: Cosiall Profile Emails - emails from LinkedIn profiles (FREE)
|
|
17
|
+
* - trykitt: TryKitt.ai - AI email finder (FREE for individuals, unlimited)
|
|
18
|
+
*
|
|
19
|
+
* PHASE 2 - Pattern guessing + verification (if Phase 1 < 80% confidence):
|
|
16
20
|
* - construct: Pattern guessing + MX check (FREE)
|
|
21
|
+
* - bounceban: BounceBan catch-all verification (FREE single, $0.003/bulk)
|
|
17
22
|
*
|
|
18
|
-
* PHASE
|
|
19
|
-
* -
|
|
20
|
-
* - snovio:
|
|
21
|
-
* - hunter: Hunter.io fallback ($0.005/email)
|
|
23
|
+
* PHASE 3 - Paid finders (only if Phase 2 inconclusive):
|
|
24
|
+
* - hunter: Hunter.io email finder ($0.005/email)
|
|
25
|
+
* - snovio: Snov.io email finder ($0.02/email)
|
|
22
26
|
*
|
|
23
|
-
* Note: dropcontact available but not in default order
|
|
27
|
+
* Note: bouncer, dropcontact available but not in default order
|
|
24
28
|
*/
|
|
25
29
|
exports.DEFAULT_PROVIDER_ORDER = [
|
|
26
30
|
"ldd",
|
|
27
31
|
"smartprospect",
|
|
32
|
+
"cosiall",
|
|
33
|
+
"trykitt",
|
|
28
34
|
"construct",
|
|
29
|
-
"
|
|
30
|
-
"snovio",
|
|
35
|
+
"bounceban",
|
|
31
36
|
"hunter",
|
|
37
|
+
"snovio",
|
|
32
38
|
];
|
|
33
39
|
/**
|
|
34
40
|
* Provider costs in USD per lookup
|
|
@@ -36,7 +42,10 @@ exports.DEFAULT_PROVIDER_ORDER = [
|
|
|
36
42
|
* Costs based on 2025 pricing:
|
|
37
43
|
* - ldd: FREE (subscription-based)
|
|
38
44
|
* - smartprospect: FREE (included in SmartLead subscription)
|
|
45
|
+
* - cosiall: FREE (uses global Cosiall config)
|
|
46
|
+
* - trykitt: FREE (unlimited for individuals, $0.005/email high volume)
|
|
39
47
|
* - construct: FREE (pattern guessing + MX check)
|
|
48
|
+
* - bounceban: FREE single / $0.003/email bulk (catch-all specialist)
|
|
40
49
|
* - bouncer: $0.006/email (SMTP verification, 99%+ accuracy)
|
|
41
50
|
* - snovio: $0.02/email (email finding + verification)
|
|
42
51
|
* - hunter: $0.005/email
|
|
@@ -46,6 +55,9 @@ exports.PROVIDER_COSTS = {
|
|
|
46
55
|
construct: 0,
|
|
47
56
|
ldd: 0,
|
|
48
57
|
smartprospect: 0,
|
|
58
|
+
cosiall: 0,
|
|
59
|
+
trykitt: 0, // FREE for individuals
|
|
60
|
+
bounceban: 0.003, // FREE single, $0.003 bulk
|
|
49
61
|
hunter: 0.005,
|
|
50
62
|
dropcontact: 0.01,
|
|
51
63
|
bouncer: 0.006,
|
|
@@ -57,46 +69,46 @@ exports.PROVIDER_COSTS = {
|
|
|
57
69
|
*/
|
|
58
70
|
exports.SMARTPROSPECT_SUB_INDUSTRIES = [
|
|
59
71
|
// Software & Internet
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
72
|
+
"Internet",
|
|
73
|
+
"Information Technology and Services",
|
|
74
|
+
"Information Services",
|
|
75
|
+
"Computer Software",
|
|
76
|
+
"Computer & Network Security",
|
|
77
|
+
"Computer Games",
|
|
66
78
|
// Real Estate & Construction
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
79
|
+
"Glass, Ceramics & Concrete",
|
|
80
|
+
"Construction",
|
|
81
|
+
"Commercial Real Estate",
|
|
82
|
+
"Civil Engineering",
|
|
83
|
+
"Building Materials",
|
|
84
|
+
"Architecture & Planning",
|
|
73
85
|
// Business Services
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
86
|
+
"Writing and Editing",
|
|
87
|
+
"Translation and Localization",
|
|
88
|
+
"Think Tanks",
|
|
89
|
+
"Staffing and Recruiting",
|
|
90
|
+
"Security and Investigations",
|
|
91
|
+
"Public Safety",
|
|
92
|
+
"Public Relations and Communications",
|
|
93
|
+
"Program Development",
|
|
94
|
+
"Professional Training & Coaching",
|
|
95
|
+
"Market Research",
|
|
96
|
+
"Marketing and Advertising",
|
|
97
|
+
"Management Consulting",
|
|
98
|
+
"Legal Services",
|
|
99
|
+
"Law Practice",
|
|
100
|
+
"Law Enforcement",
|
|
101
|
+
"International Trade and Development",
|
|
102
|
+
"Import and Export",
|
|
103
|
+
"Human Resources",
|
|
104
|
+
"Graphic Design",
|
|
105
|
+
"Facilities Services",
|
|
106
|
+
"Executive Office",
|
|
107
|
+
"Events Services",
|
|
108
|
+
"Environmental Services",
|
|
109
|
+
"Design",
|
|
110
|
+
"Business Supplies and Equipment",
|
|
111
|
+
"Animation",
|
|
112
|
+
"Alternative Dispute Resolution",
|
|
113
|
+
"Outsourcing/Offshoring",
|
|
102
114
|
];
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Candidate Parsing Utilities
|
|
3
|
+
*
|
|
4
|
+
* Extracts common fields from EnrichmentCandidate objects.
|
|
5
|
+
* Reduces code duplication across providers by centralizing field extraction logic.
|
|
6
|
+
*/
|
|
7
|
+
import type { EnrichmentCandidate } from "../types";
|
|
8
|
+
/**
|
|
9
|
+
* Parsed name components from a candidate
|
|
10
|
+
*/
|
|
11
|
+
export interface ParsedName {
|
|
12
|
+
firstName: string;
|
|
13
|
+
lastName: string;
|
|
14
|
+
fullName: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Parsed company/domain info from a candidate
|
|
18
|
+
*/
|
|
19
|
+
export interface ParsedCompany {
|
|
20
|
+
company: string | null;
|
|
21
|
+
domain: string | null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Parsed LinkedIn identifiers from a candidate
|
|
25
|
+
*/
|
|
26
|
+
export interface ParsedLinkedIn {
|
|
27
|
+
/** LinkedIn username/handle (e.g., "john-doe") */
|
|
28
|
+
username: string | null;
|
|
29
|
+
/** LinkedIn profile URL */
|
|
30
|
+
url: string | null;
|
|
31
|
+
/** Numeric LinkedIn ID (stable, never changes) */
|
|
32
|
+
numericId: string | null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Full parsed candidate with all extracted fields
|
|
36
|
+
*/
|
|
37
|
+
export interface ParsedCandidate {
|
|
38
|
+
name: ParsedName;
|
|
39
|
+
company: ParsedCompany;
|
|
40
|
+
linkedin: ParsedLinkedIn;
|
|
41
|
+
title: string | null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Extract name components from a candidate
|
|
45
|
+
*
|
|
46
|
+
* Checks multiple field naming conventions:
|
|
47
|
+
* - firstName, first_name, first
|
|
48
|
+
* - lastName, last_name, last
|
|
49
|
+
* - name, fullName, full_name (split on space)
|
|
50
|
+
*/
|
|
51
|
+
export declare function extractName(candidate: EnrichmentCandidate): ParsedName;
|
|
52
|
+
/**
|
|
53
|
+
* Extract company and domain from a candidate
|
|
54
|
+
*
|
|
55
|
+
* Checks multiple field naming conventions:
|
|
56
|
+
* - company, currentCompany, organization, org
|
|
57
|
+
* - domain, companyDomain, company_domain
|
|
58
|
+
*/
|
|
59
|
+
export declare function extractCompany(candidate: EnrichmentCandidate): ParsedCompany;
|
|
60
|
+
/**
|
|
61
|
+
* Extract LinkedIn username from a URL
|
|
62
|
+
*
|
|
63
|
+
* Handles various URL formats:
|
|
64
|
+
* - https://linkedin.com/in/john-doe
|
|
65
|
+
* - https://www.linkedin.com/in/john-doe/
|
|
66
|
+
* - linkedin.com/in/john-doe?param=value
|
|
67
|
+
*/
|
|
68
|
+
export declare function extractLinkedInUsernameFromUrl(url: string | undefined | null): string | null;
|
|
69
|
+
/**
|
|
70
|
+
* Extract numeric LinkedIn ID from various formats
|
|
71
|
+
*
|
|
72
|
+
* Handles:
|
|
73
|
+
* - Direct numeric ID: "307567"
|
|
74
|
+
* - URN format: "urn:li:member:307567"
|
|
75
|
+
* - Sales profile URN: "urn:li:fs_salesProfile:(307567,...)"
|
|
76
|
+
*/
|
|
77
|
+
export declare function extractNumericLinkedInId(input: string | undefined | null): string | null;
|
|
78
|
+
/**
|
|
79
|
+
* Extract LinkedIn identifiers from a candidate
|
|
80
|
+
*
|
|
81
|
+
* Returns username, URL, and numeric ID (if available).
|
|
82
|
+
* Numeric ID is preferred for lookups as it never changes.
|
|
83
|
+
*/
|
|
84
|
+
export declare function extractLinkedIn(candidate: EnrichmentCandidate): ParsedLinkedIn;
|
|
85
|
+
/**
|
|
86
|
+
* Extract job title from a candidate
|
|
87
|
+
*/
|
|
88
|
+
export declare function extractTitle(candidate: EnrichmentCandidate): string | null;
|
|
89
|
+
/**
|
|
90
|
+
* Parse all common fields from a candidate
|
|
91
|
+
*
|
|
92
|
+
* Use this for a complete extraction of all fields at once.
|
|
93
|
+
* For partial extraction, use the individual functions.
|
|
94
|
+
*/
|
|
95
|
+
export declare function parseCandidate(candidate: EnrichmentCandidate): ParsedCandidate;
|
|
96
|
+
/**
|
|
97
|
+
* Build a LinkedIn URL from a username/handle
|
|
98
|
+
*/
|
|
99
|
+
export declare function buildLinkedInUrl(username: string): string;
|
|
100
|
+
/**
|
|
101
|
+
* Check if a candidate has minimum required fields for email lookup
|
|
102
|
+
*
|
|
103
|
+
* @param candidate - The candidate to check
|
|
104
|
+
* @param requireDomain - Whether domain is required (default: true)
|
|
105
|
+
* @returns true if candidate has at least name and optionally domain
|
|
106
|
+
*/
|
|
107
|
+
export declare function hasMinimumFields(candidate: EnrichmentCandidate, requireDomain?: boolean): boolean;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared Candidate Parsing Utilities
|
|
4
|
+
*
|
|
5
|
+
* Extracts common fields from EnrichmentCandidate objects.
|
|
6
|
+
* Reduces code duplication across providers by centralizing field extraction logic.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.extractName = extractName;
|
|
10
|
+
exports.extractCompany = extractCompany;
|
|
11
|
+
exports.extractLinkedInUsernameFromUrl = extractLinkedInUsernameFromUrl;
|
|
12
|
+
exports.extractNumericLinkedInId = extractNumericLinkedInId;
|
|
13
|
+
exports.extractLinkedIn = extractLinkedIn;
|
|
14
|
+
exports.extractTitle = extractTitle;
|
|
15
|
+
exports.parseCandidate = parseCandidate;
|
|
16
|
+
exports.buildLinkedInUrl = buildLinkedInUrl;
|
|
17
|
+
exports.hasMinimumFields = hasMinimumFields;
|
|
18
|
+
/**
|
|
19
|
+
* Extract name components from a candidate
|
|
20
|
+
*
|
|
21
|
+
* Checks multiple field naming conventions:
|
|
22
|
+
* - firstName, first_name, first
|
|
23
|
+
* - lastName, last_name, last
|
|
24
|
+
* - name, fullName, full_name (split on space)
|
|
25
|
+
*/
|
|
26
|
+
function extractName(candidate) {
|
|
27
|
+
const rawName = candidate.name || candidate.fullName || candidate.full_name;
|
|
28
|
+
const firstName = candidate.firstName ||
|
|
29
|
+
candidate.first_name ||
|
|
30
|
+
candidate.first ||
|
|
31
|
+
rawName?.split(" ")?.[0] ||
|
|
32
|
+
"";
|
|
33
|
+
const lastName = candidate.lastName ||
|
|
34
|
+
candidate.last_name ||
|
|
35
|
+
candidate.last ||
|
|
36
|
+
rawName?.split(" ")?.slice(1).join(" ") ||
|
|
37
|
+
"";
|
|
38
|
+
const fullName = rawName || (firstName && lastName ? `${firstName} ${lastName}`.trim() : firstName);
|
|
39
|
+
return { firstName, lastName, fullName };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Extract company and domain from a candidate
|
|
43
|
+
*
|
|
44
|
+
* Checks multiple field naming conventions:
|
|
45
|
+
* - company, currentCompany, organization, org
|
|
46
|
+
* - domain, companyDomain, company_domain
|
|
47
|
+
*/
|
|
48
|
+
function extractCompany(candidate) {
|
|
49
|
+
const company = candidate.company ||
|
|
50
|
+
candidate.currentCompany ||
|
|
51
|
+
candidate.organization ||
|
|
52
|
+
candidate.org ||
|
|
53
|
+
null;
|
|
54
|
+
const domain = candidate.domain ||
|
|
55
|
+
candidate.companyDomain ||
|
|
56
|
+
candidate.company_domain ||
|
|
57
|
+
null;
|
|
58
|
+
return { company, domain };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Extract LinkedIn username from a URL
|
|
62
|
+
*
|
|
63
|
+
* Handles various URL formats:
|
|
64
|
+
* - https://linkedin.com/in/john-doe
|
|
65
|
+
* - https://www.linkedin.com/in/john-doe/
|
|
66
|
+
* - linkedin.com/in/john-doe?param=value
|
|
67
|
+
*/
|
|
68
|
+
function extractLinkedInUsernameFromUrl(url) {
|
|
69
|
+
if (!url)
|
|
70
|
+
return null;
|
|
71
|
+
const match = url.match(/linkedin\.com\/in\/([^\/\?]+)/i);
|
|
72
|
+
return match ? match[1] : null;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Extract numeric LinkedIn ID from various formats
|
|
76
|
+
*
|
|
77
|
+
* Handles:
|
|
78
|
+
* - Direct numeric ID: "307567"
|
|
79
|
+
* - URN format: "urn:li:member:307567"
|
|
80
|
+
* - Sales profile URN: "urn:li:fs_salesProfile:(307567,...)"
|
|
81
|
+
*/
|
|
82
|
+
function extractNumericLinkedInId(input) {
|
|
83
|
+
if (!input)
|
|
84
|
+
return null;
|
|
85
|
+
const trimmed = input.trim();
|
|
86
|
+
// Direct numeric ID
|
|
87
|
+
if (/^\d+$/.test(trimmed)) {
|
|
88
|
+
return trimmed;
|
|
89
|
+
}
|
|
90
|
+
// URN format: urn:li:member:307567
|
|
91
|
+
const memberMatch = trimmed.match(/urn:li:member:(\d+)/i);
|
|
92
|
+
if (memberMatch) {
|
|
93
|
+
return memberMatch[1];
|
|
94
|
+
}
|
|
95
|
+
// Sales profile URN with numeric ID
|
|
96
|
+
const salesMatch = trimmed.match(/urn:li:fs_salesProfile:\((\d+),/i);
|
|
97
|
+
if (salesMatch) {
|
|
98
|
+
return salesMatch[1];
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Extract LinkedIn identifiers from a candidate
|
|
104
|
+
*
|
|
105
|
+
* Returns username, URL, and numeric ID (if available).
|
|
106
|
+
* Numeric ID is preferred for lookups as it never changes.
|
|
107
|
+
*/
|
|
108
|
+
function extractLinkedIn(candidate) {
|
|
109
|
+
// Extract URL
|
|
110
|
+
const url = candidate.linkedinUrl || candidate.linkedin_url || null;
|
|
111
|
+
// Extract username - try direct field first, then extract from URL
|
|
112
|
+
const username = candidate.linkedinUsername ||
|
|
113
|
+
candidate.linkedin_username ||
|
|
114
|
+
candidate.linkedinHandle ||
|
|
115
|
+
candidate.linkedin_handle ||
|
|
116
|
+
extractLinkedInUsernameFromUrl(url);
|
|
117
|
+
// Extract numeric ID - check multiple fields in order of preference
|
|
118
|
+
const numericId = extractNumericLinkedInId(candidate.numericLinkedInId) ||
|
|
119
|
+
extractNumericLinkedInId(candidate.numeric_linkedin_id) ||
|
|
120
|
+
extractNumericLinkedInId(candidate.objectUrn) ||
|
|
121
|
+
extractNumericLinkedInId(candidate.object_urn) ||
|
|
122
|
+
extractNumericLinkedInId(candidate.linkedinId) ||
|
|
123
|
+
extractNumericLinkedInId(candidate.linkedin_id);
|
|
124
|
+
return { username, url, numericId };
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Extract job title from a candidate
|
|
128
|
+
*/
|
|
129
|
+
function extractTitle(candidate) {
|
|
130
|
+
return (candidate.title ||
|
|
131
|
+
candidate.currentRole ||
|
|
132
|
+
candidate.current_role ||
|
|
133
|
+
null);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Parse all common fields from a candidate
|
|
137
|
+
*
|
|
138
|
+
* Use this for a complete extraction of all fields at once.
|
|
139
|
+
* For partial extraction, use the individual functions.
|
|
140
|
+
*/
|
|
141
|
+
function parseCandidate(candidate) {
|
|
142
|
+
return {
|
|
143
|
+
name: extractName(candidate),
|
|
144
|
+
company: extractCompany(candidate),
|
|
145
|
+
linkedin: extractLinkedIn(candidate),
|
|
146
|
+
title: extractTitle(candidate),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Build a LinkedIn URL from a username/handle
|
|
151
|
+
*/
|
|
152
|
+
function buildLinkedInUrl(username) {
|
|
153
|
+
return `https://www.linkedin.com/in/${username}`;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Check if a candidate has minimum required fields for email lookup
|
|
157
|
+
*
|
|
158
|
+
* @param candidate - The candidate to check
|
|
159
|
+
* @param requireDomain - Whether domain is required (default: true)
|
|
160
|
+
* @returns true if candidate has at least name and optionally domain
|
|
161
|
+
*/
|
|
162
|
+
function hasMinimumFields(candidate, requireDomain = true) {
|
|
163
|
+
const { name, company } = parseCandidate(candidate);
|
|
164
|
+
// Must have at least first name
|
|
165
|
+
if (!name.firstName) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
// Domain required for most providers
|
|
169
|
+
if (requireDomain && !company.domain) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* No-Op Provider Factory
|
|
3
|
+
*
|
|
4
|
+
* Creates a no-op (no operation) provider function that returns null.
|
|
5
|
+
* Used when a provider is not configured (missing API key, etc.)
|
|
6
|
+
*
|
|
7
|
+
* This reduces code duplication across providers that need to return
|
|
8
|
+
* a disabled/unconfigured provider.
|
|
9
|
+
*/
|
|
10
|
+
import type { ProviderResult, ProviderName } from "../types";
|
|
11
|
+
/**
|
|
12
|
+
* Provider function type with optional name property
|
|
13
|
+
*/
|
|
14
|
+
type NoOpProviderFunc = {
|
|
15
|
+
(): Promise<ProviderResult | null>;
|
|
16
|
+
__name?: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Create a no-op provider function with the specified name
|
|
20
|
+
*
|
|
21
|
+
* The returned function:
|
|
22
|
+
* - Always returns null (no email found)
|
|
23
|
+
* - Has a `__name` property for identification in the orchestrator
|
|
24
|
+
*
|
|
25
|
+
* @param name - The provider name for identification
|
|
26
|
+
* @returns A no-op provider function
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* export function createHunterProvider(config: HunterConfig) {
|
|
31
|
+
* if (!config.apiKey) {
|
|
32
|
+
* return createNoOpProvider("hunter");
|
|
33
|
+
* }
|
|
34
|
+
* // ... actual implementation
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function createNoOpProvider(name: ProviderName): NoOpProviderFunc;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* No-Op Provider Factory
|
|
4
|
+
*
|
|
5
|
+
* Creates a no-op (no operation) provider function that returns null.
|
|
6
|
+
* Used when a provider is not configured (missing API key, etc.)
|
|
7
|
+
*
|
|
8
|
+
* This reduces code duplication across providers that need to return
|
|
9
|
+
* a disabled/unconfigured provider.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.createNoOpProvider = createNoOpProvider;
|
|
13
|
+
/**
|
|
14
|
+
* Create a no-op provider function with the specified name
|
|
15
|
+
*
|
|
16
|
+
* The returned function:
|
|
17
|
+
* - Always returns null (no email found)
|
|
18
|
+
* - Has a `__name` property for identification in the orchestrator
|
|
19
|
+
*
|
|
20
|
+
* @param name - The provider name for identification
|
|
21
|
+
* @returns A no-op provider function
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* export function createHunterProvider(config: HunterConfig) {
|
|
26
|
+
* if (!config.apiKey) {
|
|
27
|
+
* return createNoOpProvider("hunter");
|
|
28
|
+
* }
|
|
29
|
+
* // ... actual implementation
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
function createNoOpProvider(name) {
|
|
34
|
+
const noopProvider = async () => null;
|
|
35
|
+
noopProvider.__name = name;
|
|
36
|
+
return noopProvider;
|
|
37
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate Limiter for Email Enrichment Providers
|
|
3
|
+
*
|
|
4
|
+
* Tracks request rates per provider and enforces limits to avoid API throttling.
|
|
5
|
+
* Uses a sliding window approach to track requests.
|
|
6
|
+
*/
|
|
7
|
+
import type { ProviderName } from "../types";
|
|
8
|
+
/**
|
|
9
|
+
* Rate limit configuration for a provider
|
|
10
|
+
*/
|
|
11
|
+
export interface RateLimitConfig {
|
|
12
|
+
/** Maximum requests per window */
|
|
13
|
+
maxRequests: number;
|
|
14
|
+
/** Window size in milliseconds */
|
|
15
|
+
windowMs: number;
|
|
16
|
+
/** Optional minimum delay between requests in milliseconds */
|
|
17
|
+
minDelayMs?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Rate limit status for a provider
|
|
21
|
+
*/
|
|
22
|
+
export interface RateLimitStatus {
|
|
23
|
+
/** Whether the provider is currently rate limited */
|
|
24
|
+
isLimited: boolean;
|
|
25
|
+
/** Number of requests in current window */
|
|
26
|
+
requestsInWindow: number;
|
|
27
|
+
/** Maximum allowed requests */
|
|
28
|
+
maxRequests: number;
|
|
29
|
+
/** Time until rate limit resets (ms) */
|
|
30
|
+
resetInMs: number;
|
|
31
|
+
/** Recommended delay before next request (ms) */
|
|
32
|
+
recommendedDelayMs: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Default rate limits per provider
|
|
36
|
+
*
|
|
37
|
+
* Based on documented API limits:
|
|
38
|
+
* - TryKitt: 2 req/sec, 120 req/min
|
|
39
|
+
* - Hunter: 10 req/sec
|
|
40
|
+
* - Snovio: 60 req/min
|
|
41
|
+
* - BounceBan: 10 req/sec
|
|
42
|
+
* - Bouncer: 10 req/sec
|
|
43
|
+
* - Dropcontact: 5 req/sec
|
|
44
|
+
* - SmartProspect: 2 req/sec
|
|
45
|
+
*/
|
|
46
|
+
export declare const DEFAULT_RATE_LIMITS: Partial<Record<ProviderName, RateLimitConfig>>;
|
|
47
|
+
/**
|
|
48
|
+
* Rate limiter instance for tracking provider requests
|
|
49
|
+
*/
|
|
50
|
+
export declare class RateLimiter {
|
|
51
|
+
private requests;
|
|
52
|
+
private configs;
|
|
53
|
+
private lastRequestTime;
|
|
54
|
+
constructor(customConfigs?: Partial<Record<ProviderName, RateLimitConfig>>);
|
|
55
|
+
/**
|
|
56
|
+
* Record a request for a provider
|
|
57
|
+
*/
|
|
58
|
+
recordRequest(provider: ProviderName): void;
|
|
59
|
+
/**
|
|
60
|
+
* Check if a provider is rate limited
|
|
61
|
+
*/
|
|
62
|
+
isRateLimited(provider: ProviderName): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Get rate limit status for a provider
|
|
65
|
+
*/
|
|
66
|
+
getStatus(provider: ProviderName): RateLimitStatus;
|
|
67
|
+
/**
|
|
68
|
+
* Wait until rate limit allows a request
|
|
69
|
+
*
|
|
70
|
+
* @returns Promise that resolves when it's safe to make a request
|
|
71
|
+
*/
|
|
72
|
+
waitForSlot(provider: ProviderName): Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* Execute a function with rate limiting
|
|
75
|
+
*
|
|
76
|
+
* Automatically waits for rate limit slot and records the request.
|
|
77
|
+
*/
|
|
78
|
+
execute<T>(provider: ProviderName, fn: () => Promise<T>): Promise<T>;
|
|
79
|
+
/**
|
|
80
|
+
* Get all provider statuses
|
|
81
|
+
*/
|
|
82
|
+
getAllStatuses(): Map<ProviderName, RateLimitStatus>;
|
|
83
|
+
/**
|
|
84
|
+
* Reset rate limit tracking for a provider
|
|
85
|
+
*/
|
|
86
|
+
reset(provider: ProviderName): void;
|
|
87
|
+
/**
|
|
88
|
+
* Reset all rate limit tracking
|
|
89
|
+
*/
|
|
90
|
+
resetAll(): void;
|
|
91
|
+
/**
|
|
92
|
+
* Prune old records outside the window
|
|
93
|
+
*/
|
|
94
|
+
private pruneRecords;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Get the global rate limiter instance
|
|
98
|
+
*/
|
|
99
|
+
export declare function getRateLimiter(): RateLimiter;
|
|
100
|
+
/**
|
|
101
|
+
* Create a new rate limiter with custom configs
|
|
102
|
+
*/
|
|
103
|
+
export declare function createRateLimiter(configs?: Partial<Record<ProviderName, RateLimitConfig>>): RateLimiter;
|