-
Notifications
You must be signed in to change notification settings - Fork 135
feat: add Dub plugin #119
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: add Dub plugin #119
Conversation
|
@bensabic is attempting to deploy a commit to the Vercel Labs Team on Vercel. A member of the Team first needs to authorize it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds a Dub plugin for creating and managing short links with two main actions: Create Link and Upsert Link. The implementation includes proper credential management, error handling, and grouped configuration for Link IDs, Link Preview metadata, and UTM parameters.
- Adds complete Dub integration with API key authentication
- Implements two actions: Create Link (POST) and Upsert Link (PUT) with comprehensive input fields
- Returns structured link data including short URL, QR code, and metadata
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| plugins/index.ts | Registers the new Dub plugin in the plugins index |
| plugins/dub/index.ts | Main plugin configuration defining actions, form fields, and test config |
| plugins/dub/credentials.ts | Type definition for Dub API credentials |
| plugins/dub/icon.tsx | SVG icon component for Dub branding |
| plugins/dub/test.ts | Connection test function to validate API credentials |
| plugins/dub/steps/create-link.ts | Step handler for creating new short links via POST |
| plugins/dub/steps/upsert-link.ts | Step handler for creating or updating links via PUT |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| async function stepHandler( | ||
| input: CreateLinkCoreInput, | ||
| credentials: DubCredentials | ||
| ): Promise<CreateLinkResult> { | ||
| 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<string, string> = { | ||
| 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)}`, | ||
| }; | ||
| } | ||
| } |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The stepHandler function in this file and plugins/dub/steps/upsert-link.ts share almost identical logic (lines 55-136 here vs lines 55-136 there). The only differences are the API endpoint, HTTP method, and error message. Consider extracting the shared logic into a common utility function to reduce code duplication and improve maintainability. For example, create a shared function that accepts the endpoint, method, and operation name as parameters.
| 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", | ||
| }, | ||
| ], | ||
| }, | ||
| ], |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The configFields array for "create-link" (lines 51-170) and "upsert-link" (lines 187-306) are identical. Consider extracting this configuration into a shared constant to reduce duplication and ensure consistency. For example: const linkConfigFields = [...] defined once and reused for both actions.
| xmlns="http://www.w3.org/2000/svg" | ||
| > | ||
| <title>Dub</title> | ||
| <path fill-rule="evenodd" clip-rule="evenodd" d="M32 64c17.673 0 32-14.327 32-32 0-11.844-6.435-22.186-16-27.719V48h-8v-2.14A15.9 15.9 0 0 1 32 48c-8.837 0-16-7.163-16-16s7.163-16 16-16c2.914 0 5.647.78 8 2.14V1.008A32 32 0 0 0 32 0C14.327 0 0 14.327 0 32s14.327 32 32 32" fill="currentColor"/> |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SVG attributes should use React/JSX camelCase naming convention. Change fill-rule to fillRule and clip-rule to clipRule to match React's JSX syntax and be consistent with other icon components in the codebase (see plugins/fal/icon.tsx and plugins/slack/icon.tsx).
| <path fill-rule="evenodd" clip-rule="evenodd" d="M32 64c17.673 0 32-14.327 32-32 0-11.844-6.435-22.186-16-27.719V48h-8v-2.14A15.9 15.9 0 0 1 32 48c-8.837 0-16-7.163-16-16s7.163-16 16-16c2.914 0 5.647.78 8 2.14V1.008A32 32 0 0 0 32 0C14.327 0 0 14.327 0 32s14.327 32 32 32" fill="currentColor"/> | |
| <path fillRule="evenodd" clipRule="evenodd" d="M32 64c17.673 0 32-14.327 32-32 0-11.844-6.435-22.186-16-27.719V48h-8v-2.14A15.9 15.9 0 0 1 32 48c-8.837 0-16-7.163-16-16s7.163-16 16-16c2.914 0 5.647.78 8 2.14V1.008A32 32 0 0 0 32 0C14.327 0 0 14.327 0 32s14.327 32 32 32" fill="currentColor"/> |
Summary
Adds Dub plugin with the following actions:
Features:
Test plan