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.
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 +3 -3
  5. package/dist/enrichment/index.js +19 -2
  6. package/dist/enrichment/matching.d.ts +29 -9
  7. package/dist/enrichment/matching.js +545 -142
  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 +27 -0
  15. package/dist/enrichment/providers/cosiall.js +109 -0
  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 +10 -7
  21. package/dist/enrichment/providers/index.js +12 -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 +234 -17
  30. package/dist/enrichment/types.js +60 -48
  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,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
+ };
@@ -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 and name parsing utilities.
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 !== 'string')
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., "Jos" -> "Jose", "Mller" -> "Muller"
97
+ * e.g., "José" -> "Jose", "Müller" -> "Muller"
85
98
  */
86
99
  function asciiFold(s) {
87
100
  try {
88
- return s.normalize('NFD').replace(/\p{Diacritic}+/gu, '');
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 || '').toLowerCase());
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('www.') ? h.slice(4) : h;
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
+ }
@@ -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;