linkedin-secret-sauce 0.3.13 → 0.3.15
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 +6 -1
- package/dist/config.js +66 -4
- package/dist/cookie-pool.d.ts +13 -1
- package/dist/cookie-pool.js +161 -2
- package/dist/cosiall-client.js +54 -0
- package/dist/http-client.d.ts +1 -0
- package/dist/http-client.js +116 -6
- package/dist/linkedin-api.d.ts +1 -0
- package/dist/linkedin-api.js +5 -3
- package/dist/parsers/profile-parser.js +11 -1
- package/dist/utils/sentry.d.ts +8 -0
- package/dist/utils/sentry.js +47 -0
- package/package.json +1 -1
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:
|
|
23
|
-
companyCacheTtl: 10 * 60 * 1000,
|
|
24
|
-
typeaheadCacheTtl: 60 * 60 * 1000,
|
|
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) {
|
package/dist/cookie-pool.d.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import type { LinkedInCookie } from './types';
|
|
2
|
-
export declare function
|
|
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
|
}>;
|
package/dist/cookie-pool.js
CHANGED
|
@@ -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
|
-
|
|
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);
|
package/dist/cosiall-client.js
CHANGED
|
@@ -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 });
|
package/dist/http-client.d.ts
CHANGED
package/dist/http-client.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
277
|
+
throw new errors_1.LinkedInClientError(errMsg, 'ALL_ACCOUNTS_FAILED', 503);
|
|
168
278
|
}
|
|
169
279
|
finally {
|
|
170
280
|
// Restore proxy env
|
package/dist/linkedin-api.d.ts
CHANGED
|
@@ -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<{
|
package/dist/linkedin-api.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
|
@@ -13,7 +13,7 @@ function parseFullProfile(rawResponse, vanity) {
|
|
|
13
13
|
lastName: identity.lastName,
|
|
14
14
|
headline: identity.headline,
|
|
15
15
|
summary: identity.summary,
|
|
16
|
-
locationText:
|
|
16
|
+
locationText: undefined, // Will be set below after geo lookup
|
|
17
17
|
objectUrn: identity.objectUrn,
|
|
18
18
|
premium: identity.premium,
|
|
19
19
|
influencer: identity.influencer,
|
|
@@ -26,6 +26,16 @@ function parseFullProfile(rawResponse, vanity) {
|
|
|
26
26
|
const geoLocation = identity.geoLocation;
|
|
27
27
|
if (geoLocation?.geoUrn) {
|
|
28
28
|
profile.geoLocationUrn = geoLocation.geoUrn;
|
|
29
|
+
// Lookup the geo object in included array to get the actual location name
|
|
30
|
+
const geoUrn = geoLocation.geoUrn;
|
|
31
|
+
const geoObj = included.find((it) => it?.entityUrn === geoUrn);
|
|
32
|
+
if (geoObj?.defaultLocalizedName) {
|
|
33
|
+
profile.locationText = geoObj.defaultLocalizedName;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Fallback to direct location fields if geo lookup didn't work
|
|
37
|
+
if (!profile.locationText) {
|
|
38
|
+
profile.locationText = identity.locationName || identity.geoLocationName;
|
|
29
39
|
}
|
|
30
40
|
const location = identity.location;
|
|
31
41
|
if (location?.countryCode) {
|
|
@@ -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
|
+
}
|