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/brandfetch/credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type BrandfetchCredentials = {
BRANDFETCH_API_KEY?: string;
};
14 changes: 14 additions & 0 deletions plugins/brandfetch/icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export function BrandfetchIcon({ className }: { className?: string }) {
return (
<svg
aria-label="Brandfetch logo"
className={className}
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>Brandfetch</title>
<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"/>
</svg>
);
}
81 changes: 81 additions & 0 deletions plugins/brandfetch/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { IntegrationPlugin } from "../registry";
import { registerIntegration } from "../registry";
import { BrandfetchIcon } from "./icon";

const brandfetchPlugin: IntegrationPlugin = {
type: "brandfetch",
label: "Brandfetch",
description: "Get brand assets and company data",

icon: BrandfetchIcon,

formFields: [
{
id: "apiKey",
label: "API Key",
type: "password",
placeholder: "your-api-key",
configKey: "apiKey",
envVar: "BRANDFETCH_API_KEY",
helpText: "Get your API key from ",
helpLink: {
text: "Brandfetch Dashboard",
url: "https://developers.brandfetch.com/dashboard/keys",
},
},
],

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

actions: [
{
slug: "get-brand",
label: "Get Brand",
description: "Get a company's brand assets by domain, ticker, or ISIN",
category: "Brandfetch",
stepFunction: "getBrandStep",
stepImportPath: "get-brand",
outputFields: [
{ field: "name", description: "Brand name" },
{ field: "domain", description: "Brand domain" },
{ field: "description", description: "Brand description" },
{ field: "logoUrl", description: "Primary logo URL" },
{ field: "iconUrl", description: "Icon/symbol URL" },
{ field: "colors", description: "Array of brand colors (hex)" },
{ field: "links", description: "Social media and website links" },
{ field: "industry", description: "Primary industry" },
],
configFields: [
{
key: "identifierType",
label: "Identifier Type",
type: "select",
options: [
{ value: "domain", label: "Domain" },
{ value: "ticker", label: "Stock Ticker" },
{ value: "isin", label: "ISIN" },
],
defaultValue: "domain",
required: true,
},
{
key: "identifier",
label: "Identifier",
type: "template-input",
placeholder: "nike.com / NKE / US6541061031",
example: "nike.com",
required: true,
},
],
},
],
};

registerIntegration(brandfetchPlugin);

export default brandfetchPlugin;
217 changes: 217 additions & 0 deletions plugins/brandfetch/steps/get-brand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
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 { BrandfetchCredentials } from "../credentials";

const BRANDFETCH_API_URL = "https://api.brandfetch.io/v2";

type BrandfetchLogo = {
type: string;
theme: string;
formats: Array<{
src: string;
format: string;
}>;
};

type BrandfetchColor = {
hex: string;
type: string;
brightness: number;
};

type BrandfetchLink = {
name: string;
url: string;
};

type BrandfetchIndustry = {
name: string;
slug: string;
score: number;
};

type BrandfetchResponse = {
id: string;
name: string;
domain: string;
description?: string;
longDescription?: string;
logos?: BrandfetchLogo[];
colors?: BrandfetchColor[];
links?: BrandfetchLink[];
company?: {
industries?: BrandfetchIndustry[];
};
};

type GetBrandResult =
| {
success: true;
name: string;
domain: string;
description: string;
logoUrl: string;
iconUrl: string;
colors: string[];
links: Array<{ name: string; url: string }>;
industry: string;
}
| { success: false; error: string };

export type GetBrandCoreInput = {
identifierType: "domain" | "ticker" | "isin";
identifier: string;
};

export type GetBrandInput = StepInput &
GetBrandCoreInput & {
integrationId?: string;
};

function findLogoUrl(logos: BrandfetchLogo[] | undefined, type: string): string {
if (!logos) return "";

const logo = logos.find((l) => l.type === type);
if (!logo || !logo.formats || logo.formats.length === 0) return "";

// Prefer PNG or SVG
const pngFormat = logo.formats.find((f) => f.format === "png");
if (pngFormat) return pngFormat.src;

const svgFormat = logo.formats.find((f) => f.format === "svg");
if (svgFormat) return svgFormat.src;

return logo.formats[0].src;
}

async function stepHandler(
input: GetBrandCoreInput,
credentials: BrandfetchCredentials
): Promise<GetBrandResult> {
const apiKey = credentials.BRANDFETCH_API_KEY;

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

if (!input.identifier) {
return {
success: false,
error: "Identifier is required",
};
}

// Validate identifier format based on type
let identifier = input.identifier.trim();
const identifierType = input.identifierType || "domain";

if (identifierType === "domain") {
// Validate domain format: must have at least one character before and after the dot, no spaces, no leading/trailing dot
if (
!/^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?(\.[a-zA-Z]{2,})+$/.test(identifier)
) {
return {
success: false,
error: "Invalid domain format. Expected format: example.com",
};
}
} else if (identifierType === "ticker") {
identifier = identifier.toUpperCase();
if (!/^[A-Z]{1,5}$/.test(identifier)) {
return {
success: false,
error: "Invalid ticker format. Expected 1-5 uppercase letters (e.g., NKE)",
};
}
} else if (identifierType === "isin") {
identifier = identifier.toUpperCase();
if (!/^[A-Z]{2}[A-Z0-9]{10}$/.test(identifier)) {
return {
success: false,
error: "Invalid ISIN format. Expected 12 characters starting with country code (e.g., US6541061031)",
};
}
}

try {
const response = await fetch(
`${BRANDFETCH_API_URL}/brands/${encodeURIComponent(identifier)}`,
{
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
},
}
);

if (!response.ok) {
if (response.status === 404) {
return {
success: false,
error: `Brand not found for: ${identifier}`,
};
}
const errorData = (await response.json().catch(() => ({}))) as {
message?: string;
};
return {
success: false,
error: errorData.message || `HTTP ${response.status}`,
};
}

const brand = (await response.json()) as BrandfetchResponse;

// Extract primary logo and icon URLs
const logoUrl = findLogoUrl(brand.logos, "logo");
const iconUrl = findLogoUrl(brand.logos, "icon") || findLogoUrl(brand.logos, "symbol");

// Extract colors (just hex values)
const colors = brand.colors?.map((c) => c.hex) || [];

// Extract links
const links = brand.links?.map((l) => ({ name: l.name, url: l.url })) || [];

// Get primary industry
const industry = brand.company?.industries?.[0]?.name || "";

return {
success: true,
name: brand.name || "",
domain: brand.domain || "",
description: brand.description || brand.longDescription || "",
logoUrl,
iconUrl,
colors,
links,
industry,
};
} catch (error) {
return {
success: false,
error: `Failed to get brand: ${getErrorMessage(error)}`,
};
}
}

export async function getBrandStep(
input: GetBrandInput
): Promise<GetBrandResult> {
"use step";

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

return withStepLogging(input, () => stepHandler(input, credentials));
}
getBrandStep.maxRetries = 0;

export const _integrationType = "brandfetch";
42 changes: 42 additions & 0 deletions plugins/brandfetch/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const BRANDFETCH_API_URL = "https://api.brandfetch.io/v2";

export async function testBrandfetch(credentials: Record<string, string>) {
try {
const apiKey = credentials.BRANDFETCH_API_KEY;

if (!apiKey) {
return {
success: false,
error: "BRANDFETCH_API_KEY is required",
};
}

// Use brandfetch.com domain for testing (free and doesn't count against quota)
const response = await fetch(`${BRANDFETCH_API_URL}/brands/brandfetch.com`, {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
},
});

if (!response.ok) {
if (response.status === 401) {
return {
success: false,
error: "Invalid API key. Please check your Brandfetch API key.",
};
}
return {
success: false,
error: `API validation failed: HTTP ${response.status}`,
};
}

return { success: true };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
1 change: 1 addition & 0 deletions plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import "./ai-gateway";
import "./blob";
import "./brandfetch";
import "./clerk";
import "./fal";
import "./firecrawl";
Expand Down