linkedin-secret-sauce 0.12.0 → 0.12.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 +50 -21
- package/dist/cosiall-client.d.ts +1 -1
- package/dist/cosiall-client.js +1 -1
- package/dist/enrichment/index.d.ts +3 -3
- package/dist/enrichment/index.js +19 -2
- package/dist/enrichment/matching.d.ts +29 -9
- package/dist/enrichment/matching.js +545 -142
- package/dist/enrichment/providers/bounceban.d.ts +82 -0
- package/dist/enrichment/providers/bounceban.js +447 -0
- package/dist/enrichment/providers/bouncer.d.ts +1 -1
- package/dist/enrichment/providers/bouncer.js +19 -21
- package/dist/enrichment/providers/construct.d.ts +1 -1
- package/dist/enrichment/providers/construct.js +22 -38
- package/dist/enrichment/providers/cosiall.d.ts +27 -0
- package/dist/enrichment/providers/cosiall.js +109 -0
- package/dist/enrichment/providers/dropcontact.d.ts +15 -9
- package/dist/enrichment/providers/dropcontact.js +188 -19
- package/dist/enrichment/providers/hunter.d.ts +8 -1
- package/dist/enrichment/providers/hunter.js +52 -28
- package/dist/enrichment/providers/index.d.ts +10 -7
- package/dist/enrichment/providers/index.js +12 -1
- package/dist/enrichment/providers/ldd.d.ts +1 -10
- package/dist/enrichment/providers/ldd.js +20 -97
- package/dist/enrichment/providers/smartprospect.js +28 -48
- package/dist/enrichment/providers/snovio.d.ts +1 -1
- package/dist/enrichment/providers/snovio.js +29 -31
- package/dist/enrichment/providers/trykitt.d.ts +63 -0
- package/dist/enrichment/providers/trykitt.js +210 -0
- package/dist/enrichment/types.d.ts +234 -17
- package/dist/enrichment/types.js +60 -48
- package/dist/enrichment/utils/candidate-parser.d.ts +107 -0
- package/dist/enrichment/utils/candidate-parser.js +173 -0
- package/dist/enrichment/utils/noop-provider.d.ts +39 -0
- package/dist/enrichment/utils/noop-provider.js +37 -0
- package/dist/enrichment/utils/rate-limiter.d.ts +103 -0
- package/dist/enrichment/utils/rate-limiter.js +204 -0
- package/dist/enrichment/utils/validation.d.ts +75 -3
- package/dist/enrichment/utils/validation.js +164 -11
- package/dist/linkedin-api.d.ts +40 -1
- package/dist/linkedin-api.js +160 -27
- package/dist/types.d.ts +50 -1
- package/dist/utils/lru-cache.d.ts +105 -0
- package/dist/utils/lru-cache.js +175 -0
- package/package.json +25 -26
package/dist/linkedin-api.js
CHANGED
|
@@ -47,6 +47,8 @@ exports.getYearsAtCompanyOptions = getYearsAtCompanyOptions;
|
|
|
47
47
|
exports.getYearsInPositionOptions = getYearsInPositionOptions;
|
|
48
48
|
exports.getYearsOfExperienceOptions = getYearsOfExperienceOptions;
|
|
49
49
|
exports.getSalesNavigatorProfileDetails = getSalesNavigatorProfileDetails;
|
|
50
|
+
exports.getSalesNavigatorProfileFull = getSalesNavigatorProfileFull;
|
|
51
|
+
exports.extractLinkedInHandle = extractLinkedInHandle;
|
|
50
52
|
const config_1 = require("./config");
|
|
51
53
|
const http_client_1 = require("./http-client");
|
|
52
54
|
const profile_parser_1 = require("./parsers/profile-parser");
|
|
@@ -57,27 +59,23 @@ const metrics_1 = require("./utils/metrics");
|
|
|
57
59
|
const logger_1 = require("./utils/logger");
|
|
58
60
|
const errors_1 = require("./utils/errors");
|
|
59
61
|
const constants_1 = require("./constants");
|
|
62
|
+
const lru_cache_1 = require("./utils/lru-cache");
|
|
60
63
|
const LINKEDIN_API_BASE = "https://www.linkedin.com/voyager/api";
|
|
61
64
|
const SALES_NAV_BASE = "https://www.linkedin.com/sales-api";
|
|
62
|
-
//
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
65
|
+
// Cache size limits to prevent unbounded memory growth
|
|
66
|
+
const PROFILE_CACHE_MAX_SIZE = 1000;
|
|
67
|
+
const SEARCH_CACHE_MAX_SIZE = 500;
|
|
68
|
+
const COMPANY_CACHE_MAX_SIZE = 500;
|
|
69
|
+
const TYPEAHEAD_CACHE_MAX_SIZE = 200;
|
|
70
|
+
// In-memory LRU caches (per-process) with size limits
|
|
71
|
+
const profileCacheByVanity = new lru_cache_1.LRUCache(PROFILE_CACHE_MAX_SIZE);
|
|
72
|
+
const profileCacheByUrn = new lru_cache_1.LRUCache(PROFILE_CACHE_MAX_SIZE);
|
|
73
|
+
const searchCache = new lru_cache_1.LRUCache(SEARCH_CACHE_MAX_SIZE);
|
|
74
|
+
// Caches for Phase 2
|
|
75
|
+
const companyCache = new lru_cache_1.LRUCache(COMPANY_CACHE_MAX_SIZE);
|
|
76
|
+
const typeaheadCache = new lru_cache_1.LRUCache(TYPEAHEAD_CACHE_MAX_SIZE);
|
|
69
77
|
// In-flight dedupe for profiles by vanity
|
|
70
78
|
const inflightByVanity = new Map();
|
|
71
|
-
function getCached(map, key, ttl) {
|
|
72
|
-
const entry = map.get(key);
|
|
73
|
-
if (!entry)
|
|
74
|
-
return null;
|
|
75
|
-
if (ttl && ttl > 0 && Date.now() - entry.ts > ttl) {
|
|
76
|
-
map.delete(key);
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
return entry.data;
|
|
80
|
-
}
|
|
81
79
|
/**
|
|
82
80
|
* Fetches a LinkedIn profile by vanity URL (public identifier).
|
|
83
81
|
* Results are cached for the configured TTL (default: 15 minutes).
|
|
@@ -104,7 +102,7 @@ async function getProfileByVanity(vanity) {
|
|
|
104
102
|
}
|
|
105
103
|
catch { }
|
|
106
104
|
// Cache
|
|
107
|
-
const cached =
|
|
105
|
+
const cached = profileCacheByVanity.get(key, cfg.profileCacheTtl);
|
|
108
106
|
if (cached) {
|
|
109
107
|
(0, metrics_1.incrementMetric)("profileCacheHits");
|
|
110
108
|
return cached;
|
|
@@ -151,7 +149,7 @@ async function getProfileByVanity(vanity) {
|
|
|
151
149
|
catch { }
|
|
152
150
|
throw new errors_1.LinkedInClientError("Failed to parse profile", errors_1.ERROR_CODES.PARSE_ERROR, 500);
|
|
153
151
|
}
|
|
154
|
-
profileCacheByVanity.set(key,
|
|
152
|
+
profileCacheByVanity.set(key, prof);
|
|
155
153
|
(0, metrics_1.incrementMetric)("profileFetches");
|
|
156
154
|
try {
|
|
157
155
|
(0, logger_1.log)("info", "api.ok", {
|
|
@@ -210,7 +208,7 @@ async function getProfileByUrn(fsdKey) {
|
|
|
210
208
|
});
|
|
211
209
|
}
|
|
212
210
|
catch { }
|
|
213
|
-
const cachedUrn =
|
|
211
|
+
const cachedUrn = profileCacheByUrn.get(cacheKey, cfg.profileCacheTtl);
|
|
214
212
|
if (cachedUrn) {
|
|
215
213
|
(0, metrics_1.incrementMetric)("profileCacheHits");
|
|
216
214
|
return cachedUrn;
|
|
@@ -322,7 +320,7 @@ async function getProfileByUrn(fsdKey) {
|
|
|
322
320
|
catch { }
|
|
323
321
|
throw new errors_1.LinkedInClientError("Failed to parse profile", errors_1.ERROR_CODES.PARSE_ERROR, 500);
|
|
324
322
|
}
|
|
325
|
-
profileCacheByUrn.set(cacheKey,
|
|
323
|
+
profileCacheByUrn.set(cacheKey, prof);
|
|
326
324
|
(0, metrics_1.incrementMetric)("profileFetches");
|
|
327
325
|
try {
|
|
328
326
|
(0, logger_1.log)("info", "api.ok", { operation: "getProfileByUrn", selector: cacheKey });
|
|
@@ -390,7 +388,7 @@ async function searchSalesLeads(keywords, options) {
|
|
|
390
388
|
cacheKeyObj.sessionId = options.sessionId;
|
|
391
389
|
}
|
|
392
390
|
const cacheKey = JSON.stringify(cacheKeyObj);
|
|
393
|
-
const cached =
|
|
391
|
+
const cached = searchCache.get(cacheKey, cfg.searchCacheTtl);
|
|
394
392
|
if (cached) {
|
|
395
393
|
(0, metrics_1.incrementMetric)("searchCacheHits");
|
|
396
394
|
return cached;
|
|
@@ -466,7 +464,7 @@ async function searchSalesLeads(keywords, options) {
|
|
|
466
464
|
: undefined,
|
|
467
465
|
_meta: { sessionId }, // Return sessionId to consumer
|
|
468
466
|
};
|
|
469
|
-
searchCache.set(cacheKey,
|
|
467
|
+
searchCache.set(cacheKey, result);
|
|
470
468
|
(0, metrics_1.incrementMetric)("searchCacheMisses");
|
|
471
469
|
return result;
|
|
472
470
|
}
|
|
@@ -564,7 +562,7 @@ async function resolveCompanyUniversalName(universalName) {
|
|
|
564
562
|
async function getCompanyById(companyId) {
|
|
565
563
|
const cfg = (0, config_1.getConfig)();
|
|
566
564
|
const key = String(companyId || "").trim();
|
|
567
|
-
const cached =
|
|
565
|
+
const cached = companyCache.get(key, cfg.companyCacheTtl);
|
|
568
566
|
if (cached) {
|
|
569
567
|
(0, metrics_1.incrementMetric)("companyCacheHits");
|
|
570
568
|
return cached;
|
|
@@ -578,7 +576,7 @@ async function getCompanyById(companyId) {
|
|
|
578
576
|
const raw = await (0, http_client_1.executeLinkedInRequest)({ url }, "getCompanyById");
|
|
579
577
|
const parsed = (0, company_parser_1.parseCompany)(raw);
|
|
580
578
|
(0, metrics_1.incrementMetric)("companyFetches");
|
|
581
|
-
companyCache.set(key,
|
|
579
|
+
companyCache.set(key, parsed);
|
|
582
580
|
try {
|
|
583
581
|
(0, logger_1.log)("info", "api.ok", { operation: "getCompanyById", selector: key });
|
|
584
582
|
}
|
|
@@ -717,7 +715,7 @@ async function typeahead(options) {
|
|
|
717
715
|
}
|
|
718
716
|
// Make API call to LinkedIn for search queries or unsupported types
|
|
719
717
|
const cacheKey = JSON.stringify({ type, query, start, count });
|
|
720
|
-
const cached =
|
|
718
|
+
const cached = typeaheadCache.get(cacheKey, cfg.typeaheadCacheTtl);
|
|
721
719
|
if (cached) {
|
|
722
720
|
(0, metrics_1.incrementMetric)("typeaheadCacheHits");
|
|
723
721
|
return cached;
|
|
@@ -764,7 +762,7 @@ async function typeahead(options) {
|
|
|
764
762
|
total: paging2?.total,
|
|
765
763
|
},
|
|
766
764
|
};
|
|
767
|
-
typeaheadCache.set(cacheKey,
|
|
765
|
+
typeaheadCache.set(cacheKey, result);
|
|
768
766
|
try {
|
|
769
767
|
(0, logger_1.log)("info", "api.ok", {
|
|
770
768
|
operation: "typeahead",
|
|
@@ -920,3 +918,138 @@ async function getSalesNavigatorProfileDetails(profileUrnOrId) {
|
|
|
920
918
|
throw e;
|
|
921
919
|
}
|
|
922
920
|
}
|
|
921
|
+
/**
|
|
922
|
+
* Fetches full Sales Navigator profile data including flagshipProfileUrl (LinkedIn handle).
|
|
923
|
+
* This is a more complete version that returns all available profile data.
|
|
924
|
+
*
|
|
925
|
+
* The key use case is extracting the LinkedIn handle from flagshipProfileUrl
|
|
926
|
+
* for use with email finder services like Hunter.io.
|
|
927
|
+
*
|
|
928
|
+
* @param profileUrnOrId - Profile identifier in any of these formats:
|
|
929
|
+
* - Sales profile URN: "urn:li:fs_salesProfile:(ABC123xyz,NAME_SEARCH,abc)"
|
|
930
|
+
* - FSD profile URN: "urn:li:fsd_profile:ABC123xyz"
|
|
931
|
+
* - Bare key: "ABC123xyz"
|
|
932
|
+
* @returns Full Sales Navigator profile with flagshipProfileUrl, contactInfo, positions, etc.
|
|
933
|
+
* @throws LinkedInClientError with code NOT_FOUND if profile doesn't exist
|
|
934
|
+
*
|
|
935
|
+
* @example
|
|
936
|
+
* ```typescript
|
|
937
|
+
* const profile = await getSalesNavigatorProfileFull('urn:li:fs_salesProfile:(ABC123,NAME_SEARCH,xyz)');
|
|
938
|
+
* console.log(profile.flagshipProfileUrl); // "https://www.linkedin.com/in/john-doe"
|
|
939
|
+
*
|
|
940
|
+
* // Extract LinkedIn handle for Hunter.io
|
|
941
|
+
* const handle = extractLinkedInHandle(profile.flagshipProfileUrl);
|
|
942
|
+
* console.log(handle); // "john-doe"
|
|
943
|
+
* ```
|
|
944
|
+
*/
|
|
945
|
+
async function getSalesNavigatorProfileFull(profileUrnOrId) {
|
|
946
|
+
const idOrUrn = String(profileUrnOrId || "").trim();
|
|
947
|
+
// Build Sales API path supporting URN triple or explicit profileId/authType/authToken
|
|
948
|
+
let pathSeg = "";
|
|
949
|
+
let pidFromTriple = null;
|
|
950
|
+
// URN with triple
|
|
951
|
+
const mUrn = idOrUrn.match(/^urn:li:(?:fs_salesProfile|fsd_profile):\(([^,\s)]+),([^,\s)]+),([^,\s)]+)\)/i);
|
|
952
|
+
if (mUrn) {
|
|
953
|
+
const [_, pid, authType, authToken] = mUrn;
|
|
954
|
+
pathSeg = `(profileId:${pid},authType:${authType},authToken:${authToken})`;
|
|
955
|
+
pidFromTriple = pid;
|
|
956
|
+
}
|
|
957
|
+
else if (/profileId:/.test(idOrUrn) &&
|
|
958
|
+
/authType:/.test(idOrUrn) &&
|
|
959
|
+
/authToken:/.test(idOrUrn)) {
|
|
960
|
+
// Already the triple form without urn prefix
|
|
961
|
+
pathSeg = `(${idOrUrn.replace(/^\(|\)$/g, "")})`;
|
|
962
|
+
const m = idOrUrn.match(/profileId:([^,\s)]+)/);
|
|
963
|
+
pidFromTriple = m ? m[1] : null;
|
|
964
|
+
}
|
|
965
|
+
else {
|
|
966
|
+
// Fallback to simple id/urn
|
|
967
|
+
pathSeg = idOrUrn.startsWith("urn:")
|
|
968
|
+
? idOrUrn
|
|
969
|
+
: encodeURIComponent(idOrUrn);
|
|
970
|
+
}
|
|
971
|
+
// Decoration 2: Returns flagshipProfileUrl, contactInfo, summary, positions
|
|
972
|
+
// This is the key decoration for email enrichment - flagshipProfileUrl contains the LinkedIn handle
|
|
973
|
+
const decoration = encodeURIComponent("(entityUrn,objectUrn,firstName,lastName,fullName,headline,pronoun,degree,profileUnlockInfo,location,listCount,summary,savedLead,defaultPosition,contactInfo,crmStatus,pendingInvitation,unlocked,flagshipProfileUrl,positions*(companyName,current,new,description,endedOn,posId,startedOn,title,location,companyUrn~fs_salesCompany(entityUrn,name,companyPictureDisplayImage)))");
|
|
974
|
+
const base = `${SALES_NAV_BASE}/salesApiProfiles/`;
|
|
975
|
+
async function requestWith(seg) {
|
|
976
|
+
const url = `${base}${seg}?decoration=${decoration}`;
|
|
977
|
+
return (0, http_client_1.executeLinkedInRequest)({
|
|
978
|
+
url,
|
|
979
|
+
headers: { Referer: "https://www.linkedin.com/sales/search/people" },
|
|
980
|
+
}, "getSalesNavigatorProfileFull");
|
|
981
|
+
}
|
|
982
|
+
try {
|
|
983
|
+
try {
|
|
984
|
+
(0, logger_1.log)("info", "api.start", {
|
|
985
|
+
operation: "getSalesNavigatorProfileFull",
|
|
986
|
+
selector: idOrUrn,
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
catch { }
|
|
990
|
+
let raw;
|
|
991
|
+
try {
|
|
992
|
+
raw = await requestWith(pathSeg);
|
|
993
|
+
}
|
|
994
|
+
catch (e) {
|
|
995
|
+
const status = e?.status ?? 0;
|
|
996
|
+
if (status === 400 && pidFromTriple) {
|
|
997
|
+
// Fallback: some environments reject the triple form; retry with bare id
|
|
998
|
+
const fallbackSeg = encodeURIComponent(pidFromTriple);
|
|
999
|
+
try {
|
|
1000
|
+
(0, logger_1.log)("warn", "api.salesProfileFullFallback", {
|
|
1001
|
+
from: pathSeg,
|
|
1002
|
+
to: fallbackSeg,
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
catch { }
|
|
1006
|
+
raw = await requestWith(fallbackSeg);
|
|
1007
|
+
}
|
|
1008
|
+
else if (status === 404) {
|
|
1009
|
+
try {
|
|
1010
|
+
(0, logger_1.log)("warn", "api.notFound", {
|
|
1011
|
+
operation: "getSalesNavigatorProfileFull",
|
|
1012
|
+
selector: idOrUrn,
|
|
1013
|
+
status,
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
catch { }
|
|
1017
|
+
throw new errors_1.LinkedInClientError("Sales profile not found", errors_1.ERROR_CODES.NOT_FOUND, 404);
|
|
1018
|
+
}
|
|
1019
|
+
else {
|
|
1020
|
+
throw e;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
try {
|
|
1024
|
+
(0, logger_1.log)("info", "api.ok", {
|
|
1025
|
+
operation: "getSalesNavigatorProfileFull",
|
|
1026
|
+
selector: idOrUrn,
|
|
1027
|
+
hasHandle: !!raw.flagshipProfileUrl,
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
catch { }
|
|
1031
|
+
return raw;
|
|
1032
|
+
}
|
|
1033
|
+
catch (e) {
|
|
1034
|
+
throw e;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Extracts LinkedIn handle/vanity from a flagship profile URL.
|
|
1039
|
+
*
|
|
1040
|
+
* @param flagshipProfileUrl - Full LinkedIn profile URL (e.g., "https://www.linkedin.com/in/john-doe")
|
|
1041
|
+
* @returns LinkedIn handle/vanity (e.g., "john-doe") or null if not found
|
|
1042
|
+
*
|
|
1043
|
+
* @example
|
|
1044
|
+
* ```typescript
|
|
1045
|
+
* extractLinkedInHandle("https://www.linkedin.com/in/john-doe"); // "john-doe"
|
|
1046
|
+
* extractLinkedInHandle("https://www.linkedin.com/in/georgi-metodiev-tech2rec"); // "georgi-metodiev-tech2rec"
|
|
1047
|
+
* extractLinkedInHandle(null); // null
|
|
1048
|
+
* ```
|
|
1049
|
+
*/
|
|
1050
|
+
function extractLinkedInHandle(flagshipProfileUrl) {
|
|
1051
|
+
if (!flagshipProfileUrl)
|
|
1052
|
+
return null;
|
|
1053
|
+
const match = flagshipProfileUrl.match(/linkedin\.com\/in\/([^\/\?]+)/i);
|
|
1054
|
+
return match ? match[1] : null;
|
|
1055
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
export interface CosiallProfileEmailsResponse {
|
|
6
6
|
/** Cosiall internal profile ID */
|
|
7
7
|
profileId: number;
|
|
8
|
-
/** LinkedIn ObjectURN identifier (e.g., "urn:li:
|
|
8
|
+
/** LinkedIn ObjectURN identifier (e.g., "urn:li:member:129147375") */
|
|
9
9
|
objectUrn: string;
|
|
10
10
|
/** LinkedIn profile URL */
|
|
11
11
|
linkedInUrl: string;
|
|
@@ -233,6 +233,55 @@ export interface SalesNavigatorProfile {
|
|
|
233
233
|
imageUrl?: string;
|
|
234
234
|
flagshipProfileUrl?: string;
|
|
235
235
|
}
|
|
236
|
+
export interface SalesNavigatorPosition {
|
|
237
|
+
companyName?: string;
|
|
238
|
+
companyUrn?: string;
|
|
239
|
+
title?: string;
|
|
240
|
+
description?: string;
|
|
241
|
+
current?: boolean;
|
|
242
|
+
new?: boolean;
|
|
243
|
+
location?: string;
|
|
244
|
+
posId?: number;
|
|
245
|
+
startedOn?: {
|
|
246
|
+
year?: number;
|
|
247
|
+
month?: number;
|
|
248
|
+
};
|
|
249
|
+
endedOn?: {
|
|
250
|
+
year?: number;
|
|
251
|
+
month?: number;
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
export interface SalesNavigatorContactInfo {
|
|
255
|
+
emailAddresses?: string[];
|
|
256
|
+
phoneNumbers?: string[];
|
|
257
|
+
twitterHandles?: string[];
|
|
258
|
+
websites?: string[];
|
|
259
|
+
}
|
|
260
|
+
export interface SalesNavigatorProfileFull {
|
|
261
|
+
entityUrn?: string;
|
|
262
|
+
objectUrn?: string;
|
|
263
|
+
firstName?: string;
|
|
264
|
+
lastName?: string;
|
|
265
|
+
fullName?: string;
|
|
266
|
+
headline?: string;
|
|
267
|
+
summary?: string;
|
|
268
|
+
location?: string;
|
|
269
|
+
degree?: number;
|
|
270
|
+
pronoun?: string;
|
|
271
|
+
/** LinkedIn public profile URL - contains the vanity/handle (e.g., "https://www.linkedin.com/in/john-doe") */
|
|
272
|
+
flagshipProfileUrl?: string;
|
|
273
|
+
/** Contact info (emails, phones) - only available if connected */
|
|
274
|
+
contactInfo?: SalesNavigatorContactInfo;
|
|
275
|
+
/** Current and past positions */
|
|
276
|
+
positions?: SalesNavigatorPosition[];
|
|
277
|
+
defaultPosition?: SalesNavigatorPosition;
|
|
278
|
+
profileUnlockInfo?: unknown;
|
|
279
|
+
listCount?: number;
|
|
280
|
+
savedLead?: boolean;
|
|
281
|
+
crmStatus?: unknown;
|
|
282
|
+
pendingInvitation?: boolean;
|
|
283
|
+
unlocked?: boolean;
|
|
284
|
+
}
|
|
236
285
|
export type Geo = {
|
|
237
286
|
type: "region" | "country" | "state" | "city" | "postal";
|
|
238
287
|
value: string;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple LRU (Least Recently Used) Cache
|
|
3
|
+
*
|
|
4
|
+
* A memory-bounded cache that evicts the least recently used entries
|
|
5
|
+
* when the maximum size is exceeded. Prevents unbounded memory growth.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - O(1) get and set operations
|
|
9
|
+
* - Automatic eviction of stale entries
|
|
10
|
+
* - Optional TTL support
|
|
11
|
+
* - Size limit enforcement
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Cache entry with data and timestamp
|
|
15
|
+
*/
|
|
16
|
+
export interface CacheEntry<T> {
|
|
17
|
+
data: T;
|
|
18
|
+
ts: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* LRU Cache implementation using Map's insertion order
|
|
22
|
+
*
|
|
23
|
+
* JavaScript Map maintains insertion order, so we can implement LRU
|
|
24
|
+
* by deleting and re-inserting on access (moving to end).
|
|
25
|
+
*/
|
|
26
|
+
export declare class LRUCache<T> {
|
|
27
|
+
private cache;
|
|
28
|
+
private readonly maxSize;
|
|
29
|
+
private readonly ttlMs;
|
|
30
|
+
/**
|
|
31
|
+
* Create a new LRU cache
|
|
32
|
+
*
|
|
33
|
+
* @param maxSize - Maximum number of entries to store (default: 1000)
|
|
34
|
+
* @param ttlMs - Optional TTL in milliseconds (default: null = no expiry)
|
|
35
|
+
*/
|
|
36
|
+
constructor(maxSize?: number, ttlMs?: number | null);
|
|
37
|
+
/**
|
|
38
|
+
* Get a value from the cache
|
|
39
|
+
*
|
|
40
|
+
* @param key - Cache key
|
|
41
|
+
* @param ttlOverride - Optional TTL override in milliseconds (for dynamic TTL from config)
|
|
42
|
+
* @returns The cached value or null if not found/expired
|
|
43
|
+
*/
|
|
44
|
+
get(key: string, ttlOverride?: number): T | null;
|
|
45
|
+
/**
|
|
46
|
+
* Set a value in the cache
|
|
47
|
+
*
|
|
48
|
+
* @param key - Cache key
|
|
49
|
+
* @param data - Value to cache
|
|
50
|
+
*/
|
|
51
|
+
set(key: string, data: T): void;
|
|
52
|
+
/**
|
|
53
|
+
* Delete a key from the cache
|
|
54
|
+
*
|
|
55
|
+
* @param key - Cache key to delete
|
|
56
|
+
* @returns true if the key existed
|
|
57
|
+
*/
|
|
58
|
+
delete(key: string): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Check if a key exists in the cache (without updating access time)
|
|
61
|
+
*
|
|
62
|
+
* @param key - Cache key
|
|
63
|
+
* @returns true if key exists and is not expired
|
|
64
|
+
*/
|
|
65
|
+
has(key: string): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Clear all entries from the cache
|
|
68
|
+
*/
|
|
69
|
+
clear(): void;
|
|
70
|
+
/**
|
|
71
|
+
* Get the current number of entries in the cache
|
|
72
|
+
*/
|
|
73
|
+
get size(): number;
|
|
74
|
+
/**
|
|
75
|
+
* Get all keys in the cache (for debugging/testing)
|
|
76
|
+
*/
|
|
77
|
+
keys(): IterableIterator<string>;
|
|
78
|
+
/**
|
|
79
|
+
* Prune expired entries (useful for periodic cleanup)
|
|
80
|
+
*
|
|
81
|
+
* @returns Number of entries removed
|
|
82
|
+
*/
|
|
83
|
+
prune(): number;
|
|
84
|
+
/**
|
|
85
|
+
* Get cache statistics
|
|
86
|
+
*/
|
|
87
|
+
stats(): {
|
|
88
|
+
size: number;
|
|
89
|
+
maxSize: number;
|
|
90
|
+
ttlMs: number | null;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Create a new LRU cache with the specified options
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```typescript
|
|
98
|
+
* // Cache up to 500 profiles for 15 minutes
|
|
99
|
+
* const profileCache = createLRUCache<LinkedInProfile>(500, 15 * 60 * 1000);
|
|
100
|
+
*
|
|
101
|
+
* profileCache.set('johndoe', profile);
|
|
102
|
+
* const cached = profileCache.get('johndoe');
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export declare function createLRUCache<T>(maxSize?: number, ttlMs?: number | null): LRUCache<T>;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Simple LRU (Least Recently Used) Cache
|
|
4
|
+
*
|
|
5
|
+
* A memory-bounded cache that evicts the least recently used entries
|
|
6
|
+
* when the maximum size is exceeded. Prevents unbounded memory growth.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - O(1) get and set operations
|
|
10
|
+
* - Automatic eviction of stale entries
|
|
11
|
+
* - Optional TTL support
|
|
12
|
+
* - Size limit enforcement
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.LRUCache = void 0;
|
|
16
|
+
exports.createLRUCache = createLRUCache;
|
|
17
|
+
/**
|
|
18
|
+
* LRU Cache implementation using Map's insertion order
|
|
19
|
+
*
|
|
20
|
+
* JavaScript Map maintains insertion order, so we can implement LRU
|
|
21
|
+
* by deleting and re-inserting on access (moving to end).
|
|
22
|
+
*/
|
|
23
|
+
class LRUCache {
|
|
24
|
+
cache;
|
|
25
|
+
maxSize;
|
|
26
|
+
ttlMs;
|
|
27
|
+
/**
|
|
28
|
+
* Create a new LRU cache
|
|
29
|
+
*
|
|
30
|
+
* @param maxSize - Maximum number of entries to store (default: 1000)
|
|
31
|
+
* @param ttlMs - Optional TTL in milliseconds (default: null = no expiry)
|
|
32
|
+
*/
|
|
33
|
+
constructor(maxSize = 1000, ttlMs = null) {
|
|
34
|
+
this.cache = new Map();
|
|
35
|
+
this.maxSize = maxSize;
|
|
36
|
+
this.ttlMs = ttlMs;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Get a value from the cache
|
|
40
|
+
*
|
|
41
|
+
* @param key - Cache key
|
|
42
|
+
* @param ttlOverride - Optional TTL override in milliseconds (for dynamic TTL from config)
|
|
43
|
+
* @returns The cached value or null if not found/expired
|
|
44
|
+
*/
|
|
45
|
+
get(key, ttlOverride) {
|
|
46
|
+
const entry = this.cache.get(key);
|
|
47
|
+
if (!entry) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
// Check TTL expiration (use override if provided, otherwise instance TTL)
|
|
51
|
+
const effectiveTtl = ttlOverride ?? this.ttlMs;
|
|
52
|
+
if (effectiveTtl !== null && Date.now() - entry.ts > effectiveTtl) {
|
|
53
|
+
this.cache.delete(key);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
// Move to end (most recently used) by delete + set
|
|
57
|
+
this.cache.delete(key);
|
|
58
|
+
this.cache.set(key, entry);
|
|
59
|
+
return entry.data;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Set a value in the cache
|
|
63
|
+
*
|
|
64
|
+
* @param key - Cache key
|
|
65
|
+
* @param data - Value to cache
|
|
66
|
+
*/
|
|
67
|
+
set(key, data) {
|
|
68
|
+
// If key exists, delete it first (will be re-added at end)
|
|
69
|
+
if (this.cache.has(key)) {
|
|
70
|
+
this.cache.delete(key);
|
|
71
|
+
}
|
|
72
|
+
// Evict oldest entries if at capacity
|
|
73
|
+
while (this.cache.size >= this.maxSize) {
|
|
74
|
+
const oldestKey = this.cache.keys().next().value;
|
|
75
|
+
if (oldestKey !== undefined) {
|
|
76
|
+
this.cache.delete(oldestKey);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Add new entry
|
|
83
|
+
this.cache.set(key, { data, ts: Date.now() });
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Delete a key from the cache
|
|
87
|
+
*
|
|
88
|
+
* @param key - Cache key to delete
|
|
89
|
+
* @returns true if the key existed
|
|
90
|
+
*/
|
|
91
|
+
delete(key) {
|
|
92
|
+
return this.cache.delete(key);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Check if a key exists in the cache (without updating access time)
|
|
96
|
+
*
|
|
97
|
+
* @param key - Cache key
|
|
98
|
+
* @returns true if key exists and is not expired
|
|
99
|
+
*/
|
|
100
|
+
has(key) {
|
|
101
|
+
const entry = this.cache.get(key);
|
|
102
|
+
if (!entry) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
// Check TTL expiration
|
|
106
|
+
if (this.ttlMs !== null && Date.now() - entry.ts > this.ttlMs) {
|
|
107
|
+
this.cache.delete(key);
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Clear all entries from the cache
|
|
114
|
+
*/
|
|
115
|
+
clear() {
|
|
116
|
+
this.cache.clear();
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Get the current number of entries in the cache
|
|
120
|
+
*/
|
|
121
|
+
get size() {
|
|
122
|
+
return this.cache.size;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get all keys in the cache (for debugging/testing)
|
|
126
|
+
*/
|
|
127
|
+
keys() {
|
|
128
|
+
return this.cache.keys();
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Prune expired entries (useful for periodic cleanup)
|
|
132
|
+
*
|
|
133
|
+
* @returns Number of entries removed
|
|
134
|
+
*/
|
|
135
|
+
prune() {
|
|
136
|
+
if (this.ttlMs === null) {
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
let removed = 0;
|
|
141
|
+
for (const [key, entry] of this.cache) {
|
|
142
|
+
if (now - entry.ts > this.ttlMs) {
|
|
143
|
+
this.cache.delete(key);
|
|
144
|
+
removed++;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return removed;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Get cache statistics
|
|
151
|
+
*/
|
|
152
|
+
stats() {
|
|
153
|
+
return {
|
|
154
|
+
size: this.cache.size,
|
|
155
|
+
maxSize: this.maxSize,
|
|
156
|
+
ttlMs: this.ttlMs,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
exports.LRUCache = LRUCache;
|
|
161
|
+
/**
|
|
162
|
+
* Create a new LRU cache with the specified options
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```typescript
|
|
166
|
+
* // Cache up to 500 profiles for 15 minutes
|
|
167
|
+
* const profileCache = createLRUCache<LinkedInProfile>(500, 15 * 60 * 1000);
|
|
168
|
+
*
|
|
169
|
+
* profileCache.set('johndoe', profile);
|
|
170
|
+
* const cached = profileCache.get('johndoe');
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
function createLRUCache(maxSize = 1000, ttlMs = null) {
|
|
174
|
+
return new LRUCache(maxSize, ttlMs);
|
|
175
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linkedin-secret-sauce",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.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",
|
|
@@ -10,30 +10,6 @@
|
|
|
10
10
|
"publishConfig": {
|
|
11
11
|
"registry": "https://registry.npmjs.org/"
|
|
12
12
|
},
|
|
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",
|
|
17
|
-
"build": "tsc -p tsconfig.json",
|
|
18
|
-
"typecheck": "tsc --noEmit",
|
|
19
|
-
"typecheck:playground": "pnpm -C apps/playground typecheck",
|
|
20
|
-
"typecheck:all": "pnpm typecheck && pnpm typecheck:playground",
|
|
21
|
-
"lint": "eslint \"src/**/*.ts\" --max-warnings=0",
|
|
22
|
-
"lint:playground": "eslint \"apps/playground/src/**/*.{ts,tsx}\" \"apps/playground/server/**/*.ts\" --max-warnings=0",
|
|
23
|
-
"lint:all": "eslint \"src/**/*.ts\" \"apps/playground/src/**/*.{ts,tsx}\" \"apps/playground/server/**/*.ts\" --max-warnings=0",
|
|
24
|
-
"lint:fix": "eslint \"src/**/*.ts\" \"apps/playground/src/**/*.{ts,tsx}\" \"apps/playground/server/**/*.ts\" --fix",
|
|
25
|
-
"check": "pnpm typecheck:all && pnpm lint:all",
|
|
26
|
-
"dev": "pnpm dev:playground",
|
|
27
|
-
"dev:watch": "tsc -w -p tsconfig.json",
|
|
28
|
-
"test": "vitest run",
|
|
29
|
-
"docs": "typedoc",
|
|
30
|
-
"docs:netlify": "rm -rf docs/api && typedoc && echo 'Build complete'",
|
|
31
|
-
"docs:watch": "typedoc --watch",
|
|
32
|
-
"prepublishOnly": "npm run build",
|
|
33
|
-
"release:patch": "npm version patch && git push --follow-tags",
|
|
34
|
-
"release:minor": "npm version minor && git push --follow-tags",
|
|
35
|
-
"release:major": "npm version major && git push --follow-tags"
|
|
36
|
-
},
|
|
37
13
|
"keywords": [
|
|
38
14
|
"linkedin",
|
|
39
15
|
"sales-navigator",
|
|
@@ -66,5 +42,28 @@
|
|
|
66
42
|
"typedoc-plugin-markdown": "^4.9.0",
|
|
67
43
|
"typescript": "^5.9.3",
|
|
68
44
|
"vitest": "^1.6.0"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"dev:playground": "pnpm -C apps/playground dev",
|
|
48
|
+
"search": "node scripts/rg-fast.mjs",
|
|
49
|
+
"rg:fast": "node scripts/rg-fast.mjs",
|
|
50
|
+
"build": "tsc -p tsconfig.json",
|
|
51
|
+
"typecheck": "tsc --noEmit",
|
|
52
|
+
"typecheck:playground": "pnpm -C apps/playground typecheck",
|
|
53
|
+
"typecheck:all": "pnpm typecheck && pnpm typecheck:playground",
|
|
54
|
+
"lint": "eslint \"src/**/*.ts\" --max-warnings=0",
|
|
55
|
+
"lint:playground": "eslint \"apps/playground/src/**/*.{ts,tsx}\" \"apps/playground/server/**/*.ts\" --max-warnings=0",
|
|
56
|
+
"lint:all": "eslint \"src/**/*.ts\" \"apps/playground/src/**/*.{ts,tsx}\" \"apps/playground/server/**/*.ts\" --max-warnings=0",
|
|
57
|
+
"lint:fix": "eslint \"src/**/*.ts\" \"apps/playground/src/**/*.{ts,tsx}\" \"apps/playground/server/**/*.ts\" --fix",
|
|
58
|
+
"check": "pnpm typecheck:all && pnpm lint:all",
|
|
59
|
+
"dev": "pnpm dev:playground",
|
|
60
|
+
"dev:watch": "tsc -w -p tsconfig.json",
|
|
61
|
+
"test": "vitest run",
|
|
62
|
+
"docs": "typedoc",
|
|
63
|
+
"docs:netlify": "rm -rf docs/api && typedoc && echo 'Build complete'",
|
|
64
|
+
"docs:watch": "typedoc --watch",
|
|
65
|
+
"release:patch": "npm version patch && git push --follow-tags",
|
|
66
|
+
"release:minor": "npm version minor && git push --follow-tags",
|
|
67
|
+
"release:major": "npm version major && git push --follow-tags"
|
|
69
68
|
}
|
|
70
|
-
}
|
|
69
|
+
}
|