linkedin-secret-sauce 0.10.1 → 0.11.1

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 CHANGED
@@ -1,277 +1,557 @@
1
- # LinkedIn Secret Sauce Client
1
+ # LinkedIn Secret Sauce
2
2
 
3
- Private, server-side LinkedIn Sales Navigator client. It manages cookies/accounts, retries, caching, and parsing so your app calls a small, typed API.
3
+ A complete LinkedIn Sales Navigator client + Email Enrichment library for Node.js/TypeScript.
4
4
 
5
- For a step‑by‑step integration guide you can copy/paste into other apps, see `docs/INTEGRATION.md`.
5
+ **Two main modules:**
6
+ 1. **LinkedIn API** - Sales Navigator search, profiles, companies with automatic cookie/account rotation
7
+ 2. **Email Enrichment** - Find business emails via multiple providers (Hunter, Apollo, Bouncer, Snov.io, etc.)
6
8
 
7
- Advanced lead search
8
- - Strongly‑typed filters: see `docs/SALES_SEARCH.md` (seniority, titles with id or text, functions, industries, languages, regions, company headcount/type, years buckets, current company).
9
- - Relationship filters are intentionally not used (results are customer‑agnostic).
10
- - Past company is not emitted — only CURRENT_COMPANY is supported.
11
- - Power users: pass a full `rawQuery` string to `searchSalesLeads` if you already have the Sales Navigator `query=(...)` payload.
9
+ ## Documentation
12
10
 
13
- - Node: >= 18 (uses global `fetch`)
14
- - Build: `pnpm build`
15
- - Test: `pnpm test`
11
+ - [Full API Documentation](https://enerage.github.io/LinkedInSecretSauce/) - Auto-generated TypeDoc
12
+ - [Integration Guide](docs/INTEGRATION.md) - Step-by-step setup
13
+ - [Sales Search Guide](docs/SALES_SEARCH.md) - Advanced search filters
14
+ - [Playground Guide](docs/PLAYGROUND.md) - Local dev UI
16
15
 
17
- ## Install
16
+ ## Installation
18
17
 
19
18
  ```bash
20
- # pnpm (recommended)
21
19
  pnpm add linkedin-secret-sauce
22
-
23
- # npm
20
+ # or
24
21
  npm install linkedin-secret-sauce
25
22
  ```
26
23
 
27
- ## Quick Start
24
+ **Requirements:** Node.js >= 18 (uses native fetch)
25
+
26
+ ---
27
+
28
+ ## Module 1: LinkedIn API
29
+
30
+ Server-side LinkedIn Sales Navigator client with automatic cookie management, retries, caching, and parsing.
28
31
 
29
- ```ts
32
+ ### Quick Start
33
+
34
+ ```typescript
30
35
  import {
31
36
  initializeLinkedInClient,
32
37
  getProfileByVanity,
33
- getProfileByUrn,
34
38
  searchSalesLeads,
35
- resolveCompanyUniversalName,
36
39
  getCompanyById,
37
- getCompanyByUrl,
38
40
  typeahead,
39
- getSalesNavigatorProfileDetails,
40
- type LinkedInProfile,
41
- type SearchSalesResult,
42
- type Company,
43
- type TypeaheadResult,
44
- type SalesNavigatorProfile,
45
41
  } from 'linkedin-secret-sauce';
46
42
 
43
+ // Initialize once at app startup
47
44
  initializeLinkedInClient({
48
45
  cosiallApiUrl: process.env.COSIALL_API_URL!,
49
46
  cosiallApiKey: process.env.COSIALL_API_KEY!,
50
47
  proxyString: process.env.LINKEDIN_PROXY_STRING, // optional
51
- logLevel: (process.env.LOG_LEVEL as any) || 'info',
48
+ logLevel: 'info',
52
49
  });
53
50
 
54
- const prof: LinkedInProfile = await getProfileByVanity('john-doe');
55
- const prof2 = await getProfileByUrn('urn:li:fsd_profile:ACwAAAbCdEf'); // URN or bare key
51
+ // Fetch a profile
52
+ const profile = await getProfileByVanity('john-doe');
53
+ console.log(profile.firstName, profile.lastName);
54
+ console.log(profile.positions); // Work history
56
55
 
57
- // Search with paging; uses LeadSearchResult-14 for complete profile data (positions, badges, etc.)
58
- const leads: SearchSalesResult = await searchSalesLeads('cto fintech', { start: 50, count: 10 });
56
+ // Search Sales Navigator
57
+ const results = await searchSalesLeads('cto fintech', { start: 0, count: 25 });
58
+ for (const lead of results.items) {
59
+ console.log(lead.fullName, lead.title, lead.company);
60
+ }
61
+
62
+ // Get company details
63
+ const company = await getCompanyById(1234567);
64
+ console.log(company.name, company.industry, company.employeeCount);
65
+ ```
66
+
67
+ ### Configuration
68
+
69
+ | Variable | Required | Description |
70
+ |----------|----------|-------------|
71
+ | `COSIALL_API_URL` | Yes | Base URL of your Cosiall cookie service |
72
+ | `COSIALL_API_KEY` | Yes | API key for Cosiall |
73
+ | `LINKEDIN_PROXY_STRING` | No | Proxy in format `host:port` or `host:port:user:pass` |
74
+ | `LOG_LEVEL` | No | `debug`, `info`, `warn`, `error` (default: `info`) |
75
+
76
+ ### API Reference
77
+
78
+ #### Profiles
79
+ ```typescript
80
+ // By vanity URL (username)
81
+ const profile = await getProfileByVanity('john-doe');
82
+
83
+ // By URN or key
84
+ const profile = await getProfileByUrn('urn:li:fsd_profile:ACwAAAbCdEf');
85
+ const profile = await getProfileByUrn('ACwAAAbCdEf'); // Just the key works too
86
+
87
+ // Batch fetch (parallel with concurrency control)
88
+ const profiles = await getProfilesBatch(['john-doe', 'jane-smith'], 5);
89
+ ```
59
90
 
60
- // Companies
91
+ #### Search
92
+ ```typescript
93
+ // Basic keyword search
94
+ const results = await searchSalesLeads('software engineer');
95
+
96
+ // With pagination
97
+ const results = await searchSalesLeads('cto fintech', {
98
+ start: 0, // Offset
99
+ count: 25 // Results per page (max 100)
100
+ });
101
+
102
+ // Access results
103
+ results.items.forEach(lead => {
104
+ console.log(lead.fullName);
105
+ console.log(lead.title);
106
+ console.log(lead.company);
107
+ console.log(lead.linkedInUrl);
108
+ console.log(lead.fsdKey); // Use for detailed profile fetch
109
+ });
110
+ ```
111
+
112
+ #### Companies
113
+ ```typescript
114
+ // Resolve company name to ID
61
115
  const { companyId } = await resolveCompanyUniversalName('microsoft');
62
- const company: Company = await getCompanyById(companyId!);
63
- const sameCompany = await getCompanyByUrl('https://www.linkedin.com/company/microsoft/');
64
116
 
65
- // Typeahead (facet suggestions)
66
- const ta: TypeaheadResult = await typeahead({ type: 'BING_GEO', query: 'new', start: 0, count: 10 });
117
+ // Get company details
118
+ const company = await getCompanyById(companyId);
119
+ console.log(company.name, company.website, company.employeeCount);
120
+
121
+ // From LinkedIn URL
122
+ const company = await getCompanyByUrl('https://linkedin.com/company/microsoft/');
123
+ ```
124
+
125
+ #### Typeahead (Autocomplete)
126
+ ```typescript
127
+ // Geographic locations
128
+ const locations = await typeahead({
129
+ type: 'BING_GEO',
130
+ query: 'new york',
131
+ count: 10
132
+ });
67
133
 
68
- // Sales Navigator profile details
69
- const sales: SalesNavigatorProfile = await getSalesNavigatorProfileDetails('urn:li:fs_salesProfile:ACwAAA...');
134
+ // Industries, titles, companies also supported
70
135
  ```
71
136
 
72
- ## Environment & Config
73
-
74
- Required
75
- - `COSIALL_API_URL` base URL of your Cosiall service (e.g., https://cosiall.example)
76
- - `COSIALL_API_KEY` – API key for the Cosiall endpoint
77
-
78
- Optional
79
- - `LINKEDIN_PROXY_STRING` `host:port` or `host:port:user:pass`
80
- - `LOG_LEVEL` – `debug` | `info` | `warn` | `error` (default `info`)
81
-
82
- Config defaults (override in code as needed)
83
- - `profileCacheTtl`: 900_000 ms (15m)
84
- - `searchCacheTtl`: 180_000 ms (3m)
85
- - `companyCacheTtl`: 600_000 ms (10m)
86
- - `typeaheadCacheTtl`: 3_600_000 ms (1h)
87
- - `cookieRefreshInterval`: 900_000 ms (15m)
88
- - `cookieFreshnessWindow`: 86_400_000 ms (24h)
89
- - `maxRetries`: 2 (5xx retry on same account)
90
- - `retryDelayMs`: 700 ms
91
- - `accountCooldownMs`: 5_000 ms
92
- - `maxFailuresBeforeCooldown`: 3
93
- - `maxRequestHistory`: 500
94
-
95
- ## Public API (overview)
96
-
97
- - Profiles
98
- - `getProfileByVanity(vanity)` – cached 15m, in‑flight de‑dupe per key.
99
- - `getProfileByUrn(keyOrUrn)` – accepts `ACwAA...` or URNs; normalizes to key.
100
- - `getProfilesBatch(vanities, concurrency)` – returns array aligned to inputs; `null` for failures.
101
-
102
- - Search
103
- - `searchSalesLeads(keywords, { start, count, decorationId })` – when options are provided returns `{ items, page }`; uses LeadSearchResult-14 by default for complete profile data.
104
-
105
- - Companies
106
- - `resolveCompanyUniversalName(universalName)` → `{ companyId? }` (404 → NOT_FOUND)
107
- - `getCompanyById(companyId)` → `Company` (10m cache)
108
- - `getCompanyByUrl(url)` – parses numeric id or slug, then fetches
109
-
110
- - Typeahead
111
- - `typeahead({ type, query, start, count })` → `TypeaheadResult` (1h cache)
112
-
113
- - Sales Navigator profile
114
- - `getSalesNavigatorProfileDetails(idOrUrn)` → `SalesNavigatorProfile` (404 → NOT_FOUND)
115
-
116
- ## Errors
117
-
118
- All errors are `LinkedInClientError` with fields `{ code, status?, accountId? }`.
119
-
120
- Error codes (non‑exhaustive)
121
- - `NOT_INITIALIZED`, `NO_ACCOUNTS`, `NO_VALID_ACCOUNTS`
122
- - `MISSING_CSRF`, `REQUEST_FAILED`, `ALL_ACCOUNTS_FAILED`, `PARSE_ERROR`
123
- - `INVALID_INPUT`, `NOT_FOUND`, `AUTH_ERROR`, `RATE_LIMITED`
124
-
125
- ## Caching, Metrics, Proxy
126
-
127
- - Per‑process in‑memory caches with TTLs listed above. Expired entries are evicted on access.
128
- - In‑flight de‑dupe for `getProfileByVanity`.
129
- - HTTP client rotates accounts on 401/403/429 and retries 5xx on same account (`maxRetries`).
130
- - Metrics snapshot: `import { getSnapshot }` for counters (requests, retries, cache hits, etc.).
131
- - Request history: `import { getRequestHistory, clearRequestHistory }` for a bounded in‑memory log.
132
- - Proxy: set `proxyString` in config (formats `host:port` or `host:port:user:pass`).
133
-
134
- ### Request History and Metrics (example)
135
-
136
- ```ts
137
- import { getRequestHistory, clearRequestHistory, getSnapshot } from 'linkedin-secret-sauce';
138
-
139
- clearRequestHistory();
140
- // ... make some API calls ...
141
- console.log(getRequestHistory()[0]);
142
- // {
143
- // timestamp: 1730000000000,
144
- // operation: 'getProfileByVanity',
145
- // selector: 'https://www.linkedin.com/voyager/api/...profiles?q=memberIdentity&memberIdentity=john-doe',
146
- // status: 200,
147
- // durationMs: 123,
148
- // accountId: 'acc1'
149
- // }
150
-
151
- console.log(getSnapshot());
152
- // {
153
- // profileFetches: 12,
154
- // profileCacheHits: 34,
155
- // inflightDedupeHits: 2,
156
- // searchCacheHits: 5,
157
- // typeaheadCacheHits: 3,
158
- // companyCacheHits: 4,
159
- // httpRetries: 3,
160
- // httpSuccess: 21,
161
- // httpFailures: 2,
162
- // companyFetches: 4,
163
- // typeaheadRequests: 5,
164
- // requestHistorySize: 42,
165
- // authErrors: 1,
166
- // ...
167
- // }
137
+ ### Error Handling
138
+
139
+ ```typescript
140
+ import { LinkedInClientError } from 'linkedin-secret-sauce';
141
+
142
+ try {
143
+ const profile = await getProfileByVanity('nonexistent');
144
+ } catch (error) {
145
+ if (error instanceof LinkedInClientError) {
146
+ switch (error.code) {
147
+ case 'NOT_FOUND':
148
+ console.log('Profile does not exist');
149
+ break;
150
+ case 'RATE_LIMITED':
151
+ console.log('Too many requests, wait before retrying');
152
+ break;
153
+ case 'AUTH_ERROR':
154
+ console.log('Cookie/session expired');
155
+ break;
156
+ }
157
+ }
158
+ }
168
159
  ```
169
160
 
170
- ### Accounts Summary
161
+ ### Metrics & Monitoring
171
162
 
172
- ```ts
173
- import { getAccountsSummary } from 'linkedin-secret-sauce';
163
+ ```typescript
164
+ import {
165
+ getSnapshot,
166
+ getRequestHistory,
167
+ getAccountsSummary
168
+ } from 'linkedin-secret-sauce';
174
169
 
170
+ // Performance metrics
171
+ const metrics = getSnapshot();
172
+ console.log(metrics.profileFetches);
173
+ console.log(metrics.httpSuccess);
174
+ console.log(metrics.cacheHits);
175
+
176
+ // Request log (bounded, for debugging)
177
+ const history = getRequestHistory();
178
+
179
+ // Account health status
175
180
  const accounts = getAccountsSummary();
176
- // [
177
- // {
178
- // accountId: 'acc1',
179
- // healthy: false,
180
- // failures: 1,
181
- // cooldownUntil: 1730005000000,
182
- // cooldownRemainingMs: 4200,
183
- // expiresAt: 1730010000, // epoch seconds (if provided by Cosiall)
184
- // lastUsedAt: 1730004000123
185
- // },
186
- // {
187
- // accountId: 'acc2',
188
- // healthy: true,
189
- // failures: 0,
190
- // cooldownUntil: 0,
191
- // cooldownRemainingMs: 0,
192
- // expiresAt: 1730010000,
193
- // lastUsedAt: 1730003999000
194
- // }
195
- // ]
181
+ accounts.forEach(acc => {
182
+ console.log(`${acc.accountId}: healthy=${acc.healthy}`);
183
+ });
196
184
  ```
197
185
 
198
- ## Testing
186
+ ---
187
+
188
+ ## Module 2: Email Enrichment
189
+
190
+ Find and verify business emails using multiple providers with waterfall/parallel strategies.
191
+
192
+ ### Supported Providers
193
+
194
+ | Provider | Type | Cost | Best For |
195
+ |----------|------|------|----------|
196
+ | **Construct** | Pattern + MX | FREE | Quick pattern matching |
197
+ | **LDD** | Database | FREE | LinkedIn profile emails |
198
+ | **Bouncer** | SMTP Verify | $0.006/email | Catch-all detection, 99%+ accuracy |
199
+ | **Snov.io** | Email Finder | $0.02/email | Finding emails by name+domain |
200
+ | **Hunter** | Email Finder | $0.005/email | Domain search, email finder |
201
+ | **Apollo** | Database | FREE | B2B contact database |
202
+ | **SmartProspect** | Database | $0.01/email | Lead database search |
203
+
204
+ ### Quick Start
205
+
206
+ ```typescript
207
+ import { createEnrichmentClient } from 'linkedin-secret-sauce';
208
+
209
+ const enrichment = createEnrichmentClient({
210
+ providers: {
211
+ // FREE providers (no API key needed)
212
+ construct: {},
213
+
214
+ // Paid providers (add your API keys)
215
+ hunter: { apiKey: process.env.HUNTER_API_KEY },
216
+ apollo: { apiKey: process.env.APOLLO_API_KEY },
217
+ bouncer: { apiKey: process.env.BOUNCER_API_KEY },
218
+ snovio: {
219
+ clientId: process.env.SNOVIO_CLIENT_ID,
220
+ clientSecret: process.env.SNOVIO_CLIENT_SECRET
221
+ },
222
+
223
+ // Your private LinkedIn data dump
224
+ ldd: {
225
+ apiUrl: process.env.LDD_API_URL,
226
+ apiToken: process.env.LDD_API_TOKEN
227
+ },
228
+
229
+ // SmartLead (supports credentials or direct token)
230
+ smartprospect: {
231
+ email: process.env.SMARTLEAD_EMAIL,
232
+ password: process.env.SMARTLEAD_PASSWORD,
233
+ },
234
+ },
235
+
236
+ // Optional: cost tracking callback
237
+ onCost: (provider, cost) => {
238
+ console.log(`${provider} cost: $${cost}`);
239
+ },
240
+ });
241
+
242
+ // Find business email for a person
243
+ const result = await enrichment.enrich({
244
+ firstName: 'John',
245
+ lastName: 'Doe',
246
+ company: 'Acme Corp',
247
+ domain: 'acme.com', // Optional but improves accuracy
248
+ linkedinUrl: 'linkedin.com/in/johndoe', // For LDD lookups
249
+ });
250
+
251
+ console.log(result.business_email); // john.doe@acme.com
252
+ console.log(result.business_email_source); // 'hunter'
253
+ console.log(result.business_email_verified); // true
254
+ console.log(result.business_email_confidence); // 95
255
+ ```
256
+
257
+ ### Batch Processing
258
+
259
+ ```typescript
260
+ // Process multiple candidates efficiently
261
+ const candidates = [
262
+ { firstName: 'John', lastName: 'Doe', domain: 'acme.com' },
263
+ { firstName: 'Jane', lastName: 'Smith', domain: 'techcorp.io' },
264
+ // ... hundreds more
265
+ ];
266
+
267
+ const results = await enrichment.enrichBatch(candidates, {
268
+ batchSize: 50, // Process 50 at a time
269
+ delayMs: 200, // Delay between batches (rate limiting)
270
+ });
271
+
272
+ results.forEach(r => {
273
+ console.log(`${r.candidate.firstName}: ${r.business_email}`);
274
+ });
275
+ ```
276
+
277
+ ### Get ALL Emails (Multi-Provider)
278
+
279
+ ```typescript
280
+ // Get emails from ALL providers instead of stopping at first match
281
+ const result = await enrichment.enrichAll({
282
+ firstName: 'John',
283
+ lastName: 'Doe',
284
+ domain: 'acme.com',
285
+ });
286
+
287
+ // Returns all found emails sorted by confidence
288
+ result.emails.forEach(email => {
289
+ console.log(email.email); // john.doe@acme.com
290
+ console.log(email.source); // 'hunter'
291
+ console.log(email.confidence); // 95
292
+ console.log(email.verified); // true
293
+ console.log(email.type); // 'business'
294
+ });
295
+
296
+ console.log(result.totalCost); // Total $ spent across providers
297
+ console.log(result.providersQueried); // ['construct', 'hunter', 'apollo']
298
+ ```
299
+
300
+ ### Email Verification with Bouncer
301
+
302
+ ```typescript
303
+ import {
304
+ verifyEmailWithBouncer,
305
+ checkCatchAllDomain,
306
+ verifyEmailsBatch
307
+ } from 'linkedin-secret-sauce';
308
+
309
+ // Verify a single email
310
+ const verification = await verifyEmailWithBouncer(
311
+ 'john@example.com',
312
+ { apiKey: process.env.BOUNCER_API_KEY }
313
+ );
314
+
315
+ console.log(verification.status); // 'deliverable' | 'undeliverable' | 'risky'
316
+ console.log(verification.reason); // 'accepted_email' | 'rejected_email' | etc
317
+ console.log(verification.acceptAll); // true = catch-all domain (can't verify)
318
+ console.log(verification.disposable); // Is this a temp email?
319
+ console.log(verification.role); // Is this info@, support@, etc?
320
+
321
+ // Check if domain is catch-all (accepts any email)
322
+ const isCatchAll = await checkCatchAllDomain('example.com', config);
323
+ if (isCatchAll) {
324
+ console.log('Cannot verify emails - domain accepts everything');
325
+ }
326
+
327
+ // Batch verify (with concurrency control)
328
+ const results = await verifyEmailsBatch(
329
+ ['john@example.com', 'jane@example.com'],
330
+ { apiKey: process.env.BOUNCER_API_KEY }
331
+ );
332
+ ```
333
+
334
+ ### Email Finding with Snov.io
335
+
336
+ ```typescript
337
+ import {
338
+ findEmailsWithSnovio,
339
+ verifyEmailWithSnovio
340
+ } from 'linkedin-secret-sauce';
341
+
342
+ // Find emails for a person
343
+ const emails = await findEmailsWithSnovio(
344
+ 'John',
345
+ 'Doe',
346
+ 'acme.com',
347
+ {
348
+ clientId: process.env.SNOVIO_CLIENT_ID,
349
+ clientSecret: process.env.SNOVIO_CLIENT_SECRET
350
+ }
351
+ );
352
+
353
+ emails?.emails.forEach(e => {
354
+ console.log(e.email, e.emailStatus);
355
+ });
356
+ ```
357
+
358
+ ### Email Validation Utilities
359
+
360
+ ```typescript
361
+ import {
362
+ isValidEmailSyntax,
363
+ isPersonalEmail,
364
+ isBusinessEmail,
365
+ isDisposableEmail,
366
+ isRoleAccount,
367
+ PERSONAL_DOMAINS,
368
+ DISPOSABLE_DOMAINS,
369
+ } from 'linkedin-secret-sauce';
370
+
371
+ // Syntax validation
372
+ isValidEmailSyntax('john@example.com'); // true
373
+ isValidEmailSyntax('invalid'); // false
374
+
375
+ // Domain classification
376
+ isPersonalEmail('john@gmail.com'); // true (Gmail, Yahoo, etc.)
377
+ isBusinessEmail('john@acme.com'); // true (not personal domain)
378
+ isDisposableEmail('x@mailinator.com'); // true (temp email service)
379
+
380
+ // Role account detection (info@, support@, etc.)
381
+ isRoleAccount('info@company.com'); // true
382
+ isRoleAccount('john@company.com'); // false
383
+
384
+ // Access domain lists directly
385
+ console.log(PERSONAL_DOMAINS.has('gmail.com')); // true
386
+ console.log(DISPOSABLE_DOMAINS.size); // 500+ domains
387
+ ```
388
+
389
+ ### Custom Provider Order
390
+
391
+ ```typescript
392
+ const enrichment = createEnrichmentClient({
393
+ providers: { /* ... */ },
394
+ options: {
395
+ // Custom order - cheaper/faster providers first
396
+ providerOrder: [
397
+ 'construct', // FREE - pattern matching
398
+ 'ldd', // FREE - your database
399
+ 'apollo', // FREE - limited quota
400
+ 'hunter', // $0.005
401
+ 'bouncer', // $0.006 (verification only)
402
+ 'smartprospect', // $0.01
403
+ 'snovio', // $0.02
404
+ ],
405
+
406
+ // Stop after finding one verified email
407
+ stopOnFirstVerified: true,
408
+
409
+ // Minimum confidence to accept
410
+ minConfidence: 70,
411
+ },
412
+ });
413
+ ```
414
+
415
+ ### Caching
416
+
417
+ ```typescript
418
+ const enrichment = createEnrichmentClient({
419
+ providers: { /* ... */ },
420
+
421
+ // Optional cache implementation
422
+ cache: {
423
+ async get(key: string) {
424
+ return redis.get(key);
425
+ },
426
+ async set(key: string, value: any, ttlMs?: number) {
427
+ await redis.set(key, value, 'PX', ttlMs || 86400000);
428
+ },
429
+ },
430
+ });
431
+ ```
432
+
433
+ ---
434
+
435
+ ## Combined Workflow Example
436
+
437
+ Here's a complete example: Search LinkedIn -> Enrich with emails:
438
+
439
+ ```typescript
440
+ import {
441
+ initializeLinkedInClient,
442
+ searchSalesLeads,
443
+ getProfileByUrn,
444
+ createEnrichmentClient,
445
+ } from 'linkedin-secret-sauce';
446
+
447
+ // Initialize LinkedIn client
448
+ initializeLinkedInClient({
449
+ cosiallApiUrl: process.env.COSIALL_API_URL!,
450
+ cosiallApiKey: process.env.COSIALL_API_KEY!,
451
+ });
452
+
453
+ // Initialize enrichment client
454
+ const enrichment = createEnrichmentClient({
455
+ providers: {
456
+ construct: {},
457
+ hunter: { apiKey: process.env.HUNTER_API_KEY },
458
+ bouncer: { apiKey: process.env.BOUNCER_API_KEY },
459
+ },
460
+ });
461
+
462
+ async function findLeadsWithEmails(query: string, limit: number) {
463
+ // Step 1: Search LinkedIn
464
+ const searchResults = await searchSalesLeads(query, { count: limit });
465
+
466
+ const enrichedLeads = [];
467
+
468
+ for (const lead of searchResults.items) {
469
+ // Step 2: Get full profile (optional, for more data)
470
+ const profile = lead.fsdKey
471
+ ? await getProfileByUrn(lead.fsdKey)
472
+ : null;
473
+
474
+ // Step 3: Find business email
475
+ const emailResult = await enrichment.enrich({
476
+ firstName: lead.firstName,
477
+ lastName: lead.lastName,
478
+ company: lead.company,
479
+ domain: profile?.company?.domain, // If available
480
+ linkedinUrl: lead.linkedInUrl,
481
+ });
482
+
483
+ enrichedLeads.push({
484
+ name: lead.fullName,
485
+ title: lead.title,
486
+ company: lead.company,
487
+ linkedIn: lead.linkedInUrl,
488
+ email: emailResult.business_email,
489
+ emailVerified: emailResult.business_email_verified,
490
+ });
491
+ }
492
+
493
+ return enrichedLeads;
494
+ }
495
+
496
+ // Usage
497
+ const leads = await findLeadsWithEmails('cto fintech san francisco', 50);
498
+ console.table(leads);
499
+ ```
500
+
501
+ ---
502
+
503
+ ## Development
199
504
 
200
505
  ```bash
506
+ # Install dependencies
507
+ pnpm install
508
+
509
+ # Build
510
+ pnpm build
511
+
512
+ # Run tests
201
513
  pnpm test
202
- pnpm exec tsc -p tsconfig.json --noEmit
514
+
515
+ # Type check
516
+ pnpm typecheck
517
+
518
+ # Lint
519
+ pnpm lint
520
+
521
+ # Generate documentation
522
+ pnpm docs
523
+ ```
524
+
525
+ ### Playground (Dev UI)
526
+
527
+ ```bash
528
+ pnpm dev:playground
529
+ ```
530
+
531
+ Opens a React UI at http://localhost:5173 to test the API interactively.
532
+
533
+ ---
534
+
535
+ ## Publishing
536
+
537
+ ```bash
538
+ # Bump version
539
+ npm version patch # or minor/major
540
+
541
+ # Publish to npm
542
+ npm publish
543
+
544
+ # Push tags
545
+ git push --follow-tags
203
546
  ```
204
547
 
205
- ## Manual Publish (npm)
206
-
207
- - Pre-checks:
208
- - Working tree is clean; tests pass; `pnpm build` produced `dist/`.
209
- - `publishConfig.registry` points to `https://registry.npmjs.org/` (already set).
210
- - No repo `.npmrc` that overrides registry to GitHub Packages.
211
-
212
- - Steps (local):
213
- 1) `npm login` (once per machine; ensure correct org/user)
214
- 2) `npm whoami` (sanity check)
215
- 3) Bump version: `npm version patch|minor|major`
216
- 4) Publish: `npm publish`
217
- 5) Push tags: `git push --follow-tags`
218
-
219
- Notes:
220
- - If your npm account enforces 2FA for publish, you’ll be prompted for an OTP.
221
- - For scoped packages that should be public, add `--access public` (not needed here because the package name is unscoped).
222
- - The “Publish your first package” button on GitHub targets GitHub Packages (`npm publish --registry=https://npm.pkg.github.com/`), not npmjs.com.
223
-
224
- ## Changelog
225
-
226
- See [CHANGELOG.md](CHANGELOG.md) for release notes and upgrade guides.
227
-
228
- **Latest:** v0.3.10 - Fixed `fsdKey` extraction from Sales Navigator URNs
229
-
230
- ## Known Issues & Solutions
231
-
232
- ### Sales Navigator Search Results
233
-
234
- **Issue:** `fsdKey` field not populated in search results (fixed in v0.3.10)
235
-
236
- If you're on an older version, you may need to manually extract `fsdKey`:
237
- ```typescript
238
- const results = await searchSalesLeads('query');
239
- // For older versions (<0.3.10), fsdKey might be missing
240
- // Solution: Update to v0.3.10+
241
- ```
242
-
243
- **Upgrade:**
244
- ```bash
245
- npm update linkedin-secret-sauce
246
- # or
247
- pnpm update linkedin-secret-sauce
248
- ```
249
-
250
- After upgrading to v0.3.10+, `fsdKey` will be correctly populated from URNs like:
251
- ```
252
- urn:li:fs_salesProfile:(ACwAAAAM3xgBU8jQ8zPMpR_WswldHDbBxoOu6p8,NAME_SEARCH,g2em)
253
- ```
254
-
255
- ### Profile Enrichment Pattern
256
-
257
- For best results, use this two-step pattern:
258
-
259
- ```typescript
260
- // Step 1: Search (lightweight, returns many results quickly)
261
- const searchResults = await searchSalesLeads('software engineer berlin');
262
-
263
- // Step 2: Enrich selected results (slower, returns full data)
264
- for (const result of searchResults.items) {
265
- if (result.fsdKey) {
266
- const fullProfile = await getProfileByUrn(result.fsdKey);
267
- // Now you have positions[] and educations[] arrays
268
- console.log(`${fullProfile.firstName} has ${fullProfile.positions.length} jobs`);
269
- }
270
- }
271
- ```
272
-
273
-
274
- ## Playground (React + Vite)\n\nDev-only UI to exercise the library safely via a local server.\n\n- Start both server and client: \n - pnpm dev:playground\n- Or separately: \n - pnpm -C apps/playground run server (http://localhost:5175)\n - pnpm -C apps/playground run client (http://localhost:5173)\n\nThe server initializes the library and exposes endpoints for profiles, companies, typeahead, sales search, and sales profile.\nMetrics and request history are exposed at /api/metrics and /api/history.\n\nBy default the server uses a dev stub for cookies at /api/flexiq/linkedin-cookies/all.\nTo use your Cosiall backend, set env vars: COSIALL_API_URL and COSIALL_API_KEY and remove the stub in pps/playground/server/index.ts.\n
275
- ## Playground (local UI)
276
-
277
- See `docs/PLAYGROUND.md` for usage, ports, and tips.
548
+ ---
549
+
550
+ ## License
551
+
552
+ UNLICENSED - Private package
553
+
554
+ ## Support
555
+
556
+ - Issues: [GitHub Issues](https://github.com/enerage/LinkedInSecretSauce/issues)
557
+ - Docs: [API Documentation](https://enerage.github.io/LinkedInSecretSauce/)