linkedin-secret-sauce 0.3.22 → 0.3.24

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
+ }
@@ -128,9 +128,17 @@ async function getProfileByUrn(fsdKey) {
128
128
  return cachedUrn;
129
129
  }
130
130
  const url = `${LINKEDIN_API_BASE}/identity/dash/profiles?q=memberIdentity&memberIdentity=${encodeURIComponent(keyMatch)}&decorationId=com.linkedin.voyager.dash.deco.identity.profile.FullProfileWithEntities-35`;
131
+ try {
132
+ (0, logger_1.log)('debug', 'api.requestUrl', { operation: 'getProfileByUrn', url, fsdKey: keyMatch });
133
+ }
134
+ catch { }
131
135
  let raw;
132
136
  try {
133
137
  raw = await (0, http_client_1.executeLinkedInRequest)({ url }, 'getProfileByUrn');
138
+ try {
139
+ (0, logger_1.log)('debug', 'api.rawResponse', { operation: 'getProfileByUrn', responseSize: JSON.stringify(raw).length });
140
+ }
141
+ catch { }
134
142
  }
135
143
  catch (e) {
136
144
  const status = e?.status ?? 0;
@@ -143,9 +151,46 @@ async function getProfileByUrn(fsdKey) {
143
151
  }
144
152
  throw e;
145
153
  }
154
+ // Extract publicIdentifier from response to validate correct profile selection
155
+ // BUG FIX: LinkedIn API returns multiple profiles (requested profile + connections/colleagues)
156
+ // The correct profile is referenced in data.*elements[0] - we must match against this URN
157
+ const rr = raw;
158
+ const included = Array.isArray(rr?.included) ? rr.included : [];
159
+ // Get the primary profile URN from data.*elements[0]
160
+ const dataObj = rr?.data;
161
+ const elementsArray = dataObj?.['*elements'];
162
+ const requestedProfileUrn = elementsArray?.[0];
163
+ // Find the profile in included[] that matches the requested URN
164
+ const identityObj = requestedProfileUrn
165
+ ? included.find((it) => {
166
+ const rec = it;
167
+ const isProfile = String(rec?.$type || '').includes('identity.profile.Profile');
168
+ if (!isProfile)
169
+ return false;
170
+ // Match the entityUrn against the requested profile URN from data.*elements[0]
171
+ return rec.entityUrn === requestedProfileUrn;
172
+ })
173
+ : undefined;
174
+ const publicIdentifier = identityObj?.publicIdentifier || '';
175
+ try {
176
+ (0, logger_1.log)('debug', 'api.extractedIdentity', {
177
+ operation: 'getProfileByUrn',
178
+ publicIdentifier,
179
+ firstName: identityObj?.firstName,
180
+ lastName: identityObj?.lastName,
181
+ objectUrn: identityObj?.objectUrn,
182
+ profilesInResponse: included.filter(it => String(it?.$type || '').includes('identity.profile.Profile')).length
183
+ });
184
+ }
185
+ catch { }
146
186
  let prof;
147
187
  try {
148
- prof = (0, profile_parser_1.parseFullProfile)(raw, '');
188
+ // Pass publicIdentifier to parser so it can validate profile selection (same logic as getProfileByVanity)
189
+ prof = (0, profile_parser_1.parseFullProfile)(raw, publicIdentifier);
190
+ try {
191
+ (0, logger_1.log)('debug', 'api.parsedProfile', { operation: 'getProfileByUrn', firstName: prof.firstName, lastName: prof.lastName, vanity: prof.vanity });
192
+ }
193
+ catch { }
149
194
  }
150
195
  catch {
151
196
  try {
@@ -135,7 +135,7 @@ function buildLeadSearchQuery(keywords, filters) {
135
135
  // If not URN or numeric, assume it's already a URN (pass through)
136
136
  urn = idStr;
137
137
  }
138
- return valObj([`id:${urn}`, 'selectionType:INCLUDED']);
138
+ return valObj([`id:${encodeURIComponent(urn)}`, 'selectionType:INCLUDED']);
139
139
  });
140
140
  }
141
141
  const curCompanies = encodeCompanies(filters?.company?.current?.include);
@@ -171,6 +171,17 @@ function buildLeadSearchQuery(keywords, filters) {
171
171
  if (yr?.years_experience_ids?.length) {
172
172
  pushFilter(f, 'YEARS_OF_EXPERIENCE', yr.years_experience_ids.map((id) => valObj([`id:${id}`, 'selectionType:INCLUDED'])));
173
173
  }
174
- const filtersPart = f.length ? `,filters:${list(f)}` : '';
175
- return `(spellCorrectionEnabled:true,keywords:${encodedKw}${filtersPart})`;
174
+ // Build query with minimal format for maximum compatibility
175
+ // LinkedIn rejects spellCorrectionEnabled:true,keywords: with 400 errors
176
+ const parts = [];
177
+ // Only include keywords if non-empty
178
+ if (encodedKw) {
179
+ parts.push(`keywords:${encodedKw}`);
180
+ }
181
+ // Add filters if present
182
+ if (f.length) {
183
+ parts.push(`filters:${list(f)}`);
184
+ }
185
+ // Return minimal format - no spellCorrectionEnabled wrapper
186
+ return parts.length > 0 ? `(${parts.join(',')})` : '()';
176
187
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linkedin-secret-sauce",
3
- "version": "0.3.22",
3
+ "version": "0.3.24",
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",