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 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
+ ```
@@ -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,2 @@
1
+ import type { AccountCookies } from './types';
2
+ export declare function fetchCookiesFromCosiall(): Promise<AccountCookies[]>;
@@ -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,7 @@
1
+ export interface LinkedInRequestOptions {
2
+ url: string;
3
+ method?: 'GET' | 'POST';
4
+ body?: any;
5
+ headers?: Record<string, string>;
6
+ }
7
+ export declare function executeLinkedInRequest<T>(options: LinkedInRequestOptions, _operationName: string): Promise<T>;
@@ -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
+ }
@@ -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,2 @@
1
+ import type { LinkedInProfile } from '../types';
2
+ export declare function parseFullProfile(rawResponse: any, vanity: string): LinkedInProfile;
@@ -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,2 @@
1
+ import type { SalesLeadSearchResult } from '../types';
2
+ export declare function parseSalesSearchResults(rawResponse: any): SalesLeadSearchResult[];
@@ -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
+ }
@@ -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,3 @@
1
+ "use strict";
2
+ // Domain types (scaffold)
3
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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,2 @@
1
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
2
+ export declare function log(level: LogLevel, message: string, data?: any): void;
@@ -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
+ }