salesprompter-cli 0.1.43 → 0.1.44
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 +28 -0
- package/dist/cli.js +2564 -111
- package/dist/linkedin-session.js +103 -17
- package/dist/sales-navigator.js +42 -15
- package/package.json +1 -1
package/dist/linkedin-session.js
CHANGED
|
@@ -10,6 +10,10 @@ const REQUEST_TIMEOUT_MS = 20_000;
|
|
|
10
10
|
const MAX_COOKIE_VALIDATION_ATTEMPTS = 8;
|
|
11
11
|
const COOKIE_VALIDATION_TIMEOUT_MS = 75_000;
|
|
12
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";
|
|
13
17
|
const DEFAULT_EXCLUDED_LINKEDIN_SESSION_USER_EMAILS = ["hello@danielsinewe.com"];
|
|
14
18
|
const DEFAULT_EXCLUDED_LINKEDIN_SESSION_USER_HANDLES = ["danielsinewe"];
|
|
15
19
|
const ACTIVE_LINKEDIN_SESSION_LOOKBACK_HOURS = 72;
|
|
@@ -213,14 +217,26 @@ export function createLinkedInSessionSupabaseClient(env = process.env) {
|
|
|
213
217
|
if (!config) {
|
|
214
218
|
return null;
|
|
215
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
|
+
};
|
|
216
231
|
return createClient(config.supabaseUrl, config.supabaseServiceRoleKey, {
|
|
217
|
-
auth: { persistSession: false }
|
|
232
|
+
auth: { persistSession: false },
|
|
233
|
+
global: { fetch: timedFetch }
|
|
218
234
|
});
|
|
219
235
|
}
|
|
220
236
|
export async function recordLinkedInSessionCookieAudit(supabase, input) {
|
|
221
237
|
const checkedAt = input.checkedAt || new Date().toISOString();
|
|
222
238
|
const updateResult = await supabase
|
|
223
|
-
.from(
|
|
239
|
+
.from(LINKEDIN_SESSION_COOKIE_TABLE)
|
|
224
240
|
.update({
|
|
225
241
|
is_active: input.isActive,
|
|
226
242
|
inactive_reason: input.inactiveReason ?? null,
|
|
@@ -238,7 +254,7 @@ export async function recordLinkedInSessionCookieAudit(supabase, input) {
|
|
|
238
254
|
if (updateResult.error) {
|
|
239
255
|
throw new Error(`Failed to update LinkedIn session cookie status: ${updateResult.error.message}`);
|
|
240
256
|
}
|
|
241
|
-
const insertResult = await supabase.from(
|
|
257
|
+
const insertResult = await supabase.from(LINKEDIN_SESSION_COOKIE_CHECK_TABLE).insert({
|
|
242
258
|
session_cookie_sha256: input.sessionCookieSha256,
|
|
243
259
|
checked_at: checkedAt,
|
|
244
260
|
source: input.source,
|
|
@@ -262,8 +278,8 @@ function normalizeCandidateSelectionError(error) {
|
|
|
262
278
|
return error instanceof Error ? error.message : String(error);
|
|
263
279
|
}
|
|
264
280
|
async function listLinkedInSessionCookieCandidates(supabase, options) {
|
|
265
|
-
|
|
266
|
-
.from(
|
|
281
|
+
let query = supabase
|
|
282
|
+
.from(LINKEDIN_SESSION_COOKIE_TABLE)
|
|
267
283
|
.select([
|
|
268
284
|
"session_cookie_sha256",
|
|
269
285
|
"session_cookie_ciphertext",
|
|
@@ -279,17 +295,28 @@ async function listLinkedInSessionCookieCandidates(supabase, options) {
|
|
|
279
295
|
].join(","))
|
|
280
296
|
.eq("last_product_class", "sales_navigator")
|
|
281
297
|
.eq("last_sales_navigator_status", "ok")
|
|
282
|
-
.not("last_validation_at", "is", null)
|
|
283
|
-
|
|
284
|
-
.
|
|
285
|
-
|
|
286
|
-
|
|
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);
|
|
287
311
|
const filteredQuery = options.activeOnly
|
|
288
312
|
? query.eq("is_active", true)
|
|
289
313
|
: query
|
|
290
314
|
.eq("is_active", false)
|
|
291
315
|
.like("inactive_reason", `${RECOVERABLE_LINKEDIN_SESSION_INACTIVE_REASON_PREFIX}%`);
|
|
292
|
-
const
|
|
316
|
+
const executableQuery = typeof filteredQuery.retry === "function"
|
|
317
|
+
? filteredQuery.retry(false)
|
|
318
|
+
: filteredQuery;
|
|
319
|
+
const result = await executableQuery;
|
|
293
320
|
if (result.error) {
|
|
294
321
|
throw new Error(`Failed to query LinkedIn session cookie candidates: ${result.error.message}`);
|
|
295
322
|
}
|
|
@@ -368,7 +395,8 @@ async function buildClaimedLinkedInSessionCookieFromRow(supabase, row, source, e
|
|
|
368
395
|
async function claimLinkedInSessionCookieFromCandidates(supabase, source, exclusions, options) {
|
|
369
396
|
const candidates = await listLinkedInSessionCookieCandidates(supabase, {
|
|
370
397
|
activeOnly: options.activeOnly,
|
|
371
|
-
exclusions
|
|
398
|
+
exclusions,
|
|
399
|
+
includeStale: options.includeStale
|
|
372
400
|
});
|
|
373
401
|
for (const candidate of candidates) {
|
|
374
402
|
const claimed = await buildClaimedLinkedInSessionCookieFromRow(supabase, candidate, source, exclusions, {
|
|
@@ -381,6 +409,27 @@ async function claimLinkedInSessionCookieFromCandidates(supabase, source, exclus
|
|
|
381
409
|
}
|
|
382
410
|
return null;
|
|
383
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
|
+
}
|
|
384
433
|
export async function claimActiveLinkedInSessionCookie(supabase, source, env = process.env) {
|
|
385
434
|
if (typeof supabase.rpc !== "function") {
|
|
386
435
|
throw new Error("Supabase client does not support RPC");
|
|
@@ -390,7 +439,7 @@ export async function claimActiveLinkedInSessionCookie(supabase, source, env = p
|
|
|
390
439
|
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
391
440
|
try {
|
|
392
441
|
const result = await supabase
|
|
393
|
-
.rpc(
|
|
442
|
+
.rpc(CLAIM_LINKEDIN_SESSION_COOKIE_RPC, {
|
|
394
443
|
selection_source: source,
|
|
395
444
|
excluded_user_emails: exclusions.userEmails,
|
|
396
445
|
excluded_user_handles: exclusions.userHandles
|
|
@@ -427,8 +476,8 @@ export async function claimActiveLinkedInSessionCookie(supabase, source, env = p
|
|
|
427
476
|
return directFallback;
|
|
428
477
|
}
|
|
429
478
|
if (lastClaimError &&
|
|
430
|
-
/
|
|
431
|
-
throw new Error("LinkedIn session exclusion rotation RPC is unavailable. Apply the Salesprompter
|
|
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.");
|
|
432
481
|
}
|
|
433
482
|
if (lastClaimError) {
|
|
434
483
|
throw new Error(`Failed to claim LinkedIn session cookie: ${lastClaimError}`);
|
|
@@ -455,8 +504,8 @@ async function claimRecoverableInactiveLinkedInSessionCookie(supabase, source, e
|
|
|
455
504
|
return null;
|
|
456
505
|
}
|
|
457
506
|
async function describeLinkedInSessionPool(supabase, exclusions) {
|
|
458
|
-
const
|
|
459
|
-
.from(
|
|
507
|
+
const poolQuery = supabase
|
|
508
|
+
.from(LINKEDIN_SESSION_COOKIE_TABLE)
|
|
460
509
|
.select([
|
|
461
510
|
"last_user_email",
|
|
462
511
|
"last_user_handle",
|
|
@@ -467,6 +516,9 @@ async function describeLinkedInSessionPool(supabase, exclusions) {
|
|
|
467
516
|
"last_product_class"
|
|
468
517
|
].join(","))
|
|
469
518
|
.eq("last_product_class", "sales_navigator");
|
|
519
|
+
const result = await (typeof poolQuery.retry === "function"
|
|
520
|
+
? poolQuery.retry(false)
|
|
521
|
+
: poolQuery);
|
|
470
522
|
if (result.error) {
|
|
471
523
|
throw new Error(`Failed to inspect LinkedIn session pool: ${result.error.message}`);
|
|
472
524
|
}
|
|
@@ -682,6 +734,37 @@ export async function probeSalesNavigatorSearchSession(sessionCookie, queryUrl,
|
|
|
682
734
|
}
|
|
683
735
|
};
|
|
684
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
|
+
}
|
|
685
768
|
export async function claimValidatedSalesNavigatorSessionCookieForCli(options) {
|
|
686
769
|
const env = options.env ?? process.env;
|
|
687
770
|
const supabase = options.supabase ?? createLinkedInSessionSupabaseClient(env);
|
|
@@ -708,6 +791,9 @@ export async function claimValidatedSalesNavigatorSessionCookieForCli(options) {
|
|
|
708
791
|
if (!claimedSession) {
|
|
709
792
|
claimedSession = await claimRecoverableInactiveLinkedInSessionCookie(supabase, options.source, exclusions, env, seenCookieHashes);
|
|
710
793
|
}
|
|
794
|
+
if (!claimedSession) {
|
|
795
|
+
claimedSession = await claimStaleActiveLinkedInSessionCookie(supabase, options.source, exclusions, env, seenCookieHashes);
|
|
796
|
+
}
|
|
711
797
|
if (!claimedSession) {
|
|
712
798
|
lastFailureMessage =
|
|
713
799
|
claimErrorMessage ?? "No non-excluded decryptable active LinkedIn session cookies available";
|
package/dist/sales-navigator.js
CHANGED
|
@@ -230,39 +230,47 @@ export const DEFAULT_SALES_NAVIGATOR_ACCOUNT_CRAWL_DIMENSIONS = [
|
|
|
230
230
|
{
|
|
231
231
|
key: "company-headcount",
|
|
232
232
|
filterType: "COMPANY_HEADCOUNT",
|
|
233
|
+
supportsResidualExclusion: true,
|
|
233
234
|
values: [
|
|
234
235
|
{ id: "B", text: "1-10", selectionType: "INCLUDED" },
|
|
235
236
|
{ id: "C", text: "11-50", selectionType: "INCLUDED" },
|
|
236
237
|
{ id: "D", text: "51-200", selectionType: "INCLUDED" },
|
|
237
238
|
{ id: "E", text: "201-500", selectionType: "INCLUDED" },
|
|
238
|
-
{ id: "F", text: "501-
|
|
239
|
-
{ id: "G", text: "
|
|
240
|
-
{ id: "H", text: "
|
|
241
|
-
{ id: "I", text: "10,
|
|
239
|
+
{ id: "F", text: "501-1,000", selectionType: "INCLUDED" },
|
|
240
|
+
{ id: "G", text: "1,001-5,000", selectionType: "INCLUDED" },
|
|
241
|
+
{ id: "H", text: "5,001-10,000", selectionType: "INCLUDED" },
|
|
242
|
+
{ id: "I", text: "10,001+", selectionType: "INCLUDED" },
|
|
242
243
|
],
|
|
243
244
|
},
|
|
244
245
|
{
|
|
245
246
|
key: "number-of-followers",
|
|
246
247
|
filterType: "NUM_OF_FOLLOWERS",
|
|
248
|
+
supportsResidualExclusion: true,
|
|
247
249
|
values: [
|
|
248
|
-
{ id: "NFR1", text: "1-
|
|
249
|
-
{ id: "NFR2", text: "
|
|
250
|
+
{ id: "NFR1", text: "1-50", selectionType: "INCLUDED" },
|
|
251
|
+
{ id: "NFR2", text: "51-100", selectionType: "INCLUDED" },
|
|
250
252
|
{ id: "NFR3", text: "101-1000", selectionType: "INCLUDED" },
|
|
251
253
|
{ id: "NFR4", text: "1001-5000", selectionType: "INCLUDED" },
|
|
252
254
|
{ id: "NFR5", text: "5001+", selectionType: "INCLUDED" },
|
|
253
255
|
],
|
|
254
256
|
},
|
|
255
257
|
{
|
|
256
|
-
key: "
|
|
257
|
-
filterType: "
|
|
258
|
+
key: "annual-revenue",
|
|
259
|
+
filterType: "ANNUAL_REVENUE",
|
|
260
|
+
supportsResidualExclusion: false,
|
|
261
|
+
combinableValues: false,
|
|
258
262
|
values: [
|
|
259
|
-
{
|
|
260
|
-
{
|
|
261
|
-
{ text: "
|
|
262
|
-
{ text: "
|
|
263
|
-
{ text: "
|
|
264
|
-
{ text: "
|
|
265
|
-
{ text: "
|
|
263
|
+
{ text: "USD 0-0.5M", rangeValue: { min: 0, max: 0.5 }, selectedSubFilter: "USD" },
|
|
264
|
+
{ text: "USD 0.5-1M", rangeValue: { min: 0.5, max: 1 }, selectedSubFilter: "USD" },
|
|
265
|
+
{ text: "USD 1-2.5M", rangeValue: { min: 1, max: 2.5 }, selectedSubFilter: "USD" },
|
|
266
|
+
{ text: "USD 2.5-5M", rangeValue: { min: 2.5, max: 5 }, selectedSubFilter: "USD" },
|
|
267
|
+
{ text: "USD 5-10M", rangeValue: { min: 5, max: 10 }, selectedSubFilter: "USD" },
|
|
268
|
+
{ text: "USD 10-20M", rangeValue: { min: 10, max: 20 }, selectedSubFilter: "USD" },
|
|
269
|
+
{ text: "USD 20-50M", rangeValue: { min: 20, max: 50 }, selectedSubFilter: "USD" },
|
|
270
|
+
{ text: "USD 50-100M", rangeValue: { min: 50, max: 100 }, selectedSubFilter: "USD" },
|
|
271
|
+
{ text: "USD 100-500M", rangeValue: { min: 100, max: 500 }, selectedSubFilter: "USD" },
|
|
272
|
+
{ text: "USD 500M-1B", rangeValue: { min: 500, max: 1000 }, selectedSubFilter: "USD" },
|
|
273
|
+
{ text: "USD 1B+", rangeValue: { min: 1000, max: 1001 }, selectedSubFilter: "USD" },
|
|
266
274
|
],
|
|
267
275
|
},
|
|
268
276
|
];
|
|
@@ -358,6 +366,17 @@ function extractFilterType(block) {
|
|
|
358
366
|
return match?.[1] ?? null;
|
|
359
367
|
}
|
|
360
368
|
function buildFilterBlock(filter) {
|
|
369
|
+
const rangeValues = filter.values.filter((value) => value.rangeValue);
|
|
370
|
+
if (rangeValues.length > 0) {
|
|
371
|
+
if (rangeValues.length !== 1 || filter.values.length !== 1) {
|
|
372
|
+
throw new Error(`Sales Navigator range filter ${filter.type} requires exactly one range.`);
|
|
373
|
+
}
|
|
374
|
+
const value = rangeValues[0];
|
|
375
|
+
const range = value.rangeValue;
|
|
376
|
+
const selectedSubFilter = value.selectedSubFilter?.trim();
|
|
377
|
+
return (`(type:${filter.type},rangeValue:(min:${range.min},max:${range.max})` +
|
|
378
|
+
`${selectedSubFilter ? `,selectedSubFilter:${encodeURIComponent(selectedSubFilter)}` : ""})`);
|
|
379
|
+
}
|
|
361
380
|
const encodedValues = filter.values
|
|
362
381
|
.map((value) => {
|
|
363
382
|
const selectionType = value.selectionType ?? "INCLUDED";
|
|
@@ -382,6 +401,9 @@ function dedupeSalesNavigatorFilters(filters) {
|
|
|
382
401
|
value.selectionType ?? "INCLUDED",
|
|
383
402
|
value.text.trim().toLowerCase(),
|
|
384
403
|
value.id?.trim().toLowerCase() ?? "",
|
|
404
|
+
value.rangeValue?.min ?? "",
|
|
405
|
+
value.rangeValue?.max ?? "",
|
|
406
|
+
value.selectedSubFilter?.trim().toLowerCase() ?? "",
|
|
385
407
|
].join("|");
|
|
386
408
|
if (seen.has(key)) {
|
|
387
409
|
return false;
|
|
@@ -710,6 +732,7 @@ function getUnusedSalesNavigatorDimensions(attempt, dimensions) {
|
|
|
710
732
|
function buildSalesNavigatorAdaptiveChildren(attempt, dimension, options) {
|
|
711
733
|
const expandedChildren = expandSalesNavigatorCrawlAttempt(attempt, dimension, options.searchType);
|
|
712
734
|
const residualChildren = options.includeResidualExclusionSlices
|
|
735
|
+
&& dimension.supportsResidualExclusion !== false
|
|
713
736
|
? [
|
|
714
737
|
buildSalesNavigatorResidualCrawlAttempt(attempt, dimension, options.searchType),
|
|
715
738
|
]
|
|
@@ -767,6 +790,9 @@ function buildSalesNavigatorCombinedCrawlAttempt(attempt, dimension, children, s
|
|
|
767
790
|
};
|
|
768
791
|
}
|
|
769
792
|
async function coalesceSalesNavigatorUnderCapChildren(options) {
|
|
793
|
+
if (options.dimension.combinableValues === false) {
|
|
794
|
+
return options.summaries;
|
|
795
|
+
}
|
|
770
796
|
const packable = [];
|
|
771
797
|
const fixed = [];
|
|
772
798
|
for (const summary of options.summaries) {
|
|
@@ -1106,6 +1132,7 @@ export async function executeSalesNavigatorAdaptiveCrawl(options) {
|
|
|
1106
1132
|
if (children.length === 0) {
|
|
1107
1133
|
const expandedChildren = expandSalesNavigatorCrawlAttempt(attempt, nextDimension, options.searchType);
|
|
1108
1134
|
const residualChildren = options.includeResidualExclusionSlices
|
|
1135
|
+
&& nextDimension.supportsResidualExclusion !== false
|
|
1109
1136
|
? [
|
|
1110
1137
|
buildSalesNavigatorResidualCrawlAttempt(attempt, nextDimension, options.searchType),
|
|
1111
1138
|
]
|
package/package.json
CHANGED