From 020f791bf37773b267376fe5cb69b66a73f88cdf Mon Sep 17 00:00:00 2001 From: Ben Sabic Date: Wed, 3 Dec 2025 01:17:55 +1100 Subject: [PATCH 1/2] feat: add Dub plugin --- plugins/dub/credentials.ts | 3 + plugins/dub/icon.tsx | 14 ++ plugins/dub/index.ts | 313 +++++++++++++++++++++++++++++++ plugins/dub/steps/create-link.ts | 151 +++++++++++++++ plugins/dub/steps/upsert-link.ts | 151 +++++++++++++++ plugins/dub/test.ts | 42 +++++ 6 files changed, 674 insertions(+) create mode 100644 plugins/dub/credentials.ts create mode 100644 plugins/dub/icon.tsx create mode 100644 plugins/dub/index.ts create mode 100644 plugins/dub/steps/create-link.ts create mode 100644 plugins/dub/steps/upsert-link.ts create mode 100644 plugins/dub/test.ts diff --git a/plugins/dub/credentials.ts b/plugins/dub/credentials.ts new file mode 100644 index 00000000..bc9caba5 --- /dev/null +++ b/plugins/dub/credentials.ts @@ -0,0 +1,3 @@ +export type DubCredentials = { + DUB_API_KEY?: string; +}; diff --git a/plugins/dub/icon.tsx b/plugins/dub/icon.tsx new file mode 100644 index 00000000..a586eb86 --- /dev/null +++ b/plugins/dub/icon.tsx @@ -0,0 +1,14 @@ +export function DubIcon({ className }: { className?: string }) { + return ( + + Dub + + + ); +} diff --git a/plugins/dub/index.ts b/plugins/dub/index.ts new file mode 100644 index 00000000..ea8d1461 --- /dev/null +++ b/plugins/dub/index.ts @@ -0,0 +1,313 @@ +import type { IntegrationPlugin } from "../registry"; +import { registerIntegration } from "../registry"; +import { DubIcon } from "./icon"; + +const dubPlugin: IntegrationPlugin = { + type: "dub", + label: "Dub", + description: "Create and manage short links", + + icon: DubIcon, + + formFields: [ + { + id: "apiKey", + label: "API Key", + type: "password", + placeholder: "dub_xxx", + configKey: "apiKey", + envVar: "DUB_API_KEY", + helpText: "Get your API key from ", + helpLink: { + text: "Dub Dashboard", + url: "https://app.dub.co/settings/tokens", + }, + }, + ], + + testConfig: { + getTestFunction: async () => { + const { testDub } = await import("./test"); + return testDub; + }, + }, + + actions: [ + { + slug: "create-link", + label: "Create Link", + description: "Create a new short link", + category: "Dub", + stepFunction: "createLinkStep", + stepImportPath: "create-link", + outputFields: [ + { field: "id", description: "Unique link ID" }, + { field: "shortLink", description: "The full short URL" }, + { field: "qrCode", description: "QR code URL for the link" }, + { field: "domain", description: "Short link domain" }, + { field: "key", description: "Short link slug" }, + { field: "url", description: "Destination URL" }, + ], + configFields: [ + { + key: "url", + label: "Destination URL", + type: "template-input", + placeholder: "https://example.com/page", + example: "https://example.com/landing-page", + required: true, + }, + { + key: "key", + label: "Custom Slug", + type: "template-input", + placeholder: "my-link", + example: "summer-sale", + }, + { + key: "domain", + label: "Domain", + type: "template-input", + placeholder: "dub.sh", + example: "dub.sh", + }, + { + label: "Link IDs", + type: "group", + fields: [ + { + key: "externalId", + label: "External ID", + type: "template-input", + placeholder: "my-external-id", + }, + { + key: "tenantId", + label: "Tenant ID", + type: "template-input", + placeholder: "tenant-123", + }, + { + key: "programId", + label: "Program ID", + type: "template-input", + placeholder: "program-123", + }, + { + key: "partnerId", + label: "Partner ID", + type: "template-input", + placeholder: "partner-123", + }, + ], + }, + { + label: "Link Preview", + type: "group", + fields: [ + { + key: "title", + label: "Title", + type: "template-input", + placeholder: "Custom preview title", + }, + { + key: "description", + label: "Description", + type: "template-input", + placeholder: "Custom preview description", + }, + { + key: "image", + label: "Image URL", + type: "template-input", + placeholder: "https://example.com/image.png", + }, + { + key: "video", + label: "Video URL", + type: "template-input", + placeholder: "https://example.com/video.mp4", + }, + ], + }, + { + label: "UTM Parameters", + type: "group", + fields: [ + { + key: "utm_source", + label: "Source", + type: "template-input", + placeholder: "newsletter", + }, + { + key: "utm_medium", + label: "Medium", + type: "template-input", + placeholder: "email", + }, + { + key: "utm_campaign", + label: "Campaign", + type: "template-input", + placeholder: "summer-sale", + }, + { + key: "utm_term", + label: "Term", + type: "template-input", + placeholder: "running+shoes", + }, + { + key: "utm_content", + label: "Content", + type: "template-input", + placeholder: "logolink", + }, + ], + }, + ], + }, + { + slug: "upsert-link", + label: "Upsert Link", + description: "Create or update a link by URL or external ID", + category: "Dub", + stepFunction: "upsertLinkStep", + stepImportPath: "upsert-link", + outputFields: [ + { field: "id", description: "Unique link ID" }, + { field: "shortLink", description: "The full short URL" }, + { field: "qrCode", description: "QR code URL for the link" }, + { field: "domain", description: "Short link domain" }, + { field: "key", description: "Short link slug" }, + { field: "url", description: "Destination URL" }, + ], + configFields: [ + { + key: "url", + label: "Destination URL", + type: "template-input", + placeholder: "https://example.com/page", + example: "https://example.com/landing-page", + required: true, + }, + { + key: "key", + label: "Custom Slug", + type: "template-input", + placeholder: "my-link", + example: "summer-sale", + }, + { + key: "domain", + label: "Domain", + type: "template-input", + placeholder: "dub.sh", + example: "dub.sh", + }, + { + label: "Link IDs", + type: "group", + fields: [ + { + key: "externalId", + label: "External ID", + type: "template-input", + placeholder: "my-external-id", + }, + { + key: "tenantId", + label: "Tenant ID", + type: "template-input", + placeholder: "tenant-123", + }, + { + key: "programId", + label: "Program ID", + type: "template-input", + placeholder: "program-123", + }, + { + key: "partnerId", + label: "Partner ID", + type: "template-input", + placeholder: "partner-123", + }, + ], + }, + { + label: "Link Preview", + type: "group", + fields: [ + { + key: "title", + label: "Title", + type: "template-input", + placeholder: "Custom preview title", + }, + { + key: "description", + label: "Description", + type: "template-input", + placeholder: "Custom preview description", + }, + { + key: "image", + label: "Image URL", + type: "template-input", + placeholder: "https://example.com/image.png", + }, + { + key: "video", + label: "Video URL", + type: "template-input", + placeholder: "https://example.com/video.mp4", + }, + ], + }, + { + label: "UTM Parameters", + type: "group", + fields: [ + { + key: "utm_source", + label: "Source", + type: "template-input", + placeholder: "newsletter", + }, + { + key: "utm_medium", + label: "Medium", + type: "template-input", + placeholder: "email", + }, + { + key: "utm_campaign", + label: "Campaign", + type: "template-input", + placeholder: "summer-sale", + }, + { + key: "utm_term", + label: "Term", + type: "template-input", + placeholder: "running+shoes", + }, + { + key: "utm_content", + label: "Content", + type: "template-input", + placeholder: "logolink", + }, + ], + }, + ], + }, + ], +}; + +registerIntegration(dubPlugin); + +export default dubPlugin; diff --git a/plugins/dub/steps/create-link.ts b/plugins/dub/steps/create-link.ts new file mode 100644 index 00000000..b4a66ec2 --- /dev/null +++ b/plugins/dub/steps/create-link.ts @@ -0,0 +1,151 @@ +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 { DubCredentials } from "../credentials"; + +const DUB_API_URL = "https://api.dub.co"; + +type DubLinkResponse = { + id: string; + domain: string; + key: string; + url: string; + shortLink: string; + qrCode: string; +}; + +type CreateLinkResult = + | { + success: true; + id: string; + shortLink: string; + qrCode: string; + domain: string; + key: string; + url: string; + } + | { success: false; error: string }; + +export type CreateLinkCoreInput = { + url: string; + key?: string; + domain?: string; + externalId?: string; + tenantId?: string; + programId?: string; + partnerId?: string; + title?: string; + description?: string; + image?: string; + video?: string; + utm_source?: string; + utm_medium?: string; + utm_campaign?: string; + utm_term?: string; + utm_content?: string; +}; + +export type CreateLinkInput = StepInput & + CreateLinkCoreInput & { + integrationId?: string; + }; + +async function stepHandler( + input: CreateLinkCoreInput, + credentials: DubCredentials +): Promise { + const apiKey = credentials.DUB_API_KEY; + + if (!apiKey) { + return { + success: false, + error: + "DUB_API_KEY is not configured. Please add it in Project Integrations.", + }; + } + + if (!input.url) { + return { + success: false, + error: "Destination URL is required", + }; + } + + try { + const body: Record = { + url: input.url, + }; + + if (input.key) body.key = input.key; + if (input.domain) body.domain = input.domain; + if (input.externalId) body.externalId = input.externalId; + if (input.tenantId) body.tenantId = input.tenantId; + if (input.programId) body.programId = input.programId; + if (input.partnerId) body.partnerId = input.partnerId; + if (input.title) body.title = input.title; + if (input.description) body.description = input.description; + if (input.image) body.image = input.image; + if (input.video) body.video = input.video; + if (input.utm_source) body.utm_source = input.utm_source; + if (input.utm_medium) body.utm_medium = input.utm_medium; + if (input.utm_campaign) body.utm_campaign = input.utm_campaign; + if (input.utm_term) body.utm_term = input.utm_term; + if (input.utm_content) body.utm_content = input.utm_content; + + const response = await fetch(`${DUB_API_URL}/links`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const errorData = (await response.json().catch(() => ({}))) as { + error?: { message?: string }; + message?: string; + }; + const errorMessage = + errorData.error?.message || errorData.message || `HTTP ${response.status}`; + return { + success: false, + error: errorMessage, + }; + } + + const link = (await response.json()) as DubLinkResponse; + + return { + success: true, + id: link.id, + shortLink: link.shortLink, + qrCode: link.qrCode, + domain: link.domain, + key: link.key, + url: link.url, + }; + } catch (error) { + return { + success: false, + error: `Failed to create link: ${getErrorMessage(error)}`, + }; + } +} + +export async function createLinkStep( + input: CreateLinkInput +): Promise { + "use step"; + + const credentials = input.integrationId + ? await fetchCredentials(input.integrationId) + : {}; + + return withStepLogging(input, () => stepHandler(input, credentials)); +} +createLinkStep.maxRetries = 0; + +export const _integrationType = "dub"; diff --git a/plugins/dub/steps/upsert-link.ts b/plugins/dub/steps/upsert-link.ts new file mode 100644 index 00000000..1d34d903 --- /dev/null +++ b/plugins/dub/steps/upsert-link.ts @@ -0,0 +1,151 @@ +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 { DubCredentials } from "../credentials"; + +const DUB_API_URL = "https://api.dub.co"; + +type DubLinkResponse = { + id: string; + domain: string; + key: string; + url: string; + shortLink: string; + qrCode: string; +}; + +type UpsertLinkResult = + | { + success: true; + id: string; + shortLink: string; + qrCode: string; + domain: string; + key: string; + url: string; + } + | { success: false; error: string }; + +export type UpsertLinkCoreInput = { + url: string; + key?: string; + domain?: string; + externalId?: string; + tenantId?: string; + programId?: string; + partnerId?: string; + title?: string; + description?: string; + image?: string; + video?: string; + utm_source?: string; + utm_medium?: string; + utm_campaign?: string; + utm_term?: string; + utm_content?: string; +}; + +export type UpsertLinkInput = StepInput & + UpsertLinkCoreInput & { + integrationId?: string; + }; + +async function stepHandler( + input: UpsertLinkCoreInput, + credentials: DubCredentials +): Promise { + const apiKey = credentials.DUB_API_KEY; + + if (!apiKey) { + return { + success: false, + error: + "DUB_API_KEY is not configured. Please add it in Project Integrations.", + }; + } + + if (!input.url) { + return { + success: false, + error: "Destination URL is required", + }; + } + + try { + const body: Record = { + url: input.url, + }; + + if (input.key) body.key = input.key; + if (input.domain) body.domain = input.domain; + if (input.externalId) body.externalId = input.externalId; + if (input.tenantId) body.tenantId = input.tenantId; + if (input.programId) body.programId = input.programId; + if (input.partnerId) body.partnerId = input.partnerId; + if (input.title) body.title = input.title; + if (input.description) body.description = input.description; + if (input.image) body.image = input.image; + if (input.video) body.video = input.video; + if (input.utm_source) body.utm_source = input.utm_source; + if (input.utm_medium) body.utm_medium = input.utm_medium; + if (input.utm_campaign) body.utm_campaign = input.utm_campaign; + if (input.utm_term) body.utm_term = input.utm_term; + if (input.utm_content) body.utm_content = input.utm_content; + + const response = await fetch(`${DUB_API_URL}/links/upsert`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const errorData = (await response.json().catch(() => ({}))) as { + error?: { message?: string }; + message?: string; + }; + const errorMessage = + errorData.error?.message || errorData.message || `HTTP ${response.status}`; + return { + success: false, + error: errorMessage, + }; + } + + const link = (await response.json()) as DubLinkResponse; + + return { + success: true, + id: link.id, + shortLink: link.shortLink, + qrCode: link.qrCode, + domain: link.domain, + key: link.key, + url: link.url, + }; + } catch (error) { + return { + success: false, + error: `Failed to upsert link: ${getErrorMessage(error)}`, + }; + } +} + +export async function upsertLinkStep( + input: UpsertLinkInput +): Promise { + "use step"; + + const credentials = input.integrationId + ? await fetchCredentials(input.integrationId) + : {}; + + return withStepLogging(input, () => stepHandler(input, credentials)); +} +upsertLinkStep.maxRetries = 0; + +export const _integrationType = "dub"; diff --git a/plugins/dub/test.ts b/plugins/dub/test.ts new file mode 100644 index 00000000..da9eccec --- /dev/null +++ b/plugins/dub/test.ts @@ -0,0 +1,42 @@ +const DUB_API_URL = "https://api.dub.co"; + +export async function testDub(credentials: Record) { + try { + const apiKey = credentials.DUB_API_KEY; + + if (!apiKey) { + return { + success: false, + error: "DUB_API_KEY is required", + }; + } + + // Use the links endpoint to validate the API key + const response = await fetch(`${DUB_API_URL}/links?page=1&pageSize=1`, { + method: "GET", + headers: { + Authorization: `Bearer ${apiKey}`, + }, + }); + + if (!response.ok) { + if (response.status === 401) { + return { + success: false, + error: "Invalid API key. Please check your Dub 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), + }; + } +} From 7c39867d9f82edabc6d8e0d4d48a17d063119f11 Mon Sep 17 00:00:00 2001 From: Ben Sabic Date: Fri, 5 Dec 2025 13:32:27 +1100 Subject: [PATCH 2/2] chore: add dub plugin import --- plugins/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/index.ts b/plugins/index.ts index c2b41249..51430ff7 100644 --- a/plugins/index.ts +++ b/plugins/index.ts @@ -16,6 +16,7 @@ import "./ai-gateway"; import "./blob"; +import "./dub"; import "./fal"; import "./firecrawl"; import "./github";