linkedin-secret-sauce 0.1.1 → 0.2.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.
@@ -4,17 +4,29 @@ exports.getProfileByVanity = getProfileByVanity;
4
4
  exports.getProfileByUrn = getProfileByUrn;
5
5
  exports.searchSalesLeads = searchSalesLeads;
6
6
  exports.getProfilesBatch = getProfilesBatch;
7
+ exports.resolveCompanyUniversalName = resolveCompanyUniversalName;
8
+ exports.getCompanyById = getCompanyById;
9
+ exports.getCompanyByUrl = getCompanyByUrl;
10
+ exports.typeahead = typeahead;
11
+ exports.getSalesNavigatorProfileDetails = getSalesNavigatorProfileDetails;
7
12
  const config_1 = require("./config");
8
13
  const http_client_1 = require("./http-client");
9
14
  const profile_parser_1 = require("./parsers/profile-parser");
10
15
  const search_parser_1 = require("./parsers/search-parser");
16
+ const search_encoder_1 = require("./utils/search-encoder");
17
+ const company_parser_1 = require("./parsers/company-parser");
11
18
  const metrics_1 = require("./utils/metrics");
19
+ const logger_1 = require("./utils/logger");
20
+ const errors_1 = require("./utils/errors");
12
21
  const LINKEDIN_API_BASE = 'https://www.linkedin.com/voyager/api';
13
22
  const SALES_NAV_BASE = 'https://www.linkedin.com/sales-api';
14
23
  // In-memory caches (per-process)
15
24
  const profileCacheByVanity = new Map();
16
25
  const profileCacheByUrn = new Map();
17
26
  const searchCache = new Map();
27
+ // New caches for Phase 2
28
+ const companyCache = new Map();
29
+ const typeaheadCache = new Map();
18
30
  // In-flight dedupe for profiles by vanity
19
31
  const inflightByVanity = new Map();
20
32
  function getCached(map, key, ttl) {
@@ -30,6 +42,10 @@ function getCached(map, key, ttl) {
30
42
  async function getProfileByVanity(vanity) {
31
43
  const cfg = (0, config_1.getConfig)();
32
44
  const key = String(vanity || '').toLowerCase();
45
+ try {
46
+ (0, logger_1.log)('info', 'api.start', { operation: 'getProfileByVanity', selector: key });
47
+ }
48
+ catch { }
33
49
  // Cache
34
50
  const cached = getCached(profileCacheByVanity, key, cfg.profileCacheTtl);
35
51
  if (cached) {
@@ -45,10 +61,38 @@ async function getProfileByVanity(vanity) {
45
61
  const url = `${LINKEDIN_API_BASE}/identity/dash/profiles?q=memberIdentity&memberIdentity=${encodeURIComponent(vanity)}&decorationId=com.linkedin.voyager.dash.deco.identity.profile.FullProfileWithEntities-35`;
46
62
  const p = (async () => {
47
63
  try {
48
- const raw = await (0, http_client_1.executeLinkedInRequest)({ url }, 'getProfileByVanity');
49
- const prof = (0, profile_parser_1.parseFullProfile)(raw, vanity);
64
+ let raw;
65
+ try {
66
+ raw = await (0, http_client_1.executeLinkedInRequest)({ url }, 'getProfileByVanity');
67
+ }
68
+ catch (e) {
69
+ const status = e?.status ?? 0;
70
+ if (status === 404) {
71
+ try {
72
+ (0, logger_1.log)('warn', 'api.notFound', { operation: 'getProfileByVanity', selector: key, status });
73
+ }
74
+ catch { }
75
+ throw new errors_1.LinkedInClientError('Profile not found', errors_1.ERROR_CODES.NOT_FOUND, 404);
76
+ }
77
+ throw e;
78
+ }
79
+ let prof;
80
+ try {
81
+ prof = (0, profile_parser_1.parseFullProfile)(raw, vanity);
82
+ }
83
+ catch {
84
+ try {
85
+ (0, logger_1.log)('error', 'api.parseError', { operation: 'getProfileByVanity', selector: key });
86
+ }
87
+ catch { }
88
+ throw new errors_1.LinkedInClientError('Failed to parse profile', errors_1.ERROR_CODES.PARSE_ERROR, 500);
89
+ }
50
90
  profileCacheByVanity.set(key, { data: prof, ts: Date.now() });
51
91
  (0, metrics_1.incrementMetric)('profileFetches');
92
+ try {
93
+ (0, logger_1.log)('info', 'api.ok', { operation: 'getProfileByVanity', selector: key });
94
+ }
95
+ catch { }
52
96
  return prof;
53
97
  }
54
98
  finally {
@@ -60,57 +104,137 @@ async function getProfileByVanity(vanity) {
60
104
  }
61
105
  async function getProfileByUrn(fsdKey) {
62
106
  const cfg = (0, config_1.getConfig)();
63
- const key = String(fsdKey || '').toLowerCase();
64
- const cachedUrn = getCached(profileCacheByUrn, key, cfg.profileCacheTtl);
107
+ // Normalize input: accept bare KEY or URN variants and extract KEY
108
+ const input = String(fsdKey || '').trim();
109
+ const keyMatch = input.match(/^urn:li:fsd_profile:([^\s/]+)$/i)?.[1] ||
110
+ input.match(/^urn:li:fs_salesProfile:\(([^,\s)]+)/i)?.[1] ||
111
+ (input.match(/^[A-Za-z0-9_-]+$/) ? input : null);
112
+ if (!keyMatch) {
113
+ throw new errors_1.LinkedInClientError('Invalid URN or key', errors_1.ERROR_CODES.INVALID_INPUT, 400);
114
+ }
115
+ const cacheKey = keyMatch.toLowerCase();
116
+ try {
117
+ (0, logger_1.log)('info', 'api.start', { operation: 'getProfileByUrn', selector: cacheKey });
118
+ }
119
+ catch { }
120
+ const cachedUrn = getCached(profileCacheByUrn, cacheKey, cfg.profileCacheTtl);
65
121
  if (cachedUrn) {
66
122
  (0, metrics_1.incrementMetric)('profileCacheHits');
67
123
  return cachedUrn;
68
124
  }
69
- const url = `${LINKEDIN_API_BASE}/identity/dash/profiles?q=memberIdentity&memberIdentity=${encodeURIComponent(fsdKey)}&decorationId=com.linkedin.voyager.dash.deco.identity.profile.FullProfileWithEntities-35`;
70
- const raw = await (0, http_client_1.executeLinkedInRequest)({ url }, 'getProfileByUrn');
71
- const prof = (0, profile_parser_1.parseFullProfile)(raw, '');
72
- profileCacheByUrn.set(key, { data: prof, ts: Date.now() });
125
+ const url = `${LINKEDIN_API_BASE}/identity/dash/profiles?q=memberIdentity&memberIdentity=${encodeURIComponent(keyMatch)}&decorationId=com.linkedin.voyager.dash.deco.identity.profile.FullProfileWithEntities-35`;
126
+ let raw;
127
+ try {
128
+ raw = await (0, http_client_1.executeLinkedInRequest)({ url }, 'getProfileByUrn');
129
+ }
130
+ catch (e) {
131
+ const status = e?.status ?? 0;
132
+ if (status === 404) {
133
+ try {
134
+ (0, logger_1.log)('warn', 'api.notFound', { operation: 'getProfileByUrn', selector: cacheKey, status });
135
+ }
136
+ catch { }
137
+ throw new errors_1.LinkedInClientError('Profile not found', errors_1.ERROR_CODES.NOT_FOUND, 404);
138
+ }
139
+ throw e;
140
+ }
141
+ let prof;
142
+ try {
143
+ prof = (0, profile_parser_1.parseFullProfile)(raw, '');
144
+ }
145
+ catch {
146
+ try {
147
+ (0, logger_1.log)('error', 'api.parseError', { operation: 'getProfileByUrn', selector: cacheKey });
148
+ }
149
+ catch { }
150
+ throw new errors_1.LinkedInClientError('Failed to parse profile', errors_1.ERROR_CODES.PARSE_ERROR, 500);
151
+ }
152
+ profileCacheByUrn.set(cacheKey, { data: prof, ts: Date.now() });
73
153
  (0, metrics_1.incrementMetric)('profileFetches');
154
+ try {
155
+ (0, logger_1.log)('info', 'api.ok', { operation: 'getProfileByUrn', selector: cacheKey });
156
+ }
157
+ catch { }
74
158
  return prof;
75
159
  }
76
- async function searchSalesLeads(keywords) {
160
+ async function searchSalesLeads(keywords, options) {
77
161
  const cfg = (0, config_1.getConfig)();
78
- const key = String(keywords || '').toLowerCase();
79
- const cachedSearch = getCached(searchCache, key, cfg.searchCacheTtl);
80
- if (cachedSearch) {
162
+ const start = Number.isFinite(options?.start) ? Number(options.start) : 0;
163
+ const count = Number.isFinite(options?.count) ? Number(options.count) : 25;
164
+ const deco = options?.decorationId || 'com.linkedin.sales.deco.desktop.searchv2.LeadSearchResult-14';
165
+ const sig = (0, search_encoder_1.buildFilterSignature)(options?.filters, options?.rawQuery);
166
+ const cacheKey = JSON.stringify({ k: String(keywords || '').toLowerCase(), start, count, deco, sig });
167
+ const cached = getCached(searchCache, cacheKey, cfg.searchCacheTtl);
168
+ if (cached) {
81
169
  (0, metrics_1.incrementMetric)('searchCacheHits');
82
- return cachedSearch;
83
- }
84
- const encodedKeywords = String(keywords || '').replace(/\s+/g, '%20');
85
- const queryStruct = `(spellCorrectionEnabled:true,keywords:${encodedKeywords})`;
86
- const url = `${SALES_NAV_BASE}/salesApiLeadSearch?q=searchQuery&start=0&count=25&decorationId=com.linkedin.sales.deco.desktop.searchv2.LeadSearchResult-14&query=${queryStruct}`;
87
- const raw = await (0, http_client_1.executeLinkedInRequest)({
88
- url,
89
- headers: { Referer: 'https://www.linkedin.com/sales/search/people' },
90
- }, 'searchSalesLeads');
91
- const results = (0, search_parser_1.parseSalesSearchResults)(raw);
92
- searchCache.set(key, { data: results, ts: Date.now() });
170
+ return cached;
171
+ }
172
+ let queryStruct;
173
+ if (options?.rawQuery) {
174
+ queryStruct = String(options.rawQuery);
175
+ }
176
+ else {
177
+ queryStruct = (0, search_encoder_1.buildLeadSearchQuery)(keywords, options?.filters);
178
+ }
179
+ async function doRequest(decorationId) {
180
+ const url = `${SALES_NAV_BASE}/salesApiLeadSearch?q=searchQuery&start=${start}&count=${count}&decorationId=${encodeURIComponent(decorationId)}&query=${queryStruct}`;
181
+ try {
182
+ (0, logger_1.log)('info', 'api.start', { operation: 'searchSalesLeads', selector: { keywords, start, count, deco: decorationId } });
183
+ }
184
+ catch { }
185
+ const out = await (0, http_client_1.executeLinkedInRequest)({
186
+ url,
187
+ headers: { Referer: 'https://www.linkedin.com/sales/search/people' },
188
+ }, 'searchSalesLeads');
189
+ try {
190
+ (0, logger_1.log)('info', 'api.ok', { operation: 'searchSalesLeads', selector: { keywords, start, count, deco: decorationId } });
191
+ }
192
+ catch { }
193
+ return out;
194
+ }
195
+ let raw;
196
+ try {
197
+ raw = await doRequest(deco);
198
+ }
199
+ catch (e) {
200
+ if ((e?.status ?? 0) === 400) {
201
+ const fallback = deco.replace(/LeadSearchResult-\d+/, 'LeadSearchResult-17');
202
+ try {
203
+ (0, logger_1.log)('warn', 'api.decoFallback', { from: deco, to: fallback });
204
+ }
205
+ catch { }
206
+ raw = await doRequest(fallback);
207
+ }
208
+ else {
209
+ throw e;
210
+ }
211
+ }
212
+ const items = (0, search_parser_1.parseSalesSearchResults)(raw);
213
+ const rrec = (raw && typeof raw === 'object') ? raw : undefined;
214
+ const pagingVal = rrec && 'paging' in rrec ? rrec.paging : undefined;
215
+ const p = (pagingVal && typeof pagingVal === 'object') ? pagingVal : undefined;
216
+ const paging = p ?? { start, count };
217
+ const result = options
218
+ ? { items, page: { start: Number(paging.start ?? start), count: Number(paging.count ?? count), total: paging?.total } }
219
+ : items; // backward-compat: old tests expect an array when no options passed
220
+ searchCache.set(cacheKey, { data: result, ts: Date.now() });
93
221
  (0, metrics_1.incrementMetric)('searchCacheMisses');
94
- return results;
222
+ return result;
95
223
  }
96
224
  async function getProfilesBatch(vanities, concurrency = 4) {
97
225
  const limit = Math.max(1, Math.min(concurrency || 1, 16));
98
- const results = [];
226
+ const results = Array.from({ length: vanities.length }, () => null);
99
227
  let idx = 0;
100
228
  async function worker() {
101
- // eslint-disable-next-line no-constant-condition
102
- while (true) {
229
+ while (idx < vanities.length) {
103
230
  const myIdx = idx++;
104
- if (myIdx >= vanities.length)
105
- break;
106
231
  const vanity = vanities[myIdx];
107
232
  try {
108
233
  const prof = await getProfileByVanity(vanity);
109
- if (prof)
110
- results.push(prof);
234
+ results[myIdx] = prof ?? null;
111
235
  }
112
236
  catch {
113
- // Skip failures
237
+ results[myIdx] = null;
114
238
  }
115
239
  }
116
240
  }
@@ -118,3 +242,227 @@ async function getProfilesBatch(vanities, concurrency = 4) {
118
242
  await Promise.all(workers);
119
243
  return results;
120
244
  }
245
+ // ---------------------------------------------------------------------------
246
+ // Companies (Voyager organizations)
247
+ // ---------------------------------------------------------------------------
248
+ async function resolveCompanyUniversalName(universalName) {
249
+ const url = `${LINKEDIN_API_BASE}/organization/companies?q=universalName&universalName=${encodeURIComponent(String(universalName || '').trim())}`;
250
+ try {
251
+ try {
252
+ (0, logger_1.log)('info', 'api.start', { operation: 'resolveCompanyUniversalName', selector: universalName });
253
+ }
254
+ catch { }
255
+ const raw = await (0, http_client_1.executeLinkedInRequest)({ url }, 'resolveCompanyUniversalName');
256
+ const rec = (raw && typeof raw === 'object') ? raw : undefined;
257
+ const elementsVal = rec && 'elements' in rec ? rec.elements : undefined;
258
+ const first = Array.isArray(elementsVal) ? elementsVal[0] : undefined;
259
+ const id = first?.id ?? first?.entityUrn ?? undefined;
260
+ try {
261
+ (0, logger_1.log)('info', 'api.ok', { operation: 'resolveCompanyUniversalName', selector: universalName, id });
262
+ }
263
+ catch { }
264
+ return { companyId: id ? String(id).replace(/^urn:li:fsd_company:/, '') : undefined };
265
+ }
266
+ catch (e) {
267
+ const status = e?.status ?? 0;
268
+ if (status === 404) {
269
+ try {
270
+ (0, logger_1.log)('warn', 'api.notFound', { operation: 'resolveCompanyUniversalName', selector: universalName, status });
271
+ }
272
+ catch { }
273
+ throw new errors_1.LinkedInClientError('Company not found', errors_1.ERROR_CODES.NOT_FOUND, 404);
274
+ }
275
+ throw e;
276
+ }
277
+ }
278
+ async function getCompanyById(companyId) {
279
+ const cfg = (0, config_1.getConfig)();
280
+ const key = String(companyId || '').trim();
281
+ const cached = getCached(companyCache, key, cfg.companyCacheTtl);
282
+ if (cached) {
283
+ (0, metrics_1.incrementMetric)('companyCacheHits');
284
+ return cached;
285
+ }
286
+ const url = `${LINKEDIN_API_BASE}/entities/companies/${encodeURIComponent(key)}`;
287
+ try {
288
+ try {
289
+ (0, logger_1.log)('info', 'api.start', { operation: 'getCompanyById', selector: key });
290
+ }
291
+ catch { }
292
+ const raw = await (0, http_client_1.executeLinkedInRequest)({ url }, 'getCompanyById');
293
+ const parsed = (0, company_parser_1.parseCompany)(raw);
294
+ (0, metrics_1.incrementMetric)('companyFetches');
295
+ companyCache.set(key, { data: parsed, ts: Date.now() });
296
+ try {
297
+ (0, logger_1.log)('info', 'api.ok', { operation: 'getCompanyById', selector: key });
298
+ }
299
+ catch { }
300
+ return parsed;
301
+ }
302
+ catch (e) {
303
+ const status = e?.status ?? 0;
304
+ if (status === 404) {
305
+ try {
306
+ (0, logger_1.log)('warn', 'api.notFound', { operation: 'getCompanyById', selector: key, status });
307
+ }
308
+ catch { }
309
+ throw new errors_1.LinkedInClientError('Company not found', errors_1.ERROR_CODES.NOT_FOUND, 404);
310
+ }
311
+ throw e;
312
+ }
313
+ }
314
+ async function getCompanyByUrl(companyUrl) {
315
+ const input = String(companyUrl || '').trim();
316
+ let url;
317
+ try {
318
+ // Require explicit http/https scheme; do not auto-prepend
319
+ if (!/^https?:\/\//i.test(input)) {
320
+ throw new errors_1.LinkedInClientError('Invalid company URL scheme', errors_1.ERROR_CODES.INVALID_INPUT, 400);
321
+ }
322
+ url = new URL(input);
323
+ }
324
+ catch {
325
+ throw new errors_1.LinkedInClientError('Invalid company URL', errors_1.ERROR_CODES.INVALID_INPUT, 400);
326
+ }
327
+ const m = url.pathname.match(/\/company\/([^\/]+)/i);
328
+ if (!m) {
329
+ try {
330
+ (0, logger_1.log)('error', 'api.invalidInput', { operation: 'getCompanyByUrl', selector: companyUrl });
331
+ }
332
+ catch { }
333
+ ;
334
+ throw new errors_1.LinkedInClientError('Invalid company URL path', errors_1.ERROR_CODES.INVALID_INPUT, 400);
335
+ }
336
+ const ident = decodeURIComponent(m[1]);
337
+ if (/^\d+$/.test(ident)) {
338
+ return getCompanyById(ident);
339
+ }
340
+ const resolved = await resolveCompanyUniversalName(ident);
341
+ if (!resolved.companyId) {
342
+ try {
343
+ (0, logger_1.log)('warn', 'api.notFound', { operation: 'getCompanyByUrl', selector: companyUrl });
344
+ }
345
+ catch { }
346
+ ;
347
+ throw new errors_1.LinkedInClientError('Company not found', errors_1.ERROR_CODES.NOT_FOUND, 404);
348
+ }
349
+ return getCompanyById(resolved.companyId);
350
+ }
351
+ // ---------------------------------------------------------------------------
352
+ // Typeahead (Sales Navigator facets)
353
+ // ---------------------------------------------------------------------------
354
+ async function typeahead(options) {
355
+ const cfg = (0, config_1.getConfig)();
356
+ const type = String(options?.type || '').trim();
357
+ const start = Number.isFinite(options?.start) ? Number(options.start) : 0;
358
+ const count = Number.isFinite(options?.count) ? Number(options.count) : 10;
359
+ const query = (options?.query ?? '').toString();
360
+ const cacheKey = JSON.stringify({ type, query, start, count });
361
+ const cached = getCached(typeaheadCache, cacheKey, cfg.typeaheadCacheTtl);
362
+ if (cached) {
363
+ (0, metrics_1.incrementMetric)('typeaheadCacheHits');
364
+ return cached;
365
+ }
366
+ let url = `${SALES_NAV_BASE}/salesApiFacetTypeahead?q=query&type=${encodeURIComponent(type)}&start=${start}&count=${count}`;
367
+ if (query)
368
+ url += `&query=${encodeURIComponent(query)}`;
369
+ (0, metrics_1.incrementMetric)('typeaheadRequests');
370
+ try {
371
+ (0, logger_1.log)('info', 'api.start', { operation: 'typeahead', selector: { type, query, start, count } });
372
+ }
373
+ catch { }
374
+ const raw = await (0, http_client_1.executeLinkedInRequest)({ url }, 'typeahead');
375
+ const items = Array.isArray(raw?.elements)
376
+ ? raw.elements.map((it) => ({
377
+ id: String(it?.id ?? it?.backendId ?? ''),
378
+ text: it.displayValue ??
379
+ it.headline?.text ??
380
+ it.text ??
381
+ it.name ??
382
+ it.label ??
383
+ ''
384
+ }))
385
+ : [];
386
+ const rtype = (raw && typeof raw === 'object') ? raw : undefined;
387
+ const pagingVal2 = rtype && 'paging' in rtype ? rtype.paging : undefined;
388
+ const paging2 = (pagingVal2 && typeof pagingVal2 === 'object') ? pagingVal2 : undefined;
389
+ const result = { items, page: { start: Number(paging2?.start ?? start), count: Number(paging2?.count ?? count), total: paging2?.total } };
390
+ typeaheadCache.set(cacheKey, { data: result, ts: Date.now() });
391
+ try {
392
+ (0, logger_1.log)('info', 'api.ok', { operation: 'typeahead', selector: { type, query }, count: items.length });
393
+ }
394
+ catch { }
395
+ return result;
396
+ }
397
+ // ---------------------------------------------------------------------------
398
+ // Sales Navigator profile details
399
+ // ---------------------------------------------------------------------------
400
+ async function getSalesNavigatorProfileDetails(profileUrnOrId) {
401
+ const idOrUrn = String(profileUrnOrId || '').trim();
402
+ // Build Sales API path supporting URN triple or explicit profileId/authType/authToken
403
+ let pathSeg = '';
404
+ let pidFromTriple = null;
405
+ // URN with triple
406
+ const mUrn = idOrUrn.match(/^urn:li:(?:fs_salesProfile|fsd_profile):\(([^,\s)]+),([^,\s)]+),([^,\s)]+)\)/i);
407
+ if (mUrn) {
408
+ const [_, pid, authType, authToken] = mUrn;
409
+ pathSeg = `(profileId:${pid},authType:${authType},authToken:${authToken})`;
410
+ pidFromTriple = pid;
411
+ }
412
+ else if (/profileId:/.test(idOrUrn) && /authType:/.test(idOrUrn) && /authToken:/.test(idOrUrn)) {
413
+ // Already the triple form without urn prefix
414
+ pathSeg = `(${idOrUrn.replace(/^\(|\)$/g, '')})`;
415
+ const m = idOrUrn.match(/profileId:([^,\s)]+)/);
416
+ pidFromTriple = m ? m[1] : null;
417
+ }
418
+ else {
419
+ // Fallback to simple id/urn
420
+ pathSeg = idOrUrn.startsWith('urn:') ? idOrUrn : encodeURIComponent(idOrUrn);
421
+ }
422
+ 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)))');
423
+ const base = `${SALES_NAV_BASE}/salesApiProfiles/`;
424
+ async function requestWith(seg) {
425
+ const url = `${base}${seg}?decoration=${decoration}`;
426
+ return (0, http_client_1.executeLinkedInRequest)({ url, headers: { Referer: 'https://www.linkedin.com/sales/search/people' } }, 'getSalesNavigatorProfileDetails');
427
+ }
428
+ try {
429
+ try {
430
+ (0, logger_1.log)('info', 'api.start', { operation: 'getSalesNavigatorProfileDetails', selector: idOrUrn });
431
+ }
432
+ catch { }
433
+ let raw;
434
+ try {
435
+ raw = await requestWith(pathSeg);
436
+ }
437
+ catch (e) {
438
+ const status = e?.status ?? 0;
439
+ if (status === 400 && pidFromTriple) {
440
+ // Fallback: some environments reject the triple form; retry with bare id
441
+ const fallbackSeg = encodeURIComponent(pidFromTriple);
442
+ try {
443
+ (0, logger_1.log)('warn', 'api.salesProfileFallback', { from: pathSeg, to: fallbackSeg });
444
+ }
445
+ catch { }
446
+ raw = await requestWith(fallbackSeg);
447
+ }
448
+ else if (status === 404) {
449
+ try {
450
+ (0, logger_1.log)('warn', 'api.notFound', { operation: 'getSalesNavigatorProfileDetails', selector: idOrUrn, status });
451
+ }
452
+ catch { }
453
+ throw new errors_1.LinkedInClientError('Sales profile not found', errors_1.ERROR_CODES.NOT_FOUND, 404);
454
+ }
455
+ else {
456
+ throw e;
457
+ }
458
+ }
459
+ try {
460
+ (0, logger_1.log)('info', 'api.ok', { operation: 'getSalesNavigatorProfileDetails', selector: idOrUrn });
461
+ }
462
+ catch { }
463
+ return raw;
464
+ }
465
+ catch (e) {
466
+ throw e;
467
+ }
468
+ }
@@ -0,0 +1,2 @@
1
+ import type { Company } from '../types';
2
+ export declare function parseCompany(raw: unknown): Company;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseCompany = parseCompany;
4
+ const image_parser_1 = require("./image-parser");
5
+ function parseCompany(raw) {
6
+ const r = raw;
7
+ const company = {
8
+ companyId: String(r?.id ?? '')
9
+ .replace(/^urn:li:(?:fsd_)?company:/i, '') ||
10
+ String(r?.entityUrn ?? '')
11
+ .replace(/^urn:li:(?:fsd_)?company:/i, ''),
12
+ universalName: r?.universalName,
13
+ name: r?.name,
14
+ websiteUrl: r?.websiteUrl,
15
+ sizeLabel: r?.employeeCountRange?.localizedName,
16
+ headquarters: r?.headquarterLocation?.defaultLocalizedName ||
17
+ r?.headquarter?.defaultLocalizedName,
18
+ logoUrl: undefined,
19
+ coverUrl: undefined,
20
+ };
21
+ const logoVector = r?.logo?.vectorImage || r?.logoV2?.vectorImage;
22
+ const coverVector = r?.coverPhoto?.vectorImage || r?.backgroundImage?.vectorImage;
23
+ const logoUrl = (0, image_parser_1.selectBestImageUrl)(logoVector);
24
+ const coverUrl = (0, image_parser_1.selectBestImageUrl)(coverVector);
25
+ if (logoUrl)
26
+ company.logoUrl = logoUrl;
27
+ if (coverUrl)
28
+ company.coverUrl = coverUrl;
29
+ return company;
30
+ }
@@ -0,0 +1,10 @@
1
+ export type VectorArtifact = {
2
+ width?: number;
3
+ fileIdentifyingUrlPathSegment?: string;
4
+ url?: string;
5
+ };
6
+ export type VectorImage = {
7
+ rootUrl?: string;
8
+ artifacts?: VectorArtifact[];
9
+ };
10
+ export declare function selectBestImageUrl(vector: unknown): string | undefined;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.selectBestImageUrl = selectBestImageUrl;
4
+ function selectBestImageUrl(vector) {
5
+ const v = vector;
6
+ if (!v || !Array.isArray(v.artifacts) || v.artifacts.length === 0)
7
+ return undefined;
8
+ const best = v.artifacts.reduce((prev, curr) => ((curr?.width || 0) > (prev?.width || 0) ? curr : prev), {});
9
+ const seg = best?.fileIdentifyingUrlPathSegment || best?.url;
10
+ if (!seg)
11
+ return undefined;
12
+ if (/^https?:\/\//i.test(seg))
13
+ return seg;
14
+ const root = String(v.rootUrl || '');
15
+ return root + seg;
16
+ }
@@ -1,2 +1,2 @@
1
1
  import type { LinkedInProfile } from '../types';
2
- export declare function parseFullProfile(rawResponse: any, vanity: string): LinkedInProfile;
2
+ export declare function parseFullProfile(rawResponse: unknown, vanity: string): LinkedInProfile;
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseFullProfile = parseFullProfile;
4
+ const image_parser_1 = require("./image-parser");
4
5
  function parseFullProfile(rawResponse, vanity) {
5
- const included = Array.isArray(rawResponse?.included) ? rawResponse.included : [];
6
+ const rr = rawResponse;
7
+ const included = Array.isArray(rr?.included) ? rr.included : [];
6
8
  const identity = included.find((it) => String(it?.$type || '').includes('identity.profile.Profile')) || {};
7
9
  const profile = {
8
10
  vanity,
@@ -15,46 +17,89 @@ function parseFullProfile(rawResponse, vanity) {
15
17
  educations: [],
16
18
  };
17
19
  // Positions
20
+ function extractCompanyIdFromUrn(urn) {
21
+ if (!urn)
22
+ return undefined;
23
+ const s = String(urn);
24
+ const mParen = s.match(/company:\(([^,)]+)/i);
25
+ if (mParen)
26
+ return mParen[1];
27
+ const mSimple = s.match(/company:([^,\s)]+)/i);
28
+ return mSimple ? mSimple[1] : undefined;
29
+ }
30
+ function deepFindCompanyUrn(obj, depth = 0) {
31
+ if (!obj || depth > 3)
32
+ return undefined;
33
+ if (typeof obj === 'string' && /urn:li:(?:fsd_)?company:/i.test(obj))
34
+ return obj;
35
+ if (typeof obj !== 'object')
36
+ return undefined;
37
+ const rec = obj;
38
+ for (const k of Object.keys(rec)) {
39
+ const v = rec[k];
40
+ if (typeof v === 'string' && /urn:li:(?:fsd_)?company:/i.test(v))
41
+ return v;
42
+ if (v && typeof v === 'object') {
43
+ const hit = deepFindCompanyUrn(v, depth + 1);
44
+ if (hit)
45
+ return hit;
46
+ }
47
+ }
48
+ return undefined;
49
+ }
18
50
  for (const item of included) {
19
- const urn = item?.entityUrn || '';
51
+ const rec = item;
52
+ const urn = rec?.entityUrn || '';
20
53
  if (!urn.includes('fsd_profilePosition'))
21
54
  continue;
22
55
  const pos = {
23
- title: item?.title,
24
- companyName: item?.companyName,
25
- description: item?.description,
26
- isCurrent: !item?.timePeriod?.endDate,
27
- startYear: item?.timePeriod?.startDate?.year,
28
- startMonth: item?.timePeriod?.startDate?.month,
29
- endYear: item?.timePeriod?.endDate?.year,
30
- endMonth: item?.timePeriod?.endDate?.month,
56
+ title: rec?.title,
57
+ companyName: rec?.companyName,
58
+ description: rec?.description,
59
+ isCurrent: !rec?.timePeriod?.endDate,
60
+ startYear: rec?.timePeriod?.startDate?.year,
61
+ startMonth: rec?.timePeriod?.startDate?.month,
62
+ endYear: rec?.timePeriod?.endDate?.year,
63
+ endMonth: rec?.timePeriod?.endDate?.month,
31
64
  };
65
+ // Try to extract company URN and numeric id robustly
66
+ const candUrns = [
67
+ rec?.companyUrn,
68
+ rec?.company?.entityUrn,
69
+ rec?.company?.companyUrn,
70
+ rec?.companyUrnV2,
71
+ ].filter(Boolean);
72
+ const foundUrn = candUrns.find(u => /urn:li:(?:fsd_)?company:/i.test(u)) || deepFindCompanyUrn(item);
73
+ if (foundUrn) {
74
+ pos.companyUrn = foundUrn;
75
+ const cid = extractCompanyIdFromUrn(foundUrn);
76
+ if (cid)
77
+ pos.companyId = cid;
78
+ }
32
79
  profile.positions.push(pos);
33
80
  }
34
81
  // Educations
35
82
  for (const item of included) {
36
- const urn = item?.entityUrn || '';
83
+ const rec = item;
84
+ const urn = rec?.entityUrn || '';
37
85
  if (!urn.includes('fsd_profileEducation'))
38
86
  continue;
39
87
  const edu = {
40
- schoolName: item?.schoolName,
41
- degree: item?.degreeName,
42
- fieldOfStudy: item?.fieldOfStudy,
43
- startYear: item?.timePeriod?.startDate?.year,
44
- startMonth: item?.timePeriod?.startDate?.month,
45
- endYear: item?.timePeriod?.endDate?.year,
46
- endMonth: item?.timePeriod?.endDate?.month,
88
+ schoolName: rec?.schoolName,
89
+ degree: rec?.degreeName,
90
+ fieldOfStudy: rec?.fieldOfStudy,
91
+ startYear: rec?.timePeriod?.startDate?.year,
92
+ startMonth: rec?.timePeriod?.startDate?.month,
93
+ endYear: rec?.timePeriod?.endDate?.year,
94
+ endMonth: rec?.timePeriod?.endDate?.month,
47
95
  };
48
96
  profile.educations.push(edu);
49
97
  }
50
- // Avatar
98
+ // Avatar (via helper)
51
99
  const avatarItem = included.find((it) => it?.vectorImage && JSON.stringify(it).includes('vectorImage'));
52
100
  const vector = avatarItem?.vectorImage;
53
- if (vector?.artifacts?.length) {
54
- const best = vector.artifacts.reduce((prev, curr) => ((curr?.width || 0) > (prev?.width || 0) ? curr : prev), {});
55
- const seg = best?.fileIdentifyingUrlPathSegment || best?.url;
56
- if (seg)
57
- profile.avatarUrl = seg.startsWith('http') ? seg : (vector.rootUrl || '') + seg;
58
- }
101
+ const bestUrl = (0, image_parser_1.selectBestImageUrl)(vector);
102
+ if (bestUrl)
103
+ profile.avatarUrl = bestUrl;
59
104
  return profile;
60
105
  }