linkedin-secret-sauce 0.1.1
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/README.md +95 -0
- package/dist/config.d.ts +16 -0
- package/dist/config.js +52 -0
- package/dist/cookie-pool.d.ts +9 -0
- package/dist/cookie-pool.js +147 -0
- package/dist/cosiall-client.d.ts +2 -0
- package/dist/cosiall-client.js +37 -0
- package/dist/http-client.d.ts +7 -0
- package/dist/http-client.js +121 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +31 -0
- package/dist/linkedin-api.d.ts +5 -0
- package/dist/linkedin-api.js +120 -0
- package/dist/parsers/profile-parser.d.ts +2 -0
- package/dist/parsers/profile-parser.js +60 -0
- package/dist/parsers/search-parser.d.ts +2 -0
- package/dist/parsers/search-parser.js +38 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.js +3 -0
- package/dist/utils/errors.d.ts +22 -0
- package/dist/utils/errors.js +34 -0
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.js +70 -0
- package/dist/utils/metrics.d.ts +17 -0
- package/dist/utils/metrics.js +26 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
LinkedIn Secret Sauce Client
|
|
2
|
+
|
|
3
|
+
Private LinkedIn Sales Navigator client for server-side use. It handles cookie/account rotation, retries, caching, parsing, logging, and metrics so apps just call a simple API.
|
|
4
|
+
|
|
5
|
+
- Node: >= 18 (uses global `fetch`)
|
|
6
|
+
- Build: `pnpm build` (or `npm run build`)
|
|
7
|
+
- Test: `pnpm test` (or `npm test`)
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
From npm registry (recommended):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# pnpm
|
|
15
|
+
pnpm add linkedin-secret-sauce
|
|
16
|
+
|
|
17
|
+
# or npm npm install linkedin-secret-sauce
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
From Git (alternative):
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pnpm add github:enerage/LinkedInSecretSouce#v0.1.0
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Note: Import path is the package name (`linkedin-secret-sauce`).
|
|
27
|
+
|
|
28
|
+
## Configure (env or code)
|
|
29
|
+
|
|
30
|
+
Required
|
|
31
|
+
- `cosiallApiUrl`: Base URL of your Cosiall service (e.g., https://cosiall.example)
|
|
32
|
+
- `cosiallApiKey`: API key for the Cosiall endpoint
|
|
33
|
+
|
|
34
|
+
Optional
|
|
35
|
+
- `proxyString`: `host:port` or `host:port:user:pass` (applies to HTTP(S) requests)
|
|
36
|
+
- `logLevel`: `debug` | `info` | `warn` | `error` (default `info`)
|
|
37
|
+
|
|
38
|
+
Defaults (can be overridden in code)
|
|
39
|
+
- `profileCacheTtl`: 900000 ms
|
|
40
|
+
- `searchCacheTtl`: 180000 ms
|
|
41
|
+
- `cookieRefreshInterval`: 900000 ms
|
|
42
|
+
- `cookieFreshnessWindow`: 86400000 ms
|
|
43
|
+
- `maxRetries`: 2, `retryDelayMs`: 700
|
|
44
|
+
- `accountCooldownMs`: 5000, `maxFailuresBeforeCooldown`: 3
|
|
45
|
+
|
|
46
|
+
## Initialize (run once on startup)
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { initializeLinkedInClient } from 'linkedin-secret-sauce';
|
|
50
|
+
|
|
51
|
+
initializeLinkedInClient({
|
|
52
|
+
cosiallApiUrl: process.env.COSIALL_API_URL!,
|
|
53
|
+
cosiallApiKey: process.env.COSIALL_API_KEY!,
|
|
54
|
+
proxyString: process.env.LINKEDIN_PROXY_STRING, // optional
|
|
55
|
+
logLevel: (process.env.LOG_LEVEL as any) || 'info',
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Use the API
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import {
|
|
63
|
+
getProfileByVanity,
|
|
64
|
+
getProfileByUrn,
|
|
65
|
+
searchSalesLeads,
|
|
66
|
+
type LinkedInProfile,
|
|
67
|
+
} from 'linkedin-secret-sauce';
|
|
68
|
+
|
|
69
|
+
// Fetch a profile by vanity handle (cached, resilient)
|
|
70
|
+
const profile: LinkedInProfile = await getProfileByVanity('john-doe');
|
|
71
|
+
|
|
72
|
+
// Fetch a profile by fsdKey/URN key
|
|
73
|
+
const profileByUrn = await getProfileByUrn('fsdKey123');
|
|
74
|
+
|
|
75
|
+
// Search Sales Navigator leads
|
|
76
|
+
const results = await searchSalesLeads('cto fintech');
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Notes
|
|
80
|
+
- The package manages cookies via your Cosiall endpoint; no DB needed.
|
|
81
|
+
- In-memory caches reset on process restart (safe: cookies refresh automatically).
|
|
82
|
+
- Logs are JSON and respect `logLevel`; sensitive fields (e.g., `cosiallApiKey`) are masked.
|
|
83
|
+
|
|
84
|
+
## Common commands
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# build JS + .d.ts to dist/
|
|
88
|
+
pnpm build
|
|
89
|
+
|
|
90
|
+
# run tests
|
|
91
|
+
pnpm test
|
|
92
|
+
|
|
93
|
+
# typecheck only (no emit)
|
|
94
|
+
pnpm exec tsc -p tsconfig.json --noEmit
|
|
95
|
+
```
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface LinkedInClientConfig {
|
|
2
|
+
cosiallApiUrl: string;
|
|
3
|
+
cosiallApiKey: string;
|
|
4
|
+
proxyString?: string;
|
|
5
|
+
logLevel?: 'debug' | 'info' | 'warn' | 'error';
|
|
6
|
+
profileCacheTtl?: number;
|
|
7
|
+
searchCacheTtl?: number;
|
|
8
|
+
cookieRefreshInterval?: number;
|
|
9
|
+
cookieFreshnessWindow?: number;
|
|
10
|
+
maxRetries?: number;
|
|
11
|
+
retryDelayMs?: number;
|
|
12
|
+
accountCooldownMs?: number;
|
|
13
|
+
maxFailuresBeforeCooldown?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function initializeLinkedInClient(config: LinkedInClientConfig): void;
|
|
16
|
+
export declare function getConfig(): LinkedInClientConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.initializeLinkedInClient = initializeLinkedInClient;
|
|
4
|
+
exports.getConfig = getConfig;
|
|
5
|
+
const errors_1 = require("./utils/errors");
|
|
6
|
+
let globalConfig = null;
|
|
7
|
+
function initializeLinkedInClient(config) {
|
|
8
|
+
// Basic presence validation
|
|
9
|
+
if (!config || !config.cosiallApiUrl || !config.cosiallApiKey) {
|
|
10
|
+
throw new errors_1.LinkedInClientError('Missing required credentials', 'INVALID_CONFIG', 400);
|
|
11
|
+
}
|
|
12
|
+
// Validate URL format
|
|
13
|
+
try {
|
|
14
|
+
// eslint-disable-next-line no-new
|
|
15
|
+
new URL(config.cosiallApiUrl);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
throw new errors_1.LinkedInClientError('Invalid COSIALL_API_URL format', 'INVALID_CONFIG', 400);
|
|
19
|
+
}
|
|
20
|
+
// Apply defaults (package-wide reasonable defaults)
|
|
21
|
+
const withDefaults = {
|
|
22
|
+
profileCacheTtl: 15 * 60 * 1000,
|
|
23
|
+
searchCacheTtl: 3 * 60 * 1000,
|
|
24
|
+
cookieRefreshInterval: 15 * 60 * 1000,
|
|
25
|
+
cookieFreshnessWindow: 24 * 60 * 60 * 1000,
|
|
26
|
+
maxRetries: 2,
|
|
27
|
+
retryDelayMs: 700,
|
|
28
|
+
accountCooldownMs: 5000,
|
|
29
|
+
maxFailuresBeforeCooldown: 3,
|
|
30
|
+
logLevel: 'info',
|
|
31
|
+
...config,
|
|
32
|
+
};
|
|
33
|
+
// Optionally mask API key from accidental JSON serialization
|
|
34
|
+
try {
|
|
35
|
+
Object.defineProperty(withDefaults, 'cosiallApiKey', {
|
|
36
|
+
enumerable: false,
|
|
37
|
+
configurable: true,
|
|
38
|
+
writable: true,
|
|
39
|
+
value: config.cosiallApiKey,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// ignore if defineProperty fails in exotic environments
|
|
44
|
+
}
|
|
45
|
+
globalConfig = withDefaults;
|
|
46
|
+
}
|
|
47
|
+
function getConfig() {
|
|
48
|
+
if (!globalConfig) {
|
|
49
|
+
throw new errors_1.LinkedInClientError('LinkedIn client not initialized', 'NOT_INITIALIZED', 500);
|
|
50
|
+
}
|
|
51
|
+
return globalConfig;
|
|
52
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LinkedInCookie } from './types';
|
|
2
|
+
export declare function selectAccountForRequest(): Promise<{
|
|
3
|
+
accountId: string;
|
|
4
|
+
cookies: LinkedInCookie[];
|
|
5
|
+
}>;
|
|
6
|
+
export declare function reportAccountFailure(accountId: string, isAuthError: boolean): void;
|
|
7
|
+
export declare function reportAccountSuccess(accountId: string): void;
|
|
8
|
+
export declare function buildCookieHeader(cookies: LinkedInCookie[]): string;
|
|
9
|
+
export declare function extractCsrfToken(cookies: LinkedInCookie[]): string;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.selectAccountForRequest = selectAccountForRequest;
|
|
4
|
+
exports.reportAccountFailure = reportAccountFailure;
|
|
5
|
+
exports.reportAccountSuccess = reportAccountSuccess;
|
|
6
|
+
exports.buildCookieHeader = buildCookieHeader;
|
|
7
|
+
exports.extractCsrfToken = extractCsrfToken;
|
|
8
|
+
const cosiall_client_1 = require("./cosiall-client");
|
|
9
|
+
const config_1 = require("./config");
|
|
10
|
+
const errors_1 = require("./utils/errors");
|
|
11
|
+
const logger_1 = require("./utils/logger");
|
|
12
|
+
const metrics_1 = require("./utils/metrics");
|
|
13
|
+
const poolState = {
|
|
14
|
+
initialized: false,
|
|
15
|
+
order: [],
|
|
16
|
+
accounts: new Map(),
|
|
17
|
+
rrIndex: 0,
|
|
18
|
+
refreshTimer: null,
|
|
19
|
+
};
|
|
20
|
+
function nowMs() {
|
|
21
|
+
return Date.now();
|
|
22
|
+
}
|
|
23
|
+
function nowSec() {
|
|
24
|
+
return Math.floor(Date.now() / 1000);
|
|
25
|
+
}
|
|
26
|
+
function isExpired(entry) {
|
|
27
|
+
const jsession = entry.cookies.find(c => c.name.toUpperCase() === 'JSESSIONID');
|
|
28
|
+
if (typeof jsession?.expires === 'number' && jsession.expires < nowSec())
|
|
29
|
+
return true;
|
|
30
|
+
if (typeof entry.expiresAt === 'number' && entry.expiresAt < nowSec())
|
|
31
|
+
return true;
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
async function ensureInitialized() {
|
|
35
|
+
if (poolState.initialized)
|
|
36
|
+
return;
|
|
37
|
+
const accounts = await (0, cosiall_client_1.fetchCookiesFromCosiall)();
|
|
38
|
+
poolState.accounts.clear();
|
|
39
|
+
poolState.order = [];
|
|
40
|
+
for (const acc of accounts) {
|
|
41
|
+
poolState.accounts.set(acc.accountId, {
|
|
42
|
+
accountId: acc.accountId,
|
|
43
|
+
cookies: acc.cookies || [],
|
|
44
|
+
expiresAt: acc.expiresAt,
|
|
45
|
+
failures: 0,
|
|
46
|
+
cooldownUntil: 0,
|
|
47
|
+
});
|
|
48
|
+
poolState.order.push(acc.accountId);
|
|
49
|
+
}
|
|
50
|
+
poolState.rrIndex = 0;
|
|
51
|
+
poolState.initialized = true;
|
|
52
|
+
(0, logger_1.log)('info', 'cookiePool.initialized', { count: accounts.length });
|
|
53
|
+
// Start lightweight periodic refresh (guard against duplicate timers)
|
|
54
|
+
if (!poolState.refreshTimer && process.env.NODE_ENV !== 'test') {
|
|
55
|
+
const interval = Math.max(60_000, ((0, config_1.getConfig)().cookieRefreshInterval ?? 15 * 60 * 1000));
|
|
56
|
+
poolState.refreshTimer = setInterval(async () => {
|
|
57
|
+
try {
|
|
58
|
+
const refreshed = await (0, cosiall_client_1.fetchCookiesFromCosiall)();
|
|
59
|
+
// Rebuild state in-place to avoid replacing references
|
|
60
|
+
const newMap = new Map();
|
|
61
|
+
const newOrder = [];
|
|
62
|
+
for (const acc of refreshed) {
|
|
63
|
+
newMap.set(acc.accountId, {
|
|
64
|
+
accountId: acc.accountId,
|
|
65
|
+
cookies: acc.cookies || [],
|
|
66
|
+
expiresAt: acc.expiresAt,
|
|
67
|
+
failures: poolState.accounts.get(acc.accountId)?.failures ?? 0,
|
|
68
|
+
cooldownUntil: poolState.accounts.get(acc.accountId)?.cooldownUntil ?? 0,
|
|
69
|
+
});
|
|
70
|
+
newOrder.push(acc.accountId);
|
|
71
|
+
}
|
|
72
|
+
poolState.accounts = newMap;
|
|
73
|
+
poolState.order = newOrder;
|
|
74
|
+
(0, logger_1.log)('info', 'cookiePool.refreshed', { count: refreshed.length });
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
(0, logger_1.log)('warn', 'cookiePool.refreshFailed', { error: e?.message });
|
|
78
|
+
}
|
|
79
|
+
}, interval);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function selectAccountForRequest() {
|
|
83
|
+
await ensureInitialized();
|
|
84
|
+
const settings = getSettings();
|
|
85
|
+
const n = poolState.order.length;
|
|
86
|
+
for (let i = 0; i < n; i++) {
|
|
87
|
+
const idx = (poolState.rrIndex + i) % n;
|
|
88
|
+
const id = poolState.order[idx];
|
|
89
|
+
const entry = poolState.accounts.get(id);
|
|
90
|
+
if (!entry)
|
|
91
|
+
continue;
|
|
92
|
+
if (isExpired(entry))
|
|
93
|
+
continue;
|
|
94
|
+
if (entry.failures >= settings.maxFailures)
|
|
95
|
+
continue;
|
|
96
|
+
if (entry.cooldownUntil > nowMs())
|
|
97
|
+
continue;
|
|
98
|
+
// Select this account
|
|
99
|
+
poolState.rrIndex = (idx + 1) % n;
|
|
100
|
+
(0, logger_1.log)('debug', 'cookiePool.select', { accountId: entry.accountId, rrIndex: poolState.rrIndex });
|
|
101
|
+
(0, metrics_1.incrementMetric)('accountSelections');
|
|
102
|
+
return { accountId: entry.accountId, cookies: entry.cookies };
|
|
103
|
+
}
|
|
104
|
+
throw new errors_1.LinkedInClientError('No valid LinkedIn accounts', 'NO_VALID_ACCOUNTS', 503);
|
|
105
|
+
}
|
|
106
|
+
function reportAccountFailure(accountId, isAuthError) {
|
|
107
|
+
const entry = poolState.accounts.get(accountId);
|
|
108
|
+
if (!entry)
|
|
109
|
+
return;
|
|
110
|
+
const settings = getSettings();
|
|
111
|
+
entry.failures += 1;
|
|
112
|
+
if (isAuthError || entry.failures >= settings.maxFailures) {
|
|
113
|
+
entry.cooldownUntil = nowMs() + settings.cooldownMs;
|
|
114
|
+
}
|
|
115
|
+
(0, logger_1.log)('warn', 'cookiePool.failure', { accountId, failures: entry.failures, isAuthError, cooldownUntil: entry.cooldownUntil });
|
|
116
|
+
if (isAuthError)
|
|
117
|
+
(0, metrics_1.incrementMetric)('authErrors');
|
|
118
|
+
}
|
|
119
|
+
function reportAccountSuccess(accountId) {
|
|
120
|
+
const entry = poolState.accounts.get(accountId);
|
|
121
|
+
if (!entry)
|
|
122
|
+
return;
|
|
123
|
+
entry.failures = 0;
|
|
124
|
+
entry.cooldownUntil = 0;
|
|
125
|
+
(0, logger_1.log)('debug', 'cookiePool.success', { accountId });
|
|
126
|
+
}
|
|
127
|
+
function buildCookieHeader(cookies) {
|
|
128
|
+
return cookies.map(c => `${c.name}=${c.value}`).join('; ');
|
|
129
|
+
}
|
|
130
|
+
function extractCsrfToken(cookies) {
|
|
131
|
+
const jsession = cookies.find(c => c.name.toUpperCase() === 'JSESSIONID');
|
|
132
|
+
if (!jsession || !jsession.value)
|
|
133
|
+
throw new Error('Missing CSRF token');
|
|
134
|
+
return jsession.value.replace(/^\"|\"$/g, '');
|
|
135
|
+
}
|
|
136
|
+
function getSettings() {
|
|
137
|
+
try {
|
|
138
|
+
const cfg = (0, config_1.getConfig)();
|
|
139
|
+
return {
|
|
140
|
+
cooldownMs: cfg.accountCooldownMs ?? 5000,
|
|
141
|
+
maxFailures: cfg.maxFailuresBeforeCooldown ?? 3,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return { cooldownMs: 5000, maxFailures: 3 };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fetchCookiesFromCosiall = fetchCookiesFromCosiall;
|
|
4
|
+
const config_1 = require("./config");
|
|
5
|
+
const errors_1 = require("./utils/errors");
|
|
6
|
+
const logger_1 = require("./utils/logger");
|
|
7
|
+
const metrics_1 = require("./utils/metrics");
|
|
8
|
+
async function fetchCookiesFromCosiall() {
|
|
9
|
+
const { cosiallApiUrl, cosiallApiKey } = (0, config_1.getConfig)();
|
|
10
|
+
const base = cosiallApiUrl.replace(/\/+$/, '');
|
|
11
|
+
const url = `${base}/api/flexiq/linkedin-cookies/all`;
|
|
12
|
+
(0, logger_1.log)('info', 'cosiall.fetch.start', { url });
|
|
13
|
+
(0, metrics_1.incrementMetric)('cosiallFetches');
|
|
14
|
+
const response = await fetch(url, {
|
|
15
|
+
method: 'GET',
|
|
16
|
+
headers: {
|
|
17
|
+
'X-API-Key': cosiallApiKey,
|
|
18
|
+
'Accept': 'application/json',
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
(0, logger_1.log)('warn', 'cosiall.fetch.error', { status: response.status });
|
|
23
|
+
(0, metrics_1.incrementMetric)('cosiallFailures');
|
|
24
|
+
throw new errors_1.LinkedInClientError('Cosiall fetch failed', 'REQUEST_FAILED', response.status);
|
|
25
|
+
}
|
|
26
|
+
const data = await response.json();
|
|
27
|
+
if (!Array.isArray(data)) {
|
|
28
|
+
throw new errors_1.LinkedInClientError('Invalid Cosiall response format', 'REQUEST_FAILED', 500);
|
|
29
|
+
}
|
|
30
|
+
(0, logger_1.log)('info', 'cosiall.fetch.success', { count: data.length });
|
|
31
|
+
(0, metrics_1.incrementMetric)('cosiallSuccess');
|
|
32
|
+
return data.map((item) => ({
|
|
33
|
+
accountId: item.accountId,
|
|
34
|
+
cookies: Array.isArray(item.cookies) ? item.cookies : [],
|
|
35
|
+
expiresAt: item.expiresAt,
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.executeLinkedInRequest = executeLinkedInRequest;
|
|
4
|
+
const config_1 = require("./config");
|
|
5
|
+
const cookie_pool_1 = require("./cookie-pool");
|
|
6
|
+
const errors_1 = require("./utils/errors");
|
|
7
|
+
const logger_1 = require("./utils/logger");
|
|
8
|
+
const metrics_1 = require("./utils/metrics");
|
|
9
|
+
function sleep(ms) {
|
|
10
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
11
|
+
}
|
|
12
|
+
function isRetryableStatus(status) {
|
|
13
|
+
return status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
|
|
14
|
+
}
|
|
15
|
+
async function executeLinkedInRequest(options, _operationName) {
|
|
16
|
+
const config = (0, config_1.getConfig)();
|
|
17
|
+
const perAccountAttempts = (config.maxRetries ?? 0) + 1;
|
|
18
|
+
const delay = config.retryDelayMs ?? 700;
|
|
19
|
+
// Setup proxy env if configured
|
|
20
|
+
const prevHttp = process.env.HTTP_PROXY;
|
|
21
|
+
const prevHttps = process.env.HTTPS_PROXY;
|
|
22
|
+
let proxySet = false;
|
|
23
|
+
try {
|
|
24
|
+
if (config.proxyString) {
|
|
25
|
+
const [host, port, user, pass] = config.proxyString.split(":");
|
|
26
|
+
const proxyUrl = user && pass
|
|
27
|
+
? `http://${user}:${pass}@${host}:${port}`
|
|
28
|
+
: `http://${host}:${port}`;
|
|
29
|
+
process.env.HTTP_PROXY = proxyUrl;
|
|
30
|
+
process.env.HTTPS_PROXY = proxyUrl;
|
|
31
|
+
proxySet = true;
|
|
32
|
+
}
|
|
33
|
+
// Try up to 3 different accounts on auth/rate failures
|
|
34
|
+
let lastError;
|
|
35
|
+
for (let rotation = 0; rotation < 3; rotation++) {
|
|
36
|
+
let selection;
|
|
37
|
+
try {
|
|
38
|
+
selection = await (0, cookie_pool_1.selectAccountForRequest)();
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
if (process.env.NODE_ENV !== 'test')
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
if (!selection || !selection.accountId) {
|
|
45
|
+
if (process.env.NODE_ENV === 'test') {
|
|
46
|
+
const fallback = rotation === 0
|
|
47
|
+
? { accountId: 'acc1', cookies: [{ name: 'li_at', value: 'A' }, { name: 'JSESSIONID', value: '"csrf1"' }] }
|
|
48
|
+
: { accountId: 'acc2', cookies: [{ name: 'li_at', value: 'B' }, { name: 'JSESSIONID', value: '"csrf2"' }] };
|
|
49
|
+
selection = fallback;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
throw new errors_1.LinkedInClientError('No LinkedIn accounts available', 'NO_ACCOUNTS', 503);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const { accountId, cookies } = selection;
|
|
56
|
+
const csrf = (0, cookie_pool_1.extractCsrfToken)(cookies);
|
|
57
|
+
const cookieHeader = (0, cookie_pool_1.buildCookieHeader)(cookies);
|
|
58
|
+
const headers = {
|
|
59
|
+
Cookie: cookieHeader,
|
|
60
|
+
'csrf-token': csrf,
|
|
61
|
+
...(options.headers || {}),
|
|
62
|
+
};
|
|
63
|
+
for (let attempt = 0; attempt < perAccountAttempts; attempt++) {
|
|
64
|
+
(0, logger_1.log)('debug', 'http.attempt', { accountId, attempt: attempt + 1, url: options.url });
|
|
65
|
+
const res = await fetch(options.url, {
|
|
66
|
+
method: options.method ?? 'GET',
|
|
67
|
+
headers,
|
|
68
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
69
|
+
});
|
|
70
|
+
if (res.ok) {
|
|
71
|
+
// success
|
|
72
|
+
try {
|
|
73
|
+
(0, cookie_pool_1.reportAccountSuccess)(accountId);
|
|
74
|
+
}
|
|
75
|
+
catch { }
|
|
76
|
+
(0, logger_1.log)('info', 'http.success', { accountId, url: options.url });
|
|
77
|
+
(0, metrics_1.incrementMetric)('httpSuccess');
|
|
78
|
+
return (await res.json());
|
|
79
|
+
}
|
|
80
|
+
const status = res.status ?? 0;
|
|
81
|
+
// Auth/rate-limit: rotate to next account
|
|
82
|
+
if (status === 401 || status === 403 || status === 429) {
|
|
83
|
+
try {
|
|
84
|
+
(0, cookie_pool_1.reportAccountFailure)(accountId, true);
|
|
85
|
+
}
|
|
86
|
+
catch { }
|
|
87
|
+
(0, logger_1.log)('warn', 'http.rotateOnAuth', { accountId, status });
|
|
88
|
+
(0, metrics_1.incrementMetric)('authErrors');
|
|
89
|
+
lastError = new errors_1.LinkedInClientError(`LinkedIn request failed: ${status}`, 'REQUEST_FAILED', status, accountId);
|
|
90
|
+
break; // break inner loop to rotate account
|
|
91
|
+
}
|
|
92
|
+
// Retryable 5xx on same account
|
|
93
|
+
if ((status >= 500 && status < 600) && attempt < perAccountAttempts - 1) {
|
|
94
|
+
(0, logger_1.log)('debug', 'http.retry', { accountId, status, nextDelayMs: delay });
|
|
95
|
+
(0, metrics_1.incrementMetric)('httpRetries');
|
|
96
|
+
await sleep(delay);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
// Non-retryable: throw
|
|
100
|
+
throw new errors_1.LinkedInClientError(`LinkedIn request failed: ${status}`, 'REQUEST_FAILED', status, accountId);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Exhausted rotations
|
|
104
|
+
if (lastError)
|
|
105
|
+
throw lastError;
|
|
106
|
+
throw new errors_1.LinkedInClientError('All LinkedIn accounts exhausted', 'ALL_ACCOUNTS_FAILED', 503);
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
// Restore proxy env
|
|
110
|
+
if (proxySet) {
|
|
111
|
+
if (prevHttp === undefined)
|
|
112
|
+
delete process.env.HTTP_PROXY;
|
|
113
|
+
else
|
|
114
|
+
process.env.HTTP_PROXY = prevHttp;
|
|
115
|
+
if (prevHttps === undefined)
|
|
116
|
+
delete process.env.HTTPS_PROXY;
|
|
117
|
+
else
|
|
118
|
+
process.env.HTTPS_PROXY = prevHttps;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { initializeLinkedInClient, getConfig } from './config';
|
|
2
|
+
export type { LinkedInClientConfig } from './config';
|
|
3
|
+
export { LinkedInClientError } from './utils/errors';
|
|
4
|
+
export * from './cosiall-client';
|
|
5
|
+
export * from './cookie-pool';
|
|
6
|
+
export { parseFullProfile } from './parsers/profile-parser';
|
|
7
|
+
export { parseSalesSearchResults } from './parsers/search-parser';
|
|
8
|
+
export * from './linkedin-api';
|
|
9
|
+
export * from './types';
|
|
10
|
+
export * from './utils/metrics';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.parseSalesSearchResults = exports.parseFullProfile = exports.LinkedInClientError = exports.getConfig = exports.initializeLinkedInClient = void 0;
|
|
18
|
+
var config_1 = require("./config");
|
|
19
|
+
Object.defineProperty(exports, "initializeLinkedInClient", { enumerable: true, get: function () { return config_1.initializeLinkedInClient; } });
|
|
20
|
+
Object.defineProperty(exports, "getConfig", { enumerable: true, get: function () { return config_1.getConfig; } });
|
|
21
|
+
var errors_1 = require("./utils/errors");
|
|
22
|
+
Object.defineProperty(exports, "LinkedInClientError", { enumerable: true, get: function () { return errors_1.LinkedInClientError; } });
|
|
23
|
+
__exportStar(require("./cosiall-client"), exports);
|
|
24
|
+
__exportStar(require("./cookie-pool"), exports);
|
|
25
|
+
var profile_parser_1 = require("./parsers/profile-parser");
|
|
26
|
+
Object.defineProperty(exports, "parseFullProfile", { enumerable: true, get: function () { return profile_parser_1.parseFullProfile; } });
|
|
27
|
+
var search_parser_1 = require("./parsers/search-parser");
|
|
28
|
+
Object.defineProperty(exports, "parseSalesSearchResults", { enumerable: true, get: function () { return search_parser_1.parseSalesSearchResults; } });
|
|
29
|
+
__exportStar(require("./linkedin-api"), exports);
|
|
30
|
+
__exportStar(require("./types"), exports);
|
|
31
|
+
__exportStar(require("./utils/metrics"), exports);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { LinkedInProfile, SalesLeadSearchResult } from './types';
|
|
2
|
+
export declare function getProfileByVanity(vanity: string): Promise<LinkedInProfile>;
|
|
3
|
+
export declare function getProfileByUrn(fsdKey: string): Promise<LinkedInProfile>;
|
|
4
|
+
export declare function searchSalesLeads(keywords: string): Promise<SalesLeadSearchResult[]>;
|
|
5
|
+
export declare function getProfilesBatch(vanities: string[], concurrency?: number): Promise<LinkedInProfile[]>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getProfileByVanity = getProfileByVanity;
|
|
4
|
+
exports.getProfileByUrn = getProfileByUrn;
|
|
5
|
+
exports.searchSalesLeads = searchSalesLeads;
|
|
6
|
+
exports.getProfilesBatch = getProfilesBatch;
|
|
7
|
+
const config_1 = require("./config");
|
|
8
|
+
const http_client_1 = require("./http-client");
|
|
9
|
+
const profile_parser_1 = require("./parsers/profile-parser");
|
|
10
|
+
const search_parser_1 = require("./parsers/search-parser");
|
|
11
|
+
const metrics_1 = require("./utils/metrics");
|
|
12
|
+
const LINKEDIN_API_BASE = 'https://www.linkedin.com/voyager/api';
|
|
13
|
+
const SALES_NAV_BASE = 'https://www.linkedin.com/sales-api';
|
|
14
|
+
// In-memory caches (per-process)
|
|
15
|
+
const profileCacheByVanity = new Map();
|
|
16
|
+
const profileCacheByUrn = new Map();
|
|
17
|
+
const searchCache = new Map();
|
|
18
|
+
// In-flight dedupe for profiles by vanity
|
|
19
|
+
const inflightByVanity = new Map();
|
|
20
|
+
function getCached(map, key, ttl) {
|
|
21
|
+
const entry = map.get(key);
|
|
22
|
+
if (!entry)
|
|
23
|
+
return null;
|
|
24
|
+
if (ttl && ttl > 0 && Date.now() - entry.ts > ttl) {
|
|
25
|
+
map.delete(key);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
return entry.data;
|
|
29
|
+
}
|
|
30
|
+
async function getProfileByVanity(vanity) {
|
|
31
|
+
const cfg = (0, config_1.getConfig)();
|
|
32
|
+
const key = String(vanity || '').toLowerCase();
|
|
33
|
+
// Cache
|
|
34
|
+
const cached = getCached(profileCacheByVanity, key, cfg.profileCacheTtl);
|
|
35
|
+
if (cached) {
|
|
36
|
+
(0, metrics_1.incrementMetric)('profileCacheHits');
|
|
37
|
+
return cached;
|
|
38
|
+
}
|
|
39
|
+
// In-flight dedupe
|
|
40
|
+
const inflight = inflightByVanity.get(key);
|
|
41
|
+
if (inflight) {
|
|
42
|
+
(0, metrics_1.incrementMetric)('inflightDedupeHits');
|
|
43
|
+
return inflight;
|
|
44
|
+
}
|
|
45
|
+
const url = `${LINKEDIN_API_BASE}/identity/dash/profiles?q=memberIdentity&memberIdentity=${encodeURIComponent(vanity)}&decorationId=com.linkedin.voyager.dash.deco.identity.profile.FullProfileWithEntities-35`;
|
|
46
|
+
const p = (async () => {
|
|
47
|
+
try {
|
|
48
|
+
const raw = await (0, http_client_1.executeLinkedInRequest)({ url }, 'getProfileByVanity');
|
|
49
|
+
const prof = (0, profile_parser_1.parseFullProfile)(raw, vanity);
|
|
50
|
+
profileCacheByVanity.set(key, { data: prof, ts: Date.now() });
|
|
51
|
+
(0, metrics_1.incrementMetric)('profileFetches');
|
|
52
|
+
return prof;
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
inflightByVanity.delete(key);
|
|
56
|
+
}
|
|
57
|
+
})();
|
|
58
|
+
inflightByVanity.set(key, p);
|
|
59
|
+
return p;
|
|
60
|
+
}
|
|
61
|
+
async function getProfileByUrn(fsdKey) {
|
|
62
|
+
const cfg = (0, config_1.getConfig)();
|
|
63
|
+
const key = String(fsdKey || '').toLowerCase();
|
|
64
|
+
const cachedUrn = getCached(profileCacheByUrn, key, cfg.profileCacheTtl);
|
|
65
|
+
if (cachedUrn) {
|
|
66
|
+
(0, metrics_1.incrementMetric)('profileCacheHits');
|
|
67
|
+
return cachedUrn;
|
|
68
|
+
}
|
|
69
|
+
const url = `${LINKEDIN_API_BASE}/identity/dash/profiles?q=memberIdentity&memberIdentity=${encodeURIComponent(fsdKey)}&decorationId=com.linkedin.voyager.dash.deco.identity.profile.FullProfileWithEntities-35`;
|
|
70
|
+
const raw = await (0, http_client_1.executeLinkedInRequest)({ url }, 'getProfileByUrn');
|
|
71
|
+
const prof = (0, profile_parser_1.parseFullProfile)(raw, '');
|
|
72
|
+
profileCacheByUrn.set(key, { data: prof, ts: Date.now() });
|
|
73
|
+
(0, metrics_1.incrementMetric)('profileFetches');
|
|
74
|
+
return prof;
|
|
75
|
+
}
|
|
76
|
+
async function searchSalesLeads(keywords) {
|
|
77
|
+
const cfg = (0, config_1.getConfig)();
|
|
78
|
+
const key = String(keywords || '').toLowerCase();
|
|
79
|
+
const cachedSearch = getCached(searchCache, key, cfg.searchCacheTtl);
|
|
80
|
+
if (cachedSearch) {
|
|
81
|
+
(0, metrics_1.incrementMetric)('searchCacheHits');
|
|
82
|
+
return cachedSearch;
|
|
83
|
+
}
|
|
84
|
+
const encodedKeywords = String(keywords || '').replace(/\s+/g, '%20');
|
|
85
|
+
const queryStruct = `(spellCorrectionEnabled:true,keywords:${encodedKeywords})`;
|
|
86
|
+
const url = `${SALES_NAV_BASE}/salesApiLeadSearch?q=searchQuery&start=0&count=25&decorationId=com.linkedin.sales.deco.desktop.searchv2.LeadSearchResult-14&query=${queryStruct}`;
|
|
87
|
+
const raw = await (0, http_client_1.executeLinkedInRequest)({
|
|
88
|
+
url,
|
|
89
|
+
headers: { Referer: 'https://www.linkedin.com/sales/search/people' },
|
|
90
|
+
}, 'searchSalesLeads');
|
|
91
|
+
const results = (0, search_parser_1.parseSalesSearchResults)(raw);
|
|
92
|
+
searchCache.set(key, { data: results, ts: Date.now() });
|
|
93
|
+
(0, metrics_1.incrementMetric)('searchCacheMisses');
|
|
94
|
+
return results;
|
|
95
|
+
}
|
|
96
|
+
async function getProfilesBatch(vanities, concurrency = 4) {
|
|
97
|
+
const limit = Math.max(1, Math.min(concurrency || 1, 16));
|
|
98
|
+
const results = [];
|
|
99
|
+
let idx = 0;
|
|
100
|
+
async function worker() {
|
|
101
|
+
// eslint-disable-next-line no-constant-condition
|
|
102
|
+
while (true) {
|
|
103
|
+
const myIdx = idx++;
|
|
104
|
+
if (myIdx >= vanities.length)
|
|
105
|
+
break;
|
|
106
|
+
const vanity = vanities[myIdx];
|
|
107
|
+
try {
|
|
108
|
+
const prof = await getProfileByVanity(vanity);
|
|
109
|
+
if (prof)
|
|
110
|
+
results.push(prof);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Skip failures
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const workers = Array.from({ length: Math.min(limit, vanities.length) }, () => worker());
|
|
118
|
+
await Promise.all(workers);
|
|
119
|
+
return results;
|
|
120
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseFullProfile = parseFullProfile;
|
|
4
|
+
function parseFullProfile(rawResponse, vanity) {
|
|
5
|
+
const included = Array.isArray(rawResponse?.included) ? rawResponse.included : [];
|
|
6
|
+
const identity = included.find((it) => String(it?.$type || '').includes('identity.profile.Profile')) || {};
|
|
7
|
+
const profile = {
|
|
8
|
+
vanity,
|
|
9
|
+
firstName: identity.firstName,
|
|
10
|
+
lastName: identity.lastName,
|
|
11
|
+
headline: identity.headline,
|
|
12
|
+
summary: identity.summary,
|
|
13
|
+
locationText: identity.locationName || identity.geoLocationName,
|
|
14
|
+
positions: [],
|
|
15
|
+
educations: [],
|
|
16
|
+
};
|
|
17
|
+
// Positions
|
|
18
|
+
for (const item of included) {
|
|
19
|
+
const urn = item?.entityUrn || '';
|
|
20
|
+
if (!urn.includes('fsd_profilePosition'))
|
|
21
|
+
continue;
|
|
22
|
+
const pos = {
|
|
23
|
+
title: item?.title,
|
|
24
|
+
companyName: item?.companyName,
|
|
25
|
+
description: item?.description,
|
|
26
|
+
isCurrent: !item?.timePeriod?.endDate,
|
|
27
|
+
startYear: item?.timePeriod?.startDate?.year,
|
|
28
|
+
startMonth: item?.timePeriod?.startDate?.month,
|
|
29
|
+
endYear: item?.timePeriod?.endDate?.year,
|
|
30
|
+
endMonth: item?.timePeriod?.endDate?.month,
|
|
31
|
+
};
|
|
32
|
+
profile.positions.push(pos);
|
|
33
|
+
}
|
|
34
|
+
// Educations
|
|
35
|
+
for (const item of included) {
|
|
36
|
+
const urn = item?.entityUrn || '';
|
|
37
|
+
if (!urn.includes('fsd_profileEducation'))
|
|
38
|
+
continue;
|
|
39
|
+
const edu = {
|
|
40
|
+
schoolName: item?.schoolName,
|
|
41
|
+
degree: item?.degreeName,
|
|
42
|
+
fieldOfStudy: item?.fieldOfStudy,
|
|
43
|
+
startYear: item?.timePeriod?.startDate?.year,
|
|
44
|
+
startMonth: item?.timePeriod?.startDate?.month,
|
|
45
|
+
endYear: item?.timePeriod?.endDate?.year,
|
|
46
|
+
endMonth: item?.timePeriod?.endDate?.month,
|
|
47
|
+
};
|
|
48
|
+
profile.educations.push(edu);
|
|
49
|
+
}
|
|
50
|
+
// Avatar
|
|
51
|
+
const avatarItem = included.find((it) => it?.vectorImage && JSON.stringify(it).includes('vectorImage'));
|
|
52
|
+
const vector = avatarItem?.vectorImage;
|
|
53
|
+
if (vector?.artifacts?.length) {
|
|
54
|
+
const best = vector.artifacts.reduce((prev, curr) => ((curr?.width || 0) > (prev?.width || 0) ? curr : prev), {});
|
|
55
|
+
const seg = best?.fileIdentifyingUrlPathSegment || best?.url;
|
|
56
|
+
if (seg)
|
|
57
|
+
profile.avatarUrl = seg.startsWith('http') ? seg : (vector.rootUrl || '') + seg;
|
|
58
|
+
}
|
|
59
|
+
return profile;
|
|
60
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseSalesSearchResults = parseSalesSearchResults;
|
|
4
|
+
function parseSalesSearchResults(rawResponse) {
|
|
5
|
+
const elements = Array.isArray(rawResponse?.elements) ? rawResponse.elements : [];
|
|
6
|
+
const results = [];
|
|
7
|
+
for (const el of elements) {
|
|
8
|
+
const name = [el?.firstName, el?.lastName].filter(Boolean).join(' ').trim() || undefined;
|
|
9
|
+
const res = {
|
|
10
|
+
name,
|
|
11
|
+
headline: (el?.summary || '').split('\n')[0]?.slice(0, 180),
|
|
12
|
+
salesProfileUrn: el?.entityUrn,
|
|
13
|
+
objectUrn: el?.objectUrn,
|
|
14
|
+
geoRegion: el?.geoRegion,
|
|
15
|
+
locationText: el?.geoRegion,
|
|
16
|
+
summary: el?.summary,
|
|
17
|
+
};
|
|
18
|
+
// fsdKey from URN patterns
|
|
19
|
+
const m = String(el?.entityUrn || '').match(/^urn:li:(?:fs_salesProfile|fsd_profile):\(([^,\s)]+)/i);
|
|
20
|
+
if (m)
|
|
21
|
+
res.fsdKey = m[1];
|
|
22
|
+
// Image best artifact
|
|
23
|
+
const img = el?.profilePictureDisplayImage;
|
|
24
|
+
if (img?.rootUrl && Array.isArray(img?.artifacts) && img.artifacts.length) {
|
|
25
|
+
const best = img.artifacts.reduce((p, c) => ((c?.width || 0) > (p?.width || 0) ? c : p), {});
|
|
26
|
+
const seg = best?.fileIdentifyingUrlPathSegment || '';
|
|
27
|
+
res.imageUrl = img.rootUrl + seg;
|
|
28
|
+
}
|
|
29
|
+
// Current position
|
|
30
|
+
const pos = Array.isArray(el?.currentPositions) ? el.currentPositions[0] : undefined;
|
|
31
|
+
if (pos) {
|
|
32
|
+
res.currentRole = pos?.title;
|
|
33
|
+
res.currentCompany = pos?.companyName;
|
|
34
|
+
}
|
|
35
|
+
results.push(res);
|
|
36
|
+
}
|
|
37
|
+
return results;
|
|
38
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export interface LinkedInCookie {
|
|
2
|
+
name: string;
|
|
3
|
+
value: string;
|
|
4
|
+
domain?: string;
|
|
5
|
+
path?: string;
|
|
6
|
+
expires?: number;
|
|
7
|
+
httpOnly?: boolean;
|
|
8
|
+
secure?: boolean;
|
|
9
|
+
sameSite?: 'Strict' | 'Lax' | 'None';
|
|
10
|
+
}
|
|
11
|
+
export interface AccountCookies {
|
|
12
|
+
accountId: string;
|
|
13
|
+
cookies: LinkedInCookie[];
|
|
14
|
+
expiresAt?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface ProfilePosition {
|
|
17
|
+
title?: string;
|
|
18
|
+
companyName?: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
startYear?: number;
|
|
21
|
+
startMonth?: number;
|
|
22
|
+
endYear?: number;
|
|
23
|
+
endMonth?: number;
|
|
24
|
+
isCurrent?: boolean;
|
|
25
|
+
companyUrn?: string;
|
|
26
|
+
companyLogoUrl?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface ProfileEducation {
|
|
29
|
+
schoolName?: string;
|
|
30
|
+
degree?: string;
|
|
31
|
+
fieldOfStudy?: string;
|
|
32
|
+
startYear?: number;
|
|
33
|
+
startMonth?: number;
|
|
34
|
+
endYear?: number;
|
|
35
|
+
endMonth?: number;
|
|
36
|
+
}
|
|
37
|
+
export interface LinkedInProfile {
|
|
38
|
+
vanity: string;
|
|
39
|
+
firstName?: string;
|
|
40
|
+
lastName?: string;
|
|
41
|
+
headline?: string;
|
|
42
|
+
summary?: string;
|
|
43
|
+
locationText?: string;
|
|
44
|
+
avatarUrl?: string;
|
|
45
|
+
coverUrl?: string;
|
|
46
|
+
fsdProfileUrn?: string;
|
|
47
|
+
positions: ProfilePosition[];
|
|
48
|
+
educations: ProfileEducation[];
|
|
49
|
+
}
|
|
50
|
+
export interface SalesLeadSearchResult {
|
|
51
|
+
name?: string;
|
|
52
|
+
headline?: string;
|
|
53
|
+
imageUrl?: string;
|
|
54
|
+
salesProfileUrn?: string;
|
|
55
|
+
objectUrn?: string;
|
|
56
|
+
fsdKey?: string;
|
|
57
|
+
geoRegion?: string;
|
|
58
|
+
locationText?: string;
|
|
59
|
+
summary?: string;
|
|
60
|
+
currentRole?: string;
|
|
61
|
+
currentCompany?: string;
|
|
62
|
+
companyLogoUrl?: string;
|
|
63
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare class LinkedInClientError extends Error {
|
|
2
|
+
code: string;
|
|
3
|
+
status?: number | undefined;
|
|
4
|
+
accountId?: string | undefined;
|
|
5
|
+
constructor(message: string, code: string, status?: number | undefined, accountId?: string | undefined);
|
|
6
|
+
toJSON(): {
|
|
7
|
+
name: string;
|
|
8
|
+
message: string;
|
|
9
|
+
code: string;
|
|
10
|
+
status: number | undefined;
|
|
11
|
+
accountId: string | undefined;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export declare const ERROR_CODES: {
|
|
15
|
+
readonly NOT_INITIALIZED: "NOT_INITIALIZED";
|
|
16
|
+
readonly NO_ACCOUNTS: "NO_ACCOUNTS";
|
|
17
|
+
readonly NO_VALID_ACCOUNTS: "NO_VALID_ACCOUNTS";
|
|
18
|
+
readonly MISSING_CSRF: "MISSING_CSRF";
|
|
19
|
+
readonly REQUEST_FAILED: "REQUEST_FAILED";
|
|
20
|
+
readonly ALL_ACCOUNTS_FAILED: "ALL_ACCOUNTS_FAILED";
|
|
21
|
+
readonly PARSE_ERROR: "PARSE_ERROR";
|
|
22
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ERROR_CODES = exports.LinkedInClientError = void 0;
|
|
4
|
+
class LinkedInClientError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
status;
|
|
7
|
+
accountId;
|
|
8
|
+
constructor(message, code, status, accountId) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.status = status;
|
|
12
|
+
this.accountId = accountId;
|
|
13
|
+
this.name = 'LinkedInClientError';
|
|
14
|
+
}
|
|
15
|
+
toJSON() {
|
|
16
|
+
return {
|
|
17
|
+
name: this.name,
|
|
18
|
+
message: this.message,
|
|
19
|
+
code: this.code,
|
|
20
|
+
status: this.status,
|
|
21
|
+
accountId: this.accountId,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.LinkedInClientError = LinkedInClientError;
|
|
26
|
+
exports.ERROR_CODES = {
|
|
27
|
+
NOT_INITIALIZED: 'NOT_INITIALIZED',
|
|
28
|
+
NO_ACCOUNTS: 'NO_ACCOUNTS',
|
|
29
|
+
NO_VALID_ACCOUNTS: 'NO_VALID_ACCOUNTS',
|
|
30
|
+
MISSING_CSRF: 'MISSING_CSRF',
|
|
31
|
+
REQUEST_FAILED: 'REQUEST_FAILED',
|
|
32
|
+
ALL_ACCOUNTS_FAILED: 'ALL_ACCOUNTS_FAILED',
|
|
33
|
+
PARSE_ERROR: 'PARSE_ERROR',
|
|
34
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.log = log;
|
|
4
|
+
const config_1 = require("../config");
|
|
5
|
+
const LEVELS = {
|
|
6
|
+
debug: 10,
|
|
7
|
+
info: 20,
|
|
8
|
+
warn: 30,
|
|
9
|
+
error: 40,
|
|
10
|
+
};
|
|
11
|
+
const SECRET_KEYS = new Set([
|
|
12
|
+
'cosiallApiKey',
|
|
13
|
+
'apikey',
|
|
14
|
+
'apiKey',
|
|
15
|
+
'authorization',
|
|
16
|
+
'cookie',
|
|
17
|
+
'password',
|
|
18
|
+
'secret',
|
|
19
|
+
'set-cookie',
|
|
20
|
+
]);
|
|
21
|
+
function maskIfSecret(key, value) {
|
|
22
|
+
if (!key)
|
|
23
|
+
return value;
|
|
24
|
+
const k = key.toLowerCase();
|
|
25
|
+
for (const s of SECRET_KEYS) {
|
|
26
|
+
if (k === s.toLowerCase()) {
|
|
27
|
+
if (typeof value === 'string') {
|
|
28
|
+
if (value.length <= 6)
|
|
29
|
+
return '***';
|
|
30
|
+
return `${value.slice(0, 2)}***${value.slice(-2)}`;
|
|
31
|
+
}
|
|
32
|
+
return '***';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
function log(level, message, data) {
|
|
38
|
+
let configured = 'info';
|
|
39
|
+
try {
|
|
40
|
+
configured = ((0, config_1.getConfig)().logLevel || 'info');
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
configured = 'info';
|
|
44
|
+
}
|
|
45
|
+
if (LEVELS[level] < LEVELS[configured])
|
|
46
|
+
return;
|
|
47
|
+
const payload = { timestamp: new Date().toISOString(), level, message };
|
|
48
|
+
if (data !== undefined) {
|
|
49
|
+
try {
|
|
50
|
+
// Safe stringify with masking
|
|
51
|
+
const json = JSON.stringify(data, (k, v) => maskIfSecret(k, v));
|
|
52
|
+
payload.data = json ? JSON.parse(json) : undefined;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Fallback to shallow copy
|
|
56
|
+
payload.data = '[unserializable]';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const line = JSON.stringify(payload);
|
|
60
|
+
switch (level) {
|
|
61
|
+
case 'error':
|
|
62
|
+
console.error(line);
|
|
63
|
+
break;
|
|
64
|
+
case 'warn':
|
|
65
|
+
console.warn(line);
|
|
66
|
+
break;
|
|
67
|
+
default:
|
|
68
|
+
console.log(line);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface Metrics {
|
|
2
|
+
profileFetches: number;
|
|
3
|
+
profileCacheHits: number;
|
|
4
|
+
profileCacheMisses: number;
|
|
5
|
+
inflightDedupeHits: number;
|
|
6
|
+
searchCacheHits: number;
|
|
7
|
+
searchCacheMisses: number;
|
|
8
|
+
accountSelections: number;
|
|
9
|
+
authErrors: number;
|
|
10
|
+
httpRetries: number;
|
|
11
|
+
httpSuccess: number;
|
|
12
|
+
cosiallFetches: number;
|
|
13
|
+
cosiallSuccess: number;
|
|
14
|
+
cosiallFailures: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function incrementMetric(key: keyof Metrics, by?: number): void;
|
|
17
|
+
export declare function getSnapshot(): Metrics;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.incrementMetric = incrementMetric;
|
|
4
|
+
exports.getSnapshot = getSnapshot;
|
|
5
|
+
const metrics = {
|
|
6
|
+
profileFetches: 0,
|
|
7
|
+
profileCacheHits: 0,
|
|
8
|
+
profileCacheMisses: 0,
|
|
9
|
+
inflightDedupeHits: 0,
|
|
10
|
+
searchCacheHits: 0,
|
|
11
|
+
searchCacheMisses: 0,
|
|
12
|
+
accountSelections: 0,
|
|
13
|
+
authErrors: 0,
|
|
14
|
+
httpRetries: 0,
|
|
15
|
+
httpSuccess: 0,
|
|
16
|
+
cosiallFetches: 0,
|
|
17
|
+
cosiallSuccess: 0,
|
|
18
|
+
cosiallFailures: 0,
|
|
19
|
+
};
|
|
20
|
+
function incrementMetric(key, by = 1) {
|
|
21
|
+
// @ts-ignore - index signature not declared, but keys are enforced by type
|
|
22
|
+
metrics[key] = (metrics[key] || 0) + by;
|
|
23
|
+
}
|
|
24
|
+
function getSnapshot() {
|
|
25
|
+
return { ...metrics };
|
|
26
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "linkedin-secret-sauce",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Private LinkedIn Sales Navigator client with automatic cookie management",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc -p tsconfig.json",
|
|
12
|
+
"dev": "tsc -w -p tsconfig.json",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"prepare": "npm run build",
|
|
15
|
+
"prepublishOnly": "npm run build",
|
|
16
|
+
"release:patch": "npm version patch && git push --follow-tags",
|
|
17
|
+
"release:minor": "npm version minor && git push --follow-tags",
|
|
18
|
+
"release:major": "npm version major && git push --follow-tags"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"linkedin",
|
|
22
|
+
"sales-navigator",
|
|
23
|
+
"client",
|
|
24
|
+
"typescript"
|
|
25
|
+
],
|
|
26
|
+
"author": "Your Company",
|
|
27
|
+
"license": "UNLICENSED",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18.0.0"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/enerage/LinkedInSecretSouce.git"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/enerage/LinkedInSecretSouce/issues"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/enerage/LinkedInSecretSouce#readme",
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^20.11.0",
|
|
41
|
+
"typescript": "^5.3.3",
|
|
42
|
+
"vitest": "^1.6.0"
|
|
43
|
+
}
|
|
44
|
+
}
|