linkedin-secret-sauce 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +193 -60
- package/dist/config.d.ts +3 -0
- package/dist/config.js +7 -2
- package/dist/cookie-pool.d.ts +11 -0
- package/dist/cookie-pool.js +57 -4
- package/dist/cosiall-client.js +20 -5
- package/dist/http-client.d.ts +1 -1
- package/dist/http-client.js +120 -14
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -1
- package/dist/linkedin-api.d.ts +22 -3
- package/dist/linkedin-api.js +380 -32
- package/dist/parsers/company-parser.d.ts +2 -0
- package/dist/parsers/company-parser.js +30 -0
- package/dist/parsers/image-parser.d.ts +10 -0
- package/dist/parsers/image-parser.js +16 -0
- package/dist/parsers/profile-parser.d.ts +1 -1
- package/dist/parsers/profile-parser.js +70 -25
- package/dist/parsers/search-parser.d.ts +1 -1
- package/dist/parsers/search-parser.js +14 -4
- package/dist/types.d.ts +147 -0
- package/dist/utils/errors.d.ts +4 -0
- package/dist/utils/errors.js +5 -0
- package/dist/utils/linkedin-config.d.ts +43 -0
- package/dist/utils/linkedin-config.js +77 -0
- package/dist/utils/logger.d.ts +1 -1
- package/dist/utils/metrics.d.ts +9 -1
- package/dist/utils/metrics.js +16 -2
- package/dist/utils/request-history.d.ts +12 -0
- package/dist/utils/request-history.js +45 -0
- package/dist/utils/search-encoder.d.ts +4 -0
- package/dist/utils/search-encoder.js +164 -0
- package/package.json +59 -44
package/README.md
CHANGED
|
@@ -1,95 +1,228 @@
|
|
|
1
|
-
LinkedIn Secret Sauce Client
|
|
1
|
+
# LinkedIn Secret Sauce Client
|
|
2
2
|
|
|
3
|
-
Private LinkedIn Sales Navigator client
|
|
3
|
+
Private, server-side LinkedIn Sales Navigator client. It manages cookies/accounts, retries, caching, and parsing so your app calls a small, typed API.
|
|
4
|
+
|
|
5
|
+
For a step‑by‑step integration guide you can copy/paste into other apps, see `docs/INTEGRATION.md`.
|
|
6
|
+
|
|
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.
|
|
4
12
|
|
|
5
13
|
- Node: >= 18 (uses global `fetch`)
|
|
6
|
-
- Build: `pnpm build`
|
|
7
|
-
- Test: `pnpm test`
|
|
14
|
+
- Build: `pnpm build`
|
|
15
|
+
- Test: `pnpm test`
|
|
8
16
|
|
|
9
17
|
## Install
|
|
10
18
|
|
|
11
|
-
From npm registry (recommended):
|
|
12
|
-
|
|
13
19
|
```bash
|
|
14
|
-
# pnpm
|
|
20
|
+
# pnpm (recommended)
|
|
15
21
|
pnpm add linkedin-secret-sauce
|
|
16
22
|
|
|
17
|
-
#
|
|
23
|
+
# npm
|
|
24
|
+
npm install linkedin-secret-sauce
|
|
18
25
|
```
|
|
19
26
|
|
|
20
|
-
|
|
27
|
+
## Quick Start
|
|
21
28
|
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
```ts
|
|
30
|
+
import {
|
|
31
|
+
initializeLinkedInClient,
|
|
32
|
+
getProfileByVanity,
|
|
33
|
+
getProfileByUrn,
|
|
34
|
+
searchSalesLeads,
|
|
35
|
+
resolveCompanyUniversalName,
|
|
36
|
+
getCompanyById,
|
|
37
|
+
getCompanyByUrl,
|
|
38
|
+
typeahead,
|
|
39
|
+
getSalesNavigatorProfileDetails,
|
|
40
|
+
type LinkedInProfile,
|
|
41
|
+
type SearchSalesResult,
|
|
42
|
+
type Company,
|
|
43
|
+
type TypeaheadResult,
|
|
44
|
+
type SalesNavigatorProfile,
|
|
45
|
+
} from 'linkedin-secret-sauce';
|
|
25
46
|
|
|
26
|
-
|
|
47
|
+
initializeLinkedInClient({
|
|
48
|
+
cosiallApiUrl: process.env.COSIALL_API_URL!,
|
|
49
|
+
cosiallApiKey: process.env.COSIALL_API_KEY!,
|
|
50
|
+
proxyString: process.env.LINKEDIN_PROXY_STRING, // optional
|
|
51
|
+
logLevel: (process.env.LOG_LEVEL as any) || 'info',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const prof: LinkedInProfile = await getProfileByVanity('john-doe');
|
|
55
|
+
const prof2 = await getProfileByUrn('urn:li:fsd_profile:ACwAAAbCdEf'); // URN or bare key
|
|
56
|
+
|
|
57
|
+
// Search with paging; on HTTP 400, decoration -14 falls back to -17
|
|
58
|
+
const leads: SearchSalesResult = await searchSalesLeads('cto fintech', { start: 50, count: 10 });
|
|
59
|
+
|
|
60
|
+
// Companies
|
|
61
|
+
const { companyId } = await resolveCompanyUniversalName('microsoft');
|
|
62
|
+
const company: Company = await getCompanyById(companyId!);
|
|
63
|
+
const sameCompany = await getCompanyByUrl('https://www.linkedin.com/company/microsoft/');
|
|
64
|
+
|
|
65
|
+
// Typeahead (facet suggestions)
|
|
66
|
+
const ta: TypeaheadResult = await typeahead({ type: 'BING_GEO', query: 'new', start: 0, count: 10 });
|
|
27
67
|
|
|
28
|
-
|
|
68
|
+
// Sales Navigator profile details
|
|
69
|
+
const sales: SalesNavigatorProfile = await getSalesNavigatorProfileDetails('urn:li:fs_salesProfile:ACwAAA...');
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Environment & Config
|
|
29
73
|
|
|
30
74
|
Required
|
|
31
|
-
- `
|
|
32
|
-
- `
|
|
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
|
|
33
77
|
|
|
34
78
|
Optional
|
|
35
|
-
- `
|
|
36
|
-
- `
|
|
79
|
+
- `LINKEDIN_PROXY_STRING` – `host:port` or `host:port:user:pass`
|
|
80
|
+
- `LOG_LEVEL` – `debug` | `info` | `warn` | `error` (default `info`)
|
|
37
81
|
|
|
38
|
-
|
|
39
|
-
- `profileCacheTtl`:
|
|
40
|
-
- `searchCacheTtl`:
|
|
41
|
-
- `
|
|
42
|
-
- `
|
|
43
|
-
- `
|
|
44
|
-
- `
|
|
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
|
|
45
94
|
|
|
46
|
-
##
|
|
95
|
+
## Public API (overview)
|
|
47
96
|
|
|
48
|
-
|
|
49
|
-
|
|
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.
|
|
50
101
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
cosiallApiKey: process.env.COSIALL_API_KEY!,
|
|
54
|
-
proxyString: process.env.LINKEDIN_PROXY_STRING, // optional
|
|
55
|
-
logLevel: (process.env.LOG_LEVEL as any) || 'info',
|
|
56
|
-
});
|
|
57
|
-
```
|
|
102
|
+
- Search
|
|
103
|
+
- `searchSalesLeads(keywords, { start, count, decorationId })` – when options are provided returns `{ items, page }`; on HTTP 400 with `-14` decoration automatically retries with `-17`.
|
|
58
104
|
|
|
59
|
-
|
|
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
|
|
60
109
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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`
|
|
68
124
|
|
|
69
|
-
|
|
70
|
-
const profile: LinkedInProfile = await getProfileByVanity('john-doe');
|
|
125
|
+
## Caching, Metrics, Proxy
|
|
71
126
|
|
|
72
|
-
|
|
73
|
-
|
|
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`).
|
|
74
133
|
|
|
75
|
-
|
|
76
|
-
|
|
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
|
+
// }
|
|
77
168
|
```
|
|
78
169
|
|
|
79
|
-
|
|
80
|
-
- The package manages cookies via your Cosiall endpoint; no DB needed.
|
|
81
|
-
- In-memory caches reset on process restart (safe: cookies refresh automatically).
|
|
82
|
-
- Logs are JSON and respect `logLevel`; sensitive fields (e.g., `cosiallApiKey`) are masked.
|
|
170
|
+
### Accounts Summary
|
|
83
171
|
|
|
84
|
-
|
|
172
|
+
```ts
|
|
173
|
+
import { getAccountsSummary } from 'linkedin-secret-sauce';
|
|
174
|
+
|
|
175
|
+
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
|
+
// ]
|
|
196
|
+
```
|
|
85
197
|
|
|
86
|
-
|
|
87
|
-
# build JS + .d.ts to dist/
|
|
88
|
-
pnpm build
|
|
198
|
+
## Testing
|
|
89
199
|
|
|
90
|
-
|
|
200
|
+
```bash
|
|
91
201
|
pnpm test
|
|
92
|
-
|
|
93
|
-
# typecheck only (no emit)
|
|
94
202
|
pnpm exec tsc -p tsconfig.json --noEmit
|
|
95
203
|
```
|
|
204
|
+
|
|
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
|
+
|
|
225
|
+
## 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
|
|
226
|
+
## Playground (local UI)
|
|
227
|
+
|
|
228
|
+
See `docs/PLAYGROUND.md` for usage, ports, and tips.
|
package/dist/config.d.ts
CHANGED
|
@@ -5,12 +5,15 @@ export interface LinkedInClientConfig {
|
|
|
5
5
|
logLevel?: 'debug' | 'info' | 'warn' | 'error';
|
|
6
6
|
profileCacheTtl?: number;
|
|
7
7
|
searchCacheTtl?: number;
|
|
8
|
+
companyCacheTtl?: number;
|
|
9
|
+
typeaheadCacheTtl?: number;
|
|
8
10
|
cookieRefreshInterval?: number;
|
|
9
11
|
cookieFreshnessWindow?: number;
|
|
10
12
|
maxRetries?: number;
|
|
11
13
|
retryDelayMs?: number;
|
|
12
14
|
accountCooldownMs?: number;
|
|
13
15
|
maxFailuresBeforeCooldown?: number;
|
|
16
|
+
maxRequestHistory?: number;
|
|
14
17
|
}
|
|
15
18
|
export declare function initializeLinkedInClient(config: LinkedInClientConfig): void;
|
|
16
19
|
export declare function getConfig(): LinkedInClientConfig;
|
package/dist/config.js
CHANGED
|
@@ -11,7 +11,6 @@ function initializeLinkedInClient(config) {
|
|
|
11
11
|
}
|
|
12
12
|
// Validate URL format
|
|
13
13
|
try {
|
|
14
|
-
// eslint-disable-next-line no-new
|
|
15
14
|
new URL(config.cosiallApiUrl);
|
|
16
15
|
}
|
|
17
16
|
catch {
|
|
@@ -21,6 +20,8 @@ function initializeLinkedInClient(config) {
|
|
|
21
20
|
const withDefaults = {
|
|
22
21
|
profileCacheTtl: 15 * 60 * 1000,
|
|
23
22
|
searchCacheTtl: 3 * 60 * 1000,
|
|
23
|
+
companyCacheTtl: 10 * 60 * 1000, // 600_000 ms
|
|
24
|
+
typeaheadCacheTtl: 60 * 60 * 1000, // 3_600_000 ms
|
|
24
25
|
cookieRefreshInterval: 15 * 60 * 1000,
|
|
25
26
|
cookieFreshnessWindow: 24 * 60 * 60 * 1000,
|
|
26
27
|
maxRetries: 2,
|
|
@@ -28,6 +29,7 @@ function initializeLinkedInClient(config) {
|
|
|
28
29
|
accountCooldownMs: 5000,
|
|
29
30
|
maxFailuresBeforeCooldown: 3,
|
|
30
31
|
logLevel: 'info',
|
|
32
|
+
maxRequestHistory: 500,
|
|
31
33
|
...config,
|
|
32
34
|
};
|
|
33
35
|
// Optionally mask API key from accidental JSON serialization
|
|
@@ -42,7 +44,10 @@ function initializeLinkedInClient(config) {
|
|
|
42
44
|
catch {
|
|
43
45
|
// ignore if defineProperty fails in exotic environments
|
|
44
46
|
}
|
|
45
|
-
|
|
47
|
+
// Idempotent: only set once; subsequent calls are no-ops
|
|
48
|
+
if (!globalConfig) {
|
|
49
|
+
globalConfig = withDefaults;
|
|
50
|
+
}
|
|
46
51
|
}
|
|
47
52
|
function getConfig() {
|
|
48
53
|
if (!globalConfig) {
|
package/dist/cookie-pool.d.ts
CHANGED
|
@@ -3,7 +3,18 @@ export declare function selectAccountForRequest(): Promise<{
|
|
|
3
3
|
accountId: string;
|
|
4
4
|
cookies: LinkedInCookie[];
|
|
5
5
|
}>;
|
|
6
|
+
export declare function getAccountsSummary(): Array<{
|
|
7
|
+
accountId: string;
|
|
8
|
+
healthy: boolean;
|
|
9
|
+
failures: number;
|
|
10
|
+
cooldownUntil: number;
|
|
11
|
+
cooldownRemainingMs: number;
|
|
12
|
+
expiresAt?: number;
|
|
13
|
+
lastUsedAt?: number;
|
|
14
|
+
}>;
|
|
6
15
|
export declare function reportAccountFailure(accountId: string, isAuthError: boolean): void;
|
|
7
16
|
export declare function reportAccountSuccess(accountId: string): void;
|
|
8
17
|
export declare function buildCookieHeader(cookies: LinkedInCookie[]): string;
|
|
9
18
|
export declare function extractCsrfToken(cookies: LinkedInCookie[]): string;
|
|
19
|
+
export declare function adminSetCooldown(accountId: string, ms: number): void;
|
|
20
|
+
export declare function adminResetAccount(accountId: string): void;
|
package/dist/cookie-pool.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.selectAccountForRequest = selectAccountForRequest;
|
|
4
|
+
exports.getAccountsSummary = getAccountsSummary;
|
|
4
5
|
exports.reportAccountFailure = reportAccountFailure;
|
|
5
6
|
exports.reportAccountSuccess = reportAccountSuccess;
|
|
6
7
|
exports.buildCookieHeader = buildCookieHeader;
|
|
7
8
|
exports.extractCsrfToken = extractCsrfToken;
|
|
9
|
+
exports.adminSetCooldown = adminSetCooldown;
|
|
10
|
+
exports.adminResetAccount = adminResetAccount;
|
|
8
11
|
const cosiall_client_1 = require("./cosiall-client");
|
|
9
12
|
const config_1 = require("./config");
|
|
10
13
|
const errors_1 = require("./utils/errors");
|
|
@@ -23,11 +26,21 @@ function nowMs() {
|
|
|
23
26
|
function nowSec() {
|
|
24
27
|
return Math.floor(Date.now() / 1000);
|
|
25
28
|
}
|
|
29
|
+
function toEpochSeconds(v) {
|
|
30
|
+
if (typeof v !== 'number')
|
|
31
|
+
return undefined;
|
|
32
|
+
if (!Number.isFinite(v) || v <= 0)
|
|
33
|
+
return undefined;
|
|
34
|
+
// Treat large values as ms
|
|
35
|
+
return v > 1e10 ? Math.floor(v / 1000) : Math.floor(v);
|
|
36
|
+
}
|
|
26
37
|
function isExpired(entry) {
|
|
27
|
-
const jsession = entry.cookies.find(c => c.name.toUpperCase() === 'JSESSIONID');
|
|
28
|
-
|
|
38
|
+
const jsession = entry.cookies.find(c => String(c.name).toUpperCase() === 'JSESSIONID');
|
|
39
|
+
const jsExp = toEpochSeconds(jsession?.expires);
|
|
40
|
+
if (jsExp && jsExp < nowSec())
|
|
29
41
|
return true;
|
|
30
|
-
|
|
42
|
+
const accExp = toEpochSeconds(entry.expiresAt);
|
|
43
|
+
if (accExp && accExp < nowSec())
|
|
31
44
|
return true;
|
|
32
45
|
return false;
|
|
33
46
|
}
|
|
@@ -44,6 +57,7 @@ async function ensureInitialized() {
|
|
|
44
57
|
expiresAt: acc.expiresAt,
|
|
45
58
|
failures: 0,
|
|
46
59
|
cooldownUntil: 0,
|
|
60
|
+
lastUsedAt: undefined,
|
|
47
61
|
});
|
|
48
62
|
poolState.order.push(acc.accountId);
|
|
49
63
|
}
|
|
@@ -66,6 +80,7 @@ async function ensureInitialized() {
|
|
|
66
80
|
expiresAt: acc.expiresAt,
|
|
67
81
|
failures: poolState.accounts.get(acc.accountId)?.failures ?? 0,
|
|
68
82
|
cooldownUntil: poolState.accounts.get(acc.accountId)?.cooldownUntil ?? 0,
|
|
83
|
+
lastUsedAt: poolState.accounts.get(acc.accountId)?.lastUsedAt,
|
|
69
84
|
});
|
|
70
85
|
newOrder.push(acc.accountId);
|
|
71
86
|
}
|
|
@@ -74,7 +89,8 @@ async function ensureInitialized() {
|
|
|
74
89
|
(0, logger_1.log)('info', 'cookiePool.refreshed', { count: refreshed.length });
|
|
75
90
|
}
|
|
76
91
|
catch (e) {
|
|
77
|
-
|
|
92
|
+
const err = e;
|
|
93
|
+
(0, logger_1.log)('warn', 'cookiePool.refreshFailed', { error: err?.message });
|
|
78
94
|
}
|
|
79
95
|
}, interval);
|
|
80
96
|
}
|
|
@@ -99,10 +115,33 @@ async function selectAccountForRequest() {
|
|
|
99
115
|
poolState.rrIndex = (idx + 1) % n;
|
|
100
116
|
(0, logger_1.log)('debug', 'cookiePool.select', { accountId: entry.accountId, rrIndex: poolState.rrIndex });
|
|
101
117
|
(0, metrics_1.incrementMetric)('accountSelections');
|
|
118
|
+
entry.lastUsedAt = nowMs();
|
|
102
119
|
return { accountId: entry.accountId, cookies: entry.cookies };
|
|
103
120
|
}
|
|
104
121
|
throw new errors_1.LinkedInClientError('No valid LinkedIn accounts', 'NO_VALID_ACCOUNTS', 503);
|
|
105
122
|
}
|
|
123
|
+
function getAccountsSummary() {
|
|
124
|
+
const now = nowMs();
|
|
125
|
+
const out = [];
|
|
126
|
+
for (const id of poolState.order) {
|
|
127
|
+
const entry = poolState.accounts.get(id);
|
|
128
|
+
if (!entry)
|
|
129
|
+
continue;
|
|
130
|
+
const expired = isExpired(entry);
|
|
131
|
+
const cooling = entry.cooldownUntil > now;
|
|
132
|
+
const healthy = !expired && !cooling && entry.failures === 0;
|
|
133
|
+
out.push({
|
|
134
|
+
accountId: entry.accountId,
|
|
135
|
+
healthy,
|
|
136
|
+
failures: entry.failures,
|
|
137
|
+
cooldownUntil: entry.cooldownUntil,
|
|
138
|
+
cooldownRemainingMs: Math.max(0, entry.cooldownUntil - now),
|
|
139
|
+
expiresAt: entry.expiresAt,
|
|
140
|
+
lastUsedAt: entry.lastUsedAt,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
106
145
|
function reportAccountFailure(accountId, isAuthError) {
|
|
107
146
|
const entry = poolState.accounts.get(accountId);
|
|
108
147
|
if (!entry)
|
|
@@ -145,3 +184,17 @@ function getSettings() {
|
|
|
145
184
|
return { cooldownMs: 5000, maxFailures: 3 };
|
|
146
185
|
}
|
|
147
186
|
}
|
|
187
|
+
// Admin/dev helpers (no-ops if account missing). Safe to export for playground tooling.
|
|
188
|
+
function adminSetCooldown(accountId, ms) {
|
|
189
|
+
const entry = poolState.accounts.get(accountId);
|
|
190
|
+
if (!entry)
|
|
191
|
+
return;
|
|
192
|
+
entry.cooldownUntil = nowMs() + Math.max(0, Number(ms || 0));
|
|
193
|
+
}
|
|
194
|
+
function adminResetAccount(accountId) {
|
|
195
|
+
const entry = poolState.accounts.get(accountId);
|
|
196
|
+
if (!entry)
|
|
197
|
+
return;
|
|
198
|
+
entry.failures = 0;
|
|
199
|
+
entry.cooldownUntil = 0;
|
|
200
|
+
}
|
package/dist/cosiall-client.js
CHANGED
|
@@ -29,9 +29,24 @@ async function fetchCookiesFromCosiall() {
|
|
|
29
29
|
}
|
|
30
30
|
(0, logger_1.log)('info', 'cosiall.fetch.success', { count: data.length });
|
|
31
31
|
(0, metrics_1.incrementMetric)('cosiallSuccess');
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
function isCookie(obj) {
|
|
33
|
+
return !!obj && typeof obj === 'object' && 'name' in obj && 'value' in obj;
|
|
34
|
+
}
|
|
35
|
+
function isItem(obj) {
|
|
36
|
+
if (!obj || typeof obj !== 'object')
|
|
37
|
+
return false;
|
|
38
|
+
const rec = obj;
|
|
39
|
+
if (typeof rec.accountId !== 'string')
|
|
40
|
+
return false;
|
|
41
|
+
if (!Array.isArray(rec.cookies))
|
|
42
|
+
return false;
|
|
43
|
+
if (!rec.cookies.every(isCookie))
|
|
44
|
+
return false;
|
|
45
|
+
if (rec.expiresAt !== undefined && typeof rec.expiresAt !== 'number')
|
|
46
|
+
return false;
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
return data
|
|
50
|
+
.filter(isItem)
|
|
51
|
+
.map((item) => ({ accountId: item.accountId, cookies: item.cookies, expiresAt: item.expiresAt }));
|
|
37
52
|
}
|
package/dist/http-client.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export interface LinkedInRequestOptions {
|
|
2
2
|
url: string;
|
|
3
3
|
method?: 'GET' | 'POST';
|
|
4
|
-
body?:
|
|
4
|
+
body?: unknown;
|
|
5
5
|
headers?: Record<string, string>;
|
|
6
6
|
}
|
|
7
7
|
export declare function executeLinkedInRequest<T>(options: LinkedInRequestOptions, _operationName: string): Promise<T>;
|