linkedin-secret-sauce 0.3.29 → 0.5.0
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/cookie-pool.d.ts +1 -1
- package/dist/cookie-pool.js +67 -35
- package/dist/cosiall-client.d.ts +20 -1
- package/dist/cosiall-client.js +48 -25
- package/dist/enrichment/index.d.ts +43 -0
- package/dist/enrichment/index.js +231 -0
- package/dist/enrichment/orchestrator.d.ts +31 -0
- package/dist/enrichment/orchestrator.js +218 -0
- package/dist/enrichment/providers/apollo.d.ts +11 -0
- package/dist/enrichment/providers/apollo.js +136 -0
- package/dist/enrichment/providers/construct.d.ts +11 -0
- package/dist/enrichment/providers/construct.js +107 -0
- package/dist/enrichment/providers/dropcontact.d.ts +16 -0
- package/dist/enrichment/providers/dropcontact.js +37 -0
- package/dist/enrichment/providers/hunter.d.ts +11 -0
- package/dist/enrichment/providers/hunter.js +162 -0
- package/dist/enrichment/providers/index.d.ts +9 -0
- package/dist/enrichment/providers/index.js +18 -0
- package/dist/enrichment/providers/ldd.d.ts +11 -0
- package/dist/enrichment/providers/ldd.js +110 -0
- package/dist/enrichment/providers/smartprospect.d.ts +11 -0
- package/dist/enrichment/providers/smartprospect.js +249 -0
- package/dist/enrichment/types.d.ts +329 -0
- package/dist/enrichment/types.js +31 -0
- package/dist/enrichment/utils/disposable-domains.d.ts +24 -0
- package/dist/enrichment/utils/disposable-domains.js +1011 -0
- package/dist/enrichment/utils/index.d.ts +6 -0
- package/dist/enrichment/utils/index.js +22 -0
- package/dist/enrichment/utils/personal-domains.d.ts +31 -0
- package/dist/enrichment/utils/personal-domains.js +95 -0
- package/dist/enrichment/utils/validation.d.ts +42 -0
- package/dist/enrichment/utils/validation.js +130 -0
- package/dist/enrichment/verification/index.d.ts +4 -0
- package/dist/enrichment/verification/index.js +8 -0
- package/dist/enrichment/verification/mx.d.ts +16 -0
- package/dist/enrichment/verification/mx.js +168 -0
- package/dist/http-client.d.ts +1 -1
- package/dist/http-client.js +146 -63
- package/dist/index.d.ts +17 -14
- package/dist/index.js +20 -1
- package/dist/linkedin-api.d.ts +97 -4
- package/dist/linkedin-api.js +416 -134
- package/dist/parsers/company-parser.d.ts +15 -1
- package/dist/parsers/company-parser.js +45 -17
- package/dist/parsers/profile-parser.d.ts +19 -1
- package/dist/parsers/profile-parser.js +131 -81
- package/dist/parsers/search-parser.d.ts +1 -1
- package/dist/parsers/search-parser.js +24 -11
- package/dist/utils/logger.d.ts +1 -1
- package/dist/utils/logger.js +28 -18
- package/dist/utils/search-encoder.d.ts +32 -1
- package/dist/utils/search-encoder.js +102 -58
- package/dist/utils/sentry.d.ts +1 -1
- package/dist/utils/sentry.js +56 -8
- package/package.json +1 -1
package/dist/linkedin-api.js
CHANGED
|
@@ -41,6 +41,7 @@ exports.getProfilesBatch = getProfilesBatch;
|
|
|
41
41
|
exports.resolveCompanyUniversalName = resolveCompanyUniversalName;
|
|
42
42
|
exports.getCompanyById = getCompanyById;
|
|
43
43
|
exports.getCompanyByUrl = getCompanyByUrl;
|
|
44
|
+
exports.getCompaniesBatch = getCompaniesBatch;
|
|
44
45
|
exports.typeahead = typeahead;
|
|
45
46
|
exports.getYearsAtCompanyOptions = getYearsAtCompanyOptions;
|
|
46
47
|
exports.getYearsInPositionOptions = getYearsInPositionOptions;
|
|
@@ -56,8 +57,8 @@ const metrics_1 = require("./utils/metrics");
|
|
|
56
57
|
const logger_1 = require("./utils/logger");
|
|
57
58
|
const errors_1 = require("./utils/errors");
|
|
58
59
|
const constants_1 = require("./constants");
|
|
59
|
-
const LINKEDIN_API_BASE =
|
|
60
|
-
const SALES_NAV_BASE =
|
|
60
|
+
const LINKEDIN_API_BASE = "https://www.linkedin.com/voyager/api";
|
|
61
|
+
const SALES_NAV_BASE = "https://www.linkedin.com/sales-api";
|
|
61
62
|
// In-memory caches (per-process)
|
|
62
63
|
const profileCacheByVanity = new Map();
|
|
63
64
|
const profileCacheByUrn = new Map();
|
|
@@ -77,23 +78,41 @@ function getCached(map, key, ttl) {
|
|
|
77
78
|
}
|
|
78
79
|
return entry.data;
|
|
79
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Fetches a LinkedIn profile by vanity URL (public identifier).
|
|
83
|
+
* Results are cached for the configured TTL (default: 15 minutes).
|
|
84
|
+
*
|
|
85
|
+
* @param vanity - The public identifier from LinkedIn URL (e.g., "johndoe" from linkedin.com/in/johndoe)
|
|
86
|
+
* @returns Parsed LinkedInProfile with positions, education, skills, and metadata
|
|
87
|
+
* @throws LinkedInClientError with code NOT_FOUND if profile doesn't exist
|
|
88
|
+
* @throws LinkedInClientError with code PARSE_ERROR if response cannot be parsed
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```typescript
|
|
92
|
+
* const profile = await getProfileByVanity('johndoe');
|
|
93
|
+
* console.log(profile.firstName, profile.headline);
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
80
96
|
async function getProfileByVanity(vanity) {
|
|
81
97
|
const cfg = (0, config_1.getConfig)();
|
|
82
|
-
const key = String(vanity ||
|
|
98
|
+
const key = String(vanity || "").toLowerCase();
|
|
83
99
|
try {
|
|
84
|
-
(0, logger_1.log)(
|
|
100
|
+
(0, logger_1.log)("info", "api.start", {
|
|
101
|
+
operation: "getProfileByVanity",
|
|
102
|
+
selector: key,
|
|
103
|
+
});
|
|
85
104
|
}
|
|
86
105
|
catch { }
|
|
87
106
|
// Cache
|
|
88
107
|
const cached = getCached(profileCacheByVanity, key, cfg.profileCacheTtl);
|
|
89
108
|
if (cached) {
|
|
90
|
-
(0, metrics_1.incrementMetric)(
|
|
109
|
+
(0, metrics_1.incrementMetric)("profileCacheHits");
|
|
91
110
|
return cached;
|
|
92
111
|
}
|
|
93
112
|
// In-flight dedupe
|
|
94
113
|
const inflight = inflightByVanity.get(key);
|
|
95
114
|
if (inflight) {
|
|
96
|
-
(0, metrics_1.incrementMetric)(
|
|
115
|
+
(0, metrics_1.incrementMetric)("inflightDedupeHits");
|
|
97
116
|
return inflight;
|
|
98
117
|
}
|
|
99
118
|
const url = `${LINKEDIN_API_BASE}/identity/dash/profiles?q=memberIdentity&memberIdentity=${encodeURIComponent(vanity)}&decorationId=com.linkedin.voyager.dash.deco.identity.profile.FullProfileWithEntities-35`;
|
|
@@ -101,16 +120,20 @@ async function getProfileByVanity(vanity) {
|
|
|
101
120
|
try {
|
|
102
121
|
let raw;
|
|
103
122
|
try {
|
|
104
|
-
raw = await (0, http_client_1.executeLinkedInRequest)({ url },
|
|
123
|
+
raw = await (0, http_client_1.executeLinkedInRequest)({ url }, "getProfileByVanity");
|
|
105
124
|
}
|
|
106
125
|
catch (e) {
|
|
107
126
|
const status = e?.status ?? 0;
|
|
108
127
|
if (status === 404) {
|
|
109
128
|
try {
|
|
110
|
-
(0, logger_1.log)(
|
|
129
|
+
(0, logger_1.log)("warn", "api.notFound", {
|
|
130
|
+
operation: "getProfileByVanity",
|
|
131
|
+
selector: key,
|
|
132
|
+
status,
|
|
133
|
+
});
|
|
111
134
|
}
|
|
112
135
|
catch { }
|
|
113
|
-
throw new errors_1.LinkedInClientError(
|
|
136
|
+
throw new errors_1.LinkedInClientError("Profile not found", errors_1.ERROR_CODES.NOT_FOUND, 404);
|
|
114
137
|
}
|
|
115
138
|
throw e;
|
|
116
139
|
}
|
|
@@ -120,15 +143,21 @@ async function getProfileByVanity(vanity) {
|
|
|
120
143
|
}
|
|
121
144
|
catch {
|
|
122
145
|
try {
|
|
123
|
-
(0, logger_1.log)(
|
|
146
|
+
(0, logger_1.log)("error", "api.parseError", {
|
|
147
|
+
operation: "getProfileByVanity",
|
|
148
|
+
selector: key,
|
|
149
|
+
});
|
|
124
150
|
}
|
|
125
151
|
catch { }
|
|
126
|
-
throw new errors_1.LinkedInClientError(
|
|
152
|
+
throw new errors_1.LinkedInClientError("Failed to parse profile", errors_1.ERROR_CODES.PARSE_ERROR, 500);
|
|
127
153
|
}
|
|
128
154
|
profileCacheByVanity.set(key, { data: prof, ts: Date.now() });
|
|
129
|
-
(0, metrics_1.incrementMetric)(
|
|
155
|
+
(0, metrics_1.incrementMetric)("profileFetches");
|
|
130
156
|
try {
|
|
131
|
-
(0, logger_1.log)(
|
|
157
|
+
(0, logger_1.log)("info", "api.ok", {
|
|
158
|
+
operation: "getProfileByVanity",
|
|
159
|
+
selector: key,
|
|
160
|
+
});
|
|
132
161
|
}
|
|
133
162
|
catch { }
|
|
134
163
|
return prof;
|
|
@@ -140,36 +169,69 @@ async function getProfileByVanity(vanity) {
|
|
|
140
169
|
inflightByVanity.set(key, p);
|
|
141
170
|
return p;
|
|
142
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Fetches a LinkedIn profile by FSD key or URN.
|
|
174
|
+
* Accepts multiple input formats and normalizes them.
|
|
175
|
+
* Results are cached for the configured TTL (default: 15 minutes).
|
|
176
|
+
*
|
|
177
|
+
* @param fsdKey - Profile identifier in any of these formats:
|
|
178
|
+
* - Bare key: "ABC123xyz"
|
|
179
|
+
* - FSD profile URN: "urn:li:fsd_profile:ABC123xyz"
|
|
180
|
+
* - Sales profile URN: "urn:li:fs_salesProfile:(ABC123xyz,NAME_SEARCH,abc)"
|
|
181
|
+
* @returns Parsed LinkedInProfile with positions, education, skills, and metadata
|
|
182
|
+
* @throws LinkedInClientError with code INVALID_INPUT if URN format is invalid
|
|
183
|
+
* @throws LinkedInClientError with code NOT_FOUND if profile doesn't exist
|
|
184
|
+
* @throws LinkedInClientError with code PARSE_ERROR if response cannot be parsed
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```typescript
|
|
188
|
+
* // By bare key
|
|
189
|
+
* const profile = await getProfileByUrn('ABC123xyz');
|
|
190
|
+
*
|
|
191
|
+
* // By URN from search results
|
|
192
|
+
* const profile = await getProfileByUrn(searchResult.salesProfileUrn);
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
143
195
|
async function getProfileByUrn(fsdKey) {
|
|
144
196
|
const cfg = (0, config_1.getConfig)();
|
|
145
197
|
// Normalize input: accept bare KEY or URN variants and extract KEY
|
|
146
|
-
const input = String(fsdKey ||
|
|
198
|
+
const input = String(fsdKey || "").trim();
|
|
147
199
|
const keyMatch = input.match(/^urn:li:fsd_profile:([^\s/]+)$/i)?.[1] ||
|
|
148
200
|
input.match(/^urn:li:fs_salesProfile:\(([^,\s)]+)/i)?.[1] ||
|
|
149
201
|
(input.match(/^[A-Za-z0-9_-]+$/) ? input : null);
|
|
150
202
|
if (!keyMatch) {
|
|
151
|
-
throw new errors_1.LinkedInClientError(
|
|
203
|
+
throw new errors_1.LinkedInClientError("Invalid URN or key", errors_1.ERROR_CODES.INVALID_INPUT, 400);
|
|
152
204
|
}
|
|
153
205
|
const cacheKey = keyMatch; // Preserve case for FSD keys (case-sensitive)
|
|
154
206
|
try {
|
|
155
|
-
(0, logger_1.log)(
|
|
207
|
+
(0, logger_1.log)("info", "api.start", {
|
|
208
|
+
operation: "getProfileByUrn",
|
|
209
|
+
selector: keyMatch,
|
|
210
|
+
});
|
|
156
211
|
}
|
|
157
212
|
catch { }
|
|
158
213
|
const cachedUrn = getCached(profileCacheByUrn, cacheKey, cfg.profileCacheTtl);
|
|
159
214
|
if (cachedUrn) {
|
|
160
|
-
(0, metrics_1.incrementMetric)(
|
|
215
|
+
(0, metrics_1.incrementMetric)("profileCacheHits");
|
|
161
216
|
return cachedUrn;
|
|
162
217
|
}
|
|
163
218
|
const url = `${LINKEDIN_API_BASE}/identity/dash/profiles?q=memberIdentity&memberIdentity=${encodeURIComponent(keyMatch)}&decorationId=com.linkedin.voyager.dash.deco.identity.profile.FullProfileWithEntities-35`;
|
|
164
219
|
try {
|
|
165
|
-
(0, logger_1.log)(
|
|
220
|
+
(0, logger_1.log)("debug", "api.requestUrl", {
|
|
221
|
+
operation: "getProfileByUrn",
|
|
222
|
+
url,
|
|
223
|
+
fsdKey: keyMatch,
|
|
224
|
+
});
|
|
166
225
|
}
|
|
167
226
|
catch { }
|
|
168
227
|
let raw;
|
|
169
228
|
try {
|
|
170
|
-
raw = await (0, http_client_1.executeLinkedInRequest)({ url },
|
|
229
|
+
raw = await (0, http_client_1.executeLinkedInRequest)({ url }, "getProfileByUrn");
|
|
171
230
|
try {
|
|
172
|
-
(0, logger_1.log)(
|
|
231
|
+
(0, logger_1.log)("debug", "api.rawResponse", {
|
|
232
|
+
operation: "getProfileByUrn",
|
|
233
|
+
responseSize: JSON.stringify(raw).length,
|
|
234
|
+
});
|
|
173
235
|
}
|
|
174
236
|
catch { }
|
|
175
237
|
}
|
|
@@ -177,10 +239,14 @@ async function getProfileByUrn(fsdKey) {
|
|
|
177
239
|
const status = e?.status ?? 0;
|
|
178
240
|
if (status === 404) {
|
|
179
241
|
try {
|
|
180
|
-
(0, logger_1.log)(
|
|
242
|
+
(0, logger_1.log)("warn", "api.notFound", {
|
|
243
|
+
operation: "getProfileByUrn",
|
|
244
|
+
selector: cacheKey,
|
|
245
|
+
status,
|
|
246
|
+
});
|
|
181
247
|
}
|
|
182
248
|
catch { }
|
|
183
|
-
throw new errors_1.LinkedInClientError(
|
|
249
|
+
throw new errors_1.LinkedInClientError("Profile not found", errors_1.ERROR_CODES.NOT_FOUND, 404);
|
|
184
250
|
}
|
|
185
251
|
throw e;
|
|
186
252
|
}
|
|
@@ -188,31 +254,47 @@ async function getProfileByUrn(fsdKey) {
|
|
|
188
254
|
// BUG FIX: LinkedIn API returns multiple profiles (requested profile + connections/colleagues)
|
|
189
255
|
// The correct profile is referenced in data.*elements[0] - we must match against this URN
|
|
190
256
|
const rr = raw;
|
|
191
|
-
const included = Array.isArray(rr?.included)
|
|
257
|
+
const included = Array.isArray(rr?.included)
|
|
258
|
+
? rr.included
|
|
259
|
+
: [];
|
|
192
260
|
// Get the primary profile URN from data.*elements[0]
|
|
193
261
|
const dataObj = rr?.data;
|
|
194
|
-
const elementsArray = dataObj?.[
|
|
262
|
+
const elementsArray = dataObj?.["*elements"];
|
|
195
263
|
const requestedProfileUrn = elementsArray?.[0];
|
|
196
264
|
// Find the profile in included[] that matches the requested URN
|
|
197
265
|
const identityObj = requestedProfileUrn
|
|
198
266
|
? included.find((it) => {
|
|
199
267
|
const rec = it;
|
|
200
|
-
const isProfile = String(rec?.$type ||
|
|
268
|
+
const isProfile = String(rec?.$type || "").includes("identity.profile.Profile");
|
|
201
269
|
if (!isProfile)
|
|
202
270
|
return false;
|
|
203
271
|
// Match the entityUrn against the requested profile URN from data.*elements[0]
|
|
204
272
|
return rec.entityUrn === requestedProfileUrn;
|
|
205
273
|
})
|
|
206
274
|
: undefined;
|
|
207
|
-
|
|
275
|
+
// Validate that requested profile was found in response
|
|
276
|
+
if (requestedProfileUrn && !identityObj) {
|
|
277
|
+
const profilesInResponse = included.filter((it) => String(it?.$type || "").includes("identity.profile.Profile")).length;
|
|
278
|
+
try {
|
|
279
|
+
(0, logger_1.log)("error", "api.profileNotFoundInResponse", {
|
|
280
|
+
operation: "getProfileByUrn",
|
|
281
|
+
requestedUrn: requestedProfileUrn,
|
|
282
|
+
profilesInResponse,
|
|
283
|
+
selector: cacheKey,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
catch { }
|
|
287
|
+
throw new errors_1.LinkedInClientError("Requested profile not found in API response", errors_1.ERROR_CODES.PARSE_ERROR, 500);
|
|
288
|
+
}
|
|
289
|
+
const publicIdentifier = identityObj?.publicIdentifier || "";
|
|
208
290
|
try {
|
|
209
|
-
(0, logger_1.log)(
|
|
210
|
-
operation:
|
|
291
|
+
(0, logger_1.log)("debug", "api.extractedIdentity", {
|
|
292
|
+
operation: "getProfileByUrn",
|
|
211
293
|
publicIdentifier,
|
|
212
294
|
firstName: identityObj?.firstName,
|
|
213
295
|
lastName: identityObj?.lastName,
|
|
214
296
|
objectUrn: identityObj?.objectUrn,
|
|
215
|
-
profilesInResponse: included.filter(it => String(it?.$type ||
|
|
297
|
+
profilesInResponse: included.filter((it) => String(it?.$type || "").includes("identity.profile.Profile")).length,
|
|
216
298
|
});
|
|
217
299
|
}
|
|
218
300
|
catch { }
|
|
@@ -221,40 +303,100 @@ async function getProfileByUrn(fsdKey) {
|
|
|
221
303
|
// Pass publicIdentifier to parser so it can validate profile selection (same logic as getProfileByVanity)
|
|
222
304
|
prof = (0, profile_parser_1.parseFullProfile)(raw, publicIdentifier);
|
|
223
305
|
try {
|
|
224
|
-
(0, logger_1.log)(
|
|
306
|
+
(0, logger_1.log)("debug", "api.parsedProfile", {
|
|
307
|
+
operation: "getProfileByUrn",
|
|
308
|
+
firstName: prof.firstName,
|
|
309
|
+
lastName: prof.lastName,
|
|
310
|
+
vanity: prof.vanity,
|
|
311
|
+
});
|
|
225
312
|
}
|
|
226
313
|
catch { }
|
|
227
314
|
}
|
|
228
315
|
catch {
|
|
229
316
|
try {
|
|
230
|
-
(0, logger_1.log)(
|
|
317
|
+
(0, logger_1.log)("error", "api.parseError", {
|
|
318
|
+
operation: "getProfileByUrn",
|
|
319
|
+
selector: cacheKey,
|
|
320
|
+
});
|
|
231
321
|
}
|
|
232
322
|
catch { }
|
|
233
|
-
throw new errors_1.LinkedInClientError(
|
|
323
|
+
throw new errors_1.LinkedInClientError("Failed to parse profile", errors_1.ERROR_CODES.PARSE_ERROR, 500);
|
|
234
324
|
}
|
|
235
325
|
profileCacheByUrn.set(cacheKey, { data: prof, ts: Date.now() });
|
|
236
|
-
(0, metrics_1.incrementMetric)(
|
|
326
|
+
(0, metrics_1.incrementMetric)("profileFetches");
|
|
237
327
|
try {
|
|
238
|
-
(0, logger_1.log)(
|
|
328
|
+
(0, logger_1.log)("info", "api.ok", { operation: "getProfileByUrn", selector: cacheKey });
|
|
239
329
|
}
|
|
240
330
|
catch { }
|
|
241
331
|
return prof;
|
|
242
332
|
}
|
|
333
|
+
/**
|
|
334
|
+
* Searches Sales Navigator for leads matching the given criteria.
|
|
335
|
+
* Supports pagination, advanced filters, and session-based account stickiness.
|
|
336
|
+
*
|
|
337
|
+
* @param keywords - Search keywords (max 2000 characters)
|
|
338
|
+
* @param options - Search configuration options
|
|
339
|
+
* @param options.start - Pagination offset (default: 0)
|
|
340
|
+
* @param options.count - Results per page (default: 25)
|
|
341
|
+
* @param options.decorationId - LinkedIn decoration ID for response format
|
|
342
|
+
* @param options.filters - Advanced search filters (title, company, geography, etc.)
|
|
343
|
+
* @param options.rawQuery - Raw query string (bypasses filter encoding)
|
|
344
|
+
* @param options.sessionId - Session ID for consistent pagination (same account across pages)
|
|
345
|
+
* @returns SearchSalesResult with items array, pagination info, and session metadata
|
|
346
|
+
* @throws LinkedInClientError with code INVALID_INPUT if keywords exceed max length
|
|
347
|
+
*
|
|
348
|
+
* @example
|
|
349
|
+
* ```typescript
|
|
350
|
+
* // Simple search
|
|
351
|
+
* const results = await searchSalesLeads('CEO');
|
|
352
|
+
*
|
|
353
|
+
* // Paginated search with session persistence
|
|
354
|
+
* const page1 = await searchSalesLeads('CEO', { start: 0, count: 25 });
|
|
355
|
+
* const sessionId = page1._meta?.sessionId;
|
|
356
|
+
* const page2 = await searchSalesLeads('CEO', { start: 25, count: 25, sessionId });
|
|
357
|
+
*
|
|
358
|
+
* // With filters
|
|
359
|
+
* const filtered = await searchSalesLeads('', {
|
|
360
|
+
* filters: {
|
|
361
|
+
* role: { seniority_ids: [8, 9, 10] }, // VP, CXO, Partner
|
|
362
|
+
* company: { headcount: { include: ['501-1000', '1001-5000'] } }
|
|
363
|
+
* }
|
|
364
|
+
* });
|
|
365
|
+
* ```
|
|
366
|
+
*/
|
|
243
367
|
async function searchSalesLeads(keywords, options) {
|
|
244
368
|
const cfg = (0, config_1.getConfig)();
|
|
369
|
+
// Validate keyword length to prevent query injection and DoS
|
|
370
|
+
const MAX_KEYWORD_LENGTH = 2000;
|
|
371
|
+
const keywordsStr = String(keywords || "");
|
|
372
|
+
if (keywordsStr.length > MAX_KEYWORD_LENGTH) {
|
|
373
|
+
throw new errors_1.LinkedInClientError(`Search keywords too long (max ${MAX_KEYWORD_LENGTH} characters)`, errors_1.ERROR_CODES.INVALID_INPUT, 400);
|
|
374
|
+
}
|
|
245
375
|
const start = Number.isFinite(options?.start) ? Number(options.start) : 0;
|
|
246
376
|
const count = Number.isFinite(options?.count) ? Number(options.count) : 25;
|
|
247
|
-
const deco = options?.decorationId ||
|
|
248
|
-
|
|
249
|
-
const sessionId = options?.sessionId || (await Promise.resolve().then(() => __importStar(require('crypto')))).randomUUID();
|
|
377
|
+
const deco = options?.decorationId ||
|
|
378
|
+
"com.linkedin.sales.deco.desktop.searchv2.LeadSearchResult-14";
|
|
250
379
|
const sig = (0, search_encoder_1.buildFilterSignature)(options?.filters, options?.rawQuery);
|
|
251
|
-
//
|
|
252
|
-
|
|
380
|
+
// Build cache key WITHOUT sessionId to enable cache hits across sessions with same query
|
|
381
|
+
// Only include sessionId in cache key if explicitly provided (for session-specific caching)
|
|
382
|
+
const cacheKeyObj = {
|
|
383
|
+
k: String(keywords || "").toLowerCase(),
|
|
384
|
+
start,
|
|
385
|
+
count,
|
|
386
|
+
deco,
|
|
387
|
+
sig,
|
|
388
|
+
};
|
|
389
|
+
if (options?.sessionId) {
|
|
390
|
+
cacheKeyObj.sessionId = options.sessionId;
|
|
391
|
+
}
|
|
392
|
+
const cacheKey = JSON.stringify(cacheKeyObj);
|
|
253
393
|
const cached = getCached(searchCache, cacheKey, cfg.searchCacheTtl);
|
|
254
394
|
if (cached) {
|
|
255
|
-
(0, metrics_1.incrementMetric)(
|
|
395
|
+
(0, metrics_1.incrementMetric)("searchCacheHits");
|
|
256
396
|
return cached;
|
|
257
397
|
}
|
|
398
|
+
// Generate sessionId AFTER cache lookup to avoid breaking cache keys
|
|
399
|
+
const sessionId = options?.sessionId || (await Promise.resolve().then(() => __importStar(require("crypto")))).randomUUID();
|
|
258
400
|
let queryStruct;
|
|
259
401
|
if (options?.rawQuery) {
|
|
260
402
|
queryStruct = String(options.rawQuery);
|
|
@@ -265,16 +407,22 @@ async function searchSalesLeads(keywords, options) {
|
|
|
265
407
|
async function doRequest(decorationId) {
|
|
266
408
|
const url = `${SALES_NAV_BASE}/salesApiLeadSearch?q=searchQuery&start=${start}&count=${count}&decorationId=${encodeURIComponent(decorationId)}&query=${queryStruct}`;
|
|
267
409
|
try {
|
|
268
|
-
(0, logger_1.log)(
|
|
410
|
+
(0, logger_1.log)("info", "api.start", {
|
|
411
|
+
operation: "searchSalesLeads",
|
|
412
|
+
selector: { keywords, start, count, deco: decorationId, sessionId },
|
|
413
|
+
});
|
|
269
414
|
}
|
|
270
415
|
catch { }
|
|
271
416
|
const out = await (0, http_client_1.executeLinkedInRequest)({
|
|
272
417
|
url,
|
|
273
|
-
headers: { Referer:
|
|
418
|
+
headers: { Referer: "https://www.linkedin.com/sales/search/people" },
|
|
274
419
|
sessionId, // Phase 2.1: Pass sessionId for account stickiness
|
|
275
|
-
},
|
|
420
|
+
}, "searchSalesLeads");
|
|
276
421
|
try {
|
|
277
|
-
(0, logger_1.log)(
|
|
422
|
+
(0, logger_1.log)("info", "api.ok", {
|
|
423
|
+
operation: "searchSalesLeads",
|
|
424
|
+
selector: { keywords, start, count, deco: decorationId, sessionId },
|
|
425
|
+
});
|
|
278
426
|
}
|
|
279
427
|
catch { }
|
|
280
428
|
return out;
|
|
@@ -282,31 +430,44 @@ async function searchSalesLeads(keywords, options) {
|
|
|
282
430
|
// Use only the specified decoration ID - no fallbacks to ensure consistent data structure
|
|
283
431
|
const raw = await doRequest(deco);
|
|
284
432
|
const items = (0, search_parser_1.parseSalesSearchResults)(raw);
|
|
285
|
-
const rrec =
|
|
286
|
-
|
|
287
|
-
|
|
433
|
+
const rrec = raw && typeof raw === "object"
|
|
434
|
+
? raw
|
|
435
|
+
: undefined;
|
|
436
|
+
const pagingVal = rrec && "paging" in rrec
|
|
437
|
+
? rrec.paging
|
|
438
|
+
: undefined;
|
|
439
|
+
const p = pagingVal && typeof pagingVal === "object"
|
|
440
|
+
? pagingVal
|
|
441
|
+
: undefined;
|
|
288
442
|
const paging = p ?? { start, count };
|
|
289
443
|
// Extract metadata.totalDisplayCount (LinkedIn's display string like "500K+")
|
|
290
|
-
const metadataVal = rrec &&
|
|
291
|
-
|
|
444
|
+
const metadataVal = rrec && "metadata" in rrec
|
|
445
|
+
? rrec.metadata
|
|
446
|
+
: undefined;
|
|
447
|
+
const metadata = metadataVal && typeof metadataVal === "object"
|
|
448
|
+
? metadataVal
|
|
449
|
+
: undefined;
|
|
292
450
|
// Calculate hasMore using LinkedIn's total count (primary) or result length (fallback)
|
|
293
451
|
// LinkedIn API always returns paging.total, so we use it for accurate pagination
|
|
294
|
-
const hasMore = paging?.total
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
452
|
+
const hasMore = paging?.total
|
|
453
|
+
? start + count < paging.total
|
|
454
|
+
: items.length === count;
|
|
455
|
+
// Always return SearchSalesResult for consistent API (breaking change from v0.3.x)
|
|
456
|
+
const result = {
|
|
457
|
+
items,
|
|
458
|
+
page: {
|
|
459
|
+
start: Number(paging.start ?? start),
|
|
460
|
+
count: Number(paging.count ?? count),
|
|
461
|
+
total: paging?.total,
|
|
462
|
+
hasMore,
|
|
463
|
+
},
|
|
464
|
+
metadata: metadata?.totalDisplayCount
|
|
465
|
+
? { totalDisplayCount: metadata.totalDisplayCount }
|
|
466
|
+
: undefined,
|
|
467
|
+
_meta: { sessionId }, // Return sessionId to consumer
|
|
468
|
+
};
|
|
308
469
|
searchCache.set(cacheKey, { data: result, ts: Date.now() });
|
|
309
|
-
(0, metrics_1.incrementMetric)(
|
|
470
|
+
(0, metrics_1.incrementMetric)("searchCacheMisses");
|
|
310
471
|
return result;
|
|
311
472
|
}
|
|
312
473
|
async function getProfilesBatch(vanities, concurrency = 4) {
|
|
@@ -334,55 +495,92 @@ async function getProfilesBatch(vanities, concurrency = 4) {
|
|
|
334
495
|
// Companies (Voyager organizations)
|
|
335
496
|
// ---------------------------------------------------------------------------
|
|
336
497
|
async function resolveCompanyUniversalName(universalName) {
|
|
337
|
-
const url = `${LINKEDIN_API_BASE}/organization/companies?q=universalName&universalName=${encodeURIComponent(String(universalName ||
|
|
498
|
+
const url = `${LINKEDIN_API_BASE}/organization/companies?q=universalName&universalName=${encodeURIComponent(String(universalName || "").trim())}`;
|
|
338
499
|
try {
|
|
339
500
|
try {
|
|
340
|
-
(0, logger_1.log)(
|
|
501
|
+
(0, logger_1.log)("info", "api.start", {
|
|
502
|
+
operation: "resolveCompanyUniversalName",
|
|
503
|
+
selector: universalName,
|
|
504
|
+
});
|
|
341
505
|
}
|
|
342
506
|
catch { }
|
|
343
|
-
const raw = await (0, http_client_1.executeLinkedInRequest)({ url },
|
|
344
|
-
const rec =
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const
|
|
507
|
+
const raw = await (0, http_client_1.executeLinkedInRequest)({ url }, "resolveCompanyUniversalName");
|
|
508
|
+
const rec = raw && typeof raw === "object"
|
|
509
|
+
? raw
|
|
510
|
+
: undefined;
|
|
511
|
+
const elementsVal = rec && "elements" in rec
|
|
512
|
+
? rec.elements
|
|
513
|
+
: undefined;
|
|
514
|
+
const first = Array.isArray(elementsVal)
|
|
515
|
+
? elementsVal[0]
|
|
516
|
+
: undefined;
|
|
517
|
+
const id = first?.id ??
|
|
518
|
+
first?.entityUrn ??
|
|
519
|
+
undefined;
|
|
348
520
|
try {
|
|
349
|
-
(0, logger_1.log)(
|
|
521
|
+
(0, logger_1.log)("info", "api.ok", {
|
|
522
|
+
operation: "resolveCompanyUniversalName",
|
|
523
|
+
selector: universalName,
|
|
524
|
+
id,
|
|
525
|
+
});
|
|
350
526
|
}
|
|
351
527
|
catch { }
|
|
352
|
-
return {
|
|
528
|
+
return {
|
|
529
|
+
companyId: id
|
|
530
|
+
? String(id).replace(/^urn:li:fsd_company:/, "")
|
|
531
|
+
: undefined,
|
|
532
|
+
};
|
|
353
533
|
}
|
|
354
534
|
catch (e) {
|
|
355
535
|
const status = e?.status ?? 0;
|
|
356
536
|
if (status === 404) {
|
|
357
537
|
try {
|
|
358
|
-
(0, logger_1.log)(
|
|
538
|
+
(0, logger_1.log)("warn", "api.notFound", {
|
|
539
|
+
operation: "resolveCompanyUniversalName",
|
|
540
|
+
selector: universalName,
|
|
541
|
+
status,
|
|
542
|
+
});
|
|
359
543
|
}
|
|
360
544
|
catch { }
|
|
361
|
-
throw new errors_1.LinkedInClientError(
|
|
545
|
+
throw new errors_1.LinkedInClientError("Company not found", errors_1.ERROR_CODES.NOT_FOUND, 404);
|
|
362
546
|
}
|
|
363
547
|
throw e;
|
|
364
548
|
}
|
|
365
549
|
}
|
|
550
|
+
/**
|
|
551
|
+
* Fetches a company by its numeric LinkedIn ID.
|
|
552
|
+
* Results are cached for the configured TTL (default: 10 minutes).
|
|
553
|
+
*
|
|
554
|
+
* @param companyId - Numeric company ID (e.g., "1234567")
|
|
555
|
+
* @returns Company object with name, description, size, headquarters, etc.
|
|
556
|
+
* @throws LinkedInClientError with code NOT_FOUND if company doesn't exist
|
|
557
|
+
*
|
|
558
|
+
* @example
|
|
559
|
+
* ```typescript
|
|
560
|
+
* const company = await getCompanyById('1234567');
|
|
561
|
+
* console.log(company.name, company.sizeLabel);
|
|
562
|
+
* ```
|
|
563
|
+
*/
|
|
366
564
|
async function getCompanyById(companyId) {
|
|
367
565
|
const cfg = (0, config_1.getConfig)();
|
|
368
|
-
const key = String(companyId ||
|
|
566
|
+
const key = String(companyId || "").trim();
|
|
369
567
|
const cached = getCached(companyCache, key, cfg.companyCacheTtl);
|
|
370
568
|
if (cached) {
|
|
371
|
-
(0, metrics_1.incrementMetric)(
|
|
569
|
+
(0, metrics_1.incrementMetric)("companyCacheHits");
|
|
372
570
|
return cached;
|
|
373
571
|
}
|
|
374
572
|
const url = `${LINKEDIN_API_BASE}/entities/companies/${encodeURIComponent(key)}`;
|
|
375
573
|
try {
|
|
376
574
|
try {
|
|
377
|
-
(0, logger_1.log)(
|
|
575
|
+
(0, logger_1.log)("info", "api.start", { operation: "getCompanyById", selector: key });
|
|
378
576
|
}
|
|
379
577
|
catch { }
|
|
380
|
-
const raw = await (0, http_client_1.executeLinkedInRequest)({ url },
|
|
578
|
+
const raw = await (0, http_client_1.executeLinkedInRequest)({ url }, "getCompanyById");
|
|
381
579
|
const parsed = (0, company_parser_1.parseCompany)(raw);
|
|
382
|
-
(0, metrics_1.incrementMetric)(
|
|
580
|
+
(0, metrics_1.incrementMetric)("companyFetches");
|
|
383
581
|
companyCache.set(key, { data: parsed, ts: Date.now() });
|
|
384
582
|
try {
|
|
385
|
-
(0, logger_1.log)(
|
|
583
|
+
(0, logger_1.log)("info", "api.ok", { operation: "getCompanyById", selector: key });
|
|
386
584
|
}
|
|
387
585
|
catch { }
|
|
388
586
|
return parsed;
|
|
@@ -391,35 +589,41 @@ async function getCompanyById(companyId) {
|
|
|
391
589
|
const status = e?.status ?? 0;
|
|
392
590
|
if (status === 404) {
|
|
393
591
|
try {
|
|
394
|
-
(0, logger_1.log)(
|
|
592
|
+
(0, logger_1.log)("warn", "api.notFound", {
|
|
593
|
+
operation: "getCompanyById",
|
|
594
|
+
selector: key,
|
|
595
|
+
status,
|
|
596
|
+
});
|
|
395
597
|
}
|
|
396
598
|
catch { }
|
|
397
|
-
throw new errors_1.LinkedInClientError(
|
|
599
|
+
throw new errors_1.LinkedInClientError("Company not found", errors_1.ERROR_CODES.NOT_FOUND, 404);
|
|
398
600
|
}
|
|
399
601
|
throw e;
|
|
400
602
|
}
|
|
401
603
|
}
|
|
402
604
|
async function getCompanyByUrl(companyUrl) {
|
|
403
|
-
const input = String(companyUrl ||
|
|
605
|
+
const input = String(companyUrl || "").trim();
|
|
404
606
|
let url;
|
|
405
607
|
try {
|
|
406
608
|
// Require explicit http/https scheme; do not auto-prepend
|
|
407
609
|
if (!/^https?:\/\//i.test(input)) {
|
|
408
|
-
throw new errors_1.LinkedInClientError(
|
|
610
|
+
throw new errors_1.LinkedInClientError("Invalid company URL scheme", errors_1.ERROR_CODES.INVALID_INPUT, 400);
|
|
409
611
|
}
|
|
410
612
|
url = new URL(input);
|
|
411
613
|
}
|
|
412
614
|
catch {
|
|
413
|
-
throw new errors_1.LinkedInClientError(
|
|
615
|
+
throw new errors_1.LinkedInClientError("Invalid company URL", errors_1.ERROR_CODES.INVALID_INPUT, 400);
|
|
414
616
|
}
|
|
415
617
|
const m = url.pathname.match(/\/company\/([^\/]+)/i);
|
|
416
618
|
if (!m) {
|
|
417
619
|
try {
|
|
418
|
-
(0, logger_1.log)(
|
|
620
|
+
(0, logger_1.log)("error", "api.invalidInput", {
|
|
621
|
+
operation: "getCompanyByUrl",
|
|
622
|
+
selector: companyUrl,
|
|
623
|
+
});
|
|
419
624
|
}
|
|
420
625
|
catch { }
|
|
421
|
-
;
|
|
422
|
-
throw new errors_1.LinkedInClientError('Invalid company URL path', errors_1.ERROR_CODES.INVALID_INPUT, 400);
|
|
626
|
+
throw new errors_1.LinkedInClientError("Invalid company URL path", errors_1.ERROR_CODES.INVALID_INPUT, 400);
|
|
423
627
|
}
|
|
424
628
|
const ident = decodeURIComponent(m[1]);
|
|
425
629
|
if (/^\d+$/.test(ident)) {
|
|
@@ -428,50 +632,86 @@ async function getCompanyByUrl(companyUrl) {
|
|
|
428
632
|
const resolved = await resolveCompanyUniversalName(ident);
|
|
429
633
|
if (!resolved.companyId) {
|
|
430
634
|
try {
|
|
431
|
-
(0, logger_1.log)(
|
|
635
|
+
(0, logger_1.log)("warn", "api.notFound", {
|
|
636
|
+
operation: "getCompanyByUrl",
|
|
637
|
+
selector: companyUrl,
|
|
638
|
+
});
|
|
432
639
|
}
|
|
433
640
|
catch { }
|
|
434
|
-
;
|
|
435
|
-
throw new errors_1.LinkedInClientError('Company not found', errors_1.ERROR_CODES.NOT_FOUND, 404);
|
|
641
|
+
throw new errors_1.LinkedInClientError("Company not found", errors_1.ERROR_CODES.NOT_FOUND, 404);
|
|
436
642
|
}
|
|
437
643
|
return getCompanyById(resolved.companyId);
|
|
438
644
|
}
|
|
645
|
+
/**
|
|
646
|
+
* Fetch multiple companies in parallel with controlled concurrency.
|
|
647
|
+
* @param companyIds - Array of company IDs (numeric strings)
|
|
648
|
+
* @param concurrency - Maximum parallel requests (default: 4, max: 16)
|
|
649
|
+
* @returns Array of Company objects (null for failed fetches, preserves order)
|
|
650
|
+
*/
|
|
651
|
+
async function getCompaniesBatch(companyIds, concurrency = 4) {
|
|
652
|
+
const limit = Math.max(1, Math.min(concurrency || 1, 16));
|
|
653
|
+
const results = Array.from({ length: companyIds.length }, () => null);
|
|
654
|
+
let idx = 0;
|
|
655
|
+
async function worker() {
|
|
656
|
+
while (idx < companyIds.length) {
|
|
657
|
+
const myIdx = idx++;
|
|
658
|
+
const companyId = companyIds[myIdx];
|
|
659
|
+
try {
|
|
660
|
+
const company = await getCompanyById(companyId);
|
|
661
|
+
results[myIdx] = company ?? null;
|
|
662
|
+
}
|
|
663
|
+
catch {
|
|
664
|
+
results[myIdx] = null;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
const workers = Array.from({ length: Math.min(limit, companyIds.length) }, () => worker());
|
|
669
|
+
await Promise.all(workers);
|
|
670
|
+
return results;
|
|
671
|
+
}
|
|
439
672
|
// ---------------------------------------------------------------------------
|
|
440
673
|
// Typeahead (Sales Navigator facets)
|
|
441
674
|
// ---------------------------------------------------------------------------
|
|
442
675
|
async function typeahead(options) {
|
|
443
676
|
const cfg = (0, config_1.getConfig)();
|
|
444
|
-
const type = String(options?.type ||
|
|
677
|
+
const type = String(options?.type || "").trim();
|
|
445
678
|
const start = Number.isFinite(options?.start) ? Number(options.start) : 0;
|
|
446
679
|
const count = Number.isFinite(options?.count) ? Number(options.count) : 10;
|
|
447
|
-
const query = (options?.query ??
|
|
680
|
+
const query = (options?.query ?? "").toString().trim();
|
|
448
681
|
// Return static defaults when no query provided for supported types
|
|
449
682
|
if (!query) {
|
|
450
683
|
let staticOptions;
|
|
451
684
|
switch (type) {
|
|
452
|
-
case
|
|
685
|
+
case "FUNCTION":
|
|
453
686
|
staticOptions = constants_1.FUNCTION_OPTIONS;
|
|
454
687
|
break;
|
|
455
|
-
case
|
|
688
|
+
case "BING_GEO":
|
|
456
689
|
staticOptions = constants_1.REGION_OPTIONS;
|
|
457
690
|
break;
|
|
458
|
-
case
|
|
691
|
+
case "PROFILE_LANGUAGE":
|
|
459
692
|
staticOptions = constants_1.LANGUAGE_OPTIONS;
|
|
460
693
|
break;
|
|
461
|
-
case
|
|
694
|
+
case "INDUSTRY":
|
|
462
695
|
staticOptions = constants_1.INDUSTRY_OPTIONS;
|
|
463
696
|
break;
|
|
464
|
-
case
|
|
697
|
+
case "SENIORITY_LEVEL":
|
|
465
698
|
staticOptions = constants_1.SENIORITY_OPTIONS;
|
|
466
699
|
break;
|
|
467
|
-
case
|
|
700
|
+
case "COMPANY_SIZE":
|
|
468
701
|
staticOptions = constants_1.COMPANY_SIZE_OPTIONS;
|
|
469
702
|
break;
|
|
470
703
|
}
|
|
471
704
|
if (staticOptions) {
|
|
472
705
|
return {
|
|
473
|
-
items: staticOptions.map(opt => ({
|
|
474
|
-
|
|
706
|
+
items: staticOptions.map((opt) => ({
|
|
707
|
+
id: String(opt.id),
|
|
708
|
+
text: opt.text,
|
|
709
|
+
})),
|
|
710
|
+
page: {
|
|
711
|
+
start: 0,
|
|
712
|
+
count: staticOptions.length,
|
|
713
|
+
total: staticOptions.length,
|
|
714
|
+
},
|
|
475
715
|
};
|
|
476
716
|
}
|
|
477
717
|
}
|
|
@@ -479,36 +719,58 @@ async function typeahead(options) {
|
|
|
479
719
|
const cacheKey = JSON.stringify({ type, query, start, count });
|
|
480
720
|
const cached = getCached(typeaheadCache, cacheKey, cfg.typeaheadCacheTtl);
|
|
481
721
|
if (cached) {
|
|
482
|
-
(0, metrics_1.incrementMetric)(
|
|
722
|
+
(0, metrics_1.incrementMetric)("typeaheadCacheHits");
|
|
483
723
|
return cached;
|
|
484
724
|
}
|
|
485
725
|
let url = `${SALES_NAV_BASE}/salesApiFacetTypeahead?q=query&type=${encodeURIComponent(type)}&start=${start}&count=${count}`;
|
|
486
726
|
if (query)
|
|
487
727
|
url += `&query=${encodeURIComponent(query)}`;
|
|
488
|
-
(0, metrics_1.incrementMetric)(
|
|
728
|
+
(0, metrics_1.incrementMetric)("typeaheadRequests");
|
|
489
729
|
try {
|
|
490
|
-
(0, logger_1.log)(
|
|
730
|
+
(0, logger_1.log)("info", "api.start", {
|
|
731
|
+
operation: "typeahead",
|
|
732
|
+
selector: { type, query, start, count },
|
|
733
|
+
});
|
|
491
734
|
}
|
|
492
735
|
catch { }
|
|
493
|
-
const raw = await (0, http_client_1.executeLinkedInRequest)({ url },
|
|
736
|
+
const raw = await (0, http_client_1.executeLinkedInRequest)({ url }, "typeahead");
|
|
494
737
|
const items = Array.isArray(raw?.elements)
|
|
495
738
|
? raw.elements.map((it) => ({
|
|
496
|
-
id: String(it?.id ??
|
|
739
|
+
id: String(it?.id ??
|
|
740
|
+
it?.backendId ??
|
|
741
|
+
""),
|
|
497
742
|
text: it.displayValue ??
|
|
498
743
|
it.headline?.text ??
|
|
499
744
|
it.text ??
|
|
500
745
|
it.name ??
|
|
501
746
|
it.label ??
|
|
502
|
-
|
|
747
|
+
"",
|
|
503
748
|
}))
|
|
504
749
|
: [];
|
|
505
|
-
const rtype =
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
const
|
|
750
|
+
const rtype = raw && typeof raw === "object"
|
|
751
|
+
? raw
|
|
752
|
+
: undefined;
|
|
753
|
+
const pagingVal2 = rtype && "paging" in rtype
|
|
754
|
+
? rtype.paging
|
|
755
|
+
: undefined;
|
|
756
|
+
const paging2 = pagingVal2 && typeof pagingVal2 === "object"
|
|
757
|
+
? pagingVal2
|
|
758
|
+
: undefined;
|
|
759
|
+
const result = {
|
|
760
|
+
items,
|
|
761
|
+
page: {
|
|
762
|
+
start: Number(paging2?.start ?? start),
|
|
763
|
+
count: Number(paging2?.count ?? count),
|
|
764
|
+
total: paging2?.total,
|
|
765
|
+
},
|
|
766
|
+
};
|
|
509
767
|
typeaheadCache.set(cacheKey, { data: result, ts: Date.now() });
|
|
510
768
|
try {
|
|
511
|
-
(0, logger_1.log)(
|
|
769
|
+
(0, logger_1.log)("info", "api.ok", {
|
|
770
|
+
operation: "typeahead",
|
|
771
|
+
selector: { type, query },
|
|
772
|
+
count: items.length,
|
|
773
|
+
});
|
|
512
774
|
}
|
|
513
775
|
catch { }
|
|
514
776
|
return result;
|
|
@@ -532,12 +794,12 @@ Object.defineProperty(exports, "COMPANY_SIZE_OPTIONS", { enumerable: true, get:
|
|
|
532
794
|
*/
|
|
533
795
|
async function getYearsAtCompanyOptions() {
|
|
534
796
|
return {
|
|
535
|
-
items: constants_1.YEARS_OPTIONS.map(opt => ({ id: opt.id, text: opt.text })),
|
|
797
|
+
items: constants_1.YEARS_OPTIONS.map((opt) => ({ id: opt.id, text: opt.text })),
|
|
536
798
|
page: {
|
|
537
799
|
start: 0,
|
|
538
800
|
count: constants_1.YEARS_OPTIONS.length,
|
|
539
|
-
total: constants_1.YEARS_OPTIONS.length
|
|
540
|
-
}
|
|
801
|
+
total: constants_1.YEARS_OPTIONS.length,
|
|
802
|
+
},
|
|
541
803
|
};
|
|
542
804
|
}
|
|
543
805
|
/**
|
|
@@ -545,12 +807,12 @@ async function getYearsAtCompanyOptions() {
|
|
|
545
807
|
*/
|
|
546
808
|
async function getYearsInPositionOptions() {
|
|
547
809
|
return {
|
|
548
|
-
items: constants_1.YEARS_OPTIONS.map(opt => ({ id: opt.id, text: opt.text })),
|
|
810
|
+
items: constants_1.YEARS_OPTIONS.map((opt) => ({ id: opt.id, text: opt.text })),
|
|
549
811
|
page: {
|
|
550
812
|
start: 0,
|
|
551
813
|
count: constants_1.YEARS_OPTIONS.length,
|
|
552
|
-
total: constants_1.YEARS_OPTIONS.length
|
|
553
|
-
}
|
|
814
|
+
total: constants_1.YEARS_OPTIONS.length,
|
|
815
|
+
},
|
|
554
816
|
};
|
|
555
817
|
}
|
|
556
818
|
/**
|
|
@@ -558,21 +820,21 @@ async function getYearsInPositionOptions() {
|
|
|
558
820
|
*/
|
|
559
821
|
async function getYearsOfExperienceOptions() {
|
|
560
822
|
return {
|
|
561
|
-
items: constants_1.YEARS_OPTIONS.map(opt => ({ id: opt.id, text: opt.text })),
|
|
823
|
+
items: constants_1.YEARS_OPTIONS.map((opt) => ({ id: opt.id, text: opt.text })),
|
|
562
824
|
page: {
|
|
563
825
|
start: 0,
|
|
564
826
|
count: constants_1.YEARS_OPTIONS.length,
|
|
565
|
-
total: constants_1.YEARS_OPTIONS.length
|
|
566
|
-
}
|
|
827
|
+
total: constants_1.YEARS_OPTIONS.length,
|
|
828
|
+
},
|
|
567
829
|
};
|
|
568
830
|
}
|
|
569
831
|
// ---------------------------------------------------------------------------
|
|
570
832
|
// Sales Navigator profile details
|
|
571
833
|
// ---------------------------------------------------------------------------
|
|
572
834
|
async function getSalesNavigatorProfileDetails(profileUrnOrId) {
|
|
573
|
-
const idOrUrn = String(profileUrnOrId ||
|
|
835
|
+
const idOrUrn = String(profileUrnOrId || "").trim();
|
|
574
836
|
// Build Sales API path supporting URN triple or explicit profileId/authType/authToken
|
|
575
|
-
let pathSeg =
|
|
837
|
+
let pathSeg = "";
|
|
576
838
|
let pidFromTriple = null;
|
|
577
839
|
// URN with triple
|
|
578
840
|
const mUrn = idOrUrn.match(/^urn:li:(?:fs_salesProfile|fsd_profile):\(([^,\s)]+),([^,\s)]+),([^,\s)]+)\)/i);
|
|
@@ -581,25 +843,35 @@ async function getSalesNavigatorProfileDetails(profileUrnOrId) {
|
|
|
581
843
|
pathSeg = `(profileId:${pid},authType:${authType},authToken:${authToken})`;
|
|
582
844
|
pidFromTriple = pid;
|
|
583
845
|
}
|
|
584
|
-
else if (/profileId:/.test(idOrUrn) &&
|
|
846
|
+
else if (/profileId:/.test(idOrUrn) &&
|
|
847
|
+
/authType:/.test(idOrUrn) &&
|
|
848
|
+
/authToken:/.test(idOrUrn)) {
|
|
585
849
|
// Already the triple form without urn prefix
|
|
586
|
-
pathSeg = `(${idOrUrn.replace(/^\(|\)$/g,
|
|
850
|
+
pathSeg = `(${idOrUrn.replace(/^\(|\)$/g, "")})`;
|
|
587
851
|
const m = idOrUrn.match(/profileId:([^,\s)]+)/);
|
|
588
852
|
pidFromTriple = m ? m[1] : null;
|
|
589
853
|
}
|
|
590
854
|
else {
|
|
591
855
|
// Fallback to simple id/urn
|
|
592
|
-
pathSeg = idOrUrn.startsWith(
|
|
856
|
+
pathSeg = idOrUrn.startsWith("urn:")
|
|
857
|
+
? idOrUrn
|
|
858
|
+
: encodeURIComponent(idOrUrn);
|
|
593
859
|
}
|
|
594
|
-
const decoration = encodeURIComponent(
|
|
860
|
+
const decoration = encodeURIComponent("(entityUrn,objectUrn,firstName,lastName,fullName,headline,pronoun,degree,profileUnlockInfo,location,listCount,summary,savedLead,defaultPosition,contactInfo,crmStatus,pendingInvitation,unlocked,flagshipProfileUrl,positions*(companyName,current,new,description,endedOn,posId,startedOn,title,location,companyUrn~fs_salesCompany(entityUrn,name,companyPictureDisplayImage)))");
|
|
595
861
|
const base = `${SALES_NAV_BASE}/salesApiProfiles/`;
|
|
596
862
|
async function requestWith(seg) {
|
|
597
863
|
const url = `${base}${seg}?decoration=${decoration}`;
|
|
598
|
-
return (0, http_client_1.executeLinkedInRequest)({
|
|
864
|
+
return (0, http_client_1.executeLinkedInRequest)({
|
|
865
|
+
url,
|
|
866
|
+
headers: { Referer: "https://www.linkedin.com/sales/search/people" },
|
|
867
|
+
}, "getSalesNavigatorProfileDetails");
|
|
599
868
|
}
|
|
600
869
|
try {
|
|
601
870
|
try {
|
|
602
|
-
(0, logger_1.log)(
|
|
871
|
+
(0, logger_1.log)("info", "api.start", {
|
|
872
|
+
operation: "getSalesNavigatorProfileDetails",
|
|
873
|
+
selector: idOrUrn,
|
|
874
|
+
});
|
|
603
875
|
}
|
|
604
876
|
catch { }
|
|
605
877
|
let raw;
|
|
@@ -612,24 +884,34 @@ async function getSalesNavigatorProfileDetails(profileUrnOrId) {
|
|
|
612
884
|
// Fallback: some environments reject the triple form; retry with bare id
|
|
613
885
|
const fallbackSeg = encodeURIComponent(pidFromTriple);
|
|
614
886
|
try {
|
|
615
|
-
(0, logger_1.log)(
|
|
887
|
+
(0, logger_1.log)("warn", "api.salesProfileFallback", {
|
|
888
|
+
from: pathSeg,
|
|
889
|
+
to: fallbackSeg,
|
|
890
|
+
});
|
|
616
891
|
}
|
|
617
892
|
catch { }
|
|
618
893
|
raw = await requestWith(fallbackSeg);
|
|
619
894
|
}
|
|
620
895
|
else if (status === 404) {
|
|
621
896
|
try {
|
|
622
|
-
(0, logger_1.log)(
|
|
897
|
+
(0, logger_1.log)("warn", "api.notFound", {
|
|
898
|
+
operation: "getSalesNavigatorProfileDetails",
|
|
899
|
+
selector: idOrUrn,
|
|
900
|
+
status,
|
|
901
|
+
});
|
|
623
902
|
}
|
|
624
903
|
catch { }
|
|
625
|
-
throw new errors_1.LinkedInClientError(
|
|
904
|
+
throw new errors_1.LinkedInClientError("Sales profile not found", errors_1.ERROR_CODES.NOT_FOUND, 404);
|
|
626
905
|
}
|
|
627
906
|
else {
|
|
628
907
|
throw e;
|
|
629
908
|
}
|
|
630
909
|
}
|
|
631
910
|
try {
|
|
632
|
-
(0, logger_1.log)(
|
|
911
|
+
(0, logger_1.log)("info", "api.ok", {
|
|
912
|
+
operation: "getSalesNavigatorProfileDetails",
|
|
913
|
+
selector: idOrUrn,
|
|
914
|
+
});
|
|
633
915
|
}
|
|
634
916
|
catch { }
|
|
635
917
|
return raw;
|