Skip to content

Commit a742bdd

Browse files
committed
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
1 parent 3ddf254 commit a742bdd

14 files changed

Lines changed: 853 additions & 4 deletions

apps/sim/blocks/blocks/dub.ts

Lines changed: 302 additions & 3 deletions
Large diffs are not rendered by default.

apps/sim/tools/dub/create_link.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,24 @@ export const createLinkTool: ToolConfig<DubCreateLinkParams, DubCreateLinkRespon
3939
visibility: 'user-or-llm',
4040
description: 'External ID for the link in your database',
4141
},
42+
tenantId: {
43+
type: 'string',
44+
required: false,
45+
visibility: 'user-or-llm',
46+
description: 'Tenant ID for grouping links created on behalf of a customer/tenant',
47+
},
48+
folderId: {
49+
type: 'string',
50+
required: false,
51+
visibility: 'user-or-llm',
52+
description: 'Folder ID to organize the link into',
53+
},
54+
trackConversion: {
55+
type: 'boolean',
56+
required: false,
57+
visibility: 'user-or-llm',
58+
description: 'Whether to track conversions (leads/sales) for the short link',
59+
},
4260
tagIds: {
4361
type: 'string',
4462
required: false,
@@ -131,6 +149,9 @@ export const createLinkTool: ToolConfig<DubCreateLinkParams, DubCreateLinkRespon
131149
if (params.domain) body.domain = params.domain
132150
if (params.key) body.key = params.key
133151
if (params.externalId) body.externalId = params.externalId
152+
if (params.tenantId) body.tenantId = params.tenantId
153+
if (params.folderId) body.folderId = params.folderId
154+
if (params.trackConversion !== undefined) body.trackConversion = params.trackConversion
134155
if (params.tagIds) body.tagIds = params.tagIds.split(',').map((id) => id.trim())
135156
if (params.comments) body.comments = params.comments
136157
if (params.expiresAt) body.expiresAt = params.expiresAt
@@ -169,8 +190,12 @@ export const createLinkTool: ToolConfig<DubCreateLinkParams, DubCreateLinkRespon
169190
title: data.title ?? null,
170191
description: data.description ?? null,
171192
tags: data.tags ?? [],
193+
folderId: data.folderId ?? null,
194+
tenantId: data.tenantId ?? null,
195+
trackConversion: data.trackConversion ?? false,
172196
clicks: data.clicks ?? 0,
173197
leads: data.leads ?? 0,
198+
conversions: data.conversions ?? 0,
174199
sales: data.sales ?? 0,
175200
saleAmount: data.saleAmount ?? 0,
176201
lastClicked: data.lastClicked ?? null,
@@ -197,8 +222,12 @@ export const createLinkTool: ToolConfig<DubCreateLinkParams, DubCreateLinkRespon
197222
title: { type: 'string', description: 'OG title', optional: true },
198223
description: { type: 'string', description: 'OG description', optional: true },
199224
tags: { type: 'json', description: 'Tags assigned to the link (id, name, color)' },
225+
folderId: { type: 'string', description: 'Folder the link is organized into', optional: true },
226+
tenantId: { type: 'string', description: 'Tenant ID associated with the link', optional: true },
227+
trackConversion: { type: 'boolean', description: 'Whether conversion tracking is enabled' },
200228
clicks: { type: 'number', description: 'Number of clicks' },
201229
leads: { type: 'number', description: 'Number of leads' },
230+
conversions: { type: 'number', description: 'Number of conversions' },
202231
sales: { type: 'number', description: 'Number of sales' },
203232
saleAmount: { type: 'number', description: 'Total sale amount in cents' },
204233
lastClicked: { type: 'string', description: 'Last clicked timestamp', optional: true },

apps/sim/tools/dub/create_tag.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import type { DubCreateTagParams, DubCreateTagResponse } from '@/tools/dub/types'
2+
import type { ToolConfig } from '@/tools/types'
3+
4+
export const createTagTool: ToolConfig<DubCreateTagParams, DubCreateTagResponse> = {
5+
id: 'dub_create_tag',
6+
name: 'Dub Create Tag',
7+
description: 'Create a new tag in the workspace for organizing and filtering short links.',
8+
version: '1.0.0',
9+
10+
params: {
11+
apiKey: {
12+
type: 'string',
13+
required: true,
14+
visibility: 'user-only',
15+
description: 'Dub API key',
16+
},
17+
name: {
18+
type: 'string',
19+
required: true,
20+
visibility: 'user-or-llm',
21+
description: 'The name of the tag to create (1-50 characters)',
22+
},
23+
color: {
24+
type: 'string',
25+
required: false,
26+
visibility: 'user-or-llm',
27+
description:
28+
'Tag color: red, yellow, green, blue, purple, brown, gray, or pink (random if omitted)',
29+
},
30+
},
31+
32+
request: {
33+
url: 'https://api.dub.co/tags',
34+
method: 'POST',
35+
headers: (params) => ({
36+
'Content-Type': 'application/json',
37+
Authorization: `Bearer ${params.apiKey}`,
38+
}),
39+
body: (params) => {
40+
const body: Record<string, unknown> = { name: params.name }
41+
if (params.color) body.color = params.color
42+
return body
43+
},
44+
},
45+
46+
transformResponse: async (response: Response) => {
47+
const data = await response.json()
48+
49+
if (!response.ok) {
50+
throw new Error(data.error?.message || data.error || 'Failed to create tag')
51+
}
52+
53+
return {
54+
success: true,
55+
output: {
56+
id: data.id ?? '',
57+
name: data.name ?? '',
58+
color: data.color ?? '',
59+
},
60+
}
61+
},
62+
63+
outputs: {
64+
id: { type: 'string', description: 'Unique ID of the created tag' },
65+
name: { type: 'string', description: 'Name of the tag' },
66+
color: { type: 'string', description: 'Color assigned to the tag' },
67+
},
68+
}

apps/sim/tools/dub/get_link.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,12 @@ export const getLinkTool: ToolConfig<DubGetLinkParams, DubGetLinkResponse> = {
7878
title: data.title ?? null,
7979
description: data.description ?? null,
8080
tags: data.tags ?? [],
81+
folderId: data.folderId ?? null,
82+
tenantId: data.tenantId ?? null,
83+
trackConversion: data.trackConversion ?? false,
8184
clicks: data.clicks ?? 0,
8285
leads: data.leads ?? 0,
86+
conversions: data.conversions ?? 0,
8387
sales: data.sales ?? 0,
8488
saleAmount: data.saleAmount ?? 0,
8589
lastClicked: data.lastClicked ?? null,
@@ -106,8 +110,12 @@ export const getLinkTool: ToolConfig<DubGetLinkParams, DubGetLinkResponse> = {
106110
title: { type: 'string', description: 'OG title', optional: true },
107111
description: { type: 'string', description: 'OG description', optional: true },
108112
tags: { type: 'json', description: 'Tags assigned to the link (id, name, color)' },
113+
folderId: { type: 'string', description: 'Folder the link is organized into', optional: true },
114+
tenantId: { type: 'string', description: 'Tenant ID associated with the link', optional: true },
115+
trackConversion: { type: 'boolean', description: 'Whether conversion tracking is enabled' },
109116
clicks: { type: 'number', description: 'Number of clicks' },
110117
leads: { type: 'number', description: 'Number of leads' },
118+
conversions: { type: 'number', description: 'Number of conversions' },
111119
sales: { type: 'number', description: 'Number of sales' },
112120
saleAmount: { type: 'number', description: 'Total sale amount in cents' },
113121
lastClicked: { type: 'string', description: 'Last clicked timestamp', optional: true },

apps/sim/tools/dub/get_qr_code.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ export const getQrCodeTool: ToolConfig<DubGetQrCodeParams, DubGetQrCodeResponse>
2121
visibility: 'user-or-llm',
2222
description: 'The short link URL to encode in the QR code',
2323
},
24+
logo: {
25+
type: 'string',
26+
required: false,
27+
visibility: 'user-or-llm',
28+
description: 'URL of a custom logo to embed in the QR code (requires a paid Dub plan)',
29+
},
2430
size: {
2531
type: 'number',
2632
required: false,
@@ -63,6 +69,7 @@ export const getQrCodeTool: ToolConfig<DubGetQrCodeParams, DubGetQrCodeResponse>
6369
url: (params) => {
6470
const url = new URL('https://api.dub.co/qr')
6571
url.searchParams.set('url', params.url.trim())
72+
if (params.logo) url.searchParams.set('logo', params.logo)
6673
if (params.size !== undefined) url.searchParams.set('size', String(params.size))
6774
if (params.level) url.searchParams.set('level', params.level)
6875
if (params.fgColor) url.searchParams.set('fgColor', params.fgColor)

apps/sim/tools/dub/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ import { bulkCreateLinksTool } from '@/tools/dub/bulk_create_links'
22
import { bulkDeleteLinksTool } from '@/tools/dub/bulk_delete_links'
33
import { bulkUpdateLinksTool } from '@/tools/dub/bulk_update_links'
44
import { createLinkTool } from '@/tools/dub/create_link'
5+
import { createTagTool } from '@/tools/dub/create_tag'
56
import { deleteLinkTool } from '@/tools/dub/delete_link'
67
import { getAnalyticsTool } from '@/tools/dub/get_analytics'
78
import { getEventsTool } from '@/tools/dub/get_events'
89
import { getLinkTool } from '@/tools/dub/get_link'
910
import { getLinksCountTool } from '@/tools/dub/get_links_count'
1011
import { getQrCodeTool } from '@/tools/dub/get_qr_code'
12+
import { listDomainsTool } from '@/tools/dub/list_domains'
13+
import { listFoldersTool } from '@/tools/dub/list_folders'
1114
import { listLinksTool } from '@/tools/dub/list_links'
15+
import { listTagsTool } from '@/tools/dub/list_tags'
1216
import { updateLinkTool } from '@/tools/dub/update_link'
1317
import { upsertLinkTool } from '@/tools/dub/upsert_link'
1418

@@ -25,3 +29,7 @@ export const dubBulkCreateLinksTool = bulkCreateLinksTool
2529
export const dubBulkUpdateLinksTool = bulkUpdateLinksTool
2630
export const dubBulkDeleteLinksTool = bulkDeleteLinksTool
2731
export const dubGetQrCodeTool = getQrCodeTool
32+
export const dubListDomainsTool = listDomainsTool
33+
export const dubListTagsTool = listTagsTool
34+
export const dubCreateTagTool = createTagTool
35+
export const dubListFoldersTool = listFoldersTool

apps/sim/tools/dub/list_domains.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import type { DubListDomainsParams, DubListDomainsResponse } from '@/tools/dub/types'
2+
import type { ToolConfig } from '@/tools/types'
3+
4+
export const listDomainsTool: ToolConfig<DubListDomainsParams, DubListDomainsResponse> = {
5+
id: 'dub_list_domains',
6+
name: 'Dub List Domains',
7+
description:
8+
'Retrieve the custom domains registered in the workspace, so links can be created against the right domain.',
9+
version: '1.0.0',
10+
11+
params: {
12+
apiKey: {
13+
type: 'string',
14+
required: true,
15+
visibility: 'user-only',
16+
description: 'Dub API key',
17+
},
18+
archived: {
19+
type: 'boolean',
20+
required: false,
21+
visibility: 'user-or-llm',
22+
description: 'Whether to include archived domains (defaults to false)',
23+
},
24+
search: {
25+
type: 'string',
26+
required: false,
27+
visibility: 'user-or-llm',
28+
description: 'Search by domain name',
29+
},
30+
page: {
31+
type: 'number',
32+
required: false,
33+
visibility: 'user-or-llm',
34+
description: 'Page number (default: 1)',
35+
},
36+
pageSize: {
37+
type: 'number',
38+
required: false,
39+
visibility: 'user-or-llm',
40+
description: 'Number of domains per page (default: 50, max: 50)',
41+
},
42+
},
43+
44+
request: {
45+
url: (params) => {
46+
const url = new URL('https://api.dub.co/domains')
47+
if (params.archived !== undefined) url.searchParams.set('archived', String(params.archived))
48+
if (params.search) url.searchParams.set('search', params.search)
49+
if (params.page) url.searchParams.set('page', String(params.page))
50+
if (params.pageSize) url.searchParams.set('pageSize', String(params.pageSize))
51+
return url.toString()
52+
},
53+
method: 'GET',
54+
headers: (params) => ({
55+
Accept: 'application/json',
56+
Authorization: `Bearer ${params.apiKey}`,
57+
}),
58+
},
59+
60+
transformResponse: async (response: Response) => {
61+
const data = await response.json()
62+
63+
if (!response.ok) {
64+
throw new Error(data.error?.message || data.error || 'Failed to list domains')
65+
}
66+
67+
const domains = Array.isArray(data) ? (data as Record<string, unknown>[]) : []
68+
69+
return {
70+
success: true,
71+
output: {
72+
domains,
73+
count: domains.length,
74+
},
75+
}
76+
},
77+
78+
outputs: {
79+
domains: {
80+
type: 'json',
81+
description: 'Array of domain objects (slug, verified, primary, archived)',
82+
},
83+
count: { type: 'number', description: 'Number of domains returned' },
84+
},
85+
}

apps/sim/tools/dub/list_folders.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import type { DubListFoldersParams, DubListFoldersResponse } from '@/tools/dub/types'
2+
import type { ToolConfig } from '@/tools/types'
3+
4+
export const listFoldersTool: ToolConfig<DubListFoldersParams, DubListFoldersResponse> = {
5+
id: 'dub_list_folders',
6+
name: 'Dub List Folders',
7+
description:
8+
'Retrieve the folders defined in the workspace, so the right folder ID can be used to organize links.',
9+
version: '1.0.0',
10+
11+
params: {
12+
apiKey: {
13+
type: 'string',
14+
required: true,
15+
visibility: 'user-only',
16+
description: 'Dub API key',
17+
},
18+
search: {
19+
type: 'string',
20+
required: false,
21+
visibility: 'user-or-llm',
22+
description: 'Search by folder name',
23+
},
24+
page: {
25+
type: 'number',
26+
required: false,
27+
visibility: 'user-or-llm',
28+
description: 'Page number (default: 1)',
29+
},
30+
pageSize: {
31+
type: 'number',
32+
required: false,
33+
visibility: 'user-or-llm',
34+
description: 'Number of folders per page (default: 50, max: 50)',
35+
},
36+
},
37+
38+
request: {
39+
url: (params) => {
40+
const url = new URL('https://api.dub.co/folders')
41+
if (params.search) url.searchParams.set('search', params.search)
42+
if (params.page) url.searchParams.set('page', String(params.page))
43+
if (params.pageSize) url.searchParams.set('pageSize', String(params.pageSize))
44+
return url.toString()
45+
},
46+
method: 'GET',
47+
headers: (params) => ({
48+
Accept: 'application/json',
49+
Authorization: `Bearer ${params.apiKey}`,
50+
}),
51+
},
52+
53+
transformResponse: async (response: Response) => {
54+
const data = await response.json()
55+
56+
if (!response.ok) {
57+
throw new Error(data.error?.message || data.error || 'Failed to list folders')
58+
}
59+
60+
const folders = Array.isArray(data) ? (data as Record<string, unknown>[]) : []
61+
62+
return {
63+
success: true,
64+
output: {
65+
folders,
66+
count: folders.length,
67+
},
68+
}
69+
},
70+
71+
outputs: {
72+
folders: {
73+
type: 'json',
74+
description: 'Array of folder objects (id, name, accessLevel)',
75+
},
76+
count: { type: 'number', description: 'Number of folders returned' },
77+
},
78+
}

0 commit comments

Comments
 (0)