linkedin-secret-sauce 0.3.12 → 0.3.14

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/dist/config.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { SentryClient } from './utils/sentry';
1
2
  export interface LinkedInClientConfig {
2
3
  cosiallApiUrl: string;
3
4
  cosiallApiKey: string;
@@ -14,6 +15,10 @@ export interface LinkedInClientConfig {
14
15
  accountCooldownMs?: number;
15
16
  maxFailuresBeforeCooldown?: number;
16
17
  maxRequestHistory?: number;
18
+ eagerInitialization?: boolean;
19
+ onInitializationError?: (error: Error) => void;
20
+ sentryClient?: SentryClient;
21
+ fallbackWithoutProxyOnError?: boolean;
17
22
  }
18
- export declare function initializeLinkedInClient(config: LinkedInClientConfig): void;
23
+ export declare function initializeLinkedInClient(config: LinkedInClientConfig): Promise<void>;
19
24
  export declare function getConfig(): LinkedInClientConfig;
package/dist/config.js CHANGED
@@ -1,10 +1,43 @@
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.initializeLinkedInClient = initializeLinkedInClient;
4
37
  exports.getConfig = getConfig;
5
38
  const errors_1 = require("./utils/errors");
6
39
  let globalConfig = null;
7
- function initializeLinkedInClient(config) {
40
+ async function initializeLinkedInClient(config) {
8
41
  // Basic presence validation
9
42
  if (!config || !config.cosiallApiUrl || !config.cosiallApiKey) {
10
43
  throw new errors_1.LinkedInClientError('Missing required credentials', 'INVALID_CONFIG', 400);
@@ -19,9 +52,9 @@ function initializeLinkedInClient(config) {
19
52
  // Apply defaults (package-wide reasonable defaults)
20
53
  const withDefaults = {
21
54
  profileCacheTtl: 15 * 60 * 1000,
22
- searchCacheTtl: 3 * 60 * 1000,
23
- companyCacheTtl: 10 * 60 * 1000, // 600_000 ms
24
- typeaheadCacheTtl: 60 * 60 * 1000, // 3_600_000 ms
55
+ searchCacheTtl: 15 * 60 * 1000, // Increased from 3m to 15m for better caching
56
+ companyCacheTtl: 10 * 60 * 1000,
57
+ typeaheadCacheTtl: 60 * 60 * 1000,
25
58
  cookieRefreshInterval: 15 * 60 * 1000,
26
59
  cookieFreshnessWindow: 24 * 60 * 60 * 1000,
27
60
  maxRetries: 2,
@@ -30,8 +63,15 @@ function initializeLinkedInClient(config) {
30
63
  maxFailuresBeforeCooldown: 3,
31
64
  logLevel: 'info',
32
65
  maxRequestHistory: 500,
66
+ eagerInitialization: true, // NEW: Default to eager initialization
67
+ fallbackWithoutProxyOnError: false, // NEW: Opt-in for proxy fallback
33
68
  ...config,
34
69
  };
70
+ // Configure Sentry if provided
71
+ if (withDefaults.sentryClient) {
72
+ const { setSentryClient } = await Promise.resolve().then(() => __importStar(require('./utils/sentry')));
73
+ setSentryClient(withDefaults.sentryClient);
74
+ }
35
75
  // Optionally mask API key from accidental JSON serialization
36
76
  try {
37
77
  Object.defineProperty(withDefaults, 'cosiallApiKey', {
@@ -48,6 +88,28 @@ function initializeLinkedInClient(config) {
48
88
  if (!globalConfig) {
49
89
  globalConfig = withDefaults;
50
90
  }
91
+ // NEW: Eager initialization if enabled
92
+ if (withDefaults.eagerInitialization && process.env.NODE_ENV !== 'test') {
93
+ try {
94
+ const { initializeCookiePool } = await Promise.resolve().then(() => __importStar(require('./cookie-pool')));
95
+ await initializeCookiePool();
96
+ const { log } = await Promise.resolve().then(() => __importStar(require('./utils/logger')));
97
+ log('info', 'client.initialized', { cookiePoolReady: true });
98
+ }
99
+ catch (err) {
100
+ const error = err;
101
+ const { log } = await Promise.resolve().then(() => __importStar(require('./utils/logger')));
102
+ log('error', 'client.initFailed', { message: error.message, stack: error.stack });
103
+ // Call optional error handler
104
+ if (withDefaults.onInitializationError) {
105
+ withDefaults.onInitializationError(error);
106
+ }
107
+ else {
108
+ // Default: throw to alert consumer immediately
109
+ throw new errors_1.LinkedInClientError('Failed to initialize LinkedIn client: ' + error.message, 'INITIALIZATION_FAILED', 0);
110
+ }
111
+ }
112
+ }
51
113
  }
52
114
  function getConfig() {
53
115
  if (!globalConfig) {
@@ -1,5 +1,17 @@
1
1
  import type { LinkedInCookie } from './types';
2
- export declare function selectAccountForRequest(): Promise<{
2
+ export declare function initializeCookiePool(): Promise<void>;
3
+ export declare function getCookiePoolHealth(): {
4
+ initialized: boolean;
5
+ totalAccounts: number;
6
+ healthyAccounts: number;
7
+ expiredAccounts: number;
8
+ coolingDownAccounts: number;
9
+ };
10
+ export declare function forceRefreshCookies(): Promise<void>;
11
+ export declare function getAccountForSession(sessionId: string): string | undefined;
12
+ export declare function setAccountForSession(sessionId: string, accountId: string): void;
13
+ export declare function clearSessionAccount(sessionId: string): void;
14
+ export declare function selectAccountForRequest(sessionId?: string): Promise<{
3
15
  accountId: string;
4
16
  cookies: LinkedInCookie[];
5
17
  }>;
@@ -1,5 +1,44 @@
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 });
36
+ exports.initializeCookiePool = initializeCookiePool;
37
+ exports.getCookiePoolHealth = getCookiePoolHealth;
38
+ exports.forceRefreshCookies = forceRefreshCookies;
39
+ exports.getAccountForSession = getAccountForSession;
40
+ exports.setAccountForSession = setAccountForSession;
41
+ exports.clearSessionAccount = clearSessionAccount;
3
42
  exports.selectAccountForRequest = selectAccountForRequest;
4
43
  exports.getAccountsSummary = getAccountsSummary;
5
44
  exports.reportAccountFailure = reportAccountFailure;
@@ -13,12 +52,16 @@ const config_1 = require("./config");
13
52
  const errors_1 = require("./utils/errors");
14
53
  const logger_1 = require("./utils/logger");
15
54
  const metrics_1 = require("./utils/metrics");
55
+ // Session tracking for account stickiness (Phase 2.1)
56
+ const sessionAccountMap = new Map();
16
57
  const poolState = {
17
58
  initialized: false,
18
59
  order: [],
19
60
  accounts: new Map(),
20
61
  rrIndex: 0,
21
62
  refreshTimer: null,
63
+ lastMassFailureCheck: 0,
64
+ consecutiveMassFailures: 0,
22
65
  };
23
66
  function nowMs() {
24
67
  return Date.now();
@@ -91,13 +134,125 @@ async function ensureInitialized() {
91
134
  catch (e) {
92
135
  const err = e;
93
136
  (0, logger_1.log)('warn', 'cookiePool.refreshFailed', { error: err?.message });
137
+ // Report to Sentry if configured
138
+ try {
139
+ const { reportWarningToSentry } = await Promise.resolve().then(() => __importStar(require('./utils/sentry')));
140
+ reportWarningToSentry('Cookie refresh failed', {
141
+ error: err?.message,
142
+ tags: { component: 'cookie-pool', severity: 'medium' },
143
+ });
144
+ }
145
+ catch { }
94
146
  }
95
147
  }, interval);
96
148
  }
97
149
  }
98
- async function selectAccountForRequest() {
150
+ // Export for eager initialization
151
+ async function initializeCookiePool() {
152
+ await ensureInitialized();
153
+ }
154
+ // Export health check function
155
+ function getCookiePoolHealth() {
156
+ if (!poolState.initialized) {
157
+ return {
158
+ initialized: false,
159
+ totalAccounts: 0,
160
+ healthyAccounts: 0,
161
+ expiredAccounts: 0,
162
+ coolingDownAccounts: 0,
163
+ };
164
+ }
165
+ const now = nowMs();
166
+ const accounts = Array.from(poolState.accounts.values());
167
+ return {
168
+ initialized: true,
169
+ totalAccounts: accounts.length,
170
+ healthyAccounts: accounts.filter(a => !isExpired(a) && a.failures === 0 && a.cooldownUntil <= now).length,
171
+ expiredAccounts: accounts.filter(a => isExpired(a)).length,
172
+ coolingDownAccounts: accounts.filter(a => a.cooldownUntil > now).length,
173
+ };
174
+ }
175
+ // Force refresh cookies (for mass failures)
176
+ async function forceRefreshCookies() {
177
+ try {
178
+ (0, logger_1.log)('info', 'cookiePool.forceRefresh', {});
179
+ const refreshed = await (0, cosiall_client_1.fetchCookiesFromCosiall)();
180
+ // Rebuild state (same logic as periodic refresh)
181
+ const newMap = new Map();
182
+ const newOrder = [];
183
+ for (const acc of refreshed) {
184
+ newMap.set(acc.accountId, {
185
+ accountId: acc.accountId,
186
+ cookies: acc.cookies || [],
187
+ expiresAt: acc.expiresAt,
188
+ failures: 0, // Reset failures after forced refresh
189
+ cooldownUntil: 0, // Clear cooldown
190
+ lastUsedAt: poolState.accounts.get(acc.accountId)?.lastUsedAt,
191
+ });
192
+ newOrder.push(acc.accountId);
193
+ }
194
+ poolState.accounts = newMap;
195
+ poolState.order = newOrder;
196
+ poolState.consecutiveMassFailures = 0; // Reset counter
197
+ (0, logger_1.log)('info', 'cookiePool.forceRefreshed', { count: refreshed.length });
198
+ }
199
+ catch (e) {
200
+ const err = e;
201
+ (0, logger_1.log)('error', 'cookiePool.forceRefreshFailed', { error: err?.message });
202
+ throw e;
203
+ }
204
+ }
205
+ // Phase 2.1: Session management functions
206
+ function getAccountForSession(sessionId) {
207
+ const session = sessionAccountMap.get(sessionId);
208
+ if (!session)
209
+ return undefined;
210
+ // Check if session expired (30 minutes)
211
+ if (Date.now() > session.expiresAt) {
212
+ sessionAccountMap.delete(sessionId);
213
+ return undefined;
214
+ }
215
+ // Check if account is still healthy
216
+ const entry = poolState.accounts.get(session.accountId);
217
+ if (!entry || isExpired(entry) || entry.failures >= getSettings().maxFailures) {
218
+ sessionAccountMap.delete(sessionId);
219
+ return undefined;
220
+ }
221
+ return session.accountId;
222
+ }
223
+ function setAccountForSession(sessionId, accountId) {
224
+ sessionAccountMap.set(sessionId, {
225
+ accountId,
226
+ lastUsedAt: Date.now(),
227
+ expiresAt: Date.now() + (30 * 60 * 1000), // 30 minutes
228
+ });
229
+ (0, logger_1.log)('debug', 'cookiePool.sessionSticky', { sessionId, accountId });
230
+ }
231
+ function clearSessionAccount(sessionId) {
232
+ sessionAccountMap.delete(sessionId);
233
+ (0, logger_1.log)('debug', 'cookiePool.sessionCleared', { sessionId });
234
+ }
235
+ // Modified selectAccountForRequest to support sessions (Phase 2.1)
236
+ async function selectAccountForRequest(sessionId) {
99
237
  await ensureInitialized();
100
238
  const settings = getSettings();
239
+ // If sessionId provided, try to reuse same account
240
+ if (sessionId) {
241
+ const stickyAccountId = getAccountForSession(sessionId);
242
+ if (stickyAccountId) {
243
+ const entry = poolState.accounts.get(stickyAccountId);
244
+ if (entry && !isExpired(entry) && entry.failures < settings.maxFailures && entry.cooldownUntil <= nowMs()) {
245
+ (0, logger_1.log)('debug', 'cookiePool.sessionReuse', { sessionId, accountId: stickyAccountId });
246
+ entry.lastUsedAt = nowMs();
247
+ return { accountId: stickyAccountId, cookies: entry.cookies };
248
+ }
249
+ else {
250
+ // Account no longer healthy, clear session
251
+ clearSessionAccount(sessionId);
252
+ }
253
+ }
254
+ }
255
+ // Round-robin selection for new sessions or when sticky account unavailable
101
256
  const n = poolState.order.length;
102
257
  for (let i = 0; i < n; i++) {
103
258
  const idx = (poolState.rrIndex + i) % n;
@@ -113,9 +268,13 @@ async function selectAccountForRequest() {
113
268
  continue;
114
269
  // Select this account
115
270
  poolState.rrIndex = (idx + 1) % n;
116
- (0, logger_1.log)('debug', 'cookiePool.select', { accountId: entry.accountId, rrIndex: poolState.rrIndex });
271
+ (0, logger_1.log)('debug', 'cookiePool.select', { accountId: entry.accountId, rrIndex: poolState.rrIndex, sessionId: sessionId || 'none' });
117
272
  (0, metrics_1.incrementMetric)('accountSelections');
118
273
  entry.lastUsedAt = nowMs();
274
+ // Set sticky session if sessionId provided
275
+ if (sessionId) {
276
+ setAccountForSession(sessionId, entry.accountId);
277
+ }
119
278
  return { accountId: entry.accountId, cookies: entry.cookies };
120
279
  }
121
280
  throw new errors_1.LinkedInClientError('No valid LinkedIn accounts', 'NO_VALID_ACCOUNTS', 503);
@@ -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.fetchCookiesFromCosiall = fetchCookiesFromCosiall;
4
37
  const config_1 = require("./config");
@@ -21,10 +54,31 @@ async function fetchCookiesFromCosiall() {
21
54
  if (!response.ok) {
22
55
  (0, logger_1.log)('warn', 'cosiall.fetch.error', { status: response.status });
23
56
  (0, metrics_1.incrementMetric)('cosiallFailures');
57
+ // Report Cosiall downtime to Sentry (critical for operations)
58
+ try {
59
+ const { reportCriticalError } = await Promise.resolve().then(() => __importStar(require('./utils/sentry')));
60
+ reportCriticalError('Cosiall API failure - cookie service unavailable', {
61
+ status: response.status,
62
+ url,
63
+ tags: { component: 'cosiall-client', severity: 'critical' },
64
+ });
65
+ }
66
+ catch { }
24
67
  throw new errors_1.LinkedInClientError('Cosiall fetch failed', 'REQUEST_FAILED', response.status);
25
68
  }
26
69
  const data = await response.json();
27
70
  if (!Array.isArray(data)) {
71
+ (0, logger_1.log)('error', 'cosiall.fetch.invalidFormat', { dataType: typeof data });
72
+ // Report data format issues to Sentry
73
+ try {
74
+ const { reportCriticalError } = await Promise.resolve().then(() => __importStar(require('./utils/sentry')));
75
+ reportCriticalError('Cosiall API returned invalid format', {
76
+ expectedType: 'array',
77
+ actualType: typeof data,
78
+ tags: { component: 'cosiall-client', severity: 'critical' },
79
+ });
80
+ }
81
+ catch { }
28
82
  throw new errors_1.LinkedInClientError('Invalid Cosiall response format', 'REQUEST_FAILED', 500);
29
83
  }
30
84
  (0, logger_1.log)('info', 'cosiall.fetch.success', { count: data.length });
@@ -3,5 +3,6 @@ export interface LinkedInRequestOptions {
3
3
  method?: 'GET' | 'POST';
4
4
  body?: unknown;
5
5
  headers?: Record<string, string>;
6
+ sessionId?: string;
6
7
  }
7
8
  export declare function executeLinkedInRequest<T>(options: LinkedInRequestOptions, _operationName: string): Promise<T>;
@@ -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.executeLinkedInRequest = executeLinkedInRequest;
4
37
  const config_1 = require("./config");
@@ -31,15 +64,42 @@ async function executeLinkedInRequest(options, _operationName) {
31
64
  proxySet = true;
32
65
  (0, logger_1.log)('info', 'proxy.set', { host: masked });
33
66
  }
34
- // Try up to 3 different accounts on auth/rate failures
67
+ // Phase 1.4: Try ALL available accounts (not just 3)
68
+ const health = (0, cookie_pool_1.getCookiePoolHealth)();
69
+ const maxRotations = health.totalAccounts > 0 ? health.totalAccounts : 3;
35
70
  let lastError;
36
- for (let rotation = 0; rotation < 3; rotation++) {
71
+ let accountsTriedCount = 0;
72
+ for (let rotation = 0; rotation < maxRotations; rotation++) {
37
73
  let selection;
38
74
  try {
39
- selection = await (0, cookie_pool_1.selectAccountForRequest)();
75
+ selection = await (0, cookie_pool_1.selectAccountForRequest)(options.sessionId);
40
76
  }
41
77
  catch (err) {
42
- if (process.env.NODE_ENV !== 'test')
78
+ const e = err;
79
+ // Phase 1.3: If NO_VALID_ACCOUNTS, try force refresh once
80
+ if (e?.code === 'NO_VALID_ACCOUNTS' && accountsTriedCount > 0) {
81
+ (0, logger_1.log)('warn', 'http.noValidAccounts.forceRefresh', { accountsTriedCount });
82
+ try {
83
+ await (0, cookie_pool_1.forceRefreshCookies)();
84
+ // Retry selection after refresh
85
+ selection = await (0, cookie_pool_1.selectAccountForRequest)(options.sessionId);
86
+ }
87
+ catch (refreshErr) {
88
+ const re = refreshErr;
89
+ (0, logger_1.log)('error', 'http.forceRefreshFailed', { error: re?.message });
90
+ // Report to Sentry
91
+ try {
92
+ const { reportCriticalError } = await Promise.resolve().then(() => __importStar(require('./utils/sentry')));
93
+ reportCriticalError('All accounts exhausted, force refresh failed', {
94
+ error: re?.message,
95
+ accountsTriedCount,
96
+ tags: { component: 'http-client', severity: 'critical' },
97
+ });
98
+ }
99
+ catch { }
100
+ }
101
+ }
102
+ if (!selection && process.env.NODE_ENV !== 'test')
43
103
  throw err;
44
104
  }
45
105
  if (!selection || !selection.accountId) {
@@ -54,6 +114,7 @@ async function executeLinkedInRequest(options, _operationName) {
54
114
  }
55
115
  }
56
116
  const { accountId, cookies } = selection;
117
+ accountsTriedCount++;
57
118
  const csrf = (0, cookie_pool_1.extractCsrfToken)(cookies);
58
119
  const cookieHeader = (0, cookie_pool_1.buildCookieHeader)(cookies);
59
120
  // Choose header profile based on Sales vs Voyager context
@@ -89,6 +150,18 @@ async function executeLinkedInRequest(options, _operationName) {
89
150
  (0, request_history_1.recordRequest)({ operation: op, selector: options.url, status: 0, durationMs: Date.now() - started, accountId, errorMessage: String(e?.message || err) });
90
151
  }
91
152
  catch { }
153
+ // Report network errors to Sentry
154
+ try {
155
+ const { reportWarningToSentry } = await Promise.resolve().then(() => __importStar(require('./utils/sentry')));
156
+ reportWarningToSentry('Network error during LinkedIn request', {
157
+ code,
158
+ url: options.url,
159
+ accountId,
160
+ error: String(e?.message || err),
161
+ tags: { component: 'http-client', severity: 'high' },
162
+ });
163
+ }
164
+ catch { }
92
165
  lastError = new errors_1.LinkedInClientError('LinkedIn fetch failed', 'REQUEST_FAILED', 0, accountId);
93
166
  break; // rotate to next account
94
167
  }
@@ -136,6 +209,30 @@ async function executeLinkedInRequest(options, _operationName) {
136
209
  catch { }
137
210
  break; // break inner loop to rotate account
138
211
  }
212
+ // Phase 1.5: Proxy fallback on 502/503/504 if enabled
213
+ const isProxyError = status === 502 || status === 503 || status === 504;
214
+ const fallbackEnabled = config.fallbackWithoutProxyOnError ?? false;
215
+ if (isProxyError && fallbackEnabled && proxySet && attempt < perAccountAttempts - 1) {
216
+ (0, logger_1.log)('warn', 'http.proxyFallback', { accountId, status, attempt: attempt + 1 });
217
+ // Temporarily disable proxy for this retry
218
+ delete process.env.HTTP_PROXY;
219
+ delete process.env.HTTPS_PROXY;
220
+ proxySet = false;
221
+ // Report to Sentry
222
+ try {
223
+ const { reportWarningToSentry } = await Promise.resolve().then(() => __importStar(require('./utils/sentry')));
224
+ reportWarningToSentry('Proxy error, retrying without proxy', {
225
+ status,
226
+ url: options.url,
227
+ accountId,
228
+ tags: { component: 'http-client', severity: 'medium' },
229
+ });
230
+ }
231
+ catch { }
232
+ (0, metrics_1.incrementMetric)('httpRetries');
233
+ await sleep(delay);
234
+ continue;
235
+ }
139
236
  // Retryable 5xx on same account
140
237
  if ((status >= 500 && status < 600) && attempt < perAccountAttempts - 1) {
141
238
  (0, logger_1.log)('debug', 'http.retry', { accountId, status, attempt: attempt + 1, nextDelayMs: delay });
@@ -161,10 +258,23 @@ async function executeLinkedInRequest(options, _operationName) {
161
258
  throw err;
162
259
  }
163
260
  }
164
- // Exhausted rotations
261
+ // Exhausted all accounts
262
+ const errMsg = `All ${accountsTriedCount} LinkedIn accounts exhausted`;
263
+ (0, logger_1.log)('error', 'http.allAccountsExhausted', { accountsTriedCount, maxRotations });
264
+ // Report critical failure to Sentry
265
+ try {
266
+ const { reportCriticalError } = await Promise.resolve().then(() => __importStar(require('./utils/sentry')));
267
+ reportCriticalError(errMsg, {
268
+ accountsTriedCount,
269
+ maxRotations,
270
+ lastError: lastError?.message,
271
+ tags: { component: 'http-client', severity: 'critical' },
272
+ });
273
+ }
274
+ catch { }
165
275
  if (lastError)
166
276
  throw lastError;
167
- throw new errors_1.LinkedInClientError('All LinkedIn accounts exhausted', 'ALL_ACCOUNTS_FAILED', 503);
277
+ throw new errors_1.LinkedInClientError(errMsg, 'ALL_ACCOUNTS_FAILED', 503);
168
278
  }
169
279
  finally {
170
280
  // Restore proxy env
@@ -8,6 +8,7 @@ export declare function searchSalesLeads(keywords: string, options?: {
8
8
  decorationId?: string;
9
9
  filters?: SalesSearchFilters;
10
10
  rawQuery?: string;
11
+ sessionId?: string;
11
12
  }): Promise<SearchSalesResult | SalesLeadSearchResult[]>;
12
13
  export declare function getProfilesBatch(vanities: string[], concurrency?: number): Promise<(LinkedInProfile | null)[]>;
13
14
  export declare function resolveCompanyUniversalName(universalName: string): Promise<{
@@ -168,7 +168,8 @@ async function searchSalesLeads(keywords, options) {
168
168
  const count = Number.isFinite(options?.count) ? Number(options.count) : 25;
169
169
  const deco = options?.decorationId || 'com.linkedin.sales.deco.desktop.searchv2.LeadSearchResult-14';
170
170
  const sig = (0, search_encoder_1.buildFilterSignature)(options?.filters, options?.rawQuery);
171
- const cacheKey = JSON.stringify({ k: String(keywords || '').toLowerCase(), start, count, deco, sig });
171
+ // 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 });
172
173
  const cached = getCached(searchCache, cacheKey, cfg.searchCacheTtl);
173
174
  if (cached) {
174
175
  (0, metrics_1.incrementMetric)('searchCacheHits');
@@ -184,15 +185,16 @@ async function searchSalesLeads(keywords, options) {
184
185
  async function doRequest(decorationId) {
185
186
  const url = `${SALES_NAV_BASE}/salesApiLeadSearch?q=searchQuery&start=${start}&count=${count}&decorationId=${encodeURIComponent(decorationId)}&query=${queryStruct}`;
186
187
  try {
187
- (0, logger_1.log)('info', 'api.start', { operation: 'searchSalesLeads', selector: { keywords, start, count, deco: decorationId } });
188
+ (0, logger_1.log)('info', 'api.start', { operation: 'searchSalesLeads', selector: { keywords, start, count, deco: decorationId, sessionId: options?.sessionId } });
188
189
  }
189
190
  catch { }
190
191
  const out = await (0, http_client_1.executeLinkedInRequest)({
191
192
  url,
192
193
  headers: { Referer: 'https://www.linkedin.com/sales/search/people' },
194
+ sessionId: options?.sessionId, // Phase 2.1: Pass sessionId for account stickiness
193
195
  }, 'searchSalesLeads');
194
196
  try {
195
- (0, logger_1.log)('info', 'api.ok', { operation: 'searchSalesLeads', selector: { keywords, start, count, deco: decorationId } });
197
+ (0, logger_1.log)('info', 'api.ok', { operation: 'searchSalesLeads', selector: { keywords, start, count, deco: decorationId, sessionId: options?.sessionId } });
196
198
  }
197
199
  catch { }
198
200
  return out;
@@ -8,14 +8,29 @@ function parseFullProfile(rawResponse, vanity) {
8
8
  const identity = included.find((it) => String(it?.$type || '').includes('identity.profile.Profile')) || {};
9
9
  const profile = {
10
10
  vanity,
11
+ publicIdentifier: identity.publicIdentifier,
11
12
  firstName: identity.firstName,
12
13
  lastName: identity.lastName,
13
14
  headline: identity.headline,
14
15
  summary: identity.summary,
15
16
  locationText: identity.locationName || identity.geoLocationName,
17
+ objectUrn: identity.objectUrn,
18
+ premium: identity.premium,
19
+ influencer: identity.influencer,
20
+ industryUrn: identity.industryUrn,
21
+ versionTag: identity.versionTag,
16
22
  positions: [],
17
23
  educations: [],
18
24
  };
25
+ // Extract geoLocation and countryCode
26
+ const geoLocation = identity.geoLocation;
27
+ if (geoLocation?.geoUrn) {
28
+ profile.geoLocationUrn = geoLocation.geoUrn;
29
+ }
30
+ const location = identity.location;
31
+ if (location?.countryCode) {
32
+ profile.countryCode = location.countryCode;
33
+ }
19
34
  // Positions
20
35
  function extractCompanyIdFromUrn(urn) {
21
36
  if (!urn)
@@ -95,6 +110,21 @@ function parseFullProfile(rawResponse, vanity) {
95
110
  };
96
111
  profile.educations.push(edu);
97
112
  }
113
+ // Skills
114
+ const skills = [];
115
+ for (const item of included) {
116
+ const rec = item;
117
+ const urn = rec?.entityUrn || '';
118
+ if (!urn.includes('fsd_skill'))
119
+ continue;
120
+ const skill = {
121
+ name: rec?.name,
122
+ entityUrn: urn,
123
+ };
124
+ skills.push(skill);
125
+ }
126
+ if (skills.length > 0)
127
+ profile.skills = skills;
98
128
  // Avatar (via helper)
99
129
  const avatarItem = included.find((it) => it?.vectorImage && JSON.stringify(it).includes('vectorImage'));
100
130
  const vector = avatarItem?.vectorImage;
package/dist/types.d.ts CHANGED
@@ -35,8 +35,13 @@ export interface ProfileEducation {
35
35
  endYear?: number;
36
36
  endMonth?: number;
37
37
  }
38
+ export interface ProfileSkill {
39
+ name?: string;
40
+ entityUrn?: string;
41
+ }
38
42
  export interface LinkedInProfile {
39
43
  vanity: string;
44
+ publicIdentifier?: string;
40
45
  firstName?: string;
41
46
  lastName?: string;
42
47
  headline?: string;
@@ -45,8 +50,16 @@ export interface LinkedInProfile {
45
50
  avatarUrl?: string;
46
51
  coverUrl?: string;
47
52
  fsdProfileUrn?: string;
53
+ objectUrn?: string;
54
+ premium?: boolean;
55
+ influencer?: boolean;
56
+ industryUrn?: string;
57
+ geoLocationUrn?: string;
58
+ countryCode?: string;
59
+ versionTag?: string;
48
60
  positions: ProfilePosition[];
49
61
  educations: ProfileEducation[];
62
+ skills?: ProfileSkill[];
50
63
  }
51
64
  export interface LinkedInTenure {
52
65
  numYears?: number;
@@ -0,0 +1,8 @@
1
+ export interface SentryClient {
2
+ captureException(error: Error, context?: Record<string, unknown>): void;
3
+ captureMessage(message: string, level: 'error' | 'warning' | 'info', context?: Record<string, unknown>): void;
4
+ }
5
+ export declare function setSentryClient(client: SentryClient): void;
6
+ export declare function reportToSentry(error: Error | string, context?: Record<string, unknown>): void;
7
+ export declare function reportWarningToSentry(message: string, context?: Record<string, unknown>): void;
8
+ export declare function reportCriticalError(message: string, context?: Record<string, unknown>): void;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setSentryClient = setSentryClient;
4
+ exports.reportToSentry = reportToSentry;
5
+ exports.reportWarningToSentry = reportWarningToSentry;
6
+ exports.reportCriticalError = reportCriticalError;
7
+ const logger_1 = require("./logger");
8
+ let sentryClient = null;
9
+ function setSentryClient(client) {
10
+ sentryClient = client;
11
+ (0, logger_1.log)('info', 'sentry.configured', {});
12
+ }
13
+ function reportToSentry(error, context) {
14
+ if (!sentryClient)
15
+ return;
16
+ try {
17
+ if (typeof error === 'string') {
18
+ sentryClient.captureMessage(error, 'error', context);
19
+ }
20
+ else {
21
+ sentryClient.captureException(error, context);
22
+ }
23
+ }
24
+ catch (err) {
25
+ (0, logger_1.log)('error', 'sentry.reportFailed', { error: err.message });
26
+ }
27
+ }
28
+ function reportWarningToSentry(message, context) {
29
+ if (!sentryClient)
30
+ return;
31
+ try {
32
+ sentryClient.captureMessage(message, 'warning', context);
33
+ }
34
+ catch (err) {
35
+ (0, logger_1.log)('error', 'sentry.warningFailed', { error: err.message });
36
+ }
37
+ }
38
+ function reportCriticalError(message, context) {
39
+ if (!sentryClient)
40
+ return;
41
+ try {
42
+ sentryClient.captureMessage(message, 'error', context);
43
+ }
44
+ catch (err) {
45
+ (0, logger_1.log)('error', 'sentry.criticalFailed', { error: err.message });
46
+ }
47
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linkedin-secret-sauce",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
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",