Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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;
212 changes: 212 additions & 0 deletions plugins/brandfetch/steps/get-brand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
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
const identifier = input.identifier.trim();
const identifierType = input.identifierType || "domain";

if (identifierType === "domain") {
if (!identifier.includes(".")) {
return {
success: false,
error: "Invalid domain format. Expected format: example.com",
};
}
} else if (identifierType === "ticker") {
if (!/^[A-Z]{1,5}$/.test(identifier.toUpperCase())) {
return {
success: false,
error: "Invalid ticker format. Expected 1-5 uppercase letters (e.g., NKE)",
};
}
} else if (identifierType === "isin") {
if (!/^[A-Z]{2}[A-Z0-9]{10}$/.test(identifier.toUpperCase())) {
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: ${input.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),
};
}
}