linkedin-secret-sauce 0.3.29 → 0.4.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.
@@ -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 = 'https://www.linkedin.com/voyager/api';
60
- const SALES_NAV_BASE = 'https://www.linkedin.com/sales-api';
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 || '').toLowerCase();
98
+ const key = String(vanity || "").toLowerCase();
83
99
  try {
84
- (0, logger_1.log)('info', 'api.start', { operation: 'getProfileByVanity', selector: key });
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)('profileCacheHits');
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)('inflightDedupeHits');
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 }, 'getProfileByVanity');
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)('warn', 'api.notFound', { operation: 'getProfileByVanity', selector: key, status });
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('Profile not found', errors_1.ERROR_CODES.NOT_FOUND, 404);
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)('error', 'api.parseError', { operation: 'getProfileByVanity', selector: key });
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('Failed to parse profile', errors_1.ERROR_CODES.PARSE_ERROR, 500);
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)('profileFetches');
155
+ (0, metrics_1.incrementMetric)("profileFetches");
130
156
  try {
131
- (0, logger_1.log)('info', 'api.ok', { operation: 'getProfileByVanity', selector: key });
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 || '').trim();
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('Invalid URN or key', errors_1.ERROR_CODES.INVALID_INPUT, 400);
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)('info', 'api.start', { operation: 'getProfileByUrn', selector: keyMatch });
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)('profileCacheHits');
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)('debug', 'api.requestUrl', { operation: 'getProfileByUrn', url, fsdKey: keyMatch });
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 }, 'getProfileByUrn');
229
+ raw = await (0, http_client_1.executeLinkedInRequest)({ url }, "getProfileByUrn");
171
230
  try {
172
- (0, logger_1.log)('debug', 'api.rawResponse', { operation: 'getProfileByUrn', responseSize: JSON.stringify(raw).length });
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)('warn', 'api.notFound', { operation: 'getProfileByUrn', selector: cacheKey, status });
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('Profile not found', errors_1.ERROR_CODES.NOT_FOUND, 404);
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) ? 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?.['*elements'];
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 || '').includes('identity.profile.Profile');
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
- const publicIdentifier = identityObj?.publicIdentifier || '';
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)('debug', 'api.extractedIdentity', {
210
- operation: 'getProfileByUrn',
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 || '').includes('identity.profile.Profile')).length
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)('debug', 'api.parsedProfile', { operation: 'getProfileByUrn', firstName: prof.firstName, lastName: prof.lastName, vanity: prof.vanity });
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)('error', 'api.parseError', { operation: 'getProfileByUrn', selector: cacheKey });
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('Failed to parse profile', errors_1.ERROR_CODES.PARSE_ERROR, 500);
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)('profileFetches');
326
+ (0, metrics_1.incrementMetric)("profileFetches");
237
327
  try {
238
- (0, logger_1.log)('info', 'api.ok', { operation: 'getProfileByUrn', selector: cacheKey });
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 || 'com.linkedin.sales.deco.desktop.searchv2.LeadSearchResult-14';
248
- // Generate or use provided sessionId
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
- // Phase 2.1: Include sessionId in cache key for session-specific caching
252
- const cacheKey = JSON.stringify({ k: String(keywords || '').toLowerCase(), start, count, deco, sig, sessionId });
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)('searchCacheHits');
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)('info', 'api.start', { operation: 'searchSalesLeads', selector: { keywords, start, count, deco: decorationId, sessionId } });
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: 'https://www.linkedin.com/sales/search/people' },
418
+ headers: { Referer: "https://www.linkedin.com/sales/search/people" },
274
419
  sessionId, // Phase 2.1: Pass sessionId for account stickiness
275
- }, 'searchSalesLeads');
420
+ }, "searchSalesLeads");
276
421
  try {
277
- (0, logger_1.log)('info', 'api.ok', { operation: 'searchSalesLeads', selector: { keywords, start, count, deco: decorationId, sessionId } });
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 = (raw && typeof raw === 'object') ? raw : undefined;
286
- const pagingVal = rrec && 'paging' in rrec ? rrec.paging : undefined;
287
- const p = (pagingVal && typeof pagingVal === 'object') ? pagingVal : undefined;
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 && 'metadata' in rrec ? rrec.metadata : undefined;
291
- const metadata = (metadataVal && typeof metadataVal === 'object') ? metadataVal : undefined;
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 ? (start + count) < paging.total : items.length === count;
295
- const result = options
296
- ? {
297
- items,
298
- page: {
299
- start: Number(paging.start ?? start),
300
- count: Number(paging.count ?? count),
301
- total: paging?.total,
302
- hasMore
303
- },
304
- metadata: metadata?.totalDisplayCount ? { totalDisplayCount: metadata.totalDisplayCount } : undefined,
305
- _meta: { sessionId } // Return sessionId to consumer
306
- }
307
- : items; // backward-compat: old tests expect an array when no options passed
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)('searchCacheMisses');
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 || '').trim())}`;
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)('info', 'api.start', { operation: 'resolveCompanyUniversalName', selector: universalName });
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 }, 'resolveCompanyUniversalName');
344
- const rec = (raw && typeof raw === 'object') ? raw : undefined;
345
- const elementsVal = rec && 'elements' in rec ? rec.elements : undefined;
346
- const first = Array.isArray(elementsVal) ? elementsVal[0] : undefined;
347
- const id = first?.id ?? first?.entityUrn ?? undefined;
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)('info', 'api.ok', { operation: 'resolveCompanyUniversalName', selector: universalName, id });
521
+ (0, logger_1.log)("info", "api.ok", {
522
+ operation: "resolveCompanyUniversalName",
523
+ selector: universalName,
524
+ id,
525
+ });
350
526
  }
351
527
  catch { }
352
- return { companyId: id ? String(id).replace(/^urn:li:fsd_company:/, '') : undefined };
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)('warn', 'api.notFound', { operation: 'resolveCompanyUniversalName', selector: universalName, status });
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('Company not found', errors_1.ERROR_CODES.NOT_FOUND, 404);
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 || '').trim();
566
+ const key = String(companyId || "").trim();
369
567
  const cached = getCached(companyCache, key, cfg.companyCacheTtl);
370
568
  if (cached) {
371
- (0, metrics_1.incrementMetric)('companyCacheHits');
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)('info', 'api.start', { operation: 'getCompanyById', selector: key });
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 }, 'getCompanyById');
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)('companyFetches');
580
+ (0, metrics_1.incrementMetric)("companyFetches");
383
581
  companyCache.set(key, { data: parsed, ts: Date.now() });
384
582
  try {
385
- (0, logger_1.log)('info', 'api.ok', { operation: 'getCompanyById', selector: key });
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)('warn', 'api.notFound', { operation: 'getCompanyById', selector: key, status });
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('Company not found', errors_1.ERROR_CODES.NOT_FOUND, 404);
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 || '').trim();
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('Invalid company URL scheme', errors_1.ERROR_CODES.INVALID_INPUT, 400);
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('Invalid company URL', errors_1.ERROR_CODES.INVALID_INPUT, 400);
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)('error', 'api.invalidInput', { operation: 'getCompanyByUrl', selector: companyUrl });
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)('warn', 'api.notFound', { operation: 'getCompanyByUrl', selector: companyUrl });
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 || '').trim();
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 ?? '').toString().trim();
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 'FUNCTION':
685
+ case "FUNCTION":
453
686
  staticOptions = constants_1.FUNCTION_OPTIONS;
454
687
  break;
455
- case 'BING_GEO':
688
+ case "BING_GEO":
456
689
  staticOptions = constants_1.REGION_OPTIONS;
457
690
  break;
458
- case 'PROFILE_LANGUAGE':
691
+ case "PROFILE_LANGUAGE":
459
692
  staticOptions = constants_1.LANGUAGE_OPTIONS;
460
693
  break;
461
- case 'INDUSTRY':
694
+ case "INDUSTRY":
462
695
  staticOptions = constants_1.INDUSTRY_OPTIONS;
463
696
  break;
464
- case 'SENIORITY_LEVEL':
697
+ case "SENIORITY_LEVEL":
465
698
  staticOptions = constants_1.SENIORITY_OPTIONS;
466
699
  break;
467
- case 'COMPANY_SIZE':
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 => ({ id: String(opt.id), text: opt.text })),
474
- page: { start: 0, count: staticOptions.length, total: staticOptions.length }
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)('typeaheadCacheHits');
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)('typeaheadRequests');
728
+ (0, metrics_1.incrementMetric)("typeaheadRequests");
489
729
  try {
490
- (0, logger_1.log)('info', 'api.start', { operation: 'typeahead', selector: { type, query, start, count } });
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 }, 'typeahead');
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 ?? it?.backendId ?? ''),
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 = (raw && typeof raw === 'object') ? raw : undefined;
506
- const pagingVal2 = rtype && 'paging' in rtype ? rtype.paging : undefined;
507
- const paging2 = (pagingVal2 && typeof pagingVal2 === 'object') ? pagingVal2 : undefined;
508
- const result = { items, page: { start: Number(paging2?.start ?? start), count: Number(paging2?.count ?? count), total: paging2?.total } };
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)('info', 'api.ok', { operation: 'typeahead', selector: { type, query }, count: items.length });
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 || '').trim();
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) && /authType:/.test(idOrUrn) && /authToken:/.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('urn:') ? idOrUrn : encodeURIComponent(idOrUrn);
856
+ pathSeg = idOrUrn.startsWith("urn:")
857
+ ? idOrUrn
858
+ : encodeURIComponent(idOrUrn);
593
859
  }
594
- 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)))');
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)({ url, headers: { Referer: 'https://www.linkedin.com/sales/search/people' } }, 'getSalesNavigatorProfileDetails');
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)('info', 'api.start', { operation: 'getSalesNavigatorProfileDetails', selector: idOrUrn });
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)('warn', 'api.salesProfileFallback', { from: pathSeg, to: fallbackSeg });
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)('warn', 'api.notFound', { operation: 'getSalesNavigatorProfileDetails', selector: idOrUrn, status });
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('Sales profile not found', errors_1.ERROR_CODES.NOT_FOUND, 404);
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)('info', 'api.ok', { operation: 'getSalesNavigatorProfileDetails', selector: idOrUrn });
911
+ (0, logger_1.log)("info", "api.ok", {
912
+ operation: "getSalesNavigatorProfileDetails",
913
+ selector: idOrUrn,
914
+ });
633
915
  }
634
916
  catch { }
635
917
  return raw;