linkedin-secret-sauce 0.5.1 → 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.
@@ -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,36 +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
- // Find first valid email
92
- const emails = data.data.emails || [];
93
- const validEmail = emails.find((e) => e.email_address && e.email_address.includes("@"));
94
- if (!validEmail) {
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
- return {
98
- email: validEmail.email_address,
99
- verified: true, // LDD data is pre-verified
100
- score: 90, // High confidence from your own database
101
- };
182
+ return parseResponse(await response.json());
102
183
  }
103
184
  catch {
104
185
  return null;
105
186
  }
106
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
+ }
107
213
  // Mark provider name for orchestrator
108
214
  fetchEmail.__name = "ldd";
109
215
  return fetchEmail;
@@ -7,8 +7,41 @@
7
7
  * Supports two authentication methods:
8
8
  * 1. Direct token: Pass `apiToken` directly (for pre-authenticated scenarios)
9
9
  * 2. Credentials: Pass `email` and `password` for automatic login with token caching
10
+ *
11
+ * Exports:
12
+ * - createSmartProspectProvider: For email enrichment (waterfall pattern)
13
+ * - createSmartProspectClient: Full client with search/fetch capabilities
14
+ */
15
+ import type { EnrichmentCandidate, ProviderResult, ProviderMultiResult, SmartProspectConfig, SmartProspectSearchFilters, SmartProspectSearchResponse, SmartProspectFetchResponse, SmartProspectContact, SmartProspectLocationResponse } from "../types";
16
+ /**
17
+ * SmartProspect Location lookup options
10
18
  */
11
- import type { EnrichmentCandidate, ProviderResult, SmartProspectConfig } from "../types";
19
+ export interface SmartProspectLocationOptions {
20
+ search?: string;
21
+ limit?: number;
22
+ offset?: number;
23
+ }
24
+ /**
25
+ * SmartProspect Client interface for direct API access
26
+ */
27
+ export interface SmartProspectClient {
28
+ /** Search for contacts (FREE - no credits used) */
29
+ search: (filters: SmartProspectSearchFilters) => Promise<SmartProspectSearchResponse>;
30
+ /** Fetch/enrich emails for specific contact IDs (COSTS CREDITS) */
31
+ fetch: (contactIds: string[]) => Promise<SmartProspectFetchResponse>;
32
+ /** Search and immediately fetch all results (COSTS CREDITS for fetched contacts) */
33
+ searchAndFetch: (filters: SmartProspectSearchFilters) => Promise<{
34
+ searchResponse: SmartProspectSearchResponse;
35
+ fetchResponse: SmartProspectFetchResponse | null;
36
+ contacts: SmartProspectContact[];
37
+ }>;
38
+ /** Lookup countries (for typeahead) */
39
+ getCountries: (options?: SmartProspectLocationOptions) => Promise<SmartProspectLocationResponse>;
40
+ /** Lookup states (for typeahead) */
41
+ getStates: (options?: SmartProspectLocationOptions) => Promise<SmartProspectLocationResponse>;
42
+ /** Lookup cities (for typeahead) */
43
+ getCities: (options?: SmartProspectLocationOptions) => Promise<SmartProspectLocationResponse>;
44
+ }
12
45
  /**
13
46
  * Create the SmartProspect provider function
14
47
  *
@@ -16,4 +49,33 @@ import type { EnrichmentCandidate, ProviderResult, SmartProspectConfig } from ".
16
49
  * 1. Direct token: Pass `apiToken` in config
17
50
  * 2. Credentials: Pass `email` and `password` in config for auto-login
18
51
  */
19
- export declare function createSmartProspectProvider(config: SmartProspectConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | null>;
52
+ export declare function createSmartProspectProvider(config: SmartProspectConfig): (candidate: EnrichmentCandidate) => Promise<ProviderResult | ProviderMultiResult | null>;
53
+ /**
54
+ * Create a SmartProspect client for direct API access
55
+ *
56
+ * This provides access to the full SmartProspect API for:
57
+ * - Searching contacts with comprehensive filters (FREE)
58
+ * - Fetching/enriching contact emails (COSTS CREDITS)
59
+ * - Combined search + fetch operations
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * const client = createSmartProspectClient({
64
+ * email: 'user@example.com',
65
+ * password: 'password123'
66
+ * });
67
+ *
68
+ * // Search only (FREE)
69
+ * const results = await client.search({
70
+ * title: ['CEO', 'CTO'],
71
+ * company: ['Google', 'Microsoft'],
72
+ * level: ['C-Level'],
73
+ * companyHeadCount: ['1001-5000', '5001-10000'],
74
+ * limit: 25
75
+ * });
76
+ *
77
+ * // Fetch specific contacts (COSTS CREDITS)
78
+ * const enriched = await client.fetch(['contact-id-1', 'contact-id-2']);
79
+ * ```
80
+ */
81
+ export declare function createSmartProspectClient(config: SmartProspectConfig): SmartProspectClient | null;