linkedin-secret-sauce 0.3.29 → 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 +102 -58
- 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,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 ??
|
|
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,
|
|
47
|
-
.replace(/\)/g,
|
|
48
|
-
.replace(/\[/g,
|
|
49
|
-
.replace(/\]/g,
|
|
50
|
-
.replace(/\{/g,
|
|
51
|
-
.replace(/\}/g,
|
|
52
|
-
.replace(/&/g,
|
|
53
|
-
.replace(/\+/g,
|
|
54
|
-
.replace(/=/g,
|
|
55
|
-
.replace(/\s+/g,
|
|
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,
|
|
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 ===
|
|
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(
|
|
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 = [
|
|
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,
|
|
123
|
+
pushFilter(f, "CURRENT_TITLE", curTitles);
|
|
87
124
|
const pastTitles = encodeTitles(filters?.role?.past_titles?.include);
|
|
88
125
|
if (pastTitles.length)
|
|
89
|
-
pushFilter(f,
|
|
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}`,
|
|
93
|
-
pushFilter(f,
|
|
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}`,
|
|
98
|
-
pushFilter(f,
|
|
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}`,
|
|
103
|
-
pushFilter(f,
|
|
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([
|
|
110
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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}`,
|
|
168
|
+
return valObj([`id:${code}`, "selectionType:INCLUDED"]);
|
|
128
169
|
});
|
|
129
|
-
pushFilter(f,
|
|
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([
|
|
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,
|
|
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,
|
|
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:
|
|
162
|
-
PRIVATE:
|
|
163
|
-
NONPROFIT:
|
|
164
|
-
EDUCATIONAL:
|
|
165
|
-
GOVERNMENT:
|
|
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}`,
|
|
214
|
+
return valObj([`id:${code}`, "selectionType:INCLUDED"]);
|
|
171
215
|
});
|
|
172
|
-
pushFilter(f,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
}
|
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
|
}
|