diff --git a/README.md b/README.md index d6a5e29..fe4dccd 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ Visit [http://localhost:3000](http://localhost:3000) to get started. - **AI Gateway**: Generate Text, Generate Image - **Blob**: Put Blob, List Blobs +- **Clerk**: Get User, Create User, Update User, Delete User - **fal.ai**: Generate Image, Generate Video, Upscale Image, Remove Background, Image to Image - **Firecrawl**: Scrape URL, Search Web - **GitHub**: Create Issue, List Issues, Get Issue, Update Issue diff --git a/lib/workflow-codegen-sdk.ts b/lib/workflow-codegen-sdk.ts index 598bca7..44b8a29 100644 --- a/lib/workflow-codegen-sdk.ts +++ b/lib/workflow-codegen-sdk.ts @@ -393,6 +393,84 @@ export function generateWorkflowSDKCode( "apiKey: process.env.V0_API_KEY!", ]; } + + function buildClerkGetUserParams(config: Record): string[] { + return [ + `userId: \`${convertTemplateToJS((config.userId as string) || "")}\``, + ]; + } + + function buildClerkCreateUserParams( + config: Record + ): string[] { + const params = [ + `emailAddress: \`${convertTemplateToJS((config.emailAddress as string) || "")}\``, + ]; + if (config.password) { + params.push( + `password: \`${convertTemplateToJS(config.password as string)}\`` + ); + } + if (config.firstName) { + params.push( + `firstName: \`${convertTemplateToJS(config.firstName as string)}\`` + ); + } + if (config.lastName) { + params.push( + `lastName: \`${convertTemplateToJS(config.lastName as string)}\`` + ); + } + if (config.publicMetadata) { + params.push( + `publicMetadata: \`${convertTemplateToJS(config.publicMetadata as string)}\`` + ); + } + if (config.privateMetadata) { + params.push( + `privateMetadata: \`${convertTemplateToJS(config.privateMetadata as string)}\`` + ); + } + return params; + } + + function buildClerkUpdateUserParams( + config: Record + ): string[] { + const params = [ + `userId: \`${convertTemplateToJS((config.userId as string) || "")}\``, + ]; + if (config.firstName) { + params.push( + `firstName: \`${convertTemplateToJS(config.firstName as string)}\`` + ); + } + if (config.lastName) { + params.push( + `lastName: \`${convertTemplateToJS(config.lastName as string)}\`` + ); + } + if (config.publicMetadata) { + params.push( + `publicMetadata: \`${convertTemplateToJS(config.publicMetadata as string)}\`` + ); + } + if (config.privateMetadata) { + params.push( + `privateMetadata: \`${convertTemplateToJS(config.privateMetadata as string)}\`` + ); + } + return params; + } + + function buildClerkDeleteUserParams( + config: Record + ): string[] { + return [ + `userId: \`${convertTemplateToJS((config.userId as string) || "")}\``, + ]; + } + function buildStepInputParams( actionType: string, config: Record @@ -410,6 +488,11 @@ export function generateWorkflowSDKCode( Search: () => buildFirecrawlParams(actionType, config), "Create Chat": () => buildV0CreateChatParams(config), "Send Message": () => buildV0SendMessageParams(config), + // Clerk + "Get User": () => buildClerkGetUserParams(config), + "Create User": () => buildClerkCreateUserParams(config), + "Update User": () => buildClerkUpdateUserParams(config), + "Delete User": () => buildClerkDeleteUserParams(config), }; const builder = paramBuilders[actionType]; diff --git a/plugins/clerk/credentials.ts b/plugins/clerk/credentials.ts new file mode 100644 index 0000000..2fb1ef3 --- /dev/null +++ b/plugins/clerk/credentials.ts @@ -0,0 +1,3 @@ +export type ClerkCredentials = { + CLERK_SECRET_KEY?: string; +}; diff --git a/plugins/clerk/icon.tsx b/plugins/clerk/icon.tsx new file mode 100644 index 0000000..3710a7e --- /dev/null +++ b/plugins/clerk/icon.tsx @@ -0,0 +1,21 @@ +export function ClerkIcon({ className }: { className?: string }) { + return ( + + + + + + ); +} diff --git a/plugins/clerk/index.ts b/plugins/clerk/index.ts new file mode 100644 index 0000000..490bc5e --- /dev/null +++ b/plugins/clerk/index.ts @@ -0,0 +1,205 @@ +import type { IntegrationPlugin } from "../registry"; +import { registerIntegration } from "../registry"; +import { ClerkIcon } from "./icon"; + +const clerkPlugin: IntegrationPlugin = { + type: "clerk", + label: "Clerk", + description: "User authentication and management", + + icon: ClerkIcon, + + formFields: [ + { + id: "clerkSecretKey", + label: "Secret Key", + type: "password", + placeholder: "sk_live_... or sk_test_...", + configKey: "clerkSecretKey", + envVar: "CLERK_SECRET_KEY", + helpText: "Get your secret key from ", + helpLink: { + text: "Clerk Dashboard", + url: "https://dashboard.clerk.com", + }, + }, + ], + + testConfig: { + getTestFunction: async () => { + const { testClerk } = await import("./test"); + return testClerk; + }, + }, + + actions: [ + { + slug: "get-user", + label: "Get User", + description: "Fetch a user by ID from Clerk", + category: "Clerk", + stepFunction: "clerkGetUserStep", + stepImportPath: "get-user", + outputFields: [ + { field: "user.id", description: "User ID" }, + { field: "user.first_name", description: "First name" }, + { field: "user.last_name", description: "Last name" }, + ], + configFields: [ + { + key: "userId", + label: "User ID", + type: "template-input", + placeholder: "user_... or {{NodeName.userId}}", + example: "user_2abc123", + required: true, + }, + ], + }, + { + slug: "create-user", + label: "Create User", + description: "Create a new user in Clerk", + category: "Clerk", + stepFunction: "clerkCreateUserStep", + stepImportPath: "create-user", + outputFields: [ + { field: "user.id", description: "User ID" }, + { field: "user.first_name", description: "First name" }, + { field: "user.last_name", description: "Last name" }, + ], + configFields: [ + { + key: "emailAddress", + label: "Email Address", + type: "template-input", + placeholder: "user@example.com or {{NodeName.email}}", + example: "user@example.com", + required: true, + }, + { + key: "firstName", + label: "First Name", + type: "template-input", + placeholder: "John or {{NodeName.firstName}}", + example: "John", + }, + { + key: "lastName", + label: "Last Name", + type: "template-input", + placeholder: "Doe or {{NodeName.lastName}}", + example: "Doe", + }, + { + key: "password", + label: "Password", + type: "template-input", + placeholder: "Password (min 8 chars) or leave empty", + example: "securepassword123", + }, + { + label: "Metadata", + type: "group", + defaultExpanded: false, + fields: [ + { + key: "publicMetadata", + label: "Public Metadata (JSON)", + type: "template-textarea", + placeholder: '{"role": "admin"} or {{NodeName.metadata}}', + rows: 3, + }, + { + key: "privateMetadata", + label: "Private Metadata (JSON)", + type: "template-textarea", + placeholder: '{"internal_id": "123"}', + rows: 3, + }, + ], + }, + ], + }, + { + slug: "update-user", + label: "Update User", + description: "Update an existing user in Clerk", + category: "Clerk", + stepFunction: "clerkUpdateUserStep", + stepImportPath: "update-user", + outputFields: [ + { field: "user.id", description: "User ID" }, + { field: "user.first_name", description: "First name" }, + { field: "user.last_name", description: "Last name" }, + ], + configFields: [ + { + key: "userId", + label: "User ID", + type: "template-input", + placeholder: "user_... or {{NodeName.user.id}}", + example: "user_2abc123", + required: true, + }, + { + key: "firstName", + label: "First Name", + type: "template-input", + placeholder: "Jane or {{NodeName.firstName}}", + }, + { + key: "lastName", + label: "Last Name", + type: "template-input", + placeholder: "Doe or {{NodeName.lastName}}", + }, + { + label: "Metadata", + type: "group", + defaultExpanded: false, + fields: [ + { + key: "publicMetadata", + label: "Public Metadata (JSON)", + type: "template-textarea", + placeholder: '{"role": "admin"} or {{NodeName.metadata}}', + rows: 3, + }, + { + key: "privateMetadata", + label: "Private Metadata (JSON)", + type: "template-textarea", + placeholder: '{"internal_id": "123"}', + rows: 3, + }, + ], + }, + ], + }, + { + slug: "delete-user", + label: "Delete User", + description: "Delete a user from Clerk", + category: "Clerk", + stepFunction: "clerkDeleteUserStep", + stepImportPath: "delete-user", + outputFields: [{ field: "deleted", description: "Deletion success" }], + configFields: [ + { + key: "userId", + label: "User ID", + type: "template-input", + placeholder: "user_... or {{NodeName.user.id}}", + example: "user_2abc123", + required: true, + }, + ], + }, + ], +}; + +// Auto-register on import +registerIntegration(clerkPlugin); + +export default clerkPlugin; diff --git a/plugins/clerk/steps/create-user.ts b/plugins/clerk/steps/create-user.ts new file mode 100644 index 0000000..53b0368 --- /dev/null +++ b/plugins/clerk/steps/create-user.ts @@ -0,0 +1,133 @@ +import "server-only"; + +import { fetchCredentials } from "@/lib/credential-fetcher"; +import { type StepInput, withStepLogging } from "@/lib/steps/step-handler"; +import { getErrorMessage } from "@/lib/utils"; +import type { ClerkCredentials } from "../credentials"; +import type { ClerkUser } from "../types"; + +type CreateUserResult = + | { success: true; user: ClerkUser } + | { success: false; error: string }; + +export type ClerkCreateUserCoreInput = { + emailAddress: string; + firstName?: string; + lastName?: string; + password?: string; + publicMetadata?: string; + privateMetadata?: string; +}; + +export type ClerkCreateUserInput = StepInput & + ClerkCreateUserCoreInput & { + integrationId?: string; + }; + +/** + * Core logic - portable between app and export + */ +async function stepHandler( + input: ClerkCreateUserCoreInput, + credentials: ClerkCredentials +): Promise { + const secretKey = credentials.CLERK_SECRET_KEY; + + if (!secretKey) { + return { + success: false, + error: + "CLERK_SECRET_KEY is not configured. Please add it in Project Integrations.", + }; + } + + if (!input.emailAddress) { + return { + success: false, + error: "Email address is required.", + }; + } + + try { + // Build the request body + const body: Record = { + email_address: [input.emailAddress], + }; + + if (input.firstName) { + body.first_name = input.firstName; + } + if (input.lastName) { + body.last_name = input.lastName; + } + if (input.password) { + body.password = input.password; + } + if (input.publicMetadata) { + try { + body.public_metadata = JSON.parse(input.publicMetadata); + } catch { + return { + success: false, + error: "Invalid JSON format for publicMetadata", + }; + } + } + if (input.privateMetadata) { + try { + body.private_metadata = JSON.parse(input.privateMetadata); + } catch { + return { + success: false, + error: "Invalid JSON format for privateMetadata", + }; + } + } + + const response = await fetch("https://api.clerk.com/v1/users", { + method: "POST", + headers: { + Authorization: `Bearer ${secretKey}`, + "Content-Type": "application/json", + "User-Agent": "workflow-builder.dev", + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + return { + success: false, + error: + error.errors?.[0]?.message || + `Failed to create user: ${response.status}`, + }; + } + + const user = await response.json(); + return { success: true, user }; + } catch (error) { + return { + success: false, + error: `Failed to create user: ${getErrorMessage(error)}`, + }; + } +} + +/** + * App entry point - fetches credentials and wraps with logging + */ +export async function clerkCreateUserStep( + input: ClerkCreateUserInput +): Promise { + "use step"; + + const credentials = input.integrationId + ? await fetchCredentials(input.integrationId) + : {}; + + return withStepLogging(input, () => stepHandler(input, credentials)); +} +clerkCreateUserStep.maxRetries = 0; + +export const _integrationType = "clerk"; diff --git a/plugins/clerk/steps/delete-user.ts b/plugins/clerk/steps/delete-user.ts new file mode 100644 index 0000000..b3ff451 --- /dev/null +++ b/plugins/clerk/steps/delete-user.ts @@ -0,0 +1,93 @@ +import "server-only"; + +import { fetchCredentials } from "@/lib/credential-fetcher"; +import { type StepInput, withStepLogging } from "@/lib/steps/step-handler"; +import { getErrorMessage } from "@/lib/utils"; +import type { ClerkCredentials } from "../credentials"; + +type DeleteUserResult = + | { success: true; deleted: boolean } + | { success: false; error: string }; + +export type ClerkDeleteUserCoreInput = { + userId: string; +}; + +export type ClerkDeleteUserInput = StepInput & + ClerkDeleteUserCoreInput & { + integrationId?: string; + }; + +/** + * Core logic - portable between app and export + */ +async function stepHandler( + input: ClerkDeleteUserCoreInput, + credentials: ClerkCredentials +): Promise { + const secretKey = credentials.CLERK_SECRET_KEY; + + if (!secretKey) { + return { + success: false, + error: + "CLERK_SECRET_KEY is not configured. Please add it in Project Integrations.", + }; + } + + if (!input.userId) { + return { + success: false, + error: "User ID is required.", + }; + } + + try { + const response = await fetch( + `https://api.clerk.com/v1/users/${encodeURIComponent(input.userId)}`, + { + method: "DELETE", + headers: { + Authorization: `Bearer ${secretKey}`, + "Content-Type": "application/json", + "User-Agent": "workflow-builder.dev", + }, + } + ); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + return { + success: false, + error: + error.errors?.[0]?.message || + `Failed to delete user: ${response.status}`, + }; + } + + return { success: true, deleted: true }; + } catch (error) { + return { + success: false, + error: `Failed to delete user: ${getErrorMessage(error)}`, + }; + } +} + +/** + * App entry point - fetches credentials and wraps with logging + */ +export async function clerkDeleteUserStep( + input: ClerkDeleteUserInput +): Promise { + "use step"; + + const credentials = input.integrationId + ? await fetchCredentials(input.integrationId) + : {}; + + return withStepLogging(input, () => stepHandler(input, credentials)); +} +clerkDeleteUserStep.maxRetries = 0; + +export const _integrationType = "clerk"; diff --git a/plugins/clerk/steps/get-user.ts b/plugins/clerk/steps/get-user.ts new file mode 100644 index 0000000..83864e3 --- /dev/null +++ b/plugins/clerk/steps/get-user.ts @@ -0,0 +1,93 @@ +import "server-only"; + +import { fetchCredentials } from "@/lib/credential-fetcher"; +import { type StepInput, withStepLogging } from "@/lib/steps/step-handler"; +import { getErrorMessage } from "@/lib/utils"; +import type { ClerkCredentials } from "../credentials"; +import type { ClerkUser } from "../types"; + +type GetUserResult = + | { success: true; user: ClerkUser } + | { success: false; error: string }; + +export type ClerkGetUserCoreInput = { + userId: string; +}; + +export type ClerkGetUserInput = StepInput & + ClerkGetUserCoreInput & { + integrationId?: string; + }; + +/** + * Core logic - portable between app and export + */ +async function stepHandler( + input: ClerkGetUserCoreInput, + credentials: ClerkCredentials +): Promise { + const secretKey = credentials.CLERK_SECRET_KEY; + + if (!secretKey) { + return { + success: false, + error: + "CLERK_SECRET_KEY is not configured. Please add it in Project Integrations.", + }; + } + + if (!input.userId) { + return { + success: false, + error: "User ID is required.", + }; + } + + try { + const response = await fetch( + `https://api.clerk.com/v1/users/${encodeURIComponent(input.userId)}`, + { + headers: { + Authorization: `Bearer ${secretKey}`, + "Content-Type": "application/json", + "User-Agent": "workflow-builder.dev", + }, + } + ); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + return { + success: false, + error: + error.errors?.[0]?.message || `Failed to get user: ${response.status}`, + }; + } + + const user = await response.json(); + return { success: true, user }; + } catch (error) { + return { + success: false, + error: `Failed to get user: ${getErrorMessage(error)}`, + }; + } +} + +/** + * App entry point - fetches credentials and wraps with logging + */ +export async function clerkGetUserStep( + input: ClerkGetUserInput +): Promise { + "use step"; + + const credentials = input.integrationId + ? await fetchCredentials(input.integrationId) + : {}; + + return withStepLogging(input, () => stepHandler(input, credentials)); +} +clerkGetUserStep.maxRetries = 0; + +export const _integrationType = "clerk"; diff --git a/plugins/clerk/steps/update-user.ts b/plugins/clerk/steps/update-user.ts new file mode 100644 index 0000000..6083270 --- /dev/null +++ b/plugins/clerk/steps/update-user.ts @@ -0,0 +1,130 @@ +import "server-only"; + +import { fetchCredentials } from "@/lib/credential-fetcher"; +import { type StepInput, withStepLogging } from "@/lib/steps/step-handler"; +import { getErrorMessage } from "@/lib/utils"; +import type { ClerkCredentials } from "../credentials"; +import type { ClerkUser } from "../types"; + +type UpdateUserResult = + | { success: true; user: ClerkUser } + | { success: false; error: string }; + +export type ClerkUpdateUserCoreInput = { + userId: string; + firstName?: string; + lastName?: string; + publicMetadata?: string; + privateMetadata?: string; +}; + +export type ClerkUpdateUserInput = StepInput & + ClerkUpdateUserCoreInput & { + integrationId?: string; + }; + +/** + * Core logic - portable between app and export + */ +async function stepHandler( + input: ClerkUpdateUserCoreInput, + credentials: ClerkCredentials +): Promise { + const secretKey = credentials.CLERK_SECRET_KEY; + + if (!secretKey) { + return { + success: false, + error: + "CLERK_SECRET_KEY is not configured. Please add it in Project Integrations.", + }; + } + + if (!input.userId) { + return { + success: false, + error: "User ID is required.", + }; + } + + try { + // Build the request body + const body: Record = {}; + + if (input.firstName !== undefined) { + body.first_name = input.firstName; + } + if (input.lastName !== undefined) { + body.last_name = input.lastName; + } + if (input.publicMetadata) { + try { + body.public_metadata = JSON.parse(input.publicMetadata); + } catch { + return { + success: false, + error: "Invalid JSON format for publicMetadata", + }; + } + } + if (input.privateMetadata) { + try { + body.private_metadata = JSON.parse(input.privateMetadata); + } catch { + return { + success: false, + error: "Invalid JSON format for privateMetadata", + }; + } + } + + const response = await fetch( + `https://api.clerk.com/v1/users/${encodeURIComponent(input.userId)}`, + { + method: "PATCH", + headers: { + Authorization: `Bearer ${secretKey}`, + "Content-Type": "application/json", + "User-Agent": "workflow-builder.dev", + }, + body: JSON.stringify(body), + } + ); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + return { + success: false, + error: + error.errors?.[0]?.message || + `Failed to update user: ${response.status}`, + }; + } + + const user = await response.json(); + return { success: true, user }; + } catch (error) { + return { + success: false, + error: `Failed to update user: ${getErrorMessage(error)}`, + }; + } +} + +/** + * App entry point - fetches credentials and wraps with logging + */ +export async function clerkUpdateUserStep( + input: ClerkUpdateUserInput +): Promise { + "use step"; + + const credentials = input.integrationId + ? await fetchCredentials(input.integrationId) + : {}; + + return withStepLogging(input, () => stepHandler(input, credentials)); +} +clerkUpdateUserStep.maxRetries = 0; + +export const _integrationType = "clerk"; diff --git a/plugins/clerk/test.ts b/plugins/clerk/test.ts new file mode 100644 index 0000000..23898a5 --- /dev/null +++ b/plugins/clerk/test.ts @@ -0,0 +1,48 @@ +export async function testClerk(credentials: Record) { + try { + const secretKey = credentials.CLERK_SECRET_KEY; + + if (!secretKey) { + return { + success: false, + error: "Secret key is required", + }; + } + + // Validate format - Clerk secret keys start with sk_live_ or sk_test_ + if ( + !secretKey.startsWith("sk_live_") && + !secretKey.startsWith("sk_test_") + ) { + return { + success: false, + error: + "Invalid secret key format. Clerk secret keys start with 'sk_live_' or 'sk_test_'", + }; + } + + // Test the connection by fetching users list (limit 1) + const response = await fetch("https://api.clerk.com/v1/users?limit=1", { + headers: { + Authorization: `Bearer ${secretKey}`, + "Content-Type": "application/json", + "User-Agent": "workflow-builder.dev", + }, + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + return { + success: false, + error: error.errors?.[0]?.message || `API error: ${response.status}`, + }; + } + + return { success: true }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } +} diff --git a/plugins/clerk/types.ts b/plugins/clerk/types.ts new file mode 100644 index 0000000..269bb66 --- /dev/null +++ b/plugins/clerk/types.ts @@ -0,0 +1,14 @@ +export type ClerkUser = { + id: string; + first_name: string | null; + last_name: string | null; + email_addresses: Array<{ + id: string; + email_address: string; + }>; + primary_email_address_id: string | null; + public_metadata: Record; + private_metadata: Record; + created_at: number; + updated_at: number; +}; diff --git a/plugins/index.ts b/plugins/index.ts index c2b4124..6ce44cc 100644 --- a/plugins/index.ts +++ b/plugins/index.ts @@ -16,6 +16,7 @@ import "./ai-gateway"; import "./blob"; +import "./clerk"; import "./fal"; import "./firecrawl"; import "./github";