linkedin-secret-sauce 0.11.0 → 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 +512 -232
- package/dist/enrichment/auth/smartlead-auth.d.ts +3 -3
- package/dist/enrichment/auth/smartlead-auth.js +25 -25
- package/dist/enrichment/index.d.ts +6 -4
- package/dist/enrichment/index.js +25 -24
- package/dist/enrichment/matching.d.ts +8 -3
- package/dist/enrichment/matching.js +7 -5
- package/dist/enrichment/orchestrator.js +44 -14
- package/dist/enrichment/providers/construct.js +72 -14
- package/dist/enrichment/providers/hunter.js +6 -60
- package/dist/enrichment/providers/index.d.ts +0 -1
- package/dist/enrichment/providers/index.js +1 -3
- package/dist/enrichment/providers/ldd.js +5 -47
- package/dist/enrichment/providers/smartprospect.js +9 -14
- package/dist/enrichment/types.d.ts +23 -24
- package/dist/enrichment/types.js +22 -21
- package/dist/enrichment/utils/http-retry.d.ts +96 -0
- package/dist/enrichment/utils/http-retry.js +162 -0
- package/dist/enrichment/verification/index.d.ts +1 -1
- package/dist/enrichment/verification/index.js +3 -1
- package/dist/enrichment/verification/mx.d.ts +33 -0
- package/dist/enrichment/verification/mx.js +367 -7
- package/dist/index.d.ts +196 -6
- package/dist/index.js +159 -12
- package/dist/parsers/search-parser.js +7 -3
- package/package.json +30 -22
package/README.md
CHANGED
|
@@ -1,277 +1,557 @@
|
|
|
1
|
-
# LinkedIn Secret Sauce
|
|
1
|
+
# LinkedIn Secret Sauce
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A complete LinkedIn Sales Navigator client + Email Enrichment library for Node.js/TypeScript.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
48
|
+
logLevel: 'info',
|
|
52
49
|
});
|
|
53
50
|
|
|
54
|
-
|
|
55
|
-
const
|
|
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
|
|
58
|
-
const
|
|
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
|
-
|
|
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
|
-
//
|
|
66
|
-
const
|
|
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
|
-
//
|
|
69
|
-
const sales: SalesNavigatorProfile = await getSalesNavigatorProfileDetails('urn:li:fs_salesProfile:ACwAAA...');
|
|
134
|
+
// Industries, titles, companies also supported
|
|
70
135
|
```
|
|
71
136
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
###
|
|
161
|
+
### Metrics & Monitoring
|
|
171
162
|
|
|
172
|
-
```
|
|
173
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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/)
|