linkedin-secret-sauce 0.12.1 → 0.12.3
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 +339 -31
- package/dist/cosiall-client.d.ts +1 -1
- package/dist/cosiall-client.js +1 -1
- package/dist/enrichment/index.d.ts +23 -2
- package/dist/enrichment/index.js +38 -22
- package/dist/enrichment/matching.d.ts +16 -2
- package/dist/enrichment/matching.js +387 -65
- 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 +1 -1
- package/dist/enrichment/providers/cosiall.js +3 -4
- 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 +2 -0
- package/dist/enrichment/providers/index.js +10 -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 +220 -7
- package/dist/enrichment/types.js +16 -8
- 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/docs/COSIALL_PROFILE_EMAILS.md +342 -0
- package/docs/ENRICHMENT.md +622 -0
- package/docs/INTEGRATION.md +405 -0
- package/docs/PLAYGROUND.md +558 -0
- package/docs/SALES_SEARCH.md +171 -0
- package/docs/api/.nojekyll +1 -0
- package/docs/api/assets/hierarchy.js +1 -0
- package/docs/api/assets/highlight.css +92 -0
- package/docs/api/assets/icons.js +18 -0
- package/docs/api/assets/icons.svg +1 -0
- package/docs/api/assets/main.js +60 -0
- package/docs/api/assets/navigation.js +1 -0
- package/docs/api/assets/search.js +1 -0
- package/docs/api/assets/style.css +1633 -0
- package/docs/api/classes/LinkedInClientError.html +37 -0
- package/docs/api/functions/_testGetAccountCookies.html +4 -0
- package/docs/api/functions/_testGetAccountEntry.html +4 -0
- package/docs/api/functions/_testGetAllAccountIds.html +3 -0
- package/docs/api/functions/_testGetPoolState.html +3 -0
- package/docs/api/functions/adminResetAccount.html +1 -0
- package/docs/api/functions/adminSetCooldown.html +1 -0
- package/docs/api/functions/buildCookieHeader.html +1 -0
- package/docs/api/functions/clearAllSmartLeadTokens.html +2 -0
- package/docs/api/functions/clearRequestHistory.html +1 -0
- package/docs/api/functions/clearSessionAccount.html +1 -0
- package/docs/api/functions/clearSmartLeadToken.html +2 -0
- package/docs/api/functions/createEnrichmentClient.html +8 -0
- package/docs/api/functions/extractCsrfToken.html +1 -0
- package/docs/api/functions/extractLinkedInHandle.html +7 -0
- package/docs/api/functions/fetchCookiesFromCosiall.html +14 -0
- package/docs/api/functions/fetchProfileEmailsFromCosiall.html +18 -0
- package/docs/api/functions/forceRefreshCookies.html +1 -0
- package/docs/api/functions/getAccountForSession.html +1 -0
- package/docs/api/functions/getAccountsSummary.html +1 -0
- package/docs/api/functions/getCompaniesBatch.html +5 -0
- package/docs/api/functions/getCompanyById.html +9 -0
- package/docs/api/functions/getCompanyByUrl.html +1 -0
- package/docs/api/functions/getConfig.html +1 -0
- package/docs/api/functions/getCookiePoolHealth.html +1 -0
- package/docs/api/functions/getProfileByUrn.html +17 -0
- package/docs/api/functions/getProfileByVanity.html +10 -0
- package/docs/api/functions/getProfilesBatch.html +1 -0
- package/docs/api/functions/getRequestHistory.html +1 -0
- package/docs/api/functions/getSalesNavigatorProfileDetails.html +1 -0
- package/docs/api/functions/getSalesNavigatorProfileFull.html +16 -0
- package/docs/api/functions/getSmartLeadToken.html +1 -0
- package/docs/api/functions/getSmartLeadTokenCacheStats.html +2 -0
- package/docs/api/functions/getSmartLeadUser.html +2 -0
- package/docs/api/functions/getSnapshot.html +1 -0
- package/docs/api/functions/getYearsAtCompanyOptions.html +2 -0
- package/docs/api/functions/getYearsInPositionOptions.html +2 -0
- package/docs/api/functions/getYearsOfExperienceOptions.html +2 -0
- package/docs/api/functions/incrementMetric.html +1 -0
- package/docs/api/functions/initializeCookiePool.html +1 -0
- package/docs/api/functions/initializeLinkedInClient.html +1 -0
- package/docs/api/functions/isBusinessEmail.html +4 -0
- package/docs/api/functions/isDisposableDomain.html +4 -0
- package/docs/api/functions/isDisposableEmail.html +4 -0
- package/docs/api/functions/isPersonalDomain.html +4 -0
- package/docs/api/functions/isPersonalEmail.html +4 -0
- package/docs/api/functions/isRoleAccount.html +4 -0
- package/docs/api/functions/isValidEmailSyntax.html +4 -0
- package/docs/api/functions/parseFullProfile.html +15 -0
- package/docs/api/functions/parseSalesSearchResults.html +1 -0
- package/docs/api/functions/reportAccountFailure.html +1 -0
- package/docs/api/functions/reportAccountSuccess.html +1 -0
- package/docs/api/functions/resolveCompanyUniversalName.html +1 -0
- package/docs/api/functions/searchSalesLeads.html +16 -0
- package/docs/api/functions/selectAccountForRequest.html +1 -0
- package/docs/api/functions/setAccountForSession.html +1 -0
- package/docs/api/functions/typeahead.html +1 -0
- package/docs/api/functions/verifyEmailMx.html +1 -0
- package/docs/api/hierarchy.html +1 -0
- package/docs/api/index.html +12 -0
- package/docs/api/interfaces/AccountCookies.html +4 -0
- package/docs/api/interfaces/BatchEnrichmentOptions.html +14 -0
- package/docs/api/interfaces/CacheAdapter.html +6 -0
- package/docs/api/interfaces/CanonicalEmail.html +14 -0
- package/docs/api/interfaces/Company.html +17 -0
- package/docs/api/interfaces/ConstructConfig.html +8 -0
- package/docs/api/interfaces/CosiallProfileEmailsResponse.html +11 -0
- package/docs/api/interfaces/DropcontactConfig.html +3 -0
- package/docs/api/interfaces/EnrichmentCandidate.html +34 -0
- package/docs/api/interfaces/EnrichmentClient.html +10 -0
- package/docs/api/interfaces/EnrichmentClientConfig.html +12 -0
- package/docs/api/interfaces/EnrichmentLogger.html +6 -0
- package/docs/api/interfaces/EnrichmentOptions.html +10 -0
- package/docs/api/interfaces/HunterConfig.html +3 -0
- package/docs/api/interfaces/LddConfig.html +4 -0
- package/docs/api/interfaces/LddProfileData.html +6 -0
- package/docs/api/interfaces/LinkedInClientConfig.html +20 -0
- package/docs/api/interfaces/LinkedInCookie.html +9 -0
- package/docs/api/interfaces/LinkedInPosition.html +14 -0
- package/docs/api/interfaces/LinkedInProfile.html +21 -0
- package/docs/api/interfaces/LinkedInSpotlightBadge.html +5 -0
- package/docs/api/interfaces/LinkedInTenure.html +3 -0
- package/docs/api/interfaces/Metrics.html +22 -0
- package/docs/api/interfaces/MetricsSnapshot.html +23 -0
- package/docs/api/interfaces/ProfileEducation.html +8 -0
- package/docs/api/interfaces/ProfileEmailsLookupOptions.html +9 -0
- package/docs/api/interfaces/ProfilePosition.html +12 -0
- package/docs/api/interfaces/ProfileSkill.html +3 -0
- package/docs/api/interfaces/ProviderResult.html +11 -0
- package/docs/api/interfaces/ProvidersConfig.html +17 -0
- package/docs/api/interfaces/RequestHistoryEntry.html +8 -0
- package/docs/api/interfaces/SalesLeadSearchResult.html +31 -0
- package/docs/api/interfaces/SalesNavigatorContactInfo.html +5 -0
- package/docs/api/interfaces/SalesNavigatorPosition.html +11 -0
- package/docs/api/interfaces/SalesNavigatorProfile.html +9 -0
- package/docs/api/interfaces/SalesNavigatorProfileFull.html +24 -0
- package/docs/api/interfaces/SearchSalesResult.html +5 -0
- package/docs/api/interfaces/SmartLeadAuthConfig.html +6 -0
- package/docs/api/interfaces/SmartLeadCredentials.html +3 -0
- package/docs/api/interfaces/SmartLeadLoginResponse.html +3 -0
- package/docs/api/interfaces/SmartLeadUser.html +8 -0
- package/docs/api/interfaces/SmartProspectConfig.html +19 -0
- package/docs/api/interfaces/SmartProspectContact.html +24 -0
- package/docs/api/interfaces/SmartProspectSearchFilters.html +42 -0
- package/docs/api/interfaces/TypeaheadItem.html +3 -0
- package/docs/api/interfaces/TypeaheadResult.html +3 -0
- package/docs/api/interfaces/VerificationResult.html +16 -0
- package/docs/api/types/CostCallback.html +2 -0
- package/docs/api/types/Geo.html +4 -0
- package/docs/api/types/LddApiResponse.html +1 -0
- package/docs/api/types/ProviderFunc.html +2 -0
- package/docs/api/types/ProviderName.html +2 -0
- package/docs/api/types/SalesSearchFilters.html +7 -0
- package/docs/api/types/TypeaheadType.html +1 -0
- package/docs/api/variables/COMPANY_SIZE_OPTIONS.html +1 -0
- package/docs/api/variables/DEFAULT_PROVIDER_ORDER.html +20 -0
- package/docs/api/variables/DISPOSABLE_DOMAINS.html +2 -0
- package/docs/api/variables/FUNCTION_OPTIONS.html +1 -0
- package/docs/api/variables/INDUSTRY_OPTIONS.html +1 -0
- package/docs/api/variables/LANGUAGE_OPTIONS.html +1 -0
- package/docs/api/variables/PERSONAL_DOMAINS.html +2 -0
- package/docs/api/variables/PROVIDER_COSTS.html +15 -0
- package/docs/api/variables/REGION_OPTIONS.html +1 -0
- package/docs/api/variables/SENIORITY_OPTIONS.html +3 -0
- package/docs/api/variables/YEARS_OPTIONS.html +1 -0
- package/docs/index.html +98 -0
- package/package.json +16 -5
|
@@ -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;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Rate Limiter for Email Enrichment Providers
|
|
4
|
+
*
|
|
5
|
+
* Tracks request rates per provider and enforces limits to avoid API throttling.
|
|
6
|
+
* Uses a sliding window approach to track requests.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.RateLimiter = exports.DEFAULT_RATE_LIMITS = void 0;
|
|
10
|
+
exports.getRateLimiter = getRateLimiter;
|
|
11
|
+
exports.createRateLimiter = createRateLimiter;
|
|
12
|
+
/**
|
|
13
|
+
* Default rate limits per provider
|
|
14
|
+
*
|
|
15
|
+
* Based on documented API limits:
|
|
16
|
+
* - TryKitt: 2 req/sec, 120 req/min
|
|
17
|
+
* - Hunter: 10 req/sec
|
|
18
|
+
* - Snovio: 60 req/min
|
|
19
|
+
* - BounceBan: 10 req/sec
|
|
20
|
+
* - Bouncer: 10 req/sec
|
|
21
|
+
* - Dropcontact: 5 req/sec
|
|
22
|
+
* - SmartProspect: 2 req/sec
|
|
23
|
+
*/
|
|
24
|
+
exports.DEFAULT_RATE_LIMITS = {
|
|
25
|
+
trykitt: { maxRequests: 120, windowMs: 60000, minDelayMs: 500 },
|
|
26
|
+
hunter: { maxRequests: 10, windowMs: 1000, minDelayMs: 100 },
|
|
27
|
+
snovio: { maxRequests: 60, windowMs: 60000, minDelayMs: 1000 },
|
|
28
|
+
bounceban: { maxRequests: 10, windowMs: 1000, minDelayMs: 100 },
|
|
29
|
+
bouncer: { maxRequests: 10, windowMs: 1000, minDelayMs: 100 },
|
|
30
|
+
dropcontact: { maxRequests: 5, windowMs: 1000, minDelayMs: 200 },
|
|
31
|
+
smartprospect: { maxRequests: 2, windowMs: 1000, minDelayMs: 500 },
|
|
32
|
+
// Free providers have no external limits
|
|
33
|
+
construct: { maxRequests: 100, windowMs: 1000, minDelayMs: 0 },
|
|
34
|
+
ldd: { maxRequests: 100, windowMs: 1000, minDelayMs: 0 },
|
|
35
|
+
cosiall: { maxRequests: 10, windowMs: 1000, minDelayMs: 100 },
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Rate limiter instance for tracking provider requests
|
|
39
|
+
*/
|
|
40
|
+
class RateLimiter {
|
|
41
|
+
requests = new Map();
|
|
42
|
+
configs = new Map();
|
|
43
|
+
lastRequestTime = new Map();
|
|
44
|
+
constructor(customConfigs) {
|
|
45
|
+
// Initialize with default configs
|
|
46
|
+
for (const [provider, config] of Object.entries(exports.DEFAULT_RATE_LIMITS)) {
|
|
47
|
+
this.configs.set(provider, config);
|
|
48
|
+
}
|
|
49
|
+
// Override with custom configs
|
|
50
|
+
if (customConfigs) {
|
|
51
|
+
for (const [provider, config] of Object.entries(customConfigs)) {
|
|
52
|
+
if (config) {
|
|
53
|
+
this.configs.set(provider, config);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Record a request for a provider
|
|
60
|
+
*/
|
|
61
|
+
recordRequest(provider) {
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
const records = this.requests.get(provider) || [];
|
|
64
|
+
records.push({ timestamp: now });
|
|
65
|
+
this.requests.set(provider, records);
|
|
66
|
+
this.lastRequestTime.set(provider, now);
|
|
67
|
+
// Prune old records
|
|
68
|
+
this.pruneRecords(provider);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Check if a provider is rate limited
|
|
72
|
+
*/
|
|
73
|
+
isRateLimited(provider) {
|
|
74
|
+
return this.getStatus(provider).isLimited;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get rate limit status for a provider
|
|
78
|
+
*/
|
|
79
|
+
getStatus(provider) {
|
|
80
|
+
const config = this.configs.get(provider);
|
|
81
|
+
if (!config) {
|
|
82
|
+
return {
|
|
83
|
+
isLimited: false,
|
|
84
|
+
requestsInWindow: 0,
|
|
85
|
+
maxRequests: Infinity,
|
|
86
|
+
resetInMs: 0,
|
|
87
|
+
recommendedDelayMs: 0,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
this.pruneRecords(provider);
|
|
91
|
+
const records = this.requests.get(provider) || [];
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
// Calculate requests in current window
|
|
94
|
+
const windowStart = now - config.windowMs;
|
|
95
|
+
const requestsInWindow = records.filter((r) => r.timestamp >= windowStart).length;
|
|
96
|
+
// Check if limited
|
|
97
|
+
const isLimited = requestsInWindow >= config.maxRequests;
|
|
98
|
+
// Calculate reset time
|
|
99
|
+
let resetInMs = 0;
|
|
100
|
+
if (isLimited && records.length > 0) {
|
|
101
|
+
const oldestInWindow = records.find((r) => r.timestamp >= windowStart);
|
|
102
|
+
if (oldestInWindow) {
|
|
103
|
+
resetInMs = oldestInWindow.timestamp + config.windowMs - now;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Calculate recommended delay
|
|
107
|
+
let recommendedDelayMs = config.minDelayMs || 0;
|
|
108
|
+
if (isLimited) {
|
|
109
|
+
recommendedDelayMs = Math.max(recommendedDelayMs, resetInMs);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
// Check minimum delay since last request
|
|
113
|
+
const lastRequest = this.lastRequestTime.get(provider) || 0;
|
|
114
|
+
const timeSinceLastRequest = now - lastRequest;
|
|
115
|
+
if (timeSinceLastRequest < (config.minDelayMs || 0)) {
|
|
116
|
+
recommendedDelayMs = (config.minDelayMs || 0) - timeSinceLastRequest;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
isLimited,
|
|
121
|
+
requestsInWindow,
|
|
122
|
+
maxRequests: config.maxRequests,
|
|
123
|
+
resetInMs: Math.max(0, resetInMs),
|
|
124
|
+
recommendedDelayMs: Math.max(0, recommendedDelayMs),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Wait until rate limit allows a request
|
|
129
|
+
*
|
|
130
|
+
* @returns Promise that resolves when it's safe to make a request
|
|
131
|
+
*/
|
|
132
|
+
async waitForSlot(provider) {
|
|
133
|
+
const status = this.getStatus(provider);
|
|
134
|
+
if (status.recommendedDelayMs > 0) {
|
|
135
|
+
await new Promise((resolve) => setTimeout(resolve, status.recommendedDelayMs));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Execute a function with rate limiting
|
|
140
|
+
*
|
|
141
|
+
* Automatically waits for rate limit slot and records the request.
|
|
142
|
+
*/
|
|
143
|
+
async execute(provider, fn) {
|
|
144
|
+
await this.waitForSlot(provider);
|
|
145
|
+
this.recordRequest(provider);
|
|
146
|
+
return fn();
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Get all provider statuses
|
|
150
|
+
*/
|
|
151
|
+
getAllStatuses() {
|
|
152
|
+
const statuses = new Map();
|
|
153
|
+
for (const provider of this.configs.keys()) {
|
|
154
|
+
statuses.set(provider, this.getStatus(provider));
|
|
155
|
+
}
|
|
156
|
+
return statuses;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Reset rate limit tracking for a provider
|
|
160
|
+
*/
|
|
161
|
+
reset(provider) {
|
|
162
|
+
this.requests.delete(provider);
|
|
163
|
+
this.lastRequestTime.delete(provider);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Reset all rate limit tracking
|
|
167
|
+
*/
|
|
168
|
+
resetAll() {
|
|
169
|
+
this.requests.clear();
|
|
170
|
+
this.lastRequestTime.clear();
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Prune old records outside the window
|
|
174
|
+
*/
|
|
175
|
+
pruneRecords(provider) {
|
|
176
|
+
const config = this.configs.get(provider);
|
|
177
|
+
if (!config)
|
|
178
|
+
return;
|
|
179
|
+
const records = this.requests.get(provider);
|
|
180
|
+
if (!records)
|
|
181
|
+
return;
|
|
182
|
+
const windowStart = Date.now() - config.windowMs;
|
|
183
|
+
const pruned = records.filter((r) => r.timestamp >= windowStart);
|
|
184
|
+
this.requests.set(provider, pruned);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
exports.RateLimiter = RateLimiter;
|
|
188
|
+
// Global rate limiter instance
|
|
189
|
+
let globalRateLimiter = null;
|
|
190
|
+
/**
|
|
191
|
+
* Get the global rate limiter instance
|
|
192
|
+
*/
|
|
193
|
+
function getRateLimiter() {
|
|
194
|
+
if (!globalRateLimiter) {
|
|
195
|
+
globalRateLimiter = new RateLimiter();
|
|
196
|
+
}
|
|
197
|
+
return globalRateLimiter;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Create a new rate limiter with custom configs
|
|
201
|
+
*/
|
|
202
|
+
function createRateLimiter(configs) {
|
|
203
|
+
return new RateLimiter(configs);
|
|
204
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Email Validation Utilities
|
|
2
|
+
* Email and Candidate Validation Utilities
|
|
3
3
|
*
|
|
4
|
-
* Provides email syntax validation
|
|
4
|
+
* Provides email syntax validation, name parsing utilities,
|
|
5
|
+
* and EnrichmentCandidate validation.
|
|
5
6
|
*/
|
|
7
|
+
import type { EnrichmentCandidate } from "../types";
|
|
6
8
|
/**
|
|
7
9
|
* Validate email syntax
|
|
8
10
|
*
|
|
@@ -19,7 +21,7 @@ export declare function isValidEmailSyntax(email: string): boolean;
|
|
|
19
21
|
export declare function isRoleAccount(email: string): boolean;
|
|
20
22
|
/**
|
|
21
23
|
* ASCII fold diacritics for name normalization
|
|
22
|
-
* e.g., "
|
|
24
|
+
* e.g., "José" -> "Jose", "Müller" -> "Muller"
|
|
23
25
|
*/
|
|
24
26
|
export declare function asciiFold(s: string): string;
|
|
25
27
|
/**
|
|
@@ -40,3 +42,73 @@ export declare function hostnameFromUrl(url?: string | null): string | null;
|
|
|
40
42
|
* @returns username or null if not found
|
|
41
43
|
*/
|
|
42
44
|
export declare function extractLinkedInUsername(url: string): string | null;
|
|
45
|
+
/**
|
|
46
|
+
* Validation result for an EnrichmentCandidate
|
|
47
|
+
*/
|
|
48
|
+
export interface CandidateValidationResult {
|
|
49
|
+
/** Whether the candidate is valid */
|
|
50
|
+
valid: boolean;
|
|
51
|
+
/** Error messages if invalid */
|
|
52
|
+
errors: string[];
|
|
53
|
+
/** Warning messages (valid but may have issues) */
|
|
54
|
+
warnings: string[];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Validate a domain format
|
|
58
|
+
*/
|
|
59
|
+
export declare function isValidDomain(domain: string): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Validate a LinkedIn URL format
|
|
62
|
+
*/
|
|
63
|
+
export declare function isValidLinkedInUrl(url: string): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Validate an EnrichmentCandidate object
|
|
66
|
+
*
|
|
67
|
+
* Checks for:
|
|
68
|
+
* - At least one name field present
|
|
69
|
+
* - Valid domain format (if provided)
|
|
70
|
+
* - Valid LinkedIn URL format (if provided)
|
|
71
|
+
* - No completely empty candidate
|
|
72
|
+
*
|
|
73
|
+
* @param candidate - The candidate to validate
|
|
74
|
+
* @returns Validation result with errors and warnings
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* const result = validateCandidate({
|
|
79
|
+
* firstName: "John",
|
|
80
|
+
* lastName: "Doe",
|
|
81
|
+
* domain: "example.com"
|
|
82
|
+
* });
|
|
83
|
+
*
|
|
84
|
+
* if (!result.valid) {
|
|
85
|
+
* console.error("Invalid candidate:", result.errors);
|
|
86
|
+
* }
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export declare function validateCandidate(candidate: EnrichmentCandidate): CandidateValidationResult;
|
|
90
|
+
/**
|
|
91
|
+
* Validate a batch of candidates
|
|
92
|
+
*
|
|
93
|
+
* @param candidates - Array of candidates to validate
|
|
94
|
+
* @returns Array of validation results with index
|
|
95
|
+
*/
|
|
96
|
+
export declare function validateCandidates(candidates: EnrichmentCandidate[]): Array<{
|
|
97
|
+
index: number;
|
|
98
|
+
candidate: EnrichmentCandidate;
|
|
99
|
+
result: CandidateValidationResult;
|
|
100
|
+
}>;
|
|
101
|
+
/**
|
|
102
|
+
* Filter valid candidates from a batch
|
|
103
|
+
*
|
|
104
|
+
* @param candidates - Array of candidates to filter
|
|
105
|
+
* @returns Object with valid candidates and rejected ones with reasons
|
|
106
|
+
*/
|
|
107
|
+
export declare function filterValidCandidates(candidates: EnrichmentCandidate[]): {
|
|
108
|
+
valid: EnrichmentCandidate[];
|
|
109
|
+
invalid: Array<{
|
|
110
|
+
index: number;
|
|
111
|
+
candidate: EnrichmentCandidate;
|
|
112
|
+
errors: string[];
|
|
113
|
+
}>;
|
|
114
|
+
};
|