indiecrm-cli 0.1.0 → 0.2.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +447 -0
  2. package/CODE_OF_CONDUCT.md +35 -0
  3. package/CONTRIBUTING.md +7 -0
  4. package/README.md +14 -2
  5. package/RESEARCH.md +346 -0
  6. package/SECURITY.md +35 -0
  7. package/dist/affiliate-copy.js +49 -0
  8. package/dist/auth.js +453 -0
  9. package/dist/bigquery.js +199 -0
  10. package/dist/chrome-browser.js +231 -0
  11. package/dist/cli.js +19652 -0
  12. package/dist/company-identity-review.js +78 -0
  13. package/dist/company-leads.js +422 -0
  14. package/dist/company-recovery.js +84 -0
  15. package/dist/deel-outreach.js +469 -0
  16. package/dist/deel-salesnav.js +368 -0
  17. package/dist/direct-path.js +326 -0
  18. package/dist/domain.js +53 -0
  19. package/dist/domainfinder.js +764 -0
  20. package/dist/engine.js +216 -0
  21. package/dist/historical-queries.js +189 -0
  22. package/dist/hunter-emailfinder.js +252 -0
  23. package/dist/icp-templates.js +171 -0
  24. package/dist/indiecrm/commands.js +108 -0
  25. package/dist/indiecrm-cli.js +2 -104
  26. package/dist/instantly.js +136 -0
  27. package/dist/io.js +21 -0
  28. package/dist/leadlists-funnel.js +148 -0
  29. package/dist/linkedin-companies.js +562 -0
  30. package/dist/linkedin-product-details.js +1203 -0
  31. package/dist/linkedin-product-search.js +1081 -0
  32. package/dist/linkedin-products.js +786 -0
  33. package/dist/linkedin-session-contracts.js +3 -0
  34. package/dist/linkedin-session.js +846 -0
  35. package/dist/providers.js +1 -0
  36. package/dist/research-browser-preference.js +37 -0
  37. package/dist/sales-navigator.js +1231 -0
  38. package/dist/salesnav-backfill.js +710 -0
  39. package/dist/sample-data.js +34 -0
  40. package/dist/session-recovery.js +62 -0
  41. package/dist/vendor/salesprompter-shared/extension-session-contracts.js +29 -0
  42. package/dist/vendor/salesprompter-shared/linkedin-session.js +22 -0
  43. package/dist/vendor/salesprompter-shared/phantombuster-contracts.js +16 -0
  44. package/dist/vendor/salesprompter-shared/session-vault-contracts.js +17 -0
  45. package/package.json +73 -14
@@ -0,0 +1,846 @@
1
+ import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { createClient } from "@supabase/supabase-js";
6
+ import { DEFAULT_LINKEDIN_SESSION_USER_AGENT, formatLiAtCookieHeader } from "./linkedin-session-contracts.js";
7
+ const COOKIE_VAULT_KEY_CONTEXT = "salesprompter:linkedin-session-cookie:v1";
8
+ const AES_256_KEY_BYTES = 32;
9
+ const REQUEST_TIMEOUT_MS = 20_000;
10
+ const MAX_COOKIE_VALIDATION_ATTEMPTS = 8;
11
+ const COOKIE_VALIDATION_TIMEOUT_MS = 75_000;
12
+ const COOKIE_PROBE_TIMEOUT_MS = 8_000;
13
+ const DEFAULT_SUPABASE_SESSION_REQUEST_TIMEOUT_MS = 2_500;
14
+ const LINKEDIN_SESSION_COOKIE_TABLE = "salesprompter_linkedin_session_cookies";
15
+ const LINKEDIN_SESSION_COOKIE_CHECK_TABLE = "salesprompter_linkedin_session_cookie_checks";
16
+ const CLAIM_LINKEDIN_SESSION_COOKIE_RPC = "salesprompter_claim_active_linkedin_session_cookie_excluding";
17
+ const DEFAULT_EXCLUDED_LINKEDIN_SESSION_USER_EMAILS = ["hello@danielsinewe.com"];
18
+ const DEFAULT_EXCLUDED_LINKEDIN_SESSION_USER_HANDLES = ["danielsinewe"];
19
+ const ACTIVE_LINKEDIN_SESSION_LOOKBACK_HOURS = 72;
20
+ const MAX_RECOVERABLE_LINKEDIN_SESSION_CANDIDATES = 12;
21
+ const RECOVERABLE_LINKEDIN_SESSION_INACTIVE_REASON_PREFIX = "quarantined_browser_unverified";
22
+ const ENV_FILE_PATHS = [
23
+ resolve(dirname(fileURLToPath(import.meta.url)), "..", ".env.local"),
24
+ resolve(dirname(fileURLToPath(import.meta.url)), "..", ".env"),
25
+ resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "salesprompter-app", ".env.local"),
26
+ resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "salesprompter-app", ".env.vercel.local")
27
+ ];
28
+ const envFileCache = new Map();
29
+ function normalizeSessionCookie(sessionCookie) {
30
+ const trimmed = sessionCookie?.trim() || "";
31
+ return trimmed.length > 0 ? trimmed : null;
32
+ }
33
+ function normalizeIdentityValue(value) {
34
+ const trimmed = value?.trim().toLowerCase() || "";
35
+ return trimmed.length > 0 ? trimmed : null;
36
+ }
37
+ function dedupeIdentityValues(values) {
38
+ return Array.from(new Set(values
39
+ .map((value) => normalizeIdentityValue(value))
40
+ .filter((value) => Boolean(value))));
41
+ }
42
+ function parseIdentityEnvList(value) {
43
+ return dedupeIdentityValues((value || "").split(","));
44
+ }
45
+ function parseDotEnvFile(filePath) {
46
+ if (envFileCache.has(filePath)) {
47
+ return envFileCache.get(filePath);
48
+ }
49
+ const parsed = new Map();
50
+ if (existsSync(filePath)) {
51
+ const content = readFileSync(filePath, "utf8");
52
+ for (const line of content.split(/\r?\n/)) {
53
+ const trimmed = line.trim();
54
+ if (trimmed.length === 0 || trimmed.startsWith("#")) {
55
+ continue;
56
+ }
57
+ const separatorIndex = trimmed.indexOf("=");
58
+ if (separatorIndex <= 0) {
59
+ continue;
60
+ }
61
+ const key = trimmed.slice(0, separatorIndex).trim();
62
+ let value = trimmed.slice(separatorIndex + 1).trim();
63
+ if ((value.startsWith('"') && value.endsWith('"')) ||
64
+ (value.startsWith("'") && value.endsWith("'"))) {
65
+ value = value.slice(1, -1);
66
+ }
67
+ if (key.length > 0 && value.length > 0) {
68
+ parsed.set(key, value);
69
+ }
70
+ }
71
+ }
72
+ envFileCache.set(filePath, parsed);
73
+ return parsed;
74
+ }
75
+ export function resolveConfiguredEnvValue(env, key) {
76
+ const directValue = env[key]?.trim() || "";
77
+ if (directValue.length > 0) {
78
+ return directValue;
79
+ }
80
+ if (env !== process.env) {
81
+ return null;
82
+ }
83
+ for (const filePath of ENV_FILE_PATHS) {
84
+ const parsed = parseDotEnvFile(filePath);
85
+ const value = parsed.get(key)?.trim() || "";
86
+ if (value.length > 0) {
87
+ return value;
88
+ }
89
+ }
90
+ return null;
91
+ }
92
+ function isTruthySetting(value) {
93
+ return ["1", "true", "on", "yes"].includes(value?.trim().toLowerCase() || "");
94
+ }
95
+ function isFalsySetting(value) {
96
+ return ["0", "false", "off", "no"].includes(value?.trim().toLowerCase() || "");
97
+ }
98
+ export function resolveCliLinkedInSessionExclusions(env = process.env) {
99
+ const includeDefaultExclusions = !isFalsySetting(resolveConfiguredEnvValue(env, "SALESPROMPTER_LINKEDIN_SESSION_EXCLUDE_DEFAULT_IDENTITIES"));
100
+ const defaultExcludedEmails = includeDefaultExclusions
101
+ ? DEFAULT_EXCLUDED_LINKEDIN_SESSION_USER_EMAILS
102
+ : [];
103
+ const defaultExcludedHandles = includeDefaultExclusions
104
+ ? DEFAULT_EXCLUDED_LINKEDIN_SESSION_USER_HANDLES
105
+ : [];
106
+ return {
107
+ userEmails: dedupeIdentityValues([
108
+ ...defaultExcludedEmails,
109
+ ...parseIdentityEnvList(resolveConfiguredEnvValue(env, "SALESPROMPTER_LINKEDIN_SESSION_EXCLUDED_EMAILS"))
110
+ ]),
111
+ userHandles: dedupeIdentityValues([
112
+ ...defaultExcludedHandles,
113
+ ...parseIdentityEnvList(resolveConfiguredEnvValue(env, "SALESPROMPTER_LINKEDIN_SESSION_EXCLUDED_HANDLES"))
114
+ ])
115
+ };
116
+ }
117
+ function isExcludedLinkedInSessionIdentity(identity, exclusions) {
118
+ const normalizedEmail = normalizeIdentityValue(identity.userEmail);
119
+ if (normalizedEmail && exclusions.userEmails.includes(normalizedEmail)) {
120
+ return true;
121
+ }
122
+ const normalizedHandle = normalizeIdentityValue(identity.userHandle);
123
+ return Boolean(normalizedHandle && exclusions.userHandles.includes(normalizedHandle));
124
+ }
125
+ function getCookieVaultKey(env = process.env) {
126
+ const rawSecret = resolveConfiguredEnvValue(env, "LINKEDIN_SESSION_COOKIE_ENCRYPTION_KEY") ||
127
+ resolveConfiguredEnvValue(env, "EXTENSION_AUTH_SECRET") ||
128
+ resolveConfiguredEnvValue(env, "CLERK_SECRET_KEY") ||
129
+ null;
130
+ const secret = rawSecret
131
+ ? rawSecret.replace(/\\n/g, "\n").trim()
132
+ : null;
133
+ if (!secret) {
134
+ return null;
135
+ }
136
+ return createHash("sha256")
137
+ .update(`${COOKIE_VAULT_KEY_CONTEXT}:${secret}`)
138
+ .digest()
139
+ .subarray(0, AES_256_KEY_BYTES);
140
+ }
141
+ export function sessionCookieHash(sessionCookie) {
142
+ const normalized = normalizeSessionCookie(sessionCookie);
143
+ if (!normalized) {
144
+ return null;
145
+ }
146
+ return createHash("sha256").update(normalized).digest("hex");
147
+ }
148
+ export function encryptLinkedInSessionCookie(sessionCookie, env = process.env) {
149
+ const normalized = normalizeSessionCookie(sessionCookie);
150
+ const key = getCookieVaultKey(env);
151
+ if (!normalized || !key) {
152
+ return null;
153
+ }
154
+ const iv = randomBytes(12);
155
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
156
+ const ciphertext = Buffer.concat([cipher.update(normalized, "utf8"), cipher.final()]);
157
+ const authTag = cipher.getAuthTag();
158
+ return {
159
+ sessionCookieSha256: sessionCookieHash(normalized),
160
+ sessionCookieCiphertext: ciphertext.toString("base64url"),
161
+ sessionCookieIv: iv.toString("base64url"),
162
+ sessionCookieAuthTag: authTag.toString("base64url")
163
+ };
164
+ }
165
+ export function decryptLinkedInSessionCookie(record, env = process.env) {
166
+ if (!record) {
167
+ return null;
168
+ }
169
+ const key = getCookieVaultKey(env);
170
+ if (!key) {
171
+ return null;
172
+ }
173
+ const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(record.sessionCookieIv, "base64url"));
174
+ decipher.setAuthTag(Buffer.from(record.sessionCookieAuthTag, "base64url"));
175
+ const plaintext = Buffer.concat([
176
+ decipher.update(Buffer.from(record.sessionCookieCiphertext, "base64url")),
177
+ decipher.final()
178
+ ]).toString("utf8");
179
+ return normalizeSessionCookie(plaintext);
180
+ }
181
+ export function resolveCliLinkedInSessionConfig(env = process.env) {
182
+ const manageSessionsSetting = resolveConfiguredEnvValue(env, "SALESPROMPTER_CLI_MANAGE_LINKEDIN_SESSIONS");
183
+ const explicitEnable = isTruthySetting(manageSessionsSetting);
184
+ const explicitDisable = isFalsySetting(manageSessionsSetting);
185
+ const autoEnable = !manageSessionsSetting;
186
+ if (explicitDisable || (!explicitEnable && !autoEnable)) {
187
+ return null;
188
+ }
189
+ const supabaseUrl = resolveConfiguredEnvValue(env, "SALESPROMPTER_SUPABASE_URL") ||
190
+ resolveConfiguredEnvValue(env, "NEXT_PUBLIC_SUPABASE_URL") ||
191
+ "";
192
+ const supabaseServiceRoleKey = resolveConfiguredEnvValue(env, "SUPABASE_SERVICE_ROLE_KEY") || "";
193
+ const hasVaultKey = Boolean(getCookieVaultKey(env));
194
+ const missing = [];
195
+ if (!supabaseUrl) {
196
+ missing.push("SALESPROMPTER_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_URL");
197
+ }
198
+ if (!supabaseServiceRoleKey) {
199
+ missing.push("SUPABASE_SERVICE_ROLE_KEY");
200
+ }
201
+ if (!hasVaultKey) {
202
+ missing.push("LINKEDIN_SESSION_COOKIE_ENCRYPTION_KEY or EXTENSION_AUTH_SECRET or CLERK_SECRET_KEY");
203
+ }
204
+ if (missing.length > 0) {
205
+ if (!explicitEnable) {
206
+ return null;
207
+ }
208
+ throw new Error(`CLI-managed LinkedIn session rotation is enabled, but required configuration is missing: ${missing.join(", ")}. Disable SALESPROMPTER_CLI_MANAGE_LINKEDIN_SESSIONS to use the app-managed export path instead.`);
209
+ }
210
+ return {
211
+ supabaseUrl,
212
+ supabaseServiceRoleKey
213
+ };
214
+ }
215
+ export function createLinkedInSessionSupabaseClient(env = process.env) {
216
+ const config = resolveCliLinkedInSessionConfig(env);
217
+ if (!config) {
218
+ return null;
219
+ }
220
+ const configuredTimeoutMs = Number(env.SALESPROMPTER_LINKEDIN_SESSION_SUPABASE_TIMEOUT_MS);
221
+ const timeoutMs = Number.isFinite(configuredTimeoutMs) && configuredTimeoutMs >= 500
222
+ ? Math.min(30_000, Math.trunc(configuredTimeoutMs))
223
+ : DEFAULT_SUPABASE_SESSION_REQUEST_TIMEOUT_MS;
224
+ const timedFetch = (input, init) => {
225
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
226
+ const signal = init?.signal
227
+ ? AbortSignal.any([init.signal, timeoutSignal])
228
+ : timeoutSignal;
229
+ return fetch(input, { ...init, signal });
230
+ };
231
+ return createClient(config.supabaseUrl, config.supabaseServiceRoleKey, {
232
+ auth: { persistSession: false },
233
+ global: { fetch: timedFetch }
234
+ });
235
+ }
236
+ export async function recordLinkedInSessionCookieAudit(supabase, input) {
237
+ const checkedAt = input.checkedAt || new Date().toISOString();
238
+ const updateResult = await supabase
239
+ .from(LINKEDIN_SESSION_COOKIE_TABLE)
240
+ .update({
241
+ is_active: input.isActive,
242
+ inactive_reason: input.inactiveReason ?? null,
243
+ last_validation_at: checkedAt,
244
+ last_validation_status: input.isActive ? "active" : "inactive",
245
+ last_feed_status: input.feedStatus,
246
+ last_sales_navigator_status: input.salesNavigatorStatus,
247
+ last_recruiter_status: input.recruiterStatus,
248
+ last_product_class: input.productClass,
249
+ last_ingested_source: input.source,
250
+ last_validation_error: input.validationError ?? null,
251
+ last_validation_details: input.details ?? {}
252
+ })
253
+ .eq("session_cookie_sha256", input.sessionCookieSha256);
254
+ if (updateResult.error) {
255
+ throw new Error(`Failed to update LinkedIn session cookie status: ${updateResult.error.message}`);
256
+ }
257
+ const insertResult = await supabase.from(LINKEDIN_SESSION_COOKIE_CHECK_TABLE).insert({
258
+ session_cookie_sha256: input.sessionCookieSha256,
259
+ checked_at: checkedAt,
260
+ source: input.source,
261
+ is_active: input.isActive,
262
+ inactive_reason: input.inactiveReason ?? null,
263
+ feed_status: input.feedStatus,
264
+ sales_navigator_status: input.salesNavigatorStatus,
265
+ recruiter_status: input.recruiterStatus,
266
+ product_class: input.productClass,
267
+ validation_error: input.validationError ?? null,
268
+ details: input.details ?? {}
269
+ });
270
+ if (insertResult.error) {
271
+ throw new Error(`Failed to insert LinkedIn session cookie audit row: ${insertResult.error.message}`);
272
+ }
273
+ }
274
+ function lookbackThresholdIso(hours) {
275
+ return new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
276
+ }
277
+ function normalizeCandidateSelectionError(error) {
278
+ return error instanceof Error ? error.message : String(error);
279
+ }
280
+ async function listLinkedInSessionCookieCandidates(supabase, options) {
281
+ let query = supabase
282
+ .from(LINKEDIN_SESSION_COOKIE_TABLE)
283
+ .select([
284
+ "session_cookie_sha256",
285
+ "session_cookie_ciphertext",
286
+ "session_cookie_iv",
287
+ "session_cookie_auth_tag",
288
+ "last_user_email",
289
+ "last_user_handle",
290
+ "last_ingested_source",
291
+ "is_active",
292
+ "inactive_reason",
293
+ "last_validation_at",
294
+ "last_selected_at"
295
+ ].join(","))
296
+ .eq("last_product_class", "sales_navigator")
297
+ .eq("last_sales_navigator_status", "ok")
298
+ .not("last_validation_at", "is", null);
299
+ if (!options.includeStale) {
300
+ query = query.gte("last_validation_at", lookbackThresholdIso(ACTIVE_LINKEDIN_SESSION_LOOKBACK_HOURS));
301
+ }
302
+ query = options.includeStale
303
+ ? query
304
+ .order("last_validation_at", { ascending: false })
305
+ .order("last_selected_at", { ascending: true, nullsFirst: true })
306
+ .limit(options.limit ?? MAX_RECOVERABLE_LINKEDIN_SESSION_CANDIDATES)
307
+ : query
308
+ .order("last_selected_at", { ascending: true, nullsFirst: true })
309
+ .order("last_validation_at", { ascending: false })
310
+ .limit(options.limit ?? MAX_RECOVERABLE_LINKEDIN_SESSION_CANDIDATES);
311
+ const filteredQuery = options.activeOnly
312
+ ? query.eq("is_active", true)
313
+ : query
314
+ .eq("is_active", false)
315
+ .like("inactive_reason", `${RECOVERABLE_LINKEDIN_SESSION_INACTIVE_REASON_PREFIX}%`);
316
+ const executableQuery = typeof filteredQuery.retry === "function"
317
+ ? filteredQuery.retry(false)
318
+ : filteredQuery;
319
+ const result = await executableQuery;
320
+ if (result.error) {
321
+ throw new Error(`Failed to query LinkedIn session cookie candidates: ${result.error.message}`);
322
+ }
323
+ const rows = (result.data ?? []);
324
+ return rows.filter((row) => !isExcludedLinkedInSessionIdentity({
325
+ userEmail: row.last_user_email ?? null,
326
+ userHandle: row.last_user_handle ?? null
327
+ }, options.exclusions));
328
+ }
329
+ async function buildClaimedLinkedInSessionCookieFromRow(supabase, row, source, exclusions, options) {
330
+ const claimedIdentity = {
331
+ userEmail: row.last_user_email ?? null,
332
+ userHandle: row.last_user_handle ?? null
333
+ };
334
+ if (isExcludedLinkedInSessionIdentity(claimedIdentity, exclusions)) {
335
+ await recordLinkedInSessionCookieAudit(supabase, {
336
+ sessionCookieSha256: row.session_cookie_sha256,
337
+ source: `${source}_excluded_identity`,
338
+ feedStatus: null,
339
+ salesNavigatorStatus: "ok",
340
+ recruiterStatus: null,
341
+ productClass: "sales_navigator",
342
+ isActive: true,
343
+ inactiveReason: null,
344
+ validationError: "excluded_linkedin_session_identity",
345
+ details: {
346
+ userEmail: claimedIdentity.userEmail,
347
+ userHandle: claimedIdentity.userHandle,
348
+ excludedUserEmails: exclusions.userEmails,
349
+ excludedUserHandles: exclusions.userHandles,
350
+ claimStrategy: options?.claimStrategy ?? null
351
+ }
352
+ });
353
+ return null;
354
+ }
355
+ try {
356
+ const sessionCookie = decryptLinkedInSessionCookie({
357
+ sessionCookieSha256: row.session_cookie_sha256,
358
+ sessionCookieCiphertext: row.session_cookie_ciphertext,
359
+ sessionCookieIv: row.session_cookie_iv,
360
+ sessionCookieAuthTag: row.session_cookie_auth_tag
361
+ }, options?.env ?? process.env);
362
+ if (!sessionCookie) {
363
+ throw new Error("Claimed LinkedIn session cookie could not be decrypted");
364
+ }
365
+ return {
366
+ sessionCookieSha256: row.session_cookie_sha256,
367
+ sessionCookie,
368
+ userEmail: claimedIdentity.userEmail,
369
+ userHandle: claimedIdentity.userHandle,
370
+ lastIngestedSource: row.last_ingested_source ?? null,
371
+ claimStrategy: options?.claimStrategy,
372
+ previousInactiveReason: row.inactive_reason ?? null
373
+ };
374
+ }
375
+ catch (error) {
376
+ await recordLinkedInSessionCookieAudit(supabase, {
377
+ sessionCookieSha256: row.session_cookie_sha256,
378
+ source: `${source}_decrypt_failure`,
379
+ feedStatus: null,
380
+ salesNavigatorStatus: "error",
381
+ recruiterStatus: null,
382
+ productClass: null,
383
+ isActive: false,
384
+ inactiveReason: "decrypt_failed",
385
+ validationError: error instanceof Error ? error.message : String(error),
386
+ details: {
387
+ userEmail: claimedIdentity.userEmail,
388
+ userHandle: claimedIdentity.userHandle,
389
+ claimStrategy: options?.claimStrategy ?? null
390
+ }
391
+ });
392
+ return null;
393
+ }
394
+ }
395
+ async function claimLinkedInSessionCookieFromCandidates(supabase, source, exclusions, options) {
396
+ const candidates = await listLinkedInSessionCookieCandidates(supabase, {
397
+ activeOnly: options.activeOnly,
398
+ exclusions,
399
+ includeStale: options.includeStale
400
+ });
401
+ for (const candidate of candidates) {
402
+ const claimed = await buildClaimedLinkedInSessionCookieFromRow(supabase, candidate, source, exclusions, {
403
+ claimStrategy: options.claimStrategy,
404
+ env: options.env
405
+ });
406
+ if (claimed) {
407
+ return claimed;
408
+ }
409
+ }
410
+ return null;
411
+ }
412
+ async function claimStaleActiveLinkedInSessionCookie(supabase, source, exclusions, env = process.env, seenCookieHashes) {
413
+ const candidates = await listLinkedInSessionCookieCandidates(supabase, {
414
+ activeOnly: true,
415
+ exclusions,
416
+ includeStale: true,
417
+ limit: 64
418
+ });
419
+ for (const candidate of candidates) {
420
+ if (seenCookieHashes?.has(candidate.session_cookie_sha256)) {
421
+ continue;
422
+ }
423
+ const claimed = await buildClaimedLinkedInSessionCookieFromRow(supabase, candidate, source, exclusions, {
424
+ claimStrategy: "stale_active_fallback",
425
+ env
426
+ });
427
+ if (claimed) {
428
+ return claimed;
429
+ }
430
+ }
431
+ return null;
432
+ }
433
+ export async function claimActiveLinkedInSessionCookie(supabase, source, env = process.env) {
434
+ if (typeof supabase.rpc !== "function") {
435
+ throw new Error("Supabase client does not support RPC");
436
+ }
437
+ const exclusions = resolveCliLinkedInSessionExclusions(env);
438
+ let lastClaimError = null;
439
+ for (let attempt = 0; attempt < 10; attempt += 1) {
440
+ try {
441
+ const result = await supabase
442
+ .rpc(CLAIM_LINKEDIN_SESSION_COOKIE_RPC, {
443
+ selection_source: source,
444
+ excluded_user_emails: exclusions.userEmails,
445
+ excluded_user_handles: exclusions.userHandles
446
+ })
447
+ .single();
448
+ if (result.error) {
449
+ lastClaimError = result.error.message;
450
+ break;
451
+ }
452
+ const data = result.data;
453
+ if (!data) {
454
+ lastClaimError = "No active LinkedIn session cookies available";
455
+ break;
456
+ }
457
+ const claimed = await buildClaimedLinkedInSessionCookieFromRow(supabase, data, source, exclusions, {
458
+ claimStrategy: "rpc_exclusion_rotation",
459
+ env
460
+ });
461
+ if (claimed) {
462
+ return claimed;
463
+ }
464
+ }
465
+ catch (error) {
466
+ lastClaimError = normalizeCandidateSelectionError(error);
467
+ break;
468
+ }
469
+ }
470
+ const directFallback = await claimLinkedInSessionCookieFromCandidates(supabase, source, exclusions, {
471
+ activeOnly: true,
472
+ claimStrategy: "direct_active_fallback",
473
+ env
474
+ });
475
+ if (directFallback) {
476
+ return directFallback;
477
+ }
478
+ if (lastClaimError &&
479
+ /salesprompter_claim_active_linkedin_session_cookie_excluding/i.test(lastClaimError)) {
480
+ throw new Error("LinkedIn session exclusion rotation RPC is unavailable. Apply the Salesprompter migration that adds salesprompter_claim_active_linkedin_session_cookie_excluding before running Sales Navigator exports.");
481
+ }
482
+ if (lastClaimError) {
483
+ throw new Error(`Failed to claim LinkedIn session cookie: ${lastClaimError}`);
484
+ }
485
+ throw new Error("No non-excluded decryptable active LinkedIn session cookies available");
486
+ }
487
+ async function claimRecoverableInactiveLinkedInSessionCookie(supabase, source, exclusions, env = process.env, seenCookieHashes) {
488
+ const candidates = await listLinkedInSessionCookieCandidates(supabase, {
489
+ activeOnly: false,
490
+ exclusions
491
+ });
492
+ for (const candidate of candidates) {
493
+ if (seenCookieHashes?.has(candidate.session_cookie_sha256)) {
494
+ continue;
495
+ }
496
+ const claimed = await buildClaimedLinkedInSessionCookieFromRow(supabase, candidate, source, exclusions, {
497
+ claimStrategy: "recoverable_inactive_fallback",
498
+ env
499
+ });
500
+ if (claimed) {
501
+ return claimed;
502
+ }
503
+ }
504
+ return null;
505
+ }
506
+ async function describeLinkedInSessionPool(supabase, exclusions) {
507
+ const poolQuery = supabase
508
+ .from(LINKEDIN_SESSION_COOKIE_TABLE)
509
+ .select([
510
+ "last_user_email",
511
+ "last_user_handle",
512
+ "is_active",
513
+ "inactive_reason",
514
+ "last_validation_at",
515
+ "last_sales_navigator_status",
516
+ "last_product_class"
517
+ ].join(","))
518
+ .eq("last_product_class", "sales_navigator");
519
+ const result = await (typeof poolQuery.retry === "function"
520
+ ? poolQuery.retry(false)
521
+ : poolQuery);
522
+ if (result.error) {
523
+ throw new Error(`Failed to inspect LinkedIn session pool: ${result.error.message}`);
524
+ }
525
+ const rows = (result.data ?? []).filter((row) => !isExcludedLinkedInSessionIdentity({
526
+ userEmail: row.last_user_email ?? null,
527
+ userHandle: row.last_user_handle ?? null
528
+ }, exclusions));
529
+ const recentThresholdMs = Date.now() - ACTIVE_LINKEDIN_SESSION_LOOKBACK_HOURS * 60 * 60 * 1000;
530
+ const recentRows = rows.filter((row) => {
531
+ if (!row.last_validation_at) {
532
+ return false;
533
+ }
534
+ const timestamp = Date.parse(row.last_validation_at);
535
+ return Number.isFinite(timestamp) && timestamp >= recentThresholdMs;
536
+ });
537
+ const eligibleActive = recentRows.filter((row) => row.is_active === true && row.last_sales_navigator_status === "ok").length;
538
+ const recoverableInactive = recentRows.filter((row) => row.is_active === false &&
539
+ row.last_sales_navigator_status === "ok" &&
540
+ Boolean(row.inactive_reason?.startsWith(RECOVERABLE_LINKEDIN_SESSION_INACTIVE_REASON_PREFIX))).length;
541
+ const recentStatusBreakdown = recentRows.reduce((accumulator, row) => {
542
+ const key = row.last_sales_navigator_status ?? "unknown";
543
+ accumulator[key] = (accumulator[key] ?? 0) + 1;
544
+ return accumulator;
545
+ }, {});
546
+ return {
547
+ totalNonExcludedSalesNavigatorCookies: rows.length,
548
+ recentNonExcludedSalesNavigatorCookies: recentRows.length,
549
+ eligibleActive,
550
+ recoverableInactive,
551
+ recentStatusBreakdown
552
+ };
553
+ }
554
+ async function buildLinkedInSessionPoolDiagnosticsMessage(supabase, exclusions) {
555
+ try {
556
+ const summary = await describeLinkedInSessionPool(supabase, exclusions);
557
+ const breakdown = Object.entries(summary.recentStatusBreakdown)
558
+ .sort(([left], [right]) => left.localeCompare(right))
559
+ .map(([status, count]) => `${status}=${count}`)
560
+ .join(", ");
561
+ return ` LinkedIn session pool after exclusions: ${summary.totalNonExcludedSalesNavigatorCookies} sales-nav cookies, ${summary.recentNonExcludedSalesNavigatorCookies} recently validated, ${summary.eligibleActive} eligible active, ${summary.recoverableInactive} recoverable inactive${breakdown ? `; recent statuses: ${breakdown}` : ""}.`;
562
+ }
563
+ catch (error) {
564
+ return ` LinkedIn session pool diagnostics unavailable: ${normalizeCandidateSelectionError(error)}.`;
565
+ }
566
+ }
567
+ function extractTitle(body) {
568
+ const match = body.match(/<title[^>]*>([^<]+)<\/title>/i);
569
+ return match ? match[1].trim().replace(/\s+/g, " ") : null;
570
+ }
571
+ function includesAny(value, patterns) {
572
+ const normalized = value ?? "";
573
+ return patterns.some((pattern) => pattern.test(normalized));
574
+ }
575
+ function isLoggedOut(result) {
576
+ return (includesAny(result.finalUrl, [/linkedin\.com\/login/i, /uas\/login/i, /checkpoint\/challenge/i]) ||
577
+ includesAny(result.title, [/login/i, /sign in/i]) ||
578
+ includesAny(result.body, [/session has expired/i, /sign in to linkedin/i, /checkpoint\/challenge/i]));
579
+ }
580
+ function isUpsell(result) {
581
+ return includesAny(result.body, [
582
+ /unlock sales navigator/i,
583
+ /find the right prospects/i,
584
+ /start your free trial/i,
585
+ /guest_login_sales_nav/i,
586
+ /premium\/products\/sales/i,
587
+ /you don'?t have a sales navigator account/i
588
+ ]);
589
+ }
590
+ function hasSearchEntitlementSignals(result) {
591
+ const inSalesSurface = includesAny(result.finalUrl, [/\/sales\/search\/people/i, /\/sales\/home/i]);
592
+ if (!inSalesSurface) {
593
+ return false;
594
+ }
595
+ return (includesAny(result.title, [/sales navigator/i, /lead search/i]) ||
596
+ includesAny(result.body, [
597
+ /sales navigator/i,
598
+ /lead search/i,
599
+ /saved searches/i,
600
+ /personas/i,
601
+ /viewallfilters=true/i,
602
+ /current job title/i,
603
+ /company headcount/i,
604
+ /search keywords/i,
605
+ /sales\/search\/people/i,
606
+ /accounts<\/span>/i,
607
+ /leads<\/span>/i,
608
+ /messaging<\/span>/i
609
+ ]));
610
+ }
611
+ export async function fetchHtmlWithSessionCookie(url, sessionCookie, options) {
612
+ const cookieHeader = formatLiAtCookieHeader(sessionCookie);
613
+ const redirectChain = [];
614
+ let currentUrl = url;
615
+ const seenUrls = new Set([url]);
616
+ for (let index = 0; index < 5; index += 1) {
617
+ const controller = new AbortController();
618
+ const timeout = setTimeout(() => controller.abort(), options?.timeoutMs ?? REQUEST_TIMEOUT_MS);
619
+ try {
620
+ const response = await fetch(currentUrl, {
621
+ method: "GET",
622
+ redirect: "manual",
623
+ signal: controller.signal,
624
+ headers: {
625
+ Cookie: cookieHeader ?? "",
626
+ "User-Agent": options?.userAgent ?? DEFAULT_LINKEDIN_SESSION_USER_AGENT,
627
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
628
+ "Accept-Language": "en-US,en;q=0.9",
629
+ "Cache-Control": "no-cache",
630
+ Pragma: "no-cache"
631
+ }
632
+ });
633
+ clearTimeout(timeout);
634
+ const location = response.headers.get("location");
635
+ if (location && [301, 302, 303, 307, 308].includes(response.status)) {
636
+ const nextUrl = new URL(location, currentUrl).toString();
637
+ redirectChain.push({
638
+ status: response.status,
639
+ location: nextUrl
640
+ });
641
+ if (seenUrls.has(nextUrl)) {
642
+ return {
643
+ status: response.status,
644
+ finalUrl: nextUrl,
645
+ redirectChain,
646
+ title: null,
647
+ body: "",
648
+ error: "redirect_loop"
649
+ };
650
+ }
651
+ seenUrls.add(nextUrl);
652
+ currentUrl = nextUrl;
653
+ continue;
654
+ }
655
+ const body = await response.text();
656
+ return {
657
+ status: response.status,
658
+ finalUrl: currentUrl,
659
+ redirectChain,
660
+ title: extractTitle(body),
661
+ body,
662
+ error: null
663
+ };
664
+ }
665
+ catch (error) {
666
+ clearTimeout(timeout);
667
+ return {
668
+ status: null,
669
+ finalUrl: currentUrl,
670
+ redirectChain,
671
+ title: null,
672
+ body: "",
673
+ error: error instanceof Error
674
+ ? error.name === "AbortError"
675
+ ? "timeout"
676
+ : error.message
677
+ : String(error)
678
+ };
679
+ }
680
+ }
681
+ return {
682
+ status: null,
683
+ finalUrl: currentUrl,
684
+ redirectChain,
685
+ title: null,
686
+ body: "",
687
+ error: "too_many_redirects"
688
+ };
689
+ }
690
+ export async function probeSalesNavigatorSearchSession(sessionCookie, queryUrl, options) {
691
+ const result = await fetchHtmlWithSessionCookie(queryUrl, sessionCookie, {
692
+ timeoutMs: options?.timeoutMs
693
+ });
694
+ let status = "unknown";
695
+ if (result.error) {
696
+ status = "error";
697
+ }
698
+ else if (result.status === 999 || result.status === 429) {
699
+ status = "rate_limited";
700
+ }
701
+ else if (isLoggedOut(result)) {
702
+ status = "logged_out";
703
+ }
704
+ else if (isUpsell(result)) {
705
+ status = "upsell";
706
+ }
707
+ else if (hasSearchEntitlementSignals(result)) {
708
+ status = "ok";
709
+ }
710
+ else if (includesAny(result.finalUrl, [/\/sales\//i])) {
711
+ status = "unknown";
712
+ }
713
+ const validationError = status === "ok"
714
+ ? null
715
+ : result.error ??
716
+ (status === "logged_out"
717
+ ? "linkedin_session_invalid"
718
+ : status === "upsell"
719
+ ? "sales_navigator_upsell_detected"
720
+ : status === "rate_limited"
721
+ ? "linkedin_rate_limited"
722
+ : "sales_navigator_search_probe_failed");
723
+ return {
724
+ status,
725
+ finalUrl: result.finalUrl,
726
+ title: result.title,
727
+ redirectChain: result.redirectChain,
728
+ validationError,
729
+ details: {
730
+ statusCode: result.status,
731
+ error: result.error,
732
+ redirectCount: result.redirectChain.length,
733
+ queryUrl
734
+ }
735
+ };
736
+ }
737
+ export async function claimLinkedInSessionCookieForCli(options) {
738
+ const env = options.env ?? process.env;
739
+ const supabase = options.supabase ?? createLinkedInSessionSupabaseClient(env);
740
+ if (!supabase) {
741
+ return null;
742
+ }
743
+ const exclusions = resolveCliLinkedInSessionExclusions(env);
744
+ const seenCookieHashes = new Set();
745
+ let claimErrorMessage = null;
746
+ let claimedSession = null;
747
+ try {
748
+ claimedSession = await claimActiveLinkedInSessionCookie(supabase, options.source, env);
749
+ }
750
+ catch (error) {
751
+ claimErrorMessage = normalizeCandidateSelectionError(error);
752
+ }
753
+ if (claimedSession) {
754
+ seenCookieHashes.add(claimedSession.sessionCookieSha256);
755
+ return claimedSession;
756
+ }
757
+ claimedSession = await claimRecoverableInactiveLinkedInSessionCookie(supabase, options.source, exclusions, env, seenCookieHashes);
758
+ if (claimedSession) {
759
+ return claimedSession;
760
+ }
761
+ claimedSession = await claimStaleActiveLinkedInSessionCookie(supabase, options.source, exclusions, env, seenCookieHashes);
762
+ if (claimedSession) {
763
+ return claimedSession;
764
+ }
765
+ const poolDiagnostics = await buildLinkedInSessionPoolDiagnosticsMessage(supabase, exclusions);
766
+ throw new Error(`${claimErrorMessage ?? "No non-excluded decryptable LinkedIn session cookies available"}${poolDiagnostics}`);
767
+ }
768
+ export async function claimValidatedSalesNavigatorSessionCookieForCli(options) {
769
+ const env = options.env ?? process.env;
770
+ const supabase = options.supabase ?? createLinkedInSessionSupabaseClient(env);
771
+ if (!supabase) {
772
+ return null;
773
+ }
774
+ const seenCookieHashes = new Set();
775
+ const startedAt = Date.now();
776
+ let lastFailureMessage = null;
777
+ let attempts = 0;
778
+ const exclusions = resolveCliLinkedInSessionExclusions(env);
779
+ for (let attempt = 0; attempt < MAX_COOKIE_VALIDATION_ATTEMPTS; attempt += 1) {
780
+ if (Date.now() - startedAt >= COOKIE_VALIDATION_TIMEOUT_MS) {
781
+ break;
782
+ }
783
+ let claimedSession = null;
784
+ let claimErrorMessage = null;
785
+ try {
786
+ claimedSession = await claimActiveLinkedInSessionCookie(supabase, options.source, env);
787
+ }
788
+ catch (error) {
789
+ claimErrorMessage = normalizeCandidateSelectionError(error);
790
+ }
791
+ if (!claimedSession) {
792
+ claimedSession = await claimRecoverableInactiveLinkedInSessionCookie(supabase, options.source, exclusions, env, seenCookieHashes);
793
+ }
794
+ if (!claimedSession) {
795
+ claimedSession = await claimStaleActiveLinkedInSessionCookie(supabase, options.source, exclusions, env, seenCookieHashes);
796
+ }
797
+ if (!claimedSession) {
798
+ lastFailureMessage =
799
+ claimErrorMessage ?? "No non-excluded decryptable active LinkedIn session cookies available";
800
+ break;
801
+ }
802
+ attempts += 1;
803
+ if (seenCookieHashes.has(claimedSession.sessionCookieSha256)) {
804
+ break;
805
+ }
806
+ seenCookieHashes.add(claimedSession.sessionCookieSha256);
807
+ const probe = await probeSalesNavigatorSearchSession(claimedSession.sessionCookie, options.queryUrl, {
808
+ timeoutMs: COOKIE_PROBE_TIMEOUT_MS
809
+ });
810
+ const shouldDeactivate = probe.status === "logged_out" || probe.status === "upsell";
811
+ await recordLinkedInSessionCookieAudit(supabase, {
812
+ sessionCookieSha256: claimedSession.sessionCookieSha256,
813
+ source: `${options.source}_preflight`,
814
+ feedStatus: null,
815
+ salesNavigatorStatus: probe.status,
816
+ recruiterStatus: null,
817
+ productClass: probe.status === "ok" ? "sales_navigator" : null,
818
+ isActive: !shouldDeactivate,
819
+ inactiveReason: shouldDeactivate
820
+ ? probe.status === "upsell"
821
+ ? "salesnav_search_upsell_detected"
822
+ : "linkedin_session_invalid"
823
+ : null,
824
+ validationError: probe.validationError,
825
+ details: {
826
+ attemptNumber: attempts,
827
+ validationElapsedMs: Date.now() - startedAt,
828
+ claimStrategy: claimedSession.claimStrategy ?? null,
829
+ previousInactiveReason: claimedSession.previousInactiveReason ?? null,
830
+ ...probe.details,
831
+ finalUrl: probe.finalUrl,
832
+ title: probe.title,
833
+ redirectChain: probe.redirectChain.slice(0, 5)
834
+ }
835
+ });
836
+ if (probe.status === "ok") {
837
+ return claimedSession;
838
+ }
839
+ lastFailureMessage =
840
+ probe.validationError ?? `Sales Navigator search probe returned ${probe.status}`;
841
+ }
842
+ const poolDiagnostics = await buildLinkedInSessionPoolDiagnosticsMessage(supabase, exclusions);
843
+ throw new Error(lastFailureMessage
844
+ ? `No validated LinkedIn Sales Navigator session cookies available after ${attempts} preflight attempt${attempts === 1 ? "" : "s"}: ${lastFailureMessage}${poolDiagnostics}`
845
+ : `No validated LinkedIn Sales Navigator session cookies available after ${attempts} preflight attempt${attempts === 1 ? "" : "s"}${poolDiagnostics}`);
846
+ }