linkedin-secret-sauce 0.3.28 → 0.4.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.
- package/dist/cookie-pool.d.ts +1 -1
- package/dist/cookie-pool.js +67 -35
- package/dist/cosiall-client.d.ts +20 -1
- package/dist/cosiall-client.js +48 -25
- package/dist/http-client.d.ts +1 -1
- package/dist/http-client.js +146 -63
- package/dist/linkedin-api.d.ts +97 -4
- package/dist/linkedin-api.js +416 -134
- package/dist/parsers/company-parser.d.ts +15 -1
- package/dist/parsers/company-parser.js +45 -17
- package/dist/parsers/profile-parser.d.ts +19 -1
- package/dist/parsers/profile-parser.js +131 -81
- package/dist/parsers/search-parser.d.ts +1 -1
- package/dist/parsers/search-parser.js +24 -11
- package/dist/utils/logger.d.ts +1 -1
- package/dist/utils/logger.js +28 -18
- package/dist/utils/search-encoder.d.ts +32 -1
- package/dist/utils/search-encoder.js +104 -49
- package/dist/utils/sentry.d.ts +1 -1
- package/dist/utils/sentry.js +56 -8
- 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
|
-
|
|
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 !==
|
|
14
|
+
if (obj === null || typeof obj !== "object")
|
|
9
15
|
return JSON.stringify(obj);
|
|
10
16
|
if (Array.isArray(obj))
|
|
11
|
-
return
|
|
17
|
+
return "[" + obj.map((v) => stableStringify(v)).join(",") + "]";
|
|
12
18
|
const rec = obj;
|
|
13
19
|
const keys = Object.keys(rec).sort();
|
|
14
|
-
return
|
|
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
|
|
28
|
+
return "rq:" + String(rawQuery).trim();
|
|
19
29
|
if (!filters)
|
|
20
30
|
return undefined;
|
|
21
|
-
return
|
|
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,93 +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 ??
|
|
44
|
-
|
|
77
|
+
const kw = (filters?.boolean?.keywords ?? keywords ?? "").toString();
|
|
78
|
+
// Manually encode characters that encodeURIComponent doesn't handle properly in LinkedIn query context
|
|
79
|
+
const encodedKw = kw
|
|
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
|
|
45
90
|
const f = [];
|
|
46
91
|
// SENIORITY_LEVEL
|
|
47
92
|
if (filters?.role?.seniority_ids?.length) {
|
|
48
93
|
const values = filters.role.seniority_ids.map((id) => valObj([`id:${id}`, `selectionType:INCLUDED`]));
|
|
49
|
-
pushFilter(f,
|
|
94
|
+
pushFilter(f, "SENIORITY_LEVEL", values);
|
|
50
95
|
}
|
|
51
96
|
// CURRENT_TITLE / PAST_TITLE (supports id or text)
|
|
52
97
|
function encodeTitles(arr) {
|
|
53
98
|
if (!arr || !arr.length)
|
|
54
99
|
return [];
|
|
55
100
|
return arr.map((t) => {
|
|
56
|
-
if (typeof t ===
|
|
101
|
+
if (typeof t === "object") {
|
|
57
102
|
const parts = [];
|
|
58
103
|
if (t.id != null)
|
|
59
104
|
parts.push(`id:${t.id}`);
|
|
60
105
|
if (t.text)
|
|
61
106
|
parts.push(`text:${encText(t.text)}`);
|
|
62
|
-
parts.push(
|
|
107
|
+
parts.push("selectionType:INCLUDED");
|
|
63
108
|
return valObj(parts);
|
|
64
109
|
}
|
|
65
110
|
else {
|
|
66
111
|
// Heuristic: allow numeric strings to be treated as ids, otherwise text
|
|
67
112
|
const isNumeric = /^\d+$/.test(t);
|
|
68
|
-
const parts = [
|
|
113
|
+
const parts = [
|
|
114
|
+
isNumeric ? `id:${t}` : `text:${encText(t)}`,
|
|
115
|
+
"selectionType:INCLUDED",
|
|
116
|
+
];
|
|
69
117
|
return valObj(parts);
|
|
70
118
|
}
|
|
71
119
|
});
|
|
72
120
|
}
|
|
73
121
|
const curTitles = encodeTitles(filters?.role?.current_titles?.include);
|
|
74
122
|
if (curTitles.length)
|
|
75
|
-
pushFilter(f,
|
|
123
|
+
pushFilter(f, "CURRENT_TITLE", curTitles);
|
|
76
124
|
const pastTitles = encodeTitles(filters?.role?.past_titles?.include);
|
|
77
125
|
if (pastTitles.length)
|
|
78
|
-
pushFilter(f,
|
|
126
|
+
pushFilter(f, "PAST_TITLE", pastTitles);
|
|
79
127
|
// FUNCTION (ids as strings)
|
|
80
128
|
if (filters?.role?.functions?.include?.length) {
|
|
81
|
-
const values = filters.role.functions.include.map((id) => valObj([`id:${id}`,
|
|
82
|
-
pushFilter(f,
|
|
129
|
+
const values = filters.role.functions.include.map((id) => valObj([`id:${id}`, "selectionType:INCLUDED"]));
|
|
130
|
+
pushFilter(f, "FUNCTION", values);
|
|
83
131
|
}
|
|
84
132
|
// INDUSTRY (numeric ids)
|
|
85
133
|
if (filters?.personal?.industry_ids?.include?.length) {
|
|
86
|
-
const values = filters.personal.industry_ids.include.map((id) => valObj([`id:${id}`,
|
|
87
|
-
pushFilter(f,
|
|
134
|
+
const values = filters.personal.industry_ids.include.map((id) => valObj([`id:${id}`, "selectionType:INCLUDED"]));
|
|
135
|
+
pushFilter(f, "INDUSTRY", values);
|
|
88
136
|
}
|
|
89
137
|
// PROFILE_LANGUAGE (ids like en, ar)
|
|
90
138
|
if (filters?.personal?.profile_language?.include?.length) {
|
|
91
|
-
const values = filters.personal.profile_language.include.map((id) => valObj([`id:${id}`,
|
|
92
|
-
pushFilter(f,
|
|
139
|
+
const values = filters.personal.profile_language.include.map((id) => valObj([`id:${id}`, "selectionType:INCLUDED"]));
|
|
140
|
+
pushFilter(f, "PROFILE_LANGUAGE", values);
|
|
93
141
|
}
|
|
94
142
|
// REGION (BING_GEO) — require id present
|
|
95
143
|
if (filters?.personal?.geography?.include?.length) {
|
|
96
144
|
const values = (filters.personal.geography.include || [])
|
|
97
145
|
.filter((g) => g.id)
|
|
98
|
-
.map((g) => valObj([
|
|
99
|
-
|
|
146
|
+
.map((g) => valObj([
|
|
147
|
+
`id:${g.id}`,
|
|
148
|
+
`text:${encText(g.value)}`,
|
|
149
|
+
"selectionType:INCLUDED",
|
|
150
|
+
]));
|
|
151
|
+
pushFilter(f, "REGION", values);
|
|
100
152
|
}
|
|
101
153
|
// COMPANY_HEADCOUNT — map common labels to A..I; accept raw letters too
|
|
102
154
|
const HEADCOUNT_CODE = {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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",
|
|
112
164
|
};
|
|
113
165
|
if (filters?.company?.headcount?.include?.length) {
|
|
114
166
|
const values = filters.company.headcount.include.map((label) => {
|
|
115
167
|
const code = HEADCOUNT_CODE[label] ?? label; // allow passing A..I directly
|
|
116
|
-
return valObj([`id:${code}`,
|
|
168
|
+
return valObj([`id:${code}`, "selectionType:INCLUDED"]);
|
|
117
169
|
});
|
|
118
|
-
pushFilter(f,
|
|
170
|
+
pushFilter(f, "COMPANY_HEADCOUNT", values);
|
|
119
171
|
}
|
|
120
172
|
// CURRENT_COMPANY — accept org URN (urn:li:organization:ID) or numeric ID (auto-converted to URN)
|
|
121
173
|
function encodeCompanies(arr) {
|
|
@@ -135,41 +187,44 @@ function buildLeadSearchQuery(keywords, filters) {
|
|
|
135
187
|
// If not URN or numeric, assume it's already a URN (pass through)
|
|
136
188
|
urn = idStr;
|
|
137
189
|
}
|
|
138
|
-
return valObj([
|
|
190
|
+
return valObj([
|
|
191
|
+
`id:${encodeURIComponent(urn)}`,
|
|
192
|
+
"selectionType:INCLUDED",
|
|
193
|
+
]);
|
|
139
194
|
});
|
|
140
195
|
}
|
|
141
196
|
const curCompanies = encodeCompanies(filters?.company?.current?.include);
|
|
142
197
|
if (curCompanies.length)
|
|
143
|
-
pushFilter(f,
|
|
198
|
+
pushFilter(f, "CURRENT_COMPANY", curCompanies);
|
|
144
199
|
// PAST_COMPANY — same logic as current
|
|
145
200
|
const pastCompanies = encodeCompanies(filters?.company?.past?.include);
|
|
146
201
|
if (pastCompanies.length)
|
|
147
|
-
pushFilter(f,
|
|
202
|
+
pushFilter(f, "PAST_COMPANY", pastCompanies);
|
|
148
203
|
// COMPANY_TYPE — map to C,P,N,D,G when known; allow raw codes
|
|
149
204
|
const COMPANY_TYPE = {
|
|
150
|
-
PUBLIC:
|
|
151
|
-
PRIVATE:
|
|
152
|
-
NONPROFIT:
|
|
153
|
-
EDUCATIONAL:
|
|
154
|
-
GOVERNMENT:
|
|
205
|
+
PUBLIC: "C",
|
|
206
|
+
PRIVATE: "P",
|
|
207
|
+
NONPROFIT: "N",
|
|
208
|
+
EDUCATIONAL: "D",
|
|
209
|
+
GOVERNMENT: "G",
|
|
155
210
|
};
|
|
156
211
|
if (filters?.company?.type?.include?.length) {
|
|
157
212
|
const values = filters.company.type.include.map((t) => {
|
|
158
213
|
const code = COMPANY_TYPE[t] ?? t; // allow raw letter code
|
|
159
|
-
return valObj([`id:${code}`,
|
|
214
|
+
return valObj([`id:${code}`, "selectionType:INCLUDED"]);
|
|
160
215
|
});
|
|
161
|
-
pushFilter(f,
|
|
216
|
+
pushFilter(f, "COMPANY_TYPE", values);
|
|
162
217
|
}
|
|
163
218
|
// YEARS_* via explicit bucket ids if provided (1..5)
|
|
164
219
|
const yr = filters?.personal;
|
|
165
220
|
if (yr?.years_at_company_ids?.length) {
|
|
166
|
-
pushFilter(f,
|
|
221
|
+
pushFilter(f, "YEARS_AT_CURRENT_COMPANY", yr.years_at_company_ids.map((id) => valObj([`id:${id}`, "selectionType:INCLUDED"])));
|
|
167
222
|
}
|
|
168
223
|
if (yr?.years_in_current_role_ids?.length) {
|
|
169
|
-
pushFilter(f,
|
|
224
|
+
pushFilter(f, "YEARS_IN_CURRENT_POSITION", yr.years_in_current_role_ids.map((id) => valObj([`id:${id}`, "selectionType:INCLUDED"])));
|
|
170
225
|
}
|
|
171
226
|
if (yr?.years_experience_ids?.length) {
|
|
172
|
-
pushFilter(f,
|
|
227
|
+
pushFilter(f, "YEARS_OF_EXPERIENCE", yr.years_experience_ids.map((id) => valObj([`id:${id}`, "selectionType:INCLUDED"])));
|
|
173
228
|
}
|
|
174
229
|
// Build query with minimal format for maximum compatibility
|
|
175
230
|
// LinkedIn rejects spellCorrectionEnabled:true,keywords: with 400 errors
|
|
@@ -183,5 +238,5 @@ function buildLeadSearchQuery(keywords, filters) {
|
|
|
183
238
|
parts.push(`filters:${list(f)}`);
|
|
184
239
|
}
|
|
185
240
|
// Return minimal format - no spellCorrectionEnabled wrapper
|
|
186
|
-
return parts.length > 0 ? `(${parts.join(
|
|
241
|
+
return parts.length > 0 ? `(${parts.join(",")})` : "()";
|
|
187
242
|
}
|
package/dist/utils/sentry.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export interface SentryClient {
|
|
2
2
|
captureException(error: Error, context?: Record<string, unknown>): void;
|
|
3
|
-
captureMessage(message: string, level:
|
|
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;
|
package/dist/utils/sentry.js
CHANGED
|
@@ -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)(
|
|
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 ===
|
|
18
|
-
sentryClient.captureMessage(error,
|
|
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)(
|
|
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,
|
|
75
|
+
sentryClient.captureMessage(message, "warning", context);
|
|
33
76
|
}
|
|
34
77
|
catch (err) {
|
|
35
|
-
(0, logger_1.log)(
|
|
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,
|
|
90
|
+
sentryClient.captureMessage(message, "error", context);
|
|
43
91
|
}
|
|
44
92
|
catch (err) {
|
|
45
|
-
(0, logger_1.log)(
|
|
93
|
+
(0, logger_1.log)("error", "sentry.criticalFailed", { error: err.message });
|
|
46
94
|
}
|
|
47
95
|
}
|