linkedin-secret-sauce 0.1.1 → 0.1.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/README.md +193 -60
- package/dist/config.d.ts +3 -0
- package/dist/config.js +7 -1
- package/dist/cookie-pool.d.ts +11 -0
- package/dist/cookie-pool.js +55 -3
- package/dist/http-client.js +116 -14
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -1
- package/dist/linkedin-api.d.ts +22 -3
- package/dist/linkedin-api.js +366 -28
- package/dist/parsers/company-parser.d.ts +2 -0
- package/dist/parsers/company-parser.js +25 -0
- package/dist/parsers/image-parser.d.ts +1 -0
- package/dist/parsers/image-parser.js +15 -0
- package/dist/parsers/profile-parser.js +48 -7
- package/dist/parsers/search-parser.js +12 -3
- package/dist/types.d.ts +147 -0
- package/dist/utils/errors.d.ts +4 -0
- package/dist/utils/errors.js +5 -0
- package/dist/utils/linkedin-config.d.ts +43 -0
- package/dist/utils/linkedin-config.js +77 -0
- package/dist/utils/metrics.d.ts +5 -0
- package/dist/utils/metrics.js +16 -1
- package/dist/utils/request-history.d.ts +12 -0
- package/dist/utils/request-history.js +45 -0
- package/dist/utils/search-encoder.d.ts +4 -0
- package/dist/utils/search-encoder.js +163 -0
- package/package.json +16 -6
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.parseSalesSearchResults = exports.parseFullProfile = exports.LinkedInClientError = exports.getConfig = exports.initializeLinkedInClient = void 0;
|
|
17
|
+
exports.clearRequestHistory = exports.getRequestHistory = exports.parseSalesSearchResults = exports.parseFullProfile = exports.LinkedInClientError = exports.getConfig = exports.initializeLinkedInClient = void 0;
|
|
18
18
|
var config_1 = require("./config");
|
|
19
19
|
Object.defineProperty(exports, "initializeLinkedInClient", { enumerable: true, get: function () { return config_1.initializeLinkedInClient; } });
|
|
20
20
|
Object.defineProperty(exports, "getConfig", { enumerable: true, get: function () { return config_1.getConfig; } });
|
|
@@ -29,3 +29,6 @@ Object.defineProperty(exports, "parseSalesSearchResults", { enumerable: true, ge
|
|
|
29
29
|
__exportStar(require("./linkedin-api"), exports);
|
|
30
30
|
__exportStar(require("./types"), exports);
|
|
31
31
|
__exportStar(require("./utils/metrics"), exports);
|
|
32
|
+
var request_history_1 = require("./utils/request-history");
|
|
33
|
+
Object.defineProperty(exports, "getRequestHistory", { enumerable: true, get: function () { return request_history_1.getRequestHistory; } });
|
|
34
|
+
Object.defineProperty(exports, "clearRequestHistory", { enumerable: true, get: function () { return request_history_1.clearRequestHistory; } });
|
package/dist/linkedin-api.d.ts
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SalesSearchFilters } from './types';
|
|
2
|
+
import type { LinkedInProfile, SalesLeadSearchResult, SearchSalesResult, TypeaheadResult, SalesNavigatorProfile, Company } from './types';
|
|
2
3
|
export declare function getProfileByVanity(vanity: string): Promise<LinkedInProfile>;
|
|
3
4
|
export declare function getProfileByUrn(fsdKey: string): Promise<LinkedInProfile>;
|
|
4
|
-
export declare function searchSalesLeads(keywords: string
|
|
5
|
-
|
|
5
|
+
export declare function searchSalesLeads(keywords: string, options?: {
|
|
6
|
+
start?: number;
|
|
7
|
+
count?: number;
|
|
8
|
+
decorationId?: string;
|
|
9
|
+
filters?: SalesSearchFilters;
|
|
10
|
+
rawQuery?: string;
|
|
11
|
+
}): Promise<SearchSalesResult | SalesLeadSearchResult[]>;
|
|
12
|
+
export declare function getProfilesBatch(vanities: string[], concurrency?: number): Promise<(LinkedInProfile | null)[]>;
|
|
13
|
+
export declare function resolveCompanyUniversalName(universalName: string): Promise<{
|
|
14
|
+
companyId?: string;
|
|
15
|
+
}>;
|
|
16
|
+
export declare function getCompanyById(companyId: string): Promise<Company>;
|
|
17
|
+
export declare function getCompanyByUrl(companyUrl: string): Promise<Company>;
|
|
18
|
+
export declare function typeahead(options: {
|
|
19
|
+
type: string;
|
|
20
|
+
query?: string;
|
|
21
|
+
start?: number;
|
|
22
|
+
count?: number;
|
|
23
|
+
}): Promise<TypeaheadResult>;
|
|
24
|
+
export declare function getSalesNavigatorProfileDetails(profileUrnOrId: string): Promise<SalesNavigatorProfile>;
|
package/dist/linkedin-api.js
CHANGED
|
@@ -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
|
-
|
|
49
|
-
|
|
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,42 +104,123 @@ async function getProfileByVanity(vanity) {
|
|
|
60
104
|
}
|
|
61
105
|
async function getProfileByUrn(fsdKey) {
|
|
62
106
|
const cfg = (0, config_1.getConfig)();
|
|
63
|
-
|
|
64
|
-
const
|
|
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(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
79
|
-
const
|
|
80
|
-
|
|
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
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
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 paging = raw?.paging ?? { start, count };
|
|
214
|
+
const result = options
|
|
215
|
+
? { items, page: { start: Number(paging.start ?? start), count: Number(paging.count ?? count), total: paging?.total } }
|
|
216
|
+
: items; // backward-compat: old tests expect an array when no options passed
|
|
217
|
+
searchCache.set(cacheKey, { data: result, ts: Date.now() });
|
|
93
218
|
(0, metrics_1.incrementMetric)('searchCacheMisses');
|
|
94
|
-
return
|
|
219
|
+
return result;
|
|
95
220
|
}
|
|
96
221
|
async function getProfilesBatch(vanities, concurrency = 4) {
|
|
97
222
|
const limit = Math.max(1, Math.min(concurrency || 1, 16));
|
|
98
|
-
const results =
|
|
223
|
+
const results = Array.from({ length: vanities.length }, () => null);
|
|
99
224
|
let idx = 0;
|
|
100
225
|
async function worker() {
|
|
101
226
|
// eslint-disable-next-line no-constant-condition
|
|
@@ -106,11 +231,10 @@ async function getProfilesBatch(vanities, concurrency = 4) {
|
|
|
106
231
|
const vanity = vanities[myIdx];
|
|
107
232
|
try {
|
|
108
233
|
const prof = await getProfileByVanity(vanity);
|
|
109
|
-
|
|
110
|
-
results.push(prof);
|
|
234
|
+
results[myIdx] = prof ?? null;
|
|
111
235
|
}
|
|
112
236
|
catch {
|
|
113
|
-
|
|
237
|
+
results[myIdx] = null;
|
|
114
238
|
}
|
|
115
239
|
}
|
|
116
240
|
}
|
|
@@ -118,3 +242,217 @@ 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 id = raw?.elements?.[0]?.id ?? raw?.elements?.[0]?.entityUrn ?? undefined;
|
|
257
|
+
try {
|
|
258
|
+
(0, logger_1.log)('info', 'api.ok', { operation: 'resolveCompanyUniversalName', selector: universalName, id });
|
|
259
|
+
}
|
|
260
|
+
catch { }
|
|
261
|
+
return { companyId: id ? String(id).replace(/^urn:li:fsd_company:/, '') : undefined };
|
|
262
|
+
}
|
|
263
|
+
catch (e) {
|
|
264
|
+
const status = e?.status ?? 0;
|
|
265
|
+
if (status === 404) {
|
|
266
|
+
try {
|
|
267
|
+
(0, logger_1.log)('warn', 'api.notFound', { operation: 'resolveCompanyUniversalName', selector: universalName, status });
|
|
268
|
+
}
|
|
269
|
+
catch { }
|
|
270
|
+
throw new errors_1.LinkedInClientError('Company not found', errors_1.ERROR_CODES.NOT_FOUND, 404);
|
|
271
|
+
}
|
|
272
|
+
throw e;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async function getCompanyById(companyId) {
|
|
276
|
+
const cfg = (0, config_1.getConfig)();
|
|
277
|
+
const key = String(companyId || '').trim();
|
|
278
|
+
const cached = getCached(companyCache, key, cfg.companyCacheTtl);
|
|
279
|
+
if (cached) {
|
|
280
|
+
(0, metrics_1.incrementMetric)('companyCacheHits');
|
|
281
|
+
return cached;
|
|
282
|
+
}
|
|
283
|
+
const url = `${LINKEDIN_API_BASE}/entities/companies/${encodeURIComponent(key)}`;
|
|
284
|
+
try {
|
|
285
|
+
try {
|
|
286
|
+
(0, logger_1.log)('info', 'api.start', { operation: 'getCompanyById', selector: key });
|
|
287
|
+
}
|
|
288
|
+
catch { }
|
|
289
|
+
const raw = await (0, http_client_1.executeLinkedInRequest)({ url }, 'getCompanyById');
|
|
290
|
+
const parsed = (0, company_parser_1.parseCompany)(raw);
|
|
291
|
+
(0, metrics_1.incrementMetric)('companyFetches');
|
|
292
|
+
companyCache.set(key, { data: parsed, ts: Date.now() });
|
|
293
|
+
try {
|
|
294
|
+
(0, logger_1.log)('info', 'api.ok', { operation: 'getCompanyById', selector: key });
|
|
295
|
+
}
|
|
296
|
+
catch { }
|
|
297
|
+
return parsed;
|
|
298
|
+
}
|
|
299
|
+
catch (e) {
|
|
300
|
+
const status = e?.status ?? 0;
|
|
301
|
+
if (status === 404) {
|
|
302
|
+
try {
|
|
303
|
+
(0, logger_1.log)('warn', 'api.notFound', { operation: 'getCompanyById', selector: key, status });
|
|
304
|
+
}
|
|
305
|
+
catch { }
|
|
306
|
+
throw new errors_1.LinkedInClientError('Company not found', errors_1.ERROR_CODES.NOT_FOUND, 404);
|
|
307
|
+
}
|
|
308
|
+
throw e;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async function getCompanyByUrl(companyUrl) {
|
|
312
|
+
const input = String(companyUrl || '').trim();
|
|
313
|
+
let url;
|
|
314
|
+
try {
|
|
315
|
+
// Require explicit http/https scheme; do not auto-prepend
|
|
316
|
+
if (!/^https?:\/\//i.test(input)) {
|
|
317
|
+
throw new errors_1.LinkedInClientError('Invalid company URL scheme', errors_1.ERROR_CODES.INVALID_INPUT, 400);
|
|
318
|
+
}
|
|
319
|
+
url = new URL(input);
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
throw new errors_1.LinkedInClientError('Invalid company URL', errors_1.ERROR_CODES.INVALID_INPUT, 400);
|
|
323
|
+
}
|
|
324
|
+
const m = url.pathname.match(/\/company\/([^\/]+)/i);
|
|
325
|
+
if (!m) {
|
|
326
|
+
try {
|
|
327
|
+
(0, logger_1.log)('error', 'api.invalidInput', { operation: 'getCompanyByUrl', selector: companyUrl });
|
|
328
|
+
}
|
|
329
|
+
catch { }
|
|
330
|
+
;
|
|
331
|
+
throw new errors_1.LinkedInClientError('Invalid company URL path', errors_1.ERROR_CODES.INVALID_INPUT, 400);
|
|
332
|
+
}
|
|
333
|
+
const ident = decodeURIComponent(m[1]);
|
|
334
|
+
if (/^\d+$/.test(ident)) {
|
|
335
|
+
return getCompanyById(ident);
|
|
336
|
+
}
|
|
337
|
+
const resolved = await resolveCompanyUniversalName(ident);
|
|
338
|
+
if (!resolved.companyId) {
|
|
339
|
+
try {
|
|
340
|
+
(0, logger_1.log)('warn', 'api.notFound', { operation: 'getCompanyByUrl', selector: companyUrl });
|
|
341
|
+
}
|
|
342
|
+
catch { }
|
|
343
|
+
;
|
|
344
|
+
throw new errors_1.LinkedInClientError('Company not found', errors_1.ERROR_CODES.NOT_FOUND, 404);
|
|
345
|
+
}
|
|
346
|
+
return getCompanyById(resolved.companyId);
|
|
347
|
+
}
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
// Typeahead (Sales Navigator facets)
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
async function typeahead(options) {
|
|
352
|
+
const cfg = (0, config_1.getConfig)();
|
|
353
|
+
const type = String(options?.type || '').trim();
|
|
354
|
+
const start = Number.isFinite(options?.start) ? Number(options.start) : 0;
|
|
355
|
+
const count = Number.isFinite(options?.count) ? Number(options.count) : 10;
|
|
356
|
+
const query = (options?.query ?? '').toString();
|
|
357
|
+
const cacheKey = JSON.stringify({ type, query, start, count });
|
|
358
|
+
const cached = getCached(typeaheadCache, cacheKey, cfg.typeaheadCacheTtl);
|
|
359
|
+
if (cached) {
|
|
360
|
+
(0, metrics_1.incrementMetric)('typeaheadCacheHits');
|
|
361
|
+
return cached;
|
|
362
|
+
}
|
|
363
|
+
let url = `${SALES_NAV_BASE}/salesApiFacetTypeahead?q=query&type=${encodeURIComponent(type)}&start=${start}&count=${count}`;
|
|
364
|
+
if (query)
|
|
365
|
+
url += `&query=${encodeURIComponent(query)}`;
|
|
366
|
+
(0, metrics_1.incrementMetric)('typeaheadRequests');
|
|
367
|
+
try {
|
|
368
|
+
(0, logger_1.log)('info', 'api.start', { operation: 'typeahead', selector: { type, query, start, count } });
|
|
369
|
+
}
|
|
370
|
+
catch { }
|
|
371
|
+
const raw = await (0, http_client_1.executeLinkedInRequest)({ url }, 'typeahead');
|
|
372
|
+
const items = Array.isArray(raw?.elements)
|
|
373
|
+
? raw.elements.map((it) => ({
|
|
374
|
+
id: String(it?.id ?? it?.backendId ?? ''),
|
|
375
|
+
text: it?.displayValue ?? it?.headline?.text ?? it?.text ?? it?.name ?? it?.label ?? ''
|
|
376
|
+
}))
|
|
377
|
+
: [];
|
|
378
|
+
const paging = raw?.paging ?? {};
|
|
379
|
+
const result = { items, page: { start: Number(paging?.start ?? start), count: Number(paging?.count ?? count), total: paging?.total } };
|
|
380
|
+
typeaheadCache.set(cacheKey, { data: result, ts: Date.now() });
|
|
381
|
+
try {
|
|
382
|
+
(0, logger_1.log)('info', 'api.ok', { operation: 'typeahead', selector: { type, query }, count: items.length });
|
|
383
|
+
}
|
|
384
|
+
catch { }
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
// Sales Navigator profile details
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
async function getSalesNavigatorProfileDetails(profileUrnOrId) {
|
|
391
|
+
const idOrUrn = String(profileUrnOrId || '').trim();
|
|
392
|
+
// Build Sales API path supporting URN triple or explicit profileId/authType/authToken
|
|
393
|
+
let pathSeg = '';
|
|
394
|
+
let pidFromTriple = null;
|
|
395
|
+
// URN with triple
|
|
396
|
+
const mUrn = idOrUrn.match(/^urn:li:(?:fs_salesProfile|fsd_profile):\(([^,\s)]+),([^,\s)]+),([^,\s)]+)\)/i);
|
|
397
|
+
if (mUrn) {
|
|
398
|
+
const [_, pid, authType, authToken] = mUrn;
|
|
399
|
+
pathSeg = `(profileId:${pid},authType:${authType},authToken:${authToken})`;
|
|
400
|
+
pidFromTriple = pid;
|
|
401
|
+
}
|
|
402
|
+
else if (/profileId:/.test(idOrUrn) && /authType:/.test(idOrUrn) && /authToken:/.test(idOrUrn)) {
|
|
403
|
+
// Already the triple form without urn prefix
|
|
404
|
+
pathSeg = `(${idOrUrn.replace(/^\(|\)$/g, '')})`;
|
|
405
|
+
const m = idOrUrn.match(/profileId:([^,\s)]+)/);
|
|
406
|
+
pidFromTriple = m ? m[1] : null;
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
// Fallback to simple id/urn
|
|
410
|
+
pathSeg = idOrUrn.startsWith('urn:') ? idOrUrn : encodeURIComponent(idOrUrn);
|
|
411
|
+
}
|
|
412
|
+
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)))');
|
|
413
|
+
const base = `${SALES_NAV_BASE}/salesApiProfiles/`;
|
|
414
|
+
async function requestWith(seg) {
|
|
415
|
+
const url = `${base}${seg}?decoration=${decoration}`;
|
|
416
|
+
return (0, http_client_1.executeLinkedInRequest)({ url, headers: { Referer: 'https://www.linkedin.com/sales/search/people' } }, 'getSalesNavigatorProfileDetails');
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
try {
|
|
420
|
+
(0, logger_1.log)('info', 'api.start', { operation: 'getSalesNavigatorProfileDetails', selector: idOrUrn });
|
|
421
|
+
}
|
|
422
|
+
catch { }
|
|
423
|
+
let raw;
|
|
424
|
+
try {
|
|
425
|
+
raw = await requestWith(pathSeg);
|
|
426
|
+
}
|
|
427
|
+
catch (e) {
|
|
428
|
+
const status = e?.status ?? 0;
|
|
429
|
+
if (status === 400 && pidFromTriple) {
|
|
430
|
+
// Fallback: some environments reject the triple form; retry with bare id
|
|
431
|
+
const fallbackSeg = encodeURIComponent(pidFromTriple);
|
|
432
|
+
try {
|
|
433
|
+
(0, logger_1.log)('warn', 'api.salesProfileFallback', { from: pathSeg, to: fallbackSeg });
|
|
434
|
+
}
|
|
435
|
+
catch { }
|
|
436
|
+
raw = await requestWith(fallbackSeg);
|
|
437
|
+
}
|
|
438
|
+
else if (status === 404) {
|
|
439
|
+
try {
|
|
440
|
+
(0, logger_1.log)('warn', 'api.notFound', { operation: 'getSalesNavigatorProfileDetails', selector: idOrUrn, status });
|
|
441
|
+
}
|
|
442
|
+
catch { }
|
|
443
|
+
throw new errors_1.LinkedInClientError('Sales profile not found', errors_1.ERROR_CODES.NOT_FOUND, 404);
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
throw e;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
try {
|
|
450
|
+
(0, logger_1.log)('info', 'api.ok', { operation: 'getSalesNavigatorProfileDetails', selector: idOrUrn });
|
|
451
|
+
}
|
|
452
|
+
catch { }
|
|
453
|
+
return raw;
|
|
454
|
+
}
|
|
455
|
+
catch (e) {
|
|
456
|
+
throw e;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
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 company = {
|
|
7
|
+
companyId: String(raw?.id ?? '').replace(/^urn:li:(?:fsd_)?company:/i, '') || String(raw?.entityUrn ?? '').replace(/^urn:li:(?:fsd_)?company:/i, ''),
|
|
8
|
+
universalName: raw?.universalName,
|
|
9
|
+
name: raw?.name,
|
|
10
|
+
websiteUrl: raw?.websiteUrl,
|
|
11
|
+
sizeLabel: raw?.employeeCountRange?.localizedName,
|
|
12
|
+
headquarters: raw?.headquarterLocation?.defaultLocalizedName || raw?.headquarter?.defaultLocalizedName,
|
|
13
|
+
logoUrl: undefined,
|
|
14
|
+
coverUrl: undefined,
|
|
15
|
+
};
|
|
16
|
+
const logoVector = raw?.logo?.vectorImage || raw?.logoV2?.vectorImage;
|
|
17
|
+
const coverVector = raw?.coverPhoto?.vectorImage || raw?.backgroundImage?.vectorImage;
|
|
18
|
+
const logoUrl = (0, image_parser_1.selectBestImageUrl)(logoVector);
|
|
19
|
+
const coverUrl = (0, image_parser_1.selectBestImageUrl)(coverVector);
|
|
20
|
+
if (logoUrl)
|
|
21
|
+
company.logoUrl = logoUrl;
|
|
22
|
+
if (coverUrl)
|
|
23
|
+
company.coverUrl = coverUrl;
|
|
24
|
+
return company;
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function selectBestImageUrl(vector: any): string | undefined;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.selectBestImageUrl = selectBestImageUrl;
|
|
4
|
+
function selectBestImageUrl(vector) {
|
|
5
|
+
if (!vector || !Array.isArray(vector.artifacts) || vector.artifacts.length === 0)
|
|
6
|
+
return undefined;
|
|
7
|
+
const best = vector.artifacts.reduce((prev, curr) => ((curr?.width || 0) > (prev?.width || 0) ? curr : prev), {});
|
|
8
|
+
const seg = best?.fileIdentifyingUrlPathSegment || best?.url;
|
|
9
|
+
if (!seg)
|
|
10
|
+
return undefined;
|
|
11
|
+
if (/^https?:\/\//i.test(seg))
|
|
12
|
+
return seg;
|
|
13
|
+
const root = String(vector.rootUrl || '');
|
|
14
|
+
return root + seg;
|
|
15
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
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
6
|
const included = Array.isArray(rawResponse?.included) ? rawResponse.included : [];
|
|
6
7
|
const identity = included.find((it) => String(it?.$type || '').includes('identity.profile.Profile')) || {};
|
|
@@ -15,6 +16,35 @@ function parseFullProfile(rawResponse, vanity) {
|
|
|
15
16
|
educations: [],
|
|
16
17
|
};
|
|
17
18
|
// Positions
|
|
19
|
+
function extractCompanyIdFromUrn(urn) {
|
|
20
|
+
if (!urn)
|
|
21
|
+
return undefined;
|
|
22
|
+
const s = String(urn);
|
|
23
|
+
const mParen = s.match(/company:\(([^,)]+)/i);
|
|
24
|
+
if (mParen)
|
|
25
|
+
return mParen[1];
|
|
26
|
+
const mSimple = s.match(/company:([^,\s)]+)/i);
|
|
27
|
+
return mSimple ? mSimple[1] : undefined;
|
|
28
|
+
}
|
|
29
|
+
function deepFindCompanyUrn(obj, depth = 0) {
|
|
30
|
+
if (!obj || depth > 3)
|
|
31
|
+
return undefined;
|
|
32
|
+
if (typeof obj === 'string' && /urn:li:(?:fsd_)?company:/i.test(obj))
|
|
33
|
+
return obj;
|
|
34
|
+
if (typeof obj !== 'object')
|
|
35
|
+
return undefined;
|
|
36
|
+
for (const k of Object.keys(obj)) {
|
|
37
|
+
const v = obj[k];
|
|
38
|
+
if (typeof v === 'string' && /urn:li:(?:fsd_)?company:/i.test(v))
|
|
39
|
+
return v;
|
|
40
|
+
if (v && typeof v === 'object') {
|
|
41
|
+
const hit = deepFindCompanyUrn(v, depth + 1);
|
|
42
|
+
if (hit)
|
|
43
|
+
return hit;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
18
48
|
for (const item of included) {
|
|
19
49
|
const urn = item?.entityUrn || '';
|
|
20
50
|
if (!urn.includes('fsd_profilePosition'))
|
|
@@ -29,6 +59,20 @@ function parseFullProfile(rawResponse, vanity) {
|
|
|
29
59
|
endYear: item?.timePeriod?.endDate?.year,
|
|
30
60
|
endMonth: item?.timePeriod?.endDate?.month,
|
|
31
61
|
};
|
|
62
|
+
// Try to extract company URN and numeric id robustly
|
|
63
|
+
const candUrns = [
|
|
64
|
+
item?.companyUrn,
|
|
65
|
+
item?.company?.entityUrn,
|
|
66
|
+
item?.company?.companyUrn,
|
|
67
|
+
item?.companyUrnV2,
|
|
68
|
+
].filter(Boolean);
|
|
69
|
+
const foundUrn = candUrns.find(u => /urn:li:(?:fsd_)?company:/i.test(u)) || deepFindCompanyUrn(item);
|
|
70
|
+
if (foundUrn) {
|
|
71
|
+
pos.companyUrn = foundUrn;
|
|
72
|
+
const cid = extractCompanyIdFromUrn(foundUrn);
|
|
73
|
+
if (cid)
|
|
74
|
+
pos.companyId = cid;
|
|
75
|
+
}
|
|
32
76
|
profile.positions.push(pos);
|
|
33
77
|
}
|
|
34
78
|
// Educations
|
|
@@ -47,14 +91,11 @@ function parseFullProfile(rawResponse, vanity) {
|
|
|
47
91
|
};
|
|
48
92
|
profile.educations.push(edu);
|
|
49
93
|
}
|
|
50
|
-
// Avatar
|
|
94
|
+
// Avatar (via helper)
|
|
51
95
|
const avatarItem = included.find((it) => it?.vectorImage && JSON.stringify(it).includes('vectorImage'));
|
|
52
96
|
const vector = avatarItem?.vectorImage;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (seg)
|
|
57
|
-
profile.avatarUrl = seg.startsWith('http') ? seg : (vector.rootUrl || '') + seg;
|
|
58
|
-
}
|
|
97
|
+
const bestUrl = (0, image_parser_1.selectBestImageUrl)(vector);
|
|
98
|
+
if (bestUrl)
|
|
99
|
+
profile.avatarUrl = bestUrl;
|
|
59
100
|
return profile;
|
|
60
101
|
}
|
|
@@ -16,9 +16,18 @@ function parseSalesSearchResults(rawResponse) {
|
|
|
16
16
|
summary: el?.summary,
|
|
17
17
|
};
|
|
18
18
|
// fsdKey from URN patterns
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
const urn = String(el?.entityUrn || '');
|
|
20
|
+
const m3 = urn.match(/^urn:li:(?:fs_salesProfile|fsd_profile):\(([^,\s)]+),([^,\s)]+),([^,\s)]+)\)/i);
|
|
21
|
+
if (m3) {
|
|
22
|
+
res.fsdKey = m3[1];
|
|
23
|
+
res.salesAuthType = m3[2];
|
|
24
|
+
res.salesAuthToken = m3[3];
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const m1 = urn.match(/^urn:li:(?:fs_salesProfile|fsd_profile):\(([^,\s)]+)/i);
|
|
28
|
+
if (m1)
|
|
29
|
+
res.fsdKey = m1[1];
|
|
30
|
+
}
|
|
22
31
|
// Image best artifact
|
|
23
32
|
const img = el?.profilePictureDisplayImage;
|
|
24
33
|
if (img?.rootUrl && Array.isArray(img?.artifacts) && img.artifacts.length) {
|