linkedin-secret-sauce 0.12.1 → 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.
Files changed (44) hide show
  1. package/README.md +50 -21
  2. package/dist/cosiall-client.d.ts +1 -1
  3. package/dist/cosiall-client.js +1 -1
  4. package/dist/enrichment/index.d.ts +1 -1
  5. package/dist/enrichment/index.js +11 -2
  6. package/dist/enrichment/matching.d.ts +16 -2
  7. package/dist/enrichment/matching.js +387 -65
  8. package/dist/enrichment/providers/bounceban.d.ts +82 -0
  9. package/dist/enrichment/providers/bounceban.js +447 -0
  10. package/dist/enrichment/providers/bouncer.d.ts +1 -1
  11. package/dist/enrichment/providers/bouncer.js +19 -21
  12. package/dist/enrichment/providers/construct.d.ts +1 -1
  13. package/dist/enrichment/providers/construct.js +22 -38
  14. package/dist/enrichment/providers/cosiall.d.ts +1 -1
  15. package/dist/enrichment/providers/cosiall.js +3 -4
  16. package/dist/enrichment/providers/dropcontact.d.ts +15 -9
  17. package/dist/enrichment/providers/dropcontact.js +188 -19
  18. package/dist/enrichment/providers/hunter.d.ts +8 -1
  19. package/dist/enrichment/providers/hunter.js +52 -28
  20. package/dist/enrichment/providers/index.d.ts +2 -0
  21. package/dist/enrichment/providers/index.js +10 -1
  22. package/dist/enrichment/providers/ldd.d.ts +1 -10
  23. package/dist/enrichment/providers/ldd.js +20 -97
  24. package/dist/enrichment/providers/smartprospect.js +28 -48
  25. package/dist/enrichment/providers/snovio.d.ts +1 -1
  26. package/dist/enrichment/providers/snovio.js +29 -31
  27. package/dist/enrichment/providers/trykitt.d.ts +63 -0
  28. package/dist/enrichment/providers/trykitt.js +210 -0
  29. package/dist/enrichment/types.d.ts +210 -7
  30. package/dist/enrichment/types.js +16 -8
  31. package/dist/enrichment/utils/candidate-parser.d.ts +107 -0
  32. package/dist/enrichment/utils/candidate-parser.js +173 -0
  33. package/dist/enrichment/utils/noop-provider.d.ts +39 -0
  34. package/dist/enrichment/utils/noop-provider.js +37 -0
  35. package/dist/enrichment/utils/rate-limiter.d.ts +103 -0
  36. package/dist/enrichment/utils/rate-limiter.js +204 -0
  37. package/dist/enrichment/utils/validation.d.ts +75 -3
  38. package/dist/enrichment/utils/validation.js +164 -11
  39. package/dist/linkedin-api.d.ts +40 -1
  40. package/dist/linkedin-api.js +160 -27
  41. package/dist/types.d.ts +50 -1
  42. package/dist/utils/lru-cache.d.ts +105 -0
  43. package/dist/utils/lru-cache.js +175 -0
  44. package/package.json +25 -26
@@ -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 and name parsing utilities.
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., "Jos" -> "Jose", "Mller" -> "Muller"
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
+ };