linkedin-secret-sauce 0.1.1 → 0.1.2
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 +193 -60
- package/dist/config.d.ts +3 -0
- package/dist/config.js +7 -1
- package/dist/cookie-pool.d.ts +11 -0
- package/dist/cookie-pool.js +55 -3
- package/dist/http-client.js +116 -14
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -1
- package/dist/linkedin-api.d.ts +22 -3
- package/dist/linkedin-api.js +366 -28
- package/dist/parsers/company-parser.d.ts +2 -0
- package/dist/parsers/company-parser.js +25 -0
- package/dist/parsers/image-parser.d.ts +1 -0
- package/dist/parsers/image-parser.js +15 -0
- package/dist/parsers/profile-parser.js +48 -7
- package/dist/parsers/search-parser.js +12 -3
- package/dist/types.d.ts +147 -0
- package/dist/utils/errors.d.ts +4 -0
- package/dist/utils/errors.js +5 -0
- package/dist/utils/linkedin-config.d.ts +43 -0
- package/dist/utils/linkedin-config.js +77 -0
- package/dist/utils/metrics.d.ts +5 -0
- package/dist/utils/metrics.js +16 -1
- package/dist/utils/request-history.d.ts +12 -0
- package/dist/utils/request-history.js +45 -0
- package/dist/utils/search-encoder.d.ts +4 -0
- package/dist/utils/search-encoder.js +163 -0
- package/package.json +16 -6
package/dist/types.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export interface ProfilePosition {
|
|
|
23
23
|
endMonth?: number;
|
|
24
24
|
isCurrent?: boolean;
|
|
25
25
|
companyUrn?: string;
|
|
26
|
+
companyId?: string;
|
|
26
27
|
companyLogoUrl?: string;
|
|
27
28
|
}
|
|
28
29
|
export interface ProfileEducation {
|
|
@@ -54,6 +55,8 @@ export interface SalesLeadSearchResult {
|
|
|
54
55
|
salesProfileUrn?: string;
|
|
55
56
|
objectUrn?: string;
|
|
56
57
|
fsdKey?: string;
|
|
58
|
+
salesAuthType?: string;
|
|
59
|
+
salesAuthToken?: string;
|
|
57
60
|
geoRegion?: string;
|
|
58
61
|
locationText?: string;
|
|
59
62
|
summary?: string;
|
|
@@ -61,3 +64,147 @@ export interface SalesLeadSearchResult {
|
|
|
61
64
|
currentCompany?: string;
|
|
62
65
|
companyLogoUrl?: string;
|
|
63
66
|
}
|
|
67
|
+
export interface SearchSalesResult {
|
|
68
|
+
items: SalesLeadSearchResult[];
|
|
69
|
+
page: {
|
|
70
|
+
start: number;
|
|
71
|
+
count: number;
|
|
72
|
+
total?: number;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export interface Company {
|
|
76
|
+
companyId: string;
|
|
77
|
+
universalName?: string;
|
|
78
|
+
name?: string;
|
|
79
|
+
websiteUrl?: string;
|
|
80
|
+
linkedinUrl?: string;
|
|
81
|
+
description?: string;
|
|
82
|
+
sizeLabel?: string;
|
|
83
|
+
followerCount?: number;
|
|
84
|
+
industries?: string[];
|
|
85
|
+
headquarters?: string;
|
|
86
|
+
logoUrl?: string;
|
|
87
|
+
coverUrl?: string;
|
|
88
|
+
foundedYear?: number;
|
|
89
|
+
companyType?: string;
|
|
90
|
+
specialties?: string[];
|
|
91
|
+
emailDomains?: string[];
|
|
92
|
+
}
|
|
93
|
+
export type TypeaheadType = 'BING_GEO' | 'TITLE' | 'INDUSTRY' | 'SENIORITY_LEVEL' | 'FUNCTION' | 'COMPANY_SIZE' | 'COMPANY' | 'PROFILE_LANGUAGE';
|
|
94
|
+
export interface TypeaheadItem {
|
|
95
|
+
id: string;
|
|
96
|
+
text: string;
|
|
97
|
+
}
|
|
98
|
+
export interface TypeaheadResult {
|
|
99
|
+
items: TypeaheadItem[];
|
|
100
|
+
page: {
|
|
101
|
+
start: number;
|
|
102
|
+
count: number;
|
|
103
|
+
total?: number;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export interface SalesNavigatorProfile {
|
|
107
|
+
profileUrn?: string;
|
|
108
|
+
firstName?: string;
|
|
109
|
+
lastName?: string;
|
|
110
|
+
headline?: string;
|
|
111
|
+
summary?: string;
|
|
112
|
+
location?: string;
|
|
113
|
+
imageUrl?: string;
|
|
114
|
+
flagshipProfileUrl?: string;
|
|
115
|
+
}
|
|
116
|
+
export type Geo = {
|
|
117
|
+
type: 'region' | 'country' | 'state' | 'city' | 'postal';
|
|
118
|
+
value: string;
|
|
119
|
+
id?: string;
|
|
120
|
+
};
|
|
121
|
+
export type SalesSearchFilters = {
|
|
122
|
+
company?: {
|
|
123
|
+
current?: {
|
|
124
|
+
include?: string[];
|
|
125
|
+
exclude?: string[];
|
|
126
|
+
};
|
|
127
|
+
past?: {
|
|
128
|
+
include?: string[];
|
|
129
|
+
exclude?: string[];
|
|
130
|
+
};
|
|
131
|
+
headcount?: {
|
|
132
|
+
include?: ("1-10" | "11-50" | "51-200" | "201-500" | "501-1000" | "1001-5000" | "5001-10000" | "10000+")[];
|
|
133
|
+
};
|
|
134
|
+
type?: {
|
|
135
|
+
include?: ("PUBLIC" | "PRIVATE" | "NONPROFIT" | "EDUCATIONAL" | "GOVERNMENT")[];
|
|
136
|
+
};
|
|
137
|
+
hq?: Geo[];
|
|
138
|
+
};
|
|
139
|
+
role?: {
|
|
140
|
+
current_titles?: {
|
|
141
|
+
include?: (string | {
|
|
142
|
+
id?: string | number;
|
|
143
|
+
text?: string;
|
|
144
|
+
})[];
|
|
145
|
+
exclude?: (string | {
|
|
146
|
+
id?: string | number;
|
|
147
|
+
text?: string;
|
|
148
|
+
})[];
|
|
149
|
+
};
|
|
150
|
+
past_titles?: {
|
|
151
|
+
include?: (string | {
|
|
152
|
+
id?: string | number;
|
|
153
|
+
text?: string;
|
|
154
|
+
})[];
|
|
155
|
+
exclude?: (string | {
|
|
156
|
+
id?: string | number;
|
|
157
|
+
text?: string;
|
|
158
|
+
})[];
|
|
159
|
+
};
|
|
160
|
+
functions?: {
|
|
161
|
+
include?: string[];
|
|
162
|
+
exclude?: string[];
|
|
163
|
+
};
|
|
164
|
+
seniority_ids?: number[];
|
|
165
|
+
};
|
|
166
|
+
personal?: {
|
|
167
|
+
geography?: {
|
|
168
|
+
include?: Geo[];
|
|
169
|
+
exclude?: Geo[];
|
|
170
|
+
};
|
|
171
|
+
industry_ids?: {
|
|
172
|
+
include?: number[];
|
|
173
|
+
exclude?: number[];
|
|
174
|
+
};
|
|
175
|
+
profile_language?: {
|
|
176
|
+
include?: string[];
|
|
177
|
+
};
|
|
178
|
+
years_in_current_role?: {
|
|
179
|
+
min?: number;
|
|
180
|
+
max?: number;
|
|
181
|
+
};
|
|
182
|
+
years_at_company?: {
|
|
183
|
+
min?: number;
|
|
184
|
+
max?: number;
|
|
185
|
+
};
|
|
186
|
+
years_experience?: {
|
|
187
|
+
min?: number;
|
|
188
|
+
max?: number;
|
|
189
|
+
};
|
|
190
|
+
years_in_current_role_ids?: number[];
|
|
191
|
+
years_at_company_ids?: number[];
|
|
192
|
+
years_experience_ids?: number[];
|
|
193
|
+
};
|
|
194
|
+
spotlights?: {
|
|
195
|
+
recent_job_change_days?: number;
|
|
196
|
+
posted_in_last_days?: number;
|
|
197
|
+
};
|
|
198
|
+
workflow?: {
|
|
199
|
+
account_lists?: {
|
|
200
|
+
include: string[];
|
|
201
|
+
exclude?: string[];
|
|
202
|
+
};
|
|
203
|
+
saved_only?: boolean;
|
|
204
|
+
};
|
|
205
|
+
boolean?: {
|
|
206
|
+
title?: string;
|
|
207
|
+
company?: string;
|
|
208
|
+
keywords?: string;
|
|
209
|
+
};
|
|
210
|
+
};
|
package/dist/utils/errors.d.ts
CHANGED
|
@@ -19,4 +19,8 @@ export declare const ERROR_CODES: {
|
|
|
19
19
|
readonly REQUEST_FAILED: "REQUEST_FAILED";
|
|
20
20
|
readonly ALL_ACCOUNTS_FAILED: "ALL_ACCOUNTS_FAILED";
|
|
21
21
|
readonly PARSE_ERROR: "PARSE_ERROR";
|
|
22
|
+
readonly INVALID_INPUT: "INVALID_INPUT";
|
|
23
|
+
readonly NOT_FOUND: "NOT_FOUND";
|
|
24
|
+
readonly AUTH_ERROR: "AUTH_ERROR";
|
|
25
|
+
readonly RATE_LIMITED: "RATE_LIMITED";
|
|
22
26
|
};
|
package/dist/utils/errors.js
CHANGED
|
@@ -31,4 +31,9 @@ exports.ERROR_CODES = {
|
|
|
31
31
|
REQUEST_FAILED: 'REQUEST_FAILED',
|
|
32
32
|
ALL_ACCOUNTS_FAILED: 'ALL_ACCOUNTS_FAILED',
|
|
33
33
|
PARSE_ERROR: 'PARSE_ERROR',
|
|
34
|
+
// Added to satisfy merged specification
|
|
35
|
+
INVALID_INPUT: 'INVALID_INPUT',
|
|
36
|
+
NOT_FOUND: 'NOT_FOUND',
|
|
37
|
+
AUTH_ERROR: 'AUTH_ERROR',
|
|
38
|
+
RATE_LIMITED: 'RATE_LIMITED',
|
|
34
39
|
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export declare const linkedinApiUrl: "https://www.linkedin.com/voyager/api/";
|
|
2
|
+
export declare const linkedinSalesNavigatorUrl: "https://www.linkedin.com/sales-api";
|
|
3
|
+
export declare const authUrl: "https://www.linkedin.com/uas/authenticate";
|
|
4
|
+
export declare const defaultUserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36";
|
|
5
|
+
export declare const defaultAcceptNormalized: "application/vnd.linkedin.normalized+json+2.1";
|
|
6
|
+
export declare const defaultAccept: "*/*";
|
|
7
|
+
export declare const defaultAcceptLanguage: "en-US,en;q=0.9,en-GB;q=0.8";
|
|
8
|
+
export declare const defaultLiLang: "en_US";
|
|
9
|
+
export declare const defaultOrigin: "https://www.linkedin.com";
|
|
10
|
+
export type LinkedInTrackPayload = {
|
|
11
|
+
clientVersion: string;
|
|
12
|
+
mpVersion: string;
|
|
13
|
+
osName: string;
|
|
14
|
+
timezoneOffset: number;
|
|
15
|
+
timezone: string;
|
|
16
|
+
deviceFormFactor: "DESKTOP" | "MOBILE" | string;
|
|
17
|
+
mpName: string;
|
|
18
|
+
displayDensity?: number;
|
|
19
|
+
displayWidth?: number;
|
|
20
|
+
displayHeight?: number;
|
|
21
|
+
};
|
|
22
|
+
export declare const voyagerTrackPayload: LinkedInTrackPayload;
|
|
23
|
+
export declare const salesTrackPayload: LinkedInTrackPayload;
|
|
24
|
+
export type VoyagerHeaderOptions = {
|
|
25
|
+
cookiesHeader: string;
|
|
26
|
+
csrf: string;
|
|
27
|
+
referer?: string;
|
|
28
|
+
accept?: string;
|
|
29
|
+
acceptLanguage?: string;
|
|
30
|
+
userAgent?: string;
|
|
31
|
+
trackPayload?: LinkedInTrackPayload;
|
|
32
|
+
};
|
|
33
|
+
export declare function buildVoyagerHeaders(opts: VoyagerHeaderOptions): Record<string, string>;
|
|
34
|
+
export type SalesHeaderOptions = {
|
|
35
|
+
cookiesHeader: string;
|
|
36
|
+
csrf: string;
|
|
37
|
+
referer?: string;
|
|
38
|
+
acceptLanguage?: string;
|
|
39
|
+
userAgent?: string;
|
|
40
|
+
trackPayload?: LinkedInTrackPayload;
|
|
41
|
+
pageInstanceName?: string;
|
|
42
|
+
};
|
|
43
|
+
export declare function buildSalesHeaders(opts: SalesHeaderOptions): Record<string, string>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Centralized LinkedIn endpoints and header builders
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.salesTrackPayload = exports.voyagerTrackPayload = exports.defaultOrigin = exports.defaultLiLang = exports.defaultAcceptLanguage = exports.defaultAccept = exports.defaultAcceptNormalized = exports.defaultUserAgent = exports.authUrl = exports.linkedinSalesNavigatorUrl = exports.linkedinApiUrl = void 0;
|
|
5
|
+
exports.buildVoyagerHeaders = buildVoyagerHeaders;
|
|
6
|
+
exports.buildSalesHeaders = buildSalesHeaders;
|
|
7
|
+
exports.linkedinApiUrl = "https://www.linkedin.com/voyager/api/";
|
|
8
|
+
exports.linkedinSalesNavigatorUrl = "https://www.linkedin.com/sales-api";
|
|
9
|
+
exports.authUrl = "https://www.linkedin.com/uas/authenticate";
|
|
10
|
+
exports.defaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36";
|
|
11
|
+
exports.defaultAcceptNormalized = "application/vnd.linkedin.normalized+json+2.1";
|
|
12
|
+
exports.defaultAccept = "*/*";
|
|
13
|
+
exports.defaultAcceptLanguage = "en-US,en;q=0.9,en-GB;q=0.8";
|
|
14
|
+
exports.defaultLiLang = "en_US";
|
|
15
|
+
exports.defaultOrigin = "https://www.linkedin.com";
|
|
16
|
+
// Voyager (regular profile API) tracking payload
|
|
17
|
+
exports.voyagerTrackPayload = Object.freeze({
|
|
18
|
+
clientVersion: "1.13.39643",
|
|
19
|
+
mpVersion: "1.13.39643",
|
|
20
|
+
osName: "web",
|
|
21
|
+
timezoneOffset: 1,
|
|
22
|
+
timezone: "Europe/Paris",
|
|
23
|
+
deviceFormFactor: "DESKTOP",
|
|
24
|
+
mpName: "voyager-web", // Different from Sales Nav
|
|
25
|
+
displayDensity: 1,
|
|
26
|
+
displayWidth: 1920,
|
|
27
|
+
displayHeight: 1080,
|
|
28
|
+
});
|
|
29
|
+
// Sales Navigator tracking payload
|
|
30
|
+
exports.salesTrackPayload = Object.freeze({
|
|
31
|
+
clientVersion: "2.0.5594",
|
|
32
|
+
mpVersion: "2.0.5594",
|
|
33
|
+
osName: "web",
|
|
34
|
+
timezoneOffset: 1,
|
|
35
|
+
timezone: "Europe/Paris",
|
|
36
|
+
deviceFormFactor: "DESKTOP",
|
|
37
|
+
mpName: "lighthouse-web", // Sales-specific
|
|
38
|
+
displayDensity: 1,
|
|
39
|
+
displayWidth: 1920,
|
|
40
|
+
displayHeight: 1080,
|
|
41
|
+
});
|
|
42
|
+
function buildVoyagerHeaders(opts) {
|
|
43
|
+
const { cookiesHeader, csrf, referer, accept = exports.defaultAcceptNormalized, acceptLanguage = exports.defaultAcceptLanguage, userAgent = exports.defaultUserAgent, trackPayload = exports.voyagerTrackPayload, } = opts;
|
|
44
|
+
return {
|
|
45
|
+
Cookie: cookiesHeader,
|
|
46
|
+
"User-Agent": userAgent,
|
|
47
|
+
accept,
|
|
48
|
+
"accept-language": acceptLanguage,
|
|
49
|
+
"x-restli-protocol-version": "2.0.0",
|
|
50
|
+
"csrf-token": csrf,
|
|
51
|
+
"x-li-lang": exports.defaultLiLang,
|
|
52
|
+
"x-li-track": JSON.stringify(trackPayload),
|
|
53
|
+
"sec-fetch-site": "same-origin",
|
|
54
|
+
"sec-fetch-mode": "cors",
|
|
55
|
+
referer: referer ?? exports.defaultOrigin + "/",
|
|
56
|
+
origin: exports.defaultOrigin,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function buildSalesHeaders(opts) {
|
|
60
|
+
const { cookiesHeader, csrf, referer, acceptLanguage = exports.defaultAcceptLanguage, userAgent = exports.defaultUserAgent, trackPayload = exports.salesTrackPayload, pageInstanceName = "d_sales2_search_people", } = opts;
|
|
61
|
+
const pageInstance = `urn:li:page:${pageInstanceName};${Buffer.from(String(Date.now())).toString("base64")}`;
|
|
62
|
+
return {
|
|
63
|
+
Cookie: cookiesHeader,
|
|
64
|
+
"User-Agent": userAgent,
|
|
65
|
+
accept: exports.defaultAccept,
|
|
66
|
+
"accept-language": acceptLanguage,
|
|
67
|
+
"x-restli-protocol-version": "2.0.0",
|
|
68
|
+
"csrf-token": csrf,
|
|
69
|
+
"x-li-lang": exports.defaultLiLang,
|
|
70
|
+
"x-li-track": JSON.stringify(trackPayload),
|
|
71
|
+
"x-li-page-instance": pageInstance,
|
|
72
|
+
"sec-fetch-site": "same-origin",
|
|
73
|
+
"sec-fetch-mode": "cors",
|
|
74
|
+
referer: referer ?? "https://www.linkedin.com/sales/search/people",
|
|
75
|
+
origin: exports.defaultOrigin,
|
|
76
|
+
};
|
|
77
|
+
}
|
package/dist/utils/metrics.d.ts
CHANGED
|
@@ -5,13 +5,18 @@ export interface Metrics {
|
|
|
5
5
|
inflightDedupeHits: number;
|
|
6
6
|
searchCacheHits: number;
|
|
7
7
|
searchCacheMisses: number;
|
|
8
|
+
typeaheadCacheHits: number;
|
|
9
|
+
companyCacheHits: number;
|
|
8
10
|
accountSelections: number;
|
|
9
11
|
authErrors: number;
|
|
10
12
|
httpRetries: number;
|
|
11
13
|
httpSuccess: number;
|
|
14
|
+
httpFailures: number;
|
|
12
15
|
cosiallFetches: number;
|
|
13
16
|
cosiallSuccess: number;
|
|
14
17
|
cosiallFailures: number;
|
|
18
|
+
companyFetches: number;
|
|
19
|
+
typeaheadRequests: number;
|
|
15
20
|
}
|
|
16
21
|
export declare function incrementMetric(key: keyof Metrics, by?: number): void;
|
|
17
22
|
export declare function getSnapshot(): Metrics;
|
package/dist/utils/metrics.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.incrementMetric = incrementMetric;
|
|
4
4
|
exports.getSnapshot = getSnapshot;
|
|
5
|
+
const request_history_1 = require("./request-history");
|
|
5
6
|
const metrics = {
|
|
6
7
|
profileFetches: 0,
|
|
7
8
|
profileCacheHits: 0,
|
|
@@ -9,18 +10,32 @@ const metrics = {
|
|
|
9
10
|
inflightDedupeHits: 0,
|
|
10
11
|
searchCacheHits: 0,
|
|
11
12
|
searchCacheMisses: 0,
|
|
13
|
+
typeaheadCacheHits: 0,
|
|
14
|
+
companyCacheHits: 0,
|
|
12
15
|
accountSelections: 0,
|
|
13
16
|
authErrors: 0,
|
|
14
17
|
httpRetries: 0,
|
|
15
18
|
httpSuccess: 0,
|
|
19
|
+
httpFailures: 0,
|
|
16
20
|
cosiallFetches: 0,
|
|
17
21
|
cosiallSuccess: 0,
|
|
18
22
|
cosiallFailures: 0,
|
|
23
|
+
companyFetches: 0,
|
|
24
|
+
typeaheadRequests: 0,
|
|
19
25
|
};
|
|
20
26
|
function incrementMetric(key, by = 1) {
|
|
21
27
|
// @ts-ignore - index signature not declared, but keys are enforced by type
|
|
22
28
|
metrics[key] = (metrics[key] || 0) + by;
|
|
23
29
|
}
|
|
24
30
|
function getSnapshot() {
|
|
25
|
-
|
|
31
|
+
// Augment with dynamic fields derived from other modules
|
|
32
|
+
const requestHistorySize = (() => {
|
|
33
|
+
try {
|
|
34
|
+
return (0, request_history_1.getRequestHistory)().length;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
})();
|
|
40
|
+
return { ...metrics, requestHistorySize };
|
|
26
41
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface RequestHistoryEntry {
|
|
2
|
+
timestamp: number;
|
|
3
|
+
operation: string;
|
|
4
|
+
selector: string;
|
|
5
|
+
status: number;
|
|
6
|
+
durationMs: number;
|
|
7
|
+
accountId?: string;
|
|
8
|
+
errorMessage?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function recordRequest(entry: Omit<RequestHistoryEntry, 'timestamp'>): void;
|
|
11
|
+
export declare function getRequestHistory(): RequestHistoryEntry[];
|
|
12
|
+
export declare function clearRequestHistory(): void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.recordRequest = recordRequest;
|
|
4
|
+
exports.getRequestHistory = getRequestHistory;
|
|
5
|
+
exports.clearRequestHistory = clearRequestHistory;
|
|
6
|
+
const config_1 = require("../config");
|
|
7
|
+
let buffer = [];
|
|
8
|
+
function capacity() {
|
|
9
|
+
try {
|
|
10
|
+
const cap = (0, config_1.getConfig)().maxRequestHistory ?? 500;
|
|
11
|
+
return Math.max(1, cap | 0);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return 500;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function redact(s) {
|
|
18
|
+
if (!s)
|
|
19
|
+
return s;
|
|
20
|
+
return s.replace(/li_at=([^;]+)/gi, 'li_at=[REDACTED]')
|
|
21
|
+
.replace(/JSESSIONID=([^;]+)/gi, 'JSESSIONID=[REDACTED]')
|
|
22
|
+
.replace(/csrf[^=&]*=([^&]+)/gi, 'csrf=[REDACTED]');
|
|
23
|
+
}
|
|
24
|
+
function recordRequest(entry) {
|
|
25
|
+
const safe = {
|
|
26
|
+
timestamp: Date.now(),
|
|
27
|
+
operation: String(entry.operation || ''),
|
|
28
|
+
selector: redact(String(entry.selector || '')),
|
|
29
|
+
status: Number(entry.status || 0),
|
|
30
|
+
durationMs: Math.max(0, Number(entry.durationMs || 0)),
|
|
31
|
+
accountId: entry.accountId,
|
|
32
|
+
errorMessage: entry.errorMessage ? redact(String(entry.errorMessage)) : undefined,
|
|
33
|
+
};
|
|
34
|
+
buffer.push(safe);
|
|
35
|
+
const cap = capacity();
|
|
36
|
+
if (buffer.length > cap) {
|
|
37
|
+
buffer.splice(0, buffer.length - cap);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function getRequestHistory() {
|
|
41
|
+
return buffer.slice();
|
|
42
|
+
}
|
|
43
|
+
function clearRequestHistory() {
|
|
44
|
+
buffer = [];
|
|
45
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { SalesSearchFilters } from '../types';
|
|
2
|
+
export declare function stableStringify(obj: any): string;
|
|
3
|
+
export declare function buildFilterSignature(filters?: SalesSearchFilters, rawQuery?: string): string | undefined;
|
|
4
|
+
export declare function buildLeadSearchQuery(keywords: string, filters?: SalesSearchFilters): string;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.stableStringify = stableStringify;
|
|
4
|
+
exports.buildFilterSignature = buildFilterSignature;
|
|
5
|
+
exports.buildLeadSearchQuery = buildLeadSearchQuery;
|
|
6
|
+
// Stable stringify for cache keys: sorts object keys recursively.
|
|
7
|
+
function stableStringify(obj) {
|
|
8
|
+
if (obj === null || typeof obj !== 'object')
|
|
9
|
+
return JSON.stringify(obj);
|
|
10
|
+
if (Array.isArray(obj))
|
|
11
|
+
return '[' + obj.map((v) => stableStringify(v)).join(',') + ']';
|
|
12
|
+
const keys = Object.keys(obj).sort();
|
|
13
|
+
return '{' + keys.map((k) => JSON.stringify(k) + ':' + stableStringify(obj[k])).join(',') + '}';
|
|
14
|
+
}
|
|
15
|
+
function buildFilterSignature(filters, rawQuery) {
|
|
16
|
+
if (rawQuery)
|
|
17
|
+
return 'rq:' + String(rawQuery).trim();
|
|
18
|
+
if (!filters)
|
|
19
|
+
return undefined;
|
|
20
|
+
return 'f:' + stableStringify(filters);
|
|
21
|
+
}
|
|
22
|
+
// Temporary minimal encoder: today we include only keywords in the query structure to
|
|
23
|
+
// preserve existing behavior. Once facet ids are confirmed, extend this to encode filters
|
|
24
|
+
// into the Sales Navigator query payload.
|
|
25
|
+
function encText(s) {
|
|
26
|
+
if (s === undefined || s === null)
|
|
27
|
+
return undefined;
|
|
28
|
+
return encodeURIComponent(String(s));
|
|
29
|
+
}
|
|
30
|
+
function list(values) {
|
|
31
|
+
return `List(${values.join(',')})`;
|
|
32
|
+
}
|
|
33
|
+
function valObj(parts) {
|
|
34
|
+
return `(${parts.join(',')})`;
|
|
35
|
+
}
|
|
36
|
+
function pushFilter(out, type, values) {
|
|
37
|
+
if (!values.length)
|
|
38
|
+
return;
|
|
39
|
+
out.push(valObj([`type:${type}`, `values:${list(values)}`]));
|
|
40
|
+
}
|
|
41
|
+
function buildLeadSearchQuery(keywords, filters) {
|
|
42
|
+
const kw = (filters?.boolean?.keywords ?? keywords ?? '').toString();
|
|
43
|
+
const encodedKw = kw.replace(/\s+/g, '%20');
|
|
44
|
+
const f = [];
|
|
45
|
+
// SENIORITY_LEVEL
|
|
46
|
+
if (filters?.role?.seniority_ids?.length) {
|
|
47
|
+
const values = filters.role.seniority_ids.map((id) => valObj([`id:${id}`, `selectionType:INCLUDED`]));
|
|
48
|
+
pushFilter(f, 'SENIORITY_LEVEL', values);
|
|
49
|
+
}
|
|
50
|
+
// CURRENT_TITLE / PAST_TITLE (supports id or text)
|
|
51
|
+
function encodeTitles(arr) {
|
|
52
|
+
if (!arr || !arr.length)
|
|
53
|
+
return [];
|
|
54
|
+
return arr.map((t) => {
|
|
55
|
+
if (typeof t === 'object') {
|
|
56
|
+
const parts = [];
|
|
57
|
+
if (t.id != null)
|
|
58
|
+
parts.push(`id:${t.id}`);
|
|
59
|
+
if (t.text)
|
|
60
|
+
parts.push(`text:${encText(t.text)}`);
|
|
61
|
+
parts.push('selectionType:INCLUDED');
|
|
62
|
+
return valObj(parts);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// Heuristic: allow numeric strings to be treated as ids, otherwise text
|
|
66
|
+
const isNumeric = /^\d+$/.test(t);
|
|
67
|
+
const parts = [isNumeric ? `id:${t}` : `text:${encText(t)}`, 'selectionType:INCLUDED'];
|
|
68
|
+
return valObj(parts);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const curTitles = encodeTitles(filters?.role?.current_titles?.include);
|
|
73
|
+
if (curTitles.length)
|
|
74
|
+
pushFilter(f, 'CURRENT_TITLE', curTitles);
|
|
75
|
+
const pastTitles = encodeTitles(filters?.role?.past_titles?.include);
|
|
76
|
+
if (pastTitles.length)
|
|
77
|
+
pushFilter(f, 'PAST_TITLE', pastTitles);
|
|
78
|
+
// FUNCTION (ids as strings)
|
|
79
|
+
if (filters?.role?.functions?.include?.length) {
|
|
80
|
+
const values = filters.role.functions.include.map((id) => valObj([`id:${id}`, 'selectionType:INCLUDED']));
|
|
81
|
+
pushFilter(f, 'FUNCTION', values);
|
|
82
|
+
}
|
|
83
|
+
// INDUSTRY (numeric ids)
|
|
84
|
+
if (filters?.personal?.industry_ids?.include?.length) {
|
|
85
|
+
const values = filters.personal.industry_ids.include.map((id) => valObj([`id:${id}`, 'selectionType:INCLUDED']));
|
|
86
|
+
pushFilter(f, 'INDUSTRY', values);
|
|
87
|
+
}
|
|
88
|
+
// PROFILE_LANGUAGE (ids like en, ar)
|
|
89
|
+
if (filters?.personal?.profile_language?.include?.length) {
|
|
90
|
+
const values = filters.personal.profile_language.include.map((id) => valObj([`id:${id}`, 'selectionType:INCLUDED']));
|
|
91
|
+
pushFilter(f, 'PROFILE_LANGUAGE', values);
|
|
92
|
+
}
|
|
93
|
+
// REGION (BING_GEO) — require id present
|
|
94
|
+
if (filters?.personal?.geography?.include?.length) {
|
|
95
|
+
const values = (filters.personal.geography.include || [])
|
|
96
|
+
.filter((g) => g.id)
|
|
97
|
+
.map((g) => valObj([`id:${g.id}`, `text:${encText(g.value)}`, 'selectionType:INCLUDED']));
|
|
98
|
+
pushFilter(f, 'REGION', values);
|
|
99
|
+
}
|
|
100
|
+
// COMPANY_HEADCOUNT — map common labels to A..I; accept raw letters too
|
|
101
|
+
const HEADCOUNT_CODE = {
|
|
102
|
+
'Self-employed': 'A',
|
|
103
|
+
'1-10': 'B',
|
|
104
|
+
'11-50': 'C',
|
|
105
|
+
'51-200': 'D',
|
|
106
|
+
'201-500': 'E',
|
|
107
|
+
'501-1000': 'F',
|
|
108
|
+
'1001-5000': 'G',
|
|
109
|
+
'5001-10000': 'H',
|
|
110
|
+
'10000+': 'I',
|
|
111
|
+
};
|
|
112
|
+
if (filters?.company?.headcount?.include?.length) {
|
|
113
|
+
const values = filters.company.headcount.include.map((label) => {
|
|
114
|
+
const code = HEADCOUNT_CODE[label] ?? label; // allow passing A..I directly
|
|
115
|
+
return valObj([`id:${code}`, 'selectionType:INCLUDED']);
|
|
116
|
+
});
|
|
117
|
+
pushFilter(f, 'COMPANY_HEADCOUNT', values);
|
|
118
|
+
}
|
|
119
|
+
// CURRENT_COMPANY — accept org URN (urn:li:organization:ID), numeric ID, or text
|
|
120
|
+
if (filters?.company?.current?.include?.length) {
|
|
121
|
+
const values = filters.company.current.include.map((c) => {
|
|
122
|
+
const s = String(c);
|
|
123
|
+
const parts = [];
|
|
124
|
+
if (/^urn:li:organization:\d+$/i.test(s) || /^\d+$/.test(s)) {
|
|
125
|
+
parts.push(`id:${s}`);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
parts.push(`text:${encText(s)}`);
|
|
129
|
+
}
|
|
130
|
+
parts.push('selectionType:INCLUDED');
|
|
131
|
+
return valObj(parts);
|
|
132
|
+
});
|
|
133
|
+
pushFilter(f, 'CURRENT_COMPANY', values);
|
|
134
|
+
}
|
|
135
|
+
// COMPANY_TYPE — map to C,P,N,D,G when known; allow raw codes
|
|
136
|
+
const COMPANY_TYPE = {
|
|
137
|
+
PUBLIC: 'C',
|
|
138
|
+
PRIVATE: 'P',
|
|
139
|
+
NONPROFIT: 'N',
|
|
140
|
+
EDUCATIONAL: 'D',
|
|
141
|
+
GOVERNMENT: 'G',
|
|
142
|
+
};
|
|
143
|
+
if (filters?.company?.type?.include?.length) {
|
|
144
|
+
const values = filters.company.type.include.map((t) => {
|
|
145
|
+
const code = COMPANY_TYPE[t] ?? t; // allow raw letter code
|
|
146
|
+
return valObj([`id:${code}`, 'selectionType:INCLUDED']);
|
|
147
|
+
});
|
|
148
|
+
pushFilter(f, 'COMPANY_TYPE', values);
|
|
149
|
+
}
|
|
150
|
+
// YEARS_* via explicit bucket ids if provided (1..5)
|
|
151
|
+
const yr = filters?.personal;
|
|
152
|
+
if (yr?.years_at_company_ids?.length) {
|
|
153
|
+
pushFilter(f, 'YEARS_AT_CURRENT_COMPANY', yr.years_at_company_ids.map((id) => valObj([`id:${id}`, 'selectionType:INCLUDED'])));
|
|
154
|
+
}
|
|
155
|
+
if (yr?.years_in_current_role_ids?.length) {
|
|
156
|
+
pushFilter(f, 'YEARS_IN_CURRENT_POSITION', yr.years_in_current_role_ids.map((id) => valObj([`id:${id}`, 'selectionType:INCLUDED'])));
|
|
157
|
+
}
|
|
158
|
+
if (yr?.years_experience_ids?.length) {
|
|
159
|
+
pushFilter(f, 'YEARS_OF_EXPERIENCE', yr.years_experience_ids.map((id) => valObj([`id:${id}`, 'selectionType:INCLUDED'])));
|
|
160
|
+
}
|
|
161
|
+
const filtersPart = f.length ? `,filters:${list(f)}` : '';
|
|
162
|
+
return `(spellCorrectionEnabled:true,keywords:${encodedKw}${filtersPart})`;
|
|
163
|
+
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linkedin-secret-sauce",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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",
|
|
7
7
|
"files": [
|
|
8
8
|
"dist"
|
|
9
9
|
],
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"registry": "https://registry.npmjs.org/"
|
|
12
|
+
},
|
|
10
13
|
"scripts": {
|
|
14
|
+
"dev:playground": "pnpm -C apps/playground dev",
|
|
15
|
+
"search": "node scripts/rg-fast.mjs",
|
|
16
|
+
"rg:fast": "node scripts/rg-fast.mjs",
|
|
11
17
|
"build": "tsc -p tsconfig.json",
|
|
12
18
|
"dev": "tsc -w -p tsconfig.json",
|
|
13
19
|
"test": "vitest run",
|
|
14
|
-
"prepare": "
|
|
20
|
+
"prepare": "husky",
|
|
15
21
|
"prepublishOnly": "npm run build",
|
|
16
22
|
"release:patch": "npm version patch && git push --follow-tags",
|
|
17
23
|
"release:minor": "npm version minor && git push --follow-tags",
|
|
@@ -30,15 +36,19 @@
|
|
|
30
36
|
},
|
|
31
37
|
"repository": {
|
|
32
38
|
"type": "git",
|
|
33
|
-
"url": "git+https://github.com/enerage/
|
|
39
|
+
"url": "git+https://github.com/enerage/LinkedInSecretSauce.git"
|
|
34
40
|
},
|
|
35
41
|
"bugs": {
|
|
36
|
-
"url": "https://github.com/enerage/
|
|
42
|
+
"url": "https://github.com/enerage/LinkedInSecretSauce/issues"
|
|
37
43
|
},
|
|
38
|
-
"homepage": "https://github.com/enerage/
|
|
44
|
+
"homepage": "https://github.com/enerage/LinkedInSecretSauce#readme",
|
|
39
45
|
"devDependencies": {
|
|
40
46
|
"@types/node": "^20.11.0",
|
|
41
47
|
"typescript": "^5.3.3",
|
|
42
|
-
"vitest": "^1.6.0"
|
|
48
|
+
"vitest": "^1.6.0",
|
|
49
|
+
"@commitlint/cli": "^19.4.0",
|
|
50
|
+
"@commitlint/config-conventional": "^19.4.0",
|
|
51
|
+
"husky": "^9.0.11"
|
|
43
52
|
}
|
|
44
53
|
}
|
|
54
|
+
|