linkedin-secret-sauce 0.1.1 → 0.2.0

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.
@@ -6,6 +6,8 @@ const cookie_pool_1 = require("./cookie-pool");
6
6
  const errors_1 = require("./utils/errors");
7
7
  const logger_1 = require("./utils/logger");
8
8
  const metrics_1 = require("./utils/metrics");
9
+ const request_history_1 = require("./utils/request-history");
10
+ const linkedin_config_1 = require("./utils/linkedin-config");
9
11
  function sleep(ms) {
10
12
  return new Promise((resolve) => setTimeout(resolve, ms));
11
13
  }
@@ -14,6 +16,7 @@ function isRetryableStatus(status) {
14
16
  }
15
17
  async function executeLinkedInRequest(options, _operationName) {
16
18
  const config = (0, config_1.getConfig)();
19
+ const op = _operationName || 'request';
17
20
  const perAccountAttempts = (config.maxRetries ?? 0) + 1;
18
21
  const delay = config.retryDelayMs ?? 700;
19
22
  // Setup proxy env if configured
@@ -22,13 +25,11 @@ async function executeLinkedInRequest(options, _operationName) {
22
25
  let proxySet = false;
23
26
  try {
24
27
  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}`;
28
+ const { url: proxyUrl, masked } = parseProxyString(config.proxyString);
29
29
  process.env.HTTP_PROXY = proxyUrl;
30
30
  process.env.HTTPS_PROXY = proxyUrl;
31
31
  proxySet = true;
32
+ (0, logger_1.log)('info', 'proxy.set', { host: masked });
32
33
  }
33
34
  // Try up to 3 different accounts on auth/rate failures
34
35
  let lastError;
@@ -55,18 +56,42 @@ async function executeLinkedInRequest(options, _operationName) {
55
56
  const { accountId, cookies } = selection;
56
57
  const csrf = (0, cookie_pool_1.extractCsrfToken)(cookies);
57
58
  const cookieHeader = (0, cookie_pool_1.buildCookieHeader)(cookies);
59
+ // Choose header profile based on Sales vs Voyager context
60
+ const isSales = /\/sales\//i.test(String(options.headers?.Referer || ''))
61
+ || /https:\/\/www\.linkedin\.com\/sales-api/i.test(options.url);
62
+ const baseHeaders = isSales
63
+ ? (0, linkedin_config_1.buildSalesHeaders)({ cookiesHeader: cookieHeader, csrf, referer: options.headers?.Referer })
64
+ : (0, linkedin_config_1.buildVoyagerHeaders)({ cookiesHeader: cookieHeader, csrf, referer: options.headers?.Referer });
58
65
  const headers = {
59
- Cookie: cookieHeader,
60
- 'csrf-token': csrf,
66
+ ...baseHeaders,
61
67
  ...(options.headers || {}),
62
68
  };
63
69
  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
+ const started = Date.now();
71
+ (0, logger_1.log)('debug', 'http.attempt', { accountId, attempt: attempt + 1, method: options.method ?? 'GET', url: options.url });
72
+ let res;
73
+ try {
74
+ const init = {
75
+ method: options.method ?? 'GET',
76
+ headers,
77
+ };
78
+ if (options.body !== undefined) {
79
+ init.body = typeof options.body === 'string' ? options.body : JSON.stringify(options.body);
80
+ }
81
+ res = (await fetch(options.url, init));
82
+ }
83
+ catch (err) {
84
+ const e = err;
85
+ const code = e?.code || e?.cause?.code || 'FETCH_FAILED';
86
+ (0, logger_1.log)('error', 'http.networkError', { accountId, url: options.url, code, message: String(e?.message || err) });
87
+ (0, metrics_1.incrementMetric)('httpFailures');
88
+ try {
89
+ (0, request_history_1.recordRequest)({ operation: op, selector: options.url, status: 0, durationMs: Date.now() - started, accountId, errorMessage: String(e?.message || err) });
90
+ }
91
+ catch { }
92
+ lastError = new errors_1.LinkedInClientError('LinkedIn fetch failed', 'REQUEST_FAILED', 0, accountId);
93
+ break; // rotate to next account
94
+ }
70
95
  if (res.ok) {
71
96
  // success
72
97
  try {
@@ -75,6 +100,16 @@ async function executeLinkedInRequest(options, _operationName) {
75
100
  catch { }
76
101
  (0, logger_1.log)('info', 'http.success', { accountId, url: options.url });
77
102
  (0, metrics_1.incrementMetric)('httpSuccess');
103
+ try {
104
+ (0, request_history_1.recordRequest)({
105
+ operation: op,
106
+ selector: options.url,
107
+ status: res.status ?? 200,
108
+ durationMs: Date.now() - started,
109
+ accountId,
110
+ });
111
+ }
112
+ catch { }
78
113
  return (await res.json());
79
114
  }
80
115
  const status = res.status ?? 0;
@@ -86,18 +121,44 @@ async function executeLinkedInRequest(options, _operationName) {
86
121
  catch { }
87
122
  (0, logger_1.log)('warn', 'http.rotateOnAuth', { accountId, status });
88
123
  (0, metrics_1.incrementMetric)('authErrors');
124
+ (0, metrics_1.incrementMetric)('httpFailures');
89
125
  lastError = new errors_1.LinkedInClientError(`LinkedIn request failed: ${status}`, 'REQUEST_FAILED', status, accountId);
126
+ try {
127
+ (0, request_history_1.recordRequest)({
128
+ operation: op,
129
+ selector: options.url,
130
+ status,
131
+ durationMs: Date.now() - started,
132
+ accountId,
133
+ errorMessage: 'rotate',
134
+ });
135
+ }
136
+ catch { }
90
137
  break; // break inner loop to rotate account
91
138
  }
92
139
  // Retryable 5xx on same account
93
140
  if ((status >= 500 && status < 600) && attempt < perAccountAttempts - 1) {
94
- (0, logger_1.log)('debug', 'http.retry', { accountId, status, nextDelayMs: delay });
141
+ (0, logger_1.log)('debug', 'http.retry', { accountId, status, attempt: attempt + 1, nextDelayMs: delay });
95
142
  (0, metrics_1.incrementMetric)('httpRetries');
96
143
  await sleep(delay);
97
144
  continue;
98
145
  }
99
146
  // Non-retryable: throw
100
- throw new errors_1.LinkedInClientError(`LinkedIn request failed: ${status}`, 'REQUEST_FAILED', status, accountId);
147
+ const err = new errors_1.LinkedInClientError(`LinkedIn request failed: ${status}`, 'REQUEST_FAILED', status, accountId);
148
+ (0, metrics_1.incrementMetric)('httpFailures');
149
+ (0, logger_1.log)(status >= 500 ? 'warn' : 'error', 'http.fail', { accountId, status, url: options.url });
150
+ try {
151
+ (0, request_history_1.recordRequest)({
152
+ operation: op,
153
+ selector: options.url,
154
+ status,
155
+ durationMs: Date.now() - started,
156
+ accountId,
157
+ errorMessage: err.message,
158
+ });
159
+ }
160
+ catch { }
161
+ throw err;
101
162
  }
102
163
  }
103
164
  // Exhausted rotations
@@ -116,6 +177,51 @@ async function executeLinkedInRequest(options, _operationName) {
116
177
  delete process.env.HTTPS_PROXY;
117
178
  else
118
179
  process.env.HTTPS_PROXY = prevHttps;
180
+ (0, logger_1.log)('info', 'proxy.restore', {});
119
181
  }
120
182
  }
121
183
  }
184
+ // Parse proxy string supporting:
185
+ // - Full URL forms: http(s)://[user[:pass]@]host:port (host may be [ipv6])
186
+ // - Colon-delimited: host:port[:user:pass] with optional [ipv6]
187
+ function parseProxyString(input) {
188
+ const s = String(input).trim();
189
+ // URL form
190
+ if (/^\w+:\/\//.test(s)) {
191
+ try {
192
+ const u = new URL(s);
193
+ if (u.protocol !== 'http:' && u.protocol !== 'https:') {
194
+ throw new errors_1.LinkedInClientError(`Unsupported proxy protocol: ${u.protocol}`, 'INVALID_CONFIG', 400);
195
+ }
196
+ const user = u.username || '';
197
+ const pass = u.password || '';
198
+ const hostport = u.host; // includes brackets for IPv6
199
+ const url = `${u.protocol}//${user ? `${user}:${pass}@` : ''}${hostport}`;
200
+ const masked = user ? `${hostport}:${user}:***` : hostport;
201
+ return { url, masked };
202
+ }
203
+ catch {
204
+ // fall through to colon parsing
205
+ }
206
+ }
207
+ // Colon-delimited
208
+ // [ipv6]:port[:user:pass] OR host:port[:user:pass]
209
+ const m = s.match(/^(\[?[^\]]+\]?):(\d+)(?::([^:]*):(.+))?$/);
210
+ if (m) {
211
+ let host = m[1];
212
+ const port = m[2];
213
+ const user = m[3];
214
+ const pass = m[4];
215
+ // Ensure brackets for IPv6 in URL
216
+ const needsBrackets = host.includes(':') && !/^\[.*\]$/.test(host);
217
+ if (needsBrackets)
218
+ host = `[${host}]`;
219
+ const auth = user && pass ? `${user}:${pass}@` : '';
220
+ const url = `http://${auth}${host}:${port}`;
221
+ const hostport = `${host}:${port}`;
222
+ const masked = user && pass ? `${hostport}:${user}:***` : hostport;
223
+ return { url, masked };
224
+ }
225
+ // Fallback: conservative
226
+ return { url: s.includes('://') ? s : `http://${s}`, masked: '***' };
227
+ }
package/dist/index.d.ts CHANGED
@@ -8,3 +8,5 @@ export { parseSalesSearchResults } from './parsers/search-parser';
8
8
  export * from './linkedin-api';
9
9
  export * from './types';
10
10
  export * from './utils/metrics';
11
+ export { getRequestHistory, clearRequestHistory } from './utils/request-history';
12
+ export type { RequestHistoryEntry } from './utils/request-history';
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.parseSalesSearchResults = exports.parseFullProfile = exports.LinkedInClientError = exports.getConfig = exports.initializeLinkedInClient = void 0;
17
+ exports.clearRequestHistory = exports.getRequestHistory = exports.parseSalesSearchResults = exports.parseFullProfile = exports.LinkedInClientError = exports.getConfig = exports.initializeLinkedInClient = void 0;
18
18
  var config_1 = require("./config");
19
19
  Object.defineProperty(exports, "initializeLinkedInClient", { enumerable: true, get: function () { return config_1.initializeLinkedInClient; } });
20
20
  Object.defineProperty(exports, "getConfig", { enumerable: true, get: function () { return config_1.getConfig; } });
@@ -29,3 +29,6 @@ Object.defineProperty(exports, "parseSalesSearchResults", { enumerable: true, ge
29
29
  __exportStar(require("./linkedin-api"), exports);
30
30
  __exportStar(require("./types"), exports);
31
31
  __exportStar(require("./utils/metrics"), exports);
32
+ var request_history_1 = require("./utils/request-history");
33
+ Object.defineProperty(exports, "getRequestHistory", { enumerable: true, get: function () { return request_history_1.getRequestHistory; } });
34
+ Object.defineProperty(exports, "clearRequestHistory", { enumerable: true, get: function () { return request_history_1.clearRequestHistory; } });
@@ -1,5 +1,24 @@
1
- import type { LinkedInProfile, SalesLeadSearchResult } from './types';
1
+ import type { SalesSearchFilters } from './types';
2
+ import type { LinkedInProfile, SalesLeadSearchResult, SearchSalesResult, TypeaheadResult, SalesNavigatorProfile, Company } from './types';
2
3
  export declare function getProfileByVanity(vanity: string): Promise<LinkedInProfile>;
3
4
  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[]>;
5
+ export declare function searchSalesLeads(keywords: string, options?: {
6
+ start?: number;
7
+ count?: number;
8
+ decorationId?: string;
9
+ filters?: SalesSearchFilters;
10
+ rawQuery?: string;
11
+ }): Promise<SearchSalesResult | SalesLeadSearchResult[]>;
12
+ export declare function getProfilesBatch(vanities: string[], concurrency?: number): Promise<(LinkedInProfile | null)[]>;
13
+ export declare function resolveCompanyUniversalName(universalName: string): Promise<{
14
+ companyId?: string;
15
+ }>;
16
+ export declare function getCompanyById(companyId: string): Promise<Company>;
17
+ export declare function getCompanyByUrl(companyUrl: string): Promise<Company>;
18
+ export declare function typeahead(options: {
19
+ type: string;
20
+ query?: string;
21
+ start?: number;
22
+ count?: number;
23
+ }): Promise<TypeaheadResult>;
24
+ export declare function getSalesNavigatorProfileDetails(profileUrnOrId: string): Promise<SalesNavigatorProfile>;