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.
Files changed (44) hide show
  1. package/README.md +50 -21
  2. package/dist/cosiall-client.d.ts +1 -1
  3. package/dist/cosiall-client.js +1 -1
  4. package/dist/enrichment/index.d.ts +3 -3
  5. package/dist/enrichment/index.js +19 -2
  6. package/dist/enrichment/matching.d.ts +29 -9
  7. package/dist/enrichment/matching.js +545 -142
  8. package/dist/enrichment/providers/bounceban.d.ts +82 -0
  9. package/dist/enrichment/providers/bounceban.js +447 -0
  10. package/dist/enrichment/providers/bouncer.d.ts +1 -1
  11. package/dist/enrichment/providers/bouncer.js +19 -21
  12. package/dist/enrichment/providers/construct.d.ts +1 -1
  13. package/dist/enrichment/providers/construct.js +22 -38
  14. package/dist/enrichment/providers/cosiall.d.ts +27 -0
  15. package/dist/enrichment/providers/cosiall.js +109 -0
  16. package/dist/enrichment/providers/dropcontact.d.ts +15 -9
  17. package/dist/enrichment/providers/dropcontact.js +188 -19
  18. package/dist/enrichment/providers/hunter.d.ts +8 -1
  19. package/dist/enrichment/providers/hunter.js +52 -28
  20. package/dist/enrichment/providers/index.d.ts +10 -7
  21. package/dist/enrichment/providers/index.js +12 -1
  22. package/dist/enrichment/providers/ldd.d.ts +1 -10
  23. package/dist/enrichment/providers/ldd.js +20 -97
  24. package/dist/enrichment/providers/smartprospect.js +28 -48
  25. package/dist/enrichment/providers/snovio.d.ts +1 -1
  26. package/dist/enrichment/providers/snovio.js +29 -31
  27. package/dist/enrichment/providers/trykitt.d.ts +63 -0
  28. package/dist/enrichment/providers/trykitt.js +210 -0
  29. package/dist/enrichment/types.d.ts +234 -17
  30. package/dist/enrichment/types.js +60 -48
  31. package/dist/enrichment/utils/candidate-parser.d.ts +107 -0
  32. package/dist/enrichment/utils/candidate-parser.js +173 -0
  33. package/dist/enrichment/utils/noop-provider.d.ts +39 -0
  34. package/dist/enrichment/utils/noop-provider.js +37 -0
  35. package/dist/enrichment/utils/rate-limiter.d.ts +103 -0
  36. package/dist/enrichment/utils/rate-limiter.js +204 -0
  37. package/dist/enrichment/utils/validation.d.ts +75 -3
  38. package/dist/enrichment/utils/validation.js +164 -11
  39. package/dist/linkedin-api.d.ts +40 -1
  40. package/dist/linkedin-api.js +160 -27
  41. package/dist/types.d.ts +50 -1
  42. package/dist/utils/lru-cache.d.ts +105 -0
  43. package/dist/utils/lru-cache.js +175 -0
  44. package/package.json +25 -26
@@ -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
- // In-memory caches (per-process)
63
- const profileCacheByVanity = new Map();
64
- const profileCacheByUrn = new Map();
65
- const searchCache = new Map();
66
- // New caches for Phase 2
67
- const companyCache = new Map();
68
- const typeaheadCache = new Map();
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 = getCached(profileCacheByVanity, key, cfg.profileCacheTtl);
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, { data: prof, ts: Date.now() });
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 = getCached(profileCacheByUrn, cacheKey, cfg.profileCacheTtl);
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, { data: prof, ts: Date.now() });
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 = getCached(searchCache, cacheKey, cfg.searchCacheTtl);
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, { data: result, ts: Date.now() });
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 = getCached(companyCache, key, cfg.companyCacheTtl);
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, { data: parsed, ts: Date.now() });
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 = getCached(typeaheadCache, cacheKey, cfg.typeaheadCacheTtl);
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, { data: result, ts: Date.now() });
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:fsd_profile:ACoAABcdEfG") */
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.0",
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
+ }