From a742bddfa316d65ac78cfb2c2f13d3aa2c409513 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Jul 2026 08:13:03 -0700 Subject: [PATCH 1/2] feat(dub): expand link coverage and fix cross-operation param leakage - add tenantId/folderId/trackConversion support and conversions output to link tools - add cursor pagination (startingAfter/endingBefore) to list_links, logo param to get_qr_code - add list_domains, list_tags, create_tag, list_folders tools - fix block param leakage where an unset field on one operation inherited a stale value left over from another operation --- apps/sim/blocks/blocks/dub.ts | 305 ++++++++++++++++++++++++++++- apps/sim/tools/dub/create_link.ts | 29 +++ apps/sim/tools/dub/create_tag.ts | 68 +++++++ apps/sim/tools/dub/get_link.ts | 8 + apps/sim/tools/dub/get_qr_code.ts | 7 + apps/sim/tools/dub/index.ts | 8 + apps/sim/tools/dub/list_domains.ts | 85 ++++++++ apps/sim/tools/dub/list_folders.ts | 78 ++++++++ apps/sim/tools/dub/list_links.ts | 34 +++- apps/sim/tools/dub/list_tags.ts | 92 +++++++++ apps/sim/tools/dub/types.ts | 77 ++++++++ apps/sim/tools/dub/update_link.ts | 29 +++ apps/sim/tools/dub/upsert_link.ts | 29 +++ apps/sim/tools/registry.ts | 8 + 14 files changed, 853 insertions(+), 4 deletions(-) create mode 100644 apps/sim/tools/dub/create_tag.ts create mode 100644 apps/sim/tools/dub/list_domains.ts create mode 100644 apps/sim/tools/dub/list_folders.ts create mode 100644 apps/sim/tools/dub/list_tags.ts diff --git a/apps/sim/blocks/blocks/dub.ts b/apps/sim/blocks/blocks/dub.ts index e7b84937b18..1494a712852 100644 --- a/apps/sim/blocks/blocks/dub.ts +++ b/apps/sim/blocks/blocks/dub.ts @@ -34,6 +34,10 @@ export const DubBlock: BlockConfig = { { label: 'Get Analytics', id: 'get_analytics' }, { label: 'List Events', id: 'get_events' }, { label: 'Get QR Code', id: 'get_qr_code' }, + { label: 'List Domains', id: 'list_domains' }, + { label: 'List Tags', id: 'list_tags' }, + { label: 'Create Tag', id: 'create_tag' }, + { label: 'List Folders', id: 'list_folders' }, ], value: () => 'create_link', }, @@ -93,6 +97,34 @@ export const DubBlock: BlockConfig = { condition: { field: 'operation', value: ['create_link', 'upsert_link', 'update_link'] }, mode: 'advanced', }, + { + id: 'tenantId', + title: 'Tenant ID', + type: 'short-input', + placeholder: 'ID of the tenant this link belongs to', + condition: { field: 'operation', value: ['create_link', 'upsert_link', 'update_link'] }, + mode: 'advanced', + }, + { + id: 'folderId', + title: 'Folder ID', + type: 'short-input', + placeholder: 'Folder to organize this link into', + condition: { field: 'operation', value: ['create_link', 'upsert_link', 'update_link'] }, + mode: 'advanced', + }, + { + id: 'trackConversion', + title: 'Track Conversions', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: ['create_link', 'upsert_link', 'update_link'] }, + mode: 'advanced', + }, { id: 'comments', title: 'Comments', @@ -250,6 +282,22 @@ export const DubBlock: BlockConfig = { condition: { field: 'operation', value: 'list_links' }, mode: 'advanced', }, + { + id: 'listTenantId', + title: 'Filter by Tenant ID', + type: 'short-input', + placeholder: 'Tenant ID', + condition: { field: 'operation', value: 'list_links' }, + mode: 'advanced', + }, + { + id: 'listFolderId', + title: 'Filter by Folder ID', + type: 'short-input', + placeholder: 'Folder ID', + condition: { field: 'operation', value: 'list_links' }, + mode: 'advanced', + }, { id: 'showArchived', title: 'Show Archived', @@ -278,6 +326,22 @@ export const DubBlock: BlockConfig = { condition: { field: 'operation', value: 'list_links' }, mode: 'advanced', }, + { + id: 'startingAfter', + title: 'Starting After (Cursor)', + type: 'short-input', + placeholder: 'Link ID to fetch results after', + condition: { field: 'operation', value: 'list_links' }, + mode: 'advanced', + }, + { + id: 'endingBefore', + title: 'Ending Before (Cursor)', + type: 'short-input', + placeholder: 'Link ID to fetch results before', + condition: { field: 'operation', value: 'list_links' }, + mode: 'advanced', + }, { id: 'analyticsEvent', title: 'Event Type', @@ -648,6 +712,14 @@ export const DubBlock: BlockConfig = { condition: { field: 'operation', value: 'get_qr_code' }, required: { field: 'operation', value: 'get_qr_code' }, }, + { + id: 'qrLogo', + title: 'Custom Logo URL', + type: 'short-input', + placeholder: 'https://example.com/logo.png (paid plans only)', + condition: { field: 'operation', value: 'get_qr_code' }, + mode: 'advanced', + }, { id: 'qrSize', title: 'Size (px)', @@ -706,6 +778,138 @@ export const DubBlock: BlockConfig = { condition: { field: 'operation', value: 'get_qr_code' }, mode: 'advanced', }, + { + id: 'domainsSearch', + title: 'Search', + type: 'short-input', + placeholder: 'Search by domain name', + condition: { field: 'operation', value: 'list_domains' }, + }, + { + id: 'domainsArchived', + title: 'Include Archived', + type: 'dropdown', + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + value: () => 'false', + condition: { field: 'operation', value: 'list_domains' }, + mode: 'advanced', + }, + { + id: 'domainsPage', + title: 'Page', + type: 'short-input', + placeholder: '1', + condition: { field: 'operation', value: 'list_domains' }, + mode: 'advanced', + }, + { + id: 'domainsPageSize', + title: 'Page Size', + type: 'short-input', + placeholder: '50 (max: 100)', + condition: { field: 'operation', value: 'list_domains' }, + mode: 'advanced', + }, + { + id: 'tagsSearch', + title: 'Search', + type: 'short-input', + placeholder: 'Search by tag name', + condition: { field: 'operation', value: 'list_tags' }, + }, + { + id: 'tagsSortBy', + title: 'Sort By', + type: 'dropdown', + options: [ + { label: 'Name', id: 'name' }, + { label: 'Created At', id: 'createdAt' }, + ], + value: () => 'name', + condition: { field: 'operation', value: 'list_tags' }, + mode: 'advanced', + }, + { + id: 'tagsSortOrder', + title: 'Sort Order', + type: 'dropdown', + options: [ + { label: 'Ascending', id: 'asc' }, + { label: 'Descending', id: 'desc' }, + ], + value: () => 'asc', + condition: { field: 'operation', value: 'list_tags' }, + mode: 'advanced', + }, + { + id: 'tagsPage', + title: 'Page', + type: 'short-input', + placeholder: '1', + condition: { field: 'operation', value: 'list_tags' }, + mode: 'advanced', + }, + { + id: 'tagsPageSize', + title: 'Page Size', + type: 'short-input', + placeholder: '100 (max: 100)', + condition: { field: 'operation', value: 'list_tags' }, + mode: 'advanced', + }, + { + id: 'tagName', + title: 'Tag Name', + type: 'short-input', + placeholder: 'e.g., Q3-campaign', + condition: { field: 'operation', value: 'create_tag' }, + required: { field: 'operation', value: 'create_tag' }, + }, + { + id: 'tagColor', + title: 'Color', + type: 'dropdown', + options: [ + { label: 'Random', id: '' }, + { label: 'Red', id: 'red' }, + { label: 'Yellow', id: 'yellow' }, + { label: 'Green', id: 'green' }, + { label: 'Blue', id: 'blue' }, + { label: 'Purple', id: 'purple' }, + { label: 'Brown', id: 'brown' }, + { label: 'Gray', id: 'gray' }, + { label: 'Pink', id: 'pink' }, + ], + value: () => '', + condition: { field: 'operation', value: 'create_tag' }, + mode: 'advanced', + }, + { + id: 'foldersSearch', + title: 'Search', + type: 'short-input', + placeholder: 'Search by folder name', + condition: { field: 'operation', value: 'list_folders' }, + }, + { + id: 'foldersPage', + title: 'Page', + type: 'short-input', + placeholder: '1', + condition: { field: 'operation', value: 'list_folders' }, + mode: 'advanced', + }, + { + id: 'foldersPageSize', + title: 'Page Size', + type: 'short-input', + placeholder: '50 (max: 50)', + condition: { field: 'operation', value: 'list_folders' }, + mode: 'advanced', + }, { id: 'apiKey', title: 'API Key', @@ -730,18 +934,58 @@ export const DubBlock: BlockConfig = { 'dub_get_analytics', 'dub_get_events', 'dub_get_qr_code', + 'dub_list_domains', + 'dub_list_tags', + 'dub_create_tag', + 'dub_list_folders', ], config: { tool: (params) => `dub_${params.operation}`, params: (params) => { const result: Record = {} - if ( + const isLinkMutation = params.operation === 'create_link' || params.operation === 'upsert_link' || params.operation === 'update_link' + + // The executor merges { ...inputs, ...transformedParams }, so a raw subBlock + // id that shares its name with a tool param (e.g. 'domain', 'linkId') passes + // through untouched unless explicitly cleared here. These fields are only + // shown for the operations below; every other operation must null them out + // so a value left over from a previous operation selection can't leak in. + if (!isLinkMutation) { + result.domain = undefined + result.key = undefined + result.title = undefined + result.description = undefined + result.externalId = undefined + result.tagIds = undefined + result.tenantId = undefined + result.folderId = undefined + } + if (params.operation !== 'create_link' && params.operation !== 'upsert_link') { + result.url = undefined + } + if ( + params.operation !== 'get_link' && + params.operation !== 'update_link' && + params.operation !== 'delete_link' ) { + result.linkId = undefined + } + if (params.operation !== 'list_links') { + result.search = undefined + result.showArchived = undefined + result.page = undefined + result.pageSize = undefined + } + + if (isLinkMutation) { if (params.linkRewrite === 'true') result.rewrite = true if (params.linkArchived === 'true') result.archived = true + if (params.tenantId) result.tenantId = params.tenantId + if (params.folderId) result.folderId = params.folderId + result.trackConversion = params.trackConversion === 'true' } if (params.operation === 'get_link') { if (params.getLinkExternalId) result.externalId = params.getLinkExternalId @@ -754,9 +998,13 @@ export const DubBlock: BlockConfig = { if (params.operation === 'list_links') { if (params.listDomain) result.domain = params.listDomain if (params.listTagIds) result.tagIds = params.listTagIds + if (params.listTenantId) result.tenantId = params.listTenantId + if (params.listFolderId) result.folderId = params.listFolderId if (params.showArchived && params.showArchived !== 'false') result.showArchived = true if (params.page) result.page = Number(params.page) if (params.pageSize) result.pageSize = Number(params.pageSize) + if (params.startingAfter) result.startingAfter = params.startingAfter + if (params.endingBefore) result.endingBefore = params.endingBefore } if (params.operation === 'get_analytics') { if (params.analyticsEvent) result.event = params.analyticsEvent @@ -806,6 +1054,7 @@ export const DubBlock: BlockConfig = { } if (params.operation === 'get_qr_code') { if (params.qrUrl) result.url = params.qrUrl + if (params.qrLogo) result.logo = params.qrLogo if (params.qrSize) result.size = Number(params.qrSize) if (params.qrLevel) result.level = params.qrLevel if (params.qrFgColor) result.fgColor = params.qrFgColor @@ -813,6 +1062,28 @@ export const DubBlock: BlockConfig = { if (params.qrHideLogo === 'true') result.hideLogo = true if (params.qrMargin) result.margin = Number(params.qrMargin) } + if (params.operation === 'list_domains') { + if (params.domainsSearch) result.search = params.domainsSearch + if (params.domainsArchived === 'true') result.archived = true + if (params.domainsPage) result.page = Number(params.domainsPage) + if (params.domainsPageSize) result.pageSize = Number(params.domainsPageSize) + } + if (params.operation === 'list_tags') { + if (params.tagsSearch) result.search = params.tagsSearch + if (params.tagsSortBy) result.sortBy = params.tagsSortBy + if (params.tagsSortOrder) result.sortOrder = params.tagsSortOrder + if (params.tagsPage) result.page = Number(params.tagsPage) + if (params.tagsPageSize) result.pageSize = Number(params.tagsPageSize) + } + if (params.operation === 'create_tag') { + if (params.tagName) result.name = params.tagName + if (params.tagColor) result.color = params.tagColor + } + if (params.operation === 'list_folders') { + if (params.foldersSearch) result.search = params.foldersSearch + if (params.foldersPage) result.page = Number(params.foldersPage) + if (params.foldersPageSize) result.pageSize = Number(params.foldersPageSize) + } return result }, }, @@ -830,7 +1101,7 @@ export const DubBlock: BlockConfig = { linkIds: { type: 'string', description: 'Comma-separated link IDs for bulk operations' }, }, outputs: { - id: { type: 'string', description: 'Link ID' }, + id: { type: 'string', description: 'Link ID, or Tag ID for Create Tag' }, domain: { type: 'string', description: 'Domain of the short link' }, key: { type: 'string', description: 'Slug of the short link' }, url: { type: 'string', description: 'Destination URL' }, @@ -840,9 +1111,17 @@ export const DubBlock: BlockConfig = { externalId: { type: 'string', description: 'External ID' }, title: { type: 'string', description: 'OG title' }, description: { type: 'string', description: 'OG description' }, - tags: { type: 'json', description: 'Tags assigned to the link (id, name, color)' }, + tags: { + type: 'json', + description: + 'Tags assigned to the link (id, name, color), or the full array of workspace tags for List Tags', + }, + folderId: { type: 'string', description: 'Folder the link is organized into' }, + tenantId: { type: 'string', description: 'Tenant ID associated with the link' }, + trackConversion: { type: 'boolean', description: 'Whether conversion tracking is enabled' }, clicks: { type: 'number', description: 'Number of clicks' }, leads: { type: 'number', description: 'Number of leads' }, + conversions: { type: 'number', description: 'Number of conversions' }, sales: { type: 'number', description: 'Number of sales' }, saleAmount: { type: 'number', description: 'Total sale amount in cents' }, lastClicked: { type: 'string', description: 'Last clicked timestamp' }, @@ -885,6 +1164,26 @@ export const DubBlock: BlockConfig = { deletedCount: { type: 'number', description: 'Bulk delete: number of links deleted' }, file: { type: 'file', description: 'QR code image (PNG) stored in execution files' }, content: { type: 'string', description: 'QR code as base64-encoded PNG data' }, + domains: { + type: 'json', + description: 'List Domains: array of domain objects (slug, verified, primary, archived)', + condition: { field: 'operation', value: 'list_domains' }, + }, + folders: { + type: 'json', + description: 'List Folders: array of folder objects (id, name, accessLevel)', + condition: { field: 'operation', value: 'list_folders' }, + }, + name: { + type: 'string', + description: 'Create Tag: name of the created tag', + condition: { field: 'operation', value: 'create_tag' }, + }, + color: { + type: 'string', + description: 'Create Tag: color assigned to the created tag', + condition: { field: 'operation', value: 'create_tag' }, + }, }, } diff --git a/apps/sim/tools/dub/create_link.ts b/apps/sim/tools/dub/create_link.ts index 462a362b9ff..91179052923 100644 --- a/apps/sim/tools/dub/create_link.ts +++ b/apps/sim/tools/dub/create_link.ts @@ -39,6 +39,24 @@ export const createLinkTool: ToolConfig id.trim()) if (params.comments) body.comments = params.comments if (params.expiresAt) body.expiresAt = params.expiresAt @@ -169,8 +190,12 @@ export const createLinkTool: ToolConfig = { + id: 'dub_create_tag', + name: 'Dub Create Tag', + description: 'Create a new tag in the workspace for organizing and filtering short links.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Dub API key', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The name of the tag to create (1-50 characters)', + }, + color: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Tag color: red, yellow, green, blue, purple, brown, gray, or pink (random if omitted)', + }, + }, + + request: { + url: 'https://api.dub.co/tags', + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + body: (params) => { + const body: Record = { name: params.name } + if (params.color) body.color = params.color + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || data.error || 'Failed to create tag') + } + + return { + success: true, + output: { + id: data.id ?? '', + name: data.name ?? '', + color: data.color ?? '', + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Unique ID of the created tag' }, + name: { type: 'string', description: 'Name of the tag' }, + color: { type: 'string', description: 'Color assigned to the tag' }, + }, +} diff --git a/apps/sim/tools/dub/get_link.ts b/apps/sim/tools/dub/get_link.ts index dcb9c952a0a..bcb651cf009 100644 --- a/apps/sim/tools/dub/get_link.ts +++ b/apps/sim/tools/dub/get_link.ts @@ -78,8 +78,12 @@ export const getLinkTool: ToolConfig = { title: data.title ?? null, description: data.description ?? null, tags: data.tags ?? [], + folderId: data.folderId ?? null, + tenantId: data.tenantId ?? null, + trackConversion: data.trackConversion ?? false, clicks: data.clicks ?? 0, leads: data.leads ?? 0, + conversions: data.conversions ?? 0, sales: data.sales ?? 0, saleAmount: data.saleAmount ?? 0, lastClicked: data.lastClicked ?? null, @@ -106,8 +110,12 @@ export const getLinkTool: ToolConfig = { title: { type: 'string', description: 'OG title', optional: true }, description: { type: 'string', description: 'OG description', optional: true }, tags: { type: 'json', description: 'Tags assigned to the link (id, name, color)' }, + folderId: { type: 'string', description: 'Folder the link is organized into', optional: true }, + tenantId: { type: 'string', description: 'Tenant ID associated with the link', optional: true }, + trackConversion: { type: 'boolean', description: 'Whether conversion tracking is enabled' }, clicks: { type: 'number', description: 'Number of clicks' }, leads: { type: 'number', description: 'Number of leads' }, + conversions: { type: 'number', description: 'Number of conversions' }, sales: { type: 'number', description: 'Number of sales' }, saleAmount: { type: 'number', description: 'Total sale amount in cents' }, lastClicked: { type: 'string', description: 'Last clicked timestamp', optional: true }, diff --git a/apps/sim/tools/dub/get_qr_code.ts b/apps/sim/tools/dub/get_qr_code.ts index 6502d66b1f2..443ca643e38 100644 --- a/apps/sim/tools/dub/get_qr_code.ts +++ b/apps/sim/tools/dub/get_qr_code.ts @@ -21,6 +21,12 @@ export const getQrCodeTool: ToolConfig visibility: 'user-or-llm', description: 'The short link URL to encode in the QR code', }, + logo: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'URL of a custom logo to embed in the QR code (requires a paid Dub plan)', + }, size: { type: 'number', required: false, @@ -63,6 +69,7 @@ export const getQrCodeTool: ToolConfig url: (params) => { const url = new URL('https://api.dub.co/qr') url.searchParams.set('url', params.url.trim()) + if (params.logo) url.searchParams.set('logo', params.logo) if (params.size !== undefined) url.searchParams.set('size', String(params.size)) if (params.level) url.searchParams.set('level', params.level) if (params.fgColor) url.searchParams.set('fgColor', params.fgColor) diff --git a/apps/sim/tools/dub/index.ts b/apps/sim/tools/dub/index.ts index 401d48c5f03..09030412b92 100644 --- a/apps/sim/tools/dub/index.ts +++ b/apps/sim/tools/dub/index.ts @@ -2,13 +2,17 @@ import { bulkCreateLinksTool } from '@/tools/dub/bulk_create_links' import { bulkDeleteLinksTool } from '@/tools/dub/bulk_delete_links' import { bulkUpdateLinksTool } from '@/tools/dub/bulk_update_links' import { createLinkTool } from '@/tools/dub/create_link' +import { createTagTool } from '@/tools/dub/create_tag' import { deleteLinkTool } from '@/tools/dub/delete_link' import { getAnalyticsTool } from '@/tools/dub/get_analytics' import { getEventsTool } from '@/tools/dub/get_events' import { getLinkTool } from '@/tools/dub/get_link' import { getLinksCountTool } from '@/tools/dub/get_links_count' import { getQrCodeTool } from '@/tools/dub/get_qr_code' +import { listDomainsTool } from '@/tools/dub/list_domains' +import { listFoldersTool } from '@/tools/dub/list_folders' import { listLinksTool } from '@/tools/dub/list_links' +import { listTagsTool } from '@/tools/dub/list_tags' import { updateLinkTool } from '@/tools/dub/update_link' import { upsertLinkTool } from '@/tools/dub/upsert_link' @@ -25,3 +29,7 @@ export const dubBulkCreateLinksTool = bulkCreateLinksTool export const dubBulkUpdateLinksTool = bulkUpdateLinksTool export const dubBulkDeleteLinksTool = bulkDeleteLinksTool export const dubGetQrCodeTool = getQrCodeTool +export const dubListDomainsTool = listDomainsTool +export const dubListTagsTool = listTagsTool +export const dubCreateTagTool = createTagTool +export const dubListFoldersTool = listFoldersTool diff --git a/apps/sim/tools/dub/list_domains.ts b/apps/sim/tools/dub/list_domains.ts new file mode 100644 index 00000000000..b90071e10a5 --- /dev/null +++ b/apps/sim/tools/dub/list_domains.ts @@ -0,0 +1,85 @@ +import type { DubListDomainsParams, DubListDomainsResponse } from '@/tools/dub/types' +import type { ToolConfig } from '@/tools/types' + +export const listDomainsTool: ToolConfig = { + id: 'dub_list_domains', + name: 'Dub List Domains', + description: + 'Retrieve the custom domains registered in the workspace, so links can be created against the right domain.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Dub API key', + }, + archived: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to include archived domains (defaults to false)', + }, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search by domain name', + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number (default: 1)', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of domains per page (default: 50, max: 50)', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.dub.co/domains') + if (params.archived !== undefined) url.searchParams.set('archived', String(params.archived)) + if (params.search) url.searchParams.set('search', params.search) + if (params.page) url.searchParams.set('page', String(params.page)) + if (params.pageSize) url.searchParams.set('pageSize', String(params.pageSize)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || data.error || 'Failed to list domains') + } + + const domains = Array.isArray(data) ? (data as Record[]) : [] + + return { + success: true, + output: { + domains, + count: domains.length, + }, + } + }, + + outputs: { + domains: { + type: 'json', + description: 'Array of domain objects (slug, verified, primary, archived)', + }, + count: { type: 'number', description: 'Number of domains returned' }, + }, +} diff --git a/apps/sim/tools/dub/list_folders.ts b/apps/sim/tools/dub/list_folders.ts new file mode 100644 index 00000000000..cf716388480 --- /dev/null +++ b/apps/sim/tools/dub/list_folders.ts @@ -0,0 +1,78 @@ +import type { DubListFoldersParams, DubListFoldersResponse } from '@/tools/dub/types' +import type { ToolConfig } from '@/tools/types' + +export const listFoldersTool: ToolConfig = { + id: 'dub_list_folders', + name: 'Dub List Folders', + description: + 'Retrieve the folders defined in the workspace, so the right folder ID can be used to organize links.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Dub API key', + }, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search by folder name', + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number (default: 1)', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of folders per page (default: 50, max: 50)', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.dub.co/folders') + if (params.search) url.searchParams.set('search', params.search) + if (params.page) url.searchParams.set('page', String(params.page)) + if (params.pageSize) url.searchParams.set('pageSize', String(params.pageSize)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || data.error || 'Failed to list folders') + } + + const folders = Array.isArray(data) ? (data as Record[]) : [] + + return { + success: true, + output: { + folders, + count: folders.length, + }, + } + }, + + outputs: { + folders: { + type: 'json', + description: 'Array of folder objects (id, name, accessLevel)', + }, + count: { type: 'number', description: 'Number of folders returned' }, + }, +} diff --git a/apps/sim/tools/dub/list_links.ts b/apps/sim/tools/dub/list_links.ts index c5d478ebb0c..f22ae119f4d 100644 --- a/apps/sim/tools/dub/list_links.ts +++ b/apps/sim/tools/dub/list_links.ts @@ -33,6 +33,18 @@ export const listLinksTool: ToolConfig visibility: 'user-or-llm', description: 'Comma-separated tag IDs to filter by', }, + tenantId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by tenant ID', + }, + folderId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by folder ID', + }, showArchived: { type: 'boolean', required: false, @@ -43,7 +55,7 @@ export const listLinksTool: ToolConfig type: 'number', required: false, visibility: 'user-or-llm', - description: 'Page number (default: 1)', + description: 'Page number (deprecated by Dub in favor of startingAfter/endingBefore)', }, pageSize: { type: 'number', @@ -51,6 +63,18 @@ export const listLinksTool: ToolConfig visibility: 'user-or-llm', description: 'Number of links per page (default: 100, max: 100)', }, + startingAfter: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Cursor: fetch results after this link ID', + }, + endingBefore: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Cursor: fetch results before this link ID', + }, }, request: { @@ -59,10 +83,14 @@ export const listLinksTool: ToolConfig if (params.domain) url.searchParams.set('domain', params.domain) if (params.search) url.searchParams.set('search', params.search) if (params.tagIds) url.searchParams.set('tagIds', params.tagIds) + if (params.tenantId) url.searchParams.set('tenantId', params.tenantId) + if (params.folderId) url.searchParams.set('folderId', params.folderId) if (params.showArchived !== undefined) url.searchParams.set('showArchived', String(params.showArchived)) if (params.page) url.searchParams.set('page', String(params.page)) if (params.pageSize) url.searchParams.set('pageSize', String(params.pageSize)) + if (params.startingAfter) url.searchParams.set('startingAfter', params.startingAfter) + if (params.endingBefore) url.searchParams.set('endingBefore', params.endingBefore) return url.toString() }, method: 'GET', @@ -97,12 +125,16 @@ export const listLinksTool: ToolConfig description: (link.description as string) ?? null, clicks: (link.clicks as number) ?? 0, leads: (link.leads as number) ?? 0, + conversions: (link.conversions as number) ?? 0, sales: (link.sales as number) ?? 0, saleAmount: (link.saleAmount as number) ?? 0, lastClicked: (link.lastClicked as string) ?? null, createdAt: (link.createdAt as string) ?? '', updatedAt: (link.updatedAt as string) ?? '', tags: (link.tags as Array<{ id: string; name: string; color: string }>) ?? [], + folderId: (link.folderId as string) ?? null, + tenantId: (link.tenantId as string) ?? null, + trackConversion: (link.trackConversion as boolean) ?? false, utm_source: (link.utm_source as string) ?? null, utm_medium: (link.utm_medium as string) ?? null, utm_campaign: (link.utm_campaign as string) ?? null, diff --git a/apps/sim/tools/dub/list_tags.ts b/apps/sim/tools/dub/list_tags.ts new file mode 100644 index 00000000000..8503dffd9d4 --- /dev/null +++ b/apps/sim/tools/dub/list_tags.ts @@ -0,0 +1,92 @@ +import type { DubListTagsParams, DubListTagsResponse } from '@/tools/dub/types' +import type { ToolConfig } from '@/tools/types' + +export const listTagsTool: ToolConfig = { + id: 'dub_list_tags', + name: 'Dub List Tags', + description: + 'Retrieve the tags defined in the workspace, so the right tag IDs can be assigned to links.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Dub API key', + }, + search: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Search by tag name', + }, + sortBy: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Field to sort by: name (default) or createdAt', + }, + sortOrder: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort order: asc (default) or desc', + }, + page: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Page number (default: 1)', + }, + pageSize: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of tags per page (default: 100, max: 100)', + }, + }, + + request: { + url: (params) => { + const url = new URL('https://api.dub.co/tags') + if (params.search) url.searchParams.set('search', params.search) + if (params.sortBy) url.searchParams.set('sortBy', params.sortBy) + if (params.sortOrder) url.searchParams.set('sortOrder', params.sortOrder) + if (params.page) url.searchParams.set('page', String(params.page)) + if (params.pageSize) url.searchParams.set('pageSize', String(params.pageSize)) + return url.toString() + }, + method: 'GET', + headers: (params) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.apiKey}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error?.message || data.error || 'Failed to list tags') + } + + const tags = Array.isArray(data) ? (data as Record[]) : [] + + return { + success: true, + output: { + tags, + count: tags.length, + }, + } + }, + + outputs: { + tags: { + type: 'json', + description: 'Array of tag objects (id, name, color)', + }, + count: { type: 'number', description: 'Number of tags returned' }, + }, +} diff --git a/apps/sim/tools/dub/types.ts b/apps/sim/tools/dub/types.ts index 53488e74c1d..6270a26f3f8 100644 --- a/apps/sim/tools/dub/types.ts +++ b/apps/sim/tools/dub/types.ts @@ -9,6 +9,9 @@ export interface DubCreateLinkParams extends DubBaseParams { domain?: string key?: string externalId?: string + tenantId?: string + folderId?: string + trackConversion?: boolean tagIds?: string comments?: string expiresAt?: string @@ -39,6 +42,9 @@ export interface DubUpdateLinkParams extends DubBaseParams { title?: string description?: string externalId?: string + tenantId?: string + folderId?: string + trackConversion?: boolean tagIds?: string comments?: string expiresAt?: string @@ -57,6 +63,9 @@ export interface DubUpsertLinkParams extends DubBaseParams { domain?: string key?: string externalId?: string + tenantId?: string + folderId?: string + trackConversion?: boolean tagIds?: string comments?: string expiresAt?: string @@ -80,9 +89,13 @@ export interface DubListLinksParams extends DubBaseParams { domain?: string search?: string tagIds?: string + tenantId?: string + folderId?: string showArchived?: boolean page?: number pageSize?: number + startingAfter?: string + endingBefore?: string } export interface DubGetAnalyticsParams extends DubBaseParams { @@ -139,6 +152,7 @@ export interface DubBulkDeleteLinksParams extends DubBaseParams { export interface DubGetQrCodeParams extends DubBaseParams { url: string + logo?: string size?: number level?: string fgColor?: string @@ -147,6 +161,32 @@ export interface DubGetQrCodeParams extends DubBaseParams { margin?: number } +export interface DubListDomainsParams extends DubBaseParams { + archived?: boolean + search?: string + page?: number + pageSize?: number +} + +export interface DubListTagsParams extends DubBaseParams { + search?: string + sortBy?: string + sortOrder?: string + page?: number + pageSize?: number +} + +export interface DubCreateTagParams extends DubBaseParams { + name: string + color?: string +} + +export interface DubListFoldersParams extends DubBaseParams { + search?: string + page?: number + pageSize?: number +} + interface DubLink { id: string domain: string @@ -159,8 +199,12 @@ interface DubLink { title: string | null description: string | null tags: Array<{ id: string; name: string; color: string }> + folderId: string | null + tenantId: string | null + trackConversion: boolean clicks: number leads: number + conversions: number sales: number saleAmount: number lastClicked: string | null @@ -259,6 +303,35 @@ export interface DubGetQrCodeResponse extends ToolResponse { } } +export interface DubListDomainsResponse extends ToolResponse { + output: { + domains: Record[] + count: number + } +} + +export interface DubListTagsResponse extends ToolResponse { + output: { + tags: Record[] + count: number + } +} + +export interface DubCreateTagResponse extends ToolResponse { + output: { + id: string + name: string + color: string + } +} + +export interface DubListFoldersResponse extends ToolResponse { + output: { + folders: Record[] + count: number + } +} + export type DubResponse = | DubCreateLinkResponse | DubGetLinkResponse @@ -273,3 +346,7 @@ export type DubResponse = | DubBulkUpdateLinksResponse | DubBulkDeleteLinksResponse | DubGetQrCodeResponse + | DubListDomainsResponse + | DubListTagsResponse + | DubCreateTagResponse + | DubListFoldersResponse diff --git a/apps/sim/tools/dub/update_link.ts b/apps/sim/tools/dub/update_link.ts index 87f7054050f..7a5b1ca95d0 100644 --- a/apps/sim/tools/dub/update_link.ts +++ b/apps/sim/tools/dub/update_link.ts @@ -57,6 +57,24 @@ export const updateLinkTool: ToolConfig id.trim()) if (params.comments) body.comments = params.comments if (params.expiresAt) body.expiresAt = params.expiresAt @@ -176,8 +197,12 @@ export const updateLinkTool: ToolConfig id.trim()) if (params.comments) body.comments = params.comments if (params.expiresAt) body.expiresAt = params.expiresAt @@ -169,8 +190,12 @@ export const upsertLinkTool: ToolConfig = { dub_bulk_delete_links: dubBulkDeleteLinksTool, dub_bulk_update_links: dubBulkUpdateLinksTool, dub_create_link: dubCreateLinkTool, + dub_create_tag: dubCreateTagTool, dub_delete_link: dubDeleteLinkTool, dub_get_analytics: dubGetAnalyticsTool, dub_get_events: dubGetEventsTool, dub_get_link: dubGetLinkTool, dub_get_links_count: dubGetLinksCountTool, dub_get_qr_code: dubGetQrCodeTool, + dub_list_domains: dubListDomainsTool, + dub_list_folders: dubListFoldersTool, dub_list_links: dubListLinksTool, + dub_list_tags: dubListTagsTool, dub_update_link: dubUpdateLinkTool, dub_upsert_link: dubUpsertLinkTool, duckduckgo_search: duckduckgoSearchTool, From 86cc363d47fa4303c70aa8e62a79eb75a90ea554 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Jul 2026 08:20:28 -0700 Subject: [PATCH 2/2] fix(dub): stop trackConversion from resetting existing links on update - only send trackConversion true or omit it, matching linkRewrite/linkArchived's partial-PATCH-safe pattern, so a routine update_link no longer silently disables conversion tracking on an existing link - include trackConversion in the cross-operation reset block so it can't leak a stale value into non-mutation operations either --- apps/sim/blocks/blocks/dub.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/sim/blocks/blocks/dub.ts b/apps/sim/blocks/blocks/dub.ts index 1494a712852..76a9ac242db 100644 --- a/apps/sim/blocks/blocks/dub.ts +++ b/apps/sim/blocks/blocks/dub.ts @@ -962,6 +962,7 @@ export const DubBlock: BlockConfig = { result.tagIds = undefined result.tenantId = undefined result.folderId = undefined + result.trackConversion = undefined } if (params.operation !== 'create_link' && params.operation !== 'upsert_link') { result.url = undefined @@ -985,7 +986,10 @@ export const DubBlock: BlockConfig = { if (params.linkArchived === 'true') result.archived = true if (params.tenantId) result.tenantId = params.tenantId if (params.folderId) result.folderId = params.folderId - result.trackConversion = params.trackConversion === 'true' + // Only ever send `true` or omit — an explicit `false` on update_link would + // silently disable conversion tracking on links that already had it enabled, + // since Dub's update is a partial PATCH (matches linkRewrite/linkArchived). + result.trackConversion = params.trackConversion === 'true' ? true : undefined } if (params.operation === 'get_link') { if (params.getLinkExternalId) result.externalId = params.getLinkExternalId