Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions plugins/hunter/credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type HunterCredentials = {
HUNTER_API_KEY?: string;
};
14 changes: 14 additions & 0 deletions plugins/hunter/icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export function HunterIcon({ className }: { className?: string }) {
return (
<svg
aria-label="Hunter logo"
className={className}
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>Hunter</title>
<path d="M12.0671 8.43455C11.6625 8.55094 11.2164 8.55288 10.7992 8.53525C10.3141 8.51472 9.80024 8.45339 9.35223 8.25426C8.98359 8.09047 8.68787 7.79493 8.84262 7.36805C8.95175 7.06699 9.19361 6.79803 9.47319 6.64644C9.78751 6.4759 10.1329 6.50361 10.4474 6.65774C10.8005 6.83082 11.0942 7.11235 11.3604 7.3964C11.5 7.54536 11.6332 7.70002 11.7646 7.85617C11.8252 7.92801 12.2364 8.33865 12.0671 8.43455ZM18.7923 8.58131C18.17 8.43655 17.4348 8.4884 16.811 8.38867C15.8284 8.23146 14.3648 7.08576 13.5714 5.92122C13.0201 5.11202 12.757 4.28785 12.3356 3.28356C12.0415 2.58257 11.4001 0.365389 10.5032 1.40318C10.1339 1.83057 9.7204 3.23752 9.41837 3.2177C9.19467 3.26971 9.15818 2.83371 9.08739 2.64738C8.95886 2.30903 8.89071 1.9176 8.7185 1.59854C8.58086 1.34353 8.40014 1.03806 8.12337 0.91412C7.63027 0.660572 7.03575 1.42476 6.74072 2.33095C6.61457 2.81687 5.76653 3.75879 5.39721 3.9866C3.71684 5.02352 0.344233 6.11595 0.000262184 9.75358C-0.00114142 9.76867 0.000262182 9.81455 0.0573714 9.77323C0.459591 9.48197 5.02183 6.19605 2.09392 12.5476C0.300195 16.439 8.96062 18.917 9.40582 18.9271C9.46582 18.9284 9.46144 18.9011 9.46347 18.8832C10.1546 12.6724 16.9819 13.3262 18.5718 11.8387C20.1474 10.3649 20.1796 8.93816 18.7923 8.58131Z" fill="currentColor"/>
</svg>
);
}
69 changes: 69 additions & 0 deletions plugins/hunter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { IntegrationPlugin } from "../registry";
import { registerIntegration } from "../registry";
import { HunterIcon } from "./icon";

const hunterPlugin: IntegrationPlugin = {
type: "hunter",
label: "Hunter",
description: "All-in-one email outreach platform",
icon: HunterIcon,

formFields: [
{
id: "apiKey",
label: "API Key",
type: "password",
placeholder: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
configKey: "apiKey",
envVar: "HUNTER_API_KEY",
helpText: "Get your API key from ",
helpLink: {
text: "hunter.io/api-keys",
url: "https://hunter.io/api-keys",
},
},
],

testConfig: {
getTestFunction: async () => {
const { testHunter } = await import("./test");
return testHunter;
},
},

actions: [
{
slug: "enrich-lead",
label: "Enrich Lead",
description: "Get data on a lead and their company",
category: "Hunter",
stepFunction: "enrichLeadStep",
stepImportPath: "enrich-lead",
configFields: [
{
key: "email",
label: "Lead Email",
type: "template-input",
placeholder: "{{Trigger.email}} or john@example.com",
example: "john@example.com",
required: true,
},
{
key: "enrichmentType",
label: "Enrichment Type",
type: "select",
options: [
{ value: "individual", label: "Individual" },
{ value: "company", label: "Company" },
{ value: "combined", label: "Combined" },
],
defaultValue: "combined",
},
],
},
],
};

registerIntegration(hunterPlugin);

export default hunterPlugin;
307 changes: 307 additions & 0 deletions plugins/hunter/steps/enrich-lead.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
import "server-only";

import { fetchCredentials } from "@/lib/credential-fetcher";
import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
import { getErrorMessage } from "@/lib/utils";
import type { HunterCredentials } from "../credentials";

type PersonData = {
fullName?: string;
givenName?: string;
familyName?: string;
email?: string;
location?: string;
timezone?: string;
title?: string;
role?: string;
seniority?: string;
domain?: string;
company?: string;
twitter?: string;
linkedin?: string;
phone?: string;
};

type CompanyData = {
domain?: string;
name?: string;
headcount?: string;
industry?: string;
country?: string;
state?: string;
city?: string;
twitter?: string;
linkedin?: string;
facebook?: string;
};

type EnrichLeadResult =
| {
success: true;
enrichmentType: string;
person?: PersonData;
company?: CompanyData;
}
| { success: false; error: string };

export type EnrichLeadCoreInput = {
email: string;
enrichmentType?: "individual" | "company" | "combined";
};

function isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}

export type EnrichLeadInput = StepInput &
EnrichLeadCoreInput & {
integrationId?: string;
};

async function fetchPersonData(
email: string,
apiKey: string
): Promise<PersonData | null> {
const url = new URL("https://api.hunter.io/v2/people/find");
url.searchParams.set("email", email);
url.searchParams.set("api_key", apiKey);

const response = await fetch(url.toString(), {
method: "GET",
headers: { "Content-Type": "application/json" },
});

if (!response.ok) {
if (response.status === 404) {
return null;
}
const error = await response.json().catch(() => ({}));
throw new Error(
error.errors?.[0]?.details || `HTTP ${response.status}: ${response.statusText}`
);
}

const result = await response.json();
const data = result.data;

if (!data) {
return null;
}

return {
fullName: data.name?.fullName,
givenName: data.name?.givenName,
familyName: data.name?.familyName,
email: data.email,
location: data.location,
timezone: data.timeZone,
title: data.employment?.title,
role: data.employment?.role,
seniority: data.employment?.seniority,
domain: data.employment?.domain,
company: data.employment?.name,
twitter: data.twitter?.handle,
linkedin: data.linkedin?.handle,
phone: data.phone,
};
}

async function fetchCompanyData(
domain: string,
apiKey: string
): Promise<CompanyData | null> {
const url = new URL("https://api.hunter.io/v2/companies/find");
url.searchParams.set("domain", domain);
url.searchParams.set("api_key", apiKey);

const response = await fetch(url.toString(), {
method: "GET",
headers: { "Content-Type": "application/json" },
});

if (!response.ok) {
if (response.status === 404) {
return null;
}
const error = await response.json().catch(() => ({}));
throw new Error(
error.errors?.[0]?.details || `HTTP ${response.status}: ${response.statusText}`
);
}

const result = await response.json();
const data = result.data;

if (!data) {
return null;
}

return {
domain: data.domain,
name: data.name,
headcount: data.headcount,
industry: data.industry,
country: data.country,
state: data.state,
city: data.city,
twitter: data.twitter,
linkedin: data.linkedin,
facebook: data.facebook,
};
}

async function fetchCombinedData(
email: string,
apiKey: string
): Promise<{ person: PersonData | null; company: CompanyData | null }> {
const url = new URL("https://api.hunter.io/v2/combined/find");
url.searchParams.set("email", email);
url.searchParams.set("api_key", apiKey);

const response = await fetch(url.toString(), {
method: "GET",
headers: { "Content-Type": "application/json" },
});

if (!response.ok) {
if (response.status === 404) {
return { person: null, company: null };
}
const error = await response.json().catch(() => ({}));
throw new Error(
error.errors?.[0]?.details || `HTTP ${response.status}: ${response.statusText}`
);
}

const result = await response.json();
const data = result.data;

if (!data) {
return { person: null, company: null };
}

const person: PersonData | null = data.person
? {
fullName: data.person.name?.fullName,
givenName: data.person.name?.givenName,
familyName: data.person.name?.familyName,
email: data.person.email,
location: data.person.location,
timezone: data.person.timeZone,
title: data.person.employment?.title,
role: data.person.employment?.role,
seniority: data.person.employment?.seniority,
domain: data.person.employment?.domain,
company: data.person.employment?.name,
twitter: data.person.twitter?.handle,
linkedin: data.person.linkedin?.handle,
phone: data.person.phone,
}
: null;

const company: CompanyData | null = data.company
? {
domain: data.company.domain,
name: data.company.name,
headcount: data.company.headcount,
industry: data.company.industry,
country: data.company.country,
state: data.company.state,
city: data.company.city,
twitter: data.company.twitter,
linkedin: data.company.linkedin,
facebook: data.company.facebook,
}
: null;

return { person, company };
}

function extractDomain(email: string): string | null {
const parts = email.split("@");
if (parts.length !== 2) {
return null;
}
return parts[1];
}

async function stepHandler(
input: EnrichLeadCoreInput,
credentials: HunterCredentials
): Promise<EnrichLeadResult> {
const apiKey = credentials.HUNTER_API_KEY;

if (!apiKey) {
return {
success: false,
error:
"HUNTER_API_KEY is not configured. Please add it in Project Integrations.",
};
}

const enrichmentType = input.enrichmentType || "combined";
const email = input.email.trim();

if (!isValidEmail(email)) {
return {
success: false,
error: "Invalid email address format",
};
}

try {
let person: PersonData | null = null;
let company: CompanyData | null = null;

if (enrichmentType === "individual") {
person = await fetchPersonData(email, apiKey);
} else if (enrichmentType === "company") {
const domain = extractDomain(email);
if (!domain) {
return {
success: false,
error: "Invalid email format - could not extract domain",
};
}
company = await fetchCompanyData(domain, apiKey);
} else {
const result = await fetchCombinedData(email, apiKey);
person = result.person;
company = result.company;
}

if (!person && !company) {
return {
success: false,
error: "No enrichment data found for the provided identifier",
};
}

return {
success: true,
enrichmentType,
...(person && { person }),
...(company && { company }),
};
} catch (error) {
return {
success: false,
error: `Failed to enrich lead: ${getErrorMessage(error)}`,
};
}
}

export async function enrichLeadStep(
input: EnrichLeadInput
): Promise<EnrichLeadResult> {
"use step";

const credentials = input.integrationId
? await fetchCredentials(input.integrationId)
: {};

return withStepLogging(input, () => stepHandler(input, credentials));
}

export const _integrationType = "hunter";
Loading