From 9819bc73b23c9800bb2b712e0604b9efb4dcadf7 Mon Sep 17 00:00:00 2001 From: Railly Date: Fri, 28 Nov 2025 18:40:35 -0500 Subject: [PATCH 1/6] feat: Add Clerk integration plugin Adds a complete Clerk integration with 4 workflow actions: - Get User: Fetch user details by ID - Create User: Create new users with email, names - Update User: Update existing user properties - Delete User: Remove users from Clerk Features: - Full plugin implementation following the plugin architecture - Integration form with secret key configuration - Connection testing via Clerk API - Code generation templates for workflow export - Proper credential handling via fetchCredentials --- app/api/ai/generate/route.ts | 4 + .../[integrationId]/test/route.ts | 53 +++++++ .../settings/integration-form-dialog.tsx | 27 ++++ components/settings/integrations-manager.tsx | 1 + components/workflow/nodes/action-node.tsx | 15 ++ components/workflow/utils/code-generators.ts | 13 ++ lib/api-client.ts | 4 +- lib/credential-fetcher.ts | 12 ++ lib/db/integrations.ts | 5 +- lib/db/schema.ts | 1 + lib/workflow-codegen-sdk.ts | 72 ++++++++++ lib/workflow-codegen-shared.ts | 17 +++ lib/workflow-codegen.ts | 131 ++++++++++++++++++ lib/workflow-executor.workflow.ts | 30 ++++ plugins/clerk/codegen/create-user.ts | 47 +++++++ plugins/clerk/codegen/delete-user.ts | 34 +++++ plugins/clerk/codegen/get-user.ts | 33 +++++ plugins/clerk/codegen/update-user.ts | 46 ++++++ plugins/clerk/icon.tsx | 21 +++ plugins/clerk/index.tsx | 115 +++++++++++++++ plugins/clerk/settings.tsx | 47 +++++++ plugins/clerk/steps/create-user/config.tsx | 110 +++++++++++++++ plugins/clerk/steps/create-user/step.ts | 115 +++++++++++++++ plugins/clerk/steps/delete-user/config.tsx | 36 +++++ plugins/clerk/steps/delete-user/step.ts | 69 +++++++++ plugins/clerk/steps/get-user/config.tsx | 36 +++++ plugins/clerk/steps/get-user/step.ts | 84 +++++++++++ plugins/clerk/steps/update-user/config.tsx | 97 +++++++++++++ plugins/clerk/steps/update-user/step.ts | 119 ++++++++++++++++ plugins/clerk/test.ts | 43 ++++++ plugins/index.ts | 3 +- 31 files changed, 1437 insertions(+), 3 deletions(-) create mode 100644 plugins/clerk/codegen/create-user.ts create mode 100644 plugins/clerk/codegen/delete-user.ts create mode 100644 plugins/clerk/codegen/get-user.ts create mode 100644 plugins/clerk/codegen/update-user.ts create mode 100644 plugins/clerk/icon.tsx create mode 100644 plugins/clerk/index.tsx create mode 100644 plugins/clerk/settings.tsx create mode 100644 plugins/clerk/steps/create-user/config.tsx create mode 100644 plugins/clerk/steps/create-user/step.ts create mode 100644 plugins/clerk/steps/delete-user/config.tsx create mode 100644 plugins/clerk/steps/delete-user/step.ts create mode 100644 plugins/clerk/steps/get-user/config.tsx create mode 100644 plugins/clerk/steps/get-user/step.ts create mode 100644 plugins/clerk/steps/update-user/config.tsx create mode 100644 plugins/clerk/steps/update-user/step.ts create mode 100644 plugins/clerk/test.ts diff --git a/app/api/ai/generate/route.ts b/app/api/ai/generate/route.ts index 5717b894..a92882d6 100644 --- a/app/api/ai/generate/route.ts +++ b/app/api/ai/generate/route.ts @@ -198,6 +198,10 @@ Action types: - Condition: {"actionType": "Condition", "condition": "{{@nodeId:Label.field}} === 'value'"} - Create Chat (v0): {"actionType": "Create Chat", "message": "Create a line graph showing DAU over time", "system": "You are an expert coder"} - Use v0 for generating UI components, visualizations (charts, graphs, dashboards), landing pages, or any React/Next.js code. PREFER v0 over Generate Text/Image for any visual output like charts, graphs, or UI. - Send Message (v0): {"actionType": "Send Message", "chatId": "{{@nodeId:Label.chatId}}", "message": "Add dark mode"} - Use this to continue a v0 chat conversation +- Get User (Clerk): {"actionType": "Get User", "userId": "user_xxx"} - Fetch a Clerk user by ID. Returns {success, user: {id, first_name, last_name, email_addresses, ...}}. +- Create User (Clerk): {"actionType": "Create User", "emailAddress": "user@example.com", "firstName": "John", "lastName": "Doe"} - Create a new user in Clerk. Returns {success, user: {id, ...}}. Password is optional (min 8 chars if provided, or omit to let user set their own). +- Update User (Clerk): {"actionType": "Update User", "userId": "{{@create-user-node-id:Create User.user.id}}", "firstName": "Jane"} - Update an existing Clerk user. IMPORTANT: Use exact format {{@actualNodeId:Node Label.user.id}} where actualNodeId is the node's ID from the addNode operation. +- Delete User (Clerk): {"actionType": "Delete User", "userId": "{{@create-user-node-id:Create User.user.id}}"} - Delete a user from Clerk. IMPORTANT: Use exact format {{@actualNodeId:Node Label.user.id}} where actualNodeId is the node's ID from the addNode operation. CRITICAL ABOUT CONDITION NODES: - Condition nodes evaluate a boolean expression diff --git a/app/api/integrations/[integrationId]/test/route.ts b/app/api/integrations/[integrationId]/test/route.ts index 0247e731..1368a6f7 100644 --- a/app/api/integrations/[integrationId]/test/route.ts +++ b/app/api/integrations/[integrationId]/test/route.ts @@ -68,6 +68,11 @@ export async function POST( integration.config.firecrawlApiKey ); break; + case "clerk": + result = await testClerkConnection( + integration.config.clerkSecretKey + ); + break; default: return NextResponse.json( { error: "Invalid integration type" }, @@ -281,3 +286,51 @@ async function testFirecrawlConnection( }; } } + +async function testClerkConnection( + secretKey?: string +): Promise { + try { + if (!secretKey) { + return { + status: "error", + message: "Secret key is required", + }; + } + + // Validate key format + if (!secretKey.startsWith("sk_live_") && !secretKey.startsWith("sk_test_")) { + return { + status: "error", + message: "Invalid secret key format. Must start with sk_live_ or sk_test_", + }; + } + + // Test by fetching users list + 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 { + status: "error", + message: error.errors?.[0]?.message || "Authentication failed", + }; + } + + return { + status: "success", + message: "Connection successful", + }; + } catch (error) { + return { + status: "error", + message: error instanceof Error ? error.message : "Connection failed", + }; + } +} diff --git a/components/settings/integration-form-dialog.tsx b/components/settings/integration-form-dialog.tsx index 862de062..b3de0e34 100644 --- a/components/settings/integration-form-dialog.tsx +++ b/components/settings/integration-form-dialog.tsx @@ -41,6 +41,7 @@ type IntegrationFormData = { const INTEGRATION_TYPES: IntegrationType[] = [ "ai-gateway", + "clerk", "database", "firecrawl", "linear", @@ -57,6 +58,7 @@ const INTEGRATION_LABELS: Record = { "ai-gateway": "AI Gateway", firecrawl: "Firecrawl", v0: "v0", + clerk: "Clerk", }; export function IntegrationFormDialog({ @@ -316,6 +318,31 @@ export function IntegrationFormDialog({

); + case "clerk": + return ( +
+ + updateConfig("clerkSecretKey", e.target.value)} + placeholder="sk_live_... or sk_test_..." + type="password" + value={formData.config.clerkSecretKey || ""} + /> +

+ Get your secret key from{" "} + + dashboard.clerk.com + + {" "}under API Keys. +

+
+ ); default: return null; } diff --git a/components/settings/integrations-manager.tsx b/components/settings/integrations-manager.tsx index 388a6efe..aeac4360 100644 --- a/components/settings/integrations-manager.tsx +++ b/components/settings/integrations-manager.tsx @@ -27,6 +27,7 @@ const INTEGRATION_TYPE_LABELS: Record = { "ai-gateway": "AI Gateway", firecrawl: "Firecrawl", v0: "v0", + clerk: "Clerk", }; type IntegrationsManagerProps = { diff --git a/components/workflow/nodes/action-node.tsx b/components/workflow/nodes/action-node.tsx index a12817b5..611d7f1e 100644 --- a/components/workflow/nodes/action-node.tsx +++ b/components/workflow/nodes/action-node.tsx @@ -82,6 +82,11 @@ const getIntegrationFromActionType = (actionType: string): string => { Condition: "Condition", "Create Chat": "v0", "Send Message": "v0", + // Clerk + "Get User": "Clerk", + "Create User": "Clerk", + "Update User": "Clerk", + "Delete User": "Clerk", }; return integrationMap[actionType] || "System"; }; @@ -111,6 +116,11 @@ const requiresIntegration = (actionType: string): boolean => { "Search", "Create Chat", "Send Message", + // Clerk + "Get User", + "Create User", + "Update User", + "Delete User", ]; return requiresIntegrationActions.includes(actionType); }; @@ -147,6 +157,11 @@ const getProviderLogo = (actionType: string) => { case "Create Chat": case "Send Message": return ; + case "Get User": + case "Create User": + case "Update User": + case "Delete User": + return ; default: return ; } diff --git a/components/workflow/utils/code-generators.ts b/components/workflow/utils/code-generators.ts index a7bb03e8..5eebf817 100644 --- a/components/workflow/utils/code-generators.ts +++ b/components/workflow/utils/code-generators.ts @@ -7,6 +7,10 @@ import databaseQueryTemplate from "@/lib/codegen-templates/database-query"; import httpRequestTemplate from "@/lib/codegen-templates/http-request"; import { generateImageCodegenTemplate } from "@/plugins/ai-gateway/codegen/generate-image"; import { generateTextCodegenTemplate } from "@/plugins/ai-gateway/codegen/generate-text"; +import { createUserCodegenTemplate } from "@/plugins/clerk/codegen/create-user"; +import { deleteUserCodegenTemplate } from "@/plugins/clerk/codegen/delete-user"; +import { getUserCodegenTemplate } from "@/plugins/clerk/codegen/get-user"; +import { updateUserCodegenTemplate } from "@/plugins/clerk/codegen/update-user"; import { scrapeCodegenTemplate } from "@/plugins/firecrawl/codegen/scrape"; import { searchCodegenTemplate } from "@/plugins/firecrawl/codegen/search"; import { createTicketCodegenTemplate } from "@/plugins/linear/codegen/create-ticket"; @@ -90,6 +94,15 @@ export async function POST(request: NextRequest) { return createChatCodegenTemplate; case "Send Message": return sendMessageCodegenTemplate; + // Clerk + case "Get User": + return getUserCodegenTemplate; + case "Create User": + return createUserCodegenTemplate; + case "Update User": + return updateUserCodegenTemplate; + case "Delete User": + return deleteUserCodegenTemplate; default: return `async function actionStep(input: Record) { "use step"; diff --git a/lib/api-client.ts b/lib/api-client.ts index b808bbc4..dc2ef337 100644 --- a/lib/api-client.ts +++ b/lib/api-client.ts @@ -318,7 +318,8 @@ export type IntegrationType = | "database" | "ai-gateway" | "firecrawl" - | "v0"; + | "v0" + | "clerk"; export type IntegrationConfig = { apiKey?: string; @@ -327,6 +328,7 @@ export type IntegrationConfig = { url?: string; openaiApiKey?: string; firecrawlApiKey?: string; + clerkSecretKey?: string; }; export type Integration = { diff --git a/lib/credential-fetcher.ts b/lib/credential-fetcher.ts index 3e4c9e86..64955718 100644 --- a/lib/credential-fetcher.ts +++ b/lib/credential-fetcher.ts @@ -27,6 +27,7 @@ export type WorkflowCredentials = { DATABASE_URL?: string; FIRECRAWL_API_KEY?: string; V0_API_KEY?: string; + CLERK_SECRET_KEY?: string; }; function mapResendConfig(config: IntegrationConfig): WorkflowCredentials { @@ -91,6 +92,14 @@ function mapV0Config(config: IntegrationConfig): WorkflowCredentials { return creds; } +function mapClerkConfig(config: IntegrationConfig): WorkflowCredentials { + const creds: WorkflowCredentials = {}; + if (config.clerkSecretKey) { + creds.CLERK_SECRET_KEY = config.clerkSecretKey; + } + return creds; +} + /** * Map integration config to WorkflowCredentials format */ @@ -119,6 +128,9 @@ function mapIntegrationConfig( if (integrationType === "v0") { return mapV0Config(config); } + if (integrationType === "clerk") { + return mapClerkConfig(config); + } return {}; } diff --git a/lib/db/integrations.ts b/lib/db/integrations.ts index 598fc8cb..1056fc07 100644 --- a/lib/db/integrations.ts +++ b/lib/db/integrations.ts @@ -101,7 +101,8 @@ export type IntegrationType = | "database" | "ai-gateway" | "firecrawl" - | "v0"; + | "v0" + | "clerk"; export type IntegrationConfig = { // Resend @@ -117,6 +118,8 @@ export type IntegrationConfig = { // Firecrawl firecrawlApiKey?: string; // v0 (uses apiKey) + // Clerk + clerkSecretKey?: string; }; export type DecryptedIntegration = { diff --git a/lib/db/schema.ts b/lib/db/schema.ts index 843cccc8..3822a4f6 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -92,6 +92,7 @@ export const integrations = pgTable("integrations", { | "ai-gateway" | "firecrawl" | "v0" + | "clerk" >(), // biome-ignore lint/suspicious/noExplicitAny: JSONB type - encrypted credentials stored as JSON config: jsonb("config").notNull().$type(), diff --git a/lib/workflow-codegen-sdk.ts b/lib/workflow-codegen-sdk.ts index a2a324c3..a2116df3 100644 --- a/lib/workflow-codegen-sdk.ts +++ b/lib/workflow-codegen-sdk.ts @@ -2,6 +2,10 @@ import "server-only"; import { generateImageCodegenTemplate } from "../plugins/ai-gateway/codegen/generate-image"; import { generateTextCodegenTemplate } from "../plugins/ai-gateway/codegen/generate-text"; +import { createUserCodegenTemplate } from "../plugins/clerk/codegen/create-user"; +import { deleteUserCodegenTemplate } from "../plugins/clerk/codegen/delete-user"; +import { getUserCodegenTemplate } from "../plugins/clerk/codegen/get-user"; +import { updateUserCodegenTemplate } from "../plugins/clerk/codegen/update-user"; import { scrapeCodegenTemplate } from "../plugins/firecrawl/codegen/scrape"; import { searchCodegenTemplate } from "../plugins/firecrawl/codegen/search"; import { createTicketCodegenTemplate } from "../plugins/linear/codegen/create-ticket"; @@ -46,6 +50,11 @@ function loadStepImplementation(actionType: string): string | null { Condition: conditionTemplate, "Create Chat": createChatCodegenTemplate, "Send Message": sendMessageCodegenTemplate, + // Clerk + "Get User": getUserCodegenTemplate, + "Create User": createUserCodegenTemplate, + "Update User": updateUserCodegenTemplate, + "Delete User": deleteUserCodegenTemplate, }; const template = templateMap[actionType]; @@ -619,6 +628,64 @@ export function generateWorkflowSDKCode( "apiKey: process.env.V0_API_KEY!", ]; } + + function buildClerkGetUserParams(config: Record): string[] { + return [ + `userId: \`${convertTemplateToJS((config.userId as string) || "")}\``, + "secretKey: process.env.CLERK_SECRET_KEY!", + ]; + } + + function buildClerkCreateUserParams(config: Record): string[] { + const params = [ + `emailAddress: \`${convertTemplateToJS((config.emailAddress as string) || "")}\``, + "secretKey: process.env.CLERK_SECRET_KEY!", + ]; + 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) || "")}\``, + "secretKey: process.env.CLERK_SECRET_KEY!", + ]; + 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) || "")}\``, + "secretKey: process.env.CLERK_SECRET_KEY!", + ]; + } + function buildStepInputParams( actionType: string, config: Record @@ -636,6 +703,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/lib/workflow-codegen-shared.ts b/lib/workflow-codegen-shared.ts index 05a0a3cc..da843ea4 100644 --- a/lib/workflow-codegen-shared.ts +++ b/lib/workflow-codegen-shared.ts @@ -337,6 +337,23 @@ export function getStepInfo(actionType: string): { functionName: "sendMessageStep", importPath: "./steps/v0", }, + // Clerk + "Get User": { + functionName: "clerkGetUserStep", + importPath: "./steps/clerk", + }, + "Create User": { + functionName: "clerkCreateUserStep", + importPath: "./steps/clerk", + }, + "Update User": { + functionName: "clerkUpdateUserStep", + importPath: "./steps/clerk", + }, + "Delete User": { + functionName: "clerkDeleteUserStep", + importPath: "./steps/clerk", + }, }; return ( diff --git a/lib/workflow-codegen.ts b/lib/workflow-codegen.ts index fee5823c..bda9946c 100644 --- a/lib/workflow-codegen.ts +++ b/lib/workflow-codegen.ts @@ -592,6 +592,121 @@ export function generateWorkflowCode( return lines; } + // Clerk action generators + function generateClerkGetUserActionCode( + node: WorkflowNode, + indent: string, + varName: string + ): string[] { + const stepInfo = getStepInfo("Get User"); + imports.add( + `import { ${stepInfo.functionName} } from '${stepInfo.importPath}';` + ); + + const config = node.data.config || {}; + const userId = (config.userId as string) || ""; + + const lines = [ + `${indent}const ${varName} = await ${stepInfo.functionName}({`, + `${indent} userId: ${formatTemplateValue(userId)},`, + `${indent}});`, + ]; + + return lines; + } + + function generateClerkCreateUserActionCode( + node: WorkflowNode, + indent: string, + varName: string + ): string[] { + const stepInfo = getStepInfo("Create User"); + imports.add( + `import { ${stepInfo.functionName} } from '${stepInfo.importPath}';` + ); + + const config = node.data.config || {}; + const emailAddress = (config.emailAddress as string) || ""; + const firstName = (config.firstName as string) || ""; + const lastName = (config.lastName as string) || ""; + const password = (config.password as string) || ""; + + const lines = [ + `${indent}const ${varName} = await ${stepInfo.functionName}({`, + ]; + + if (emailAddress) { + lines.push(`${indent} emailAddress: ${formatTemplateValue(emailAddress)},`); + } + if (firstName) { + lines.push(`${indent} firstName: ${formatTemplateValue(firstName)},`); + } + if (lastName) { + lines.push(`${indent} lastName: ${formatTemplateValue(lastName)},`); + } + if (password) { + lines.push(`${indent} password: ${formatTemplateValue(password)},`); + } + + lines.push(`${indent}});`); + + return lines; + } + + function generateClerkUpdateUserActionCode( + node: WorkflowNode, + indent: string, + varName: string + ): string[] { + const stepInfo = getStepInfo("Update User"); + imports.add( + `import { ${stepInfo.functionName} } from '${stepInfo.importPath}';` + ); + + const config = node.data.config || {}; + const userId = (config.userId as string) || ""; + const firstName = (config.firstName as string) || ""; + const lastName = (config.lastName as string) || ""; + + const lines = [ + `${indent}const ${varName} = await ${stepInfo.functionName}({`, + `${indent} userId: ${formatTemplateValue(userId)},`, + ]; + + if (firstName) { + lines.push(`${indent} firstName: ${formatTemplateValue(firstName)},`); + } + if (lastName) { + lines.push(`${indent} lastName: ${formatTemplateValue(lastName)},`); + } + + lines.push(`${indent}});`); + + return lines; + } + + function generateClerkDeleteUserActionCode( + node: WorkflowNode, + indent: string, + varName: string + ): string[] { + const stepInfo = getStepInfo("Delete User"); + imports.add( + `import { ${stepInfo.functionName} } from '${stepInfo.importPath}';` + ); + + const config = node.data.config || {}; + const userId = (config.userId as string) || ""; + + const lines = [ + `${indent}const ${varName} = await ${stepInfo.functionName}({`, + `${indent} userId: ${formatTemplateValue(userId)},`, + `${indent}});`, + ]; + + return lines; + } + // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Action type routing requires many conditionals function generateActionNodeCode( node: WorkflowNode, @@ -740,6 +855,22 @@ export function generateWorkflowCode( lines.push( ...wrapActionCall(generateFindIssuesActionCode(indent, varName)) ); + } else if (actionType === "Get User") { + lines.push( + ...wrapActionCall(generateClerkGetUserActionCode(node, indent, varName)) + ); + } else if (actionType === "Create User") { + lines.push( + ...wrapActionCall(generateClerkCreateUserActionCode(node, indent, varName)) + ); + } else if (actionType === "Update User") { + lines.push( + ...wrapActionCall(generateClerkUpdateUserActionCode(node, indent, varName)) + ); + } else if (actionType === "Delete User") { + lines.push( + ...wrapActionCall(generateClerkDeleteUserActionCode(node, indent, varName)) + ); } else if (outputIsUsed) { lines.push(`${indent}const ${varName} = { status: 'success' };`); } else { diff --git a/lib/workflow-executor.workflow.ts b/lib/workflow-executor.workflow.ts index 3febcd0d..828ee9b2 100644 --- a/lib/workflow-executor.workflow.ts +++ b/lib/workflow-executor.workflow.ts @@ -226,6 +226,36 @@ async function executeActionStep(input: { return await sendMessageStep(stepInput as any); } + // Clerk actions + if (actionType === "Get User") { + const { clerkGetUserStep } = await import( + "../plugins/clerk/steps/get-user/step" + ); + // biome-ignore lint/suspicious/noExplicitAny: Dynamic step input type + return await clerkGetUserStep(stepInput as any); + } + if (actionType === "Create User") { + const { clerkCreateUserStep } = await import( + "../plugins/clerk/steps/create-user/step" + ); + // biome-ignore lint/suspicious/noExplicitAny: Dynamic step input type + return await clerkCreateUserStep(stepInput as any); + } + if (actionType === "Update User") { + const { clerkUpdateUserStep } = await import( + "../plugins/clerk/steps/update-user/step" + ); + // biome-ignore lint/suspicious/noExplicitAny: Dynamic step input type + return await clerkUpdateUserStep(stepInput as any); + } + if (actionType === "Delete User") { + const { clerkDeleteUserStep } = await import( + "../plugins/clerk/steps/delete-user/step" + ); + // biome-ignore lint/suspicious/noExplicitAny: Dynamic step input type + return await clerkDeleteUserStep(stepInput as any); + } + // Fallback for unknown action types return { success: false, diff --git a/plugins/clerk/codegen/create-user.ts b/plugins/clerk/codegen/create-user.ts new file mode 100644 index 00000000..038bc8a0 --- /dev/null +++ b/plugins/clerk/codegen/create-user.ts @@ -0,0 +1,47 @@ +/** + * Code generation template for Create User action + * Used when exporting workflows to standalone Next.js projects + */ +export const createUserCodegenTemplate = `export async function clerkCreateUserStep(input: { + emailAddress: string; + password?: string; + firstName?: string; + lastName?: string; + publicMetadata?: string; + privateMetadata?: string; +}) { + "use step"; + + const secretKey = process.env.CLERK_SECRET_KEY; + + if (!secretKey) { + throw new Error('CLERK_SECRET_KEY environment variable is required'); + } + + const body: Record = { + email_address: [input.emailAddress], + }; + + if (input.password) body.password = input.password; + if (input.firstName) body.first_name = input.firstName; + if (input.lastName) body.last_name = input.lastName; + if (input.publicMetadata) body.public_metadata = JSON.parse(input.publicMetadata); + if (input.privateMetadata) body.private_metadata = JSON.parse(input.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(() => ({})); + throw new Error(error.errors?.[0]?.message || \`Failed to create user: \${response.status}\`); + } + + return await response.json(); +}`; diff --git a/plugins/clerk/codegen/delete-user.ts b/plugins/clerk/codegen/delete-user.ts new file mode 100644 index 00000000..68e70ba8 --- /dev/null +++ b/plugins/clerk/codegen/delete-user.ts @@ -0,0 +1,34 @@ +/** + * Code generation template for Delete User action + * Used when exporting workflows to standalone Next.js projects + */ +export const deleteUserCodegenTemplate = `export async function clerkDeleteUserStep(input: { + userId: string; +}) { + "use step"; + + const secretKey = process.env.CLERK_SECRET_KEY; + + if (!secretKey) { + throw new Error('CLERK_SECRET_KEY environment variable is required'); + } + + const response = await fetch( + \`https://api.clerk.com/v1/users/\${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(() => ({})); + throw new Error(error.errors?.[0]?.message || \`Failed to delete user: \${response.status}\`); + } + + return { deleted: true, id: input.userId }; +}`; diff --git a/plugins/clerk/codegen/get-user.ts b/plugins/clerk/codegen/get-user.ts new file mode 100644 index 00000000..67e91c70 --- /dev/null +++ b/plugins/clerk/codegen/get-user.ts @@ -0,0 +1,33 @@ +/** + * Code generation template for Get User action + * Used when exporting workflows to standalone Next.js projects + */ +export const getUserCodegenTemplate = `export async function clerkGetUserStep(input: { + userId: string; +}) { + "use step"; + + const secretKey = process.env.CLERK_SECRET_KEY; + + if (!secretKey) { + throw new Error('CLERK_SECRET_KEY environment variable is required'); + } + + const response = await fetch( + \`https://api.clerk.com/v1/users/\${input.userId}\`, + { + headers: { + Authorization: \`Bearer \${secretKey}\`, + 'Content-Type': 'application/json', + 'User-Agent': 'workflow-builder.dev', + }, + } + ); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + throw new Error(error.errors?.[0]?.message || \`Failed to get user: \${response.status}\`); + } + + return await response.json(); +}`; diff --git a/plugins/clerk/codegen/update-user.ts b/plugins/clerk/codegen/update-user.ts new file mode 100644 index 00000000..692f7712 --- /dev/null +++ b/plugins/clerk/codegen/update-user.ts @@ -0,0 +1,46 @@ +/** + * Code generation template for Update User action + * Used when exporting workflows to standalone Next.js projects + */ +export const updateUserCodegenTemplate = `export async function clerkUpdateUserStep(input: { + userId: string; + firstName?: string; + lastName?: string; + publicMetadata?: string; + privateMetadata?: string; +}) { + "use step"; + + const secretKey = process.env.CLERK_SECRET_KEY; + + if (!secretKey) { + throw new Error('CLERK_SECRET_KEY environment variable is required'); + } + + const body: Record = {}; + + if (input.firstName) body.first_name = input.firstName; + if (input.lastName) body.last_name = input.lastName; + if (input.publicMetadata) body.public_metadata = JSON.parse(input.publicMetadata); + if (input.privateMetadata) body.private_metadata = JSON.parse(input.privateMetadata); + + const response = await fetch( + \`https://api.clerk.com/v1/users/\${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(() => ({})); + throw new Error(error.errors?.[0]?.message || \`Failed to update user: \${response.status}\`); + } + + return await response.json(); +}`; diff --git a/plugins/clerk/icon.tsx b/plugins/clerk/icon.tsx new file mode 100644 index 00000000..3710a7e2 --- /dev/null +++ b/plugins/clerk/icon.tsx @@ -0,0 +1,21 @@ +export function ClerkIcon({ className }: { className?: string }) { + return ( + + + + + + ); +} diff --git a/plugins/clerk/index.tsx b/plugins/clerk/index.tsx new file mode 100644 index 00000000..37af34df --- /dev/null +++ b/plugins/clerk/index.tsx @@ -0,0 +1,115 @@ +import { User, UserPlus, UserCog, UserX } from "lucide-react"; +import type { IntegrationPlugin } from "../registry"; +import { registerIntegration } from "../registry"; +import { createUserCodegenTemplate } from "./codegen/create-user"; +import { deleteUserCodegenTemplate } from "./codegen/delete-user"; +import { getUserCodegenTemplate } from "./codegen/get-user"; +import { updateUserCodegenTemplate } from "./codegen/update-user"; +import { ClerkIcon } from "./icon"; +import { ClerkSettings } from "./settings"; +import { CreateUserConfigFields } from "./steps/create-user/config"; +import { DeleteUserConfigFields } from "./steps/delete-user/config"; +import { GetUserConfigFields } from "./steps/get-user/config"; +import { UpdateUserConfigFields } from "./steps/update-user/config"; + +// Export step functions for workflow execution +export { clerkGetUserStep } from "./steps/get-user/step"; +export { clerkCreateUserStep } from "./steps/create-user/step"; +export { clerkUpdateUserStep } from "./steps/update-user/step"; +export { clerkDeleteUserStep } from "./steps/delete-user/step"; + +const clerkPlugin: IntegrationPlugin = { + type: "clerk", + label: "Clerk", + description: "User authentication and management", + + icon: { + type: "svg", + value: "ClerkIcon", + svgComponent: ClerkIcon, + }, + + settingsComponent: ClerkSettings, + + formFields: [ + { + id: "clerkSecretKey", + label: "Secret Key", + type: "password", + placeholder: "sk_live_...", + configKey: "clerkSecretKey", + helpText: "Get your secret key from ", + helpLink: { + text: "Clerk Dashboard", + url: "https://dashboard.clerk.com", + }, + }, + ], + + credentialMapping: (config) => { + const creds: Record = {}; + if (config.clerkSecretKey) { + creds.CLERK_SECRET_KEY = String(config.clerkSecretKey); + } + return creds; + }, + + testConfig: { + getTestFunction: async () => { + const { testClerk } = await import("./test"); + return testClerk; + }, + }, + + actions: [ + { + id: "Get User", + label: "Get User", + description: "Fetch a user by ID from Clerk", + category: "Clerk", + icon: User, + stepFunction: "clerkGetUserStep", + stepImportPath: "get-user", + configFields: GetUserConfigFields, + codegenTemplate: getUserCodegenTemplate, + }, + { + id: "Create User", + label: "Create User", + description: "Create a new user in Clerk", + category: "Clerk", + icon: UserPlus, + stepFunction: "clerkCreateUserStep", + stepImportPath: "create-user", + configFields: CreateUserConfigFields, + codegenTemplate: createUserCodegenTemplate, + }, + { + id: "Update User", + label: "Update User", + description: "Update an existing user in Clerk", + category: "Clerk", + icon: UserCog, + stepFunction: "clerkUpdateUserStep", + stepImportPath: "update-user", + configFields: UpdateUserConfigFields, + codegenTemplate: updateUserCodegenTemplate, + }, + { + id: "Delete User", + label: "Delete User", + description: "Delete a user from Clerk", + category: "Clerk", + icon: UserX, + stepFunction: "clerkDeleteUserStep", + stepImportPath: "delete-user", + configFields: DeleteUserConfigFields, + codegenTemplate: deleteUserCodegenTemplate, + }, + ], +}; + +// Auto-register on import +registerIntegration(clerkPlugin); + +export default clerkPlugin; diff --git a/plugins/clerk/settings.tsx b/plugins/clerk/settings.tsx new file mode 100644 index 00000000..8bef3cf5 --- /dev/null +++ b/plugins/clerk/settings.tsx @@ -0,0 +1,47 @@ +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +export function ClerkSettings({ + apiKey, + hasKey, + onApiKeyChange, +}: { + apiKey: string; + hasKey?: boolean; + onApiKeyChange: (key: string) => void; + showCard?: boolean; + config?: Record; + onConfigChange?: (key: string, value: string) => void; +}) { + return ( +
+
+ + onApiKeyChange(e.target.value)} + placeholder={ + hasKey ? "Secret key is configured" : "sk_live_..." + } + type="password" + value={apiKey} + /> +

+ Get your secret key from{" "} + + Clerk Dashboard + + {" "}under API Keys. +

+
+
+ ); +} diff --git a/plugins/clerk/steps/create-user/config.tsx b/plugins/clerk/steps/create-user/config.tsx new file mode 100644 index 00000000..370cae8a --- /dev/null +++ b/plugins/clerk/steps/create-user/config.tsx @@ -0,0 +1,110 @@ +import { Label } from "@/components/ui/label"; +import { TemplateBadgeInput } from "@/components/ui/template-badge-input"; +import { TemplateBadgeTextarea } from "@/components/ui/template-badge-textarea"; + +/** + * Create User Config Fields Component + * UI for configuring the create user action + */ +export function CreateUserConfigFields({ + config, + onUpdateConfig, + disabled, +}: { + config: Record; + onUpdateConfig: (key: string, value: unknown) => void; + disabled?: boolean; +}) { + return ( + <> +
+ + onUpdateConfig("emailAddress", value)} + placeholder="user@example.com or {{NodeName.email}}" + value={(config?.emailAddress as string) || ""} + /> +
+ +
+ + onUpdateConfig("password", value)} + placeholder="Min 8 chars, or leave empty" + value={(config?.password as string) || ""} + /> +

+ Must be at least 8 characters. Leave empty to let user set their own. +

+
+ +
+ + onUpdateConfig("firstName", value)} + placeholder="John or {{NodeName.firstName}}" + value={(config?.firstName as string) || ""} + /> +
+ +
+ + onUpdateConfig("lastName", value)} + placeholder="Doe or {{NodeName.lastName}}" + value={(config?.lastName as string) || ""} + /> +
+ +
+ + onUpdateConfig("publicMetadata", value)} + placeholder='{"role": "user"}' + rows={3} + value={(config?.publicMetadata as string) || ""} + /> +

+ JSON object visible to the frontend. +

+
+ +
+ + onUpdateConfig("privateMetadata", value)} + placeholder='{"internalId": "abc123"}' + rows={3} + value={(config?.privateMetadata as string) || ""} + /> +

+ JSON object only accessible server-side. +

+
+ + ); +} diff --git a/plugins/clerk/steps/create-user/step.ts b/plugins/clerk/steps/create-user/step.ts new file mode 100644 index 00000000..95eb70c6 --- /dev/null +++ b/plugins/clerk/steps/create-user/step.ts @@ -0,0 +1,115 @@ +import "server-only"; + +import { fetchCredentials } from "@/lib/credential-fetcher"; +import { getErrorMessage } from "@/lib/utils"; + +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; +}; + +type CreateUserResult = + | { success: true; user: ClerkUser } + | { success: false; error: string }; + +/** + * Create User Step + * Creates a new user in Clerk + */ +export async function clerkCreateUserStep(input: { + integrationId?: string; + emailAddress: string; + password?: string; + firstName?: string; + lastName?: string; + publicMetadata?: string; + privateMetadata?: string; +}): Promise { + "use step"; + + const credentials = input.integrationId + ? await fetchCredentials(input.integrationId) + : {}; + + const secretKey = credentials.CLERK_SECRET_KEY; + + if (!secretKey) { + return { + success: false, + error: + "CLERK_SECRET_KEY is not configured. Please add it in Project Integrations.", + }; + } + + try { + const body: Record = { + email_address: [input.emailAddress], + }; + + if (input.password) { + body.password = input.password; + } + if (input.firstName) { + body.first_name = input.firstName; + } + if (input.lastName) { + body.last_name = input.lastName; + } + if (input.publicMetadata) { + try { + body.public_metadata = JSON.parse(input.publicMetadata); + } catch { + return { + success: false, + error: "Invalid JSON in public metadata", + }; + } + } + if (input.privateMetadata) { + try { + body.private_metadata = JSON.parse(input.privateMetadata); + } catch { + return { + success: false, + error: "Invalid JSON in private metadata", + }; + } + } + + 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)}`, + }; + } +} diff --git a/plugins/clerk/steps/delete-user/config.tsx b/plugins/clerk/steps/delete-user/config.tsx new file mode 100644 index 00000000..d2ff2392 --- /dev/null +++ b/plugins/clerk/steps/delete-user/config.tsx @@ -0,0 +1,36 @@ +import { Label } from "@/components/ui/label"; +import { TemplateBadgeInput } from "@/components/ui/template-badge-input"; + +/** + * Delete User Config Fields Component + * UI for configuring the delete user action + */ +export function DeleteUserConfigFields({ + config, + onUpdateConfig, + disabled, +}: { + config: Record; + onUpdateConfig: (key: string, value: unknown) => void; + disabled?: boolean; +}) { + return ( + <> +
+ + onUpdateConfig("userId", value)} + placeholder="user_... or {{NodeName.userId}}" + value={(config?.userId as string) || ""} + /> +

+ The Clerk user ID to delete. This action is irreversible. +

+
+ + ); +} diff --git a/plugins/clerk/steps/delete-user/step.ts b/plugins/clerk/steps/delete-user/step.ts new file mode 100644 index 00000000..97fb11cf --- /dev/null +++ b/plugins/clerk/steps/delete-user/step.ts @@ -0,0 +1,69 @@ +import "server-only"; + +import { fetchCredentials } from "@/lib/credential-fetcher"; +import { getErrorMessage } from "@/lib/utils"; + +type DeleteUserResult = + | { success: true; deleted: true; id: string } + | { success: false; error: string }; + +/** + * Delete User Step + * Deletes a user from Clerk + */ +export async function clerkDeleteUserStep(input: { + integrationId?: string; + userId: string; +}): Promise { + "use step"; + + const credentials = input.integrationId + ? await fetchCredentials(input.integrationId) + : {}; + + 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. Example: {{@node-id:Node Label.user.id}}", + }; + } + + try { + const response = await fetch( + `https://api.clerk.com/v1/users/${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, id: input.userId }; + } catch (error) { + return { + success: false, + error: `Failed to delete user: ${getErrorMessage(error)}`, + }; + } +} diff --git a/plugins/clerk/steps/get-user/config.tsx b/plugins/clerk/steps/get-user/config.tsx new file mode 100644 index 00000000..6e6baa3c --- /dev/null +++ b/plugins/clerk/steps/get-user/config.tsx @@ -0,0 +1,36 @@ +import { Label } from "@/components/ui/label"; +import { TemplateBadgeInput } from "@/components/ui/template-badge-input"; + +/** + * Get User Config Fields Component + * UI for configuring the get user action + */ +export function GetUserConfigFields({ + config, + onUpdateConfig, + disabled, +}: { + config: Record; + onUpdateConfig: (key: string, value: unknown) => void; + disabled?: boolean; +}) { + return ( + <> +
+ + onUpdateConfig("userId", value)} + placeholder="user_... or {{NodeName.userId}}" + value={(config?.userId as string) || ""} + /> +

+ The Clerk user ID to fetch. +

+
+ + ); +} diff --git a/plugins/clerk/steps/get-user/step.ts b/plugins/clerk/steps/get-user/step.ts new file mode 100644 index 00000000..add74384 --- /dev/null +++ b/plugins/clerk/steps/get-user/step.ts @@ -0,0 +1,84 @@ +import "server-only"; + +import { fetchCredentials } from "@/lib/credential-fetcher"; +import { getErrorMessage } from "@/lib/utils"; + +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; +}; + +type GetUserResult = + | { success: true; user: ClerkUser } + | { success: false; error: string }; + +/** + * Get User Step + * Fetches a user by ID from Clerk + */ +export async function clerkGetUserStep(input: { + integrationId?: string; + userId: string; +}): Promise { + "use step"; + + const credentials = input.integrationId + ? await fetchCredentials(input.integrationId) + : {}; + + 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/${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)}`, + }; + } +} diff --git a/plugins/clerk/steps/update-user/config.tsx b/plugins/clerk/steps/update-user/config.tsx new file mode 100644 index 00000000..664b9806 --- /dev/null +++ b/plugins/clerk/steps/update-user/config.tsx @@ -0,0 +1,97 @@ +import { Label } from "@/components/ui/label"; +import { TemplateBadgeInput } from "@/components/ui/template-badge-input"; +import { TemplateBadgeTextarea } from "@/components/ui/template-badge-textarea"; + +/** + * Update User Config Fields Component + * UI for configuring the update user action + */ +export function UpdateUserConfigFields({ + config, + onUpdateConfig, + disabled, +}: { + config: Record; + onUpdateConfig: (key: string, value: unknown) => void; + disabled?: boolean; +}) { + return ( + <> +
+ + onUpdateConfig("userId", value)} + placeholder="user_... or {{NodeName.userId}}" + value={(config?.userId as string) || ""} + /> +

+ The Clerk user ID to update. +

+
+ +
+ + onUpdateConfig("firstName", value)} + placeholder="John or {{NodeName.firstName}}" + value={(config?.firstName as string) || ""} + /> +
+ +
+ + onUpdateConfig("lastName", value)} + placeholder="Doe or {{NodeName.lastName}}" + value={(config?.lastName as string) || ""} + /> +
+ +
+ + onUpdateConfig("publicMetadata", value)} + placeholder='{"role": "admin"}' + rows={3} + value={(config?.publicMetadata as string) || ""} + /> +

+ JSON object to merge with existing public metadata. +

+
+ +
+ + onUpdateConfig("privateMetadata", value)} + placeholder='{"stripeId": "cus_..."}' + rows={3} + value={(config?.privateMetadata as string) || ""} + /> +

+ JSON object to merge with existing private metadata. +

+
+ + ); +} diff --git a/plugins/clerk/steps/update-user/step.ts b/plugins/clerk/steps/update-user/step.ts new file mode 100644 index 00000000..a1c33fee --- /dev/null +++ b/plugins/clerk/steps/update-user/step.ts @@ -0,0 +1,119 @@ +import "server-only"; + +import { fetchCredentials } from "@/lib/credential-fetcher"; +import { getErrorMessage } from "@/lib/utils"; + +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; +}; + +type UpdateUserResult = + | { success: true; user: ClerkUser } + | { success: false; error: string }; + +/** + * Update User Step + * Updates an existing user in Clerk + */ +export async function clerkUpdateUserStep(input: { + integrationId?: string; + userId: string; + firstName?: string; + lastName?: string; + publicMetadata?: string; + privateMetadata?: string; +}): Promise { + "use step"; + + const credentials = input.integrationId + ? await fetchCredentials(input.integrationId) + : {}; + + 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. Example: {{@node-id:Node Label.user.id}}", + }; + } + + try { + const body: Record = {}; + + if (input.firstName !== undefined && input.firstName !== "") { + body.first_name = input.firstName; + } + if (input.lastName !== undefined && input.lastName !== "") { + body.last_name = input.lastName; + } + if (input.publicMetadata) { + try { + body.public_metadata = JSON.parse(input.publicMetadata); + } catch { + return { + success: false, + error: "Invalid JSON in public metadata", + }; + } + } + if (input.privateMetadata) { + try { + body.private_metadata = JSON.parse(input.privateMetadata); + } catch { + return { + success: false, + error: "Invalid JSON in private metadata", + }; + } + } + + const response = await fetch( + `https://api.clerk.com/v1/users/${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)}`, + }; + } +} diff --git a/plugins/clerk/test.ts b/plugins/clerk/test.ts new file mode 100644 index 00000000..1f235ea4 --- /dev/null +++ b/plugins/clerk/test.ts @@ -0,0 +1,43 @@ +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", + }, + }); + + 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/index.ts b/plugins/index.ts index 50ffd6ba..09bb785d 100644 --- a/plugins/index.ts +++ b/plugins/index.ts @@ -13,10 +13,11 @@ * 1. Delete the plugin directory * 2. Run: pnpm discover-plugins (or it runs automatically on build) * - * Discovered plugins: ai-gateway, firecrawl, linear, resend, slack, v0 + * Discovered plugins: ai-gateway, clerk, firecrawl, linear, resend, slack, v0 */ import "./ai-gateway"; +import "./clerk"; import "./firecrawl"; import "./linear"; import "./resend"; From 9de8d177fd803f3f4e4bd9c7c0d6bb94d8078567 Mon Sep 17 00:00:00 2001 From: Railly Date: Fri, 28 Nov 2025 19:28:23 -0500 Subject: [PATCH 2/6] fix: add user-agent to test clerk function + fix formatting --- .../[integrationId]/test/route.ts | 11 +++-- .../settings/integration-form-dialog.tsx | 4 +- lib/workflow-codegen-sdk.ts | 48 ++++++++++++++----- lib/workflow-codegen.ts | 16 +++++-- plugins/clerk/test.ts | 9 +++- 5 files changed, 63 insertions(+), 25 deletions(-) diff --git a/app/api/integrations/[integrationId]/test/route.ts b/app/api/integrations/[integrationId]/test/route.ts index 1368a6f7..a4a1de70 100644 --- a/app/api/integrations/[integrationId]/test/route.ts +++ b/app/api/integrations/[integrationId]/test/route.ts @@ -69,9 +69,7 @@ export async function POST( ); break; case "clerk": - result = await testClerkConnection( - integration.config.clerkSecretKey - ); + result = await testClerkConnection(integration.config.clerkSecretKey); break; default: return NextResponse.json( @@ -299,10 +297,13 @@ async function testClerkConnection( } // Validate key format - if (!secretKey.startsWith("sk_live_") && !secretKey.startsWith("sk_test_")) { + if ( + !(secretKey.startsWith("sk_live_") || secretKey.startsWith("sk_test_")) + ) { return { status: "error", - message: "Invalid secret key format. Must start with sk_live_ or sk_test_", + message: + "Invalid secret key format. Must start with sk_live_ or sk_test_", }; } diff --git a/components/settings/integration-form-dialog.tsx b/components/settings/integration-form-dialog.tsx index b3de0e34..396a7a58 100644 --- a/components/settings/integration-form-dialog.tsx +++ b/components/settings/integration-form-dialog.tsx @@ -338,8 +338,8 @@ export function IntegrationFormDialog({ target="_blank" > dashboard.clerk.com - - {" "}under API Keys. + {" "} + under API Keys.

); diff --git a/lib/workflow-codegen-sdk.ts b/lib/workflow-codegen-sdk.ts index a2116df3..084b08ca 100644 --- a/lib/workflow-codegen-sdk.ts +++ b/lib/workflow-codegen-sdk.ts @@ -636,50 +636,74 @@ export function generateWorkflowSDKCode( ]; } - function buildClerkCreateUserParams(config: Record): string[] { + function buildClerkCreateUserParams( + config: Record + ): string[] { const params = [ `emailAddress: \`${convertTemplateToJS((config.emailAddress as string) || "")}\``, "secretKey: process.env.CLERK_SECRET_KEY!", ]; if (config.password) { - params.push(`password: \`${convertTemplateToJS(config.password as string)}\``); + params.push( + `password: \`${convertTemplateToJS(config.password as string)}\`` + ); } if (config.firstName) { - params.push(`firstName: \`${convertTemplateToJS(config.firstName as string)}\``); + params.push( + `firstName: \`${convertTemplateToJS(config.firstName as string)}\`` + ); } if (config.lastName) { - params.push(`lastName: \`${convertTemplateToJS(config.lastName as string)}\``); + params.push( + `lastName: \`${convertTemplateToJS(config.lastName as string)}\`` + ); } if (config.publicMetadata) { - params.push(`publicMetadata: \`${convertTemplateToJS(config.publicMetadata as string)}\``); + params.push( + `publicMetadata: \`${convertTemplateToJS(config.publicMetadata as string)}\`` + ); } if (config.privateMetadata) { - params.push(`privateMetadata: \`${convertTemplateToJS(config.privateMetadata as string)}\``); + params.push( + `privateMetadata: \`${convertTemplateToJS(config.privateMetadata as string)}\`` + ); } return params; } - function buildClerkUpdateUserParams(config: Record): string[] { + function buildClerkUpdateUserParams( + config: Record + ): string[] { const params = [ `userId: \`${convertTemplateToJS((config.userId as string) || "")}\``, "secretKey: process.env.CLERK_SECRET_KEY!", ]; if (config.firstName) { - params.push(`firstName: \`${convertTemplateToJS(config.firstName as string)}\``); + params.push( + `firstName: \`${convertTemplateToJS(config.firstName as string)}\`` + ); } if (config.lastName) { - params.push(`lastName: \`${convertTemplateToJS(config.lastName as string)}\``); + params.push( + `lastName: \`${convertTemplateToJS(config.lastName as string)}\`` + ); } if (config.publicMetadata) { - params.push(`publicMetadata: \`${convertTemplateToJS(config.publicMetadata as string)}\``); + params.push( + `publicMetadata: \`${convertTemplateToJS(config.publicMetadata as string)}\`` + ); } if (config.privateMetadata) { - params.push(`privateMetadata: \`${convertTemplateToJS(config.privateMetadata as string)}\``); + params.push( + `privateMetadata: \`${convertTemplateToJS(config.privateMetadata as string)}\`` + ); } return params; } - function buildClerkDeleteUserParams(config: Record): string[] { + function buildClerkDeleteUserParams( + config: Record + ): string[] { return [ `userId: \`${convertTemplateToJS((config.userId as string) || "")}\``, "secretKey: process.env.CLERK_SECRET_KEY!", diff --git a/lib/workflow-codegen.ts b/lib/workflow-codegen.ts index bda9946c..9dc5c855 100644 --- a/lib/workflow-codegen.ts +++ b/lib/workflow-codegen.ts @@ -636,7 +636,9 @@ export function generateWorkflowCode( ]; if (emailAddress) { - lines.push(`${indent} emailAddress: ${formatTemplateValue(emailAddress)},`); + lines.push( + `${indent} emailAddress: ${formatTemplateValue(emailAddress)},` + ); } if (firstName) { lines.push(`${indent} firstName: ${formatTemplateValue(firstName)},`); @@ -861,15 +863,21 @@ export function generateWorkflowCode( ); } else if (actionType === "Create User") { lines.push( - ...wrapActionCall(generateClerkCreateUserActionCode(node, indent, varName)) + ...wrapActionCall( + generateClerkCreateUserActionCode(node, indent, varName) + ) ); } else if (actionType === "Update User") { lines.push( - ...wrapActionCall(generateClerkUpdateUserActionCode(node, indent, varName)) + ...wrapActionCall( + generateClerkUpdateUserActionCode(node, indent, varName) + ) ); } else if (actionType === "Delete User") { lines.push( - ...wrapActionCall(generateClerkDeleteUserActionCode(node, indent, varName)) + ...wrapActionCall( + generateClerkDeleteUserActionCode(node, indent, varName) + ) ); } else if (outputIsUsed) { lines.push(`${indent}const ${varName} = { status: 'success' };`); diff --git a/plugins/clerk/test.ts b/plugins/clerk/test.ts index 1f235ea4..23898a54 100644 --- a/plugins/clerk/test.ts +++ b/plugins/clerk/test.ts @@ -10,10 +10,14 @@ export async function testClerk(credentials: Record) { } // Validate format - Clerk secret keys start with sk_live_ or sk_test_ - if (!secretKey.startsWith("sk_live_") && !secretKey.startsWith("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_'", + error: + "Invalid secret key format. Clerk secret keys start with 'sk_live_' or 'sk_test_'", }; } @@ -22,6 +26,7 @@ export async function testClerk(credentials: Record) { headers: { Authorization: `Bearer ${secretKey}`, "Content-Type": "application/json", + "User-Agent": "workflow-builder.dev", }, }); From 47613de16d8aa001c64a38962184cb61c1fb174e Mon Sep 17 00:00:00 2001 From: Railly Date: Mon, 1 Dec 2025 19:09:32 -0500 Subject: [PATCH 3/6] fix: use ClerkCredentials type in delete-user step --- plugins/clerk/steps/delete-user.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/clerk/steps/delete-user.ts b/plugins/clerk/steps/delete-user.ts index c4e38ab9..0206121b 100644 --- a/plugins/clerk/steps/delete-user.ts +++ b/plugins/clerk/steps/delete-user.ts @@ -23,7 +23,7 @@ export type ClerkDeleteUserInput = StepInput & */ async function stepHandler( input: ClerkDeleteUserCoreInput, - credentials: { CLERK_SECRET_KEY?: string } + credentials: ClerkCredentials ): Promise { const secretKey = credentials.CLERK_SECRET_KEY; From 08bd23ca6f5f9789adc3ba6b7ecd487eb42c2a79 Mon Sep 17 00:00:00 2001 From: Railly Date: Tue, 2 Dec 2025 13:17:17 -0500 Subject: [PATCH 4/6] fix: address PR review comments - Revert AGENTS.md to upstream version - Remove plugins/clerk/codegen/ folder (auto-generated) - Remove template variable hint from AI prompt (auto-generated) --- AGENTS.md | 5 +++ app/api/ai/generate/route.ts | 3 -- plugins/clerk/codegen/create-user.ts | 47 ---------------------------- plugins/clerk/codegen/delete-user.ts | 34 -------------------- plugins/clerk/codegen/get-user.ts | 33 ------------------- plugins/clerk/codegen/update-user.ts | 46 --------------------------- 6 files changed, 5 insertions(+), 163 deletions(-) delete mode 100644 plugins/clerk/codegen/create-user.ts delete mode 100644 plugins/clerk/codegen/delete-user.ts delete mode 100644 plugins/clerk/codegen/get-user.ts delete mode 100644 plugins/clerk/codegen/update-user.ts diff --git a/AGENTS.md b/AGENTS.md index 3779821a..bbe28478 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -78,3 +78,8 @@ If any of the above commands fail or show errors: - `api.workflow.*` - Workflow CRUD and operations (create, update, delete, deploy, execute, etc.) - **No Barrel Files**: Do not create barrel/index files that re-export from other files +## Plugin Guidelines +- **No SDK Dependencies**: Plugin step files must use `fetch` directly instead of SDK client libraries. Do not add npm package dependencies for API integrations. +- **No dependencies field**: Do not use the `dependencies` field in plugin `index.ts` files. All API calls should use native `fetch`. +- **Why**: Using `fetch` instead of SDKs reduces supply chain attack surface. SDKs have transitive dependencies that could be compromised. + diff --git a/app/api/ai/generate/route.ts b/app/api/ai/generate/route.ts index e4d7d68e..d79f483b 100644 --- a/app/api/ai/generate/route.ts +++ b/app/api/ai/generate/route.ts @@ -220,9 +220,6 @@ Edge structure: "type": "default" } -TEMPLATE VARIABLES: To reference data from another node, use {{@nodeId:Label.field}} -Example: If node id="create-user" label="Create User" returns {user: {id: "..."}}, reference it as {{@create-user:Create User.user.id}} - WORKFLOW FLOW: - Trigger connects to first action - Actions connect in sequence or to multiple branches diff --git a/plugins/clerk/codegen/create-user.ts b/plugins/clerk/codegen/create-user.ts deleted file mode 100644 index 038bc8a0..00000000 --- a/plugins/clerk/codegen/create-user.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Code generation template for Create User action - * Used when exporting workflows to standalone Next.js projects - */ -export const createUserCodegenTemplate = `export async function clerkCreateUserStep(input: { - emailAddress: string; - password?: string; - firstName?: string; - lastName?: string; - publicMetadata?: string; - privateMetadata?: string; -}) { - "use step"; - - const secretKey = process.env.CLERK_SECRET_KEY; - - if (!secretKey) { - throw new Error('CLERK_SECRET_KEY environment variable is required'); - } - - const body: Record = { - email_address: [input.emailAddress], - }; - - if (input.password) body.password = input.password; - if (input.firstName) body.first_name = input.firstName; - if (input.lastName) body.last_name = input.lastName; - if (input.publicMetadata) body.public_metadata = JSON.parse(input.publicMetadata); - if (input.privateMetadata) body.private_metadata = JSON.parse(input.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(() => ({})); - throw new Error(error.errors?.[0]?.message || \`Failed to create user: \${response.status}\`); - } - - return await response.json(); -}`; diff --git a/plugins/clerk/codegen/delete-user.ts b/plugins/clerk/codegen/delete-user.ts deleted file mode 100644 index 68e70ba8..00000000 --- a/plugins/clerk/codegen/delete-user.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Code generation template for Delete User action - * Used when exporting workflows to standalone Next.js projects - */ -export const deleteUserCodegenTemplate = `export async function clerkDeleteUserStep(input: { - userId: string; -}) { - "use step"; - - const secretKey = process.env.CLERK_SECRET_KEY; - - if (!secretKey) { - throw new Error('CLERK_SECRET_KEY environment variable is required'); - } - - const response = await fetch( - \`https://api.clerk.com/v1/users/\${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(() => ({})); - throw new Error(error.errors?.[0]?.message || \`Failed to delete user: \${response.status}\`); - } - - return { deleted: true, id: input.userId }; -}`; diff --git a/plugins/clerk/codegen/get-user.ts b/plugins/clerk/codegen/get-user.ts deleted file mode 100644 index 67e91c70..00000000 --- a/plugins/clerk/codegen/get-user.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Code generation template for Get User action - * Used when exporting workflows to standalone Next.js projects - */ -export const getUserCodegenTemplate = `export async function clerkGetUserStep(input: { - userId: string; -}) { - "use step"; - - const secretKey = process.env.CLERK_SECRET_KEY; - - if (!secretKey) { - throw new Error('CLERK_SECRET_KEY environment variable is required'); - } - - const response = await fetch( - \`https://api.clerk.com/v1/users/\${input.userId}\`, - { - headers: { - Authorization: \`Bearer \${secretKey}\`, - 'Content-Type': 'application/json', - 'User-Agent': 'workflow-builder.dev', - }, - } - ); - - if (!response.ok) { - const error = await response.json().catch(() => ({})); - throw new Error(error.errors?.[0]?.message || \`Failed to get user: \${response.status}\`); - } - - return await response.json(); -}`; diff --git a/plugins/clerk/codegen/update-user.ts b/plugins/clerk/codegen/update-user.ts deleted file mode 100644 index 692f7712..00000000 --- a/plugins/clerk/codegen/update-user.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Code generation template for Update User action - * Used when exporting workflows to standalone Next.js projects - */ -export const updateUserCodegenTemplate = `export async function clerkUpdateUserStep(input: { - userId: string; - firstName?: string; - lastName?: string; - publicMetadata?: string; - privateMetadata?: string; -}) { - "use step"; - - const secretKey = process.env.CLERK_SECRET_KEY; - - if (!secretKey) { - throw new Error('CLERK_SECRET_KEY environment variable is required'); - } - - const body: Record = {}; - - if (input.firstName) body.first_name = input.firstName; - if (input.lastName) body.last_name = input.lastName; - if (input.publicMetadata) body.public_metadata = JSON.parse(input.publicMetadata); - if (input.privateMetadata) body.private_metadata = JSON.parse(input.privateMetadata); - - const response = await fetch( - \`https://api.clerk.com/v1/users/\${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(() => ({})); - throw new Error(error.errors?.[0]?.message || \`Failed to update user: \${response.status}\`); - } - - return await response.json(); -}`; From 5e2ff19e7e55f0f889360bfe22d23d19174f0eb2 Mon Sep 17 00:00:00 2001 From: Railly Date: Tue, 2 Dec 2025 13:39:05 -0500 Subject: [PATCH 5/6] fix: align Clerk plugin with project conventions - Remove codegen imports (auto-generated) - Add outputFields to all actions - Add maxRetries = 0 to all step functions --- plugins/clerk/index.ts | 24 ++++++++++++++++-------- plugins/clerk/steps/create-user.ts | 1 + plugins/clerk/steps/delete-user.ts | 1 + plugins/clerk/steps/get-user.ts | 1 + plugins/clerk/steps/update-user.ts | 1 + 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/plugins/clerk/index.ts b/plugins/clerk/index.ts index 755612ad..490bc5eb 100644 --- a/plugins/clerk/index.ts +++ b/plugins/clerk/index.ts @@ -1,9 +1,5 @@ import type { IntegrationPlugin } from "../registry"; import { registerIntegration } from "../registry"; -import { createUserCodegenTemplate } from "./codegen/create-user"; -import { deleteUserCodegenTemplate } from "./codegen/delete-user"; -import { getUserCodegenTemplate } from "./codegen/get-user"; -import { updateUserCodegenTemplate } from "./codegen/update-user"; import { ClerkIcon } from "./icon"; const clerkPlugin: IntegrationPlugin = { @@ -44,7 +40,11 @@ const clerkPlugin: IntegrationPlugin = { category: "Clerk", stepFunction: "clerkGetUserStep", stepImportPath: "get-user", - codegenTemplate: getUserCodegenTemplate, + outputFields: [ + { field: "user.id", description: "User ID" }, + { field: "user.first_name", description: "First name" }, + { field: "user.last_name", description: "Last name" }, + ], configFields: [ { key: "userId", @@ -63,7 +63,11 @@ const clerkPlugin: IntegrationPlugin = { category: "Clerk", stepFunction: "clerkCreateUserStep", stepImportPath: "create-user", - codegenTemplate: createUserCodegenTemplate, + outputFields: [ + { field: "user.id", description: "User ID" }, + { field: "user.first_name", description: "First name" }, + { field: "user.last_name", description: "Last name" }, + ], configFields: [ { key: "emailAddress", @@ -124,7 +128,11 @@ const clerkPlugin: IntegrationPlugin = { category: "Clerk", stepFunction: "clerkUpdateUserStep", stepImportPath: "update-user", - codegenTemplate: updateUserCodegenTemplate, + outputFields: [ + { field: "user.id", description: "User ID" }, + { field: "user.first_name", description: "First name" }, + { field: "user.last_name", description: "Last name" }, + ], configFields: [ { key: "userId", @@ -176,7 +184,7 @@ const clerkPlugin: IntegrationPlugin = { category: "Clerk", stepFunction: "clerkDeleteUserStep", stepImportPath: "delete-user", - codegenTemplate: deleteUserCodegenTemplate, + outputFields: [{ field: "deleted", description: "Deletion success" }], configFields: [ { key: "userId", diff --git a/plugins/clerk/steps/create-user.ts b/plugins/clerk/steps/create-user.ts index b2ab5e36..91ae703d 100644 --- a/plugins/clerk/steps/create-user.ts +++ b/plugins/clerk/steps/create-user.ts @@ -135,5 +135,6 @@ export async function clerkCreateUserStep( 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 index 0206121b..f53a507d 100644 --- a/plugins/clerk/steps/delete-user.ts +++ b/plugins/clerk/steps/delete-user.ts @@ -88,5 +88,6 @@ export async function clerkDeleteUserStep( 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 index 6a5d6437..9e0e33b7 100644 --- a/plugins/clerk/steps/get-user.ts +++ b/plugins/clerk/steps/get-user.ts @@ -102,5 +102,6 @@ export async function clerkGetUserStep( 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 index 7414d693..5ab073a5 100644 --- a/plugins/clerk/steps/update-user.ts +++ b/plugins/clerk/steps/update-user.ts @@ -132,5 +132,6 @@ export async function clerkUpdateUserStep( return withStepLogging(input, () => stepHandler(input, credentials)); } +clerkUpdateUserStep.maxRetries = 0; export const _integrationType = "clerk"; From bf29126257bb930e1c70cacb20ec2f32659dc715 Mon Sep 17 00:00:00 2001 From: Railly Date: Fri, 5 Dec 2025 11:51:34 -0500 Subject: [PATCH 6/6] fix: address PR review feedback for Clerk plugin - Add encodeURIComponent to userId in URL paths (security) - Return explicit errors on invalid JSON metadata instead of silent fallback - Extract ClerkUser type to shared types.ts file --- plugins/clerk/steps/create-user.ts | 25 +++++++++---------------- plugins/clerk/steps/delete-user.ts | 2 +- plugins/clerk/steps/get-user.ts | 18 ++---------------- plugins/clerk/steps/update-user.ts | 27 ++++++++++----------------- plugins/clerk/types.ts | 14 ++++++++++++++ 5 files changed, 36 insertions(+), 50 deletions(-) create mode 100644 plugins/clerk/types.ts diff --git a/plugins/clerk/steps/create-user.ts b/plugins/clerk/steps/create-user.ts index 91ae703d..53b0368a 100644 --- a/plugins/clerk/steps/create-user.ts +++ b/plugins/clerk/steps/create-user.ts @@ -4,20 +4,7 @@ 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 ClerkUser = { - id: string; - first_name: string | null; - last_name: string | null; - email_addresses: Array<{ - id: string; - email_address: string; - }>; - public_metadata: Record; - private_metadata: Record; - created_at: number; - updated_at: number; -}; +import type { ClerkUser } from "../types"; type CreateUserResult = | { success: true; user: ClerkUser } @@ -80,14 +67,20 @@ async function stepHandler( try { body.public_metadata = JSON.parse(input.publicMetadata); } catch { - body.public_metadata = input.publicMetadata; + return { + success: false, + error: "Invalid JSON format for publicMetadata", + }; } } if (input.privateMetadata) { try { body.private_metadata = JSON.parse(input.privateMetadata); } catch { - body.private_metadata = input.privateMetadata; + return { + success: false, + error: "Invalid JSON format for privateMetadata", + }; } } diff --git a/plugins/clerk/steps/delete-user.ts b/plugins/clerk/steps/delete-user.ts index f53a507d..b3ff451b 100644 --- a/plugins/clerk/steps/delete-user.ts +++ b/plugins/clerk/steps/delete-user.ts @@ -44,7 +44,7 @@ async function stepHandler( try { const response = await fetch( - `https://api.clerk.com/v1/users/${input.userId}`, + `https://api.clerk.com/v1/users/${encodeURIComponent(input.userId)}`, { method: "DELETE", headers: { diff --git a/plugins/clerk/steps/get-user.ts b/plugins/clerk/steps/get-user.ts index 9e0e33b7..83864e3a 100644 --- a/plugins/clerk/steps/get-user.ts +++ b/plugins/clerk/steps/get-user.ts @@ -4,21 +4,7 @@ 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 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; -}; +import type { ClerkUser } from "../types"; type GetUserResult = | { success: true; user: ClerkUser } @@ -59,7 +45,7 @@ async function stepHandler( try { const response = await fetch( - `https://api.clerk.com/v1/users/${input.userId}`, + `https://api.clerk.com/v1/users/${encodeURIComponent(input.userId)}`, { headers: { Authorization: `Bearer ${secretKey}`, diff --git a/plugins/clerk/steps/update-user.ts b/plugins/clerk/steps/update-user.ts index 5ab073a5..6083270a 100644 --- a/plugins/clerk/steps/update-user.ts +++ b/plugins/clerk/steps/update-user.ts @@ -4,20 +4,7 @@ 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 ClerkUser = { - id: string; - first_name: string | null; - last_name: string | null; - email_addresses: Array<{ - id: string; - email_address: string; - }>; - public_metadata: Record; - private_metadata: Record; - created_at: number; - updated_at: number; -}; +import type { ClerkUser } from "../types"; type UpdateUserResult = | { success: true; user: ClerkUser } @@ -74,19 +61,25 @@ async function stepHandler( try { body.public_metadata = JSON.parse(input.publicMetadata); } catch { - body.public_metadata = input.publicMetadata; + return { + success: false, + error: "Invalid JSON format for publicMetadata", + }; } } if (input.privateMetadata) { try { body.private_metadata = JSON.parse(input.privateMetadata); } catch { - body.private_metadata = input.privateMetadata; + return { + success: false, + error: "Invalid JSON format for privateMetadata", + }; } } const response = await fetch( - `https://api.clerk.com/v1/users/${input.userId}`, + `https://api.clerk.com/v1/users/${encodeURIComponent(input.userId)}`, { method: "PATCH", headers: { diff --git a/plugins/clerk/types.ts b/plugins/clerk/types.ts new file mode 100644 index 00000000..269bb669 --- /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; +};