linkedin-secret-sauce 0.3.23 → 0.3.25

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.
@@ -1,4 +1,12 @@
1
1
  import type { LinkedInCookie } from './types';
2
+ interface AccountEntry {
3
+ accountId: string;
4
+ cookies: LinkedInCookie[];
5
+ expiresAt?: number;
6
+ failures: number;
7
+ cooldownUntil: number;
8
+ lastUsedAt?: number;
9
+ }
2
10
  export declare function initializeCookiePool(): Promise<void>;
3
11
  export declare function getCookiePoolHealth(): {
4
12
  initialized: boolean;
@@ -30,3 +38,31 @@ export declare function buildCookieHeader(cookies: LinkedInCookie[]): string;
30
38
  export declare function extractCsrfToken(cookies: LinkedInCookie[]): string;
31
39
  export declare function adminSetCooldown(accountId: string, ms: number): void;
32
40
  export declare function adminResetAccount(accountId: string): void;
41
+ /**
42
+ * TEST-ONLY: Get all account IDs from the pool
43
+ * @returns Array of account IDs in the pool
44
+ */
45
+ export declare function _testGetAllAccountIds(): string[];
46
+ /**
47
+ * TEST-ONLY: Get cookies for a specific account
48
+ * @param accountId - The account ID to get cookies for
49
+ * @returns Cookies array or undefined if account not found
50
+ */
51
+ export declare function _testGetAccountCookies(accountId: string): LinkedInCookie[] | undefined;
52
+ /**
53
+ * TEST-ONLY: Get full account entry for diagnostics
54
+ * @param accountId - The account ID to inspect
55
+ * @returns Account entry with all metadata
56
+ */
57
+ export declare function _testGetAccountEntry(accountId: string): AccountEntry | undefined;
58
+ /**
59
+ * TEST-ONLY: Get current pool state snapshot
60
+ * @returns Pool state for debugging
61
+ */
62
+ export declare function _testGetPoolState(): {
63
+ initialized: boolean;
64
+ totalAccounts: number;
65
+ currentRRIndex: number;
66
+ accountOrder: string[];
67
+ };
68
+ export {};
@@ -47,6 +47,10 @@ exports.buildCookieHeader = buildCookieHeader;
47
47
  exports.extractCsrfToken = extractCsrfToken;
48
48
  exports.adminSetCooldown = adminSetCooldown;
49
49
  exports.adminResetAccount = adminResetAccount;
50
+ exports._testGetAllAccountIds = _testGetAllAccountIds;
51
+ exports._testGetAccountCookies = _testGetAccountCookies;
52
+ exports._testGetAccountEntry = _testGetAccountEntry;
53
+ exports._testGetPoolState = _testGetPoolState;
50
54
  const cosiall_client_1 = require("./cosiall-client");
51
55
  const config_1 = require("./config");
52
56
  const errors_1 = require("./utils/errors");
@@ -357,3 +361,52 @@ function adminResetAccount(accountId) {
357
361
  entry.failures = 0;
358
362
  entry.cooldownUntil = 0;
359
363
  }
364
+ // ============================================================================
365
+ // TEST-ONLY EXPORTS - Not for production use
366
+ // ============================================================================
367
+ // These functions expose internal pool state for diagnostic/testing purposes.
368
+ // They bypass normal selection logic and should only be used in test scripts.
369
+ /**
370
+ * TEST-ONLY: Get all account IDs from the pool
371
+ * @returns Array of account IDs in the pool
372
+ */
373
+ function _testGetAllAccountIds() {
374
+ if (!poolState.initialized) {
375
+ throw new Error('TEST: Cookie pool not initialized. Call initializeLinkedInClient() first.');
376
+ }
377
+ return Array.from(poolState.accounts.keys());
378
+ }
379
+ /**
380
+ * TEST-ONLY: Get cookies for a specific account
381
+ * @param accountId - The account ID to get cookies for
382
+ * @returns Cookies array or undefined if account not found
383
+ */
384
+ function _testGetAccountCookies(accountId) {
385
+ if (!poolState.initialized) {
386
+ throw new Error('TEST: Cookie pool not initialized. Call initializeLinkedInClient() first.');
387
+ }
388
+ return poolState.accounts.get(accountId)?.cookies;
389
+ }
390
+ /**
391
+ * TEST-ONLY: Get full account entry for diagnostics
392
+ * @param accountId - The account ID to inspect
393
+ * @returns Account entry with all metadata
394
+ */
395
+ function _testGetAccountEntry(accountId) {
396
+ if (!poolState.initialized) {
397
+ throw new Error('TEST: Cookie pool not initialized. Call initializeLinkedInClient() first.');
398
+ }
399
+ return poolState.accounts.get(accountId);
400
+ }
401
+ /**
402
+ * TEST-ONLY: Get current pool state snapshot
403
+ * @returns Pool state for debugging
404
+ */
405
+ function _testGetPoolState() {
406
+ return {
407
+ initialized: poolState.initialized,
408
+ totalAccounts: poolState.accounts.size,
409
+ currentRRIndex: poolState.rrIndex,
410
+ accountOrder: [...poolState.order],
411
+ };
412
+ }
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.COMPANY_SIZE_OPTIONS = exports.INDUSTRY_OPTIONS = exports.LANGUAGE_OPTIONS = exports.REGION_OPTIONS = exports.FUNCTION_OPTIONS = exports.SENIORITY_OPTIONS = exports.YEARS_OF_EXPERIENCE_OPTIONS = exports.YEARS_IN_POSITION_OPTIONS = exports.YEARS_AT_COMPANY_OPTIONS = void 0;
4
37
  exports.getProfileByVanity = getProfileByVanity;
@@ -128,9 +161,17 @@ async function getProfileByUrn(fsdKey) {
128
161
  return cachedUrn;
129
162
  }
130
163
  const url = `${LINKEDIN_API_BASE}/identity/dash/profiles?q=memberIdentity&memberIdentity=${encodeURIComponent(keyMatch)}&decorationId=com.linkedin.voyager.dash.deco.identity.profile.FullProfileWithEntities-35`;
164
+ try {
165
+ (0, logger_1.log)('debug', 'api.requestUrl', { operation: 'getProfileByUrn', url, fsdKey: keyMatch });
166
+ }
167
+ catch { }
131
168
  let raw;
132
169
  try {
133
170
  raw = await (0, http_client_1.executeLinkedInRequest)({ url }, 'getProfileByUrn');
171
+ try {
172
+ (0, logger_1.log)('debug', 'api.rawResponse', { operation: 'getProfileByUrn', responseSize: JSON.stringify(raw).length });
173
+ }
174
+ catch { }
134
175
  }
135
176
  catch (e) {
136
177
  const status = e?.status ?? 0;
@@ -143,9 +184,46 @@ async function getProfileByUrn(fsdKey) {
143
184
  }
144
185
  throw e;
145
186
  }
187
+ // Extract publicIdentifier from response to validate correct profile selection
188
+ // BUG FIX: LinkedIn API returns multiple profiles (requested profile + connections/colleagues)
189
+ // The correct profile is referenced in data.*elements[0] - we must match against this URN
190
+ const rr = raw;
191
+ const included = Array.isArray(rr?.included) ? rr.included : [];
192
+ // Get the primary profile URN from data.*elements[0]
193
+ const dataObj = rr?.data;
194
+ const elementsArray = dataObj?.['*elements'];
195
+ const requestedProfileUrn = elementsArray?.[0];
196
+ // Find the profile in included[] that matches the requested URN
197
+ const identityObj = requestedProfileUrn
198
+ ? included.find((it) => {
199
+ const rec = it;
200
+ const isProfile = String(rec?.$type || '').includes('identity.profile.Profile');
201
+ if (!isProfile)
202
+ return false;
203
+ // Match the entityUrn against the requested profile URN from data.*elements[0]
204
+ return rec.entityUrn === requestedProfileUrn;
205
+ })
206
+ : undefined;
207
+ const publicIdentifier = identityObj?.publicIdentifier || '';
208
+ try {
209
+ (0, logger_1.log)('debug', 'api.extractedIdentity', {
210
+ operation: 'getProfileByUrn',
211
+ publicIdentifier,
212
+ firstName: identityObj?.firstName,
213
+ lastName: identityObj?.lastName,
214
+ objectUrn: identityObj?.objectUrn,
215
+ profilesInResponse: included.filter(it => String(it?.$type || '').includes('identity.profile.Profile')).length
216
+ });
217
+ }
218
+ catch { }
146
219
  let prof;
147
220
  try {
148
- prof = (0, profile_parser_1.parseFullProfile)(raw, '');
221
+ // Pass publicIdentifier to parser so it can validate profile selection (same logic as getProfileByVanity)
222
+ prof = (0, profile_parser_1.parseFullProfile)(raw, publicIdentifier);
223
+ try {
224
+ (0, logger_1.log)('debug', 'api.parsedProfile', { operation: 'getProfileByUrn', firstName: prof.firstName, lastName: prof.lastName, vanity: prof.vanity });
225
+ }
226
+ catch { }
149
227
  }
150
228
  catch {
151
229
  try {
@@ -167,9 +245,11 @@ async function searchSalesLeads(keywords, options) {
167
245
  const start = Number.isFinite(options?.start) ? Number(options.start) : 0;
168
246
  const count = Number.isFinite(options?.count) ? Number(options.count) : 25;
169
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();
170
250
  const sig = (0, search_encoder_1.buildFilterSignature)(options?.filters, options?.rawQuery);
171
251
  // Phase 2.1: Include sessionId in cache key for session-specific caching
172
- const cacheKey = JSON.stringify({ k: String(keywords || '').toLowerCase(), start, count, deco, sig, sessionId: options?.sessionId || null });
252
+ const cacheKey = JSON.stringify({ k: String(keywords || '').toLowerCase(), start, count, deco, sig, sessionId });
173
253
  const cached = getCached(searchCache, cacheKey, cfg.searchCacheTtl);
174
254
  if (cached) {
175
255
  (0, metrics_1.incrementMetric)('searchCacheHits');
@@ -185,16 +265,16 @@ async function searchSalesLeads(keywords, options) {
185
265
  async function doRequest(decorationId) {
186
266
  const url = `${SALES_NAV_BASE}/salesApiLeadSearch?q=searchQuery&start=${start}&count=${count}&decorationId=${encodeURIComponent(decorationId)}&query=${queryStruct}`;
187
267
  try {
188
- (0, logger_1.log)('info', 'api.start', { operation: 'searchSalesLeads', selector: { keywords, start, count, deco: decorationId, sessionId: options?.sessionId } });
268
+ (0, logger_1.log)('info', 'api.start', { operation: 'searchSalesLeads', selector: { keywords, start, count, deco: decorationId, sessionId } });
189
269
  }
190
270
  catch { }
191
271
  const out = await (0, http_client_1.executeLinkedInRequest)({
192
272
  url,
193
273
  headers: { Referer: 'https://www.linkedin.com/sales/search/people' },
194
- sessionId: options?.sessionId, // Phase 2.1: Pass sessionId for account stickiness
274
+ sessionId, // Phase 2.1: Pass sessionId for account stickiness
195
275
  }, 'searchSalesLeads');
196
276
  try {
197
- (0, logger_1.log)('info', 'api.ok', { operation: 'searchSalesLeads', selector: { keywords, start, count, deco: decorationId, sessionId: options?.sessionId } });
277
+ (0, logger_1.log)('info', 'api.ok', { operation: 'searchSalesLeads', selector: { keywords, start, count, deco: decorationId, sessionId } });
198
278
  }
199
279
  catch { }
200
280
  return out;
@@ -213,7 +293,8 @@ async function searchSalesLeads(keywords, options) {
213
293
  ? {
214
294
  items,
215
295
  page: { start: Number(paging.start ?? start), count: Number(paging.count ?? count), total: paging?.total },
216
- metadata: metadata?.totalDisplayCount ? { totalDisplayCount: metadata.totalDisplayCount } : undefined
296
+ metadata: metadata?.totalDisplayCount ? { totalDisplayCount: metadata.totalDisplayCount } : undefined,
297
+ _meta: { sessionId } // Return sessionId to consumer
217
298
  }
218
299
  : items; // backward-compat: old tests expect an array when no options passed
219
300
  searchCache.set(cacheKey, { data: result, ts: Date.now() });
package/dist/types.d.ts CHANGED
@@ -157,6 +157,10 @@ export interface SearchSalesResult {
157
157
  metadata?: {
158
158
  totalDisplayCount?: string;
159
159
  };
160
+ _meta?: {
161
+ sessionId?: string;
162
+ accountId?: string;
163
+ };
160
164
  }
161
165
  export interface Company {
162
166
  companyId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linkedin-secret-sauce",
3
- "version": "0.3.23",
3
+ "version": "0.3.25",
4
4
  "description": "Private LinkedIn Sales Navigator client with automatic cookie management",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",