mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-22 03:01:08 -05:00
Compare commits
11 Commits
improvemen
...
v0.5.94
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15ace5e63f | ||
|
|
ab48787422 | ||
|
|
91aa1f9a52 | ||
|
|
2979269ac3 | ||
|
|
cf28822a1c | ||
|
|
fdca73679d | ||
|
|
86ca984926 | ||
|
|
e3964624ac | ||
|
|
7c7c0fd955 | ||
|
|
da46a387c9 | ||
|
|
b7e377ec4b |
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -122,6 +122,7 @@
|
||||
"twilio_sms",
|
||||
"twilio_voice",
|
||||
"typeform",
|
||||
"vercel",
|
||||
"video_generator",
|
||||
"vision",
|
||||
"wealthbox",
|
||||
|
||||
1391
apps/docs/content/docs/en/tools/vercel.mdx
Normal file
1391
apps/docs/content/docs/en/tools/vercel.mdx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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!,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
137
apps/sim/app/api/workflows/route.test.ts
Normal file
137
apps/sim/app/api/workflows/route.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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
|
||||
|
||||
1007
apps/sim/blocks/blocks/vercel.ts
Normal file
1007
apps/sim/blocks/blocks/vercel.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
177
apps/sim/hooks/queries/folders.test.ts
Normal file
177
apps/sim/hooks/queries/folders.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
44
apps/sim/hooks/queries/utils/top-insertion-sort-order.ts
Normal file
44
apps/sim/hooks/queries/utils/top-insertion-sort-order.ts
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
197
apps/sim/lib/workflows/persistence/duplicate.test.ts
Normal file
197
apps/sim/lib/workflows/persistence/duplicate.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
@@ -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>()
|
||||
|
||||
@@ -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
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
84
apps/sim/tools/vercel/add_domain.ts
Normal file
84
apps/sim/tools/vercel/add_domain.ts
Normal 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' },
|
||||
},
|
||||
},
|
||||
}
|
||||
113
apps/sim/tools/vercel/add_project_domain.ts
Normal file
113
apps/sim/tools/vercel/add_project_domain.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
83
apps/sim/tools/vercel/cancel_deployment.ts
Normal file
83
apps/sim/tools/vercel/cancel_deployment.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
87
apps/sim/tools/vercel/create_alias.ts
Normal file
87
apps/sim/tools/vercel/create_alias.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
141
apps/sim/tools/vercel/create_check.ts
Normal file
141
apps/sim/tools/vercel/create_check.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
}
|
||||
136
apps/sim/tools/vercel/create_deployment.ts
Normal file
136
apps/sim/tools/vercel/create_deployment.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
107
apps/sim/tools/vercel/create_dns_record.ts
Normal file
107
apps/sim/tools/vercel/create_dns_record.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
106
apps/sim/tools/vercel/create_edge_config.ts
Normal file
106
apps/sim/tools/vercel/create_edge_config.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
145
apps/sim/tools/vercel/create_env_var.ts
Normal file
145
apps/sim/tools/vercel/create_env_var.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
}
|
||||
108
apps/sim/tools/vercel/create_project.ts
Normal file
108
apps/sim/tools/vercel/create_project.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
105
apps/sim/tools/vercel/create_webhook.ts
Normal file
105
apps/sim/tools/vercel/create_webhook.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
62
apps/sim/tools/vercel/delete_alias.ts
Normal file
62
apps/sim/tools/vercel/delete_alias.ts
Normal 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)',
|
||||
},
|
||||
},
|
||||
}
|
||||
77
apps/sim/tools/vercel/delete_deployment.ts
Normal file
77
apps/sim/tools/vercel/delete_deployment.ts
Normal 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)',
|
||||
},
|
||||
},
|
||||
}
|
||||
69
apps/sim/tools/vercel/delete_dns_record.ts
Normal file
69
apps/sim/tools/vercel/delete_dns_record.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
64
apps/sim/tools/vercel/delete_domain.ts
Normal file
64
apps/sim/tools/vercel/delete_domain.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
69
apps/sim/tools/vercel/delete_env_var.ts
Normal file
69
apps/sim/tools/vercel/delete_env_var.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
60
apps/sim/tools/vercel/delete_project.ts
Normal file
60
apps/sim/tools/vercel/delete_project.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
63
apps/sim/tools/vercel/delete_webhook.ts
Normal file
63
apps/sim/tools/vercel/delete_webhook.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
97
apps/sim/tools/vercel/get_alias.ts
Normal file
97
apps/sim/tools/vercel/get_alias.ts
Normal 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)',
|
||||
},
|
||||
},
|
||||
}
|
||||
99
apps/sim/tools/vercel/get_check.ts
Normal file
99
apps/sim/tools/vercel/get_check.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
}
|
||||
176
apps/sim/tools/vercel/get_deployment.ts
Normal file
176
apps/sim/tools/vercel/get_deployment.ts
Normal 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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
135
apps/sim/tools/vercel/get_deployment_events.ts
Normal file
135
apps/sim/tools/vercel/get_deployment_events.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
94
apps/sim/tools/vercel/get_domain.ts
Normal file
94
apps/sim/tools/vercel/get_domain.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
107
apps/sim/tools/vercel/get_domain_config.ts
Normal file
107
apps/sim/tools/vercel/get_domain_config.ts
Normal 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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
100
apps/sim/tools/vercel/get_edge_config.ts
Normal file
100
apps/sim/tools/vercel/get_edge_config.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
93
apps/sim/tools/vercel/get_edge_config_items.ts
Normal file
93
apps/sim/tools/vercel/get_edge_config_items.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
99
apps/sim/tools/vercel/get_env_vars.ts
Normal file
99
apps/sim/tools/vercel/get_env_vars.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
89
apps/sim/tools/vercel/get_project.ts
Normal file
89
apps/sim/tools/vercel/get_project.ts
Normal 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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
98
apps/sim/tools/vercel/get_team.ts
Normal file
98
apps/sim/tools/vercel/get_team.ts
Normal 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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
73
apps/sim/tools/vercel/get_user.ts
Normal file
73
apps/sim/tools/vercel/get_user.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
126
apps/sim/tools/vercel/index.ts
Normal file
126
apps/sim/tools/vercel/index.ts
Normal 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'
|
||||
107
apps/sim/tools/vercel/list_aliases.ts
Normal file
107
apps/sim/tools/vercel/list_aliases.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
99
apps/sim/tools/vercel/list_checks.ts
Normal file
99
apps/sim/tools/vercel/list_checks.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
114
apps/sim/tools/vercel/list_deployment_files.ts
Normal file
114
apps/sim/tools/vercel/list_deployment_files.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
170
apps/sim/tools/vercel/list_deployments.ts
Normal file
170
apps/sim/tools/vercel/list_deployments.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
116
apps/sim/tools/vercel/list_dns_records.ts
Normal file
116
apps/sim/tools/vercel/list_dns_records.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
109
apps/sim/tools/vercel/list_domains.ts
Normal file
109
apps/sim/tools/vercel/list_domains.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
91
apps/sim/tools/vercel/list_edge_configs.ts
Normal file
91
apps/sim/tools/vercel/list_edge_configs.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
116
apps/sim/tools/vercel/list_project_domains.ts
Normal file
116
apps/sim/tools/vercel/list_project_domains.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
106
apps/sim/tools/vercel/list_projects.ts
Normal file
106
apps/sim/tools/vercel/list_projects.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
151
apps/sim/tools/vercel/list_team_members.ts
Normal file
151
apps/sim/tools/vercel/list_team_members.ts
Normal 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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
132
apps/sim/tools/vercel/list_teams.ts
Normal file
132
apps/sim/tools/vercel/list_teams.ts
Normal 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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
100
apps/sim/tools/vercel/list_webhooks.ts
Normal file
100
apps/sim/tools/vercel/list_webhooks.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
}
|
||||
65
apps/sim/tools/vercel/pause_project.ts
Normal file
65
apps/sim/tools/vercel/pause_project.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
69
apps/sim/tools/vercel/remove_project_domain.ts
Normal file
69
apps/sim/tools/vercel/remove_project_domain.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
65
apps/sim/tools/vercel/rerequest_check.ts
Normal file
65
apps/sim/tools/vercel/rerequest_check.ts
Normal 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' },
|
||||
},
|
||||
}
|
||||
1025
apps/sim/tools/vercel/types.ts
Normal file
1025
apps/sim/tools/vercel/types.ts
Normal file
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
Reference in New Issue
Block a user