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.
@@ -1,2 +1,2 @@
1
1
  import type { SalesLeadSearchResult } from '../types';
2
- export declare function parseSalesSearchResults(rawResponse: any): SalesLeadSearchResult[];
2
+ export declare function parseSalesSearchResults(rawResponse: unknown): SalesLeadSearchResult[];
@@ -2,7 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseSalesSearchResults = parseSalesSearchResults;
4
4
  function parseSalesSearchResults(rawResponse) {
5
- const elements = Array.isArray(rawResponse?.elements) ? rawResponse.elements : [];
5
+ const rr = rawResponse;
6
+ const elements = Array.isArray(rr?.elements) ? rr?.elements : [];
6
7
  const results = [];
7
8
  for (const el of elements) {
8
9
  const name = [el?.firstName, el?.lastName].filter(Boolean).join(' ').trim() || undefined;
@@ -16,9 +17,18 @@ function parseSalesSearchResults(rawResponse) {
16
17
  summary: el?.summary,
17
18
  };
18
19
  // fsdKey from URN patterns
19
- const m = String(el?.entityUrn || '').match(/^urn:li:(?:fs_salesProfile|fsd_profile):\(([^,\s)]+)/i);
20
- if (m)
21
- res.fsdKey = m[1];
20
+ const urn = String(el?.entityUrn || '');
21
+ const m3 = urn.match(/^urn:li:(?:fs_salesProfile|fsd_profile):\(([^,\s)]+),([^,\s)]+),([^,\s)]+)\)/i);
22
+ if (m3) {
23
+ res.fsdKey = m3[1];
24
+ res.salesAuthType = m3[2];
25
+ res.salesAuthToken = m3[3];
26
+ }
27
+ else {
28
+ const m1 = urn.match(/^urn:li:(?:fs_salesProfile|fsd_profile):\(([^,\s)]+)/i);
29
+ if (m1)
30
+ res.fsdKey = m1[1];
31
+ }
22
32
  // Image best artifact
23
33
  const img = el?.profilePictureDisplayImage;
24
34
  if (img?.rootUrl && Array.isArray(img?.artifacts) && img.artifacts.length) {
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
+ };
@@ -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
  };
@@ -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
+ }
@@ -1,2 +1,2 @@
1
1
  export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
2
- export declare function log(level: LogLevel, message: string, data?: any): void;
2
+ export declare function log(level: LogLevel, message: string, data?: unknown): void;
@@ -5,13 +5,21 @@ 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
- export declare function getSnapshot(): Metrics;
22
+ export interface MetricsSnapshot extends Metrics {
23
+ requestHistorySize: number;
24
+ }
25
+ export declare function getSnapshot(): MetricsSnapshot;
@@ -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,31 @@ 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
- // @ts-ignore - index signature not declared, but keys are enforced by type
22
27
  metrics[key] = (metrics[key] || 0) + by;
23
28
  }
24
29
  function getSnapshot() {
25
- return { ...metrics };
30
+ // Augment with dynamic fields derived from other modules
31
+ const requestHistorySize = (() => {
32
+ try {
33
+ return (0, request_history_1.getRequestHistory)().length;
34
+ }
35
+ catch {
36
+ return 0;
37
+ }
38
+ })();
39
+ return { ...metrics, requestHistorySize };
26
40
  }
@@ -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: unknown): 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,164 @@
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 rec = obj;
13
+ const keys = Object.keys(rec).sort();
14
+ return '{' + keys.map((k) => JSON.stringify(k) + ':' + stableStringify(rec[k])).join(',') + '}';
15
+ }
16
+ function buildFilterSignature(filters, rawQuery) {
17
+ if (rawQuery)
18
+ return 'rq:' + String(rawQuery).trim();
19
+ if (!filters)
20
+ return undefined;
21
+ return 'f:' + stableStringify(filters);
22
+ }
23
+ // Temporary minimal encoder: today we include only keywords in the query structure to
24
+ // preserve existing behavior. Once facet ids are confirmed, extend this to encode filters
25
+ // into the Sales Navigator query payload.
26
+ function encText(s) {
27
+ if (s === undefined || s === null)
28
+ return undefined;
29
+ return encodeURIComponent(String(s));
30
+ }
31
+ function list(values) {
32
+ return `List(${values.join(',')})`;
33
+ }
34
+ function valObj(parts) {
35
+ return `(${parts.join(',')})`;
36
+ }
37
+ function pushFilter(out, type, values) {
38
+ if (!values.length)
39
+ return;
40
+ out.push(valObj([`type:${type}`, `values:${list(values)}`]));
41
+ }
42
+ function buildLeadSearchQuery(keywords, filters) {
43
+ const kw = (filters?.boolean?.keywords ?? keywords ?? '').toString();
44
+ const encodedKw = kw.replace(/\s+/g, '%20');
45
+ const f = [];
46
+ // SENIORITY_LEVEL
47
+ if (filters?.role?.seniority_ids?.length) {
48
+ const values = filters.role.seniority_ids.map((id) => valObj([`id:${id}`, `selectionType:INCLUDED`]));
49
+ pushFilter(f, 'SENIORITY_LEVEL', values);
50
+ }
51
+ // CURRENT_TITLE / PAST_TITLE (supports id or text)
52
+ function encodeTitles(arr) {
53
+ if (!arr || !arr.length)
54
+ return [];
55
+ return arr.map((t) => {
56
+ if (typeof t === 'object') {
57
+ const parts = [];
58
+ if (t.id != null)
59
+ parts.push(`id:${t.id}`);
60
+ if (t.text)
61
+ parts.push(`text:${encText(t.text)}`);
62
+ parts.push('selectionType:INCLUDED');
63
+ return valObj(parts);
64
+ }
65
+ else {
66
+ // Heuristic: allow numeric strings to be treated as ids, otherwise text
67
+ const isNumeric = /^\d+$/.test(t);
68
+ const parts = [isNumeric ? `id:${t}` : `text:${encText(t)}`, 'selectionType:INCLUDED'];
69
+ return valObj(parts);
70
+ }
71
+ });
72
+ }
73
+ const curTitles = encodeTitles(filters?.role?.current_titles?.include);
74
+ if (curTitles.length)
75
+ pushFilter(f, 'CURRENT_TITLE', curTitles);
76
+ const pastTitles = encodeTitles(filters?.role?.past_titles?.include);
77
+ if (pastTitles.length)
78
+ pushFilter(f, 'PAST_TITLE', pastTitles);
79
+ // FUNCTION (ids as strings)
80
+ if (filters?.role?.functions?.include?.length) {
81
+ const values = filters.role.functions.include.map((id) => valObj([`id:${id}`, 'selectionType:INCLUDED']));
82
+ pushFilter(f, 'FUNCTION', values);
83
+ }
84
+ // INDUSTRY (numeric ids)
85
+ if (filters?.personal?.industry_ids?.include?.length) {
86
+ const values = filters.personal.industry_ids.include.map((id) => valObj([`id:${id}`, 'selectionType:INCLUDED']));
87
+ pushFilter(f, 'INDUSTRY', values);
88
+ }
89
+ // PROFILE_LANGUAGE (ids like en, ar)
90
+ if (filters?.personal?.profile_language?.include?.length) {
91
+ const values = filters.personal.profile_language.include.map((id) => valObj([`id:${id}`, 'selectionType:INCLUDED']));
92
+ pushFilter(f, 'PROFILE_LANGUAGE', values);
93
+ }
94
+ // REGION (BING_GEO) — require id present
95
+ if (filters?.personal?.geography?.include?.length) {
96
+ const values = (filters.personal.geography.include || [])
97
+ .filter((g) => g.id)
98
+ .map((g) => valObj([`id:${g.id}`, `text:${encText(g.value)}`, 'selectionType:INCLUDED']));
99
+ pushFilter(f, 'REGION', values);
100
+ }
101
+ // COMPANY_HEADCOUNT — map common labels to A..I; accept raw letters too
102
+ const HEADCOUNT_CODE = {
103
+ 'Self-employed': 'A',
104
+ '1-10': 'B',
105
+ '11-50': 'C',
106
+ '51-200': 'D',
107
+ '201-500': 'E',
108
+ '501-1000': 'F',
109
+ '1001-5000': 'G',
110
+ '5001-10000': 'H',
111
+ '10000+': 'I',
112
+ };
113
+ if (filters?.company?.headcount?.include?.length) {
114
+ const values = filters.company.headcount.include.map((label) => {
115
+ const code = HEADCOUNT_CODE[label] ?? label; // allow passing A..I directly
116
+ return valObj([`id:${code}`, 'selectionType:INCLUDED']);
117
+ });
118
+ pushFilter(f, 'COMPANY_HEADCOUNT', values);
119
+ }
120
+ // CURRENT_COMPANY — accept org URN (urn:li:organization:ID), numeric ID, or text
121
+ if (filters?.company?.current?.include?.length) {
122
+ const values = filters.company.current.include.map((c) => {
123
+ const s = String(c);
124
+ const parts = [];
125
+ if (/^urn:li:organization:\d+$/i.test(s) || /^\d+$/.test(s)) {
126
+ parts.push(`id:${s}`);
127
+ }
128
+ else {
129
+ parts.push(`text:${encText(s)}`);
130
+ }
131
+ parts.push('selectionType:INCLUDED');
132
+ return valObj(parts);
133
+ });
134
+ pushFilter(f, 'CURRENT_COMPANY', values);
135
+ }
136
+ // COMPANY_TYPE — map to C,P,N,D,G when known; allow raw codes
137
+ const COMPANY_TYPE = {
138
+ PUBLIC: 'C',
139
+ PRIVATE: 'P',
140
+ NONPROFIT: 'N',
141
+ EDUCATIONAL: 'D',
142
+ GOVERNMENT: 'G',
143
+ };
144
+ if (filters?.company?.type?.include?.length) {
145
+ const values = filters.company.type.include.map((t) => {
146
+ const code = COMPANY_TYPE[t] ?? t; // allow raw letter code
147
+ return valObj([`id:${code}`, 'selectionType:INCLUDED']);
148
+ });
149
+ pushFilter(f, 'COMPANY_TYPE', values);
150
+ }
151
+ // YEARS_* via explicit bucket ids if provided (1..5)
152
+ const yr = filters?.personal;
153
+ if (yr?.years_at_company_ids?.length) {
154
+ pushFilter(f, 'YEARS_AT_CURRENT_COMPANY', yr.years_at_company_ids.map((id) => valObj([`id:${id}`, 'selectionType:INCLUDED'])));
155
+ }
156
+ if (yr?.years_in_current_role_ids?.length) {
157
+ pushFilter(f, 'YEARS_IN_CURRENT_POSITION', yr.years_in_current_role_ids.map((id) => valObj([`id:${id}`, 'selectionType:INCLUDED'])));
158
+ }
159
+ if (yr?.years_experience_ids?.length) {
160
+ pushFilter(f, 'YEARS_OF_EXPERIENCE', yr.years_experience_ids.map((id) => valObj([`id:${id}`, 'selectionType:INCLUDED'])));
161
+ }
162
+ const filtersPart = f.length ? `,filters:${list(f)}` : '';
163
+ return `(spellCorrectionEnabled:true,keywords:${encodedKw}${filtersPart})`;
164
+ }