Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
526 changes: 498 additions & 28 deletions apps/sim/blocks/blocks/clerk.ts

Large diffs are not rendered by default.

123 changes: 123 additions & 0 deletions apps/sim/tools/clerk/add_organization_member.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { createLogger } from '@sim/logger'
import type {
ClerkAddOrganizationMemberParams,
ClerkAddOrganizationMemberResponse,
ClerkApiError,
ClerkOrganizationMembership,
} from '@/tools/clerk/types'
import type { ToolConfig } from '@/tools/types'

const logger = createLogger('ClerkAddOrganizationMember')

export const clerkAddOrganizationMemberTool: ToolConfig<
ClerkAddOrganizationMemberParams,
ClerkAddOrganizationMemberResponse
> = {
id: 'clerk_add_organization_member',
name: 'Add Organization Member in Clerk',
description: 'Add a user as a member of a Clerk organization with a given role',
version: '1.0.0',

params: {
secretKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The Clerk Secret Key for API authentication',
},
organizationId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the organization (e.g., org_2NNEqL2nrIRdJ194ndJqAHwEfxC)',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the user to add as a member',
},
role: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Role to assign, e.g. org:admin or org:member',
},
},

request: {
url: (params) =>
`https://api.clerk.com/v1/organizations/${params.organizationId?.trim()}/memberships`,
method: 'POST',
headers: (params) => {
if (!params.secretKey) {
throw new Error('Clerk Secret Key is required')
}
return {
Authorization: `Bearer ${params.secretKey}`,
'Content-Type': 'application/json',
}
},
body: (params) => ({
user_id: params.userId?.trim(),
role: params.role,
}),
},

transformResponse: async (response: Response) => {
const data: ClerkOrganizationMembership | ClerkApiError = await response.json()

if (!response.ok) {
logger.error('Clerk API request failed', { data, status: response.status })
throw new Error(
(data as ClerkApiError).errors?.[0]?.message || 'Failed to add organization member in Clerk'
)
}

const membership = data as ClerkOrganizationMembership
return {
success: true,
output: {
id: membership.id,
role: membership.role,
roleName: membership.role_name ?? null,
permissions: membership.permissions ?? [],
organizationId: membership.organization.id,
userId: membership.public_user_data.user_id,
firstName: membership.public_user_data.first_name ?? null,
lastName: membership.public_user_data.last_name ?? null,
imageUrl: membership.public_user_data.image_url ?? null,
identifier: membership.public_user_data.identifier ?? null,
username: membership.public_user_data.username ?? null,
banned: membership.public_user_data.banned ?? false,
publicMetadata: membership.public_metadata ?? {},
createdAt: membership.created_at,
updatedAt: membership.updated_at,
success: true,
},
}
},

outputs: {
id: { type: 'string', description: 'Membership ID' },
role: { type: 'string', description: 'Member role' },
roleName: { type: 'string', description: 'Human-readable role name', optional: true },
permissions: {
type: 'array',
description: 'Permissions granted by the role',
items: { type: 'string' },
},
organizationId: { type: 'string', description: 'Organization ID' },
userId: { type: 'string', description: 'Member user ID' },
firstName: { type: 'string', description: 'Member first name', optional: true },
lastName: { type: 'string', description: 'Member last name', optional: true },
imageUrl: { type: 'string', description: 'Member profile image URL', optional: true },
identifier: { type: 'string', description: 'Member identifier (e.g., email)', optional: true },
username: { type: 'string', description: 'Member username', optional: true },
banned: { type: 'boolean', description: 'Whether the member is banned' },
publicMetadata: { type: 'json', description: 'Public metadata' },
createdAt: { type: 'number', description: 'Creation timestamp' },
updatedAt: { type: 'number', description: 'Last update timestamp' },
success: { type: 'boolean', description: 'Operation success status' },
},
}
87 changes: 87 additions & 0 deletions apps/sim/tools/clerk/ban_user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { createLogger } from '@sim/logger'
import type {
ClerkApiError,
ClerkBanUserParams,
ClerkBanUserResponse,
ClerkUser,
} from '@/tools/clerk/types'
import type { ToolConfig } from '@/tools/types'

const logger = createLogger('ClerkBanUser')

export const clerkBanUserTool: ToolConfig<ClerkBanUserParams, ClerkBanUserResponse> = {
id: 'clerk_ban_user',
name: 'Ban User in Clerk',
description: 'Ban a user, preventing them from signing in',
version: '1.0.0',

params: {
secretKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The Clerk Secret Key for API authentication',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the user to ban (e.g., user_2NNEqL2nrIRdJ194ndJqAHwEfxC)',
},
},

request: {
url: (params) => `https://api.clerk.com/v1/users/${params.userId?.trim()}/ban`,
method: 'POST',
headers: (params) => {
if (!params.secretKey) {
throw new Error('Clerk Secret Key is required')
}
return {
Authorization: `Bearer ${params.secretKey}`,
'Content-Type': 'application/json',
}
},
},

transformResponse: async (response: Response) => {
const data: ClerkUser | ClerkApiError = await response.json()

if (!response.ok) {
logger.error('Clerk API request failed', { data, status: response.status })
throw new Error((data as ClerkApiError).errors?.[0]?.message || 'Failed to ban user in Clerk')
}

const user = data as ClerkUser
return {
success: true,
output: {
id: user.id,
username: user.username ?? null,
firstName: user.first_name ?? null,
lastName: user.last_name ?? null,
banned: user.banned ?? false,
locked: user.locked ?? false,
lockoutExpiresInSeconds: user.lockout_expires_in_seconds ?? null,
updatedAt: user.updated_at,
success: true,
},
}
},

outputs: {
id: { type: 'string', description: 'User ID' },
username: { type: 'string', description: 'Username', optional: true },
firstName: { type: 'string', description: 'First name', optional: true },
lastName: { type: 'string', description: 'Last name', optional: true },
banned: { type: 'boolean', description: 'Whether the user is banned' },
locked: { type: 'boolean', description: 'Whether the user is locked' },
lockoutExpiresInSeconds: {
type: 'number',
description: 'Seconds until lockout expires',
optional: true,
},
updatedAt: { type: 'number', description: 'Last update timestamp' },
success: { type: 'boolean', description: 'Operation success status' },
},
}
121 changes: 121 additions & 0 deletions apps/sim/tools/clerk/create_actor_token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { createLogger } from '@sim/logger'
import type {
ClerkActorToken,
ClerkApiError,
ClerkCreateActorTokenParams,
ClerkCreateActorTokenResponse,
} from '@/tools/clerk/types'
import type { ToolConfig } from '@/tools/types'

const logger = createLogger('ClerkCreateActorToken')

export const clerkCreateActorTokenTool: ToolConfig<
ClerkCreateActorTokenParams,
ClerkCreateActorTokenResponse
> = {
id: 'clerk_create_actor_token',
name: 'Create Actor Token in Clerk',
description:
'Create an actor token to impersonate a user (God Mode / act-as-user), e.g. for support tooling',
version: '1.0.0',

params: {
secretKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'The Clerk Secret Key for API authentication',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the user to impersonate',
},
actor: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'Actor JSON object identifying who is impersonating, must include a "sub" field, e.g. {"sub": "user_support_agent_id"}',
},
expiresInSeconds: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Seconds until the token expires (default 3600)',
},
sessionMaxDurationInSeconds: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Max duration in seconds for sessions created with this token (default 1800)',
},
},

request: {
url: () => 'https://api.clerk.com/v1/actor_tokens',
method: 'POST',
headers: (params) => {
if (!params.secretKey) {
throw new Error('Clerk Secret Key is required')
}
return {
Authorization: `Bearer ${params.secretKey}`,
'Content-Type': 'application/json',
}
},
body: (params) => {
const body: Record<string, unknown> = {
user_id: params.userId?.trim(),
actor: params.actor,
}

if (params.expiresInSeconds !== undefined) body.expires_in_seconds = params.expiresInSeconds
if (params.sessionMaxDurationInSeconds !== undefined)
body.session_max_duration_in_seconds = params.sessionMaxDurationInSeconds

return body
},
},

transformResponse: async (response: Response) => {
const data: ClerkActorToken | ClerkApiError = await response.json()

if (!response.ok) {
logger.error('Clerk API request failed', { data, status: response.status })
throw new Error(
(data as ClerkApiError).errors?.[0]?.message || 'Failed to create actor token in Clerk'
)
}

const actorToken = data as ClerkActorToken

return {
success: true,
output: {
id: actorToken.id,
status: actorToken.status,
userId: actorToken.user_id,
actor: actorToken.actor ?? {},
token: actorToken.token ?? null,
url: actorToken.url ?? null,
createdAt: actorToken.created_at,
updatedAt: actorToken.updated_at,
success: true,
},
}
},

outputs: {
id: { type: 'string', description: 'Actor token ID' },
status: { type: 'string', description: 'Actor token status' },
userId: { type: 'string', description: 'ID of the impersonated user' },
actor: { type: 'json', description: 'Actor object identifying who is impersonating' },
token: { type: 'string', description: 'Signed actor token (JWT)', optional: true },
url: { type: 'string', description: 'Sign-in URL for the actor token', optional: true },
createdAt: { type: 'number', description: 'Creation timestamp' },
updatedAt: { type: 'number', description: 'Last update timestamp' },
success: { type: 'boolean', description: 'Operation success status' },
},
}
Loading
Loading