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
|
@@ -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
|
+
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Email Validation Utilities
|
|
3
|
+
* Email and Candidate Validation Utilities
|
|
4
4
|
*
|
|
5
|
-
* Provides email syntax validation
|
|
5
|
+
* Provides email syntax validation, name parsing utilities,
|
|
6
|
+
* and EnrichmentCandidate validation.
|
|
6
7
|
*/
|
|
7
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
9
|
exports.isValidEmailSyntax = isValidEmailSyntax;
|
|
@@ -11,6 +12,15 @@ exports.asciiFold = asciiFold;
|
|
|
11
12
|
exports.cleanNamePart = cleanNamePart;
|
|
12
13
|
exports.hostnameFromUrl = hostnameFromUrl;
|
|
13
14
|
exports.extractLinkedInUsername = extractLinkedInUsername;
|
|
15
|
+
exports.isValidDomain = isValidDomain;
|
|
16
|
+
exports.isValidLinkedInUrl = isValidLinkedInUrl;
|
|
17
|
+
exports.validateCandidate = validateCandidate;
|
|
18
|
+
exports.validateCandidates = validateCandidates;
|
|
19
|
+
exports.filterValidCandidates = filterValidCandidates;
|
|
20
|
+
const candidate_parser_1 = require("./candidate-parser");
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// Email Syntax Validation
|
|
23
|
+
// =============================================================================
|
|
14
24
|
/**
|
|
15
25
|
* Validate email syntax
|
|
16
26
|
*
|
|
@@ -18,17 +28,17 @@ exports.extractLinkedInUsername = extractLinkedInUsername;
|
|
|
18
28
|
* @returns true if the email has valid syntax
|
|
19
29
|
*/
|
|
20
30
|
function isValidEmailSyntax(email) {
|
|
21
|
-
if (!email || typeof email !==
|
|
31
|
+
if (!email || typeof email !== "string")
|
|
22
32
|
return false;
|
|
23
|
-
if (email.includes(
|
|
33
|
+
if (email.includes(" "))
|
|
24
34
|
return false;
|
|
25
|
-
const parts = email.split(
|
|
35
|
+
const parts = email.split("@");
|
|
26
36
|
if (parts.length !== 2)
|
|
27
37
|
return false;
|
|
28
38
|
const [local, domain] = parts;
|
|
29
39
|
if (!local || !domain)
|
|
30
40
|
return false;
|
|
31
|
-
if (!domain.includes(
|
|
41
|
+
if (!domain.includes("."))
|
|
32
42
|
return false;
|
|
33
43
|
if (local.length > 64)
|
|
34
44
|
return false;
|
|
@@ -79,13 +89,16 @@ function isRoleAccount(email) {
|
|
|
79
89
|
const emailLower = email.toLowerCase();
|
|
80
90
|
return ROLE_PATTERNS.some((pattern) => pattern.test(emailLower));
|
|
81
91
|
}
|
|
92
|
+
// =============================================================================
|
|
93
|
+
// Name Utilities
|
|
94
|
+
// =============================================================================
|
|
82
95
|
/**
|
|
83
96
|
* ASCII fold diacritics for name normalization
|
|
84
|
-
* e.g., "
|
|
97
|
+
* e.g., "José" -> "Jose", "Müller" -> "Muller"
|
|
85
98
|
*/
|
|
86
99
|
function asciiFold(s) {
|
|
87
100
|
try {
|
|
88
|
-
return s.normalize(
|
|
101
|
+
return s.normalize("NFD").replace(/\p{Diacritic}+/gu, "");
|
|
89
102
|
}
|
|
90
103
|
catch {
|
|
91
104
|
return s;
|
|
@@ -95,9 +108,12 @@ function asciiFold(s) {
|
|
|
95
108
|
* Clean name part: lowercase, remove diacritics, keep only a-z0-9
|
|
96
109
|
*/
|
|
97
110
|
function cleanNamePart(s) {
|
|
98
|
-
const lower = asciiFold(String(s ||
|
|
99
|
-
return lower.replace(/[^a-z0-9]/g,
|
|
111
|
+
const lower = asciiFold(String(s || "").toLowerCase());
|
|
112
|
+
return lower.replace(/[^a-z0-9]/g, "");
|
|
100
113
|
}
|
|
114
|
+
// =============================================================================
|
|
115
|
+
// URL Utilities
|
|
116
|
+
// =============================================================================
|
|
101
117
|
/**
|
|
102
118
|
* Extract hostname from URL
|
|
103
119
|
*
|
|
@@ -110,7 +126,7 @@ function hostnameFromUrl(url) {
|
|
|
110
126
|
try {
|
|
111
127
|
const u = new URL(url);
|
|
112
128
|
const h = u.hostname.toLowerCase();
|
|
113
|
-
return h.startsWith(
|
|
129
|
+
return h.startsWith("www.") ? h.slice(4) : h;
|
|
114
130
|
}
|
|
115
131
|
catch {
|
|
116
132
|
return null;
|
|
@@ -128,3 +144,140 @@ function extractLinkedInUsername(url) {
|
|
|
128
144
|
const match = url.match(/linkedin\.com\/in\/([^/?]+)/);
|
|
129
145
|
return match ? match[1] : null;
|
|
130
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Domain regex pattern
|
|
149
|
+
*/
|
|
150
|
+
const DOMAIN_REGEX = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$/;
|
|
151
|
+
/**
|
|
152
|
+
* LinkedIn URL pattern
|
|
153
|
+
*/
|
|
154
|
+
const LINKEDIN_URL_REGEX = /linkedin\.com\/in\/[a-zA-Z0-9_-]+/i;
|
|
155
|
+
/**
|
|
156
|
+
* Validate a domain format
|
|
157
|
+
*/
|
|
158
|
+
function isValidDomain(domain) {
|
|
159
|
+
if (!domain || typeof domain !== "string") {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
return DOMAIN_REGEX.test(domain.trim().toLowerCase());
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Validate a LinkedIn URL format
|
|
166
|
+
*/
|
|
167
|
+
function isValidLinkedInUrl(url) {
|
|
168
|
+
if (!url || typeof url !== "string") {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
return LINKEDIN_URL_REGEX.test(url);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Validate an EnrichmentCandidate object
|
|
175
|
+
*
|
|
176
|
+
* Checks for:
|
|
177
|
+
* - At least one name field present
|
|
178
|
+
* - Valid domain format (if provided)
|
|
179
|
+
* - Valid LinkedIn URL format (if provided)
|
|
180
|
+
* - No completely empty candidate
|
|
181
|
+
*
|
|
182
|
+
* @param candidate - The candidate to validate
|
|
183
|
+
* @returns Validation result with errors and warnings
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```typescript
|
|
187
|
+
* const result = validateCandidate({
|
|
188
|
+
* firstName: "John",
|
|
189
|
+
* lastName: "Doe",
|
|
190
|
+
* domain: "example.com"
|
|
191
|
+
* });
|
|
192
|
+
*
|
|
193
|
+
* if (!result.valid) {
|
|
194
|
+
* console.error("Invalid candidate:", result.errors);
|
|
195
|
+
* }
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
function validateCandidate(candidate) {
|
|
199
|
+
const errors = [];
|
|
200
|
+
const warnings = [];
|
|
201
|
+
// Check if candidate is an object
|
|
202
|
+
if (!candidate || typeof candidate !== "object") {
|
|
203
|
+
return {
|
|
204
|
+
valid: false,
|
|
205
|
+
errors: ["Candidate must be a non-null object"],
|
|
206
|
+
warnings: [],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
// Parse the candidate to get normalized fields
|
|
210
|
+
const parsed = (0, candidate_parser_1.parseCandidate)(candidate);
|
|
211
|
+
// Check for at least some identifying information
|
|
212
|
+
const hasName = !!parsed.name.firstName || !!parsed.name.lastName || !!parsed.name.fullName;
|
|
213
|
+
const hasLinkedIn = !!parsed.linkedin.username ||
|
|
214
|
+
!!parsed.linkedin.url ||
|
|
215
|
+
!!parsed.linkedin.numericId;
|
|
216
|
+
const hasDomain = !!parsed.company.domain;
|
|
217
|
+
const hasCompany = !!parsed.company.company;
|
|
218
|
+
if (!hasName && !hasLinkedIn) {
|
|
219
|
+
errors.push("Candidate must have at least a name or LinkedIn identifier");
|
|
220
|
+
}
|
|
221
|
+
// Validate domain format if provided
|
|
222
|
+
if (parsed.company.domain) {
|
|
223
|
+
// Remove common protocol prefixes that users might accidentally include
|
|
224
|
+
const cleanDomain = parsed.company.domain
|
|
225
|
+
.toLowerCase()
|
|
226
|
+
.replace(/^(https?:\/\/)?(www\.)?/, "");
|
|
227
|
+
if (!isValidDomain(cleanDomain)) {
|
|
228
|
+
errors.push(`Invalid domain format: "${parsed.company.domain}"`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Validate LinkedIn URL if provided
|
|
232
|
+
if (parsed.linkedin.url) {
|
|
233
|
+
if (!isValidLinkedInUrl(parsed.linkedin.url)) {
|
|
234
|
+
warnings.push(`LinkedIn URL may be invalid: "${parsed.linkedin.url}"`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Warn if name is very short (might be initial only)
|
|
238
|
+
if (parsed.name.firstName && parsed.name.firstName.length === 1) {
|
|
239
|
+
warnings.push("First name appears to be an initial - results may be less accurate");
|
|
240
|
+
}
|
|
241
|
+
// Warn if no domain/company for providers that need it
|
|
242
|
+
if (!hasDomain && !hasCompany && hasName && !hasLinkedIn) {
|
|
243
|
+
warnings.push("No domain or company provided - only LinkedIn-based providers will work");
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
valid: errors.length === 0,
|
|
247
|
+
errors,
|
|
248
|
+
warnings,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Validate a batch of candidates
|
|
253
|
+
*
|
|
254
|
+
* @param candidates - Array of candidates to validate
|
|
255
|
+
* @returns Array of validation results with index
|
|
256
|
+
*/
|
|
257
|
+
function validateCandidates(candidates) {
|
|
258
|
+
return candidates.map((candidate, index) => ({
|
|
259
|
+
index,
|
|
260
|
+
candidate,
|
|
261
|
+
result: validateCandidate(candidate),
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Filter valid candidates from a batch
|
|
266
|
+
*
|
|
267
|
+
* @param candidates - Array of candidates to filter
|
|
268
|
+
* @returns Object with valid candidates and rejected ones with reasons
|
|
269
|
+
*/
|
|
270
|
+
function filterValidCandidates(candidates) {
|
|
271
|
+
const valid = [];
|
|
272
|
+
const invalid = [];
|
|
273
|
+
candidates.forEach((candidate, index) => {
|
|
274
|
+
const result = validateCandidate(candidate);
|
|
275
|
+
if (result.valid) {
|
|
276
|
+
valid.push(candidate);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
invalid.push({ index, candidate, errors: result.errors });
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
return { valid, invalid };
|
|
283
|
+
}
|
package/dist/linkedin-api.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SalesSearchFilters, LinkedInProfile, SearchSalesResult, TypeaheadResult, SalesNavigatorProfile, Company } from "./types";
|
|
1
|
+
import type { SalesSearchFilters, LinkedInProfile, SearchSalesResult, TypeaheadResult, SalesNavigatorProfile, SalesNavigatorProfileFull, Company } from "./types";
|
|
2
2
|
/**
|
|
3
3
|
* Fetches a LinkedIn profile by vanity URL (public identifier).
|
|
4
4
|
* Results are cached for the configured TTL (default: 15 minutes).
|
|
@@ -128,3 +128,42 @@ export declare function getYearsInPositionOptions(): Promise<TypeaheadResult>;
|
|
|
128
128
|
*/
|
|
129
129
|
export declare function getYearsOfExperienceOptions(): Promise<TypeaheadResult>;
|
|
130
130
|
export declare function getSalesNavigatorProfileDetails(profileUrnOrId: string): Promise<SalesNavigatorProfile>;
|
|
131
|
+
/**
|
|
132
|
+
* Fetches full Sales Navigator profile data including flagshipProfileUrl (LinkedIn handle).
|
|
133
|
+
* This is a more complete version that returns all available profile data.
|
|
134
|
+
*
|
|
135
|
+
* The key use case is extracting the LinkedIn handle from flagshipProfileUrl
|
|
136
|
+
* for use with email finder services like Hunter.io.
|
|
137
|
+
*
|
|
138
|
+
* @param profileUrnOrId - Profile identifier in any of these formats:
|
|
139
|
+
* - Sales profile URN: "urn:li:fs_salesProfile:(ABC123xyz,NAME_SEARCH,abc)"
|
|
140
|
+
* - FSD profile URN: "urn:li:fsd_profile:ABC123xyz"
|
|
141
|
+
* - Bare key: "ABC123xyz"
|
|
142
|
+
* @returns Full Sales Navigator profile with flagshipProfileUrl, contactInfo, positions, etc.
|
|
143
|
+
* @throws LinkedInClientError with code NOT_FOUND if profile doesn't exist
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```typescript
|
|
147
|
+
* const profile = await getSalesNavigatorProfileFull('urn:li:fs_salesProfile:(ABC123,NAME_SEARCH,xyz)');
|
|
148
|
+
* console.log(profile.flagshipProfileUrl); // "https://www.linkedin.com/in/john-doe"
|
|
149
|
+
*
|
|
150
|
+
* // Extract LinkedIn handle for Hunter.io
|
|
151
|
+
* const handle = extractLinkedInHandle(profile.flagshipProfileUrl);
|
|
152
|
+
* console.log(handle); // "john-doe"
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
export declare function getSalesNavigatorProfileFull(profileUrnOrId: string): Promise<SalesNavigatorProfileFull>;
|
|
156
|
+
/**
|
|
157
|
+
* Extracts LinkedIn handle/vanity from a flagship profile URL.
|
|
158
|
+
*
|
|
159
|
+
* @param flagshipProfileUrl - Full LinkedIn profile URL (e.g., "https://www.linkedin.com/in/john-doe")
|
|
160
|
+
* @returns LinkedIn handle/vanity (e.g., "john-doe") or null if not found
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```typescript
|
|
164
|
+
* extractLinkedInHandle("https://www.linkedin.com/in/john-doe"); // "john-doe"
|
|
165
|
+
* extractLinkedInHandle("https://www.linkedin.com/in/georgi-metodiev-tech2rec"); // "georgi-metodiev-tech2rec"
|
|
166
|
+
* extractLinkedInHandle(null); // null
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
export declare function extractLinkedInHandle(flagshipProfileUrl: string | null | undefined): string | null;
|