linkedin-secret-sauce 0.5.1 → 0.7.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.
@@ -8,9 +8,14 @@
8
8
  * Supports two authentication methods:
9
9
  * 1. Direct token: Pass `apiToken` directly (for pre-authenticated scenarios)
10
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
11
15
  */
12
16
  Object.defineProperty(exports, "__esModule", { value: true });
13
17
  exports.createSmartProspectProvider = createSmartProspectProvider;
18
+ exports.createSmartProspectClient = createSmartProspectClient;
14
19
  const smartlead_auth_1 = require("../auth/smartlead-auth");
15
20
  const DEFAULT_API_URL = "https://prospect-api.smartlead.ai/api/search-email-leads";
16
21
  /**
@@ -19,6 +24,12 @@ const DEFAULT_API_URL = "https://prospect-api.smartlead.ai/api/search-email-lead
19
24
  async function delay(ms) {
20
25
  return new Promise((r) => setTimeout(r, ms));
21
26
  }
27
+ const DEFAULT_POLLING_CONFIG = {
28
+ initialDelay: 500,
29
+ pollInterval: 1000,
30
+ maxAttempts: 10,
31
+ maxWaitTime: 15000,
32
+ };
22
33
  /**
23
34
  * HTTP request with retry on 429 rate limit
24
35
  */
@@ -197,11 +208,85 @@ function createSmartProspectProvider(config) {
197
208
  };
198
209
  }
199
210
  }
211
+ /**
212
+ * Poll get-contacts until email lookup is complete (provider version)
213
+ */
214
+ async function pollForContactResults(filterId, expectedCount) {
215
+ const { initialDelay, pollInterval, maxAttempts, maxWaitTime } = DEFAULT_POLLING_CONFIG;
216
+ const startTime = Date.now();
217
+ await delay(initialDelay);
218
+ const makeRequest = async (token) => {
219
+ return requestWithRetry(`${apiUrl}/get-contacts`, {
220
+ method: "POST",
221
+ headers: {
222
+ Authorization: `Bearer ${token}`,
223
+ "Content-Type": "application/json",
224
+ Accept: "application/json",
225
+ },
226
+ body: JSON.stringify({
227
+ filter_id: filterId,
228
+ limit: expectedCount,
229
+ offset: 0,
230
+ }),
231
+ });
232
+ };
233
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
234
+ if (Date.now() - startTime > maxWaitTime) {
235
+ break;
236
+ }
237
+ try {
238
+ const token = await getAuthToken();
239
+ const result = await makeRequest(token);
240
+ if (result.success && result.data.metrics) {
241
+ const { completed, totalContacts } = result.data.metrics;
242
+ if (completed >= totalContacts || completed >= expectedCount) {
243
+ return result;
244
+ }
245
+ }
246
+ await delay(pollInterval);
247
+ }
248
+ catch (err) {
249
+ if (err instanceof Error && err.message.includes("401") && hasCredentials) {
250
+ await handleAuthError();
251
+ }
252
+ await delay(pollInterval);
253
+ }
254
+ }
255
+ // Return last result
256
+ try {
257
+ const token = await getAuthToken();
258
+ return await makeRequest(token);
259
+ }
260
+ catch {
261
+ return {
262
+ success: false,
263
+ message: "Polling timed out",
264
+ data: {
265
+ list: [],
266
+ total_count: 0,
267
+ metrics: {
268
+ totalContacts: 0,
269
+ totalEmails: 0,
270
+ noEmailFound: 0,
271
+ invalidEmails: 0,
272
+ catchAllEmails: 0,
273
+ verifiedEmails: 0,
274
+ completed: 0,
275
+ },
276
+ },
277
+ };
278
+ }
279
+ }
200
280
  /**
201
281
  * Fetch/enrich emails for specific contact IDs (COSTS CREDITS)
282
+ *
283
+ * Uses async polling pattern:
284
+ * 1. Call fetch-contacts (returns immediately with status: "pending")
285
+ * 2. Poll get-contacts until completed
286
+ * 3. Return enriched contacts with emails
202
287
  */
203
288
  async function fetchContacts(contactIds) {
204
- const makeRequest = async (token) => {
289
+ const makeFetchRequest = async (token) => {
205
290
  return requestWithRetry(`${apiUrl}/fetch-contacts`, {
206
291
  method: "POST",
207
292
  headers: {
@@ -214,7 +299,30 @@ function createSmartProspectProvider(config) {
214
299
  };
215
300
  try {
216
301
  const token = await getAuthToken();
217
- return await makeRequest(token);
302
+ const fetchResult = await makeFetchRequest(token);
303
+ // If fetch-contacts returns pending status with filter_id, poll for results
304
+ const filterId = fetchResult.data?.filter_id;
305
+ const status = fetchResult.data?.status;
306
+ if (filterId && status === "pending") {
307
+ const pollResult = await pollForContactResults(filterId, contactIds.length);
308
+ if (pollResult.success && pollResult.data.list.length > 0) {
309
+ return {
310
+ success: true,
311
+ message: "Fetch completed",
312
+ data: {
313
+ list: pollResult.data.list,
314
+ total_count: pollResult.data.total_count,
315
+ metrics: pollResult.data.metrics,
316
+ leads_found: pollResult.data.metrics.totalContacts,
317
+ email_fetched: pollResult.data.metrics.totalEmails,
318
+ verification_status_list: [],
319
+ filter_id: filterId,
320
+ status: "completed",
321
+ },
322
+ };
323
+ }
324
+ }
325
+ return fetchResult;
218
326
  }
219
327
  catch (err) {
220
328
  // On 401, clear token and retry once with fresh token
@@ -224,7 +332,7 @@ function createSmartProspectProvider(config) {
224
332
  await handleAuthError();
225
333
  try {
226
334
  const freshToken = await getAuthToken();
227
- return await makeRequest(freshToken);
335
+ return await makeFetchRequest(freshToken);
228
336
  }
229
337
  catch {
230
338
  // Retry failed too
@@ -252,6 +360,22 @@ function createSmartProspectProvider(config) {
252
360
  };
253
361
  }
254
362
  }
363
+ /**
364
+ * Calculate confidence score for a contact
365
+ */
366
+ function calculateConfidence(contact) {
367
+ const isVerified = contact.verificationStatus === "valid";
368
+ const isCatchAll = contact.verificationStatus === "catch_all";
369
+ const deliverability = contact.emailDeliverability || 0;
370
+ let confidence = deliverability * 100;
371
+ if (isVerified) {
372
+ confidence = Math.max(confidence, 90);
373
+ }
374
+ else if (isCatchAll) {
375
+ confidence = Math.min(confidence, 75);
376
+ }
377
+ return confidence;
378
+ }
255
379
  async function fetchEmail(candidate) {
256
380
  const { firstName, lastName } = extractNames(candidate);
257
381
  if (!firstName) {
@@ -270,64 +394,507 @@ function createSmartProspectProvider(config) {
270
394
  candidate.companyDomain ||
271
395
  candidate.company_domain ||
272
396
  undefined;
273
- // Build search filters
397
+ // Build search filters using correct API field names
274
398
  const filters = {
275
399
  name: [fullName],
276
- limit: 5, // Get top 5 to find best match
400
+ limit: 10, // Get more results to return multiple emails
277
401
  };
278
402
  if (company) {
279
- filters.company = [company];
403
+ filters.companyName = [company];
280
404
  }
281
405
  if (domain) {
282
- filters.domain = [domain];
406
+ filters.companyDomain = [domain];
283
407
  }
284
408
  // Step 1: Search for contacts (FREE)
285
409
  const searchResult = await searchContacts(filters);
286
410
  if (!searchResult.success || searchResult.data.list.length === 0) {
287
411
  return null;
288
412
  }
289
- // Find best match by scoring
413
+ // Score and sort all matches
290
414
  const matches = searchResult.data.list;
291
- let bestMatch = null;
292
- let bestScore = 0;
293
- for (const contact of matches) {
294
- const score = calculateMatchScore(contact, fullName, company, title);
295
- if (score > bestScore) {
296
- bestScore = score;
297
- bestMatch = contact;
298
- }
299
- }
300
- if (!bestMatch) {
415
+ const scoredMatches = matches
416
+ .map((contact) => ({
417
+ contact,
418
+ score: calculateMatchScore(contact, fullName, company, title),
419
+ }))
420
+ .filter((m) => m.score > 0) // Only include matches with some relevance
421
+ .sort((a, b) => b.score - a.score);
422
+ if (scoredMatches.length === 0) {
301
423
  return null;
302
424
  }
303
- // Step 2: Fetch email for best match (COSTS CREDITS)
304
- const fetchResult = await fetchContacts([bestMatch.id]);
425
+ // Step 2: Fetch emails for ALL matching contacts (COSTS CREDITS)
426
+ // Note: This costs more credits but gives us all available emails
427
+ const contactIds = scoredMatches.map((m) => m.contact.id);
428
+ const fetchResult = await fetchContacts(contactIds);
305
429
  if (!fetchResult.success || fetchResult.data.list.length === 0) {
306
430
  return null;
307
431
  }
308
- const enrichedContact = fetchResult.data.list[0];
309
- const email = enrichedContact?.email;
310
- if (!email) {
432
+ // Build multi-result with all found emails
433
+ const emails = [];
434
+ const seenEmails = new Set();
435
+ for (const enrichedContact of fetchResult.data.list) {
436
+ const email = enrichedContact?.email;
437
+ if (!email)
438
+ continue;
439
+ const emailLower = email.toLowerCase();
440
+ if (seenEmails.has(emailLower))
441
+ continue;
442
+ seenEmails.add(emailLower);
443
+ const isVerified = enrichedContact.verificationStatus === "valid";
444
+ const isCatchAll = enrichedContact.verificationStatus === "catch_all";
445
+ emails.push({
446
+ email,
447
+ verified: isVerified || isCatchAll,
448
+ confidence: calculateConfidence(enrichedContact),
449
+ isCatchAll,
450
+ metadata: {
451
+ fullName: enrichedContact.fullName,
452
+ title: enrichedContact.title,
453
+ company: enrichedContact.company?.name,
454
+ linkedin: enrichedContact.linkedin,
455
+ verificationStatus: enrichedContact.verificationStatus,
456
+ matchScore: scoredMatches.find((m) => m.contact.id === enrichedContact.id)?.score,
457
+ },
458
+ });
459
+ }
460
+ if (emails.length === 0) {
311
461
  return null;
312
462
  }
313
- // Calculate verification confidence
314
- const isVerified = enrichedContact.verificationStatus === "valid";
315
- const isCatchAll = enrichedContact.verificationStatus === "catch_all";
316
- const deliverability = enrichedContact.emailDeliverability || 0;
317
- let confidence = deliverability * 100;
318
- if (isVerified) {
319
- confidence = Math.max(confidence, 90);
463
+ // Sort by confidence (highest first)
464
+ emails.sort((a, b) => (b.confidence ?? 0) - (a.confidence ?? 0));
465
+ // Return multi-result format
466
+ return { emails };
467
+ }
468
+ // Mark provider name for orchestrator
469
+ fetchEmail.__name = "smartprospect";
470
+ return fetchEmail;
471
+ }
472
+ /**
473
+ * Create a SmartProspect client for direct API access
474
+ *
475
+ * This provides access to the full SmartProspect API for:
476
+ * - Searching contacts with comprehensive filters (FREE)
477
+ * - Fetching/enriching contact emails (COSTS CREDITS)
478
+ * - Combined search + fetch operations
479
+ *
480
+ * @example
481
+ * ```ts
482
+ * const client = createSmartProspectClient({
483
+ * email: 'user@example.com',
484
+ * password: 'password123'
485
+ * });
486
+ *
487
+ * // Search only (FREE)
488
+ * const results = await client.search({
489
+ * title: ['CEO', 'CTO'],
490
+ * company: ['Google', 'Microsoft'],
491
+ * level: ['C-Level'],
492
+ * companyHeadCount: ['1001-5000', '5001-10000'],
493
+ * limit: 25
494
+ * });
495
+ *
496
+ * // Fetch specific contacts (COSTS CREDITS)
497
+ * const enriched = await client.fetch(['contact-id-1', 'contact-id-2']);
498
+ * ```
499
+ */
500
+ function createSmartProspectClient(config) {
501
+ const { apiToken, email, password, apiUrl = DEFAULT_API_URL, loginUrl, eagerInit = true, } = config;
502
+ // Check if we have valid auth config
503
+ const hasDirectToken = !!apiToken;
504
+ const hasCredentials = !!email && !!password;
505
+ if (!hasDirectToken && !hasCredentials) {
506
+ return null;
507
+ }
508
+ /**
509
+ * Get the current auth token
510
+ */
511
+ async function getAuthToken() {
512
+ if (hasDirectToken) {
513
+ return apiToken;
320
514
  }
321
- else if (isCatchAll) {
322
- confidence = Math.min(confidence, 75);
515
+ return (0, smartlead_auth_1.getSmartLeadToken)({
516
+ credentials: { email: email, password: password },
517
+ loginUrl,
518
+ });
519
+ }
520
+ /**
521
+ * Handle 401 errors
522
+ */
523
+ async function handleAuthError() {
524
+ if (hasCredentials) {
525
+ (0, smartlead_auth_1.clearSmartLeadToken)(email);
526
+ }
527
+ }
528
+ // Eager initialization
529
+ if (eagerInit && hasCredentials) {
530
+ getAuthToken().catch(() => { });
531
+ }
532
+ /**
533
+ * Build the request payload with proper defaults
534
+ * Field names match the actual SmartProspect API
535
+ */
536
+ function buildSearchPayload(filters) {
537
+ const payload = {};
538
+ // Contact-based filters
539
+ if (filters.name?.length)
540
+ payload.name = filters.name;
541
+ if (filters.firstName?.length)
542
+ payload.firstName = filters.firstName;
543
+ if (filters.lastName?.length)
544
+ payload.lastName = filters.lastName;
545
+ // Title filters
546
+ if (filters.title?.length)
547
+ payload.title = filters.title;
548
+ payload.titleExactMatch = filters.titleExactMatch ?? false;
549
+ // Department & Level
550
+ if (filters.department?.length)
551
+ payload.department = filters.department;
552
+ if (filters.level?.length)
553
+ payload.level = filters.level;
554
+ // Company filters (note: API uses companyName and companyDomain)
555
+ if (filters.companyName?.length)
556
+ payload.companyName = filters.companyName;
557
+ if (filters.companyDomain?.length)
558
+ payload.companyDomain = filters.companyDomain;
559
+ // Industry filters (note: API uses companyIndustry and companySubIndustry)
560
+ if (filters.companyIndustry?.length)
561
+ payload.companyIndustry = filters.companyIndustry;
562
+ if (filters.companySubIndustry?.length)
563
+ payload.companySubIndustry = filters.companySubIndustry;
564
+ // Company size filters
565
+ if (filters.companyHeadCount?.length)
566
+ payload.companyHeadCount = filters.companyHeadCount;
567
+ if (filters.companyRevenue?.length)
568
+ payload.companyRevenue = filters.companyRevenue;
569
+ // Location filters
570
+ if (filters.country?.length)
571
+ payload.country = filters.country;
572
+ if (filters.state?.length)
573
+ payload.state = filters.state;
574
+ if (filters.city?.length)
575
+ payload.city = filters.city;
576
+ // Workflow options
577
+ payload.dontDisplayOwnedContact = filters.dontDisplayOwnedContact ?? true;
578
+ // Pagination
579
+ payload.limit = filters.limit ?? 25;
580
+ if (filters.scroll_id)
581
+ payload.scroll_id = filters.scroll_id;
582
+ return payload;
583
+ }
584
+ /**
585
+ * Search for contacts (FREE - no credits)
586
+ */
587
+ async function search(filters) {
588
+ const payload = buildSearchPayload(filters);
589
+ const makeRequest = async (token) => {
590
+ return requestWithRetry(`${apiUrl}/search-contacts`, {
591
+ method: "POST",
592
+ headers: {
593
+ Authorization: `Bearer ${token}`,
594
+ "Content-Type": "application/json",
595
+ Accept: "application/json",
596
+ },
597
+ body: JSON.stringify(payload),
598
+ });
599
+ };
600
+ try {
601
+ const token = await getAuthToken();
602
+ return await makeRequest(token);
603
+ }
604
+ catch (err) {
605
+ if (err instanceof Error &&
606
+ err.message.includes("401") &&
607
+ hasCredentials) {
608
+ await handleAuthError();
609
+ try {
610
+ const freshToken = await getAuthToken();
611
+ return await makeRequest(freshToken);
612
+ }
613
+ catch {
614
+ // Retry failed
615
+ }
616
+ }
617
+ return {
618
+ success: false,
619
+ message: err instanceof Error ? err.message : "Search failed",
620
+ data: { list: [], total_count: 0 },
621
+ };
622
+ }
623
+ }
624
+ /**
625
+ * Poll get-contacts until email lookup is complete
626
+ */
627
+ async function pollForResults(filterId, expectedCount, pollingConfig = DEFAULT_POLLING_CONFIG) {
628
+ const { initialDelay, pollInterval, maxAttempts, maxWaitTime } = pollingConfig;
629
+ const startTime = Date.now();
630
+ // Wait initial delay before first poll
631
+ await delay(initialDelay);
632
+ const makeRequest = async (token) => {
633
+ return requestWithRetry(`${apiUrl}/get-contacts`, {
634
+ method: "POST",
635
+ headers: {
636
+ Authorization: `Bearer ${token}`,
637
+ "Content-Type": "application/json",
638
+ Accept: "application/json",
639
+ },
640
+ body: JSON.stringify({
641
+ filter_id: filterId,
642
+ limit: expectedCount,
643
+ offset: 0,
644
+ }),
645
+ });
646
+ };
647
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
648
+ // Check if we've exceeded max wait time
649
+ if (Date.now() - startTime > maxWaitTime) {
650
+ break;
651
+ }
652
+ try {
653
+ const token = await getAuthToken();
654
+ const result = await makeRequest(token);
655
+ if (result.success && result.data.metrics) {
656
+ const { completed, totalContacts } = result.data.metrics;
657
+ // Check if all contacts have been processed
658
+ if (completed >= totalContacts || completed >= expectedCount) {
659
+ return result;
660
+ }
661
+ }
662
+ // Wait before next poll
663
+ await delay(pollInterval);
664
+ }
665
+ catch (err) {
666
+ // On 401, clear token and retry
667
+ if (err instanceof Error &&
668
+ err.message.includes("401") &&
669
+ hasCredentials) {
670
+ await handleAuthError();
671
+ }
672
+ // Continue polling on errors
673
+ await delay(pollInterval);
674
+ }
675
+ }
676
+ // Return last result even if not complete
677
+ try {
678
+ const token = await getAuthToken();
679
+ return await makeRequest(token);
680
+ }
681
+ catch {
682
+ return {
683
+ success: false,
684
+ message: "Polling timed out",
685
+ data: {
686
+ list: [],
687
+ total_count: 0,
688
+ metrics: {
689
+ totalContacts: 0,
690
+ totalEmails: 0,
691
+ noEmailFound: 0,
692
+ invalidEmails: 0,
693
+ catchAllEmails: 0,
694
+ verifiedEmails: 0,
695
+ completed: 0,
696
+ },
697
+ },
698
+ };
323
699
  }
700
+ }
701
+ /**
702
+ * Fetch/enrich emails for specific contact IDs (COSTS CREDITS)
703
+ *
704
+ * SmartProspect uses async email lookup:
705
+ * 1. Call fetch-contacts to initiate lookup (returns immediately with status: "pending")
706
+ * 2. Poll get-contacts until metrics.completed >= expected count
707
+ * 3. Return the final enriched contacts with emails
708
+ */
709
+ async function fetch(contactIds) {
710
+ if (!contactIds.length) {
711
+ return {
712
+ success: true,
713
+ message: "No contacts to fetch",
714
+ data: {
715
+ list: [],
716
+ total_count: 0,
717
+ metrics: {
718
+ totalContacts: 0,
719
+ totalEmails: 0,
720
+ noEmailFound: 0,
721
+ invalidEmails: 0,
722
+ catchAllEmails: 0,
723
+ verifiedEmails: 0,
724
+ completed: 0,
725
+ },
726
+ leads_found: 0,
727
+ email_fetched: 0,
728
+ verification_status_list: [],
729
+ },
730
+ };
731
+ }
732
+ const makeFetchRequest = async (token) => {
733
+ return requestWithRetry(`${apiUrl}/fetch-contacts`, {
734
+ method: "POST",
735
+ headers: {
736
+ Authorization: `Bearer ${token}`,
737
+ "Content-Type": "application/json",
738
+ Accept: "application/json",
739
+ },
740
+ body: JSON.stringify({ contactIds }),
741
+ });
742
+ };
743
+ try {
744
+ const token = await getAuthToken();
745
+ const fetchResult = await makeFetchRequest(token);
746
+ // If fetch-contacts returns a filter_id with pending status, we need to poll
747
+ const filterId = fetchResult.data?.filter_id;
748
+ const status = fetchResult.data?.status;
749
+ if (filterId && status === "pending") {
750
+ // Poll get-contacts for final results
751
+ const pollResult = await pollForResults(filterId, contactIds.length);
752
+ if (pollResult.success && pollResult.data.list.length > 0) {
753
+ // Convert poll result to fetch response format
754
+ return {
755
+ success: true,
756
+ message: "Fetch completed",
757
+ data: {
758
+ list: pollResult.data.list,
759
+ total_count: pollResult.data.total_count,
760
+ metrics: pollResult.data.metrics,
761
+ leads_found: pollResult.data.metrics.totalContacts,
762
+ email_fetched: pollResult.data.metrics.totalEmails,
763
+ verification_status_list: [],
764
+ filter_id: filterId,
765
+ status: "completed",
766
+ },
767
+ };
768
+ }
769
+ }
770
+ // If no polling needed or polling failed, return original result
771
+ return fetchResult;
772
+ }
773
+ catch (err) {
774
+ if (err instanceof Error &&
775
+ err.message.includes("401") &&
776
+ hasCredentials) {
777
+ await handleAuthError();
778
+ try {
779
+ const freshToken = await getAuthToken();
780
+ return await makeFetchRequest(freshToken);
781
+ }
782
+ catch {
783
+ // Retry failed
784
+ }
785
+ }
786
+ return {
787
+ success: false,
788
+ message: err instanceof Error ? err.message : "Fetch failed",
789
+ data: {
790
+ list: [],
791
+ total_count: 0,
792
+ metrics: {
793
+ totalContacts: 0,
794
+ totalEmails: 0,
795
+ noEmailFound: 0,
796
+ invalidEmails: 0,
797
+ catchAllEmails: 0,
798
+ verifiedEmails: 0,
799
+ completed: 0,
800
+ },
801
+ leads_found: 0,
802
+ email_fetched: 0,
803
+ verification_status_list: [],
804
+ },
805
+ };
806
+ }
807
+ }
808
+ /**
809
+ * Search and immediately fetch all results
810
+ */
811
+ async function searchAndFetch(filters) {
812
+ const searchResponse = await search(filters);
813
+ if (!searchResponse.success || searchResponse.data.list.length === 0) {
814
+ return {
815
+ searchResponse,
816
+ fetchResponse: null,
817
+ contacts: [],
818
+ };
819
+ }
820
+ const contactIds = searchResponse.data.list.map((c) => c.id);
821
+ const fetchResponse = await fetch(contactIds);
324
822
  return {
325
- email,
326
- verified: isVerified || isCatchAll, // Accept catch-all as semi-verified
327
- score: confidence,
823
+ searchResponse,
824
+ fetchResponse,
825
+ contacts: fetchResponse.success ? fetchResponse.data.list : [],
328
826
  };
329
827
  }
330
- // Mark provider name for orchestrator
331
- fetchEmail.__name = "smartprospect";
332
- return fetchEmail;
828
+ /**
829
+ * Generic location lookup (countries, states, cities)
830
+ */
831
+ async function getLocations(type, options = {}) {
832
+ const { search: searchQuery, limit = 20, offset = 0 } = options;
833
+ const params = new URLSearchParams();
834
+ params.set('limit', String(limit));
835
+ if (offset > 0)
836
+ params.set('offset', String(offset));
837
+ if (searchQuery)
838
+ params.set('search', searchQuery);
839
+ const makeRequest = async (token) => {
840
+ return requestWithRetry(`${apiUrl}/${type}?${params.toString()}`, {
841
+ method: "GET",
842
+ headers: {
843
+ Authorization: `Bearer ${token}`,
844
+ Accept: "application/json",
845
+ },
846
+ });
847
+ };
848
+ try {
849
+ const token = await getAuthToken();
850
+ return await makeRequest(token);
851
+ }
852
+ catch (err) {
853
+ if (err instanceof Error &&
854
+ err.message.includes("401") &&
855
+ hasCredentials) {
856
+ await handleAuthError();
857
+ try {
858
+ const freshToken = await getAuthToken();
859
+ return await makeRequest(freshToken);
860
+ }
861
+ catch {
862
+ // Retry failed
863
+ }
864
+ }
865
+ return {
866
+ success: false,
867
+ message: err instanceof Error ? err.message : "Lookup failed",
868
+ data: [],
869
+ pagination: { limit, offset, page: 1, count: 0 },
870
+ search: searchQuery || null,
871
+ };
872
+ }
873
+ }
874
+ /**
875
+ * Lookup countries (for typeahead)
876
+ */
877
+ async function getCountries(options) {
878
+ return getLocations('countries', options);
879
+ }
880
+ /**
881
+ * Lookup states (for typeahead)
882
+ */
883
+ async function getStates(options) {
884
+ return getLocations('states', options);
885
+ }
886
+ /**
887
+ * Lookup cities (for typeahead)
888
+ */
889
+ async function getCities(options) {
890
+ return getLocations('cities', options);
891
+ }
892
+ return {
893
+ search,
894
+ fetch,
895
+ searchAndFetch,
896
+ getCountries,
897
+ getStates,
898
+ getCities,
899
+ };
333
900
  }