linkedin-secret-sauce 0.6.0 → 0.7.1

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/dist/config.js CHANGED
@@ -106,7 +106,7 @@ async function initializeLinkedInClient(config) {
106
106
  }
107
107
  else {
108
108
  // Default: throw to alert consumer immediately
109
- throw new errors_1.LinkedInClientError('Failed to initialize LinkedIn client: ' + error.message, 'INITIALIZATION_FAILED', 0);
109
+ throw new errors_1.LinkedInClientError(`Failed to initialize LinkedIn client: ${error.message}`, 'INITIALIZATION_FAILED', 0);
110
110
  }
111
111
  }
112
112
  }
@@ -115,8 +115,9 @@ async function fetchCookiesFromCosiall() {
115
115
  return false;
116
116
  if (!rec.cookies.every(isCookie))
117
117
  return false;
118
- if (rec.expiresAt !== undefined && typeof rec.expiresAt !== "number")
118
+ if (rec.expiresAt !== undefined && typeof rec.expiresAt !== "number") {
119
119
  return false;
120
+ }
120
121
  return true;
121
122
  }
122
123
  return data
@@ -306,8 +306,6 @@ function clearFileCache() {
306
306
  // Ignore errors
307
307
  }
308
308
  }
309
- // Patch the existing getSmartLeadToken to save to file cache after login
310
- const originalGetSmartLeadToken = getSmartLeadToken;
311
309
  // We need to modify the token caching behavior to persist to file
312
310
  // This is done by wrapping the cache set operation
313
311
  // Override tokenCache.set to also persist to file
@@ -40,6 +40,7 @@ export { isDisposableEmail, isDisposableDomain, DISPOSABLE_DOMAINS, } from "./ut
40
40
  export { isValidEmailSyntax, isRoleAccount, asciiFold, cleanNamePart, hostnameFromUrl, extractLinkedInUsername, } from "./utils/validation";
41
41
  export { verifyEmailMx } from "./verification/mx";
42
42
  export { createConstructProvider, createLddProvider, createSmartProspectProvider, createHunterProvider, createApolloProvider, createDropcontactProvider, } from "./providers";
43
+ export { extractNumericLinkedInId } from "./providers/ldd";
43
44
  export { createSmartProspectClient, type SmartProspectClient, type SmartProspectLocationOptions, } from "./providers/smartprospect";
44
45
  export { enrichBusinessEmail, enrichBatch, enrichAllEmails, enrichAllBatch } from "./orchestrator";
45
46
  export { getSmartLeadToken, getSmartLeadUser, clearSmartLeadToken, clearAllSmartLeadTokens, getSmartLeadTokenCacheStats, enableFileCache, disableFileCache, isFileCacheEnabled, clearFileCache, type SmartLeadCredentials, type SmartLeadAuthConfig, type SmartLeadUser, type SmartLeadLoginResponse, } from "./auth";
@@ -42,7 +42,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
42
42
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
43
43
  };
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
- exports.createLinkedInEnricher = exports.enrichLinkedInContactsBatch = exports.enrichLinkedInContact = exports.parseLinkedInSearchResponse = exports.buildSmartProspectFiltersFromLinkedIn = exports.matchContacts = exports.findBestMatch = exports.classifyMatchQuality = exports.calculateMatchConfidence = exports.clearFileCache = exports.isFileCacheEnabled = exports.disableFileCache = exports.enableFileCache = exports.getSmartLeadTokenCacheStats = exports.clearAllSmartLeadTokens = exports.clearSmartLeadToken = exports.getSmartLeadUser = exports.getSmartLeadToken = exports.enrichAllBatch = exports.enrichAllEmails = exports.enrichBatch = exports.enrichBusinessEmail = exports.createSmartProspectClient = exports.createDropcontactProvider = exports.createApolloProvider = exports.createHunterProvider = exports.createSmartProspectProvider = exports.createLddProvider = exports.createConstructProvider = exports.verifyEmailMx = exports.extractLinkedInUsername = exports.hostnameFromUrl = exports.cleanNamePart = exports.asciiFold = exports.isRoleAccount = exports.isValidEmailSyntax = exports.DISPOSABLE_DOMAINS = exports.isDisposableDomain = exports.isDisposableEmail = exports.PERSONAL_DOMAINS = exports.isPersonalDomain = exports.isBusinessEmail = exports.isPersonalEmail = void 0;
45
+ exports.createLinkedInEnricher = exports.enrichLinkedInContactsBatch = exports.enrichLinkedInContact = exports.parseLinkedInSearchResponse = exports.buildSmartProspectFiltersFromLinkedIn = exports.matchContacts = exports.findBestMatch = exports.classifyMatchQuality = exports.calculateMatchConfidence = exports.clearFileCache = exports.isFileCacheEnabled = exports.disableFileCache = exports.enableFileCache = exports.getSmartLeadTokenCacheStats = exports.clearAllSmartLeadTokens = exports.clearSmartLeadToken = exports.getSmartLeadUser = exports.getSmartLeadToken = exports.enrichAllBatch = exports.enrichAllEmails = exports.enrichBatch = exports.enrichBusinessEmail = exports.createSmartProspectClient = exports.extractNumericLinkedInId = exports.createDropcontactProvider = exports.createApolloProvider = exports.createHunterProvider = exports.createSmartProspectProvider = exports.createLddProvider = exports.createConstructProvider = exports.verifyEmailMx = exports.extractLinkedInUsername = exports.hostnameFromUrl = exports.cleanNamePart = exports.asciiFold = exports.isRoleAccount = exports.isValidEmailSyntax = exports.DISPOSABLE_DOMAINS = exports.isDisposableDomain = exports.isDisposableEmail = exports.PERSONAL_DOMAINS = exports.isPersonalDomain = exports.isBusinessEmail = exports.isPersonalEmail = void 0;
46
46
  exports.createEnrichmentClient = createEnrichmentClient;
47
47
  const orchestrator_1 = require("./orchestrator");
48
48
  const construct_1 = require("./providers/construct");
@@ -250,6 +250,9 @@ Object.defineProperty(exports, "createSmartProspectProvider", { enumerable: true
250
250
  Object.defineProperty(exports, "createHunterProvider", { enumerable: true, get: function () { return providers_1.createHunterProvider; } });
251
251
  Object.defineProperty(exports, "createApolloProvider", { enumerable: true, get: function () { return providers_1.createApolloProvider; } });
252
252
  Object.defineProperty(exports, "createDropcontactProvider", { enumerable: true, get: function () { return providers_1.createDropcontactProvider; } });
253
+ // Re-export LDD utilities
254
+ var ldd_2 = require("./providers/ldd");
255
+ Object.defineProperty(exports, "extractNumericLinkedInId", { enumerable: true, get: function () { return ldd_2.extractNumericLinkedInId; } });
253
256
  // Re-export SmartProspect client for direct API access
254
257
  var smartprospect_2 = require("./providers/smartprospect");
255
258
  Object.defineProperty(exports, "createSmartProspectClient", { enumerable: true, get: function () { return smartprospect_2.createSmartProspectClient; } });
@@ -107,6 +107,8 @@ export interface LinkedInEnrichmentResult {
107
107
  success: boolean;
108
108
  /** The original LinkedIn contact */
109
109
  linkedInContact: LinkedInContact;
110
+ /** Numeric LinkedIn ID extracted from objectUrn (stable identifier for LDD lookup) */
111
+ numericLinkedInId: string | null;
110
112
  /** The matched SmartProspect contact (if found) */
111
113
  matchedContact: SmartProspectContact | null;
112
114
  /** Match confidence score (0-100) */
@@ -16,6 +16,7 @@ exports.enrichLinkedInContact = enrichLinkedInContact;
16
16
  exports.enrichLinkedInContactsBatch = enrichLinkedInContactsBatch;
17
17
  exports.createLinkedInEnricher = createLinkedInEnricher;
18
18
  const smartprospect_1 = require("./providers/smartprospect");
19
+ const ldd_1 = require("./providers/ldd");
19
20
  // =============================================================================
20
21
  // Utility Functions
21
22
  // =============================================================================
@@ -143,10 +144,10 @@ function calculateMatchConfidence(linkedin, smartprospect, options = {}) {
143
144
  const matchedFields = [];
144
145
  let score = 0;
145
146
  // === Name matching (up to 40 points) ===
146
- const liFirstName = normalize(linkedin.firstName);
147
- const liLastName = normalize(linkedin.lastName);
148
- const spFirstName = normalize(smartprospect.firstName);
149
- const spLastName = normalize(smartprospect.lastName);
147
+ const _liFirstName = normalize(linkedin.firstName);
148
+ const _liLastName = normalize(linkedin.lastName);
149
+ const _spFirstName = normalize(smartprospect.firstName);
150
+ const _spLastName = normalize(smartprospect.lastName);
150
151
  // First name match
151
152
  if (exactMatch(linkedin.firstName, smartprospect.firstName)) {
152
153
  score += 20;
@@ -353,10 +354,13 @@ function parseLinkedInSearchResponse(elements) {
353
354
  */
354
355
  async function enrichLinkedInContact(linkedInContact, smartProspectConfig, options = {}) {
355
356
  const { minConfidence = 60, autoFetch = true, fuzzyNames = true, fuzzyCompany = true, searchLimit = 25, includeCompany = true, includeLocation = false, } = options;
357
+ // Extract numeric LinkedIn ID from objectUrn (stable identifier for LDD)
358
+ const numericLinkedInId = (0, ldd_1.extractNumericLinkedInId)(linkedInContact.objectUrn);
356
359
  // Initialize result
357
360
  const result = {
358
361
  success: false,
359
362
  linkedInContact,
363
+ numericLinkedInId,
360
364
  matchedContact: null,
361
365
  matchConfidence: 0,
362
366
  matchQuality: 'none',
@@ -497,10 +501,13 @@ function createLinkedInEnricher(smartProspectConfig, defaultOptions = {}) {
497
501
  client,
498
502
  async enrich(contact, options = {}) {
499
503
  const mergedOptions = { ...defaultOptions, ...options };
504
+ // Extract numeric LinkedIn ID from objectUrn (stable identifier for LDD)
505
+ const numericLinkedInId = (0, ldd_1.extractNumericLinkedInId)(contact.objectUrn);
500
506
  // We reuse the existing client rather than creating a new one
501
507
  const result = {
502
508
  success: false,
503
509
  linkedInContact: contact,
510
+ numericLinkedInId,
504
511
  matchedContact: null,
505
512
  matchConfidence: 0,
506
513
  matchQuality: 'none',
@@ -16,7 +16,7 @@ const validation_1 = require("./utils/validation");
16
16
  /**
17
17
  * Default provider costs in USD per lookup
18
18
  */
19
- const DEFAULT_PROVIDER_COSTS = {
19
+ const _PROVIDER_COSTS = {
20
20
  construct: 0,
21
21
  ldd: 0,
22
22
  smartprospect: 0.01,
@@ -63,7 +63,7 @@ function getProviderName(provider, index) {
63
63
  * Get provider cost
64
64
  */
65
65
  function getProviderCost(providerName) {
66
- return DEFAULT_PROVIDER_COSTS[providerName] ?? 0;
66
+ return _PROVIDER_COSTS[providerName] ?? 0;
67
67
  }
68
68
  /**
69
69
  * Check if a provider result is a multi-result (returns multiple emails)
@@ -3,9 +3,27 @@
3
3
  *
4
4
  * YOUR private database of ~500M scraped LinkedIn profiles with emails.
5
5
  * This is FREE and unlimited - it's your own service.
6
+ *
7
+ * IMPORTANT: Prefer numeric LinkedIn ID over username for lookups.
8
+ * The numeric ID (from objectUrn like "urn:li:member:307567") is STABLE and never changes,
9
+ * while usernames can be changed by users at any time.
6
10
  */
7
11
  import type { EnrichmentCandidate, ProviderResult, ProviderMultiResult, LddConfig } from "../types";
12
+ /**
13
+ * Extract numeric LinkedIn ID from various formats:
14
+ * - Direct number: "307567"
15
+ * - URN format: "urn:li:member:307567"
16
+ * - Full URN: "urn:li:fs_salesProfile:(ACwAAAAEsW8B...,NAME_SEARCH,PJOV)"
17
+ * - objectUrn: "urn:li:member:307567"
18
+ *
19
+ * Returns the numeric ID as a string, or null if not found.
20
+ */
21
+ export declare function extractNumericLinkedInId(input: string | undefined | null): string | null;
8
22
  /**
9
23
  * Create the LDD provider function
24
+ *
25
+ * Lookup priority:
26
+ * 1. Numeric LinkedIn ID (PREFERRED - stable, never changes)
27
+ * 2. Username (FALLBACK - can change over time)
10
28
  */
11
29
  export declare function createLddProvider(config: LddConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | ProviderMultiResult | null>;
@@ -4,8 +4,13 @@
4
4
  *
5
5
  * YOUR private database of ~500M scraped LinkedIn profiles with emails.
6
6
  * This is FREE and unlimited - it's your own service.
7
+ *
8
+ * IMPORTANT: Prefer numeric LinkedIn ID over username for lookups.
9
+ * The numeric ID (from objectUrn like "urn:li:member:307567") is STABLE and never changes,
10
+ * while usernames can be changed by users at any time.
7
11
  */
8
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.extractNumericLinkedInId = extractNumericLinkedInId;
9
14
  exports.createLddProvider = createLddProvider;
10
15
  const validation_1 = require("../utils/validation");
11
16
  /**
@@ -45,6 +50,35 @@ async function requestWithRetry(url, token, retries = 1, backoffMs = 200) {
45
50
  }
46
51
  throw lastErr ?? new Error("ldd_http_error");
47
52
  }
53
+ /**
54
+ * Extract numeric LinkedIn ID from various formats:
55
+ * - Direct number: "307567"
56
+ * - URN format: "urn:li:member:307567"
57
+ * - Full URN: "urn:li:fs_salesProfile:(ACwAAAAEsW8B...,NAME_SEARCH,PJOV)"
58
+ * - objectUrn: "urn:li:member:307567"
59
+ *
60
+ * Returns the numeric ID as a string, or null if not found.
61
+ */
62
+ function extractNumericLinkedInId(input) {
63
+ if (!input)
64
+ return null;
65
+ const trimmed = input.trim();
66
+ // Direct numeric ID
67
+ if (/^\d+$/.test(trimmed)) {
68
+ return trimmed;
69
+ }
70
+ // URN format: urn:li:member:307567
71
+ const memberMatch = trimmed.match(/urn:li:member:(\d+)/i);
72
+ if (memberMatch) {
73
+ return memberMatch[1];
74
+ }
75
+ // Sales profile URN with numeric ID
76
+ const salesMatch = trimmed.match(/urn:li:fs_salesProfile:\((\d+),/i);
77
+ if (salesMatch) {
78
+ return salesMatch[1];
79
+ }
80
+ return null;
81
+ }
48
82
  /**
49
83
  * Extract LinkedIn username from candidate
50
84
  */
@@ -62,8 +96,43 @@ function extractUsername(candidate) {
62
96
  }
63
97
  return null;
64
98
  }
99
+ /**
100
+ * Extract numeric LinkedIn ID from candidate.
101
+ * Checks multiple fields in order of preference:
102
+ * 1. numericLinkedInId / numeric_linkedin_id (direct numeric ID)
103
+ * 2. objectUrn / object_urn (URN format like "urn:li:member:307567")
104
+ * 3. linkedinId / linkedin_id (may contain URN or numeric ID)
105
+ */
106
+ function extractNumericId(candidate) {
107
+ // Direct numeric ID field
108
+ const numericId = candidate.numericLinkedInId || candidate.numeric_linkedin_id;
109
+ if (numericId) {
110
+ const extracted = extractNumericLinkedInId(numericId);
111
+ if (extracted)
112
+ return extracted;
113
+ }
114
+ // objectUrn field (from Sales Navigator search results)
115
+ const objectUrn = candidate.objectUrn || candidate.object_urn;
116
+ if (objectUrn) {
117
+ const extracted = extractNumericLinkedInId(objectUrn);
118
+ if (extracted)
119
+ return extracted;
120
+ }
121
+ // linkedinId field (may contain URN or numeric ID)
122
+ const linkedinId = candidate.linkedinId || candidate.linkedin_id;
123
+ if (linkedinId) {
124
+ const extracted = extractNumericLinkedInId(linkedinId);
125
+ if (extracted)
126
+ return extracted;
127
+ }
128
+ return null;
129
+ }
65
130
  /**
66
131
  * Create the LDD provider function
132
+ *
133
+ * Lookup priority:
134
+ * 1. Numeric LinkedIn ID (PREFERRED - stable, never changes)
135
+ * 2. Username (FALLBACK - can change over time)
67
136
  */
68
137
  function createLddProvider(config) {
69
138
  const { apiUrl, apiToken } = config;
@@ -74,44 +143,73 @@ function createLddProvider(config) {
74
143
  return noopProvider;
75
144
  }
76
145
  async function fetchEmail(candidate) {
146
+ // PRIORITY 1: Try numeric ID first (stable identifier)
147
+ const numericId = extractNumericId(candidate);
148
+ if (numericId) {
149
+ const result = await lookupByNumericId(numericId);
150
+ if (result)
151
+ return result;
152
+ }
153
+ // PRIORITY 2: Fall back to username (less reliable)
77
154
  const username = extractUsername(candidate);
78
- if (!username) {
79
- return null;
155
+ if (username) {
156
+ const result = await lookupByUsername(username);
157
+ if (result)
158
+ return result;
80
159
  }
160
+ return null;
161
+ }
162
+ async function lookupByNumericId(numericId) {
81
163
  try {
82
- const endpoint = `${apiUrl}/api/v1/profiles/by-username/${encodeURIComponent(username)}`;
164
+ const endpoint = `${apiUrl}/api/v1/profiles/by-numeric-id/${encodeURIComponent(numericId)}`;
83
165
  const response = await requestWithRetry(endpoint, apiToken, 1, 100);
84
166
  if (!response.ok) {
85
167
  return null;
86
168
  }
87
- const data = (await response.json());
88
- if (!data.success || !data.data) {
89
- return null;
90
- }
91
- // Return ALL valid emails from the profile
92
- const profileEmails = data.data.emails || [];
93
- const validEmails = profileEmails.filter((e) => e.email_address && e.email_address.includes("@"));
94
- if (validEmails.length === 0) {
169
+ return parseResponse(await response.json());
170
+ }
171
+ catch {
172
+ return null;
173
+ }
174
+ }
175
+ async function lookupByUsername(username) {
176
+ try {
177
+ const endpoint = `${apiUrl}/api/v1/profiles/by-username/${encodeURIComponent(username)}`;
178
+ const response = await requestWithRetry(endpoint, apiToken, 1, 100);
179
+ if (!response.ok) {
95
180
  return null;
96
181
  }
97
- // Build multi-result with all emails
98
- const emails = validEmails.map((e) => ({
99
- email: e.email_address,
100
- verified: true, // LDD data is pre-verified
101
- confidence: 90, // High confidence from your own database
102
- metadata: {
103
- emailType: e.email_type,
104
- fullName: data.data.full_name,
105
- linkedinUsername: data.data.linkedin_username,
106
- linkedinUrl: data.data.linkedin_profile_url,
107
- },
108
- }));
109
- return { emails };
182
+ return parseResponse(await response.json());
110
183
  }
111
184
  catch {
112
185
  return null;
113
186
  }
114
187
  }
188
+ function parseResponse(data) {
189
+ if (!data.success || !data.data) {
190
+ return null;
191
+ }
192
+ // Return ALL valid emails from the profile
193
+ const profileEmails = data.data.emails || [];
194
+ const validEmails = profileEmails.filter((e) => e.email_address && e.email_address.includes("@"));
195
+ if (validEmails.length === 0) {
196
+ return null;
197
+ }
198
+ // Build multi-result with all emails
199
+ const emails = validEmails.map((e) => ({
200
+ email: e.email_address,
201
+ verified: true, // LDD data is pre-verified
202
+ confidence: 90, // High confidence from your own database
203
+ metadata: {
204
+ emailType: e.email_type,
205
+ fullName: data.data.full_name,
206
+ linkedinUsername: data.data.linkedin_username,
207
+ linkedinUrl: data.data.linkedin_profile_url,
208
+ numericLinkedInId: data.data.linkedin_id,
209
+ },
210
+ }));
211
+ return { emails };
212
+ }
115
213
  // Mark provider name for orchestrator
116
214
  fetchEmail.__name = "ldd";
117
215
  return fetchEmail;
@@ -24,6 +24,12 @@ const DEFAULT_API_URL = "https://prospect-api.smartlead.ai/api/search-email-lead
24
24
  async function delay(ms) {
25
25
  return new Promise((r) => setTimeout(r, ms));
26
26
  }
27
+ const DEFAULT_POLLING_CONFIG = {
28
+ initialDelay: 500,
29
+ pollInterval: 1000,
30
+ maxAttempts: 10,
31
+ maxWaitTime: 15000,
32
+ };
27
33
  /**
28
34
  * HTTP request with retry on 429 rate limit
29
35
  */
@@ -202,11 +208,85 @@ function createSmartProspectProvider(config) {
202
208
  };
203
209
  }
204
210
  }
211
+ /**
212
+ * Poll get-contacts until email lookup is complete (provider version)
213
+ */
214
+ async function pollForContactResults(filterId, expectedCount) {
215
+ const { initialDelay, pollInterval, maxAttempts, maxWaitTime } = DEFAULT_POLLING_CONFIG;
216
+ const startTime = Date.now();
217
+ await delay(initialDelay);
218
+ const makeRequest = async (token) => {
219
+ return requestWithRetry(`${apiUrl}/get-contacts`, {
220
+ method: "POST",
221
+ headers: {
222
+ Authorization: `Bearer ${token}`,
223
+ "Content-Type": "application/json",
224
+ Accept: "application/json",
225
+ },
226
+ body: JSON.stringify({
227
+ filter_id: filterId,
228
+ limit: expectedCount,
229
+ offset: 0,
230
+ }),
231
+ });
232
+ };
233
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
234
+ if (Date.now() - startTime > maxWaitTime) {
235
+ break;
236
+ }
237
+ try {
238
+ const token = await getAuthToken();
239
+ const result = await makeRequest(token);
240
+ if (result.success && result.data.metrics) {
241
+ const { completed, totalContacts } = result.data.metrics;
242
+ if (completed >= totalContacts || completed >= expectedCount) {
243
+ return result;
244
+ }
245
+ }
246
+ await delay(pollInterval);
247
+ }
248
+ catch (err) {
249
+ if (err instanceof Error && err.message.includes("401") && hasCredentials) {
250
+ await handleAuthError();
251
+ }
252
+ await delay(pollInterval);
253
+ }
254
+ }
255
+ // Return last result
256
+ try {
257
+ const token = await getAuthToken();
258
+ return await makeRequest(token);
259
+ }
260
+ catch {
261
+ return {
262
+ success: false,
263
+ message: "Polling timed out",
264
+ data: {
265
+ list: [],
266
+ total_count: 0,
267
+ metrics: {
268
+ totalContacts: 0,
269
+ totalEmails: 0,
270
+ noEmailFound: 0,
271
+ invalidEmails: 0,
272
+ catchAllEmails: 0,
273
+ verifiedEmails: 0,
274
+ completed: 0,
275
+ },
276
+ },
277
+ };
278
+ }
279
+ }
205
280
  /**
206
281
  * Fetch/enrich emails for specific contact IDs (COSTS CREDITS)
282
+ *
283
+ * Uses async polling pattern:
284
+ * 1. Call fetch-contacts (returns immediately with status: "pending")
285
+ * 2. Poll get-contacts until completed
286
+ * 3. Return enriched contacts with emails
207
287
  */
208
288
  async function fetchContacts(contactIds) {
209
- const makeRequest = async (token) => {
289
+ const makeFetchRequest = async (token) => {
210
290
  return requestWithRetry(`${apiUrl}/fetch-contacts`, {
211
291
  method: "POST",
212
292
  headers: {
@@ -219,7 +299,30 @@ function createSmartProspectProvider(config) {
219
299
  };
220
300
  try {
221
301
  const token = await getAuthToken();
222
- return await makeRequest(token);
302
+ const fetchResult = await makeFetchRequest(token);
303
+ // If fetch-contacts returns pending status with filter_id, poll for results
304
+ const filterId = fetchResult.data?.filter_id;
305
+ const status = fetchResult.data?.status;
306
+ if (filterId && status === "pending") {
307
+ const pollResult = await pollForContactResults(filterId, contactIds.length);
308
+ if (pollResult.success && pollResult.data.list.length > 0) {
309
+ return {
310
+ success: true,
311
+ message: "Fetch completed",
312
+ data: {
313
+ list: pollResult.data.list,
314
+ total_count: pollResult.data.total_count,
315
+ metrics: pollResult.data.metrics,
316
+ leads_found: pollResult.data.metrics.totalContacts,
317
+ email_fetched: pollResult.data.metrics.totalEmails,
318
+ verification_status_list: [],
319
+ filter_id: filterId,
320
+ status: "completed",
321
+ },
322
+ };
323
+ }
324
+ }
325
+ return fetchResult;
223
326
  }
224
327
  catch (err) {
225
328
  // On 401, clear token and retry once with fresh token
@@ -229,7 +332,7 @@ function createSmartProspectProvider(config) {
229
332
  await handleAuthError();
230
333
  try {
231
334
  const freshToken = await getAuthToken();
232
- return await makeRequest(freshToken);
335
+ return await makeFetchRequest(freshToken);
233
336
  }
234
337
  catch {
235
338
  // Retry failed too
@@ -518,8 +621,90 @@ function createSmartProspectClient(config) {
518
621
  };
519
622
  }
520
623
  }
624
+ /**
625
+ * Poll get-contacts until email lookup is complete
626
+ */
627
+ async function pollForResults(filterId, expectedCount, pollingConfig = DEFAULT_POLLING_CONFIG) {
628
+ const { initialDelay, pollInterval, maxAttempts, maxWaitTime } = pollingConfig;
629
+ const startTime = Date.now();
630
+ // Wait initial delay before first poll
631
+ await delay(initialDelay);
632
+ const makeRequest = async (token) => {
633
+ return requestWithRetry(`${apiUrl}/get-contacts`, {
634
+ method: "POST",
635
+ headers: {
636
+ Authorization: `Bearer ${token}`,
637
+ "Content-Type": "application/json",
638
+ Accept: "application/json",
639
+ },
640
+ body: JSON.stringify({
641
+ filter_id: filterId,
642
+ limit: expectedCount,
643
+ offset: 0,
644
+ }),
645
+ });
646
+ };
647
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
648
+ // Check if we've exceeded max wait time
649
+ if (Date.now() - startTime > maxWaitTime) {
650
+ break;
651
+ }
652
+ try {
653
+ const token = await getAuthToken();
654
+ const result = await makeRequest(token);
655
+ if (result.success && result.data.metrics) {
656
+ const { completed, totalContacts } = result.data.metrics;
657
+ // Check if all contacts have been processed
658
+ if (completed >= totalContacts || completed >= expectedCount) {
659
+ return result;
660
+ }
661
+ }
662
+ // Wait before next poll
663
+ await delay(pollInterval);
664
+ }
665
+ catch (err) {
666
+ // On 401, clear token and retry
667
+ if (err instanceof Error &&
668
+ err.message.includes("401") &&
669
+ hasCredentials) {
670
+ await handleAuthError();
671
+ }
672
+ // Continue polling on errors
673
+ await delay(pollInterval);
674
+ }
675
+ }
676
+ // Return last result even if not complete
677
+ try {
678
+ const token = await getAuthToken();
679
+ return await makeRequest(token);
680
+ }
681
+ catch {
682
+ return {
683
+ success: false,
684
+ message: "Polling timed out",
685
+ data: {
686
+ list: [],
687
+ total_count: 0,
688
+ metrics: {
689
+ totalContacts: 0,
690
+ totalEmails: 0,
691
+ noEmailFound: 0,
692
+ invalidEmails: 0,
693
+ catchAllEmails: 0,
694
+ verifiedEmails: 0,
695
+ completed: 0,
696
+ },
697
+ },
698
+ };
699
+ }
700
+ }
521
701
  /**
522
702
  * Fetch/enrich emails for specific contact IDs (COSTS CREDITS)
703
+ *
704
+ * SmartProspect uses async email lookup:
705
+ * 1. Call fetch-contacts to initiate lookup (returns immediately with status: "pending")
706
+ * 2. Poll get-contacts until metrics.completed >= expected count
707
+ * 3. Return the final enriched contacts with emails
523
708
  */
524
709
  async function fetch(contactIds) {
525
710
  if (!contactIds.length) {
@@ -544,7 +729,7 @@ function createSmartProspectClient(config) {
544
729
  },
545
730
  };
546
731
  }
547
- const makeRequest = async (token) => {
732
+ const makeFetchRequest = async (token) => {
548
733
  return requestWithRetry(`${apiUrl}/fetch-contacts`, {
549
734
  method: "POST",
550
735
  headers: {
@@ -557,7 +742,33 @@ function createSmartProspectClient(config) {
557
742
  };
558
743
  try {
559
744
  const token = await getAuthToken();
560
- return await makeRequest(token);
745
+ const fetchResult = await makeFetchRequest(token);
746
+ // If fetch-contacts returns a filter_id with pending status, we need to poll
747
+ const filterId = fetchResult.data?.filter_id;
748
+ const status = fetchResult.data?.status;
749
+ if (filterId && status === "pending") {
750
+ // Poll get-contacts for final results
751
+ const pollResult = await pollForResults(filterId, contactIds.length);
752
+ if (pollResult.success && pollResult.data.list.length > 0) {
753
+ // Convert poll result to fetch response format
754
+ return {
755
+ success: true,
756
+ message: "Fetch completed",
757
+ data: {
758
+ list: pollResult.data.list,
759
+ total_count: pollResult.data.total_count,
760
+ metrics: pollResult.data.metrics,
761
+ leads_found: pollResult.data.metrics.totalContacts,
762
+ email_fetched: pollResult.data.metrics.totalEmails,
763
+ verification_status_list: [],
764
+ filter_id: filterId,
765
+ status: "completed",
766
+ },
767
+ };
768
+ }
769
+ }
770
+ // If no polling needed or polling failed, return original result
771
+ return fetchResult;
561
772
  }
562
773
  catch (err) {
563
774
  if (err instanceof Error &&
@@ -566,7 +777,7 @@ function createSmartProspectClient(config) {
566
777
  await handleAuthError();
567
778
  try {
568
779
  const freshToken = await getAuthToken();
569
- return await makeRequest(freshToken);
780
+ return await makeFetchRequest(freshToken);
570
781
  }
571
782
  catch {
572
783
  // Retry failed
@@ -112,6 +112,10 @@ export interface EnrichmentCandidate {
112
112
  linkedin_url?: string;
113
113
  linkedinId?: string;
114
114
  linkedin_id?: string;
115
+ numericLinkedInId?: string;
116
+ numeric_linkedin_id?: string;
117
+ objectUrn?: string;
118
+ object_urn?: string;
115
119
  title?: string;
116
120
  currentRole?: string;
117
121
  current_role?: string;
@@ -436,6 +440,30 @@ export interface SmartProspectFetchResponse {
436
440
  leads_found: number;
437
441
  email_fetched: number;
438
442
  verification_status_list: string[];
443
+ /** Filter ID returned by fetch-contacts for polling */
444
+ filter_id?: string;
445
+ /** Status of the fetch operation: "pending" means still processing */
446
+ status?: string;
447
+ };
448
+ }
449
+ /**
450
+ * Response from get-contacts polling endpoint
451
+ */
452
+ export interface SmartProspectGetContactsResponse {
453
+ success: boolean;
454
+ message: string;
455
+ data: {
456
+ list: SmartProspectContact[];
457
+ total_count: number;
458
+ metrics: {
459
+ totalContacts: number;
460
+ totalEmails: number;
461
+ noEmailFound: number;
462
+ invalidEmails: number;
463
+ catchAllEmails: number;
464
+ verifiedEmails: number;
465
+ completed: number;
466
+ };
439
467
  };
440
468
  }
441
469
  export interface LddProfileEmail {
@@ -44,7 +44,7 @@ const linkedin_config_1 = require("./utils/linkedin-config");
44
44
  function sleep(ms) {
45
45
  return new Promise((resolve) => setTimeout(resolve, ms));
46
46
  }
47
- function isRetryableStatus(status) {
47
+ function _isRetryableStatus(status) {
48
48
  return (status === 429 ||
49
49
  status === 500 ||
50
50
  status === 502 ||
@@ -1,5 +1,4 @@
1
- import type { SalesSearchFilters } from "./types";
2
- import type { LinkedInProfile, SearchSalesResult, TypeaheadResult, SalesNavigatorProfile, Company } from "./types";
1
+ import type { SalesSearchFilters, LinkedInProfile, SearchSalesResult, TypeaheadResult, SalesNavigatorProfile, Company } from "./types";
3
2
  /**
4
3
  * Fetches a LinkedIn profile by vanity URL (public identifier).
5
4
  * Results are cached for the configured TTL (default: 15 minutes).
@@ -102,15 +102,17 @@ function parseFullProfile(rawResponse, vanity) {
102
102
  function deepFindCompanyUrn(obj, depth = 0) {
103
103
  if (!obj || depth > 3)
104
104
  return undefined;
105
- if (typeof obj === "string" && /urn:li:(?:fsd_)?company:/i.test(obj))
105
+ if (typeof obj === "string" && /urn:li:(?:fsd_)?company:/i.test(obj)) {
106
106
  return obj;
107
+ }
107
108
  if (typeof obj !== "object")
108
109
  return undefined;
109
110
  const rec = obj;
110
111
  for (const k of Object.keys(rec)) {
111
112
  const v = rec[k];
112
- if (typeof v === "string" && /urn:li:(?:fsd_)?company:/i.test(v))
113
+ if (typeof v === "string" && /urn:li:(?:fsd_)?company:/i.test(v)) {
113
114
  return v;
115
+ }
114
116
  if (v && typeof v === "object") {
115
117
  const hit = deepFindCompanyUrn(v, depth + 1);
116
118
  if (hit)
@@ -52,7 +52,7 @@ function buildVoyagerHeaders(opts) {
52
52
  "x-li-track": JSON.stringify(trackPayload),
53
53
  "sec-fetch-site": "same-origin",
54
54
  "sec-fetch-mode": "cors",
55
- referer: referer ?? exports.defaultOrigin + "/",
55
+ referer: referer ?? `${exports.defaultOrigin}/`,
56
56
  origin: exports.defaultOrigin,
57
57
  };
58
58
  }
@@ -13,22 +13,21 @@ exports.buildLeadSearchQuery = buildLeadSearchQuery;
13
13
  function stableStringify(obj) {
14
14
  if (obj === null || typeof obj !== "object")
15
15
  return JSON.stringify(obj);
16
- if (Array.isArray(obj))
17
- return "[" + obj.map((v) => stableStringify(v)).join(",") + "]";
16
+ if (Array.isArray(obj)) {
17
+ return `[${obj.map((v) => stableStringify(v)).join(",")}]`;
18
+ }
18
19
  const rec = obj;
19
20
  const keys = Object.keys(rec).sort();
20
- return ("{" +
21
- keys
22
- .map((k) => JSON.stringify(k) + ":" + stableStringify(rec[k]))
23
- .join(",") +
24
- "}");
21
+ return (`{${keys
22
+ .map((k) => `${JSON.stringify(k)}:${stableStringify(rec[k])}`)
23
+ .join(",")}}`);
25
24
  }
26
25
  function buildFilterSignature(filters, rawQuery) {
27
26
  if (rawQuery)
28
- return "rq:" + String(rawQuery).trim();
27
+ return `rq:${String(rawQuery).trim()}`;
29
28
  if (!filters)
30
29
  return undefined;
31
- return "f:" + stableStringify(filters);
30
+ return `f:${stableStringify(filters)}`;
32
31
  }
33
32
  // Temporary minimal encoder: today we include only keywords in the query structure to
34
33
  // preserve existing behavior. Once facet ids are confirmed, extend this to encode filters
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linkedin-secret-sauce",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "Private LinkedIn Sales Navigator client with automatic cookie management",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -15,8 +15,14 @@
15
15
  "search": "node scripts/rg-fast.mjs",
16
16
  "rg:fast": "node scripts/rg-fast.mjs",
17
17
  "build": "tsc -p tsconfig.json",
18
+ "typecheck": "tsc --noEmit",
19
+ "typecheck:playground": "pnpm -C apps/playground typecheck",
20
+ "typecheck:all": "pnpm typecheck && pnpm typecheck:playground",
18
21
  "lint": "eslint \"src/**/*.ts\" --max-warnings=0",
19
- "lint:fix": "eslint \"src/**/*.ts\" --fix",
22
+ "lint:playground": "eslint \"apps/playground/src/**/*.{ts,tsx}\" \"apps/playground/server/**/*.ts\" --max-warnings=0",
23
+ "lint:all": "eslint \"src/**/*.ts\" \"apps/playground/src/**/*.{ts,tsx}\" \"apps/playground/server/**/*.ts\" --max-warnings=0",
24
+ "lint:fix": "eslint \"src/**/*.ts\" \"apps/playground/src/**/*.{ts,tsx}\" \"apps/playground/server/**/*.ts\" --fix",
25
+ "check": "pnpm typecheck:all && pnpm lint:all",
20
26
  "dev": "tsc -w -p tsconfig.json",
21
27
  "test": "vitest run",
22
28
  "prepublishOnly": "npm run build",