-
Notifications
You must be signed in to change notification settings - Fork 131
feat: Add Clerk integration plugin #76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
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
|
@Railly is attempting to deploy a commit to the Vercel Labs Team on Vercel. A member of the Team first needs to authorize it. |
|
Hey @Railly, the plugin system was reworked to make things simpler, and your PR needs a few updates to stay compatible. Let me know if you want any help or if you’d prefer I take care of it. I’d really love to get this plugin in. Sorry for the inconvenience. |
|
No problem, I'll give it a try tomorrow. I'll let you know if I need any help |
- Refactored Clerk plugin to use declarative configFields - Migrated from nested step structure to flat files - Added credentials.ts for type definitions - Updated all 4 actions: get-user, create-user, update-user, delete-user - Added template variable hint in AI prompt for proper node referencing
- Add publicMetadata and privateMetadata to Clerk code generators - Remove secretKey from Clerk param builders (handled internally) - Add template variable hint in AI prompt
|
@ctate updated! let me know if that works! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additional Suggestion:
The Clerk action codegen templates return raw Clerk API responses instead of wrapped success/error objects, inconsistent with the actual step functions that return { success, user/error }.
View Details
📝 Patch Details
diff --git a/lib/codegen-registry.ts b/lib/codegen-registry.ts
index 375bbe0..7174ab8 100644
--- a/lib/codegen-registry.ts
+++ b/lib/codegen-registry.ts
@@ -17,7 +17,7 @@
export const AUTO_GENERATED_TEMPLATES: Record<string, string> = {
"ai-gateway/generate-text": `import { createGateway, generateObject, generateText } from "ai";
import { z } from "zod";
-import { fetchCredentials } from './lib/credential-helper';
+import { fetchCredentials } from "./lib/credential-helper";
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
@@ -36,10 +36,12 @@ export type GenerateTextCoreInput = {
aiSchema?: string;
};
-export async function generateTextStep(input: GenerateTextCoreInput): Promise<GenerateTextResult> {
+export async function generateTextStep(
+ input: GenerateTextCoreInput,
+): Promise<GenerateTextResult> {
"use step";
const credentials = await fetchCredentials("ai-gateway");
-const apiKey = credentials.AI_GATEWAY_API_KEY;
+ const apiKey = credentials.AI_GATEWAY_API_KEY;
if (!apiKey) {
return {
@@ -92,14 +94,12 @@ const apiKey = credentials.AI_GATEWAY_API_KEY;
error: \`Text generation failed: \${message}\`,
};
}
-}`,
+}
+`,
"ai-gateway/generate-image": `import type { ImageModelV2 } from "@ai-sdk/provider";
-import {
- createGateway,
- experimental_generateImage as generateImage,
-} from "ai";
-import { fetchCredentials } from './lib/credential-helper';
+import { createGateway, experimental_generateImage as generateImage } from "ai";
+import { fetchCredentials } from "./lib/credential-helper";
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
@@ -115,10 +115,12 @@ export type GenerateImageCoreInput = {
imagePrompt: string;
};
-export async function generateImageStep(input: GenerateImageCoreInput): Promise<GenerateImageResult> {
+export async function generateImageStep(
+ input: GenerateImageCoreInput,
+): Promise<GenerateImageResult> {
"use step";
const credentials = await fetchCredentials("ai-gateway");
-const apiKey = credentials.AI_GATEWAY_API_KEY;
+ const apiKey = credentials.AI_GATEWAY_API_KEY;
if (!apiKey) {
return {
@@ -158,10 +160,10 @@ const apiKey = credentials.AI_GATEWAY_API_KEY;
error: \`Image generation failed: \${message}\`,
};
}
-}`,
+}
+`,
- "clerk/get-user": `
-import { fetchCredentials } from './lib/credential-helper';
+ "clerk/get-user": `import { fetchCredentials } from "./lib/credential-helper";
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
@@ -176,10 +178,12 @@ export type ClerkGetUserCoreInput = {
userId: string;
};
-export async function clerkGetUserStep(input: ClerkGetUserCoreInput): Promise<GetUserResult> {
+export async function clerkGetUserStep(
+ input: ClerkGetUserCoreInput,
+): Promise<GetUserResult> {
"use step";
const credentials = await fetchCredentials("clerk");
-const secretKey = credentials.CLERK_SECRET_KEY;
+ const secretKey = credentials.CLERK_SECRET_KEY;
if (!secretKey) {
return {
@@ -205,7 +209,7 @@ const secretKey = credentials.CLERK_SECRET_KEY;
"Content-Type": "application/json",
"User-Agent": "workflow-builder.dev",
},
- }
+ },
);
if (!response.ok) {
@@ -213,7 +217,8 @@ const secretKey = credentials.CLERK_SECRET_KEY;
return {
success: false,
error:
- error.errors?.[0]?.message || \`Failed to get user: \${response.status}\`,
+ error.errors?.[0]?.message ||
+ \`Failed to get user: \${response.status}\`,
};
}
@@ -225,10 +230,10 @@ const secretKey = credentials.CLERK_SECRET_KEY;
error: \`Failed to get user: \${getErrorMessage(error)}\`,
};
}
-}`,
+}
+`,
- "clerk/create-user": `
-import { fetchCredentials } from './lib/credential-helper';
+ "clerk/create-user": `import { fetchCredentials } from "./lib/credential-helper";
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
@@ -248,10 +253,12 @@ export type ClerkCreateUserCoreInput = {
privateMetadata?: string;
};
-export async function clerkCreateUserStep(input: ClerkCreateUserCoreInput): Promise<CreateUserResult> {
+export async function clerkCreateUserStep(
+ input: ClerkCreateUserCoreInput,
+): Promise<CreateUserResult> {
"use step";
const credentials = await fetchCredentials("clerk");
-const secretKey = credentials.CLERK_SECRET_KEY;
+ const secretKey = credentials.CLERK_SECRET_KEY;
if (!secretKey) {
return {
@@ -326,10 +333,10 @@ const secretKey = credentials.CLERK_SECRET_KEY;
error: \`Failed to create user: \${getErrorMessage(error)}\`,
};
}
-}`,
+}
+`,
- "clerk/update-user": `
-import { fetchCredentials } from './lib/credential-helper';
+ "clerk/update-user": `import { fetchCredentials } from "./lib/credential-helper";
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
@@ -348,10 +355,12 @@ export type ClerkUpdateUserCoreInput = {
privateMetadata?: string;
};
-export async function clerkUpdateUserStep(input: ClerkUpdateUserCoreInput): Promise<UpdateUserResult> {
+export async function clerkUpdateUserStep(
+ input: ClerkUpdateUserCoreInput,
+): Promise<UpdateUserResult> {
"use step";
const credentials = await fetchCredentials("clerk");
-const secretKey = credentials.CLERK_SECRET_KEY;
+ const secretKey = credentials.CLERK_SECRET_KEY;
if (!secretKey) {
return {
@@ -403,7 +412,7 @@ const secretKey = credentials.CLERK_SECRET_KEY;
"User-Agent": "workflow-builder.dev",
},
body: JSON.stringify(body),
- }
+ },
);
if (!response.ok) {
@@ -424,10 +433,10 @@ const secretKey = credentials.CLERK_SECRET_KEY;
error: \`Failed to update user: \${getErrorMessage(error)}\`,
};
}
-}`,
+}
+`,
- "clerk/delete-user": `
-import { fetchCredentials } from './lib/credential-helper';
+ "clerk/delete-user": `import { fetchCredentials } from "./lib/credential-helper";
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
@@ -442,10 +451,12 @@ export type ClerkDeleteUserCoreInput = {
userId: string;
};
-export async function clerkDeleteUserStep(input: ClerkDeleteUserCoreInput): Promise<DeleteUserResult> {
+export async function clerkDeleteUserStep(
+ input: ClerkDeleteUserCoreInput,
+): Promise<DeleteUserResult> {
"use step";
const credentials = await fetchCredentials("clerk");
-const secretKey = credentials.CLERK_SECRET_KEY;
+ const secretKey = credentials.CLERK_SECRET_KEY;
if (!secretKey) {
return {
@@ -472,7 +483,7 @@ const secretKey = credentials.CLERK_SECRET_KEY;
"Content-Type": "application/json",
"User-Agent": "workflow-builder.dev",
},
- }
+ },
);
if (!response.ok) {
@@ -492,10 +503,10 @@ const secretKey = credentials.CLERK_SECRET_KEY;
error: \`Failed to delete user: \${getErrorMessage(error)}\`,
};
}
-}`,
+}
+`,
- "firecrawl/scrape": `
-import { fetchCredentials } from './lib/credential-helper';
+ "firecrawl/scrape": `import { fetchCredentials } from "./lib/credential-helper";
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
@@ -512,10 +523,12 @@ export type FirecrawlScrapeCoreInput = {
formats?: ("markdown" | "html" | "rawHtml" | "links" | "screenshot")[];
};
-export async function firecrawlScrapeStep(input: FirecrawlScrapeCoreInput): Promise<ScrapeResult> {
+export async function firecrawlScrapeStep(
+ input: FirecrawlScrapeCoreInput,
+): Promise<ScrapeResult> {
"use step";
const credentials = await fetchCredentials("firecrawl");
-const apiKey = credentials.FIRECRAWL_API_KEY;
+ const apiKey = credentials.FIRECRAWL_API_KEY;
if (!apiKey) {
throw new Error("Firecrawl API Key is not configured.");
@@ -552,10 +565,10 @@ const apiKey = credentials.FIRECRAWL_API_KEY;
} catch (error) {
throw new Error(\`Failed to scrape: \${getErrorMessage(error)}\`);
}
-}`,
+}
+`,
- "firecrawl/search": `
-import { fetchCredentials } from './lib/credential-helper';
+ "firecrawl/search": `import { fetchCredentials } from "./lib/credential-helper";
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
@@ -574,10 +587,12 @@ export type FirecrawlSearchCoreInput = {
};
};
-export async function firecrawlSearchStep(input: FirecrawlSearchCoreInput): Promise<SearchResult> {
+export async function firecrawlSearchStep(
+ input: FirecrawlSearchCoreInput,
+): Promise<SearchResult> {
"use step";
const credentials = await fetchCredentials("firecrawl");
-const apiKey = credentials.FIRECRAWL_API_KEY;
+ const apiKey = credentials.FIRECRAWL_API_KEY;
if (!apiKey) {
throw new Error("Firecrawl API Key is not configured.");
@@ -614,10 +629,10 @@ const apiKey = credentials.FIRECRAWL_API_KEY;
} catch (error) {
throw new Error(\`Failed to search: \${getErrorMessage(error)}\`);
}
-}`,
+}
+`,
- "linear/create-ticket": `
-import { fetchCredentials } from './lib/credential-helper';
+ "linear/create-ticket": `import { fetchCredentials } from "./lib/credential-helper";
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
@@ -633,10 +648,12 @@ export type CreateTicketCoreInput = {
ticketDescription: string;
};
-export async function createTicketStep(input: CreateTicketCoreInput): Promise<CreateTicketResult> {
+export async function createTicketStep(
+ input: CreateTicketCoreInput,
+): Promise<CreateTicketResult> {
"use step";
const credentials = await fetchCredentials("linear");
-const apiKey = credentials.LINEAR_API_KEY;
+ const apiKey = credentials.LINEAR_API_KEY;
const teamId = credentials.LINEAR_TEAM_ID;
if (!apiKey) {
@@ -653,7 +670,7 @@ const apiKey = credentials.LINEAR_API_KEY;
if (!targetTeamId) {
const teamsResult = await linearQuery<TeamsQueryResponse>(
apiKey,
- \`query { teams { nodes { id name } } }\`
+ \`query { teams { nodes { id name } } }\`,
);
if (teamsResult.errors?.length) {
@@ -689,7 +706,7 @@ const apiKey = credentials.LINEAR_API_KEY;
title: input.ticketTitle,
description: input.ticketDescription,
teamId: targetTeamId,
- }
+ },
);
if (createResult.errors?.length) {
@@ -719,10 +736,10 @@ const apiKey = credentials.LINEAR_API_KEY;
error: \`Failed to create ticket: \${getErrorMessage(error)}\`,
};
}
-}`,
+}
+`,
- "linear/find-issues": `
-import { fetchCredentials } from './lib/credential-helper';
+ "linear/find-issues": `import { fetchCredentials } from "./lib/credential-helper";
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
@@ -740,10 +757,12 @@ export type FindIssuesCoreInput = {
linearLabel?: string;
};
-export async function findIssuesStep(input: FindIssuesCoreInput): Promise<FindIssuesResult> {
+export async function findIssuesStep(
+ input: FindIssuesCoreInput,
+): Promise<FindIssuesResult> {
"use step";
const credentials = await fetchCredentials("linear");
-const apiKey = credentials.LINEAR_API_KEY;
+ const apiKey = credentials.LINEAR_API_KEY;
if (!apiKey) {
return {
@@ -789,7 +808,7 @@ const apiKey = credentials.LINEAR_API_KEY;
}
}
}\`,
- { filter: Object.keys(filter).length > 0 ? filter : undefined }
+ { filter: Object.keys(filter).length > 0 ? filter : undefined },
);
if (result.errors?.length) {
@@ -807,7 +826,7 @@ const apiKey = credentials.LINEAR_API_KEY;
state: issue.state?.name || "Unknown",
priority: issue.priority,
assigneeId: issue.assigneeId || undefined,
- })
+ }),
);
return {
@@ -821,10 +840,10 @@ const apiKey = credentials.LINEAR_API_KEY;
error: \`Failed to find issues: \${getErrorMessage(error)}\`,
};
}
-}`,
+}
+`,
- "resend/send-email": `
-import { fetchCredentials } from './lib/credential-helper';
+ "resend/send-email": `import { fetchCredentials } from "./lib/credential-helper";
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
@@ -848,10 +867,12 @@ export type SendEmailCoreInput = {
idempotencyKey?: string;
};
-export async function sendEmailStep(input: SendEmailCoreInput): Promise<SendEmailResult> {
+export async function sendEmailStep(
+ input: SendEmailCoreInput,
+): Promise<SendEmailResult> {
"use step";
const credentials = await fetchCredentials("resend");
-const apiKey = credentials.RESEND_API_KEY;
+ const apiKey = credentials.RESEND_API_KEY;
const fromEmail = credentials.RESEND_FROM_EMAIL;
if (!apiKey) {
@@ -901,7 +922,8 @@ const apiKey = credentials.RESEND_API_KEY;
const errorData = (await response.json()) as ResendErrorResponse;
return {
success: false,
- error: errorData.message || \`HTTP \${response.status}: Failed to send email\`,
+ error:
+ errorData.message || \`HTTP \${response.status}: Failed to send email\`,
};
}
@@ -914,10 +936,10 @@ const apiKey = credentials.RESEND_API_KEY;
error: \`Failed to send email: \${message}\`,
};
}
-}`,
+}
+`,
- "slack/send-message": `
-import { fetchCredentials } from './lib/credential-helper';
+ "slack/send-message": `import { fetchCredentials } from "./lib/credential-helper";
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
@@ -933,10 +955,12 @@ export type SendSlackMessageCoreInput = {
slackMessage: string;
};
-export async function sendSlackMessageStep(input: SendSlackMessageCoreInput): Promise<SendSlackMessageResult> {
+export async function sendSlackMessageStep(
+ input: SendSlackMessageCoreInput,
+): Promise<SendSlackMessageResult> {
"use step";
const credentials = await fetchCredentials("slack");
-const apiKey = credentials.SLACK_API_KEY;
+ const apiKey = credentials.SLACK_API_KEY;
if (!apiKey) {
return {
@@ -986,10 +1010,10 @@ const apiKey = credentials.SLACK_API_KEY;
error: \`Failed to send Slack message: \${getErrorMessage(error)}\`,
};
}
-}`,
+}
+`,
- "superagent/guard": `
-import { fetchCredentials } from './lib/credential-helper';
+ "superagent/guard": `import { fetchCredentials } from "./lib/credential-helper";
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
@@ -1007,10 +1031,12 @@ export type SuperagentGuardCoreInput = {
text: string;
};
-export async function superagentGuardStep(input: SuperagentGuardCoreInput): Promise<GuardResult> {
+export async function superagentGuardStep(
+ input: SuperagentGuardCoreInput,
+): Promise<GuardResult> {
"use step";
const credentials = await fetchCredentials("superagent");
-const apiKey = credentials.SUPERAGENT_API_KEY;
+ const apiKey = credentials.SUPERAGENT_API_KEY;
if (!apiKey) {
throw new Error("Superagent API Key is not configured.");
@@ -1039,7 +1065,7 @@ const apiKey = credentials.SUPERAGENT_API_KEY;
if (!content || typeof content !== "object") {
throw new Error(
- "Invalid Guard API response: missing or invalid content structure"
+ "Invalid Guard API response: missing or invalid content structure",
);
}
@@ -1049,7 +1075,7 @@ const apiKey = credentials.SUPERAGENT_API_KEY;
(classification !== "allow" && classification !== "block")
) {
throw new Error(
- \`Invalid Guard API response: missing or invalid classification (received: \${JSON.stringify(classification)})\`
+ \`Invalid Guard API response: missing or invalid classification (received: \${JSON.stringify(classification)})\`,
);
}
@@ -1062,10 +1088,10 @@ const apiKey = credentials.SUPERAGENT_API_KEY;
} catch (error) {
throw new Error(\`Failed to analyze text: \${getErrorMessage(error)}\`);
}
-}`,
+}
+`,
- "superagent/redact": `
-import { fetchCredentials } from './lib/credential-helper';
+ "superagent/redact": `import { fetchCredentials } from "./lib/credential-helper";
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
@@ -1082,10 +1108,12 @@ export type SuperagentRedactCoreInput = {
entities?: string[] | string;
};
-export async function superagentRedactStep(input: SuperagentRedactCoreInput): Promise<RedactResult> {
+export async function superagentRedactStep(
+ input: SuperagentRedactCoreInput,
+): Promise<RedactResult> {
"use step";
const credentials = await fetchCredentials("superagent");
-const apiKey = credentials.SUPERAGENT_API_KEY;
+ const apiKey = credentials.SUPERAGENT_API_KEY;
if (!apiKey) {
throw new Error("Superagent API Key is not configured.");
@@ -1138,10 +1166,11 @@ const apiKey = credentials.SUPERAGENT_API_KEY;
} catch (error) {
throw new Error(\`Failed to redact text: \${getErrorMessage(error)}\`);
}
-}`,
+}
+`,
"v0/create-chat": `import { createClient, type ChatsCreateResponse } from "v0-sdk";
-import { fetchCredentials } from './lib/credential-helper';
+import { fetchCredentials } from "./lib/credential-helper";
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
@@ -1157,10 +1186,12 @@ export type CreateChatCoreInput = {
system?: string;
};
-export async function createChatStep(input: CreateChatCoreInput): Promise<CreateChatResult> {
+export async function createChatStep(
+ input: CreateChatCoreInput,
+): Promise<CreateChatResult> {
"use step";
const credentials = await fetchCredentials("v0");
-const apiKey = credentials.V0_API_KEY;
+ const apiKey = credentials.V0_API_KEY;
if (!apiKey) {
return {
@@ -1190,10 +1221,11 @@ const apiKey = credentials.V0_API_KEY;
error: \`Failed to create chat: \${getErrorMessage(error)}\`,
};
}
-}`,
+}
+`,
"v0/send-message": `import { createClient, type ChatsSendMessageResponse } from "v0-sdk";
-import { fetchCredentials } from './lib/credential-helper';
+import { fetchCredentials } from "./lib/credential-helper";
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
@@ -1209,10 +1241,12 @@ export type SendMessageCoreInput = {
message: string;
};
-export async function sendMessageStep(input: SendMessageCoreInput): Promise<SendMessageResult> {
+export async function sendMessageStep(
+ input: SendMessageCoreInput,
+): Promise<SendMessageResult> {
"use step";
const credentials = await fetchCredentials("v0");
-const apiKey = credentials.V0_API_KEY;
+ const apiKey = credentials.V0_API_KEY;
if (!apiKey) {
return {
@@ -1241,7 +1275,8 @@ const apiKey = credentials.V0_API_KEY;
error: \`Failed to send message: \${getErrorMessage(error)}\`,
};
}
-}`,
+}
+`,
};
/**
diff --git a/plugins/clerk/codegen/create-user.ts b/plugins/clerk/codegen/create-user.ts
index 038bc8a..ecf0f8d 100644
--- a/plugins/clerk/codegen/create-user.ts
+++ b/plugins/clerk/codegen/create-user.ts
@@ -40,8 +40,12 @@ export const createUserCodegenTemplate = `export async function clerkCreateUserS
if (!response.ok) {
const error = await response.json().catch(() => ({}));
- throw new Error(error.errors?.[0]?.message || \`Failed to create user: \${response.status}\`);
+ return {
+ success: false,
+ error: error.errors?.[0]?.message || \`Failed to create user: \${response.status}\`,
+ };
}
- return await response.json();
+ const user = await response.json();
+ return { success: true, user };
}`;
diff --git a/plugins/clerk/codegen/delete-user.ts b/plugins/clerk/codegen/delete-user.ts
index 68e70ba..d7d5aea 100644
--- a/plugins/clerk/codegen/delete-user.ts
+++ b/plugins/clerk/codegen/delete-user.ts
@@ -27,8 +27,11 @@ export const deleteUserCodegenTemplate = `export async function clerkDeleteUserS
if (!response.ok) {
const error = await response.json().catch(() => ({}));
- throw new Error(error.errors?.[0]?.message || \`Failed to delete user: \${response.status}\`);
+ return {
+ success: false,
+ error: error.errors?.[0]?.message || \`Failed to delete user: \${response.status}\`,
+ };
}
- return { deleted: true, id: input.userId };
+ return { success: true, deleted: true };
}`;
diff --git a/plugins/clerk/codegen/get-user.ts b/plugins/clerk/codegen/get-user.ts
index 67e91c7..a84b4eb 100644
--- a/plugins/clerk/codegen/get-user.ts
+++ b/plugins/clerk/codegen/get-user.ts
@@ -26,8 +26,12 @@ export const getUserCodegenTemplate = `export async function clerkGetUserStep(in
if (!response.ok) {
const error = await response.json().catch(() => ({}));
- throw new Error(error.errors?.[0]?.message || \`Failed to get user: \${response.status}\`);
+ return {
+ success: false,
+ error: error.errors?.[0]?.message || \`Failed to get user: \${response.status}\`,
+ };
}
- return await response.json();
+ const user = await response.json();
+ return { success: true, user };
}`;
diff --git a/plugins/clerk/codegen/update-user.ts b/plugins/clerk/codegen/update-user.ts
index 692f771..8a319dc 100644
--- a/plugins/clerk/codegen/update-user.ts
+++ b/plugins/clerk/codegen/update-user.ts
@@ -39,8 +39,12 @@ export const updateUserCodegenTemplate = `export async function clerkUpdateUserS
if (!response.ok) {
const error = await response.json().catch(() => ({}));
- throw new Error(error.errors?.[0]?.message || \`Failed to update user: \${response.status}\`);
+ return {
+ success: false,
+ error: error.errors?.[0]?.message || \`Failed to update user: \${response.status}\`,
+ };
}
- return await response.json();
+ const user = await response.json();
+ return { success: true, user };
}`;
Analysis
Clerk action codegen templates return inconsistent response format
What fails: Codegen templates for Clerk user actions (clerkCreateUserStep, clerkUpdateUserStep, clerkGetUserStep, clerkDeleteUserStep) return unwrapped raw Clerk API responses or throw errors, while the actual step functions return wrapped { success, data/error } objects.
How to reproduce:
- Generate code using
plugins/clerk/codegen/create-user.ts(or other Clerk codegen templates) - Export the workflow to a standalone Next.js project using these generated templates
- Try to use the result with code that expects
{ success, user }response format:
const result = await clerkCreateUserStep({ emailAddress: "user@example.com" });
if (result.success) {
console.log(result.user); // Would fail - result is raw Clerk user object, not wrapped
}Result: TypeErrors due to missing success field. Exported code has different API than app code.
Expected behavior: Generated code should return the same response format as step functions:
- Success:
{ success: true, user: ClerkUser }for create/update/get operations, or{ success: true, deleted: true }for delete - Failure:
{ success: false, error: string }for all operations
Note: The step functions are the source of truth for the API contract. All generated code must wrap responses in the same { success, data/error } format to maintain consistency when workflows are exported.
ctate
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking great so far! There is some code that needs to be removed / added back. Lmk if you have any questions
- Revert AGENTS.md to upstream version - Remove plugins/clerk/codegen/ folder (auto-generated) - Remove template variable hint from AI prompt (auto-generated)
- Accept upstream plugin architecture changes - Keep Clerk plugin files - Remove auto-generated files (lib/codegen-registry.ts, lib/step-registry.ts, lib/types/integration.ts) - Merge new plugins (blob, fal, github, perplexity, stripe)
|
@ctate addressed! let me know if there's more to modify! |
- Remove codegen imports (auto-generated) - Add outputFields to all actions - Add maxRetries = 0 to all step functions
plugins/clerk/steps/delete-user.ts
Outdated
|
|
||
| try { | ||
| const response = await fetch( | ||
| `https://api.clerk.com/v1/users/${input.userId}`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In get-user.ts, update-user.ts, and delete-user.ts, user-controlled input is directly interpolated into URL paths without sanitization.
If input.userId contains malicious content like:
- ../../other-endpoint (path traversal)
- user123?admin=true (query string injection)
- URL-encoded special characters
The request could be manipulated.
Mitigation Recommendation: Add URL encoding:
const response = await fetch(
`https://api.clerk.com/v1/users/${encodeURIComponent(input.userId)}`,
| if (input.publicMetadata) { | ||
| try { | ||
| body.public_metadata = JSON.parse(input.publicMetadata); | ||
| } catch { | ||
| body.public_metadata = input.publicMetadata; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In create-user.ts and update-user.ts, if JSON parsing fails, the string is sent directly. Clerk's API expects an object for metadata, so this would likely cause an API error. Consider either:
- Always requiring valid JSON
- Returning an error on parse failure instead of silently falling back
plugins/clerk/steps/create-user.ts
Outdated
| type ClerkUser = { | ||
| id: string; | ||
| first_name: string | null; | ||
| last_name: string | null; | ||
| email_addresses: Array<{ | ||
| id: string; | ||
| email_address: string; | ||
| }>; | ||
| public_metadata: Record<string, unknown>; | ||
| private_metadata: Record<string, unknown>; | ||
| created_at: number; | ||
| updated_at: number; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: The ClerkUser type is defined in 3 step files (get-user.ts, create-user.ts, update-user.ts). Could be extracted to a shared types file.
- 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
|
Thanks for the review @ctate! Applied all three fixes. |
|
Thank you @Railly! Changes look great. Will look into expanding the visual editor capabilities. |
Summary
fetchCredentialsUser-Agent: workflow-builder.devheader for Clerk BAPI trackingTest plan