linkedin-secret-sauce 0.6.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.js +1 -1
- package/dist/cosiall-client.js +2 -1
- package/dist/enrichment/auth/smartlead-auth.js +0 -2
- package/dist/enrichment/index.d.ts +1 -0
- package/dist/enrichment/index.js +4 -1
- package/dist/enrichment/matching.d.ts +2 -0
- package/dist/enrichment/matching.js +11 -4
- package/dist/enrichment/orchestrator.js +2 -2
- package/dist/enrichment/providers/ldd.d.ts +18 -0
- package/dist/enrichment/providers/ldd.js +158 -24
- package/dist/enrichment/providers/smartprospect.js +217 -6
- package/dist/enrichment/types.d.ts +36 -14
- package/dist/http-client.js +1 -1
- package/dist/linkedin-api.d.ts +1 -2
- package/dist/parsers/profile-parser.js +4 -2
- package/dist/utils/linkedin-config.js +1 -1
- package/dist/utils/search-encoder.js +8 -9
- package/package.json +8 -2
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(
|
|
109
|
+
throw new errors_1.LinkedInClientError(`Failed to initialize LinkedIn client: ${error.message}`, 'INITIALIZATION_FAILED', 0);
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
}
|
package/dist/cosiall-client.js
CHANGED
|
@@ -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";
|
package/dist/enrichment/index.js
CHANGED
|
@@ -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
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
const
|
|
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
|
|
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
|
|
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,109 @@ 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 (
|
|
79
|
-
|
|
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-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
+
/**
|
|
189
|
+
* Calculate confidence score based on email type
|
|
190
|
+
* Professional emails are scored higher than personal ones
|
|
191
|
+
*/
|
|
192
|
+
function getConfidenceScore(emailType) {
|
|
193
|
+
if (!emailType)
|
|
194
|
+
return 75; // Unknown type
|
|
195
|
+
const type = emailType.toLowerCase();
|
|
196
|
+
// Professional/business emails - highest confidence
|
|
197
|
+
if (type === 'professional' || type === 'business' || type === 'work') {
|
|
198
|
+
return 95;
|
|
199
|
+
}
|
|
200
|
+
// Personal emails - still valid but lower priority for B2B
|
|
201
|
+
if (type === 'personal' || type === 'private') {
|
|
202
|
+
return 70;
|
|
203
|
+
}
|
|
204
|
+
// Other types (e.g., "other", "unknown")
|
|
205
|
+
return 75;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Classify email type for the enrichment result
|
|
209
|
+
*/
|
|
210
|
+
function classifyEmailType(emailType) {
|
|
211
|
+
if (!emailType)
|
|
212
|
+
return 'unknown';
|
|
213
|
+
const type = emailType.toLowerCase();
|
|
214
|
+
if (type === 'professional' || type === 'business' || type === 'work') {
|
|
215
|
+
return 'business';
|
|
216
|
+
}
|
|
217
|
+
if (type === 'personal' || type === 'private') {
|
|
218
|
+
return 'personal';
|
|
219
|
+
}
|
|
220
|
+
return 'unknown';
|
|
221
|
+
}
|
|
222
|
+
function parseResponse(data) {
|
|
223
|
+
if (!data.success || !data.data) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
// Return ALL valid emails from the profile
|
|
227
|
+
const profileEmails = data.data.emails || [];
|
|
228
|
+
const validEmails = profileEmails.filter((e) => e.emailAddress && e.emailAddress.includes("@"));
|
|
229
|
+
if (validEmails.length === 0) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
// Build multi-result with all emails, sorted by confidence (professional first)
|
|
233
|
+
const emails = validEmails
|
|
234
|
+
.map((e) => ({
|
|
235
|
+
email: e.emailAddress,
|
|
236
|
+
verified: true, // LDD data is pre-verified
|
|
237
|
+
confidence: getConfidenceScore(e.emailType),
|
|
238
|
+
metadata: {
|
|
239
|
+
emailType: e.emailType,
|
|
240
|
+
emailTypeClassified: classifyEmailType(e.emailType),
|
|
241
|
+
linkedinUsername: data.data.linkedInUsername,
|
|
242
|
+
numericLinkedInId: data.data.numericLinkedInId,
|
|
243
|
+
sourceRecordId: data.data.sourceRecordId,
|
|
244
|
+
},
|
|
245
|
+
}))
|
|
246
|
+
.sort((a, b) => b.confidence - a.confidence); // Sort by confidence descending
|
|
247
|
+
return { emails };
|
|
248
|
+
}
|
|
115
249
|
// Mark provider name for orchestrator
|
|
116
250
|
fetchEmail.__name = "ldd";
|
|
117
251
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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,27 +440,45 @@ 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 {
|
|
442
|
-
|
|
443
|
-
|
|
470
|
+
id: number;
|
|
471
|
+
emailAddress: string | null;
|
|
472
|
+
emailType: string | null;
|
|
444
473
|
}
|
|
445
474
|
export interface LddProfilePhone {
|
|
446
|
-
|
|
447
|
-
|
|
475
|
+
id: number;
|
|
476
|
+
phoneNumber: string | null;
|
|
448
477
|
}
|
|
449
478
|
export interface LddProfileData {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
linkedin_username?: string | null;
|
|
454
|
-
first_name?: string | null;
|
|
455
|
-
last_name?: string | null;
|
|
456
|
-
full_name?: string | null;
|
|
457
|
-
profile_headline?: string | null;
|
|
458
|
-
profile_summary?: string | null;
|
|
459
|
-
profile_location?: string | null;
|
|
479
|
+
sourceRecordId: string;
|
|
480
|
+
numericLinkedInId: string | null;
|
|
481
|
+
linkedInUsername: string | null;
|
|
460
482
|
emails: LddProfileEmail[];
|
|
461
483
|
phones: LddProfilePhone[];
|
|
462
484
|
}
|
package/dist/http-client.js
CHANGED
|
@@ -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
|
|
47
|
+
function _isRetryableStatus(status) {
|
|
48
48
|
return (status === 429 ||
|
|
49
49
|
status === 500 ||
|
|
50
50
|
status === 502 ||
|
package/dist/linkedin-api.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
22
|
-
|
|
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
|
|
27
|
+
return `rq:${String(rawQuery).trim()}`;
|
|
29
28
|
if (!filters)
|
|
30
29
|
return undefined;
|
|
31
|
-
return
|
|
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.
|
|
3
|
+
"version": "0.7.2",
|
|
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:
|
|
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",
|