linkedin-secret-sauce 0.3.29 → 0.5.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.
Files changed (55) hide show
  1. package/dist/cookie-pool.d.ts +1 -1
  2. package/dist/cookie-pool.js +67 -35
  3. package/dist/cosiall-client.d.ts +20 -1
  4. package/dist/cosiall-client.js +48 -25
  5. package/dist/enrichment/index.d.ts +43 -0
  6. package/dist/enrichment/index.js +231 -0
  7. package/dist/enrichment/orchestrator.d.ts +31 -0
  8. package/dist/enrichment/orchestrator.js +218 -0
  9. package/dist/enrichment/providers/apollo.d.ts +11 -0
  10. package/dist/enrichment/providers/apollo.js +136 -0
  11. package/dist/enrichment/providers/construct.d.ts +11 -0
  12. package/dist/enrichment/providers/construct.js +107 -0
  13. package/dist/enrichment/providers/dropcontact.d.ts +16 -0
  14. package/dist/enrichment/providers/dropcontact.js +37 -0
  15. package/dist/enrichment/providers/hunter.d.ts +11 -0
  16. package/dist/enrichment/providers/hunter.js +162 -0
  17. package/dist/enrichment/providers/index.d.ts +9 -0
  18. package/dist/enrichment/providers/index.js +18 -0
  19. package/dist/enrichment/providers/ldd.d.ts +11 -0
  20. package/dist/enrichment/providers/ldd.js +110 -0
  21. package/dist/enrichment/providers/smartprospect.d.ts +11 -0
  22. package/dist/enrichment/providers/smartprospect.js +249 -0
  23. package/dist/enrichment/types.d.ts +329 -0
  24. package/dist/enrichment/types.js +31 -0
  25. package/dist/enrichment/utils/disposable-domains.d.ts +24 -0
  26. package/dist/enrichment/utils/disposable-domains.js +1011 -0
  27. package/dist/enrichment/utils/index.d.ts +6 -0
  28. package/dist/enrichment/utils/index.js +22 -0
  29. package/dist/enrichment/utils/personal-domains.d.ts +31 -0
  30. package/dist/enrichment/utils/personal-domains.js +95 -0
  31. package/dist/enrichment/utils/validation.d.ts +42 -0
  32. package/dist/enrichment/utils/validation.js +130 -0
  33. package/dist/enrichment/verification/index.d.ts +4 -0
  34. package/dist/enrichment/verification/index.js +8 -0
  35. package/dist/enrichment/verification/mx.d.ts +16 -0
  36. package/dist/enrichment/verification/mx.js +168 -0
  37. package/dist/http-client.d.ts +1 -1
  38. package/dist/http-client.js +146 -63
  39. package/dist/index.d.ts +17 -14
  40. package/dist/index.js +20 -1
  41. package/dist/linkedin-api.d.ts +97 -4
  42. package/dist/linkedin-api.js +416 -134
  43. package/dist/parsers/company-parser.d.ts +15 -1
  44. package/dist/parsers/company-parser.js +45 -17
  45. package/dist/parsers/profile-parser.d.ts +19 -1
  46. package/dist/parsers/profile-parser.js +131 -81
  47. package/dist/parsers/search-parser.d.ts +1 -1
  48. package/dist/parsers/search-parser.js +24 -11
  49. package/dist/utils/logger.d.ts +1 -1
  50. package/dist/utils/logger.js +28 -18
  51. package/dist/utils/search-encoder.d.ts +32 -1
  52. package/dist/utils/search-encoder.js +102 -58
  53. package/dist/utils/sentry.d.ts +1 -1
  54. package/dist/utils/sentry.js +56 -8
  55. package/package.json +1 -1
@@ -3,22 +3,32 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.stableStringify = stableStringify;
4
4
  exports.buildFilterSignature = buildFilterSignature;
5
5
  exports.buildLeadSearchQuery = buildLeadSearchQuery;
6
- // Stable stringify for cache keys: sorts object keys recursively.
6
+ /**
7
+ * Creates a deterministic string representation of an object for use as cache keys.
8
+ * Sorts object keys recursively to ensure consistent output regardless of property order.
9
+ *
10
+ * @param obj - Any JSON-serializable value
11
+ * @returns Stable string representation suitable for cache key usage
12
+ */
7
13
  function stableStringify(obj) {
8
- if (obj === null || typeof obj !== 'object')
14
+ if (obj === null || typeof obj !== "object")
9
15
  return JSON.stringify(obj);
10
16
  if (Array.isArray(obj))
11
- return '[' + obj.map((v) => stableStringify(v)).join(',') + ']';
17
+ return "[" + obj.map((v) => stableStringify(v)).join(",") + "]";
12
18
  const rec = obj;
13
19
  const keys = Object.keys(rec).sort();
14
- return '{' + keys.map((k) => JSON.stringify(k) + ':' + stableStringify(rec[k])).join(',') + '}';
20
+ return ("{" +
21
+ keys
22
+ .map((k) => JSON.stringify(k) + ":" + stableStringify(rec[k]))
23
+ .join(",") +
24
+ "}");
15
25
  }
16
26
  function buildFilterSignature(filters, rawQuery) {
17
27
  if (rawQuery)
18
- return 'rq:' + String(rawQuery).trim();
28
+ return "rq:" + String(rawQuery).trim();
19
29
  if (!filters)
20
30
  return undefined;
21
- return 'f:' + stableStringify(filters);
31
+ return "f:" + stableStringify(filters);
22
32
  }
23
33
  // Temporary minimal encoder: today we include only keywords in the query structure to
24
34
  // preserve existing behavior. Once facet ids are confirmed, extend this to encode filters
@@ -29,104 +39,135 @@ function encText(s) {
29
39
  return encodeURIComponent(String(s));
30
40
  }
31
41
  function list(values) {
32
- return `List(${values.join(',')})`;
42
+ return `List(${values.join(",")})`;
33
43
  }
34
44
  function valObj(parts) {
35
- return `(${parts.join(',')})`;
45
+ return `(${parts.join(",")})`;
36
46
  }
37
47
  function pushFilter(out, type, values) {
38
48
  if (!values.length)
39
49
  return;
40
50
  out.push(valObj([`type:${type}`, `values:${list(values)}`]));
41
51
  }
52
+ /**
53
+ * Builds a Sales Navigator search query string from keywords and filters.
54
+ * Encodes the query in LinkedIn's proprietary format for the salesApiLeadSearch endpoint.
55
+ *
56
+ * @param keywords - Search keywords to include in the query
57
+ * @param filters - Optional SalesSearchFilters object with advanced criteria:
58
+ * - role: seniority_ids, current_titles, past_titles, functions
59
+ * - company: current, past, headcount, type
60
+ * - personal: geography, industry_ids, profile_language, years_* ranges
61
+ * @returns Encoded query string ready for use in Sales Navigator API URL
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * // Simple keyword search
66
+ * const query = buildLeadSearchQuery('CEO');
67
+ * // Returns: "(keywords:CEO)"
68
+ *
69
+ * // With filters
70
+ * const query = buildLeadSearchQuery('', {
71
+ * role: { seniority_ids: [9, 10] },
72
+ * company: { headcount: { include: ['51-200', '201-500'] } }
73
+ * });
74
+ * ```
75
+ */
42
76
  function buildLeadSearchQuery(keywords, filters) {
43
- const kw = (filters?.boolean?.keywords ?? keywords ?? '').toString();
77
+ const kw = (filters?.boolean?.keywords ?? keywords ?? "").toString();
44
78
  // Manually encode characters that encodeURIComponent doesn't handle properly in LinkedIn query context
45
79
  const encodedKw = kw
46
- .replace(/\(/g, '%28') // Encode (
47
- .replace(/\)/g, '%29') // Encode )
48
- .replace(/\[/g, '%5B') // Encode [
49
- .replace(/\]/g, '%5D') // Encode ]
50
- .replace(/\{/g, '%7B') // Encode {
51
- .replace(/\}/g, '%7D') // Encode }
52
- .replace(/&/g, '%26') // Encode &
53
- .replace(/\+/g, '%2B') // Encode +
54
- .replace(/=/g, '%3D') // Encode =
55
- .replace(/\s+/g, '%20'); // Encode spaces
80
+ .replace(/\(/g, "%28") // Encode (
81
+ .replace(/\)/g, "%29") // Encode )
82
+ .replace(/\[/g, "%5B") // Encode [
83
+ .replace(/\]/g, "%5D") // Encode ]
84
+ .replace(/\{/g, "%7B") // Encode {
85
+ .replace(/\}/g, "%7D") // Encode }
86
+ .replace(/&/g, "%26") // Encode &
87
+ .replace(/\+/g, "%2B") // Encode +
88
+ .replace(/=/g, "%3D") // Encode =
89
+ .replace(/\s+/g, "%20"); // Encode spaces
56
90
  const f = [];
57
91
  // SENIORITY_LEVEL
58
92
  if (filters?.role?.seniority_ids?.length) {
59
93
  const values = filters.role.seniority_ids.map((id) => valObj([`id:${id}`, `selectionType:INCLUDED`]));
60
- pushFilter(f, 'SENIORITY_LEVEL', values);
94
+ pushFilter(f, "SENIORITY_LEVEL", values);
61
95
  }
62
96
  // CURRENT_TITLE / PAST_TITLE (supports id or text)
63
97
  function encodeTitles(arr) {
64
98
  if (!arr || !arr.length)
65
99
  return [];
66
100
  return arr.map((t) => {
67
- if (typeof t === 'object') {
101
+ if (typeof t === "object") {
68
102
  const parts = [];
69
103
  if (t.id != null)
70
104
  parts.push(`id:${t.id}`);
71
105
  if (t.text)
72
106
  parts.push(`text:${encText(t.text)}`);
73
- parts.push('selectionType:INCLUDED');
107
+ parts.push("selectionType:INCLUDED");
74
108
  return valObj(parts);
75
109
  }
76
110
  else {
77
111
  // Heuristic: allow numeric strings to be treated as ids, otherwise text
78
112
  const isNumeric = /^\d+$/.test(t);
79
- const parts = [isNumeric ? `id:${t}` : `text:${encText(t)}`, 'selectionType:INCLUDED'];
113
+ const parts = [
114
+ isNumeric ? `id:${t}` : `text:${encText(t)}`,
115
+ "selectionType:INCLUDED",
116
+ ];
80
117
  return valObj(parts);
81
118
  }
82
119
  });
83
120
  }
84
121
  const curTitles = encodeTitles(filters?.role?.current_titles?.include);
85
122
  if (curTitles.length)
86
- pushFilter(f, 'CURRENT_TITLE', curTitles);
123
+ pushFilter(f, "CURRENT_TITLE", curTitles);
87
124
  const pastTitles = encodeTitles(filters?.role?.past_titles?.include);
88
125
  if (pastTitles.length)
89
- pushFilter(f, 'PAST_TITLE', pastTitles);
126
+ pushFilter(f, "PAST_TITLE", pastTitles);
90
127
  // FUNCTION (ids as strings)
91
128
  if (filters?.role?.functions?.include?.length) {
92
- const values = filters.role.functions.include.map((id) => valObj([`id:${id}`, 'selectionType:INCLUDED']));
93
- pushFilter(f, 'FUNCTION', values);
129
+ const values = filters.role.functions.include.map((id) => valObj([`id:${id}`, "selectionType:INCLUDED"]));
130
+ pushFilter(f, "FUNCTION", values);
94
131
  }
95
132
  // INDUSTRY (numeric ids)
96
133
  if (filters?.personal?.industry_ids?.include?.length) {
97
- const values = filters.personal.industry_ids.include.map((id) => valObj([`id:${id}`, 'selectionType:INCLUDED']));
98
- pushFilter(f, 'INDUSTRY', values);
134
+ const values = filters.personal.industry_ids.include.map((id) => valObj([`id:${id}`, "selectionType:INCLUDED"]));
135
+ pushFilter(f, "INDUSTRY", values);
99
136
  }
100
137
  // PROFILE_LANGUAGE (ids like en, ar)
101
138
  if (filters?.personal?.profile_language?.include?.length) {
102
- const values = filters.personal.profile_language.include.map((id) => valObj([`id:${id}`, 'selectionType:INCLUDED']));
103
- pushFilter(f, 'PROFILE_LANGUAGE', values);
139
+ const values = filters.personal.profile_language.include.map((id) => valObj([`id:${id}`, "selectionType:INCLUDED"]));
140
+ pushFilter(f, "PROFILE_LANGUAGE", values);
104
141
  }
105
142
  // REGION (BING_GEO) — require id present
106
143
  if (filters?.personal?.geography?.include?.length) {
107
144
  const values = (filters.personal.geography.include || [])
108
145
  .filter((g) => g.id)
109
- .map((g) => valObj([`id:${g.id}`, `text:${encText(g.value)}`, 'selectionType:INCLUDED']));
110
- pushFilter(f, 'REGION', values);
146
+ .map((g) => valObj([
147
+ `id:${g.id}`,
148
+ `text:${encText(g.value)}`,
149
+ "selectionType:INCLUDED",
150
+ ]));
151
+ pushFilter(f, "REGION", values);
111
152
  }
112
153
  // COMPANY_HEADCOUNT — map common labels to A..I; accept raw letters too
113
154
  const HEADCOUNT_CODE = {
114
- 'Self-employed': 'A',
115
- '1-10': 'B',
116
- '11-50': 'C',
117
- '51-200': 'D',
118
- '201-500': 'E',
119
- '501-1000': 'F',
120
- '1001-5000': 'G',
121
- '5001-10000': 'H',
122
- '10000+': 'I',
155
+ "Self-employed": "A",
156
+ "1-10": "B",
157
+ "11-50": "C",
158
+ "51-200": "D",
159
+ "201-500": "E",
160
+ "501-1000": "F",
161
+ "1001-5000": "G",
162
+ "5001-10000": "H",
163
+ "10000+": "I",
123
164
  };
124
165
  if (filters?.company?.headcount?.include?.length) {
125
166
  const values = filters.company.headcount.include.map((label) => {
126
167
  const code = HEADCOUNT_CODE[label] ?? label; // allow passing A..I directly
127
- return valObj([`id:${code}`, 'selectionType:INCLUDED']);
168
+ return valObj([`id:${code}`, "selectionType:INCLUDED"]);
128
169
  });
129
- pushFilter(f, 'COMPANY_HEADCOUNT', values);
170
+ pushFilter(f, "COMPANY_HEADCOUNT", values);
130
171
  }
131
172
  // CURRENT_COMPANY — accept org URN (urn:li:organization:ID) or numeric ID (auto-converted to URN)
132
173
  function encodeCompanies(arr) {
@@ -146,41 +187,44 @@ function buildLeadSearchQuery(keywords, filters) {
146
187
  // If not URN or numeric, assume it's already a URN (pass through)
147
188
  urn = idStr;
148
189
  }
149
- return valObj([`id:${encodeURIComponent(urn)}`, 'selectionType:INCLUDED']);
190
+ return valObj([
191
+ `id:${encodeURIComponent(urn)}`,
192
+ "selectionType:INCLUDED",
193
+ ]);
150
194
  });
151
195
  }
152
196
  const curCompanies = encodeCompanies(filters?.company?.current?.include);
153
197
  if (curCompanies.length)
154
- pushFilter(f, 'CURRENT_COMPANY', curCompanies);
198
+ pushFilter(f, "CURRENT_COMPANY", curCompanies);
155
199
  // PAST_COMPANY — same logic as current
156
200
  const pastCompanies = encodeCompanies(filters?.company?.past?.include);
157
201
  if (pastCompanies.length)
158
- pushFilter(f, 'PAST_COMPANY', pastCompanies);
202
+ pushFilter(f, "PAST_COMPANY", pastCompanies);
159
203
  // COMPANY_TYPE — map to C,P,N,D,G when known; allow raw codes
160
204
  const COMPANY_TYPE = {
161
- PUBLIC: 'C',
162
- PRIVATE: 'P',
163
- NONPROFIT: 'N',
164
- EDUCATIONAL: 'D',
165
- GOVERNMENT: 'G',
205
+ PUBLIC: "C",
206
+ PRIVATE: "P",
207
+ NONPROFIT: "N",
208
+ EDUCATIONAL: "D",
209
+ GOVERNMENT: "G",
166
210
  };
167
211
  if (filters?.company?.type?.include?.length) {
168
212
  const values = filters.company.type.include.map((t) => {
169
213
  const code = COMPANY_TYPE[t] ?? t; // allow raw letter code
170
- return valObj([`id:${code}`, 'selectionType:INCLUDED']);
214
+ return valObj([`id:${code}`, "selectionType:INCLUDED"]);
171
215
  });
172
- pushFilter(f, 'COMPANY_TYPE', values);
216
+ pushFilter(f, "COMPANY_TYPE", values);
173
217
  }
174
218
  // YEARS_* via explicit bucket ids if provided (1..5)
175
219
  const yr = filters?.personal;
176
220
  if (yr?.years_at_company_ids?.length) {
177
- pushFilter(f, 'YEARS_AT_CURRENT_COMPANY', yr.years_at_company_ids.map((id) => valObj([`id:${id}`, 'selectionType:INCLUDED'])));
221
+ pushFilter(f, "YEARS_AT_CURRENT_COMPANY", yr.years_at_company_ids.map((id) => valObj([`id:${id}`, "selectionType:INCLUDED"])));
178
222
  }
179
223
  if (yr?.years_in_current_role_ids?.length) {
180
- pushFilter(f, 'YEARS_IN_CURRENT_POSITION', yr.years_in_current_role_ids.map((id) => valObj([`id:${id}`, 'selectionType:INCLUDED'])));
224
+ pushFilter(f, "YEARS_IN_CURRENT_POSITION", yr.years_in_current_role_ids.map((id) => valObj([`id:${id}`, "selectionType:INCLUDED"])));
181
225
  }
182
226
  if (yr?.years_experience_ids?.length) {
183
- pushFilter(f, 'YEARS_OF_EXPERIENCE', yr.years_experience_ids.map((id) => valObj([`id:${id}`, 'selectionType:INCLUDED'])));
227
+ pushFilter(f, "YEARS_OF_EXPERIENCE", yr.years_experience_ids.map((id) => valObj([`id:${id}`, "selectionType:INCLUDED"])));
184
228
  }
185
229
  // Build query with minimal format for maximum compatibility
186
230
  // LinkedIn rejects spellCorrectionEnabled:true,keywords: with 400 errors
@@ -194,5 +238,5 @@ function buildLeadSearchQuery(keywords, filters) {
194
238
  parts.push(`filters:${list(f)}`);
195
239
  }
196
240
  // Return minimal format - no spellCorrectionEnabled wrapper
197
- return parts.length > 0 ? `(${parts.join(',')})` : '()';
241
+ return parts.length > 0 ? `(${parts.join(",")})` : "()";
198
242
  }
@@ -1,6 +1,6 @@
1
1
  export interface SentryClient {
2
2
  captureException(error: Error, context?: Record<string, unknown>): void;
3
- captureMessage(message: string, level: 'error' | 'warning' | 'info', context?: Record<string, unknown>): void;
3
+ captureMessage(message: string, level: "error" | "warning" | "info", context?: Record<string, unknown>): void;
4
4
  }
5
5
  export declare function setSentryClient(client: SentryClient): void;
6
6
  export declare function reportToSentry(error: Error | string, context?: Record<string, unknown>): void;
@@ -6,42 +6,90 @@ exports.reportWarningToSentry = reportWarningToSentry;
6
6
  exports.reportCriticalError = reportCriticalError;
7
7
  const logger_1 = require("./logger");
8
8
  let sentryClient = null;
9
+ // Deduplication: track recent error reports to prevent flooding Sentry
10
+ const recentReports = new Map();
11
+ const DEDUP_INTERVAL_MS = 60_000; // Only report same error once per minute
12
+ const MAX_RECENT_REPORTS = 1000; // Prevent memory growth
13
+ /**
14
+ * Generate a deduplication key for an error/message
15
+ */
16
+ function getDedupKey(message, level) {
17
+ return `${level}:${message}`;
18
+ }
19
+ /**
20
+ * Check if we should report this error (deduplication)
21
+ * Returns true if we should report, false if deduplicated
22
+ */
23
+ function shouldReport(key) {
24
+ const now = Date.now();
25
+ const lastReport = recentReports.get(key);
26
+ if (lastReport && now - lastReport < DEDUP_INTERVAL_MS) {
27
+ return false; // Skip - reported recently
28
+ }
29
+ // Cleanup old entries if map is too large
30
+ if (recentReports.size >= MAX_RECENT_REPORTS) {
31
+ const cutoff = now - DEDUP_INTERVAL_MS;
32
+ for (const [k, ts] of recentReports.entries()) {
33
+ if (ts < cutoff) {
34
+ recentReports.delete(k);
35
+ }
36
+ }
37
+ }
38
+ recentReports.set(key, now);
39
+ return true;
40
+ }
9
41
  function setSentryClient(client) {
10
42
  sentryClient = client;
11
- (0, logger_1.log)('info', 'sentry.configured', {});
43
+ (0, logger_1.log)("info", "sentry.configured", {});
12
44
  }
13
45
  function reportToSentry(error, context) {
14
46
  if (!sentryClient)
15
47
  return;
48
+ const message = typeof error === "string" ? error : error.message;
49
+ const key = getDedupKey(message, "error");
50
+ if (!shouldReport(key)) {
51
+ (0, logger_1.log)("debug", "sentry.deduplicated", { message });
52
+ return;
53
+ }
16
54
  try {
17
- if (typeof error === 'string') {
18
- sentryClient.captureMessage(error, 'error', context);
55
+ if (typeof error === "string") {
56
+ sentryClient.captureMessage(error, "error", context);
19
57
  }
20
58
  else {
21
59
  sentryClient.captureException(error, context);
22
60
  }
23
61
  }
24
62
  catch (err) {
25
- (0, logger_1.log)('error', 'sentry.reportFailed', { error: err.message });
63
+ (0, logger_1.log)("error", "sentry.reportFailed", { error: err.message });
26
64
  }
27
65
  }
28
66
  function reportWarningToSentry(message, context) {
29
67
  if (!sentryClient)
30
68
  return;
69
+ const key = getDedupKey(message, "warning");
70
+ if (!shouldReport(key)) {
71
+ (0, logger_1.log)("debug", "sentry.deduplicated", { message });
72
+ return;
73
+ }
31
74
  try {
32
- sentryClient.captureMessage(message, 'warning', context);
75
+ sentryClient.captureMessage(message, "warning", context);
33
76
  }
34
77
  catch (err) {
35
- (0, logger_1.log)('error', 'sentry.warningFailed', { error: err.message });
78
+ (0, logger_1.log)("error", "sentry.warningFailed", { error: err.message });
36
79
  }
37
80
  }
38
81
  function reportCriticalError(message, context) {
39
82
  if (!sentryClient)
40
83
  return;
84
+ const key = getDedupKey(message, "critical");
85
+ if (!shouldReport(key)) {
86
+ (0, logger_1.log)("debug", "sentry.deduplicated", { message });
87
+ return;
88
+ }
41
89
  try {
42
- sentryClient.captureMessage(message, 'error', context);
90
+ sentryClient.captureMessage(message, "error", context);
43
91
  }
44
92
  catch (err) {
45
- (0, logger_1.log)('error', 'sentry.criticalFailed', { error: err.message });
93
+ (0, logger_1.log)("error", "sentry.criticalFailed", { error: err.message });
46
94
  }
47
95
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linkedin-secret-sauce",
3
- "version": "0.3.29",
3
+ "version": "0.5.0",
4
4
  "description": "Private LinkedIn Sales Navigator client with automatic cookie management",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",