linkedin-secret-sauce 0.5.0 → 0.6.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.
@@ -4,10 +4,20 @@
4
4
  *
5
5
  * Smartlead's prospect database - a REVERSE-ENGINEERED private API.
6
6
  * Two-phase process: search (free) then fetch (costs credits).
7
+ *
8
+ * Supports two authentication methods:
9
+ * 1. Direct token: Pass `apiToken` directly (for pre-authenticated scenarios)
10
+ * 2. Credentials: Pass `email` and `password` for automatic login with token caching
11
+ *
12
+ * Exports:
13
+ * - createSmartProspectProvider: For email enrichment (waterfall pattern)
14
+ * - createSmartProspectClient: Full client with search/fetch capabilities
7
15
  */
8
16
  Object.defineProperty(exports, "__esModule", { value: true });
9
17
  exports.createSmartProspectProvider = createSmartProspectProvider;
10
- const DEFAULT_API_URL = 'https://prospect-api.smartlead.ai/api/search-email-leads';
18
+ exports.createSmartProspectClient = createSmartProspectClient;
19
+ const smartlead_auth_1 = require("../auth/smartlead-auth");
20
+ const DEFAULT_API_URL = "https://prospect-api.smartlead.ai/api/search-email-leads";
11
21
  /**
12
22
  * Delay helper for retry logic
13
23
  */
@@ -28,7 +38,7 @@ async function requestWithRetry(url, options, retries = 2, backoffMs = 300) {
28
38
  continue;
29
39
  }
30
40
  if (!res.ok) {
31
- const errorText = await res.text().catch(() => '');
41
+ const errorText = await res.text().catch(() => "");
32
42
  throw new Error(`SmartProspect API error: ${res.status} - ${errorText}`);
33
43
  }
34
44
  return (await res.json());
@@ -41,7 +51,7 @@ async function requestWithRetry(url, options, retries = 2, backoffMs = 300) {
41
51
  }
42
52
  }
43
53
  }
44
- throw lastErr ?? new Error('smartprospect_http_error');
54
+ throw lastErr ?? new Error("smartprospect_http_error");
45
55
  }
46
56
  /**
47
57
  * Calculate match score between search input and contact result
@@ -49,30 +59,33 @@ async function requestWithRetry(url, options, retries = 2, backoffMs = 300) {
49
59
  function calculateMatchScore(contact, searchName, company, title) {
50
60
  let score = 0;
51
61
  // Name match scoring (0-50 points)
52
- const contactName = contact.fullName?.toLowerCase() || '';
62
+ const contactName = contact.fullName?.toLowerCase() || "";
53
63
  const searchNameLower = searchName.toLowerCase();
54
64
  if (contactName === searchNameLower) {
55
65
  score += 50; // Exact match
56
66
  }
57
- else if (contactName.includes(searchNameLower) || searchNameLower.includes(contactName)) {
67
+ else if (contactName.includes(searchNameLower) ||
68
+ searchNameLower.includes(contactName)) {
58
69
  score += 30; // Partial match
59
70
  }
60
71
  // Company match scoring (0-30 points)
61
72
  if (company) {
62
- const contactCompany = contact.company?.name?.toLowerCase() || '';
73
+ const contactCompany = contact.company?.name?.toLowerCase() || "";
63
74
  const searchCompany = company.toLowerCase();
64
75
  if (contactCompany === searchCompany) {
65
76
  score += 30; // Exact match
66
77
  }
67
- else if (contactCompany.includes(searchCompany) || searchCompany.includes(contactCompany)) {
78
+ else if (contactCompany.includes(searchCompany) ||
79
+ searchCompany.includes(contactCompany)) {
68
80
  score += 15; // Partial match
69
81
  }
70
82
  }
71
83
  // Title match scoring (0-10 points)
72
84
  if (title) {
73
- const contactTitle = contact.title?.toLowerCase() || '';
85
+ const contactTitle = contact.title?.toLowerCase() || "";
74
86
  const searchTitle = title.toLowerCase();
75
- if (contactTitle.includes(searchTitle) || searchTitle.includes(contactTitle)) {
87
+ if (contactTitle.includes(searchTitle) ||
88
+ searchTitle.includes(contactTitle)) {
76
89
  score += 10;
77
90
  }
78
91
  }
@@ -89,37 +102,72 @@ function extractNames(candidate) {
89
102
  const firstName = candidate.firstName ||
90
103
  candidate.first_name ||
91
104
  candidate.first ||
92
- candidate.name?.split(' ')?.[0] ||
93
- '';
105
+ candidate.name?.split(" ")?.[0] ||
106
+ "";
94
107
  const lastName = candidate.lastName ||
95
108
  candidate.last_name ||
96
109
  candidate.last ||
97
- candidate.name?.split(' ')?.slice(1).join(' ') ||
98
- '';
110
+ candidate.name?.split(" ")?.slice(1).join(" ") ||
111
+ "";
99
112
  return { firstName, lastName };
100
113
  }
101
114
  /**
102
115
  * Create the SmartProspect provider function
116
+ *
117
+ * Supports two auth methods:
118
+ * 1. Direct token: Pass `apiToken` in config
119
+ * 2. Credentials: Pass `email` and `password` in config for auto-login
103
120
  */
104
121
  function createSmartProspectProvider(config) {
105
- const { apiToken, apiUrl = DEFAULT_API_URL } = config;
106
- if (!apiToken) {
122
+ const { apiToken, email, password, apiUrl = DEFAULT_API_URL, loginUrl, eagerInit = true, } = config;
123
+ // Check if we have valid auth config
124
+ const hasDirectToken = !!apiToken;
125
+ const hasCredentials = !!email && !!password;
126
+ if (!hasDirectToken && !hasCredentials) {
107
127
  // Return a no-op provider if not configured
108
128
  const noopProvider = async () => null;
109
- noopProvider.__name = 'smartprospect';
129
+ noopProvider.__name = "smartprospect";
110
130
  return noopProvider;
111
131
  }
132
+ /**
133
+ * Get the current auth token (either direct or via login)
134
+ */
135
+ async function getAuthToken() {
136
+ if (hasDirectToken) {
137
+ return apiToken;
138
+ }
139
+ // Use credentials-based auth with token caching
140
+ return (0, smartlead_auth_1.getSmartLeadToken)({
141
+ credentials: { email: email, password: password },
142
+ loginUrl,
143
+ });
144
+ }
145
+ // Eager initialization: pre-fetch token so it's ready for first request
146
+ if (eagerInit && hasCredentials) {
147
+ // Fire and forget - don't block provider creation
148
+ getAuthToken().catch(() => {
149
+ // Silently ignore errors during eager init - will retry on first request
150
+ });
151
+ }
152
+ /**
153
+ * Handle 401 errors by clearing cached token
154
+ */
155
+ async function handleAuthError() {
156
+ if (hasCredentials) {
157
+ (0, smartlead_auth_1.clearSmartLeadToken)(email);
158
+ }
159
+ }
112
160
  /**
113
161
  * Search for contacts matching filters (FREE - no credits used)
114
162
  */
115
163
  async function searchContacts(filters) {
116
- try {
117
- const response = await requestWithRetry(`${apiUrl}/search-contacts`, {
118
- method: 'POST',
164
+ const makeRequest = async (token) => {
165
+ return requestWithRetry(`${apiUrl}/search-contacts`, {
166
+ method: "POST",
119
167
  headers: {
120
- Authorization: `Bearer ${apiToken}`,
121
- 'Content-Type': 'application/json',
122
- Accept: 'application/json',
168
+ Authorization: `Bearer ${token}`,
169
+ "Content-Type": "application/json",
170
+ Accept: "application/json",
123
171
  },
124
172
  body: JSON.stringify({
125
173
  ...filters,
@@ -128,12 +176,28 @@ function createSmartProspectProvider(config) {
128
176
  titleExactMatch: filters.titleExactMatch ?? false,
129
177
  }),
130
178
  });
131
- return response;
179
+ };
180
+ try {
181
+ const token = await getAuthToken();
182
+ return await makeRequest(token);
132
183
  }
133
- catch {
184
+ catch (err) {
185
+ // On 401, clear token and retry once with fresh token
186
+ if (err instanceof Error &&
187
+ err.message.includes("401") &&
188
+ hasCredentials) {
189
+ await handleAuthError();
190
+ try {
191
+ const freshToken = await getAuthToken();
192
+ return await makeRequest(freshToken);
193
+ }
194
+ catch {
195
+ // Retry failed too
196
+ }
197
+ }
134
198
  return {
135
199
  success: false,
136
- message: 'Search failed',
200
+ message: "Search failed",
137
201
  data: { list: [], total_count: 0 },
138
202
  };
139
203
  }
@@ -142,22 +206,38 @@ function createSmartProspectProvider(config) {
142
206
  * Fetch/enrich emails for specific contact IDs (COSTS CREDITS)
143
207
  */
144
208
  async function fetchContacts(contactIds) {
145
- try {
146
- const response = await requestWithRetry(`${apiUrl}/fetch-contacts`, {
147
- method: 'POST',
209
+ const makeRequest = async (token) => {
210
+ return requestWithRetry(`${apiUrl}/fetch-contacts`, {
211
+ method: "POST",
148
212
  headers: {
149
- Authorization: `Bearer ${apiToken}`,
150
- 'Content-Type': 'application/json',
151
- Accept: 'application/json',
213
+ Authorization: `Bearer ${token}`,
214
+ "Content-Type": "application/json",
215
+ Accept: "application/json",
152
216
  },
153
217
  body: JSON.stringify({ contactIds }),
154
218
  });
155
- return response;
219
+ };
220
+ try {
221
+ const token = await getAuthToken();
222
+ return await makeRequest(token);
156
223
  }
157
- catch {
224
+ catch (err) {
225
+ // On 401, clear token and retry once with fresh token
226
+ if (err instanceof Error &&
227
+ err.message.includes("401") &&
228
+ hasCredentials) {
229
+ await handleAuthError();
230
+ try {
231
+ const freshToken = await getAuthToken();
232
+ return await makeRequest(freshToken);
233
+ }
234
+ catch {
235
+ // Retry failed too
236
+ }
237
+ }
158
238
  return {
159
239
  success: false,
160
- message: 'Fetch failed',
240
+ message: "Fetch failed",
161
241
  data: {
162
242
  list: [],
163
243
  total_count: 0,
@@ -177,73 +257,433 @@ function createSmartProspectProvider(config) {
177
257
  };
178
258
  }
179
259
  }
260
+ /**
261
+ * Calculate confidence score for a contact
262
+ */
263
+ function calculateConfidence(contact) {
264
+ const isVerified = contact.verificationStatus === "valid";
265
+ const isCatchAll = contact.verificationStatus === "catch_all";
266
+ const deliverability = contact.emailDeliverability || 0;
267
+ let confidence = deliverability * 100;
268
+ if (isVerified) {
269
+ confidence = Math.max(confidence, 90);
270
+ }
271
+ else if (isCatchAll) {
272
+ confidence = Math.min(confidence, 75);
273
+ }
274
+ return confidence;
275
+ }
180
276
  async function fetchEmail(candidate) {
181
277
  const { firstName, lastName } = extractNames(candidate);
182
278
  if (!firstName) {
183
279
  return null; // Minimum requirement
184
280
  }
185
281
  const fullName = `${firstName} ${lastName}`.trim();
186
- const company = candidate.company || candidate.currentCompany || candidate.organization || undefined;
187
- const title = candidate.title || candidate.currentRole || candidate.current_role || undefined;
188
- const domain = candidate.domain || candidate.companyDomain || candidate.company_domain || undefined;
189
- // Build search filters
282
+ const company = candidate.company ||
283
+ candidate.currentCompany ||
284
+ candidate.organization ||
285
+ undefined;
286
+ const title = candidate.title ||
287
+ candidate.currentRole ||
288
+ candidate.current_role ||
289
+ undefined;
290
+ const domain = candidate.domain ||
291
+ candidate.companyDomain ||
292
+ candidate.company_domain ||
293
+ undefined;
294
+ // Build search filters using correct API field names
190
295
  const filters = {
191
296
  name: [fullName],
192
- limit: 5, // Get top 5 to find best match
297
+ limit: 10, // Get more results to return multiple emails
193
298
  };
194
299
  if (company) {
195
- filters.company = [company];
300
+ filters.companyName = [company];
196
301
  }
197
302
  if (domain) {
198
- filters.domain = [domain];
303
+ filters.companyDomain = [domain];
199
304
  }
200
305
  // Step 1: Search for contacts (FREE)
201
306
  const searchResult = await searchContacts(filters);
202
307
  if (!searchResult.success || searchResult.data.list.length === 0) {
203
308
  return null;
204
309
  }
205
- // Find best match by scoring
310
+ // Score and sort all matches
206
311
  const matches = searchResult.data.list;
207
- let bestMatch = null;
208
- let bestScore = 0;
209
- for (const contact of matches) {
210
- const score = calculateMatchScore(contact, fullName, company, title);
211
- if (score > bestScore) {
212
- bestScore = score;
213
- bestMatch = contact;
214
- }
215
- }
216
- if (!bestMatch) {
312
+ const scoredMatches = matches
313
+ .map((contact) => ({
314
+ contact,
315
+ score: calculateMatchScore(contact, fullName, company, title),
316
+ }))
317
+ .filter((m) => m.score > 0) // Only include matches with some relevance
318
+ .sort((a, b) => b.score - a.score);
319
+ if (scoredMatches.length === 0) {
217
320
  return null;
218
321
  }
219
- // Step 2: Fetch email for best match (COSTS CREDITS)
220
- const fetchResult = await fetchContacts([bestMatch.id]);
322
+ // Step 2: Fetch emails for ALL matching contacts (COSTS CREDITS)
323
+ // Note: This costs more credits but gives us all available emails
324
+ const contactIds = scoredMatches.map((m) => m.contact.id);
325
+ const fetchResult = await fetchContacts(contactIds);
221
326
  if (!fetchResult.success || fetchResult.data.list.length === 0) {
222
327
  return null;
223
328
  }
224
- const enrichedContact = fetchResult.data.list[0];
225
- const email = enrichedContact?.email;
226
- if (!email) {
329
+ // Build multi-result with all found emails
330
+ const emails = [];
331
+ const seenEmails = new Set();
332
+ for (const enrichedContact of fetchResult.data.list) {
333
+ const email = enrichedContact?.email;
334
+ if (!email)
335
+ continue;
336
+ const emailLower = email.toLowerCase();
337
+ if (seenEmails.has(emailLower))
338
+ continue;
339
+ seenEmails.add(emailLower);
340
+ const isVerified = enrichedContact.verificationStatus === "valid";
341
+ const isCatchAll = enrichedContact.verificationStatus === "catch_all";
342
+ emails.push({
343
+ email,
344
+ verified: isVerified || isCatchAll,
345
+ confidence: calculateConfidence(enrichedContact),
346
+ isCatchAll,
347
+ metadata: {
348
+ fullName: enrichedContact.fullName,
349
+ title: enrichedContact.title,
350
+ company: enrichedContact.company?.name,
351
+ linkedin: enrichedContact.linkedin,
352
+ verificationStatus: enrichedContact.verificationStatus,
353
+ matchScore: scoredMatches.find((m) => m.contact.id === enrichedContact.id)?.score,
354
+ },
355
+ });
356
+ }
357
+ if (emails.length === 0) {
227
358
  return null;
228
359
  }
229
- // Calculate verification confidence
230
- const isVerified = enrichedContact.verificationStatus === 'valid';
231
- const isCatchAll = enrichedContact.verificationStatus === 'catch_all';
232
- const deliverability = enrichedContact.emailDeliverability || 0;
233
- let confidence = deliverability * 100;
234
- if (isVerified) {
235
- confidence = Math.max(confidence, 90);
360
+ // Sort by confidence (highest first)
361
+ emails.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0));
362
+ // Return multi-result format
363
+ return { emails };
364
+ }
365
+ // Mark provider name for orchestrator
366
+ fetchEmail.__name = "smartprospect";
367
+ return fetchEmail;
368
+ }
369
+ /**
370
+ * Create a SmartProspect client for direct API access
371
+ *
372
+ * This provides access to the full SmartProspect API for:
373
+ * - Searching contacts with comprehensive filters (FREE)
374
+ * - Fetching/enriching contact emails (COSTS CREDITS)
375
+ * - Combined search + fetch operations
376
+ *
377
+ * @example
378
+ * ```ts
379
+ * const client = createSmartProspectClient({
380
+ * email: 'user@example.com',
381
+ * password: 'password123'
382
+ * });
383
+ *
384
+ * // Search only (FREE)
385
+ * const results = await client.search({
386
+ * title: ['CEO', 'CTO'],
387
+ * company: ['Google', 'Microsoft'],
388
+ * level: ['C-Level'],
389
+ * companyHeadCount: ['1001-5000', '5001-10000'],
390
+ * limit: 25
391
+ * });
392
+ *
393
+ * // Fetch specific contacts (COSTS CREDITS)
394
+ * const enriched = await client.fetch(['contact-id-1', 'contact-id-2']);
395
+ * ```
396
+ */
397
+ function createSmartProspectClient(config) {
398
+ const { apiToken, email, password, apiUrl = DEFAULT_API_URL, loginUrl, eagerInit = true, } = config;
399
+ // Check if we have valid auth config
400
+ const hasDirectToken = !!apiToken;
401
+ const hasCredentials = !!email && !!password;
402
+ if (!hasDirectToken && !hasCredentials) {
403
+ return null;
404
+ }
405
+ /**
406
+ * Get the current auth token
407
+ */
408
+ async function getAuthToken() {
409
+ if (hasDirectToken) {
410
+ return apiToken;
236
411
  }
237
- else if (isCatchAll) {
238
- confidence = Math.min(confidence, 75);
412
+ return (0, smartlead_auth_1.getSmartLeadToken)({
413
+ credentials: { email: email, password: password },
414
+ loginUrl,
415
+ });
416
+ }
417
+ /**
418
+ * Handle 401 errors
419
+ */
420
+ async function handleAuthError() {
421
+ if (hasCredentials) {
422
+ (0, smartlead_auth_1.clearSmartLeadToken)(email);
423
+ }
424
+ }
425
+ // Eager initialization
426
+ if (eagerInit && hasCredentials) {
427
+ getAuthToken().catch(() => { });
428
+ }
429
+ /**
430
+ * Build the request payload with proper defaults
431
+ * Field names match the actual SmartProspect API
432
+ */
433
+ function buildSearchPayload(filters) {
434
+ const payload = {};
435
+ // Contact-based filters
436
+ if (filters.name?.length)
437
+ payload.name = filters.name;
438
+ if (filters.firstName?.length)
439
+ payload.firstName = filters.firstName;
440
+ if (filters.lastName?.length)
441
+ payload.lastName = filters.lastName;
442
+ // Title filters
443
+ if (filters.title?.length)
444
+ payload.title = filters.title;
445
+ payload.titleExactMatch = filters.titleExactMatch ?? false;
446
+ // Department & Level
447
+ if (filters.department?.length)
448
+ payload.department = filters.department;
449
+ if (filters.level?.length)
450
+ payload.level = filters.level;
451
+ // Company filters (note: API uses companyName and companyDomain)
452
+ if (filters.companyName?.length)
453
+ payload.companyName = filters.companyName;
454
+ if (filters.companyDomain?.length)
455
+ payload.companyDomain = filters.companyDomain;
456
+ // Industry filters (note: API uses companyIndustry and companySubIndustry)
457
+ if (filters.companyIndustry?.length)
458
+ payload.companyIndustry = filters.companyIndustry;
459
+ if (filters.companySubIndustry?.length)
460
+ payload.companySubIndustry = filters.companySubIndustry;
461
+ // Company size filters
462
+ if (filters.companyHeadCount?.length)
463
+ payload.companyHeadCount = filters.companyHeadCount;
464
+ if (filters.companyRevenue?.length)
465
+ payload.companyRevenue = filters.companyRevenue;
466
+ // Location filters
467
+ if (filters.country?.length)
468
+ payload.country = filters.country;
469
+ if (filters.state?.length)
470
+ payload.state = filters.state;
471
+ if (filters.city?.length)
472
+ payload.city = filters.city;
473
+ // Workflow options
474
+ payload.dontDisplayOwnedContact = filters.dontDisplayOwnedContact ?? true;
475
+ // Pagination
476
+ payload.limit = filters.limit ?? 25;
477
+ if (filters.scroll_id)
478
+ payload.scroll_id = filters.scroll_id;
479
+ return payload;
480
+ }
481
+ /**
482
+ * Search for contacts (FREE - no credits)
483
+ */
484
+ async function search(filters) {
485
+ const payload = buildSearchPayload(filters);
486
+ const makeRequest = async (token) => {
487
+ return requestWithRetry(`${apiUrl}/search-contacts`, {
488
+ method: "POST",
489
+ headers: {
490
+ Authorization: `Bearer ${token}`,
491
+ "Content-Type": "application/json",
492
+ Accept: "application/json",
493
+ },
494
+ body: JSON.stringify(payload),
495
+ });
496
+ };
497
+ try {
498
+ const token = await getAuthToken();
499
+ return await makeRequest(token);
500
+ }
501
+ catch (err) {
502
+ if (err instanceof Error &&
503
+ err.message.includes("401") &&
504
+ hasCredentials) {
505
+ await handleAuthError();
506
+ try {
507
+ const freshToken = await getAuthToken();
508
+ return await makeRequest(freshToken);
509
+ }
510
+ catch {
511
+ // Retry failed
512
+ }
513
+ }
514
+ return {
515
+ success: false,
516
+ message: err instanceof Error ? err.message : "Search failed",
517
+ data: { list: [], total_count: 0 },
518
+ };
519
+ }
520
+ }
521
+ /**
522
+ * Fetch/enrich emails for specific contact IDs (COSTS CREDITS)
523
+ */
524
+ async function fetch(contactIds) {
525
+ if (!contactIds.length) {
526
+ return {
527
+ success: true,
528
+ message: "No contacts to fetch",
529
+ data: {
530
+ list: [],
531
+ total_count: 0,
532
+ metrics: {
533
+ totalContacts: 0,
534
+ totalEmails: 0,
535
+ noEmailFound: 0,
536
+ invalidEmails: 0,
537
+ catchAllEmails: 0,
538
+ verifiedEmails: 0,
539
+ completed: 0,
540
+ },
541
+ leads_found: 0,
542
+ email_fetched: 0,
543
+ verification_status_list: [],
544
+ },
545
+ };
546
+ }
547
+ const makeRequest = async (token) => {
548
+ return requestWithRetry(`${apiUrl}/fetch-contacts`, {
549
+ method: "POST",
550
+ headers: {
551
+ Authorization: `Bearer ${token}`,
552
+ "Content-Type": "application/json",
553
+ Accept: "application/json",
554
+ },
555
+ body: JSON.stringify({ contactIds }),
556
+ });
557
+ };
558
+ try {
559
+ const token = await getAuthToken();
560
+ return await makeRequest(token);
561
+ }
562
+ catch (err) {
563
+ if (err instanceof Error &&
564
+ err.message.includes("401") &&
565
+ hasCredentials) {
566
+ await handleAuthError();
567
+ try {
568
+ const freshToken = await getAuthToken();
569
+ return await makeRequest(freshToken);
570
+ }
571
+ catch {
572
+ // Retry failed
573
+ }
574
+ }
575
+ return {
576
+ success: false,
577
+ message: err instanceof Error ? err.message : "Fetch failed",
578
+ data: {
579
+ list: [],
580
+ total_count: 0,
581
+ metrics: {
582
+ totalContacts: 0,
583
+ totalEmails: 0,
584
+ noEmailFound: 0,
585
+ invalidEmails: 0,
586
+ catchAllEmails: 0,
587
+ verifiedEmails: 0,
588
+ completed: 0,
589
+ },
590
+ leads_found: 0,
591
+ email_fetched: 0,
592
+ verification_status_list: [],
593
+ },
594
+ };
239
595
  }
596
+ }
597
+ /**
598
+ * Search and immediately fetch all results
599
+ */
600
+ async function searchAndFetch(filters) {
601
+ const searchResponse = await search(filters);
602
+ if (!searchResponse.success || searchResponse.data.list.length === 0) {
603
+ return {
604
+ searchResponse,
605
+ fetchResponse: null,
606
+ contacts: [],
607
+ };
608
+ }
609
+ const contactIds = searchResponse.data.list.map((c) => c.id);
610
+ const fetchResponse = await fetch(contactIds);
240
611
  return {
241
- email,
242
- verified: isVerified || isCatchAll, // Accept catch-all as semi-verified
243
- score: confidence,
612
+ searchResponse,
613
+ fetchResponse,
614
+ contacts: fetchResponse.success ? fetchResponse.data.list : [],
244
615
  };
245
616
  }
246
- // Mark provider name for orchestrator
247
- fetchEmail.__name = 'smartprospect';
248
- return fetchEmail;
617
+ /**
618
+ * Generic location lookup (countries, states, cities)
619
+ */
620
+ async function getLocations(type, options = {}) {
621
+ const { search: searchQuery, limit = 20, offset = 0 } = options;
622
+ const params = new URLSearchParams();
623
+ params.set('limit', String(limit));
624
+ if (offset > 0)
625
+ params.set('offset', String(offset));
626
+ if (searchQuery)
627
+ params.set('search', searchQuery);
628
+ const makeRequest = async (token) => {
629
+ return requestWithRetry(`${apiUrl}/${type}?${params.toString()}`, {
630
+ method: "GET",
631
+ headers: {
632
+ Authorization: `Bearer ${token}`,
633
+ Accept: "application/json",
634
+ },
635
+ });
636
+ };
637
+ try {
638
+ const token = await getAuthToken();
639
+ return await makeRequest(token);
640
+ }
641
+ catch (err) {
642
+ if (err instanceof Error &&
643
+ err.message.includes("401") &&
644
+ hasCredentials) {
645
+ await handleAuthError();
646
+ try {
647
+ const freshToken = await getAuthToken();
648
+ return await makeRequest(freshToken);
649
+ }
650
+ catch {
651
+ // Retry failed
652
+ }
653
+ }
654
+ return {
655
+ success: false,
656
+ message: err instanceof Error ? err.message : "Lookup failed",
657
+ data: [],
658
+ pagination: { limit, offset, page: 1, count: 0 },
659
+ search: searchQuery || null,
660
+ };
661
+ }
662
+ }
663
+ /**
664
+ * Lookup countries (for typeahead)
665
+ */
666
+ async function getCountries(options) {
667
+ return getLocations('countries', options);
668
+ }
669
+ /**
670
+ * Lookup states (for typeahead)
671
+ */
672
+ async function getStates(options) {
673
+ return getLocations('states', options);
674
+ }
675
+ /**
676
+ * Lookup cities (for typeahead)
677
+ */
678
+ async function getCities(options) {
679
+ return getLocations('cities', options);
680
+ }
681
+ return {
682
+ search,
683
+ fetch,
684
+ searchAndFetch,
685
+ getCountries,
686
+ getStates,
687
+ getCities,
688
+ };
249
689
  }