Compare commits

...

11 Commits

Author SHA1 Message Date
Waleed
15ace5e63f v0.5.94: vercel integration, folder insertion, migrated tracking redirects to rewrites 2026-02-18 16:53:34 -08:00
Waleed
ab48787422 chore(deps): upgrade next.js from 16.1.0-canary.21 to 16.1.6 (#3254) 2026-02-18 16:25:28 -08:00
Waleed
91aa1f9a52 feat(tools): added vercel block & tools (#3252)
* feat(vercel): add complete Vercel integration with 42 API tools

Add Vercel platform management integration covering deployments, projects,
environment variables, domains, DNS records, aliases, edge configs, and
team/user management. All tools use API key authentication with Bearer tokens.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(vercel): add webhook and deployment check tools

Add 8 new Vercel API tools:
- Webhooks: list, create, delete
- Deployment Checks: create, get, list, update, rerequest

Brings total Vercel tools to 50.

* fix(vercel): expand all object and array output definitions

Expand unexpanded output types:
- get_deployment: meta and gitSource objects now have properties
- list_deployment_files: children array now has items definition
- get_team: teamRoles and teamPermissions arrays now have items

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* update icon size, update docs

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 15:54:49 -08:00
Waleed
2979269ac3 fix(sidebar): unify workflow and folder insertion ordering (#3250)
* fix(sidebar): unify workflow and folder insertion ordering

* ack comments

* ack comments

* ack

* ack comment

* upgrade turbo

* fix build
2026-02-18 14:41:55 -08:00
Waleed
cf28822a1c fix(shortlink): remove isHosted guard from redirects, not available at build time on ECS (#3251)
* fix(shortlink): remove isHosted guard from redirects, not available at build time on ECS

* fix(shortlink): use rewrite instead of redirect for Beluga tracking
2026-02-18 14:00:25 -08:00
Waleed
fdca73679d v0.5.93: NextJS config changes, MCP and Blocks whitelisting, copilot keyboard shortcuts, audit logs 2026-02-18 12:10:05 -08:00
Waleed
86ca984926 fix(normalization): update allowed integrations checks to be fully lowercase (#3248) 2026-02-18 12:08:03 -08:00
Emir Karabeg
e3964624ac feat(sub): hide usage limits and seats info from enterprise members (non-admin) (#3243)
- Add isEnterpriseMember and canViewUsageInfo flags to subscription permissions
- Hide UsageHeader, CreditBalance, billing date, and usage notifications from enterprise members
- Show only plan name in subscription tab for enterprise members (non-admin)
- Hide usage indicator details (amount, progress pills) from enterprise members
- Team tab already hidden via requiresTeam check in settings modal

Closes #6882

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Emir Karabeg <emir-karabeg@users.noreply.github.com>
2026-02-18 12:01:47 -08:00
Waleed
7c7c0fd955 feat(audit-log): add audit events for templates, billing, credentials, env, deployments, passwords (#3246)
* feat(audit-log): add audit events for templates, billing, credentials, env, deployments, passwords

* improvement(audit-log): add actorName/actorEmail to all recordAudit calls

* fix(audit-log): resolve user for password reset, add CREDENTIAL_SET_INVITATION_RESENT action

* fix(audit-log): add workspaceId to deployment activation audit

* improvement(audit-log): use better-auth callback for password reset audit, remove cast

- Move password reset audit to onPasswordReset callback in auth config
  instead of coupling to better-auth's verification table internals
- Remove ugly double-cast on workflowData.workspaceId in deployment activation

* fix(audit-log): add missing actorName/actorEmail to workflow duplicate

* improvement(audit-log): add resourceName to credential set invitation accept
2026-02-18 11:53:08 -08:00
Waleed
da46a387c9 v0.5.92: shortlinks, copilot scrolling stickiness, pagination 2026-02-17 15:13:21 -08:00
Waleed
b7e377ec4b v0.5.91: docs i18n, turborepo upgrade 2026-02-16 00:36:05 -08:00
108 changed files with 9989 additions and 367 deletions

View File

@@ -5532,3 +5532,18 @@ export function OnePasswordIcon(props: SVGProps<SVGSVGElement>) {
</svg>
)
}
export function VercelIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
viewBox='0 0 256 222'
xmlns='http://www.w3.org/2000/svg'
preserveAspectRatio='xMidYMid'
>
<g transform='translate(19.2 16.63) scale(0.85)'>
<polygon fill='#fafafa' points='128 0 256 221.705007 0 221.705007' />
</g>
</svg>
)
}

View File

@@ -125,6 +125,7 @@ import {
TTSIcon,
TwilioIcon,
TypeformIcon,
VercelIcon,
VideoIcon,
WealthboxIcon,
WebflowIcon,
@@ -262,6 +263,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
twilio_sms: TwilioIcon,
twilio_voice: TwilioIcon,
typeform: TypeformIcon,
vercel: VercelIcon,
video_generator_v2: VideoIcon,
vision_v2: EyeIcon,
wealthbox: WealthboxIcon,

View File

@@ -122,6 +122,7 @@
"twilio_sms",
"twilio_voice",
"typeform",
"vercel",
"video_generator",
"vision",
"wealthbox",

File diff suppressed because it is too large Load Diff

View File

@@ -21,7 +21,7 @@
"fumadocs-mdx": "14.1.0",
"fumadocs-ui": "16.2.3",
"lucide-react": "^0.511.0",
"next": "16.1.0-canary.21",
"next": "16.1.6",
"next-themes": "^0.4.6",
"postgres": "^3.4.5",
"react": "19.2.1",

View File

@@ -1,6 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { getCreditBalance } from '@/lib/billing/credits/balance'
import { purchaseCredits } from '@/lib/billing/credits/purchase'
@@ -57,6 +58,17 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: result.error }, { status: 400 })
}
recordAudit({
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: AuditAction.CREDIT_PURCHASED,
resourceType: AuditResourceType.BILLING,
description: `Purchased $${validation.data.amount} in credits`,
metadata: { amount: validation.data.amount, requestId: validation.data.requestId },
request,
})
return NextResponse.json({ success: true })
} catch (error) {
logger.error('Failed to purchase credits', { error, userId: session.user.id })

View File

@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getEmailSubject, renderPollingGroupInvitationEmail } from '@/components/emails'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { hasCredentialSetsAccess } from '@/lib/billing'
import { getBaseUrl } from '@/lib/core/utils/urls'
@@ -148,6 +149,19 @@ export async function POST(
userId: session.user.id,
})
recordAudit({
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: AuditAction.CREDENTIAL_SET_INVITATION_RESENT,
resourceType: AuditResourceType.CREDENTIAL_SET,
resourceId: id,
resourceName: result.set.name,
description: `Resent credential set invitation to ${invitation.email}`,
metadata: { invitationId, email: invitation.email },
request: req,
})
return NextResponse.json({ success: true })
} catch (error) {
logger.error('Error resending invitation', error)

View File

@@ -8,6 +8,7 @@ import {
import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
@@ -78,6 +79,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
status: credentialSetInvitation.status,
expiresAt: credentialSetInvitation.expiresAt,
invitedBy: credentialSetInvitation.invitedBy,
credentialSetName: credentialSet.name,
providerId: credentialSet.providerId,
})
.from(credentialSetInvitation)
@@ -125,7 +127,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
const now = new Date()
const requestId = crypto.randomUUID().slice(0, 8)
// Use transaction to ensure membership + invitation update + webhook sync are atomic
await db.transaction(async (tx) => {
await tx.insert(credentialSetMember).values({
id: crypto.randomUUID(),
@@ -147,8 +148,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
})
.where(eq(credentialSetInvitation.id, invitation.id))
// Clean up all other pending invitations for the same credential set and email
// This prevents duplicate invites from showing up after accepting one
if (invitation.email) {
await tx
.update(credentialSetInvitation)
@@ -166,7 +165,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
)
}
// Sync webhooks within the transaction
const syncResult = await syncAllWebhooksForCredentialSet(
invitation.credentialSetId,
requestId,
@@ -184,6 +182,19 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
userId: session.user.id,
})
recordAudit({
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: AuditAction.CREDENTIAL_SET_INVITATION_ACCEPTED,
resourceType: AuditResourceType.CREDENTIAL_SET,
resourceId: invitation.credentialSetId,
resourceName: invitation.credentialSetName,
description: `Accepted credential set invitation`,
metadata: { invitationId: invitation.id },
request: req,
})
return NextResponse.json({
success: true,
credentialSetId: invitation.credentialSetId,

View File

@@ -3,6 +3,7 @@ import { credentialSet, credentialSetMember, organization } from '@sim/db/schema
import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
@@ -106,6 +107,17 @@ export async function DELETE(req: NextRequest) {
userId: session.user.id,
})
recordAudit({
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: AuditAction.CREDENTIAL_SET_MEMBER_LEFT,
resourceType: AuditResourceType.CREDENTIAL_SET,
resourceId: credentialSetId,
description: `Left credential set`,
request: req,
})
return NextResponse.json({ success: true })
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to leave credential set'

View File

@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption'
import { generateRequestId } from '@/lib/core/utils/request'
@@ -53,6 +54,17 @@ export async function POST(req: NextRequest) {
},
})
recordAudit({
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: AuditAction.ENVIRONMENT_UPDATED,
resourceType: AuditResourceType.ENVIRONMENT,
description: 'Updated global environment variables',
metadata: { variableCount: Object.keys(variables).length },
request: req,
})
return NextResponse.json({ success: true })
} catch (validationError) {
if (validationError instanceof z.ZodError) {

View File

@@ -1,7 +1,7 @@
import { db } from '@sim/db'
import { workflow, workflowFolder } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import { and, eq, isNull, min } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
@@ -37,7 +37,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
logger.info(`[${requestId}] Duplicating folder ${sourceFolderId} for user ${session.user.id}`)
// Verify the source folder exists
const sourceFolder = await db
.select()
.from(workflowFolder)
@@ -48,7 +47,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
throw new Error('Source folder not found')
}
// Check if user has permission to access the source folder
const userPermission = await getUserEntityPermissions(
session.user.id,
'workspace',
@@ -61,26 +59,51 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
const targetWorkspaceId = workspaceId || sourceFolder.workspaceId
// Step 1: Duplicate folder structure
const { newFolderId, folderMapping } = await db.transaction(async (tx) => {
const newFolderId = crypto.randomUUID()
const now = new Date()
const targetParentId = parentId ?? sourceFolder.parentId
const folderParentCondition = targetParentId
? eq(workflowFolder.parentId, targetParentId)
: isNull(workflowFolder.parentId)
const workflowParentCondition = targetParentId
? eq(workflow.folderId, targetParentId)
: isNull(workflow.folderId)
const [[folderResult], [workflowResult]] = await Promise.all([
tx
.select({ minSortOrder: min(workflowFolder.sortOrder) })
.from(workflowFolder)
.where(and(eq(workflowFolder.workspaceId, targetWorkspaceId), folderParentCondition)),
tx
.select({ minSortOrder: min(workflow.sortOrder) })
.from(workflow)
.where(and(eq(workflow.workspaceId, targetWorkspaceId), workflowParentCondition)),
])
const minSortOrder = [folderResult?.minSortOrder, workflowResult?.minSortOrder].reduce<
number | null
>((currentMin, candidate) => {
if (candidate == null) return currentMin
if (currentMin == null) return candidate
return Math.min(currentMin, candidate)
}, null)
const sortOrder = minSortOrder != null ? minSortOrder - 1 : 0
// Create the new root folder
await tx.insert(workflowFolder).values({
id: newFolderId,
userId: session.user.id,
workspaceId: targetWorkspaceId,
name,
color: color || sourceFolder.color,
parentId: parentId || sourceFolder.parentId,
sortOrder: sourceFolder.sortOrder,
parentId: targetParentId,
sortOrder,
isExpanded: false,
createdAt: now,
updatedAt: now,
})
// Recursively duplicate child folders
const folderMapping = new Map<string, string>([[sourceFolderId, newFolderId]])
await duplicateFolderStructure(
tx,
@@ -96,7 +119,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
return { newFolderId, folderMapping }
})
// Step 2: Duplicate workflows
const workflowStats = await duplicateWorkflowsInFolderTree(
sourceFolder.workspaceId,
targetWorkspaceId,
@@ -173,7 +195,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
}
}
// Helper to recursively duplicate folder structure
async function duplicateFolderStructure(
tx: any,
sourceFolderId: string,
@@ -184,7 +205,6 @@ async function duplicateFolderStructure(
timestamp: Date,
folderMapping: Map<string, string>
): Promise<void> {
// Get all child folders
const childFolders = await tx
.select()
.from(workflowFolder)
@@ -195,7 +215,6 @@ async function duplicateFolderStructure(
)
)
// Create each child folder and recurse
for (const childFolder of childFolders) {
const newChildFolderId = crypto.randomUUID()
folderMapping.set(childFolder.id, newChildFolderId)
@@ -213,7 +232,6 @@ async function duplicateFolderStructure(
updatedAt: timestamp,
})
// Recurse for this child's children
await duplicateFolderStructure(
tx,
childFolder.id,
@@ -227,7 +245,6 @@ async function duplicateFolderStructure(
}
}
// Helper to duplicate all workflows in a folder tree
async function duplicateWorkflowsInFolderTree(
sourceWorkspaceId: string,
targetWorkspaceId: string,
@@ -237,9 +254,7 @@ async function duplicateWorkflowsInFolderTree(
): Promise<{ total: number; succeeded: number; failed: number }> {
const stats = { total: 0, succeeded: 0, failed: 0 }
// Process each folder in the mapping
for (const [oldFolderId, newFolderId] of folderMapping.entries()) {
// Get workflows in this folder
const workflowsInFolder = await db
.select()
.from(workflow)
@@ -247,7 +262,6 @@ async function duplicateWorkflowsInFolderTree(
stats.total += workflowsInFolder.length
// Duplicate each workflow
for (const sourceWorkflow of workflowsInFolder) {
try {
await duplicateWorkflow({

View File

@@ -10,9 +10,14 @@ import {
mockConsoleLogger,
setupCommonApiMocks,
} from '@sim/testing'
import { drizzleOrmMock } from '@sim/testing/mocks'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('@/lib/audit/log', () => auditMock)
vi.mock('drizzle-orm', () => ({
...drizzleOrmMock,
min: vi.fn((field) => ({ type: 'min', field })),
}))
interface CapturedFolderValues {
name?: string
@@ -24,29 +29,35 @@ interface CapturedFolderValues {
}
function createMockTransaction(mockData: {
selectData?: Array<{ id: string; [key: string]: unknown }>
selectResults?: Array<Array<{ [key: string]: unknown }>>
insertResult?: Array<{ id: string; [key: string]: unknown }>
onInsertValues?: (values: CapturedFolderValues) => void
}) {
const { selectData = [], insertResult = [] } = mockData
return vi.fn().mockImplementation(async (callback: (tx: unknown) => Promise<unknown>) => {
const { selectResults = [[], []], insertResult = [], onInsertValues } = mockData
return async (callback: (tx: unknown) => Promise<unknown>) => {
const where = vi.fn()
for (const result of selectResults) {
where.mockReturnValueOnce(result)
}
where.mockReturnValue([])
const tx = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({
orderBy: vi.fn().mockReturnValue({
limit: vi.fn().mockReturnValue(selectData),
}),
}),
where,
}),
}),
insert: vi.fn().mockReturnValue({
values: vi.fn().mockReturnValue({
returning: vi.fn().mockReturnValue(insertResult),
values: vi.fn().mockImplementation((values: CapturedFolderValues) => {
onInsertValues?.(values)
return {
returning: vi.fn().mockReturnValue(insertResult),
}
}),
}),
}
return await callback(tx)
})
}
}
describe('Folders API Route', () => {
@@ -257,25 +268,12 @@ describe('Folders API Route', () => {
it('should create a new folder successfully', async () => {
mockAuthenticatedUser()
mockTransaction.mockImplementationOnce(async (callback: any) => {
const tx = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({
orderBy: vi.fn().mockReturnValue({
limit: vi.fn().mockReturnValue([]), // No existing folders
}),
}),
}),
}),
insert: vi.fn().mockReturnValue({
values: vi.fn().mockReturnValue({
returning: vi.fn().mockReturnValue([mockFolders[0]]),
}),
}),
}
return await callback(tx)
})
mockTransaction.mockImplementationOnce(
createMockTransaction({
selectResults: [[], []],
insertResult: [mockFolders[0]],
})
)
const req = createMockRequest('POST', {
name: 'New Test Folder',
@@ -285,12 +283,11 @@ describe('Folders API Route', () => {
const { POST } = await import('@/app/api/folders/route')
const response = await POST(req)
const responseBody = await response.json()
expect(response.status).toBe(200)
const data = await response.json()
expect(data).toHaveProperty('folder')
expect(data.folder).toMatchObject({
expect(responseBody).toHaveProperty('folder')
expect(responseBody.folder).toMatchObject({
id: 'folder-1',
name: 'Test Folder 1',
workspaceId: 'workspace-123',
@@ -299,26 +296,17 @@ describe('Folders API Route', () => {
it('should create folder with correct sort order', async () => {
mockAuthenticatedUser()
let capturedValues: CapturedFolderValues | null = null
mockTransaction.mockImplementationOnce(async (callback: any) => {
const tx = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({
orderBy: vi.fn().mockReturnValue({
limit: vi.fn().mockReturnValue([{ sortOrder: 5 }]), // Existing folder with sort order 5
}),
}),
}),
}),
insert: vi.fn().mockReturnValue({
values: vi.fn().mockReturnValue({
returning: vi.fn().mockReturnValue([{ ...mockFolders[0], sortOrder: 6 }]),
}),
}),
}
return await callback(tx)
})
mockTransaction.mockImplementationOnce(
createMockTransaction({
selectResults: [[{ minSortOrder: 5 }], [{ minSortOrder: 2 }]],
insertResult: [{ ...mockFolders[0], sortOrder: 1 }],
onInsertValues: (values) => {
capturedValues = values
},
})
)
const req = createMockRequest('POST', {
name: 'New Test Folder',
@@ -332,8 +320,10 @@ describe('Folders API Route', () => {
const data = await response.json()
expect(data.folder).toMatchObject({
sortOrder: 6,
sortOrder: 1,
})
expect(capturedValues).not.toBeNull()
expect(capturedValues!.sortOrder).toBe(1)
})
it('should create subfolder with parent reference', async () => {
@@ -341,7 +331,7 @@ describe('Folders API Route', () => {
mockTransaction.mockImplementationOnce(
createMockTransaction({
selectData: [], // No existing folders
selectResults: [[], []],
insertResult: [{ ...mockFolders[1] }],
})
)
@@ -402,25 +392,12 @@ describe('Folders API Route', () => {
mockAuthenticatedUser()
mockGetUserEntityPermissions.mockResolvedValue('write') // Write permissions
mockTransaction.mockImplementationOnce(async (callback: any) => {
const tx = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({
orderBy: vi.fn().mockReturnValue({
limit: vi.fn().mockReturnValue([]), // No existing folders
}),
}),
}),
}),
insert: vi.fn().mockReturnValue({
values: vi.fn().mockReturnValue({
returning: vi.fn().mockReturnValue([mockFolders[0]]),
}),
}),
}
return await callback(tx)
})
mockTransaction.mockImplementationOnce(
createMockTransaction({
selectResults: [[], []],
insertResult: [mockFolders[0]],
})
)
const req = createMockRequest('POST', {
name: 'Test Folder',
@@ -440,25 +417,12 @@ describe('Folders API Route', () => {
mockAuthenticatedUser()
mockGetUserEntityPermissions.mockResolvedValue('admin') // Admin permissions
mockTransaction.mockImplementationOnce(async (callback: any) => {
const tx = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({
orderBy: vi.fn().mockReturnValue({
limit: vi.fn().mockReturnValue([]), // No existing folders
}),
}),
}),
}),
insert: vi.fn().mockReturnValue({
values: vi.fn().mockReturnValue({
returning: vi.fn().mockReturnValue([mockFolders[0]]),
}),
}),
}
return await callback(tx)
})
mockTransaction.mockImplementationOnce(
createMockTransaction({
selectResults: [[], []],
insertResult: [mockFolders[0]],
})
)
const req = createMockRequest('POST', {
name: 'Test Folder',
@@ -527,28 +491,15 @@ describe('Folders API Route', () => {
let capturedValues: CapturedFolderValues | null = null
mockTransaction.mockImplementationOnce(async (callback: any) => {
const tx = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({
orderBy: vi.fn().mockReturnValue({
limit: vi.fn().mockReturnValue([]),
}),
}),
}),
}),
insert: vi.fn().mockReturnValue({
values: vi.fn().mockImplementation((values) => {
capturedValues = values
return {
returning: vi.fn().mockReturnValue([mockFolders[0]]),
}
}),
}),
}
return await callback(tx)
})
mockTransaction.mockImplementationOnce(
createMockTransaction({
selectResults: [[], []],
insertResult: [mockFolders[0]],
onInsertValues: (values) => {
capturedValues = values
},
})
)
const req = createMockRequest('POST', {
name: ' Test Folder With Spaces ',
@@ -567,28 +518,15 @@ describe('Folders API Route', () => {
let capturedValues: CapturedFolderValues | null = null
mockTransaction.mockImplementationOnce(async (callback: any) => {
const tx = {
select: vi.fn().mockReturnValue({
from: vi.fn().mockReturnValue({
where: vi.fn().mockReturnValue({
orderBy: vi.fn().mockReturnValue({
limit: vi.fn().mockReturnValue([]),
}),
}),
}),
}),
insert: vi.fn().mockReturnValue({
values: vi.fn().mockImplementation((values) => {
capturedValues = values
return {
returning: vi.fn().mockReturnValue([mockFolders[0]]),
}
}),
}),
}
return await callback(tx)
})
mockTransaction.mockImplementationOnce(
createMockTransaction({
selectResults: [[], []],
insertResult: [mockFolders[0]],
onInsertValues: (values) => {
capturedValues = values
},
})
)
const req = createMockRequest('POST', {
name: 'Test Folder',

View File

@@ -1,7 +1,7 @@
import { db } from '@sim/db'
import { workflowFolder } from '@sim/db/schema'
import { workflow, workflowFolder } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, asc, desc, eq, isNull } from 'drizzle-orm'
import { and, asc, eq, isNull, min } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
@@ -87,19 +87,33 @@ export async function POST(request: NextRequest) {
if (providedSortOrder !== undefined) {
sortOrder = providedSortOrder
} else {
const existingFolders = await tx
.select({ sortOrder: workflowFolder.sortOrder })
.from(workflowFolder)
.where(
and(
eq(workflowFolder.workspaceId, workspaceId),
parentId ? eq(workflowFolder.parentId, parentId) : isNull(workflowFolder.parentId)
)
)
.orderBy(desc(workflowFolder.sortOrder))
.limit(1)
const folderParentCondition = parentId
? eq(workflowFolder.parentId, parentId)
: isNull(workflowFolder.parentId)
const workflowParentCondition = parentId
? eq(workflow.folderId, parentId)
: isNull(workflow.folderId)
sortOrder = existingFolders.length > 0 ? existingFolders[0].sortOrder + 1 : 0
const [[folderResult], [workflowResult]] = await Promise.all([
tx
.select({ minSortOrder: min(workflowFolder.sortOrder) })
.from(workflowFolder)
.where(and(eq(workflowFolder.workspaceId, workspaceId), folderParentCondition)),
tx
.select({ minSortOrder: min(workflow.sortOrder) })
.from(workflow)
.where(and(eq(workflow.workspaceId, workspaceId), workflowParentCondition)),
])
const minSortOrder = [folderResult?.minSortOrder, workflowResult?.minSortOrder].reduce<
number | null
>((currentMin, candidate) => {
if (candidate == null) return currentMin
if (currentMin == null) return candidate
return Math.min(currentMin, candidate)
}, null)
sortOrder = minSortOrder != null ? minSortOrder - 1 : 0
}
const [folder] = await tx

View File

@@ -201,6 +201,8 @@ export async function PUT(
recordAudit({
workspaceId: accessCheck.knowledgeBase?.workspaceId ?? null,
actorId: userId,
actorName: auth.userName,
actorEmail: auth.userEmail,
action: AuditAction.DOCUMENT_UPDATED,
resourceType: AuditResourceType.DOCUMENT,
resourceId: documentId,
@@ -272,6 +274,8 @@ export async function DELETE(
recordAudit({
workspaceId: accessCheck.knowledgeBase?.workspaceId ?? null,
actorId: userId,
actorName: auth.userName,
actorEmail: auth.userEmail,
action: AuditAction.DOCUMENT_DELETED,
resourceType: AuditResourceType.DOCUMENT,
resourceId: documentId,

View File

@@ -248,6 +248,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
recordAudit({
workspaceId: accessCheck.knowledgeBase?.workspaceId ?? null,
actorId: userId,
actorName: auth.userName,
actorEmail: auth.userEmail,
action: AuditAction.DOCUMENT_UPLOADED,
resourceType: AuditResourceType.DOCUMENT,
resourceId: knowledgeBaseId,
@@ -307,6 +309,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
recordAudit({
workspaceId: accessCheck.knowledgeBase?.workspaceId ?? null,
actorId: userId,
actorName: auth.userName,
actorEmail: auth.userEmail,
action: AuditAction.DOCUMENT_UPLOADED,
resourceType: AuditResourceType.DOCUMENT,
resourceId: knowledgeBaseId,

View File

@@ -139,6 +139,8 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
recordAudit({
workspaceId: accessCheck.knowledgeBase.workspaceId ?? null,
actorId: userId,
actorName: auth.userName,
actorEmail: auth.userEmail,
action: AuditAction.KNOWLEDGE_BASE_UPDATED,
resourceType: AuditResourceType.KNOWLEDGE_BASE,
resourceId: id,
@@ -212,6 +214,8 @@ export async function DELETE(
recordAudit({
workspaceId: accessCheck.knowledgeBase.workspaceId ?? null,
actorId: userId,
actorName: auth.userName,
actorEmail: auth.userEmail,
action: AuditAction.KNOWLEDGE_BASE_DELETED,
resourceType: AuditResourceType.KNOWLEDGE_BASE,
resourceId: id,

View File

@@ -17,7 +17,11 @@ export const dynamic = 'force-dynamic'
* PATCH - Update an MCP server in the workspace (requires write or admin permission)
*/
export const PATCH = withMcpAuth<{ id: string }>('write')(
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
async (
request: NextRequest,
{ userId, userName, userEmail, workspaceId, requestId },
{ params }
) => {
const { id: serverId } = await params
try {
@@ -90,6 +94,8 @@ export const PATCH = withMcpAuth<{ id: string }>('write')(
recordAudit({
workspaceId,
actorId: userId,
actorName: userName,
actorEmail: userEmail,
action: AuditAction.MCP_SERVER_UPDATED,
resourceType: AuditResourceType.MCP_SERVER,
resourceId: serverId,

View File

@@ -56,7 +56,7 @@ export const GET = withMcpAuth('read')(
* it will be updated instead of creating a duplicate.
*/
export const POST = withMcpAuth('write')(
async (request: NextRequest, { userId, workspaceId, requestId }) => {
async (request: NextRequest, { userId, userName, userEmail, workspaceId, requestId }) => {
try {
const body = getParsedBody(request) || (await request.json())
@@ -165,6 +165,8 @@ export const POST = withMcpAuth('write')(
recordAudit({
workspaceId,
actorId: userId,
actorName: userName,
actorEmail: userEmail,
action: AuditAction.MCP_SERVER_ADDED,
resourceType: AuditResourceType.MCP_SERVER,
resourceId: serverId,
@@ -190,7 +192,7 @@ export const POST = withMcpAuth('write')(
* DELETE - Delete an MCP server from the workspace (requires admin permission)
*/
export const DELETE = withMcpAuth('admin')(
async (request: NextRequest, { userId, workspaceId, requestId }) => {
async (request: NextRequest, { userId, userName, userEmail, workspaceId, requestId }) => {
try {
const { searchParams } = new URL(request.url)
const serverId = searchParams.get('serverId')
@@ -225,6 +227,8 @@ export const DELETE = withMcpAuth('admin')(
recordAudit({
workspaceId,
actorId: userId,
actorName: userName,
actorEmail: userEmail,
action: AuditAction.MCP_SERVER_REMOVED,
resourceType: AuditResourceType.MCP_SERVER,
resourceId: serverId!,

View File

@@ -72,7 +72,11 @@ export const GET = withMcpAuth<RouteParams>('read')(
* PATCH - Update a workflow MCP server
*/
export const PATCH = withMcpAuth<RouteParams>('write')(
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
async (
request: NextRequest,
{ userId, userName, userEmail, workspaceId, requestId },
{ params }
) => {
try {
const { id: serverId } = await params
const body = getParsedBody(request) || (await request.json())
@@ -116,6 +120,8 @@ export const PATCH = withMcpAuth<RouteParams>('write')(
recordAudit({
workspaceId,
actorId: userId,
actorName: userName,
actorEmail: userEmail,
action: AuditAction.MCP_SERVER_UPDATED,
resourceType: AuditResourceType.MCP_SERVER,
resourceId: serverId,
@@ -140,7 +146,11 @@ export const PATCH = withMcpAuth<RouteParams>('write')(
* DELETE - Delete a workflow MCP server and all its tools
*/
export const DELETE = withMcpAuth<RouteParams>('admin')(
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
async (
request: NextRequest,
{ userId, userName, userEmail, workspaceId, requestId },
{ params }
) => {
try {
const { id: serverId } = await params
@@ -164,6 +174,8 @@ export const DELETE = withMcpAuth<RouteParams>('admin')(
recordAudit({
workspaceId,
actorId: userId,
actorName: userName,
actorEmail: userEmail,
action: AuditAction.MCP_SERVER_REMOVED,
resourceType: AuditResourceType.MCP_SERVER,
resourceId: serverId,

View File

@@ -66,7 +66,11 @@ export const GET = withMcpAuth<RouteParams>('read')(
* PATCH - Update a tool's configuration
*/
export const PATCH = withMcpAuth<RouteParams>('write')(
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
async (
request: NextRequest,
{ userId, userName, userEmail, workspaceId, requestId },
{ params }
) => {
try {
const { id: serverId, toolId } = await params
const body = getParsedBody(request) || (await request.json())
@@ -122,6 +126,8 @@ export const PATCH = withMcpAuth<RouteParams>('write')(
recordAudit({
workspaceId,
actorId: userId,
actorName: userName,
actorEmail: userEmail,
action: AuditAction.MCP_SERVER_UPDATED,
resourceType: AuditResourceType.MCP_SERVER,
resourceId: serverId,
@@ -146,7 +152,11 @@ export const PATCH = withMcpAuth<RouteParams>('write')(
* DELETE - Remove a tool from an MCP server
*/
export const DELETE = withMcpAuth<RouteParams>('write')(
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
async (
request: NextRequest,
{ userId, userName, userEmail, workspaceId, requestId },
{ params }
) => {
try {
const { id: serverId, toolId } = await params
@@ -180,6 +190,8 @@ export const DELETE = withMcpAuth<RouteParams>('write')(
recordAudit({
workspaceId,
actorId: userId,
actorName: userName,
actorEmail: userEmail,
action: AuditAction.MCP_SERVER_UPDATED,
resourceType: AuditResourceType.MCP_SERVER,
resourceId: serverId,

View File

@@ -77,7 +77,11 @@ export const GET = withMcpAuth<RouteParams>('read')(
* POST - Add a workflow as a tool to an MCP server
*/
export const POST = withMcpAuth<RouteParams>('write')(
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
async (
request: NextRequest,
{ userId, userName, userEmail, workspaceId, requestId },
{ params }
) => {
try {
const { id: serverId } = await params
const body = getParsedBody(request) || (await request.json())
@@ -201,6 +205,8 @@ export const POST = withMcpAuth<RouteParams>('write')(
recordAudit({
workspaceId,
actorId: userId,
actorName: userName,
actorEmail: userEmail,
action: AuditAction.MCP_SERVER_UPDATED,
resourceType: AuditResourceType.MCP_SERVER,
resourceId: serverId,

View File

@@ -86,7 +86,7 @@ export const GET = withMcpAuth('read')(
* POST - Create a new workflow MCP server
*/
export const POST = withMcpAuth('write')(
async (request: NextRequest, { userId, workspaceId, requestId }) => {
async (request: NextRequest, { userId, userName, userEmail, workspaceId, requestId }) => {
try {
const body = getParsedBody(request) || (await request.json())
@@ -192,6 +192,8 @@ export const POST = withMcpAuth('write')(
recordAudit({
workspaceId,
actorId: userId,
actorName: userName,
actorEmail: userEmail,
action: AuditAction.MCP_SERVER_ADDED,
resourceType: AuditResourceType.MCP_SERVER,
resourceId: serverId,

View File

@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
import { eq, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import {
@@ -247,6 +248,18 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
logger.info(`[${requestId}] Successfully updated template: ${id}`)
recordAudit({
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: AuditAction.TEMPLATE_UPDATED,
resourceType: AuditResourceType.TEMPLATE,
resourceId: id,
resourceName: name ?? template.name,
description: `Updated template "${name ?? template.name}"`,
request,
})
return NextResponse.json({
data: updatedTemplate[0],
message: 'Template updated successfully',
@@ -300,6 +313,19 @@ export async function DELETE(
await db.delete(templates).where(eq(templates.id, id))
logger.info(`[${requestId}] Deleted template: ${id}`)
recordAudit({
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: AuditAction.TEMPLATE_DELETED,
resourceType: AuditResourceType.TEMPLATE,
resourceId: id,
resourceName: template.name,
description: `Deleted template "${template.name}"`,
request,
})
return NextResponse.json({ success: true })
} catch (error: any) {
logger.error(`[${requestId}] Error deleting template: ${id}`, error)

View File

@@ -11,6 +11,7 @@ import { and, desc, eq, ilike, or, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import { verifyEffectiveSuperUser } from '@/lib/templates/permissions'
@@ -285,6 +286,18 @@ export async function POST(request: NextRequest) {
logger.info(`[${requestId}] Successfully created template: ${templateId}`)
recordAudit({
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: AuditAction.TEMPLATE_CREATED,
resourceType: AuditResourceType.TEMPLATE,
resourceId: templateId,
resourceName: data.name,
description: `Created template "${data.name}"`,
request,
})
return NextResponse.json(
{
id: templateId,

View File

@@ -265,6 +265,8 @@ export async function DELETE(
recordAudit({
workspaceId: webhookData.workflow.workspaceId || null,
actorId: userId,
actorName: auth.userName,
actorEmail: auth.userEmail,
action: AuditAction.WEBHOOK_DELETED,
resourceType: AuditResourceType.WEBHOOK,
resourceId: id,

View File

@@ -146,7 +146,8 @@ export async function GET(request: NextRequest) {
// Create or Update a webhook
export async function POST(request: NextRequest) {
const requestId = generateRequestId()
const userId = (await getSession())?.user?.id
const session = await getSession()
const userId = session?.user?.id
if (!userId) {
logger.warn(`[${requestId}] Unauthorized webhook creation attempt`)
@@ -683,6 +684,8 @@ export async function POST(request: NextRequest) {
recordAudit({
workspaceId: workflowRecord.workspaceId || null,
actorId: userId,
actorName: session?.user?.name ?? undefined,
actorEmail: session?.user?.email ?? undefined,
action: AuditAction.WEBHOOK_CREATED,
resourceType: AuditResourceType.WEBHOOK,
resourceId: savedWebhook.id,

View File

@@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { generateRequestId } from '@/lib/core/utils/request'
import { syncMcpToolsForWorkflow } from '@/lib/mcp/workflow-mcp-sync'
import { restorePreviousVersionWebhooks, saveTriggerWebhooksForDeploy } from '@/lib/webhooks/deploy'
@@ -297,6 +298,19 @@ export async function PATCH(
}
}
recordAudit({
workspaceId: workflowData?.workspaceId,
actorId: actorUserId,
actorName: session?.user?.name,
actorEmail: session?.user?.email,
action: AuditAction.WORKFLOW_DEPLOYMENT_ACTIVATED,
resourceType: AuditResourceType.WORKFLOW,
resourceId: id,
description: `Activated deployment version ${versionNum}`,
metadata: { version: versionNum },
request,
})
return createSuccessResponse({
success: true,
deployedAt: result.deployedAt,

View File

@@ -65,6 +65,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
recordAudit({
workspaceId: workspaceId || null,
actorId: userId,
actorName: auth.userName,
actorEmail: auth.userEmail,
action: AuditAction.WORKFLOW_DUPLICATED,
resourceType: AuditResourceType.WORKFLOW,
resourceId: result.id,

View File

@@ -340,6 +340,8 @@ export async function DELETE(
recordAudit({
workspaceId: workflowData.workspaceId || null,
actorId: userId,
actorName: auth.userName,
actorEmail: auth.userEmail,
action: AuditAction.WORKFLOW_DELETED,
resourceType: AuditResourceType.WORKFLOW,
resourceId: workflowId,

View File

@@ -83,6 +83,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
recordAudit({
workspaceId: workflowData.workspaceId ?? null,
actorId: userId,
actorName: auth.userName,
actorEmail: auth.userEmail,
action: AuditAction.WORKFLOW_VARIABLES_UPDATED,
resourceType: AuditResourceType.WORKFLOW,
resourceId: workflowId,

View File

@@ -0,0 +1,137 @@
/**
* @vitest-environment node
*/
import { auditMock, createMockRequest, mockConsoleLogger, setupCommonApiMocks } from '@sim/testing'
import { drizzleOrmMock } from '@sim/testing/mocks'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mockCheckSessionOrInternalAuth = vi.fn()
const mockGetUserEntityPermissions = vi.fn()
const mockDbSelect = vi.fn()
const mockDbInsert = vi.fn()
const mockWorkflowCreated = vi.fn()
vi.mock('drizzle-orm', () => ({
...drizzleOrmMock,
min: vi.fn((field) => ({ type: 'min', field })),
}))
vi.mock('@/lib/audit/log', () => auditMock)
describe('Workflows API Route - POST ordering', () => {
beforeEach(() => {
vi.resetModules()
vi.clearAllMocks()
setupCommonApiMocks()
mockConsoleLogger()
vi.stubGlobal('crypto', {
randomUUID: vi.fn().mockReturnValue('workflow-new-id'),
})
mockCheckSessionOrInternalAuth.mockResolvedValue({
success: true,
userId: 'user-123',
userName: 'Test User',
userEmail: 'test@example.com',
})
mockGetUserEntityPermissions.mockResolvedValue('write')
vi.doMock('@sim/db', () => ({
db: {
select: (...args: unknown[]) => mockDbSelect(...args),
insert: (...args: unknown[]) => mockDbInsert(...args),
},
}))
vi.doMock('@/lib/auth/hybrid', () => ({
checkSessionOrInternalAuth: (...args: unknown[]) => mockCheckSessionOrInternalAuth(...args),
}))
vi.doMock('@/lib/workspaces/permissions/utils', () => ({
getUserEntityPermissions: (...args: unknown[]) => mockGetUserEntityPermissions(...args),
workspaceExists: vi.fn(),
}))
vi.doMock('@/app/api/workflows/utils', () => ({
verifyWorkspaceMembership: vi.fn(),
}))
vi.doMock('@/lib/core/telemetry', () => ({
PlatformEvents: {
workflowCreated: (...args: unknown[]) => mockWorkflowCreated(...args),
},
}))
})
it('uses top insertion against mixed siblings (folders + workflows)', async () => {
const minResultsQueue: Array<Array<{ minOrder: number }>> = [
[{ minOrder: 5 }],
[{ minOrder: 2 }],
]
mockDbSelect.mockImplementation(() => ({
from: vi.fn().mockReturnValue({
where: vi.fn().mockImplementation(() => Promise.resolve(minResultsQueue.shift() ?? [])),
}),
}))
let insertedValues: Record<string, unknown> | null = null
mockDbInsert.mockReturnValue({
values: vi.fn().mockImplementation((values: Record<string, unknown>) => {
insertedValues = values
return Promise.resolve(undefined)
}),
})
const req = createMockRequest('POST', {
name: 'New Workflow',
description: 'desc',
color: '#3972F6',
workspaceId: 'workspace-123',
folderId: null,
})
const { POST } = await import('@/app/api/workflows/route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.sortOrder).toBe(1)
expect(insertedValues).not.toBeNull()
expect(insertedValues?.sortOrder).toBe(1)
})
it('defaults to sortOrder 0 when there are no siblings', async () => {
const minResultsQueue: Array<Array<{ minOrder: number }>> = [[], []]
mockDbSelect.mockImplementation(() => ({
from: vi.fn().mockReturnValue({
where: vi.fn().mockImplementation(() => Promise.resolve(minResultsQueue.shift() ?? [])),
}),
}))
let insertedValues: Record<string, unknown> | null = null
mockDbInsert.mockReturnValue({
values: vi.fn().mockImplementation((values: Record<string, unknown>) => {
insertedValues = values
return Promise.resolve(undefined)
}),
})
const req = createMockRequest('POST', {
name: 'New Workflow',
description: 'desc',
color: '#3972F6',
workspaceId: 'workspace-123',
folderId: null,
})
const { POST } = await import('@/app/api/workflows/route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.sortOrder).toBe(0)
expect(insertedValues?.sortOrder).toBe(0)
})
})

View File

@@ -1,5 +1,5 @@
import { db } from '@sim/db'
import { permissions, workflow } from '@sim/db/schema'
import { permissions, workflow, workflowFolder } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, asc, eq, inArray, isNull, min } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
@@ -162,12 +162,33 @@ export async function POST(req: NextRequest) {
if (providedSortOrder !== undefined) {
sortOrder = providedSortOrder
} else {
const folderCondition = folderId ? eq(workflow.folderId, folderId) : isNull(workflow.folderId)
const [minResult] = await db
.select({ minOrder: min(workflow.sortOrder) })
.from(workflow)
.where(and(eq(workflow.workspaceId, workspaceId), folderCondition))
sortOrder = (minResult?.minOrder ?? 1) - 1
const workflowParentCondition = folderId
? eq(workflow.folderId, folderId)
: isNull(workflow.folderId)
const folderParentCondition = folderId
? eq(workflowFolder.parentId, folderId)
: isNull(workflowFolder.parentId)
const [[workflowMinResult], [folderMinResult]] = await Promise.all([
db
.select({ minOrder: min(workflow.sortOrder) })
.from(workflow)
.where(and(eq(workflow.workspaceId, workspaceId), workflowParentCondition)),
db
.select({ minOrder: min(workflowFolder.sortOrder) })
.from(workflowFolder)
.where(and(eq(workflowFolder.workspaceId, workspaceId), folderParentCondition)),
])
const minSortOrder = [workflowMinResult?.minOrder, folderMinResult?.minOrder].reduce<
number | null
>((currentMin, candidate) => {
if (candidate == null) return currentMin
if (currentMin == null) return candidate
return Math.min(currentMin, candidate)
}, null)
sortOrder = minSortOrder != null ? minSortOrder - 1 : 0
}
await db.insert(workflow).values({
@@ -192,6 +213,8 @@ export async function POST(req: NextRequest) {
recordAudit({
workspaceId,
actorId: userId,
actorName: auth.userName,
actorEmail: auth.userEmail,
action: AuditAction.WORKFLOW_CREATED,
resourceType: AuditResourceType.WORKFLOW,
resourceId: workflowId,

View File

@@ -227,7 +227,7 @@ export function Integrations({ onOpenChange, registerCloseHandler }: Integration
(acc, service) => {
if (
permissionConfig.allowedIntegrations !== null &&
!permissionConfig.allowedIntegrations.includes(service.id.replace(/-/g, '_'))
!permissionConfig.allowedIntegrations.includes(service.id.replace(/-/g, '_').toLowerCase())
) {
return acc
}

View File

@@ -7,6 +7,8 @@ export interface SubscriptionPermissions {
canCancelSubscription: boolean
showTeamMemberView: boolean
showUpgradePlans: boolean
isEnterpriseMember: boolean
canViewUsageInfo: boolean
}
export interface SubscriptionState {
@@ -31,6 +33,9 @@ export function getSubscriptionPermissions(
const { isFree, isPro, isTeam, isEnterprise, isPaid } = subscription
const { isTeamAdmin } = userRole
const isEnterpriseMember = isEnterprise && !isTeamAdmin
const canViewUsageInfo = !isEnterpriseMember
return {
canUpgradeToPro: isFree,
canUpgradeToTeam: isFree || (isPro && !isTeam),
@@ -40,6 +45,8 @@ export function getSubscriptionPermissions(
canCancelSubscription: isPaid && !isEnterprise && !(isTeam && !isTeamAdmin), // Team members can't cancel
showTeamMemberView: isTeam && !isTeamAdmin,
showUpgradePlans: isFree || (isPro && !isTeam) || (isTeam && isTeamAdmin), // Free users, Pro users, Team owners see plans
isEnterpriseMember,
canViewUsageInfo,
}
}

View File

@@ -300,12 +300,16 @@ export function Subscription() {
)
const showBadge =
(permissions.canEditUsageLimit && !permissions.showTeamMemberView) ||
permissions.showTeamMemberView ||
subscription.isEnterprise ||
isBlocked
!permissions.isEnterpriseMember &&
((permissions.canEditUsageLimit && !permissions.showTeamMemberView) ||
permissions.showTeamMemberView ||
subscription.isEnterprise ||
isBlocked)
const getBadgeConfig = (): { text: string; variant: 'blue-secondary' | 'red' } => {
if (permissions.isEnterpriseMember) {
return { text: '', variant: 'blue-secondary' }
}
if (permissions.showTeamMemberView || subscription.isEnterprise) {
return { text: `${subscription.seats} seats`, variant: 'blue-secondary' }
}
@@ -443,67 +447,75 @@ export function Subscription() {
return (
<div className='flex h-full flex-col gap-[20px]'>
{/* Current Plan & Usage Overview */}
<UsageHeader
title={formatPlanName(subscription.plan)}
showBadge={showBadge}
badgeText={badgeConfig.text}
badgeVariant={badgeConfig.variant}
onBadgeClick={permissions.showTeamMemberView ? undefined : handleBadgeClick}
seatsText={
permissions.canManageTeam || subscription.isEnterprise
? `${subscription.seats} seats`
: undefined
}
current={usage.current}
limit={
subscription.isEnterprise || subscription.isTeam
? organizationBillingData?.data?.totalUsageLimit
: !subscription.isFree &&
(permissions.canEditUsageLimit || permissions.showTeamMemberView)
? usage.current // placeholder; rightContent will render UsageLimit
: usage.limit
}
isBlocked={isBlocked}
progressValue={Math.min(usage.percentUsed, 100)}
rightContent={
!subscription.isFree &&
(permissions.canEditUsageLimit || permissions.showTeamMemberView) ? (
<UsageLimit
ref={usageLimitRef}
currentLimit={
(subscription.isTeam || subscription.isEnterprise) &&
isTeamAdmin &&
organizationBillingData?.data
? organizationBillingData.data.totalUsageLimit
: usageLimitData.currentLimit || usage.limit
}
currentUsage={usage.current}
canEdit={permissions.canEditUsageLimit}
minimumLimit={
(subscription.isTeam || subscription.isEnterprise) &&
isTeamAdmin &&
organizationBillingData?.data
? organizationBillingData.data.minimumBillingAmount
: usageLimitData.minimumLimit || (subscription.isPro ? 20 : 40)
}
context={
(subscription.isTeam || subscription.isEnterprise) && isTeamAdmin
? 'organization'
: 'user'
}
organizationId={
(subscription.isTeam || subscription.isEnterprise) && isTeamAdmin
? activeOrgId
: undefined
}
onLimitUpdated={() => {
logger.info('Usage limit updated')
}}
/>
) : undefined
}
/>
{/* Current Plan & Usage Overview - hidden from enterprise members (non-admin) */}
{permissions.canViewUsageInfo ? (
<UsageHeader
title={formatPlanName(subscription.plan)}
showBadge={showBadge}
badgeText={badgeConfig.text}
badgeVariant={badgeConfig.variant}
onBadgeClick={permissions.showTeamMemberView ? undefined : handleBadgeClick}
seatsText={
permissions.canManageTeam || subscription.isEnterprise
? `${subscription.seats} seats`
: undefined
}
current={usage.current}
limit={
subscription.isEnterprise || subscription.isTeam
? organizationBillingData?.data?.totalUsageLimit
: !subscription.isFree &&
(permissions.canEditUsageLimit || permissions.showTeamMemberView)
? usage.current // placeholder; rightContent will render UsageLimit
: usage.limit
}
isBlocked={isBlocked}
progressValue={Math.min(usage.percentUsed, 100)}
rightContent={
!subscription.isFree &&
(permissions.canEditUsageLimit || permissions.showTeamMemberView) ? (
<UsageLimit
ref={usageLimitRef}
currentLimit={
(subscription.isTeam || subscription.isEnterprise) &&
isTeamAdmin &&
organizationBillingData?.data
? organizationBillingData.data.totalUsageLimit
: usageLimitData.currentLimit || usage.limit
}
currentUsage={usage.current}
canEdit={permissions.canEditUsageLimit}
minimumLimit={
(subscription.isTeam || subscription.isEnterprise) &&
isTeamAdmin &&
organizationBillingData?.data
? organizationBillingData.data.minimumBillingAmount
: usageLimitData.minimumLimit || (subscription.isPro ? 20 : 40)
}
context={
(subscription.isTeam || subscription.isEnterprise) && isTeamAdmin
? 'organization'
: 'user'
}
organizationId={
(subscription.isTeam || subscription.isEnterprise) && isTeamAdmin
? activeOrgId
: undefined
}
onLimitUpdated={() => {
logger.info('Usage limit updated')
}}
/>
) : undefined
}
/>
) : (
<div className='flex items-center'>
<span className='font-medium text-[14px] text-[var(--text-primary)]'>
{formatPlanName(subscription.plan)}
</span>
</div>
)}
{/* Upgrade Plans */}
{permissions.showUpgradePlans && (
@@ -539,8 +551,8 @@ export function Subscription() {
</div>
)}
{/* Credit Balance */}
{subscription.isPaid && (
{/* Credit Balance - hidden from enterprise members (non-admin) */}
{subscription.isPaid && permissions.canViewUsageInfo && (
<CreditBalance
balance={subscriptionData?.data?.creditBalance ?? 0}
canPurchase={permissions.canEditUsageLimit}
@@ -554,10 +566,11 @@ export function Subscription() {
<ReferralCode onRedeemComplete={() => refetchSubscription()} />
)}
{/* Next Billing Date - hidden from team members */}
{/* Next Billing Date - hidden from team members and enterprise members (non-admin) */}
{subscription.isPaid &&
subscriptionData?.data?.periodEnd &&
!permissions.showTeamMemberView && (
!permissions.showTeamMemberView &&
!permissions.isEnterpriseMember && (
<div className='flex items-center justify-between'>
<Label>Next Billing Date</Label>
<span className='text-[12px] text-[var(--text-secondary)]'>
@@ -566,8 +579,8 @@ export function Subscription() {
</div>
)}
{/* Usage notifications */}
{subscription.isPaid && <BillingUsageNotificationsToggle />}
{/* Usage notifications - hidden from enterprise members (non-admin) */}
{subscription.isPaid && permissions.canViewUsageInfo && <BillingUsageNotificationsToggle />}
{/* Cancel Subscription */}
{permissions.canCancelSubscription && (

View File

@@ -285,6 +285,7 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) {
const isPro = planType === 'pro'
const isTeam = planType === 'team'
const isEnterprise = planType === 'enterprise'
const isEnterpriseMember = isEnterprise && !userCanManageBilling
const handleUpgradeToPro = useCallback(async () => {
try {
@@ -463,6 +464,18 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) {
}
}
if (isEnterpriseMember) {
return (
<div className='flex flex-shrink-0 flex-col border-t px-[13.5px] pt-[8px] pb-[10px]'>
<div className='flex h-[18px] items-center'>
<span className='font-medium text-[12px] text-[var(--text-primary)]'>
{PLAN_NAMES[planType]}
</span>
</div>
</div>
)
}
return (
<>
<div

File diff suppressed because it is too large Load Diff

View File

@@ -146,6 +146,7 @@ import { TwilioSMSBlock } from '@/blocks/blocks/twilio'
import { TwilioVoiceBlock } from '@/blocks/blocks/twilio_voice'
import { TypeformBlock } from '@/blocks/blocks/typeform'
import { VariablesBlock } from '@/blocks/blocks/variables'
import { VercelBlock } from '@/blocks/blocks/vercel'
import { VideoGeneratorBlock, VideoGeneratorV2Block } from '@/blocks/blocks/video_generator'
import { VisionBlock, VisionV2Block } from '@/blocks/blocks/vision'
import { WaitBlock } from '@/blocks/blocks/wait'
@@ -330,6 +331,7 @@ export const registry: Record<string, BlockConfig> = {
twilio_sms: TwilioSMSBlock,
twilio_voice: TwilioVoiceBlock,
typeform: TypeformBlock,
vercel: VercelBlock,
variables: VariablesBlock,
video_generator: VideoGeneratorBlock,
video_generator_v2: VideoGeneratorV2Block,

View File

@@ -5532,3 +5532,18 @@ export function OnePasswordIcon(props: SVGProps<SVGSVGElement>) {
</svg>
)
}
export function VercelIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
viewBox='0 0 256 222'
xmlns='http://www.w3.org/2000/svg'
preserveAspectRatio='xMidYMid'
>
<g transform='translate(19.2 16.63) scale(0.85)'>
<polygon fill='#fafafa' points='128 0 256 221.705007 0 221.705007' />
</g>
</svg>
)
}

View File

@@ -0,0 +1,177 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { mockLogger, queryClient, useFolderStoreMock, useWorkflowRegistryMock } = vi.hoisted(() => ({
mockLogger: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
queryClient: {
cancelQueries: vi.fn().mockResolvedValue(undefined),
invalidateQueries: vi.fn().mockResolvedValue(undefined),
},
useFolderStoreMock: Object.assign(vi.fn(), {
getState: vi.fn(),
setState: vi.fn(),
}),
useWorkflowRegistryMock: Object.assign(vi.fn(), {
getState: vi.fn(),
setState: vi.fn(),
}),
}))
let folderState: {
folders: Record<string, any>
}
let workflowRegistryState: {
workflows: Record<string, any>
}
vi.mock('@sim/logger', () => ({
createLogger: vi.fn(() => mockLogger),
}))
vi.mock('@tanstack/react-query', () => ({
keepPreviousData: {},
useQuery: vi.fn(),
useQueryClient: vi.fn(() => queryClient),
useMutation: vi.fn((options) => options),
}))
vi.mock('@/stores/folders/store', () => ({
useFolderStore: useFolderStoreMock,
}))
vi.mock('@/stores/workflows/registry/store', () => ({
useWorkflowRegistry: useWorkflowRegistryMock,
}))
vi.mock('@/hooks/queries/workflows', () => ({
workflowKeys: {
list: (workspaceId: string | undefined) => ['workflows', 'list', workspaceId ?? ''],
},
}))
import { useCreateFolder, useDuplicateFolderMutation } from '@/hooks/queries/folders'
function getOptimisticFolderByName(name: string) {
return Object.values(folderState.folders).find((folder: any) => folder.name === name) as
| { sortOrder: number }
| undefined
}
describe('folder optimistic top insertion ordering', () => {
beforeEach(() => {
vi.clearAllMocks()
useFolderStoreMock.getState.mockImplementation(() => folderState)
useFolderStoreMock.setState.mockImplementation((updater: any) => {
if (typeof updater === 'function') {
const next = updater(folderState)
if (next) {
folderState = { ...folderState, ...next }
}
return
}
folderState = { ...folderState, ...updater }
})
useWorkflowRegistryMock.getState.mockImplementation(() => workflowRegistryState)
folderState = {
folders: {
'folder-parent-match': {
id: 'folder-parent-match',
name: 'Existing sibling folder',
userId: 'user-1',
workspaceId: 'ws-1',
parentId: 'parent-1',
color: '#808080',
isExpanded: false,
sortOrder: 5,
createdAt: new Date(),
updatedAt: new Date(),
},
'folder-other-parent': {
id: 'folder-other-parent',
name: 'Other parent folder',
userId: 'user-1',
workspaceId: 'ws-1',
parentId: 'parent-2',
color: '#808080',
isExpanded: false,
sortOrder: -100,
createdAt: new Date(),
updatedAt: new Date(),
},
},
}
workflowRegistryState = {
workflows: {
'workflow-parent-match': {
id: 'workflow-parent-match',
name: 'Existing sibling workflow',
workspaceId: 'ws-1',
folderId: 'parent-1',
sortOrder: 2,
},
'workflow-other-parent': {
id: 'workflow-other-parent',
name: 'Other parent workflow',
workspaceId: 'ws-1',
folderId: 'parent-2',
sortOrder: -50,
},
},
}
})
it('creates folders at top of mixed non-root siblings', async () => {
const mutation = useCreateFolder()
await mutation.onMutate({
workspaceId: 'ws-1',
name: 'New child folder',
parentId: 'parent-1',
})
const optimisticFolder = getOptimisticFolderByName('New child folder')
expect(optimisticFolder).toBeDefined()
expect(optimisticFolder?.sortOrder).toBe(1)
})
it('duplicates folders at top of mixed non-root siblings', async () => {
const mutation = useDuplicateFolderMutation()
await mutation.onMutate({
workspaceId: 'ws-1',
id: 'folder-parent-match',
name: 'Duplicated child folder',
parentId: 'parent-1',
})
const optimisticFolder = getOptimisticFolderByName('Duplicated child folder')
expect(optimisticFolder).toBeDefined()
expect(optimisticFolder?.sortOrder).toBe(1)
})
it('uses source parent scope when duplicate parentId is undefined', async () => {
const mutation = useDuplicateFolderMutation()
await mutation.onMutate({
workspaceId: 'ws-1',
id: 'folder-parent-match',
name: 'Duplicated with inherited parent',
// parentId intentionally omitted to mirror duplicate fallback behavior
})
const optimisticFolder = getOptimisticFolderByName('Duplicated with inherited parent') as
| { parentId: string | null; sortOrder: number }
| undefined
expect(optimisticFolder).toBeDefined()
expect(optimisticFolder?.parentId).toBe('parent-1')
expect(optimisticFolder?.sortOrder).toBe(1)
})
})

View File

@@ -5,9 +5,11 @@ import {
createOptimisticMutationHandlers,
generateTempId,
} from '@/hooks/queries/utils/optimistic-mutation'
import { getTopInsertionSortOrder } from '@/hooks/queries/utils/top-insertion-sort-order'
import { workflowKeys } from '@/hooks/queries/workflows'
import { useFolderStore } from '@/stores/folders/store'
import type { WorkflowFolder } from '@/stores/folders/types'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
const logger = createLogger('FolderQueries')
@@ -133,40 +135,35 @@ function createFolderMutationHandlers<TVariables extends { workspaceId: string }
})
}
/**
* Calculates the next sort order for a folder in a given parent
*/
function getNextSortOrder(
folders: Record<string, WorkflowFolder>,
workspaceId: string,
parentId: string | null | undefined
): number {
const siblingFolders = Object.values(folders).filter(
(f) => f.workspaceId === workspaceId && f.parentId === (parentId || null)
)
return siblingFolders.reduce((max, f) => Math.max(max, f.sortOrder), -1) + 1
}
export function useCreateFolder() {
const queryClient = useQueryClient()
const handlers = createFolderMutationHandlers<CreateFolderVariables>(
queryClient,
'CreateFolder',
(variables, tempId, previousFolders) => ({
id: tempId,
name: variables.name,
userId: '',
workspaceId: variables.workspaceId,
parentId: variables.parentId || null,
color: variables.color || '#808080',
isExpanded: false,
sortOrder:
variables.sortOrder ??
getNextSortOrder(previousFolders, variables.workspaceId, variables.parentId),
createdAt: new Date(),
updatedAt: new Date(),
})
(variables, tempId, previousFolders) => {
const currentWorkflows = useWorkflowRegistry.getState().workflows
return {
id: tempId,
name: variables.name,
userId: '',
workspaceId: variables.workspaceId,
parentId: variables.parentId || null,
color: variables.color || '#808080',
isExpanded: false,
sortOrder:
variables.sortOrder ??
getTopInsertionSortOrder(
currentWorkflows,
previousFolders,
variables.workspaceId,
variables.parentId
),
createdAt: new Date(),
updatedAt: new Date(),
}
}
)
return useMutation({
@@ -242,17 +239,25 @@ export function useDuplicateFolderMutation() {
queryClient,
'DuplicateFolder',
(variables, tempId, previousFolders) => {
const currentWorkflows = useWorkflowRegistry.getState().workflows
// Get source folder info if available
const sourceFolder = previousFolders[variables.id]
const targetParentId = variables.parentId ?? sourceFolder?.parentId ?? null
return {
id: tempId,
name: variables.name,
userId: sourceFolder?.userId || '',
workspaceId: variables.workspaceId,
parentId: variables.parentId ?? sourceFolder?.parentId ?? null,
parentId: targetParentId,
color: variables.color || sourceFolder?.color || '#808080',
isExpanded: false,
sortOrder: getNextSortOrder(previousFolders, variables.workspaceId, variables.parentId),
sortOrder: getTopInsertionSortOrder(
currentWorkflows,
previousFolders,
variables.workspaceId,
targetParentId
),
createdAt: new Date(),
updatedAt: new Date(),
}

View File

@@ -0,0 +1,44 @@
interface SortableWorkflow {
workspaceId?: string
folderId?: string | null
sortOrder?: number
}
interface SortableFolder {
workspaceId?: string
parentId?: string | null
sortOrder: number
}
/**
* Calculates the insertion sort order that places a new item at the top of a
* mixed list of folders and workflows within the same parent scope.
*/
export function getTopInsertionSortOrder(
workflows: Record<string, SortableWorkflow>,
folders: Record<string, SortableFolder>,
workspaceId: string,
parentId: string | null | undefined
): number {
const normalizedParentId = parentId ?? null
const siblingWorkflows = Object.values(workflows).filter(
(workflow) =>
workflow.workspaceId === workspaceId && (workflow.folderId ?? null) === normalizedParentId
)
const siblingFolders = Object.values(folders).filter(
(folder) =>
folder.workspaceId === workspaceId && (folder.parentId ?? null) === normalizedParentId
)
const siblingOrders = [
...siblingWorkflows.map((workflow) => workflow.sortOrder ?? 0),
...siblingFolders.map((folder) => folder.sortOrder),
]
if (siblingOrders.length === 0) {
return 0
}
return Math.min(...siblingOrders) - 1
}

View File

@@ -8,6 +8,8 @@ import {
createOptimisticMutationHandlers,
generateTempId,
} from '@/hooks/queries/utils/optimistic-mutation'
import { getTopInsertionSortOrder } from '@/hooks/queries/utils/top-insertion-sort-order'
import { useFolderStore } from '@/stores/folders/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import type { WorkflowMetadata } from '@/stores/workflows/registry/types'
import { generateCreativeWorkflowName } from '@/stores/workflows/registry/utils'
@@ -223,11 +225,13 @@ export function useCreateWorkflow() {
sortOrder = variables.sortOrder
} else {
const currentWorkflows = useWorkflowRegistry.getState().workflows
const targetFolderId = variables.folderId || null
const workflowsInFolder = Object.values(currentWorkflows).filter(
(w) => w.folderId === targetFolderId
const currentFolders = useFolderStore.getState().folders
sortOrder = getTopInsertionSortOrder(
currentWorkflows,
currentFolders,
variables.workspaceId,
variables.folderId
)
sortOrder = workflowsInFolder.reduce((min, w) => Math.min(min, w.sortOrder ?? 0), 1) - 1
}
return {
@@ -323,11 +327,8 @@ export function useDuplicateWorkflowMutation() {
'DuplicateWorkflow',
(variables, tempId) => {
const currentWorkflows = useWorkflowRegistry.getState().workflows
const targetFolderId = variables.folderId || null
const workflowsInFolder = Object.values(currentWorkflows).filter(
(w) => w.folderId === targetFolderId
)
const minSortOrder = workflowsInFolder.reduce((min, w) => Math.min(min, w.sortOrder ?? 0), 1)
const currentFolders = useFolderStore.getState().folders
const targetFolderId = variables.folderId ?? null
return {
id: tempId,
@@ -338,7 +339,12 @@ export function useDuplicateWorkflowMutation() {
color: variables.color,
workspaceId: variables.workspaceId,
folderId: targetFolderId,
sortOrder: minSortOrder - 1,
sortOrder: getTopInsertionSortOrder(
currentWorkflows,
currentFolders,
variables.workspaceId,
targetFolderId
),
}
}
)

View File

@@ -44,7 +44,7 @@ function useAllowedIntegrationsFromEnv() {
*/
function intersectAllowlists(a: string[] | null, b: string[] | null): string[] | null {
if (a === null) return b
if (b === null) return a
if (b === null) return a.map((i) => i.toLowerCase())
return a.map((i) => i.toLowerCase()).filter((i) => b.includes(i))
}

View File

@@ -24,12 +24,18 @@ export const AuditAction = {
CHAT_UPDATED: 'chat.updated',
CHAT_DELETED: 'chat.deleted',
// Billing
CREDIT_PURCHASED: 'credit.purchased',
// Credential Sets
CREDENTIAL_SET_CREATED: 'credential_set.created',
CREDENTIAL_SET_UPDATED: 'credential_set.updated',
CREDENTIAL_SET_DELETED: 'credential_set.deleted',
CREDENTIAL_SET_MEMBER_REMOVED: 'credential_set_member.removed',
CREDENTIAL_SET_MEMBER_LEFT: 'credential_set_member.left',
CREDENTIAL_SET_INVITATION_CREATED: 'credential_set_invitation.created',
CREDENTIAL_SET_INVITATION_ACCEPTED: 'credential_set_invitation.accepted',
CREDENTIAL_SET_INVITATION_RESENT: 'credential_set_invitation.resent',
CREDENTIAL_SET_INVITATION_REVOKED: 'credential_set_invitation.revoked',
// Documents
@@ -81,6 +87,9 @@ export const AuditAction = {
// OAuth
OAUTH_DISCONNECTED: 'oauth.disconnected',
// Password
PASSWORD_RESET: 'password.reset',
// Organizations
ORGANIZATION_CREATED: 'organization.created',
ORGANIZATION_UPDATED: 'organization.updated',
@@ -103,6 +112,11 @@ export const AuditAction = {
// Schedules
SCHEDULE_UPDATED: 'schedule.updated',
// Templates
TEMPLATE_CREATED: 'template.created',
TEMPLATE_UPDATED: 'template.updated',
TEMPLATE_DELETED: 'template.deleted',
// Webhooks
WEBHOOK_CREATED: 'webhook.created',
WEBHOOK_DELETED: 'webhook.deleted',
@@ -113,6 +127,7 @@ export const AuditAction = {
WORKFLOW_DEPLOYED: 'workflow.deployed',
WORKFLOW_UNDEPLOYED: 'workflow.undeployed',
WORKFLOW_DUPLICATED: 'workflow.duplicated',
WORKFLOW_DEPLOYMENT_ACTIVATED: 'workflow.deployment_activated',
WORKFLOW_DEPLOYMENT_REVERTED: 'workflow.deployment_reverted',
WORKFLOW_VARIABLES_UPDATED: 'workflow.variables_updated',
@@ -129,6 +144,7 @@ export type AuditActionType = (typeof AuditAction)[keyof typeof AuditAction]
*/
export const AuditResourceType = {
API_KEY: 'api_key',
BILLING: 'billing',
BYOK_KEY: 'byok_key',
CHAT: 'chat',
CREDENTIAL_SET: 'credential_set',
@@ -142,8 +158,10 @@ export const AuditResourceType = {
NOTIFICATION: 'notification',
OAUTH: 'oauth',
ORGANIZATION: 'organization',
PASSWORD: 'password',
PERMISSION_GROUP: 'permission_group',
SCHEDULE: 'schedule',
TEMPLATE: 'template',
WEBHOOK: 'webhook',
WORKFLOW: 'workflow',
WORKSPACE: 'workspace',

View File

@@ -483,6 +483,17 @@ export const auth = betterAuth({
throw new Error(`Failed to send reset password email: ${result.message}`)
}
},
onPasswordReset: async ({ user: resetUser }) => {
const { AuditAction, AuditResourceType, recordAudit } = await import('@/lib/audit/log')
recordAudit({
actorId: resetUser.id,
actorName: resetUser.name,
actorEmail: resetUser.email,
action: AuditAction.PASSWORD_RESET,
resourceType: AuditResourceType.PASSWORD,
description: 'Password reset completed',
})
},
},
hooks: {
before: createAuthMiddleware(async (ctx) => {

View File

@@ -9,6 +9,8 @@ const logger = createLogger('HybridAuth')
export interface AuthResult {
success: boolean
userId?: string
userName?: string | null
userEmail?: string | null
authType?: 'session' | 'api_key' | 'internal_jwt'
apiKeyType?: 'personal' | 'workspace'
error?: string
@@ -142,6 +144,8 @@ export async function checkSessionOrInternalAuth(
return {
success: true,
userId: session.user.id,
userName: session.user.name,
userEmail: session.user.email,
authType: 'session',
}
}
@@ -189,6 +193,8 @@ export async function checkHybridAuth(
return {
success: true,
userId: session.user.id,
userName: session.user.name,
userEmail: session.user.email,
authType: 'session',
}
}

View File

@@ -11,6 +11,8 @@ export type McpPermissionLevel = 'read' | 'write' | 'admin'
export interface McpAuthContext {
userId: string
userName?: string | null
userEmail?: string | null
workspaceId: string
requestId: string
}
@@ -114,6 +116,8 @@ async function validateMcpAuth(
success: true,
context: {
userId: auth.userId,
userName: auth.userName,
userEmail: auth.userEmail,
workspaceId,
requestId,
},

View File

@@ -0,0 +1,197 @@
/**
* @vitest-environment node
*/
import { mockConsoleLogger, setupCommonApiMocks } from '@sim/testing'
import { drizzleOrmMock } from '@sim/testing/mocks'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mockAuthorizeWorkflowByWorkspacePermission = vi.fn()
const mockGetUserEntityPermissions = vi.fn()
const { mockDb } = vi.hoisted(() => ({
mockDb: {
transaction: vi.fn(),
},
}))
vi.mock('drizzle-orm', () => ({
...drizzleOrmMock,
min: vi.fn((field) => ({ type: 'min', field })),
}))
vi.mock('@/lib/workflows/utils', () => ({
authorizeWorkflowByWorkspacePermission: (...args: unknown[]) =>
mockAuthorizeWorkflowByWorkspacePermission(...args),
}))
vi.mock('@/lib/workspaces/permissions/utils', () => ({
getUserEntityPermissions: (...args: unknown[]) => mockGetUserEntityPermissions(...args),
}))
vi.mock('@sim/db/schema', () => ({
workflow: {
id: 'id',
workspaceId: 'workspaceId',
folderId: 'folderId',
sortOrder: 'sortOrder',
variables: 'variables',
},
workflowFolder: {
workspaceId: 'workspaceId',
parentId: 'parentId',
sortOrder: 'sortOrder',
},
workflowBlocks: {
workflowId: 'workflowId',
},
workflowEdges: {
workflowId: 'workflowId',
},
workflowSubflows: {
workflowId: 'workflowId',
},
}))
vi.mock('@sim/db', () => ({
db: mockDb,
}))
import { duplicateWorkflow } from './duplicate'
function createMockTx(
selectResults: unknown[],
onWorkflowInsert?: (values: Record<string, unknown>) => void
) {
let selectCallCount = 0
const select = vi.fn().mockImplementation(() => ({
from: vi.fn().mockReturnValue({
where: vi.fn().mockImplementation(() => {
const result = selectResults[selectCallCount++] ?? []
if (selectCallCount === 1) {
return {
limit: vi.fn().mockResolvedValue(result),
}
}
return Promise.resolve(result)
}),
}),
}))
const insert = vi.fn().mockReturnValue({
values: vi.fn().mockImplementation((values: Record<string, unknown>) => {
onWorkflowInsert?.(values)
return Promise.resolve(undefined)
}),
})
const update = vi.fn().mockReturnValue({
set: vi.fn().mockReturnValue({
where: vi.fn().mockResolvedValue(undefined),
}),
})
return {
select,
insert,
update,
}
}
describe('duplicateWorkflow ordering', () => {
beforeEach(() => {
setupCommonApiMocks()
mockConsoleLogger()
vi.clearAllMocks()
vi.stubGlobal('crypto', {
randomUUID: vi.fn().mockReturnValue('new-workflow-id'),
})
mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: true })
mockGetUserEntityPermissions.mockResolvedValue('write')
})
it('uses mixed-sibling top insertion sort order', async () => {
let insertedWorkflowValues: Record<string, unknown> | null = null
const tx = createMockTx(
[
[
{
id: 'source-workflow-id',
workspaceId: 'workspace-123',
folderId: null,
description: 'source',
color: '#000000',
variables: {},
},
],
[{ minOrder: 5 }],
[{ minOrder: 2 }],
[],
[],
[],
],
(values) => {
insertedWorkflowValues = values
}
)
mockDb.transaction.mockImplementation(async (callback: (txArg: unknown) => Promise<unknown>) =>
callback(tx)
)
const result = await duplicateWorkflow({
sourceWorkflowId: 'source-workflow-id',
userId: 'user-123',
name: 'Duplicated',
workspaceId: 'workspace-123',
folderId: null,
requestId: 'req-1',
})
expect(result.sortOrder).toBe(1)
expect(insertedWorkflowValues?.sortOrder).toBe(1)
})
it('defaults to sortOrder 0 when target has no siblings', async () => {
let insertedWorkflowValues: Record<string, unknown> | null = null
const tx = createMockTx(
[
[
{
id: 'source-workflow-id',
workspaceId: 'workspace-123',
folderId: null,
description: 'source',
color: '#000000',
variables: {},
},
],
[],
[],
[],
[],
[],
],
(values) => {
insertedWorkflowValues = values
}
)
mockDb.transaction.mockImplementation(async (callback: (txArg: unknown) => Promise<unknown>) =>
callback(tx)
)
const result = await duplicateWorkflow({
sourceWorkflowId: 'source-workflow-id',
userId: 'user-123',
name: 'Duplicated',
workspaceId: 'workspace-123',
folderId: null,
requestId: 'req-2',
})
expect(result.sortOrder).toBe(0)
expect(insertedWorkflowValues?.sortOrder).toBe(0)
})
})

View File

@@ -1,5 +1,11 @@
import { db } from '@sim/db'
import { workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@sim/db/schema'
import {
workflow,
workflowBlocks,
workflowEdges,
workflowFolder,
workflowSubflows,
} from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq, isNull, min } from 'drizzle-orm'
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
@@ -132,15 +138,31 @@ export async function duplicateWorkflow(
throw new Error('Write or admin access required for target workspace')
}
const targetFolderId = folderId !== undefined ? folderId : source.folderId
const folderCondition = targetFolderId
const workflowParentCondition = targetFolderId
? eq(workflow.folderId, targetFolderId)
: isNull(workflow.folderId)
const folderParentCondition = targetFolderId
? eq(workflowFolder.parentId, targetFolderId)
: isNull(workflowFolder.parentId)
const [minResult] = await tx
.select({ minOrder: min(workflow.sortOrder) })
.from(workflow)
.where(and(eq(workflow.workspaceId, targetWorkspaceId), folderCondition))
const sortOrder = (minResult?.minOrder ?? 1) - 1
const [[workflowMinResult], [folderMinResult]] = await Promise.all([
tx
.select({ minOrder: min(workflow.sortOrder) })
.from(workflow)
.where(and(eq(workflow.workspaceId, targetWorkspaceId), workflowParentCondition)),
tx
.select({ minOrder: min(workflowFolder.sortOrder) })
.from(workflowFolder)
.where(and(eq(workflowFolder.workspaceId, targetWorkspaceId), folderParentCondition)),
])
const minSortOrder = [workflowMinResult?.minOrder, folderMinResult?.minOrder].reduce<
number | null
>((currentMin, candidate) => {
if (candidate == null) return currentMin
if (currentMin == null) return candidate
return Math.min(currentMin, candidate)
}, null)
const sortOrder = minSortOrder != null ? minSortOrder - 1 : 0
// Mapping from old variable IDs to new variable IDs (populated during variable duplication)
const varIdMapping = new Map<string, string>()

View File

@@ -1,6 +1,6 @@
import type { NextConfig } from 'next'
import { env, getEnv, isTruthy } from './lib/core/config/env'
import { isDev, isHosted } from './lib/core/config/feature-flags'
import { isDev } from './lib/core/config/feature-flags'
import {
getFormEmbedCSPPolicy,
getMainCSPPolicy,
@@ -306,34 +306,15 @@ const nextConfig: NextConfig = {
}
)
// Only enable domain redirects for the hosted version
if (isHosted) {
redirects.push(
{
source: '/((?!api|_next|_vercel|favicon|static|ingest|.*\\..*).*)',
destination: 'https://www.sim.ai/$1',
permanent: true,
has: [{ type: 'host' as const, value: 'simstudio.ai' }],
},
{
source: '/((?!api|_next|_vercel|favicon|static|ingest|.*\\..*).*)',
destination: 'https://www.sim.ai/$1',
permanent: true,
has: [{ type: 'host' as const, value: 'www.simstudio.ai' }],
}
)
}
// Beluga campaign short link tracking
if (isHosted) {
redirects.push({
return redirects
},
async rewrites() {
return [
{
source: '/r/:shortCode',
destination: 'https://go.trybeluga.ai/:shortCode',
permanent: false,
})
}
return redirects
},
]
},
}

View File

@@ -125,7 +125,7 @@
"mysql2": "3.14.3",
"nanoid": "^3.3.7",
"neo4j-driver": "6.0.1",
"next": "16.1.0-canary.21",
"next": "16.1.6",
"next-mdx-remote": "^5.0.0",
"next-runtime-env": "3.3.0",
"next-themes": "^0.4.6",
@@ -208,8 +208,8 @@
"sharp"
],
"overrides": {
"next": "16.1.0-canary.21",
"@next/env": "16.1.0-canary.21",
"next": "16.1.6",
"@next/env": "16.1.6",
"drizzle-orm": "^0.44.5",
"postgres": "^3.4.5"
}

View File

@@ -1693,6 +1693,58 @@ import {
typeformUpdateFormTool,
} from '@/tools/typeform'
import type { ToolConfig } from '@/tools/types'
import {
vercelAddDomainTool,
vercelAddProjectDomainTool,
vercelCancelDeploymentTool,
vercelCreateAliasTool,
vercelCreateCheckTool,
vercelCreateDeploymentTool,
vercelCreateDnsRecordTool,
vercelCreateEdgeConfigTool,
vercelCreateEnvVarTool,
vercelCreateProjectTool,
vercelCreateWebhookTool,
vercelDeleteAliasTool,
vercelDeleteDeploymentTool,
vercelDeleteDnsRecordTool,
vercelDeleteDomainTool,
vercelDeleteEnvVarTool,
vercelDeleteProjectTool,
vercelDeleteWebhookTool,
vercelGetAliasTool,
vercelGetCheckTool,
vercelGetDeploymentEventsTool,
vercelGetDeploymentTool,
vercelGetDomainConfigTool,
vercelGetDomainTool,
vercelGetEdgeConfigItemsTool,
vercelGetEdgeConfigTool,
vercelGetEnvVarsTool,
vercelGetProjectTool,
vercelGetTeamTool,
vercelGetUserTool,
vercelListAliasesTool,
vercelListChecksTool,
vercelListDeploymentFilesTool,
vercelListDeploymentsTool,
vercelListDnsRecordsTool,
vercelListDomainsTool,
vercelListEdgeConfigsTool,
vercelListProjectDomainsTool,
vercelListProjectsTool,
vercelListTeamMembersTool,
vercelListTeamsTool,
vercelListWebhooksTool,
vercelPauseProjectTool,
vercelRemoveProjectDomainTool,
vercelRerequestCheckTool,
vercelUnpauseProjectTool,
vercelUpdateCheckTool,
vercelUpdateEdgeConfigItemsTool,
vercelUpdateEnvVarTool,
vercelUpdateProjectTool,
} from '@/tools/vercel'
import {
falaiVideoTool,
lumaVideoTool,
@@ -2700,6 +2752,66 @@ export const tools: Record<string, ToolConfig> = {
trello_update_card: trelloUpdateCardTool,
trello_get_actions: trelloGetActionsTool,
trello_add_comment: trelloAddCommentTool,
// Vercel - Deployments
vercel_list_deployments: vercelListDeploymentsTool,
vercel_get_deployment: vercelGetDeploymentTool,
vercel_create_deployment: vercelCreateDeploymentTool,
vercel_cancel_deployment: vercelCancelDeploymentTool,
vercel_delete_deployment: vercelDeleteDeploymentTool,
vercel_get_deployment_events: vercelGetDeploymentEventsTool,
vercel_list_deployment_files: vercelListDeploymentFilesTool,
// Vercel - Projects
vercel_list_projects: vercelListProjectsTool,
vercel_get_project: vercelGetProjectTool,
vercel_create_project: vercelCreateProjectTool,
vercel_update_project: vercelUpdateProjectTool,
vercel_delete_project: vercelDeleteProjectTool,
vercel_pause_project: vercelPauseProjectTool,
vercel_unpause_project: vercelUnpauseProjectTool,
vercel_list_project_domains: vercelListProjectDomainsTool,
vercel_add_project_domain: vercelAddProjectDomainTool,
vercel_remove_project_domain: vercelRemoveProjectDomainTool,
// Vercel - Environment Variables
vercel_get_env_vars: vercelGetEnvVarsTool,
vercel_create_env_var: vercelCreateEnvVarTool,
vercel_update_env_var: vercelUpdateEnvVarTool,
vercel_delete_env_var: vercelDeleteEnvVarTool,
// Vercel - Domains
vercel_list_domains: vercelListDomainsTool,
vercel_get_domain: vercelGetDomainTool,
vercel_add_domain: vercelAddDomainTool,
vercel_delete_domain: vercelDeleteDomainTool,
vercel_get_domain_config: vercelGetDomainConfigTool,
// Vercel - DNS
vercel_list_dns_records: vercelListDnsRecordsTool,
vercel_create_dns_record: vercelCreateDnsRecordTool,
vercel_delete_dns_record: vercelDeleteDnsRecordTool,
// Vercel - Aliases
vercel_list_aliases: vercelListAliasesTool,
vercel_get_alias: vercelGetAliasTool,
vercel_create_alias: vercelCreateAliasTool,
vercel_delete_alias: vercelDeleteAliasTool,
// Vercel - Edge Config
vercel_list_edge_configs: vercelListEdgeConfigsTool,
vercel_get_edge_config: vercelGetEdgeConfigTool,
vercel_create_edge_config: vercelCreateEdgeConfigTool,
vercel_get_edge_config_items: vercelGetEdgeConfigItemsTool,
vercel_update_edge_config_items: vercelUpdateEdgeConfigItemsTool,
// Vercel - Teams & User
vercel_list_teams: vercelListTeamsTool,
vercel_get_team: vercelGetTeamTool,
vercel_list_team_members: vercelListTeamMembersTool,
vercel_get_user: vercelGetUserTool,
// Webhooks
vercel_list_webhooks: vercelListWebhooksTool,
vercel_create_webhook: vercelCreateWebhookTool,
vercel_delete_webhook: vercelDeleteWebhookTool,
// Checks
vercel_create_check: vercelCreateCheckTool,
vercel_get_check: vercelGetCheckTool,
vercel_list_checks: vercelListChecksTool,
vercel_update_check: vercelUpdateCheckTool,
vercel_rerequest_check: vercelRerequestCheckTool,
twilio_send_sms: sendSMSTool,
twilio_voice_make_call: makeCallTool,
twilio_voice_list_calls: listCallsTool,

View File

@@ -0,0 +1,84 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelAddDomainParams, VercelAddDomainResponse } from '@/tools/vercel/types'
export const vercelAddDomainTool: ToolConfig<VercelAddDomainParams, VercelAddDomainResponse> = {
id: 'vercel_add_domain',
name: 'Vercel Add Domain',
description: 'Add a new domain to a Vercel account or team',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The domain name to add',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelAddDomainParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v7/domains${qs ? `?${qs}` : ''}`
},
method: 'POST',
headers: (params: VercelAddDomainParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params: VercelAddDomainParams) => ({
method: 'add',
name: params.name.trim(),
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
const d = data.domain ?? data
return {
success: true,
output: {
id: d.id ?? null,
name: d.name ?? null,
verified: d.verified ?? false,
createdAt: d.createdAt ?? null,
serviceType: d.serviceType ?? null,
nameservers: d.nameservers ?? [],
intendedNameservers: d.intendedNameservers ?? [],
},
}
},
outputs: {
id: { type: 'string', description: 'Domain ID' },
name: { type: 'string', description: 'Domain name' },
verified: { type: 'boolean', description: 'Whether domain is verified' },
createdAt: { type: 'number', description: 'Creation timestamp' },
serviceType: { type: 'string', description: 'Service type (zeit.world, external, na)' },
nameservers: {
type: 'array',
description: 'Current nameservers',
items: { type: 'string' },
},
intendedNameservers: {
type: 'array',
description: 'Intended nameservers',
items: { type: 'string' },
},
},
}

View File

@@ -0,0 +1,113 @@
import type { ToolConfig } from '@/tools/types'
import type {
VercelAddProjectDomainParams,
VercelAddProjectDomainResponse,
} from '@/tools/vercel/types'
export const vercelAddProjectDomainTool: ToolConfig<
VercelAddProjectDomainParams,
VercelAddProjectDomainResponse
> = {
id: 'vercel_add_project_domain',
name: 'Vercel Add Project Domain',
description: 'Add a domain to a Vercel project',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
projectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Project ID or name',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Domain name to add',
},
redirect: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Target domain for redirect',
},
redirectStatusCode: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'HTTP status code for redirect (301, 302, 307, 308)',
},
gitBranch: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Git branch to link the domain to',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelAddProjectDomainParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v10/projects/${params.projectId.trim()}/domains${qs ? `?${qs}` : ''}`
},
method: 'POST',
headers: (params: VercelAddProjectDomainParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params: VercelAddProjectDomainParams) => {
const body: Record<string, unknown> = { name: params.domain.trim() }
if (params.redirect) body.redirect = params.redirect.trim()
if (params.redirectStatusCode) body.redirectStatusCode = params.redirectStatusCode
if (params.gitBranch) body.gitBranch = params.gitBranch.trim()
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
name: data.name,
apexName: data.apexName,
verified: data.verified,
gitBranch: data.gitBranch ?? null,
redirect: data.redirect ?? null,
redirectStatusCode: data.redirectStatusCode ?? null,
createdAt: data.createdAt,
updatedAt: data.updatedAt,
},
}
},
outputs: {
name: { type: 'string', description: 'Domain name' },
apexName: { type: 'string', description: 'Apex domain name' },
verified: { type: 'boolean', description: 'Whether the domain is verified' },
gitBranch: { type: 'string', description: 'Git branch for the domain', optional: true },
redirect: { type: 'string', description: 'Redirect target domain', optional: true },
redirectStatusCode: {
type: 'number',
description: 'HTTP status code for redirect (301, 302, 307, 308)',
optional: true,
},
createdAt: { type: 'number', description: 'Creation timestamp' },
updatedAt: { type: 'number', description: 'Last updated timestamp' },
},
}

View File

@@ -0,0 +1,83 @@
import type { ToolConfig } from '@/tools/types'
import type {
VercelCancelDeploymentParams,
VercelCancelDeploymentResponse,
} from '@/tools/vercel/types'
export const vercelCancelDeploymentTool: ToolConfig<
VercelCancelDeploymentParams,
VercelCancelDeploymentResponse
> = {
id: 'vercel_cancel_deployment',
name: 'Vercel Cancel Deployment',
description: 'Cancel a running Vercel deployment',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
deploymentId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The deployment ID to cancel',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelCancelDeploymentParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v12/deployments/${params.deploymentId.trim()}/cancel${qs ? `?${qs}` : ''}`
},
method: 'PATCH',
headers: (params: VercelCancelDeploymentParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
id: data.id ?? data.uid,
name: data.name ?? null,
state: data.readyState ?? data.state ?? 'CANCELED',
url: data.url ?? null,
},
}
},
outputs: {
id: {
type: 'string',
description: 'Deployment ID',
},
name: {
type: 'string',
description: 'Deployment name',
},
state: {
type: 'string',
description: 'Deployment state after cancellation',
},
url: {
type: 'string',
description: 'Deployment URL',
},
},
}

View File

@@ -0,0 +1,87 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelCreateAliasParams, VercelCreateAliasResponse } from '@/tools/vercel/types'
export const vercelCreateAliasTool: ToolConfig<VercelCreateAliasParams, VercelCreateAliasResponse> =
{
id: 'vercel_create_alias',
name: 'Vercel Create Alias',
description: 'Assign an alias (domain/subdomain) to a deployment',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
deploymentId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Deployment ID to assign the alias to',
},
alias: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The domain or subdomain to assign as an alias',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelCreateAliasParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v2/deployments/${params.deploymentId.trim()}/aliases${qs ? `?${qs}` : ''}`
},
method: 'POST',
headers: (params: VercelCreateAliasParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params: VercelCreateAliasParams) => ({
alias: params.alias.trim(),
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
uid: data.uid ?? null,
alias: data.alias ?? null,
created: data.created ?? null,
oldDeploymentId: data.oldDeploymentId ?? null,
},
}
},
outputs: {
uid: {
type: 'string',
description: 'Alias ID',
},
alias: {
type: 'string',
description: 'Alias hostname',
},
created: {
type: 'string',
description: 'Creation timestamp as ISO 8601 date-time string',
},
oldDeploymentId: {
type: 'string',
description: 'ID of the previously aliased deployment, if the alias was reassigned',
},
},
}

View File

@@ -0,0 +1,141 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelCheckResponse, VercelCreateCheckParams } from '@/tools/vercel/types'
export const vercelCreateCheckTool: ToolConfig<VercelCreateCheckParams, VercelCheckResponse> = {
id: 'vercel_create_check',
name: 'Vercel Create Check',
description: 'Create a new deployment check',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
deploymentId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Deployment ID to create the check for',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name of the check (max 100 characters)',
},
blocking: {
type: 'boolean',
required: true,
visibility: 'user-or-llm',
description: 'Whether the check blocks the deployment',
},
path: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Page path being checked',
},
detailsUrl: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'URL with details about the check',
},
externalId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'External identifier for the check',
},
rerequestable: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the check can be rerequested',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelCreateCheckParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v1/deployments/${params.deploymentId.trim()}/checks${qs ? `?${qs}` : ''}`
},
method: 'POST',
headers: (params: VercelCreateCheckParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params: VercelCreateCheckParams) => {
const body: Record<string, unknown> = {
name: params.name.trim(),
blocking: params.blocking,
}
if (params.path) body.path = params.path
if (params.detailsUrl) body.detailsUrl = params.detailsUrl
if (params.externalId) body.externalId = params.externalId
if (params.rerequestable !== undefined) body.rerequestable = params.rerequestable
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
id: data.id,
name: data.name,
status: data.status ?? 'registered',
conclusion: data.conclusion ?? null,
blocking: data.blocking ?? false,
deploymentId: data.deploymentId,
integrationId: data.integrationId ?? null,
externalId: data.externalId ?? null,
detailsUrl: data.detailsUrl ?? null,
path: data.path ?? null,
rerequestable: data.rerequestable ?? false,
createdAt: data.createdAt,
updatedAt: data.updatedAt,
startedAt: data.startedAt ?? null,
completedAt: data.completedAt ?? null,
},
}
},
outputs: {
id: { type: 'string', description: 'Check ID' },
name: { type: 'string', description: 'Check name' },
status: { type: 'string', description: 'Check status: registered, running, or completed' },
conclusion: {
type: 'string',
description: 'Check conclusion: canceled, failed, neutral, succeeded, skipped, or stale',
optional: true,
},
blocking: { type: 'boolean', description: 'Whether the check blocks the deployment' },
deploymentId: { type: 'string', description: 'Associated deployment ID' },
integrationId: { type: 'string', description: 'Associated integration ID', optional: true },
externalId: { type: 'string', description: 'External identifier', optional: true },
detailsUrl: { type: 'string', description: 'URL with details about the check', optional: true },
path: { type: 'string', description: 'Page path being checked', optional: true },
rerequestable: { type: 'boolean', description: 'Whether the check can be rerequested' },
createdAt: { type: 'number', description: 'Creation timestamp in milliseconds' },
updatedAt: { type: 'number', description: 'Last update timestamp in milliseconds' },
startedAt: { type: 'number', description: 'Start timestamp in milliseconds', optional: true },
completedAt: {
type: 'number',
description: 'Completion timestamp in milliseconds',
optional: true,
},
},
}

View File

@@ -0,0 +1,136 @@
import type { ToolConfig } from '@/tools/types'
import type {
VercelCreateDeploymentParams,
VercelCreateDeploymentResponse,
} from '@/tools/vercel/types'
export const vercelCreateDeploymentTool: ToolConfig<
VercelCreateDeploymentParams,
VercelCreateDeploymentResponse
> = {
id: 'vercel_create_deployment',
name: 'Vercel Create Deployment',
description: 'Create a new deployment or redeploy an existing one',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Project name for the deployment',
},
project: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Project ID (overrides name for project lookup)',
},
deploymentId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Existing deployment ID to redeploy',
},
target: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Target environment: production, staging, or a custom environment identifier',
},
gitSource: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'JSON string defining the Git Repository source to deploy (e.g. {"type":"github","repo":"owner/repo","ref":"main"})',
},
forceNew: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Forces a new deployment even if there is a previous similar deployment (0 or 1)',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelCreateDeploymentParams) => {
const query = new URLSearchParams()
if (params.forceNew) query.set('forceNew', params.forceNew)
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v13/deployments${qs ? `?${qs}` : ''}`
},
method: 'POST',
headers: (params: VercelCreateDeploymentParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params: VercelCreateDeploymentParams) => {
const body: Record<string, any> = {
name: params.name.trim(),
}
if (params.project) body.project = params.project.trim()
if (params.deploymentId) body.deploymentId = params.deploymentId.trim()
if (params.target) body.target = params.target
if (params.gitSource) {
try {
body.gitSource = JSON.parse(params.gitSource)
} catch {
body.gitSource = params.gitSource
}
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
id: data.id,
name: data.name,
url: data.url ?? '',
readyState: data.readyState ?? 'QUEUED',
projectId: data.projectId ?? '',
createdAt: data.createdAt ?? data.created,
alias: data.alias ?? [],
target: data.target ?? null,
inspectorUrl: data.inspectorUrl ?? '',
},
}
},
outputs: {
id: { type: 'string', description: 'Deployment ID' },
name: { type: 'string', description: 'Deployment name' },
url: { type: 'string', description: 'Unique deployment URL' },
readyState: {
type: 'string',
description: 'Deployment ready state: QUEUED, BUILDING, ERROR, INITIALIZING, READY, CANCELED',
},
projectId: { type: 'string', description: 'Associated project ID' },
createdAt: { type: 'number', description: 'Creation timestamp in milliseconds' },
alias: {
type: 'array',
description: 'Assigned aliases',
items: { type: 'string', description: 'Alias domain' },
},
target: { type: 'string', description: 'Target environment', optional: true },
inspectorUrl: { type: 'string', description: 'Vercel inspector URL' },
},
}

View File

@@ -0,0 +1,107 @@
import type { ToolConfig } from '@/tools/types'
import type {
VercelCreateDnsRecordParams,
VercelCreateDnsRecordResponse,
} from '@/tools/vercel/types'
export const vercelCreateDnsRecordTool: ToolConfig<
VercelCreateDnsRecordParams,
VercelCreateDnsRecordResponse
> = {
id: 'vercel_create_dns_record',
name: 'Vercel Create DNS Record',
description: 'Create a DNS record for a domain in a Vercel account',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The domain name to create the record for',
},
recordName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The subdomain or record name',
},
recordType: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'DNS record type (A, AAAA, ALIAS, CAA, CNAME, HTTPS, MX, SRV, TXT, NS)',
},
value: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The value of the DNS record',
},
ttl: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Time to live in seconds',
},
mxPriority: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Priority for MX records',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelCreateDnsRecordParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v2/domains/${params.domain.trim()}/records${qs ? `?${qs}` : ''}`
},
method: 'POST',
headers: (params: VercelCreateDnsRecordParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params: VercelCreateDnsRecordParams) => {
const body: Record<string, unknown> = {
name: params.recordName.trim(),
type: params.recordType.trim(),
value: params.value.trim(),
}
if (params.ttl != null) body.ttl = params.ttl
if (params.mxPriority != null) body.mxPriority = params.mxPriority
return body
},
},
transformResponse: async (response: Response) => {
const d = await response.json()
return {
success: true,
output: {
uid: d.uid ?? null,
updated: d.updated ?? null,
},
}
},
outputs: {
uid: { type: 'string', description: 'The DNS record ID' },
updated: { type: 'number', description: 'Timestamp of the update' },
},
}

View File

@@ -0,0 +1,106 @@
import type { ToolConfig } from '@/tools/types'
import type {
VercelCreateEdgeConfigParams,
VercelCreateEdgeConfigResponse,
} from '@/tools/vercel/types'
export const vercelCreateEdgeConfigTool: ToolConfig<
VercelCreateEdgeConfigParams,
VercelCreateEdgeConfigResponse
> = {
id: 'vercel_create_edge_config',
name: 'Vercel Create Edge Config',
description: 'Create a new Edge Config store',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
slug: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The name/slug for the new Edge Config',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelCreateEdgeConfigParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v1/edge-config${qs ? `?${qs}` : ''}`
},
method: 'POST',
headers: (params: VercelCreateEdgeConfigParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params: VercelCreateEdgeConfigParams) => ({
slug: params.slug.trim(),
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
id: data.id ?? null,
slug: data.slug ?? null,
ownerId: data.ownerId ?? null,
digest: data.digest ?? null,
createdAt: data.createdAt ?? null,
updatedAt: data.updatedAt ?? null,
itemCount: data.itemCount ?? 0,
sizeInBytes: data.sizeInBytes ?? 0,
},
}
},
outputs: {
id: {
type: 'string',
description: 'Edge Config ID',
},
slug: {
type: 'string',
description: 'Edge Config slug',
},
ownerId: {
type: 'string',
description: 'Owner ID',
},
digest: {
type: 'string',
description: 'Content digest hash',
},
createdAt: {
type: 'number',
description: 'Creation timestamp',
},
updatedAt: {
type: 'number',
description: 'Last update timestamp',
},
itemCount: {
type: 'number',
description: 'Number of items',
},
sizeInBytes: {
type: 'number',
description: 'Size in bytes',
},
},
}

View File

@@ -0,0 +1,145 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelCreateEnvVarParams, VercelCreateEnvVarResponse } from '@/tools/vercel/types'
export const vercelCreateEnvVarTool: ToolConfig<
VercelCreateEnvVarParams,
VercelCreateEnvVarResponse
> = {
id: 'vercel_create_env_var',
name: 'Vercel Create Environment Variable',
description: 'Create an environment variable for a Vercel project',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
projectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Project ID or name',
},
key: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Environment variable name',
},
value: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Environment variable value',
},
target: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Comma-separated list of target environments (production, preview, development)',
},
type: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Variable type: system, secret, encrypted, plain, or sensitive (default: plain)',
},
gitBranch: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Git branch to associate with the variable (requires target to include preview)',
},
comment: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comment to add context to the variable (max 500 characters)',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelCreateEnvVarParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v10/projects/${params.projectId.trim()}/env${qs ? `?${qs}` : ''}`
},
method: 'POST',
headers: (params: VercelCreateEnvVarParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params: VercelCreateEnvVarParams) => {
const body: Record<string, unknown> = {
key: params.key,
value: params.value,
target: params.target.split(',').map((t) => t.trim()),
type: params.type || 'plain',
}
if (params.gitBranch) body.gitBranch = params.gitBranch
if (params.comment) body.comment = params.comment
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
const env = data.created ?? data
return {
success: true,
output: {
id: env.id,
key: env.key,
value: env.value ?? '',
type: env.type ?? 'plain',
target: env.target ?? [],
gitBranch: env.gitBranch ?? null,
comment: env.comment ?? null,
},
}
},
outputs: {
id: {
type: 'string',
description: 'Environment variable ID',
},
key: {
type: 'string',
description: 'Variable name',
},
value: {
type: 'string',
description: 'Variable value',
},
type: {
type: 'string',
description: 'Variable type (secret, system, encrypted, plain, sensitive)',
},
target: {
type: 'array',
description: 'Target environments',
items: { type: 'string', description: 'Environment name' },
},
gitBranch: {
type: 'string',
description: 'Git branch filter',
optional: true,
},
comment: {
type: 'string',
description: 'Comment providing context for the variable',
optional: true,
},
},
}

View File

@@ -0,0 +1,108 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelCreateProjectParams, VercelCreateProjectResponse } from '@/tools/vercel/types'
export const vercelCreateProjectTool: ToolConfig<
VercelCreateProjectParams,
VercelCreateProjectResponse
> = {
id: 'vercel_create_project',
name: 'Vercel Create Project',
description: 'Create a new Vercel project',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Project name',
},
framework: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Project framework (e.g. nextjs, remix, vite)',
},
gitRepository: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description: 'Git repository connection object with type and repo',
},
buildCommand: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Custom build command',
},
outputDirectory: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Custom output directory',
},
installCommand: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Custom install command',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelCreateProjectParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v11/projects${qs ? `?${qs}` : ''}`
},
method: 'POST',
headers: (params: VercelCreateProjectParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params: VercelCreateProjectParams) => {
const body: Record<string, unknown> = { name: params.name.trim() }
if (params.framework) body.framework = params.framework.trim()
if (params.gitRepository) body.gitRepository = params.gitRepository
if (params.buildCommand) body.buildCommand = params.buildCommand.trim()
if (params.outputDirectory) body.outputDirectory = params.outputDirectory.trim()
if (params.installCommand) body.installCommand = params.installCommand.trim()
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
id: data.id,
name: data.name,
framework: data.framework ?? null,
createdAt: data.createdAt,
updatedAt: data.updatedAt,
},
}
},
outputs: {
id: { type: 'string', description: 'Project ID' },
name: { type: 'string', description: 'Project name' },
framework: { type: 'string', description: 'Project framework', optional: true },
createdAt: { type: 'number', description: 'Creation timestamp' },
updatedAt: { type: 'number', description: 'Last updated timestamp' },
},
}

View File

@@ -0,0 +1,105 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelCreateWebhookParams, VercelCreateWebhookResponse } from '@/tools/vercel/types'
export const vercelCreateWebhookTool: ToolConfig<
VercelCreateWebhookParams,
VercelCreateWebhookResponse
> = {
id: 'vercel_create_webhook',
name: 'Vercel Create Webhook',
description: 'Create a new webhook for a Vercel team',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
url: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Webhook URL (must be https)',
},
events: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Comma-separated event names to subscribe to',
},
projectIds: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated project IDs to scope the webhook to',
},
teamId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Team ID to create the webhook for',
},
},
request: {
url: (params: VercelCreateWebhookParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v1/webhooks${qs ? `?${qs}` : ''}`
},
method: 'POST',
headers: (params: VercelCreateWebhookParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
body: (params: VercelCreateWebhookParams) => {
const body: Record<string, any> = {
url: params.url.trim(),
events: params.events.split(',').map((e) => e.trim()),
}
if (params.projectIds) {
body.projectIds = params.projectIds.split(',').map((p) => p.trim())
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
id: data.id ?? null,
url: data.url ?? null,
secret: data.secret ?? null,
events: data.events ?? [],
ownerId: data.ownerId ?? null,
projectIds: data.projectIds ?? [],
createdAt: data.createdAt ?? null,
updatedAt: data.updatedAt ?? null,
},
}
},
outputs: {
id: { type: 'string', description: 'Webhook ID' },
url: { type: 'string', description: 'Webhook URL' },
secret: { type: 'string', description: 'Webhook signing secret' },
events: {
type: 'array',
description: 'Events the webhook listens to',
items: { type: 'string', description: 'Event name' },
},
ownerId: { type: 'string', description: 'Owner ID' },
projectIds: {
type: 'array',
description: 'Associated project IDs',
items: { type: 'string', description: 'Project ID' },
},
createdAt: { type: 'number', description: 'Creation timestamp' },
updatedAt: { type: 'number', description: 'Last updated timestamp' },
},
}

View File

@@ -0,0 +1,62 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelDeleteAliasParams, VercelDeleteAliasResponse } from '@/tools/vercel/types'
export const vercelDeleteAliasTool: ToolConfig<VercelDeleteAliasParams, VercelDeleteAliasResponse> =
{
id: 'vercel_delete_alias',
name: 'Vercel Delete Alias',
description: 'Delete an alias by its ID',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
aliasId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Alias ID to delete',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelDeleteAliasParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v2/aliases/${params.aliasId.trim()}${qs ? `?${qs}` : ''}`
},
method: 'DELETE',
headers: (params: VercelDeleteAliasParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
status: data.status ?? 'SUCCESS',
},
}
},
outputs: {
status: {
type: 'string',
description: 'Deletion status (SUCCESS)',
},
},
}

View File

@@ -0,0 +1,77 @@
import type { ToolConfig } from '@/tools/types'
import type {
VercelDeleteDeploymentParams,
VercelDeleteDeploymentResponse,
} from '@/tools/vercel/types'
export const vercelDeleteDeploymentTool: ToolConfig<
VercelDeleteDeploymentParams,
VercelDeleteDeploymentResponse
> = {
id: 'vercel_delete_deployment',
name: 'Vercel Delete Deployment',
description: 'Delete a Vercel deployment',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
deploymentId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The deployment ID or URL to delete',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelDeleteDeploymentParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const id = params.deploymentId.trim()
if (id.includes('.')) {
query.set('url', id)
}
const qs = query.toString()
return `https://api.vercel.com/v13/deployments/${id}${qs ? `?${qs}` : ''}`
},
method: 'DELETE',
headers: (params: VercelDeleteDeploymentParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
uid: data.uid ?? data.id ?? null,
state: data.state ?? 'DELETED',
},
}
},
outputs: {
uid: {
type: 'string',
description: 'The removed deployment ID',
},
state: {
type: 'string',
description: 'Deployment state after deletion (DELETED)',
},
},
}

View File

@@ -0,0 +1,69 @@
import type { ToolConfig } from '@/tools/types'
import type {
VercelDeleteDnsRecordParams,
VercelDeleteDnsRecordResponse,
} from '@/tools/vercel/types'
export const vercelDeleteDnsRecordTool: ToolConfig<
VercelDeleteDnsRecordParams,
VercelDeleteDnsRecordResponse
> = {
id: 'vercel_delete_dns_record',
name: 'Vercel Delete DNS Record',
description: 'Delete a DNS record for a domain in a Vercel account',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The domain name the record belongs to',
},
recordId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the DNS record to delete',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelDeleteDnsRecordParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v2/domains/${params.domain.trim()}/records/${params.recordId.trim()}${qs ? `?${qs}` : ''}`
},
method: 'DELETE',
headers: (params: VercelDeleteDnsRecordParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async () => {
return {
success: true,
output: {
deleted: true,
},
}
},
outputs: {
deleted: { type: 'boolean', description: 'Whether the record was deleted' },
},
}

View File

@@ -0,0 +1,64 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelDeleteDomainParams, VercelDeleteDomainResponse } from '@/tools/vercel/types'
export const vercelDeleteDomainTool: ToolConfig<
VercelDeleteDomainParams,
VercelDeleteDomainResponse
> = {
id: 'vercel_delete_domain',
name: 'Vercel Delete Domain',
description: 'Delete a domain from a Vercel account or team',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The domain name to delete',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelDeleteDomainParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v6/domains/${params.domain.trim()}${qs ? `?${qs}` : ''}`
},
method: 'DELETE',
headers: (params: VercelDeleteDomainParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const d = await response.json()
return {
success: true,
output: {
uid: d.uid ?? null,
deleted: true,
},
}
},
outputs: {
uid: { type: 'string', description: 'The ID of the deleted domain' },
deleted: { type: 'boolean', description: 'Whether the domain was deleted' },
},
}

View File

@@ -0,0 +1,69 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelDeleteEnvVarParams, VercelDeleteEnvVarResponse } from '@/tools/vercel/types'
export const vercelDeleteEnvVarTool: ToolConfig<
VercelDeleteEnvVarParams,
VercelDeleteEnvVarResponse
> = {
id: 'vercel_delete_env_var',
name: 'Vercel Delete Environment Variable',
description: 'Delete an environment variable from a Vercel project',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
projectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Project ID or name',
},
envId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Environment variable ID to delete',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelDeleteEnvVarParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v9/projects/${params.projectId.trim()}/env/${params.envId.trim()}${qs ? `?${qs}` : ''}`
},
method: 'DELETE',
headers: (params: VercelDeleteEnvVarParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async () => {
return {
success: true,
output: {
deleted: true,
},
}
},
outputs: {
deleted: {
type: 'boolean',
description: 'Whether the environment variable was successfully deleted',
},
},
}

View File

@@ -0,0 +1,60 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelDeleteProjectParams, VercelDeleteProjectResponse } from '@/tools/vercel/types'
export const vercelDeleteProjectTool: ToolConfig<
VercelDeleteProjectParams,
VercelDeleteProjectResponse
> = {
id: 'vercel_delete_project',
name: 'Vercel Delete Project',
description: 'Delete a Vercel project',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
projectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Project ID or name',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelDeleteProjectParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v9/projects/${params.projectId.trim()}${qs ? `?${qs}` : ''}`
},
method: 'DELETE',
headers: (params: VercelDeleteProjectParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async () => {
return {
success: true,
output: {
deleted: true,
},
}
},
outputs: {
deleted: { type: 'boolean', description: 'Whether the project was successfully deleted' },
},
}

View File

@@ -0,0 +1,63 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelDeleteWebhookParams, VercelDeleteWebhookResponse } from '@/tools/vercel/types'
export const vercelDeleteWebhookTool: ToolConfig<
VercelDeleteWebhookParams,
VercelDeleteWebhookResponse
> = {
id: 'vercel_delete_webhook',
name: 'Vercel Delete Webhook',
description: 'Delete a webhook from a Vercel team',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
webhookId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The webhook ID to delete',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelDeleteWebhookParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v1/webhooks/${params.webhookId.trim()}${qs ? `?${qs}` : ''}`
},
method: 'DELETE',
headers: (params: VercelDeleteWebhookParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async () => {
return {
success: true,
output: {
deleted: true,
},
}
},
outputs: {
deleted: {
type: 'boolean',
description: 'Whether the webhook was successfully deleted',
},
},
}

View File

@@ -0,0 +1,97 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelGetAliasParams, VercelGetAliasResponse } from '@/tools/vercel/types'
export const vercelGetAliasTool: ToolConfig<VercelGetAliasParams, VercelGetAliasResponse> = {
id: 'vercel_get_alias',
name: 'Vercel Get Alias',
description: 'Get details about a specific alias by ID or hostname',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
aliasId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Alias ID or hostname to look up',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelGetAliasParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v4/aliases/${params.aliasId.trim()}${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params: VercelGetAliasParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
uid: data.uid ?? null,
alias: data.alias ?? null,
deploymentId: data.deploymentId ?? null,
projectId: data.projectId ?? null,
createdAt: data.createdAt ?? null,
updatedAt: data.updatedAt ?? null,
redirect: data.redirect ?? null,
redirectStatusCode: data.redirectStatusCode ?? null,
},
}
},
outputs: {
uid: {
type: 'string',
description: 'Alias ID',
},
alias: {
type: 'string',
description: 'Alias hostname',
},
deploymentId: {
type: 'string',
description: 'Associated deployment ID',
},
projectId: {
type: 'string',
description: 'Associated project ID',
},
createdAt: {
type: 'number',
description: 'Creation timestamp in milliseconds',
},
updatedAt: {
type: 'number',
description: 'Last update timestamp in milliseconds',
},
redirect: {
type: 'string',
description: 'Target domain for redirect aliases',
},
redirectStatusCode: {
type: 'number',
description: 'HTTP status code for redirect (301, 302, 307, or 308)',
},
},
}

View File

@@ -0,0 +1,99 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelCheckResponse, VercelGetCheckParams } from '@/tools/vercel/types'
export const vercelGetCheckTool: ToolConfig<VercelGetCheckParams, VercelCheckResponse> = {
id: 'vercel_get_check',
name: 'Vercel Get Check',
description: 'Get details of a specific deployment check',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
deploymentId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Deployment ID the check belongs to',
},
checkId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Check ID to retrieve',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelGetCheckParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v1/deployments/${params.deploymentId.trim()}/checks/${params.checkId.trim()}${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params: VercelGetCheckParams) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
id: data.id,
name: data.name,
status: data.status ?? 'registered',
conclusion: data.conclusion ?? null,
blocking: data.blocking ?? false,
deploymentId: data.deploymentId,
integrationId: data.integrationId ?? null,
externalId: data.externalId ?? null,
detailsUrl: data.detailsUrl ?? null,
path: data.path ?? null,
rerequestable: data.rerequestable ?? false,
createdAt: data.createdAt,
updatedAt: data.updatedAt,
startedAt: data.startedAt ?? null,
completedAt: data.completedAt ?? null,
},
}
},
outputs: {
id: { type: 'string', description: 'Check ID' },
name: { type: 'string', description: 'Check name' },
status: { type: 'string', description: 'Check status: registered, running, or completed' },
conclusion: {
type: 'string',
description: 'Check conclusion: canceled, failed, neutral, succeeded, skipped, or stale',
optional: true,
},
blocking: { type: 'boolean', description: 'Whether the check blocks the deployment' },
deploymentId: { type: 'string', description: 'Associated deployment ID' },
integrationId: { type: 'string', description: 'Associated integration ID', optional: true },
externalId: { type: 'string', description: 'External identifier', optional: true },
detailsUrl: { type: 'string', description: 'URL with details about the check', optional: true },
path: { type: 'string', description: 'Page path being checked', optional: true },
rerequestable: { type: 'boolean', description: 'Whether the check can be rerequested' },
createdAt: { type: 'number', description: 'Creation timestamp in milliseconds' },
updatedAt: { type: 'number', description: 'Last update timestamp in milliseconds' },
startedAt: { type: 'number', description: 'Start timestamp in milliseconds', optional: true },
completedAt: {
type: 'number',
description: 'Completion timestamp in milliseconds',
optional: true,
},
},
}

View File

@@ -0,0 +1,176 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelGetDeploymentParams, VercelGetDeploymentResponse } from '@/tools/vercel/types'
export const vercelGetDeploymentTool: ToolConfig<
VercelGetDeploymentParams,
VercelGetDeploymentResponse
> = {
id: 'vercel_get_deployment',
name: 'Vercel Get Deployment',
description: 'Get details of a specific Vercel deployment',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
deploymentId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The unique deployment identifier or hostname',
},
withGitRepoInfo: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Whether to add in gitRepo information (true/false)',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelGetDeploymentParams) => {
const query = new URLSearchParams()
if (params.withGitRepoInfo) query.set('withGitRepoInfo', params.withGitRepoInfo)
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v13/deployments/${params.deploymentId.trim()}${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params: VercelGetDeploymentParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
id: data.id,
name: data.name,
url: data.url ?? '',
readyState: data.readyState ?? 'UNKNOWN',
status: data.status ?? data.readyState ?? 'UNKNOWN',
target: data.target ?? null,
createdAt: data.createdAt ?? data.created,
buildingAt: data.buildingAt ?? null,
ready: data.ready ?? null,
source: data.source ?? '',
alias: data.alias ?? [],
regions: data.regions ?? [],
inspectorUrl: data.inspectorUrl ?? '',
projectId: data.projectId ?? '',
creator: {
uid: data.creator?.uid ?? '',
username: data.creator?.username ?? '',
},
project: data.project
? {
id: data.project.id,
name: data.project.name,
framework: data.project.framework ?? null,
}
: null,
meta: data.meta ?? {},
gitSource: data.gitSource ?? null,
},
}
},
outputs: {
id: { type: 'string', description: 'Deployment ID' },
name: { type: 'string', description: 'Deployment name' },
url: { type: 'string', description: 'Unique deployment URL' },
readyState: {
type: 'string',
description: 'Deployment ready state: QUEUED, BUILDING, ERROR, INITIALIZING, READY, CANCELED',
},
status: {
type: 'string',
description: 'Deployment status',
},
target: { type: 'string', description: 'Target environment', optional: true },
createdAt: { type: 'number', description: 'Creation timestamp in milliseconds' },
buildingAt: { type: 'number', description: 'Build start timestamp', optional: true },
ready: { type: 'number', description: 'Ready timestamp', optional: true },
source: {
type: 'string',
description: 'Deployment source: cli, git, redeploy, import, v0-web, etc.',
},
alias: {
type: 'array',
description: 'Assigned aliases',
items: { type: 'string', description: 'Alias domain' },
},
regions: {
type: 'array',
description: 'Deployment regions',
items: { type: 'string', description: 'Region code' },
},
inspectorUrl: { type: 'string', description: 'Vercel inspector URL' },
projectId: { type: 'string', description: 'Associated project ID' },
creator: {
type: 'object',
description: 'Creator information',
properties: {
uid: { type: 'string', description: 'Creator user ID' },
username: { type: 'string', description: 'Creator username' },
},
},
project: {
type: 'object',
description: 'Associated project',
optional: true,
properties: {
id: { type: 'string', description: 'Project ID' },
name: { type: 'string', description: 'Project name' },
framework: { type: 'string', description: 'Project framework', optional: true },
},
},
meta: {
type: 'object',
description: 'Deployment metadata (key-value strings)',
properties: {
githubCommitSha: { type: 'string', description: 'GitHub commit SHA', optional: true },
githubCommitMessage: {
type: 'string',
description: 'GitHub commit message',
optional: true,
},
githubCommitRef: { type: 'string', description: 'GitHub branch/ref', optional: true },
githubRepo: { type: 'string', description: 'GitHub repository', optional: true },
githubOrg: { type: 'string', description: 'GitHub organization', optional: true },
githubCommitAuthorName: {
type: 'string',
description: 'Commit author name',
optional: true,
},
},
},
gitSource: {
type: 'object',
description: 'Git source information',
optional: true,
properties: {
type: {
type: 'string',
description: 'Git provider type (e.g., github, gitlab, bitbucket)',
},
ref: { type: 'string', description: 'Git ref (branch or tag)' },
sha: { type: 'string', description: 'Git commit SHA' },
repoId: { type: 'string', description: 'Repository ID', optional: true },
},
},
},
}

View File

@@ -0,0 +1,135 @@
import type { ToolConfig } from '@/tools/types'
import type {
VercelGetDeploymentEventsParams,
VercelGetDeploymentEventsResponse,
} from '@/tools/vercel/types'
export const vercelGetDeploymentEventsTool: ToolConfig<
VercelGetDeploymentEventsParams,
VercelGetDeploymentEventsResponse
> = {
id: 'vercel_get_deployment_events',
name: 'Vercel Get Deployment Events',
description: 'Get build and runtime events for a Vercel deployment',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
deploymentId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The unique deployment identifier or hostname',
},
direction: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Order of events by timestamp: backward or forward (default: forward)',
},
follow: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'When set to 1, returns live events as they happen',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of events to return (-1 for all)',
},
since: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Timestamp to start pulling build logs from',
},
until: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Timestamp to stop pulling build logs at',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelGetDeploymentEventsParams) => {
const query = new URLSearchParams()
if (params.direction) query.set('direction', params.direction)
if (params.follow !== undefined) query.set('follow', String(params.follow))
if (params.limit !== undefined) query.set('limit', String(params.limit))
if (params.since !== undefined) query.set('since', String(params.since))
if (params.until !== undefined) query.set('until', String(params.until))
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v3/deployments/${params.deploymentId.trim()}/events${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params: VercelGetDeploymentEventsParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
const events = (Array.isArray(data) ? data : (data.events ?? [])).map((e: any) => ({
type: e.type ?? null,
created: e.created ?? null,
date: e.date ?? null,
text: e.text ?? e.payload?.text ?? null,
serial: e.serial ?? null,
deploymentId: e.deploymentId ?? e.payload?.deploymentId ?? null,
id: e.id ?? null,
level: e.level ?? null,
}))
return {
success: true,
output: {
events,
count: events.length,
},
}
},
outputs: {
events: {
type: 'array',
description: 'List of deployment events',
items: {
type: 'object',
properties: {
type: {
type: 'string',
description:
'Event type: delimiter, command, stdout, stderr, exit, deployment-state, middleware, middleware-invocation, edge-function-invocation, metric, report, fatal',
},
created: { type: 'number', description: 'Event creation timestamp' },
date: { type: 'number', description: 'Event date timestamp' },
text: { type: 'string', description: 'Event text content' },
serial: { type: 'string', description: 'Event serial identifier' },
deploymentId: { type: 'string', description: 'Associated deployment ID' },
id: { type: 'string', description: 'Event unique identifier' },
level: { type: 'string', description: 'Event level: error or warning' },
},
},
},
count: {
type: 'number',
description: 'Number of events returned',
},
},
}

View File

@@ -0,0 +1,94 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelGetDomainParams, VercelGetDomainResponse } from '@/tools/vercel/types'
export const vercelGetDomainTool: ToolConfig<VercelGetDomainParams, VercelGetDomainResponse> = {
id: 'vercel_get_domain',
name: 'Vercel Get Domain',
description: 'Get information about a specific domain in a Vercel account',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The domain name to retrieve',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelGetDomainParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v5/domains/${params.domain.trim()}${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params: VercelGetDomainParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
const d = data.domain ?? data
return {
success: true,
output: {
id: d.id ?? null,
name: d.name ?? null,
verified: d.verified ?? false,
createdAt: d.createdAt ?? null,
expiresAt: d.expiresAt ?? null,
serviceType: d.serviceType ?? null,
nameservers: d.nameservers ?? [],
intendedNameservers: d.intendedNameservers ?? [],
customNameservers: d.customNameservers ?? [],
renew: d.renew ?? false,
boughtAt: d.boughtAt ?? null,
transferredAt: d.transferredAt ?? null,
},
}
},
outputs: {
id: { type: 'string', description: 'Domain ID' },
name: { type: 'string', description: 'Domain name' },
verified: { type: 'boolean', description: 'Whether domain is verified' },
createdAt: { type: 'number', description: 'Creation timestamp' },
expiresAt: { type: 'number', description: 'Expiration timestamp' },
serviceType: { type: 'string', description: 'Service type (zeit.world, external, na)' },
nameservers: {
type: 'array',
description: 'Current nameservers',
items: { type: 'string' },
},
intendedNameservers: {
type: 'array',
description: 'Intended nameservers',
items: { type: 'string' },
},
customNameservers: {
type: 'array',
description: 'Custom nameservers',
items: { type: 'string' },
},
renew: { type: 'boolean', description: 'Whether auto-renewal is enabled' },
boughtAt: { type: 'number', description: 'Purchase timestamp' },
transferredAt: { type: 'number', description: 'Transfer completion timestamp' },
},
}

View File

@@ -0,0 +1,107 @@
import type { ToolConfig } from '@/tools/types'
import type {
VercelGetDomainConfigParams,
VercelGetDomainConfigResponse,
} from '@/tools/vercel/types'
export const vercelGetDomainConfigTool: ToolConfig<
VercelGetDomainConfigParams,
VercelGetDomainConfigResponse
> = {
id: 'vercel_get_domain_config',
name: 'Vercel Get Domain Config',
description: 'Get the configuration for a domain in a Vercel account',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The domain name to get configuration for',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelGetDomainConfigParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v6/domains/${params.domain.trim()}/config${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params: VercelGetDomainConfigParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const d = await response.json()
return {
success: true,
output: {
configuredBy: d.configuredBy ?? null,
acceptedChallenges: d.acceptedChallenges ?? [],
misconfigured: d.misconfigured ?? false,
recommendedIPv4: d.recommendedIPv4 ?? [],
recommendedCNAME: d.recommendedCNAME ?? [],
},
}
},
outputs: {
configuredBy: {
type: 'string',
description: 'How the domain is configured (CNAME, A, http, dns-01, or null)',
},
acceptedChallenges: {
type: 'array',
description: 'Accepted challenge types for certificate issuance (dns-01, http-01)',
items: { type: 'string' },
},
misconfigured: {
type: 'boolean',
description: 'Whether the domain is misconfigured for TLS certificate generation',
},
recommendedIPv4: {
type: 'array',
description: 'Recommended IPv4 addresses with rank values',
items: {
type: 'object',
properties: {
rank: { type: 'number', description: 'Priority rank (1 is preferred)' },
value: {
type: 'array',
description: 'IPv4 addresses',
items: { type: 'string' },
},
},
},
},
recommendedCNAME: {
type: 'array',
description: 'Recommended CNAME records with rank values',
items: {
type: 'object',
properties: {
rank: { type: 'number', description: 'Priority rank (1 is preferred)' },
value: { type: 'string', description: 'CNAME value' },
},
},
},
},
}

View File

@@ -0,0 +1,100 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelGetEdgeConfigParams, VercelGetEdgeConfigResponse } from '@/tools/vercel/types'
export const vercelGetEdgeConfigTool: ToolConfig<
VercelGetEdgeConfigParams,
VercelGetEdgeConfigResponse
> = {
id: 'vercel_get_edge_config',
name: 'Vercel Get Edge Config',
description: 'Get details about a specific Edge Config store',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
edgeConfigId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Edge Config ID to look up',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelGetEdgeConfigParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v1/edge-config/${params.edgeConfigId.trim()}${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params: VercelGetEdgeConfigParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
id: data.id ?? null,
slug: data.slug ?? null,
ownerId: data.ownerId ?? null,
digest: data.digest ?? null,
createdAt: data.createdAt ?? null,
updatedAt: data.updatedAt ?? null,
itemCount: data.itemCount ?? 0,
sizeInBytes: data.sizeInBytes ?? 0,
},
}
},
outputs: {
id: {
type: 'string',
description: 'Edge Config ID',
},
slug: {
type: 'string',
description: 'Edge Config slug',
},
ownerId: {
type: 'string',
description: 'Owner ID',
},
digest: {
type: 'string',
description: 'Content digest hash',
},
createdAt: {
type: 'number',
description: 'Creation timestamp',
},
updatedAt: {
type: 'number',
description: 'Last update timestamp',
},
itemCount: {
type: 'number',
description: 'Number of items',
},
sizeInBytes: {
type: 'number',
description: 'Size in bytes',
},
},
}

View File

@@ -0,0 +1,93 @@
import type { ToolConfig } from '@/tools/types'
import type {
VercelGetEdgeConfigItemsParams,
VercelGetEdgeConfigItemsResponse,
} from '@/tools/vercel/types'
export const vercelGetEdgeConfigItemsTool: ToolConfig<
VercelGetEdgeConfigItemsParams,
VercelGetEdgeConfigItemsResponse
> = {
id: 'vercel_get_edge_config_items',
name: 'Vercel Get Edge Config Items',
description: 'Get all items in an Edge Config store',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
edgeConfigId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Edge Config ID to get items from',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelGetEdgeConfigItemsParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v1/edge-config/${params.edgeConfigId.trim()}/items${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params: VercelGetEdgeConfigItemsParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
const rawItems = Array.isArray(data) ? data : (data.items ?? [])
const items = rawItems.map((item: any) => ({
key: item.key ?? null,
value: item.value ?? null,
description: item.description ?? null,
edgeConfigId: item.edgeConfigId ?? null,
createdAt: item.createdAt ?? null,
updatedAt: item.updatedAt ?? null,
}))
return {
success: true,
output: {
items,
count: items.length,
},
}
},
outputs: {
items: {
type: 'array',
description: 'List of Edge Config items',
items: {
type: 'object',
properties: {
key: { type: 'string', description: 'Item key' },
value: { type: 'json', description: 'Item value' },
description: { type: 'string', description: 'Item description' },
edgeConfigId: { type: 'string', description: 'Parent Edge Config ID' },
createdAt: { type: 'number', description: 'Creation timestamp' },
updatedAt: { type: 'number', description: 'Last update timestamp' },
},
},
},
count: {
type: 'number',
description: 'Number of items returned',
},
},
}

View File

@@ -0,0 +1,99 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelGetEnvVarsParams, VercelGetEnvVarsResponse } from '@/tools/vercel/types'
export const vercelGetEnvVarsTool: ToolConfig<VercelGetEnvVarsParams, VercelGetEnvVarsResponse> = {
id: 'vercel_get_env_vars',
name: 'Vercel Get Environment Variables',
description: 'Retrieve environment variables for a Vercel project',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
projectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Project ID or name',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelGetEnvVarsParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v10/projects/${params.projectId.trim()}/env${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params: VercelGetEnvVarsParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
const envs = (data.envs ?? []).map((e: any) => ({
id: e.id,
key: e.key,
value: e.value ?? '',
type: e.type ?? 'plain',
target: e.target ?? [],
gitBranch: e.gitBranch ?? null,
comment: e.comment ?? null,
}))
return {
success: true,
output: {
envs,
count: envs.length,
},
}
},
outputs: {
envs: {
type: 'array',
description: 'List of environment variables',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Environment variable ID' },
key: { type: 'string', description: 'Variable name' },
value: { type: 'string', description: 'Variable value' },
type: {
type: 'string',
description: 'Variable type (secret, system, encrypted, plain, sensitive)',
},
target: {
type: 'array',
description: 'Target environments',
items: { type: 'string', description: 'Environment name' },
},
gitBranch: { type: 'string', description: 'Git branch filter', optional: true },
comment: {
type: 'string',
description: 'Comment providing context for the variable',
optional: true,
},
},
},
},
count: {
type: 'number',
description: 'Number of environment variables returned',
},
},
}

View File

@@ -0,0 +1,89 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelGetProjectParams, VercelGetProjectResponse } from '@/tools/vercel/types'
export const vercelGetProjectTool: ToolConfig<VercelGetProjectParams, VercelGetProjectResponse> = {
id: 'vercel_get_project',
name: 'Vercel Get Project',
description: 'Get details of a specific Vercel project',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
projectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Project ID or name',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelGetProjectParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v9/projects/${params.projectId.trim()}${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params: VercelGetProjectParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
id: data.id,
name: data.name,
framework: data.framework ?? null,
createdAt: data.createdAt,
updatedAt: data.updatedAt,
domains: data.domains ?? [],
link: data.link
? {
type: data.link.type,
repo: data.link.repo,
org: data.link.org,
}
: null,
},
}
},
outputs: {
id: { type: 'string', description: 'Project ID' },
name: { type: 'string', description: 'Project name' },
framework: { type: 'string', description: 'Project framework', optional: true },
createdAt: { type: 'number', description: 'Creation timestamp' },
updatedAt: { type: 'number', description: 'Last updated timestamp' },
domains: {
type: 'array',
description: 'Project domains',
items: { type: 'string', description: 'Domain' },
},
link: {
type: 'object',
description: 'Git repository connection',
optional: true,
properties: {
type: { type: 'string', description: 'Repository type (github, gitlab, bitbucket)' },
repo: { type: 'string', description: 'Repository name' },
org: { type: 'string', description: 'Organization or owner' },
},
},
},
}

View File

@@ -0,0 +1,98 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelGetTeamParams, VercelGetTeamResponse } from '@/tools/vercel/types'
export const vercelGetTeamTool: ToolConfig<VercelGetTeamParams, VercelGetTeamResponse> = {
id: 'vercel_get_team',
name: 'Vercel Get Team',
description: 'Get information about a specific Vercel team',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
teamId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The team ID to retrieve',
},
},
request: {
url: (params: VercelGetTeamParams) => `https://api.vercel.com/v2/teams/${params.teamId.trim()}`,
method: 'GET',
headers: (params: VercelGetTeamParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const d = await response.json()
return {
success: true,
output: {
id: d.id ?? null,
slug: d.slug ?? null,
name: d.name ?? null,
avatar: d.avatar ?? null,
description: d.description ?? null,
createdAt: d.createdAt ?? null,
updatedAt: d.updatedAt ?? null,
creatorId: d.creatorId ?? null,
membership: d.membership
? {
uid: d.membership.uid ?? null,
teamId: d.membership.teamId ?? null,
role: d.membership.role ?? null,
confirmed: d.membership.confirmed ?? false,
created: d.membership.created ?? null,
createdAt: d.membership.createdAt ?? null,
accessRequestedAt: d.membership.accessRequestedAt ?? null,
teamRoles: d.membership.teamRoles ?? [],
teamPermissions: d.membership.teamPermissions ?? [],
}
: null,
},
}
},
outputs: {
id: { type: 'string', description: 'Team ID' },
slug: { type: 'string', description: 'Team slug' },
name: { type: 'string', description: 'Team name' },
avatar: { type: 'string', description: 'Avatar file ID' },
description: { type: 'string', description: 'Short team description' },
createdAt: { type: 'number', description: 'Creation timestamp in milliseconds' },
updatedAt: { type: 'number', description: 'Last update timestamp in milliseconds' },
creatorId: { type: 'string', description: 'User ID of team creator' },
membership: {
type: 'object',
description: 'Current user membership details',
properties: {
uid: { type: 'string', description: 'User ID of the member' },
teamId: { type: 'string', description: 'Team ID' },
role: { type: 'string', description: 'Membership role' },
confirmed: { type: 'boolean', description: 'Whether membership is confirmed' },
created: { type: 'number', description: 'Membership creation timestamp' },
createdAt: { type: 'number', description: 'Membership creation timestamp (milliseconds)' },
accessRequestedAt: { type: 'number', description: 'When access was requested' },
teamRoles: {
type: 'array',
description: 'Team role assignments',
items: { type: 'string', description: 'Role name' },
},
teamPermissions: {
type: 'array',
description: 'Team permission assignments',
items: { type: 'string', description: 'Permission name' },
},
},
},
},
}

View File

@@ -0,0 +1,73 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelGetUserParams, VercelGetUserResponse } from '@/tools/vercel/types'
export const vercelGetUserTool: ToolConfig<VercelGetUserParams, VercelGetUserResponse> = {
id: 'vercel_get_user',
name: 'Vercel Get User',
description: 'Get information about the authenticated Vercel user',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
},
request: {
url: () => 'https://api.vercel.com/v2/user',
method: 'GET',
headers: (params: VercelGetUserParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
const d = data.user ?? data
return {
success: true,
output: {
id: d.id ?? null,
email: d.email ?? null,
username: d.username ?? null,
name: d.name ?? null,
avatar: d.avatar ?? null,
defaultTeamId: d.defaultTeamId ?? null,
createdAt: d.createdAt ?? null,
stagingPrefix: d.stagingPrefix ?? null,
softBlock: d.softBlock
? {
blockedAt: d.softBlock.blockedAt ?? null,
reason: d.softBlock.reason ?? null,
}
: null,
hasTrialAvailable: d.hasTrialAvailable ?? null,
},
}
},
outputs: {
id: { type: 'string', description: 'User ID' },
email: { type: 'string', description: 'User email' },
username: { type: 'string', description: 'Username' },
name: { type: 'string', description: 'Display name' },
avatar: { type: 'string', description: 'SHA1 hash of the avatar' },
defaultTeamId: { type: 'string', description: 'Default team ID' },
createdAt: { type: 'number', description: 'Account creation timestamp in milliseconds' },
stagingPrefix: { type: 'string', description: 'Prefix for preview deployment URLs' },
softBlock: {
type: 'object',
description: 'Account restriction details if blocked',
properties: {
blockedAt: { type: 'number', description: 'When the account was blocked' },
reason: { type: 'string', description: 'Reason for the block' },
},
},
hasTrialAvailable: { type: 'boolean', description: 'Whether a trial is available' },
},
}

View File

@@ -0,0 +1,126 @@
// Deployment tools
// Domain tools
import { vercelAddDomainTool } from '@/tools/vercel/add_domain'
// Project tools
import { vercelAddProjectDomainTool } from '@/tools/vercel/add_project_domain'
import { vercelCancelDeploymentTool } from '@/tools/vercel/cancel_deployment'
// Alias tools
import { vercelCreateAliasTool } from '@/tools/vercel/create_alias'
// Check tools
import { vercelCreateCheckTool } from '@/tools/vercel/create_check'
import { vercelCreateDeploymentTool } from '@/tools/vercel/create_deployment'
// DNS tools
import { vercelCreateDnsRecordTool } from '@/tools/vercel/create_dns_record'
// Edge Config tools
import { vercelCreateEdgeConfigTool } from '@/tools/vercel/create_edge_config'
// Environment variable tools
import { vercelCreateEnvVarTool } from '@/tools/vercel/create_env_var'
import { vercelCreateProjectTool } from '@/tools/vercel/create_project'
// Webhook tools
import { vercelCreateWebhookTool } from '@/tools/vercel/create_webhook'
import { vercelDeleteAliasTool } from '@/tools/vercel/delete_alias'
import { vercelDeleteDeploymentTool } from '@/tools/vercel/delete_deployment'
import { vercelDeleteDnsRecordTool } from '@/tools/vercel/delete_dns_record'
import { vercelDeleteDomainTool } from '@/tools/vercel/delete_domain'
import { vercelDeleteEnvVarTool } from '@/tools/vercel/delete_env_var'
import { vercelDeleteProjectTool } from '@/tools/vercel/delete_project'
import { vercelDeleteWebhookTool } from '@/tools/vercel/delete_webhook'
import { vercelGetAliasTool } from '@/tools/vercel/get_alias'
import { vercelGetCheckTool } from '@/tools/vercel/get_check'
import { vercelGetDeploymentTool } from '@/tools/vercel/get_deployment'
import { vercelGetDeploymentEventsTool } from '@/tools/vercel/get_deployment_events'
import { vercelGetDomainTool } from '@/tools/vercel/get_domain'
import { vercelGetDomainConfigTool } from '@/tools/vercel/get_domain_config'
import { vercelGetEdgeConfigTool } from '@/tools/vercel/get_edge_config'
import { vercelGetEdgeConfigItemsTool } from '@/tools/vercel/get_edge_config_items'
import { vercelGetEnvVarsTool } from '@/tools/vercel/get_env_vars'
import { vercelGetProjectTool } from '@/tools/vercel/get_project'
// Team & User tools
import { vercelGetTeamTool } from '@/tools/vercel/get_team'
import { vercelGetUserTool } from '@/tools/vercel/get_user'
import { vercelListAliasesTool } from '@/tools/vercel/list_aliases'
import { vercelListChecksTool } from '@/tools/vercel/list_checks'
import { vercelListDeploymentFilesTool } from '@/tools/vercel/list_deployment_files'
import { vercelListDeploymentsTool } from '@/tools/vercel/list_deployments'
import { vercelListDnsRecordsTool } from '@/tools/vercel/list_dns_records'
import { vercelListDomainsTool } from '@/tools/vercel/list_domains'
import { vercelListEdgeConfigsTool } from '@/tools/vercel/list_edge_configs'
import { vercelListProjectDomainsTool } from '@/tools/vercel/list_project_domains'
import { vercelListProjectsTool } from '@/tools/vercel/list_projects'
import { vercelListTeamMembersTool } from '@/tools/vercel/list_team_members'
import { vercelListTeamsTool } from '@/tools/vercel/list_teams'
import { vercelListWebhooksTool } from '@/tools/vercel/list_webhooks'
import { vercelPauseProjectTool } from '@/tools/vercel/pause_project'
import { vercelRemoveProjectDomainTool } from '@/tools/vercel/remove_project_domain'
import { vercelRerequestCheckTool } from '@/tools/vercel/rerequest_check'
import { vercelUnpauseProjectTool } from '@/tools/vercel/unpause_project'
import { vercelUpdateCheckTool } from '@/tools/vercel/update_check'
import { vercelUpdateEdgeConfigItemsTool } from '@/tools/vercel/update_edge_config_items'
import { vercelUpdateEnvVarTool } from '@/tools/vercel/update_env_var'
import { vercelUpdateProjectTool } from '@/tools/vercel/update_project'
export {
// Deployments
vercelListDeploymentsTool,
vercelGetDeploymentTool,
vercelCreateDeploymentTool,
vercelCancelDeploymentTool,
vercelDeleteDeploymentTool,
vercelGetDeploymentEventsTool,
vercelListDeploymentFilesTool,
// Projects
vercelListProjectsTool,
vercelGetProjectTool,
vercelCreateProjectTool,
vercelUpdateProjectTool,
vercelDeleteProjectTool,
vercelPauseProjectTool,
vercelUnpauseProjectTool,
vercelListProjectDomainsTool,
vercelAddProjectDomainTool,
vercelRemoveProjectDomainTool,
// Environment Variables
vercelGetEnvVarsTool,
vercelCreateEnvVarTool,
vercelUpdateEnvVarTool,
vercelDeleteEnvVarTool,
// Domains
vercelListDomainsTool,
vercelGetDomainTool,
vercelAddDomainTool,
vercelDeleteDomainTool,
vercelGetDomainConfigTool,
// DNS
vercelListDnsRecordsTool,
vercelCreateDnsRecordTool,
vercelDeleteDnsRecordTool,
// Aliases
vercelListAliasesTool,
vercelGetAliasTool,
vercelCreateAliasTool,
vercelDeleteAliasTool,
// Edge Config
vercelListEdgeConfigsTool,
vercelGetEdgeConfigTool,
vercelCreateEdgeConfigTool,
vercelGetEdgeConfigItemsTool,
vercelUpdateEdgeConfigItemsTool,
// Teams & User
vercelListTeamsTool,
vercelGetTeamTool,
vercelListTeamMembersTool,
vercelGetUserTool,
// Webhooks
vercelListWebhooksTool,
vercelCreateWebhookTool,
vercelDeleteWebhookTool,
// Checks
vercelCreateCheckTool,
vercelGetCheckTool,
vercelListChecksTool,
vercelUpdateCheckTool,
vercelRerequestCheckTool,
}
export * from './types'

View File

@@ -0,0 +1,107 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelListAliasesParams, VercelListAliasesResponse } from '@/tools/vercel/types'
export const vercelListAliasesTool: ToolConfig<VercelListAliasesParams, VercelListAliasesResponse> =
{
id: 'vercel_list_aliases',
name: 'Vercel List Aliases',
description: 'List aliases for a Vercel project or team',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
projectId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter aliases by project ID',
},
domain: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter aliases by domain',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of aliases to return',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelListAliasesParams) => {
const query = new URLSearchParams()
if (params.projectId) query.set('projectId', params.projectId.trim())
if (params.domain) query.set('domain', params.domain.trim())
if (params.limit) query.set('limit', String(params.limit))
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v4/aliases${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params: VercelListAliasesParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
const aliases = (data.aliases ?? []).map((a: any) => ({
uid: a.uid ?? null,
alias: a.alias ?? null,
deploymentId: a.deploymentId ?? null,
projectId: a.projectId ?? null,
createdAt: a.createdAt ?? null,
updatedAt: a.updatedAt ?? null,
}))
return {
success: true,
output: {
aliases,
count: aliases.length,
hasMore: data.pagination?.next != null,
},
}
},
outputs: {
aliases: {
type: 'array',
description: 'List of aliases',
items: {
type: 'object',
properties: {
uid: { type: 'string', description: 'Alias ID' },
alias: { type: 'string', description: 'Alias hostname' },
deploymentId: { type: 'string', description: 'Associated deployment ID' },
projectId: { type: 'string', description: 'Associated project ID' },
createdAt: { type: 'number', description: 'Creation timestamp in milliseconds' },
updatedAt: { type: 'number', description: 'Last update timestamp in milliseconds' },
},
},
},
count: {
type: 'number',
description: 'Number of aliases returned',
},
hasMore: {
type: 'boolean',
description: 'Whether more aliases are available',
},
},
}

View File

@@ -0,0 +1,99 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelListChecksParams, VercelListChecksResponse } from '@/tools/vercel/types'
export const vercelListChecksTool: ToolConfig<VercelListChecksParams, VercelListChecksResponse> = {
id: 'vercel_list_checks',
name: 'Vercel List Checks',
description: 'List all checks for a deployment',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
deploymentId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Deployment ID to list checks for',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelListChecksParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v1/deployments/${params.deploymentId.trim()}/checks${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params: VercelListChecksParams) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
const checks = (data.checks ?? []).map((check: Record<string, unknown>) => ({
id: check.id,
name: check.name,
status: check.status ?? 'registered',
conclusion: check.conclusion ?? null,
blocking: check.blocking ?? false,
deploymentId: check.deploymentId,
integrationId: check.integrationId ?? null,
externalId: check.externalId ?? null,
detailsUrl: check.detailsUrl ?? null,
path: check.path ?? null,
rerequestable: check.rerequestable ?? false,
createdAt: check.createdAt,
updatedAt: check.updatedAt,
startedAt: check.startedAt ?? null,
completedAt: check.completedAt ?? null,
}))
return {
success: true,
output: {
checks,
count: checks.length,
},
}
},
outputs: {
checks: {
type: 'array',
description: 'List of deployment checks',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Check ID' },
name: { type: 'string', description: 'Check name' },
status: { type: 'string', description: 'Check status' },
conclusion: { type: 'string', description: 'Check conclusion' },
blocking: { type: 'boolean', description: 'Whether the check blocks the deployment' },
deploymentId: { type: 'string', description: 'Associated deployment ID' },
integrationId: { type: 'string', description: 'Associated integration ID' },
externalId: { type: 'string', description: 'External identifier' },
detailsUrl: { type: 'string', description: 'URL with details about the check' },
path: { type: 'string', description: 'Page path being checked' },
rerequestable: { type: 'boolean', description: 'Whether the check can be rerequested' },
createdAt: { type: 'number', description: 'Creation timestamp' },
updatedAt: { type: 'number', description: 'Last update timestamp' },
startedAt: { type: 'number', description: 'Start timestamp' },
completedAt: { type: 'number', description: 'Completion timestamp' },
},
},
},
count: { type: 'number', description: 'Total number of checks' },
},
}

View File

@@ -0,0 +1,114 @@
import type { ToolConfig } from '@/tools/types'
import type {
VercelListDeploymentFilesParams,
VercelListDeploymentFilesResponse,
} from '@/tools/vercel/types'
export const vercelListDeploymentFilesTool: ToolConfig<
VercelListDeploymentFilesParams,
VercelListDeploymentFilesResponse
> = {
id: 'vercel_list_deployment_files',
name: 'Vercel List Deployment Files',
description: 'List files in a Vercel deployment',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
deploymentId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The deployment ID to list files for',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelListDeploymentFilesParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v6/deployments/${params.deploymentId.trim()}/files${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params: VercelListDeploymentFilesParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
const files = (Array.isArray(data) ? data : (data.files ?? [])).map((f: any) => ({
name: f.name ?? null,
type: f.type ?? null,
uid: f.uid ?? null,
mode: f.mode ?? null,
contentType: f.contentType ?? null,
children: f.children ?? [],
}))
return {
success: true,
output: {
files,
count: files.length,
},
}
},
outputs: {
files: {
type: 'array',
description: 'List of deployment files',
items: {
type: 'object',
properties: {
name: { type: 'string', description: 'The name of the file tree entry' },
type: {
type: 'string',
description: 'File type: directory, file, symlink, lambda, middleware, or invalid',
},
uid: {
type: 'string',
description: 'Unique file identifier (only valid for file type)',
optional: true,
},
mode: { type: 'number', description: 'File mode indicating file type and permissions' },
contentType: {
type: 'string',
description: 'Content-type of the file (only valid for file type)',
optional: true,
},
children: {
type: 'array',
description: 'Child files of the directory (only valid for directory type)',
items: {
type: 'object',
properties: {
name: { type: 'string', description: 'File name' },
type: { type: 'string', description: 'Entry type' },
uid: { type: 'string', description: 'File identifier', optional: true },
},
},
},
},
},
},
count: {
type: 'number',
description: 'Number of files returned',
},
},
}

View File

@@ -0,0 +1,170 @@
import type { ToolConfig } from '@/tools/types'
import type {
VercelListDeploymentsParams,
VercelListDeploymentsResponse,
} from '@/tools/vercel/types'
export const vercelListDeploymentsTool: ToolConfig<
VercelListDeploymentsParams,
VercelListDeploymentsResponse
> = {
id: 'vercel_list_deployments',
name: 'Vercel List Deployments',
description: 'List deployments for a Vercel project or team',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
projectId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter deployments by project ID or name',
},
target: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by environment: production or staging',
},
state: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by state: BUILDING, ERROR, INITIALIZING, QUEUED, READY, CANCELED',
},
app: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by deployment name',
},
since: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Get deployments created after this JavaScript timestamp',
},
until: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Get deployments created before this JavaScript timestamp',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of deployments to return per request',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelListDeploymentsParams) => {
const query = new URLSearchParams()
if (params.projectId) query.set('projectId', params.projectId.trim())
if (params.target) query.set('target', params.target)
if (params.state) query.set('state', params.state)
if (params.app) query.set('app', params.app.trim())
if (params.since) query.set('since', String(params.since))
if (params.until) query.set('until', String(params.until))
if (params.limit) query.set('limit', String(params.limit))
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v6/deployments${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params: VercelListDeploymentsParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
const deployments = (data.deployments ?? []).map((d: any) => ({
uid: d.uid,
name: d.name,
url: d.url ?? null,
state: d.state ?? d.readyState ?? 'UNKNOWN',
target: d.target ?? null,
created: d.created ?? d.createdAt,
projectId: d.projectId ?? '',
source: d.source ?? '',
inspectorUrl: d.inspectorUrl ?? '',
creator: {
uid: d.creator?.uid ?? '',
email: d.creator?.email ?? '',
username: d.creator?.username ?? '',
},
meta: d.meta ?? {},
}))
return {
success: true,
output: {
deployments,
count: deployments.length,
hasMore: data.pagination?.next != null,
},
}
},
outputs: {
deployments: {
type: 'array',
description: 'List of deployments',
items: {
type: 'object',
properties: {
uid: { type: 'string', description: 'Unique deployment identifier' },
name: { type: 'string', description: 'Deployment name' },
url: { type: 'string', description: 'Deployment URL', optional: true },
state: {
type: 'string',
description:
'Deployment state: BUILDING, ERROR, INITIALIZING, QUEUED, READY, CANCELED, DELETED',
},
target: { type: 'string', description: 'Target environment', optional: true },
created: { type: 'number', description: 'Creation timestamp' },
projectId: { type: 'string', description: 'Associated project ID' },
source: {
type: 'string',
description:
'Deployment source: api-trigger-git-deploy, cli, clone/repo, git, import, import/repo, redeploy, v0-web',
},
inspectorUrl: { type: 'string', description: 'Vercel inspector URL' },
creator: {
type: 'object',
description: 'Creator information',
properties: {
uid: { type: 'string', description: 'Creator user ID' },
email: { type: 'string', description: 'Creator email' },
username: { type: 'string', description: 'Creator username' },
},
},
meta: { type: 'object', description: 'Git provider metadata (key-value strings)' },
},
},
},
count: {
type: 'number',
description: 'Number of deployments returned',
},
hasMore: {
type: 'boolean',
description: 'Whether more deployments are available',
},
},
}

View File

@@ -0,0 +1,116 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelListDnsRecordsParams, VercelListDnsRecordsResponse } from '@/tools/vercel/types'
export const vercelListDnsRecordsTool: ToolConfig<
VercelListDnsRecordsParams,
VercelListDnsRecordsResponse
> = {
id: 'vercel_list_dns_records',
name: 'Vercel List DNS Records',
description: 'List all DNS records for a domain in a Vercel account',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The domain name to list records for',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of records to return',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelListDnsRecordsParams) => {
const query = new URLSearchParams()
if (params.limit) query.set('limit', String(params.limit))
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v4/domains/${params.domain.trim()}/records${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params: VercelListDnsRecordsParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
const records = (data.records ?? []).map((r: any) => ({
id: r.id ?? null,
slug: r.slug ?? null,
name: r.name ?? null,
type: r.type ?? null,
value: r.value ?? null,
ttl: r.ttl ?? null,
mxPriority: r.mxPriority ?? null,
priority: r.priority ?? null,
creator: r.creator ?? null,
createdAt: r.createdAt ?? null,
updatedAt: r.updatedAt ?? null,
comment: r.comment ?? null,
}))
return {
success: true,
output: {
records,
count: records.length,
hasMore: data.pagination?.next != null,
},
}
},
outputs: {
records: {
type: 'array',
description: 'List of DNS records',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Record ID' },
slug: { type: 'string', description: 'Record slug' },
name: { type: 'string', description: 'Record name' },
type: {
type: 'string',
description: 'Record type (A, AAAA, ALIAS, CAA, CNAME, HTTPS, MX, SRV, TXT, NS)',
},
value: { type: 'string', description: 'Record value' },
ttl: { type: 'number', description: 'Time to live in seconds' },
mxPriority: { type: 'number', description: 'MX record priority' },
priority: { type: 'number', description: 'Record priority' },
creator: { type: 'string', description: 'Creator identifier' },
createdAt: { type: 'number', description: 'Creation timestamp' },
updatedAt: { type: 'number', description: 'Last update timestamp' },
comment: { type: 'string', description: 'Record comment' },
},
},
},
count: {
type: 'number',
description: 'Number of records returned',
},
hasMore: {
type: 'boolean',
description: 'Whether more records are available',
},
},
}

View File

@@ -0,0 +1,109 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelListDomainsParams, VercelListDomainsResponse } from '@/tools/vercel/types'
export const vercelListDomainsTool: ToolConfig<VercelListDomainsParams, VercelListDomainsResponse> =
{
id: 'vercel_list_domains',
name: 'Vercel List Domains',
description: 'List all domains in a Vercel account or team',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of domains to return (default 20)',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelListDomainsParams) => {
const query = new URLSearchParams()
if (params.limit) query.set('limit', String(params.limit))
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v5/domains${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params: VercelListDomainsParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
const domains = (data.domains ?? []).map((d: any) => ({
id: d.id,
name: d.name,
verified: d.verified ?? false,
createdAt: d.createdAt,
expiresAt: d.expiresAt ?? null,
serviceType: d.serviceType ?? 'external',
nameservers: d.nameservers ?? [],
intendedNameservers: d.intendedNameservers ?? [],
renew: d.renew ?? false,
boughtAt: d.boughtAt ?? null,
}))
return {
success: true,
output: {
domains,
count: domains.length,
hasMore: data.pagination?.next != null,
},
}
},
outputs: {
domains: {
type: 'array',
description: 'List of domains',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Domain ID' },
name: { type: 'string', description: 'Domain name' },
verified: { type: 'boolean', description: 'Whether domain is verified' },
createdAt: { type: 'number', description: 'Creation timestamp' },
expiresAt: { type: 'number', description: 'Expiration timestamp' },
serviceType: { type: 'string', description: 'Service type (zeit.world, external, na)' },
nameservers: {
type: 'array',
description: 'Current nameservers',
items: { type: 'string' },
},
intendedNameservers: {
type: 'array',
description: 'Intended nameservers',
items: { type: 'string' },
},
renew: { type: 'boolean', description: 'Whether auto-renewal is enabled' },
boughtAt: { type: 'number', description: 'Purchase timestamp' },
},
},
},
count: {
type: 'number',
description: 'Number of domains returned',
},
hasMore: {
type: 'boolean',
description: 'Whether more domains are available',
},
},
}

View File

@@ -0,0 +1,91 @@
import type { ToolConfig } from '@/tools/types'
import type {
VercelListEdgeConfigsParams,
VercelListEdgeConfigsResponse,
} from '@/tools/vercel/types'
export const vercelListEdgeConfigsTool: ToolConfig<
VercelListEdgeConfigsParams,
VercelListEdgeConfigsResponse
> = {
id: 'vercel_list_edge_configs',
name: 'Vercel List Edge Configs',
description: 'List all Edge Config stores for a team',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelListEdgeConfigsParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v1/edge-config${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params: VercelListEdgeConfigsParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
const items = Array.isArray(data) ? data : (data.edgeConfigs ?? [])
const edgeConfigs = items.map((ec: any) => ({
id: ec.id ?? null,
slug: ec.slug ?? null,
ownerId: ec.ownerId ?? null,
digest: ec.digest ?? null,
createdAt: ec.createdAt ?? null,
updatedAt: ec.updatedAt ?? null,
itemCount: ec.itemCount ?? 0,
sizeInBytes: ec.sizeInBytes ?? 0,
}))
return {
success: true,
output: {
edgeConfigs,
count: edgeConfigs.length,
},
}
},
outputs: {
edgeConfigs: {
type: 'array',
description: 'List of Edge Config stores',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Edge Config ID' },
slug: { type: 'string', description: 'Edge Config slug' },
ownerId: { type: 'string', description: 'Owner ID' },
digest: { type: 'string', description: 'Content digest hash' },
createdAt: { type: 'number', description: 'Creation timestamp' },
updatedAt: { type: 'number', description: 'Last update timestamp' },
itemCount: { type: 'number', description: 'Number of items' },
sizeInBytes: { type: 'number', description: 'Size in bytes' },
},
},
},
count: {
type: 'number',
description: 'Number of Edge Configs returned',
},
},
}

View File

@@ -0,0 +1,116 @@
import type { ToolConfig } from '@/tools/types'
import type {
VercelListProjectDomainsParams,
VercelListProjectDomainsResponse,
} from '@/tools/vercel/types'
export const vercelListProjectDomainsTool: ToolConfig<
VercelListProjectDomainsParams,
VercelListProjectDomainsResponse
> = {
id: 'vercel_list_project_domains',
name: 'Vercel List Project Domains',
description: 'List all domains for a Vercel project',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
projectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Project ID or name',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of domains to return',
},
},
request: {
url: (params: VercelListProjectDomainsParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
if (params.limit) query.set('limit', String(params.limit))
const qs = query.toString()
return `https://api.vercel.com/v9/projects/${params.projectId.trim()}/domains${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params: VercelListProjectDomainsParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
const domains = (data.domains ?? []).map(
(d: {
name: string
apexName: string
redirect: string | null
redirectStatusCode: number | null
verified: boolean
gitBranch: string | null
createdAt: number
updatedAt: number
}) => ({
name: d.name,
apexName: d.apexName,
redirect: d.redirect ?? null,
redirectStatusCode: d.redirectStatusCode ?? null,
verified: d.verified,
gitBranch: d.gitBranch ?? null,
createdAt: d.createdAt,
updatedAt: d.updatedAt,
})
)
return {
success: true,
output: {
domains,
count: domains.length,
hasMore: data.pagination?.next != null,
},
}
},
outputs: {
domains: {
type: 'array',
description: 'List of project domains',
items: {
type: 'object',
properties: {
name: { type: 'string', description: 'Domain name' },
apexName: { type: 'string', description: 'Apex domain name' },
redirect: { type: 'string', description: 'Redirect target', optional: true },
redirectStatusCode: {
type: 'number',
description: 'Redirect status code',
optional: true,
},
verified: { type: 'boolean', description: 'Whether the domain is verified' },
gitBranch: { type: 'string', description: 'Git branch for the domain', optional: true },
createdAt: { type: 'number', description: 'Creation timestamp' },
updatedAt: { type: 'number', description: 'Last updated timestamp' },
},
},
},
count: { type: 'number', description: 'Number of domains returned' },
hasMore: { type: 'boolean', description: 'Whether more domains are available' },
},
}

View File

@@ -0,0 +1,106 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelListProjectsParams, VercelListProjectsResponse } from '@/tools/vercel/types'
export const vercelListProjectsTool: ToolConfig<
VercelListProjectsParams,
VercelListProjectsResponse
> = {
id: 'vercel_list_projects',
name: 'Vercel List Projects',
description: 'List all projects in a Vercel team or account',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
search: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Search projects by name',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of projects to return',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelListProjectsParams) => {
const query = new URLSearchParams()
if (params.search) query.set('search', params.search)
if (params.limit) query.set('limit', String(params.limit))
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v10/projects${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params: VercelListProjectsParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
const projects = (data.projects ?? []).map((p: any) => ({
id: p.id,
name: p.name,
framework: p.framework ?? null,
createdAt: p.createdAt,
updatedAt: p.updatedAt,
domains: p.domains ?? [],
}))
return {
success: true,
output: {
projects,
count: projects.length,
hasMore: data.pagination?.next != null,
},
}
},
outputs: {
projects: {
type: 'array',
description: 'List of projects',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Project ID' },
name: { type: 'string', description: 'Project name' },
framework: { type: 'string', description: 'Framework', optional: true },
createdAt: { type: 'number', description: 'Creation timestamp' },
updatedAt: { type: 'number', description: 'Last updated timestamp' },
domains: {
type: 'array',
description: 'Project domains',
items: { type: 'string', description: 'Domain' },
},
},
},
},
count: {
type: 'number',
description: 'Number of projects returned',
},
hasMore: {
type: 'boolean',
description: 'Whether more projects are available',
},
},
}

View File

@@ -0,0 +1,151 @@
import type { ToolConfig } from '@/tools/types'
import type {
VercelListTeamMembersParams,
VercelListTeamMembersResponse,
} from '@/tools/vercel/types'
export const vercelListTeamMembersTool: ToolConfig<
VercelListTeamMembersParams,
VercelListTeamMembersResponse
> = {
id: 'vercel_list_team_members',
name: 'Vercel List Team Members',
description: 'List all members of a Vercel team',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
teamId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The team ID to list members for',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of members to return',
},
role: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Filter by role (OWNER, MEMBER, DEVELOPER, SECURITY, BILLING, VIEWER, VIEWER_FOR_PLUS, CONTRIBUTOR)',
},
since: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Timestamp in milliseconds to only include members added since then',
},
until: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Timestamp in milliseconds to only include members added until then',
},
search: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Search team members by their name, username, and email',
},
},
request: {
url: (params: VercelListTeamMembersParams) => {
const query = new URLSearchParams()
if (params.limit) query.set('limit', String(params.limit))
if (params.role) query.set('role', params.role.trim())
if (params.since) query.set('since', String(params.since))
if (params.until) query.set('until', String(params.until))
if (params.search) query.set('search', params.search.trim())
const qs = query.toString()
return `https://api.vercel.com/v3/teams/${params.teamId.trim()}/members${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params: VercelListTeamMembersParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
const members = (data.members ?? []).map((m: any) => ({
uid: m.uid ?? null,
email: m.email ?? null,
username: m.username ?? null,
name: m.name ?? null,
avatar: m.avatar ?? null,
role: m.role ?? null,
confirmed: m.confirmed ?? false,
createdAt: m.createdAt ?? null,
joinedFrom: m.joinedFrom
? {
origin: m.joinedFrom.origin ?? null,
}
: null,
}))
return {
success: true,
output: {
members,
count: members.length,
pagination: data.pagination
? {
hasNext: data.pagination.hasNext ?? false,
count: data.pagination.count ?? 0,
}
: null,
},
}
},
outputs: {
members: {
type: 'array',
description: 'List of team members',
items: {
type: 'object',
properties: {
uid: { type: 'string', description: 'Member user ID' },
email: { type: 'string', description: 'Member email' },
username: { type: 'string', description: 'Member username' },
name: { type: 'string', description: 'Member full name' },
avatar: { type: 'string', description: 'Avatar file ID' },
role: { type: 'string', description: 'Member role' },
confirmed: { type: 'boolean', description: 'Whether membership is confirmed' },
createdAt: { type: 'number', description: 'Join timestamp in milliseconds' },
joinedFrom: {
type: 'object',
description: 'Origin of how the member joined',
properties: {
origin: { type: 'string', description: 'Join origin identifier' },
},
},
},
},
},
count: {
type: 'number',
description: 'Number of members returned',
},
pagination: {
type: 'object',
description: 'Pagination information',
properties: {
hasNext: { type: 'boolean', description: 'Whether there are more pages' },
count: { type: 'number', description: 'Items in current page' },
},
},
},
}

View File

@@ -0,0 +1,132 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelListTeamsParams, VercelListTeamsResponse } from '@/tools/vercel/types'
export const vercelListTeamsTool: ToolConfig<VercelListTeamsParams, VercelListTeamsResponse> = {
id: 'vercel_list_teams',
name: 'Vercel List Teams',
description: 'List all teams in a Vercel account',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of teams to return',
},
since: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Timestamp in milliseconds to only include teams created since then',
},
until: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Timestamp in milliseconds to only include teams created until then',
},
},
request: {
url: (params: VercelListTeamsParams) => {
const query = new URLSearchParams()
if (params.limit) query.set('limit', String(params.limit))
if (params.since) query.set('since', String(params.since))
if (params.until) query.set('until', String(params.until))
const qs = query.toString()
return `https://api.vercel.com/v2/teams${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params: VercelListTeamsParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
const teams = (data.teams ?? []).map((t: any) => ({
id: t.id ?? null,
slug: t.slug ?? null,
name: t.name ?? null,
avatar: t.avatar ?? null,
createdAt: t.createdAt ?? null,
updatedAt: t.updatedAt ?? null,
creatorId: t.creatorId ?? null,
membership: t.membership
? {
role: t.membership.role ?? null,
confirmed: t.membership.confirmed ?? false,
created: t.membership.created ?? null,
uid: t.membership.uid ?? null,
teamId: t.membership.teamId ?? null,
}
: null,
}))
return {
success: true,
output: {
teams,
count: teams.length,
pagination: data.pagination
? {
count: data.pagination.count ?? 0,
next: data.pagination.next ?? null,
prev: data.pagination.prev ?? null,
}
: null,
},
}
},
outputs: {
teams: {
type: 'array',
description: 'List of teams',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Team ID' },
slug: { type: 'string', description: 'Team slug' },
name: { type: 'string', description: 'Team name' },
avatar: { type: 'string', description: 'Avatar file ID' },
createdAt: { type: 'number', description: 'Creation timestamp in milliseconds' },
updatedAt: { type: 'number', description: 'Last update timestamp in milliseconds' },
creatorId: { type: 'string', description: 'User ID of team creator' },
membership: {
type: 'object',
description: 'Current user membership details',
properties: {
role: { type: 'string', description: 'Membership role' },
confirmed: { type: 'boolean', description: 'Whether membership is confirmed' },
created: { type: 'number', description: 'Membership creation timestamp' },
uid: { type: 'string', description: 'User ID of the member' },
teamId: { type: 'string', description: 'Team ID' },
},
},
},
},
},
count: {
type: 'number',
description: 'Number of teams returned',
},
pagination: {
type: 'object',
description: 'Pagination information',
properties: {
count: { type: 'number', description: 'Items in current page' },
next: { type: 'number', description: 'Timestamp for next page request' },
prev: { type: 'number', description: 'Timestamp for previous page request' },
},
},
},
}

View File

@@ -0,0 +1,100 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelListWebhooksParams, VercelListWebhooksResponse } from '@/tools/vercel/types'
export const vercelListWebhooksTool: ToolConfig<
VercelListWebhooksParams,
VercelListWebhooksResponse
> = {
id: 'vercel_list_webhooks',
name: 'Vercel List Webhooks',
description: 'List webhooks for a Vercel project or team',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
projectId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter webhooks by project ID',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelListWebhooksParams) => {
const query = new URLSearchParams()
if (params.projectId) query.set('projectId', params.projectId.trim())
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v1/webhooks${qs ? `?${qs}` : ''}`
},
method: 'GET',
headers: (params: VercelListWebhooksParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
const webhooks = (Array.isArray(data) ? data : []).map((w: any) => ({
id: w.id ?? null,
url: w.url ?? null,
events: w.events ?? [],
ownerId: w.ownerId ?? null,
projectIds: w.projectIds ?? [],
createdAt: w.createdAt ?? null,
updatedAt: w.updatedAt ?? null,
}))
return {
success: true,
output: {
webhooks,
count: webhooks.length,
},
}
},
outputs: {
webhooks: {
type: 'array',
description: 'List of webhooks',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Webhook ID' },
url: { type: 'string', description: 'Webhook URL' },
events: {
type: 'array',
description: 'Events the webhook listens to',
items: { type: 'string', description: 'Event name' },
},
ownerId: { type: 'string', description: 'Owner ID' },
projectIds: {
type: 'array',
description: 'Associated project IDs',
items: { type: 'string', description: 'Project ID' },
},
createdAt: { type: 'number', description: 'Creation timestamp' },
updatedAt: { type: 'number', description: 'Last updated timestamp' },
},
},
},
count: {
type: 'number',
description: 'Number of webhooks returned',
},
},
}

View File

@@ -0,0 +1,65 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelPauseProjectParams, VercelPauseProjectResponse } from '@/tools/vercel/types'
export const vercelPauseProjectTool: ToolConfig<
VercelPauseProjectParams,
VercelPauseProjectResponse
> = {
id: 'vercel_pause_project',
name: 'Vercel Pause Project',
description: 'Pause a Vercel project',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
projectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Project ID or name',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelPauseProjectParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v1/projects/${params.projectId.trim()}/pause${qs ? `?${qs}` : ''}`
},
method: 'POST',
headers: (params: VercelPauseProjectParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
id: data.id,
name: data.name,
paused: data.paused ?? true,
},
}
},
outputs: {
id: { type: 'string', description: 'Project ID' },
name: { type: 'string', description: 'Project name' },
paused: { type: 'boolean', description: 'Whether the project is paused' },
},
}

View File

@@ -0,0 +1,69 @@
import type { ToolConfig } from '@/tools/types'
import type {
VercelRemoveProjectDomainParams,
VercelRemoveProjectDomainResponse,
} from '@/tools/vercel/types'
export const vercelRemoveProjectDomainTool: ToolConfig<
VercelRemoveProjectDomainParams,
VercelRemoveProjectDomainResponse
> = {
id: 'vercel_remove_project_domain',
name: 'Vercel Remove Project Domain',
description: 'Remove a domain from a Vercel project',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
projectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Project ID or name',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Domain name to remove',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelRemoveProjectDomainParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v9/projects/${params.projectId.trim()}/domains/${params.domain.trim()}${qs ? `?${qs}` : ''}`
},
method: 'DELETE',
headers: (params: VercelRemoveProjectDomainParams) => ({
Authorization: `Bearer ${params.apiKey}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async () => {
return {
success: true,
output: {
deleted: true,
},
}
},
outputs: {
deleted: { type: 'boolean', description: 'Whether the domain was successfully removed' },
},
}

View File

@@ -0,0 +1,65 @@
import type { ToolConfig } from '@/tools/types'
import type { VercelRerequestCheckParams, VercelRerequestCheckResponse } from '@/tools/vercel/types'
export const vercelRerequestCheckTool: ToolConfig<
VercelRerequestCheckParams,
VercelRerequestCheckResponse
> = {
id: 'vercel_rerequest_check',
name: 'Vercel Rerequest Check',
description: 'Rerequest a deployment check',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Vercel Access Token',
},
deploymentId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Deployment ID the check belongs to',
},
checkId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Check ID to rerequest',
},
teamId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Team ID to scope the request',
},
},
request: {
url: (params: VercelRerequestCheckParams) => {
const query = new URLSearchParams()
if (params.teamId) query.set('teamId', params.teamId.trim())
const qs = query.toString()
return `https://api.vercel.com/v1/deployments/${params.deploymentId.trim()}/checks/${params.checkId.trim()}/rerequest${qs ? `?${qs}` : ''}`
},
method: 'POST',
headers: (params: VercelRerequestCheckParams) => ({
Authorization: `Bearer ${params.apiKey}`,
}),
},
transformResponse: async () => {
return {
success: true,
output: {
rerequested: true,
},
}
},
outputs: {
rerequested: { type: 'boolean', description: 'Whether the check was successfully rerequested' },
},
}

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More