Spaces:
Running
Running
| import axios from "axios"; | |
| import { getEnv } from "../../shared/config/env"; | |
| import { withRetry, isCircuitOpen, recordFailure, recordSuccess } from "../../shared/utils/retry"; | |
| import { hunterLimiter } from "../../shared/utils/rate-limiter"; | |
| import { logger } from "../../shared/utils/logger"; | |
| const PROVIDER = "hunter"; | |
| export interface HunterEmailResult { | |
| email: string | null; | |
| score: number; // Hunter confidence 0-100 | |
| source: "hunter"; | |
| firstName: string | null; | |
| lastName: string | null; | |
| } | |
| /** | |
| * Finds a professional email address using Hunter.io. | |
| * Falls through to pattern generation if not found. | |
| */ | |
| export async function findEmail( | |
| domain: string, | |
| firstName: string, | |
| lastName: string | |
| ): Promise<HunterEmailResult | null> { | |
| if (isCircuitOpen(PROVIDER)) return null; | |
| await hunterLimiter.consume(PROVIDER); | |
| try { | |
| const result = await withRetry( | |
| () => callHunterEmailFinder(domain, firstName, lastName), | |
| { provider: PROVIDER } | |
| ); | |
| recordSuccess(PROVIDER); | |
| return result; | |
| } catch (err) { | |
| recordFailure(PROVIDER); | |
| logger.warn({ domain, err }, "Hunter email find failed β will try pattern generation"); | |
| return null; | |
| } | |
| } | |
| /** | |
| * Searches all known emails for a domain (domain search). | |
| */ | |
| export async function searchDomain(domain: string): Promise<HunterEmailResult[]> { | |
| if (isCircuitOpen(PROVIDER)) return []; | |
| await hunterLimiter.consume(PROVIDER); | |
| try { | |
| const result = await withRetry( | |
| () => callHunterDomainSearch(domain), | |
| { provider: PROVIDER } | |
| ); | |
| recordSuccess(PROVIDER); | |
| return result; | |
| } catch (err) { | |
| recordFailure(PROVIDER); | |
| logger.warn({ domain, err }, "Hunter domain search failed"); | |
| return []; | |
| } | |
| } | |
| async function callHunterEmailFinder( | |
| domain: string, | |
| firstName: string, | |
| lastName: string | |
| ): Promise<HunterEmailResult | null> { | |
| const env = getEnv(); | |
| const response = await axios.get("https://api.hunter.io/v2/email-finder", { | |
| params: { | |
| domain, | |
| first_name: firstName, | |
| last_name: lastName, | |
| api_key: env.HUNTER_API_KEY, | |
| }, | |
| timeout: 8_000, | |
| }); | |
| const data = response.data?.data; | |
| if (!data?.email) return null; | |
| return { | |
| email: data.email, | |
| score: data.score ?? 0, | |
| source: "hunter", | |
| firstName: data.first_name ?? null, | |
| lastName: data.last_name ?? null, | |
| }; | |
| } | |
| async function callHunterDomainSearch(domain: string): Promise<HunterEmailResult[]> { | |
| const env = getEnv(); | |
| const response = await axios.get("https://api.hunter.io/v2/domain-search", { | |
| params: { domain, api_key: env.HUNTER_API_KEY, limit: 10 }, | |
| timeout: 8_000, | |
| }); | |
| const emails = response.data?.data?.emails ?? []; | |
| return emails | |
| .filter((e: { type: string }) => e.type === "professional") | |
| .map((e: { value: string; confidence: number; first_name: string; last_name: string }) => ({ | |
| email: e.value, | |
| score: e.confidence, | |
| source: "hunter" as const, | |
| firstName: e.first_name ?? null, | |
| lastName: e.last_name ?? null, | |
| })); | |
| } | |
| // βββ Aliases for contact-enricher.ts compatibility ββββββββββ | |
| export type HunterContact = { | |
| value: string; // email | |
| first_name: string | null; | |
| last_name: string | null; | |
| position: string | null; | |
| seniority: string | null; | |
| confidence: number; | |
| }; | |
| /** | |
| * Search for contacts at a domain β used by contact-enricher. | |
| * Maps Hunter's domain-search response to HunterContact format. | |
| */ | |
| export async function searchHunterContacts(domain: string): Promise<HunterContact[]> { | |
| if (isCircuitOpen(PROVIDER)) return []; | |
| await hunterLimiter.consume(PROVIDER); | |
| try { | |
| const env = getEnv(); | |
| const response = await axios.get("https://api.hunter.io/v2/domain-search", { | |
| params: { domain, api_key: env.HUNTER_API_KEY, limit: 10 }, | |
| timeout: 8_000, | |
| }); | |
| recordSuccess(PROVIDER); | |
| const emails = response.data?.data?.emails ?? []; | |
| return emails.map((e: Record<string, unknown>) => ({ | |
| value: (e.value as string) ?? "", | |
| first_name: (e.first_name as string) ?? null, | |
| last_name: (e.last_name as string) ?? null, | |
| position: (e.position as string) ?? null, | |
| seniority: (e.seniority as string) ?? null, | |
| confidence: (e.confidence as number) ?? 0, | |
| })); | |
| } catch (err) { | |
| recordFailure(PROVIDER); | |
| logger.warn({ domain, err }, "Hunter domain search failed"); | |
| return []; | |
| } | |
| } | |