Skip to content

Commit 4db48f4

Browse files
committed
feat: add Brandfetch plugin
1 parent 3436d68 commit 4db48f4

File tree

5 files changed

+352
-0
lines changed

5 files changed

+352
-0
lines changed

plugins/brandfetch/credentials.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export type BrandfetchCredentials = {
2+
BRANDFETCH_API_KEY?: string;
3+
};

plugins/brandfetch/icon.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export function BrandfetchIcon({ className }: { className?: string }) {
2+
return (
3+
<svg
4+
aria-label="Brandfetch logo"
5+
className={className}
6+
fill="currentColor"
7+
viewBox="0 0 24 24"
8+
xmlns="http://www.w3.org/2000/svg"
9+
>
10+
<title>Brandfetch</title>
11+
<path d="M23.226 5.842c0 1.491-.53 2.78-1.589 3.869-1.059 1.07-2.603 1.842-4.633 2.315 1.094.299 1.924.79 2.489 1.474.565.667.847 1.439.847 2.316 0 1.386-.441 2.588-1.324 3.605-.865 1.018-2.136 1.807-3.812 2.368-1.677.544-3.698.816-6.063.816-.883 0-1.589-.026-2.118-.079-.018.491-.23.86-.636 1.106-.406.245-.927.368-1.562.368s-1.077-.14-1.324-.421c-.23-.28-.326-.693-.291-1.237.159-2.456.468-5.026.927-7.71a75.521 75.521 0 0 1 1.747-7.816c.124-.439.37-.746.741-.921.371-.176.856-.263 1.457-.263 1.076 0 1.615.298 1.615.894 0 .246-.053.527-.16.842-.458 1.369-.917 3.228-1.376 5.58a65.729 65.729 0 0 0-.98 6.684c.848.07 1.536.105 2.066.105 2.47 0 4.28-.351 5.427-1.053 1.165-.72 1.747-1.631 1.747-2.737 0-.772-.335-1.42-1.006-1.947-.653-.526-1.756-.816-3.31-.868-.352-.018-.6-.106-.74-.264-.142-.157-.212-.412-.212-.763 0-.509.106-.92.317-1.237.212-.315.6-.482 1.165-.5 1.254-.035 2.383-.219 3.39-.552 1.023-.334 1.826-.798 2.409-1.395.582-.614.873-1.325.873-2.132 0-1.017-.503-1.815-1.509-2.394C16.792 3.298 15.248 3 13.165 3c-1.889 0-3.716.246-5.48.737-1.766.474-3.266 1.079-4.501 1.816-.565.333-1.042.5-1.43.5-.318 0-.565-.106-.742-.316-.158-.228-.238-.509-.238-.842 0-.439.088-.816.265-1.132.194-.316.644-.675 1.35-1.079 1.483-.842 3.221-1.5 5.216-1.973A26.377 26.377 0 0 1 13.721 0c3.195 0 5.577.535 7.148 1.605 1.571 1.07 2.357 2.483 2.357 4.237z"/>
12+
</svg>
13+
);
14+
}

plugins/brandfetch/index.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import type { IntegrationPlugin } from "../registry";
2+
import { registerIntegration } from "../registry";
3+
import { BrandfetchIcon } from "./icon";
4+
5+
const brandfetchPlugin: IntegrationPlugin = {
6+
type: "brandfetch",
7+
label: "Brandfetch",
8+
description: "Get brand assets and company data",
9+
10+
icon: BrandfetchIcon,
11+
12+
formFields: [
13+
{
14+
id: "apiKey",
15+
label: "API Key",
16+
type: "password",
17+
placeholder: "your-api-key",
18+
configKey: "apiKey",
19+
envVar: "BRANDFETCH_API_KEY",
20+
helpText: "Get your API key from ",
21+
helpLink: {
22+
text: "Brandfetch Dashboard",
23+
url: "https://developers.brandfetch.com/dashboard/keys",
24+
},
25+
},
26+
],
27+
28+
testConfig: {
29+
getTestFunction: async () => {
30+
const { testBrandfetch } = await import("./test");
31+
return testBrandfetch;
32+
},
33+
},
34+
35+
actions: [
36+
{
37+
slug: "get-brand",
38+
label: "Get Brand",
39+
description: "Get a company's brand assets by domain, ticker, or ISIN",
40+
category: "Brandfetch",
41+
stepFunction: "getBrandStep",
42+
stepImportPath: "get-brand",
43+
outputFields: [
44+
{ field: "name", description: "Brand name" },
45+
{ field: "domain", description: "Brand domain" },
46+
{ field: "description", description: "Brand description" },
47+
{ field: "logoUrl", description: "Primary logo URL" },
48+
{ field: "iconUrl", description: "Icon/symbol URL" },
49+
{ field: "colors", description: "Array of brand colors (hex)" },
50+
{ field: "links", description: "Social media and website links" },
51+
{ field: "industry", description: "Primary industry" },
52+
],
53+
configFields: [
54+
{
55+
key: "identifierType",
56+
label: "Identifier Type",
57+
type: "select",
58+
options: [
59+
{ value: "domain", label: "Domain" },
60+
{ value: "ticker", label: "Stock Ticker" },
61+
{ value: "isin", label: "ISIN" },
62+
],
63+
defaultValue: "domain",
64+
required: true,
65+
},
66+
{
67+
key: "identifier",
68+
label: "Identifier",
69+
type: "template-input",
70+
placeholder: "nike.com / NKE / US6541061031",
71+
example: "nike.com",
72+
required: true,
73+
},
74+
],
75+
},
76+
],
77+
};
78+
79+
registerIntegration(brandfetchPlugin);
80+
81+
export default brandfetchPlugin;
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import "server-only";
2+
3+
import { fetchCredentials } from "@/lib/credential-fetcher";
4+
import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
5+
import { getErrorMessage } from "@/lib/utils";
6+
import type { BrandfetchCredentials } from "../credentials";
7+
8+
const BRANDFETCH_API_URL = "https://api.brandfetch.io/v2";
9+
10+
type BrandfetchLogo = {
11+
type: string;
12+
theme: string;
13+
formats: Array<{
14+
src: string;
15+
format: string;
16+
}>;
17+
};
18+
19+
type BrandfetchColor = {
20+
hex: string;
21+
type: string;
22+
brightness: number;
23+
};
24+
25+
type BrandfetchLink = {
26+
name: string;
27+
url: string;
28+
};
29+
30+
type BrandfetchIndustry = {
31+
name: string;
32+
slug: string;
33+
score: number;
34+
};
35+
36+
type BrandfetchResponse = {
37+
id: string;
38+
name: string;
39+
domain: string;
40+
description?: string;
41+
longDescription?: string;
42+
logos?: BrandfetchLogo[];
43+
colors?: BrandfetchColor[];
44+
links?: BrandfetchLink[];
45+
company?: {
46+
industries?: BrandfetchIndustry[];
47+
};
48+
};
49+
50+
type GetBrandResult =
51+
| {
52+
success: true;
53+
name: string;
54+
domain: string;
55+
description: string;
56+
logoUrl: string;
57+
iconUrl: string;
58+
colors: string[];
59+
links: Array<{ name: string; url: string }>;
60+
industry: string;
61+
}
62+
| { success: false; error: string };
63+
64+
export type GetBrandCoreInput = {
65+
identifierType: "domain" | "ticker" | "isin";
66+
identifier: string;
67+
};
68+
69+
export type GetBrandInput = StepInput &
70+
GetBrandCoreInput & {
71+
integrationId?: string;
72+
};
73+
74+
function findLogoUrl(logos: BrandfetchLogo[] | undefined, type: string): string {
75+
if (!logos) return "";
76+
77+
const logo = logos.find((l) => l.type === type);
78+
if (!logo || !logo.formats || logo.formats.length === 0) return "";
79+
80+
// Prefer PNG or SVG
81+
const pngFormat = logo.formats.find((f) => f.format === "png");
82+
if (pngFormat) return pngFormat.src;
83+
84+
const svgFormat = logo.formats.find((f) => f.format === "svg");
85+
if (svgFormat) return svgFormat.src;
86+
87+
return logo.formats[0].src;
88+
}
89+
90+
async function stepHandler(
91+
input: GetBrandCoreInput,
92+
credentials: BrandfetchCredentials
93+
): Promise<GetBrandResult> {
94+
const apiKey = credentials.BRANDFETCH_API_KEY;
95+
96+
if (!apiKey) {
97+
return {
98+
success: false,
99+
error:
100+
"BRANDFETCH_API_KEY is not configured. Please add it in Project Integrations.",
101+
};
102+
}
103+
104+
if (!input.identifier) {
105+
return {
106+
success: false,
107+
error: "Identifier is required",
108+
};
109+
}
110+
111+
// Validate identifier format based on type
112+
const identifier = input.identifier.trim();
113+
const identifierType = input.identifierType || "domain";
114+
115+
if (identifierType === "domain") {
116+
if (!identifier.includes(".")) {
117+
return {
118+
success: false,
119+
error: "Invalid domain format. Expected format: example.com",
120+
};
121+
}
122+
} else if (identifierType === "ticker") {
123+
if (!/^[A-Z]{1,5}$/.test(identifier.toUpperCase())) {
124+
return {
125+
success: false,
126+
error: "Invalid ticker format. Expected 1-5 uppercase letters (e.g., NKE)",
127+
};
128+
}
129+
} else if (identifierType === "isin") {
130+
if (!/^[A-Z]{2}[A-Z0-9]{10}$/.test(identifier.toUpperCase())) {
131+
return {
132+
success: false,
133+
error: "Invalid ISIN format. Expected 12 characters starting with country code (e.g., US6541061031)",
134+
};
135+
}
136+
}
137+
138+
try {
139+
const response = await fetch(
140+
`${BRANDFETCH_API_URL}/brands/${encodeURIComponent(identifier)}`,
141+
{
142+
method: "GET",
143+
headers: {
144+
Authorization: `Bearer ${apiKey}`,
145+
},
146+
}
147+
);
148+
149+
if (!response.ok) {
150+
if (response.status === 404) {
151+
return {
152+
success: false,
153+
error: `Brand not found for: ${input.identifier}`,
154+
};
155+
}
156+
const errorData = (await response.json().catch(() => ({}))) as {
157+
message?: string;
158+
};
159+
return {
160+
success: false,
161+
error: errorData.message || `HTTP ${response.status}`,
162+
};
163+
}
164+
165+
const brand = (await response.json()) as BrandfetchResponse;
166+
167+
// Extract primary logo and icon URLs
168+
const logoUrl = findLogoUrl(brand.logos, "logo");
169+
const iconUrl = findLogoUrl(brand.logos, "icon") || findLogoUrl(brand.logos, "symbol");
170+
171+
// Extract colors (just hex values)
172+
const colors = brand.colors?.map((c) => c.hex) || [];
173+
174+
// Extract links
175+
const links = brand.links?.map((l) => ({ name: l.name, url: l.url })) || [];
176+
177+
// Get primary industry
178+
const industry = brand.company?.industries?.[0]?.name || "";
179+
180+
return {
181+
success: true,
182+
name: brand.name || "",
183+
domain: brand.domain || "",
184+
description: brand.description || brand.longDescription || "",
185+
logoUrl,
186+
iconUrl,
187+
colors,
188+
links,
189+
industry,
190+
};
191+
} catch (error) {
192+
return {
193+
success: false,
194+
error: `Failed to get brand: ${getErrorMessage(error)}`,
195+
};
196+
}
197+
}
198+
199+
export async function getBrandStep(
200+
input: GetBrandInput
201+
): Promise<GetBrandResult> {
202+
"use step";
203+
204+
const credentials = input.integrationId
205+
? await fetchCredentials(input.integrationId)
206+
: {};
207+
208+
return withStepLogging(input, () => stepHandler(input, credentials));
209+
}
210+
getBrandStep.maxRetries = 0;
211+
212+
export const _integrationType = "brandfetch";

plugins/brandfetch/test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
const BRANDFETCH_API_URL = "https://api.brandfetch.io/v2";
2+
3+
export async function testBrandfetch(credentials: Record<string, string>) {
4+
try {
5+
const apiKey = credentials.BRANDFETCH_API_KEY;
6+
7+
if (!apiKey) {
8+
return {
9+
success: false,
10+
error: "BRANDFETCH_API_KEY is required",
11+
};
12+
}
13+
14+
// Use brandfetch.com domain for testing (free and doesn't count against quota)
15+
const response = await fetch(`${BRANDFETCH_API_URL}/brands/brandfetch.com`, {
16+
method: "GET",
17+
headers: {
18+
Authorization: `Bearer ${apiKey}`,
19+
},
20+
});
21+
22+
if (!response.ok) {
23+
if (response.status === 401) {
24+
return {
25+
success: false,
26+
error: "Invalid API key. Please check your Brandfetch API key.",
27+
};
28+
}
29+
return {
30+
success: false,
31+
error: `API validation failed: HTTP ${response.status}`,
32+
};
33+
}
34+
35+
return { success: true };
36+
} catch (error) {
37+
return {
38+
success: false,
39+
error: error instanceof Error ? error.message : String(error),
40+
};
41+
}
42+
}

0 commit comments

Comments
 (0)