diff --git a/apps/sim/blocks/blocks/clerk.ts b/apps/sim/blocks/blocks/clerk.ts index ebdb7309157..b383283d005 100644 --- a/apps/sim/blocks/blocks/clerk.ts +++ b/apps/sim/blocks/blocks/clerk.ts @@ -9,7 +9,7 @@ export const ClerkBlock: BlockConfig = { name: 'Clerk', description: 'Manage users, organizations, and sessions in Clerk', longDescription: - 'Integrate Clerk authentication and user management into your workflow. Create, update, delete, and list users. Manage organizations and their memberships. Monitor and control user sessions.', + 'Integrate Clerk authentication and user management into your workflow. Create, update, delete, ban, lock, and list users. Manage organizations, their memberships, and invitations. Monitor and control user sessions. Maintain allowlist/blocklist identifiers, JWT templates, and actor tokens.', docsLink: 'https://docs.sim.ai/integrations/clerk', category: 'tools', integrationType: IntegrationType.Security, @@ -27,12 +27,35 @@ export const ClerkBlock: BlockConfig = { { label: 'Create User', id: 'clerk_create_user' }, { label: 'Update User', id: 'clerk_update_user' }, { label: 'Delete User', id: 'clerk_delete_user' }, + { label: 'Ban User', id: 'clerk_ban_user' }, + { label: 'Unban User', id: 'clerk_unban_user' }, + { label: 'Lock User', id: 'clerk_lock_user' }, + { label: 'Unlock User', id: 'clerk_unlock_user' }, + { label: 'Get User OAuth Token', id: 'clerk_get_user_oauth_token' }, { label: 'List Organizations', id: 'clerk_list_organizations' }, { label: 'Get Organization', id: 'clerk_get_organization' }, { label: 'Create Organization', id: 'clerk_create_organization' }, + { label: 'Update Organization', id: 'clerk_update_organization' }, + { label: 'Delete Organization', id: 'clerk_delete_organization' }, + { label: 'List Organization Memberships', id: 'clerk_list_organization_memberships' }, + { label: 'Add Organization Member', id: 'clerk_add_organization_member' }, + { label: 'Update Organization Membership', id: 'clerk_update_organization_membership' }, + { label: 'Remove Organization Member', id: 'clerk_remove_organization_member' }, + { label: 'Create Organization Invitation', id: 'clerk_create_organization_invitation' }, + { label: 'List Organization Invitations', id: 'clerk_list_organization_invitations' }, { label: 'List Sessions', id: 'clerk_list_sessions' }, { label: 'Get Session', id: 'clerk_get_session' }, { label: 'Revoke Session', id: 'clerk_revoke_session' }, + { label: 'List Allowlist Identifiers', id: 'clerk_list_allowlist_identifiers' }, + { label: 'Create Allowlist Identifier', id: 'clerk_create_allowlist_identifier' }, + { label: 'Delete Allowlist Identifier', id: 'clerk_delete_allowlist_identifier' }, + { label: 'List Blocklist Identifiers', id: 'clerk_list_blocklist_identifiers' }, + { label: 'Create Blocklist Identifier', id: 'clerk_create_blocklist_identifier' }, + { label: 'Delete Blocklist Identifier', id: 'clerk_delete_blocklist_identifier' }, + { label: 'List JWT Templates', id: 'clerk_list_jwt_templates' }, + { label: 'Get JWT Template', id: 'clerk_get_jwt_template' }, + { label: 'Create Actor Token', id: 'clerk_create_actor_token' }, + { label: 'Revoke Actor Token', id: 'clerk_revoke_actor_token' }, ], value: () => 'clerk_list_users', }, @@ -68,7 +91,47 @@ export const ClerkBlock: BlockConfig = { condition: { field: 'operation', value: 'clerk_list_users' }, mode: 'advanced', }, - // Get User params + { + id: 'phoneNumberFilter', + title: 'Phone Filter', + type: 'short-input', + placeholder: 'Filter by phone number (comma-separated)', + condition: { field: 'operation', value: 'clerk_list_users' }, + mode: 'advanced', + }, + { + id: 'externalIdFilter', + title: 'External ID Filter', + type: 'short-input', + placeholder: 'Filter by external ID (comma-separated)', + condition: { field: 'operation', value: 'clerk_list_users' }, + mode: 'advanced', + }, + { + id: 'userIdFilter', + title: 'User ID Filter', + type: 'short-input', + placeholder: 'Filter by user ID (comma-separated)', + condition: { field: 'operation', value: 'clerk_list_users' }, + mode: 'advanced', + }, + { + id: 'orderBy', + title: 'Sort By', + type: 'short-input', + placeholder: 'e.g. -created_at', + condition: { + field: 'operation', + value: [ + 'clerk_list_users', + 'clerk_list_organizations', + 'clerk_list_organization_memberships', + 'clerk_list_organization_invitations', + ], + }, + mode: 'advanced', + }, + // Get/Update/Delete/Ban/Unban/Lock/Unlock User, OAuth token, and Actor Token params { id: 'userId', title: 'User ID', @@ -76,20 +139,58 @@ export const ClerkBlock: BlockConfig = { placeholder: 'user_...', condition: { field: 'operation', - value: ['clerk_get_user', 'clerk_update_user', 'clerk_delete_user'], + value: [ + 'clerk_get_user', + 'clerk_update_user', + 'clerk_delete_user', + 'clerk_ban_user', + 'clerk_unban_user', + 'clerk_lock_user', + 'clerk_unlock_user', + 'clerk_get_user_oauth_token', + 'clerk_add_organization_member', + 'clerk_update_organization_membership', + 'clerk_remove_organization_member', + 'clerk_create_actor_token', + ], }, required: { field: 'operation', - value: ['clerk_get_user', 'clerk_update_user', 'clerk_delete_user'], + value: [ + 'clerk_get_user', + 'clerk_update_user', + 'clerk_delete_user', + 'clerk_ban_user', + 'clerk_unban_user', + 'clerk_lock_user', + 'clerk_unlock_user', + 'clerk_get_user_oauth_token', + 'clerk_add_organization_member', + 'clerk_update_organization_membership', + 'clerk_remove_organization_member', + 'clerk_create_actor_token', + ], }, }, + { + id: 'provider', + title: 'OAuth Provider', + type: 'short-input', + placeholder: 'google, github, microsoft...', + condition: { field: 'operation', value: 'clerk_get_user_oauth_token' }, + required: { field: 'operation', value: 'clerk_get_user_oauth_token' }, + }, // Create/Update User params { id: 'emailAddress', title: 'Email Address', type: 'short-input', placeholder: 'user@example.com (comma-separated for multiple)', - condition: { field: 'operation', value: 'clerk_create_user' }, + condition: { + field: 'operation', + value: ['clerk_create_user', 'clerk_create_organization_invitation'], + }, + required: { field: 'operation', value: 'clerk_create_organization_invitation' }, }, { id: 'phoneNumber', @@ -143,7 +244,15 @@ export const ClerkBlock: BlockConfig = { type: 'code', language: 'json', placeholder: '{"role": "admin"}', - condition: { field: 'operation', value: ['clerk_create_user', 'clerk_update_user'] }, + condition: { + field: 'operation', + value: [ + 'clerk_create_user', + 'clerk_update_user', + 'clerk_create_organization', + 'clerk_create_organization_invitation', + ], + }, mode: 'advanced', }, { @@ -152,7 +261,15 @@ export const ClerkBlock: BlockConfig = { type: 'code', language: 'json', placeholder: '{"internalId": "123"}', - condition: { field: 'operation', value: ['clerk_create_user', 'clerk_update_user'] }, + condition: { + field: 'operation', + value: [ + 'clerk_create_user', + 'clerk_update_user', + 'clerk_create_organization', + 'clerk_create_organization_invitation', + ], + }, mode: 'advanced', }, // Organization params @@ -176,15 +293,44 @@ export const ClerkBlock: BlockConfig = { title: 'Organization ID', type: 'short-input', placeholder: 'org_... or slug', - condition: { field: 'operation', value: 'clerk_get_organization' }, - required: { field: 'operation', value: 'clerk_get_organization' }, + condition: { + field: 'operation', + value: [ + 'clerk_get_organization', + 'clerk_update_organization', + 'clerk_delete_organization', + 'clerk_list_organization_memberships', + 'clerk_add_organization_member', + 'clerk_update_organization_membership', + 'clerk_remove_organization_member', + 'clerk_create_organization_invitation', + 'clerk_list_organization_invitations', + ], + }, + required: { + field: 'operation', + value: [ + 'clerk_get_organization', + 'clerk_update_organization', + 'clerk_delete_organization', + 'clerk_list_organization_memberships', + 'clerk_add_organization_member', + 'clerk_update_organization_membership', + 'clerk_remove_organization_member', + 'clerk_create_organization_invitation', + 'clerk_list_organization_invitations', + ], + }, }, { id: 'orgName', title: 'Organization Name', type: 'short-input', placeholder: 'Acme Corp', - condition: { field: 'operation', value: 'clerk_create_organization' }, + condition: { + field: 'operation', + value: ['clerk_create_organization', 'clerk_update_organization'], + }, required: { field: 'operation', value: 'clerk_create_organization' }, }, { @@ -200,7 +346,10 @@ export const ClerkBlock: BlockConfig = { title: 'Slug', type: 'short-input', placeholder: 'acme-corp', - condition: { field: 'operation', value: 'clerk_create_organization' }, + condition: { + field: 'operation', + value: ['clerk_create_organization', 'clerk_update_organization'], + }, mode: 'advanced', }, { @@ -208,7 +357,95 @@ export const ClerkBlock: BlockConfig = { title: 'Max Members', type: 'short-input', placeholder: '0 for unlimited', - condition: { field: 'operation', value: 'clerk_create_organization' }, + condition: { + field: 'operation', + value: ['clerk_create_organization', 'clerk_update_organization'], + }, + mode: 'advanced', + }, + { + id: 'adminDeleteEnabled', + title: 'Admin Delete Enabled', + type: 'switch', + condition: { field: 'operation', value: 'clerk_update_organization' }, + mode: 'advanced', + }, + // Organization Membership / Invitation params + { + id: 'role', + title: 'Role', + type: 'short-input', + placeholder: 'org:admin or org:member', + condition: { + field: 'operation', + value: [ + 'clerk_add_organization_member', + 'clerk_update_organization_membership', + 'clerk_create_organization_invitation', + 'clerk_list_organization_memberships', + ], + }, + required: { + field: 'operation', + value: [ + 'clerk_add_organization_member', + 'clerk_update_organization_membership', + 'clerk_create_organization_invitation', + ], + }, + }, + { + id: 'inviterUserId', + title: 'Inviter User ID', + type: 'short-input', + placeholder: 'user_... (who sent the invite)', + condition: { field: 'operation', value: 'clerk_create_organization_invitation' }, + mode: 'advanced', + }, + { + id: 'redirectUrl', + title: 'Redirect URL', + type: 'short-input', + placeholder: 'https://yourapp.com/accept-invite', + condition: { field: 'operation', value: 'clerk_create_organization_invitation' }, + mode: 'advanced', + }, + { + id: 'expiresInDays', + title: 'Expires In (Days)', + type: 'short-input', + placeholder: '1-365, default: 30', + condition: { field: 'operation', value: 'clerk_create_organization_invitation' }, + mode: 'advanced', + }, + { + id: 'notifyInvitation', + title: 'Send Invitation Email', + type: 'switch', + condition: { field: 'operation', value: 'clerk_create_organization_invitation' }, + mode: 'advanced', + }, + { + id: 'invitationEmailFilter', + title: 'Email Filter', + type: 'short-input', + placeholder: 'Filter by invited email', + condition: { field: 'operation', value: 'clerk_list_organization_invitations' }, + mode: 'advanced', + }, + { + id: 'invitationStatus', + title: 'Status', + type: 'dropdown', + options: [ + { label: 'All', id: '' }, + { label: 'Pending', id: 'pending' }, + { label: 'Accepted', id: 'accepted' }, + { label: 'Revoked', id: 'revoked' }, + { label: 'Expired', id: 'expired' }, + ], + value: () => '', + condition: { field: 'operation', value: 'clerk_list_organization_invitations' }, mode: 'advanced', }, // Session params @@ -238,6 +475,8 @@ export const ClerkBlock: BlockConfig = { { label: 'Ended', id: 'ended' }, { label: 'Expired', id: 'expired' }, { label: 'Revoked', id: 'revoked' }, + { label: 'Removed', id: 'removed' }, + { label: 'Replaced', id: 'replaced' }, { label: 'Abandoned', id: 'abandoned' }, { label: 'Pending', id: 'pending' }, ], @@ -253,6 +492,85 @@ export const ClerkBlock: BlockConfig = { condition: { field: 'operation', value: ['clerk_get_session', 'clerk_revoke_session'] }, required: { field: 'operation', value: ['clerk_get_session', 'clerk_revoke_session'] }, }, + // Allowlist / Blocklist params + { + id: 'identifier', + title: 'Identifier', + type: 'short-input', + placeholder: 'user@example.com, +1234567890, or a web3 wallet', + condition: { + field: 'operation', + value: ['clerk_create_allowlist_identifier', 'clerk_create_blocklist_identifier'], + }, + required: { + field: 'operation', + value: ['clerk_create_allowlist_identifier', 'clerk_create_blocklist_identifier'], + }, + }, + { + id: 'allowlistNotify', + title: 'Notify Identifier', + type: 'switch', + condition: { field: 'operation', value: 'clerk_create_allowlist_identifier' }, + mode: 'advanced', + }, + { + id: 'identifierId', + title: 'Identifier ID', + type: 'short-input', + placeholder: 'The ID of the allowlist/blocklist identifier', + condition: { + field: 'operation', + value: ['clerk_delete_allowlist_identifier', 'clerk_delete_blocklist_identifier'], + }, + required: { + field: 'operation', + value: ['clerk_delete_allowlist_identifier', 'clerk_delete_blocklist_identifier'], + }, + }, + // JWT Template params + { + id: 'templateId', + title: 'Template ID', + type: 'short-input', + placeholder: 'The ID of the JWT template', + condition: { field: 'operation', value: 'clerk_get_jwt_template' }, + required: { field: 'operation', value: 'clerk_get_jwt_template' }, + }, + // Actor Token params + { + id: 'actor', + title: 'Actor', + type: 'code', + language: 'json', + placeholder: '{"sub": "user_support_agent_id"}', + condition: { field: 'operation', value: 'clerk_create_actor_token' }, + required: { field: 'operation', value: 'clerk_create_actor_token' }, + }, + { + id: 'expiresInSeconds', + title: 'Expires In (Seconds)', + type: 'short-input', + placeholder: 'Default: 3600', + condition: { field: 'operation', value: 'clerk_create_actor_token' }, + mode: 'advanced', + }, + { + id: 'sessionMaxDurationInSeconds', + title: 'Session Max Duration (Seconds)', + type: 'short-input', + placeholder: 'Default: 1800', + condition: { field: 'operation', value: 'clerk_create_actor_token' }, + mode: 'advanced', + }, + { + id: 'actorTokenId', + title: 'Actor Token ID', + type: 'short-input', + placeholder: 'The ID of the actor token to revoke', + condition: { field: 'operation', value: 'clerk_revoke_actor_token' }, + required: { field: 'operation', value: 'clerk_revoke_actor_token' }, + }, // Pagination params (common) { id: 'limit', @@ -261,7 +579,14 @@ export const ClerkBlock: BlockConfig = { placeholder: 'Results per page (1-500, default: 10)', condition: { field: 'operation', - value: ['clerk_list_users', 'clerk_list_organizations', 'clerk_list_sessions'], + value: [ + 'clerk_list_users', + 'clerk_list_organizations', + 'clerk_list_sessions', + 'clerk_list_organization_memberships', + 'clerk_list_organization_invitations', + 'clerk_list_allowlist_identifiers', + ], }, mode: 'advanced', }, @@ -272,7 +597,14 @@ export const ClerkBlock: BlockConfig = { placeholder: 'Skip N results for pagination', condition: { field: 'operation', - value: ['clerk_list_users', 'clerk_list_organizations', 'clerk_list_sessions'], + value: [ + 'clerk_list_users', + 'clerk_list_organizations', + 'clerk_list_sessions', + 'clerk_list_organization_memberships', + 'clerk_list_organization_invitations', + 'clerk_list_allowlist_identifiers', + ], }, mode: 'advanced', }, @@ -280,8 +612,15 @@ export const ClerkBlock: BlockConfig = { ...getTrigger('clerk_user_updated').subBlocks, ...getTrigger('clerk_user_deleted').subBlocks, ...getTrigger('clerk_session_created').subBlocks, + ...getTrigger('clerk_session_ended').subBlocks, + ...getTrigger('clerk_session_removed').subBlocks, + ...getTrigger('clerk_session_revoked').subBlocks, ...getTrigger('clerk_organization_created').subBlocks, + ...getTrigger('clerk_organization_updated').subBlocks, + ...getTrigger('clerk_organization_deleted').subBlocks, ...getTrigger('clerk_organization_membership_created').subBlocks, + ...getTrigger('clerk_organization_membership_updated').subBlocks, + ...getTrigger('clerk_organization_membership_deleted').subBlocks, ...getTrigger('clerk_webhook').subBlocks, ], @@ -292,8 +631,15 @@ export const ClerkBlock: BlockConfig = { 'clerk_user_updated', 'clerk_user_deleted', 'clerk_session_created', + 'clerk_session_ended', + 'clerk_session_removed', + 'clerk_session_revoked', 'clerk_organization_created', + 'clerk_organization_updated', + 'clerk_organization_deleted', 'clerk_organization_membership_created', + 'clerk_organization_membership_updated', + 'clerk_organization_membership_deleted', 'clerk_webhook', ], }, @@ -305,12 +651,35 @@ export const ClerkBlock: BlockConfig = { 'clerk_create_user', 'clerk_update_user', 'clerk_delete_user', + 'clerk_ban_user', + 'clerk_unban_user', + 'clerk_lock_user', + 'clerk_unlock_user', + 'clerk_get_user_oauth_token', 'clerk_list_organizations', 'clerk_get_organization', 'clerk_create_organization', + 'clerk_update_organization', + 'clerk_delete_organization', + 'clerk_list_organization_memberships', + 'clerk_add_organization_member', + 'clerk_update_organization_membership', + 'clerk_remove_organization_member', + 'clerk_create_organization_invitation', + 'clerk_list_organization_invitations', 'clerk_list_sessions', 'clerk_get_session', 'clerk_revoke_session', + 'clerk_list_allowlist_identifiers', + 'clerk_create_allowlist_identifier', + 'clerk_delete_allowlist_identifier', + 'clerk_list_blocklist_identifiers', + 'clerk_create_blocklist_identifier', + 'clerk_delete_blocklist_identifier', + 'clerk_list_jwt_templates', + 'clerk_get_jwt_template', + 'clerk_create_actor_token', + 'clerk_revoke_actor_token', ], config: { tool: (params) => params.operation as string, @@ -320,13 +689,20 @@ export const ClerkBlock: BlockConfig = { secretKey, emailAddressFilter, usernameFilter, + phoneNumberFilter, + externalIdFilter, + userIdFilter, orgQuery, orgName, sessionUserId, sessionStatus, + invitationEmailFilter, + invitationStatus, + notifyInvitation, + allowlistNotify, publicMetadata, privateMetadata, - maxAllowedMemberships, + actor, ...rest } = params @@ -339,9 +715,14 @@ export const ClerkBlock: BlockConfig = { case 'clerk_list_users': if (emailAddressFilter) cleanParams.emailAddress = emailAddressFilter if (usernameFilter) cleanParams.username = usernameFilter + if (phoneNumberFilter) cleanParams.phoneNumber = phoneNumberFilter + if (externalIdFilter) cleanParams.externalId = externalIdFilter + if (userIdFilter) cleanParams.userId = userIdFilter break case 'clerk_create_user': case 'clerk_update_user': + case 'clerk_create_organization_invitation': + case 'clerk_create_organization': if (publicMetadata) { cleanParams.publicMetadata = typeof publicMetadata === 'string' ? JSON.parse(publicMetadata) : publicMetadata @@ -350,25 +731,54 @@ export const ClerkBlock: BlockConfig = { cleanParams.privateMetadata = typeof privateMetadata === 'string' ? JSON.parse(privateMetadata) : privateMetadata } + if ( + operation === 'clerk_create_organization_invitation' && + notifyInvitation !== undefined + ) { + cleanParams.notify = notifyInvitation + } + if (operation === 'clerk_create_organization' && orgName) { + cleanParams.name = orgName + } break case 'clerk_list_organizations': if (orgQuery) cleanParams.query = orgQuery break - case 'clerk_create_organization': + case 'clerk_update_organization': if (orgName) cleanParams.name = orgName - if (maxAllowedMemberships) - cleanParams.maxAllowedMemberships = Number(maxAllowedMemberships) break case 'clerk_list_sessions': if (sessionUserId) cleanParams.userId = sessionUserId if (sessionStatus) cleanParams.status = sessionStatus break + case 'clerk_list_organization_invitations': + if (invitationEmailFilter) cleanParams.emailAddress = invitationEmailFilter + if (invitationStatus) cleanParams.status = invitationStatus + break + case 'clerk_create_allowlist_identifier': + if (allowlistNotify !== undefined) cleanParams.notify = allowlistNotify + break + case 'clerk_create_actor_token': + if (actor !== undefined) { + cleanParams.actor = typeof actor === 'string' ? JSON.parse(actor) : actor + } + break } + // Fields that arrive as strings from short-input UI but must be numbers at execution time + const numericFields = new Set([ + 'limit', + 'offset', + 'maxAllowedMemberships', + 'expiresInDays', + 'expiresInSeconds', + 'sessionMaxDurationInSeconds', + ]) + // Add remaining params that don't need mapping Object.entries(rest).forEach(([key, value]) => { if (value !== undefined && value !== null && value !== '') { - cleanParams[key] = value + cleanParams[key] = numericFields.has(key) ? Number(value) : value } }) @@ -383,6 +793,7 @@ export const ClerkBlock: BlockConfig = { userId: { type: 'string', description: 'User ID' }, organizationId: { type: 'string', description: 'Organization ID or slug' }, sessionId: { type: 'string', description: 'Session ID' }, + role: { type: 'string', description: 'Organization role, e.g. org:admin or org:member' }, query: { type: 'string', description: 'Search query' }, limit: { type: 'number', description: 'Results per page' }, offset: { type: 'number', description: 'Pagination offset' }, @@ -393,8 +804,13 @@ export const ClerkBlock: BlockConfig = { users: { type: 'json', description: 'Array of user objects' }, organizations: { type: 'json', description: 'Array of organization objects' }, sessions: { type: 'json', description: 'Array of session objects' }, + memberships: { type: 'json', description: 'Array of organization membership objects' }, + invitations: { type: 'json', description: 'Array of organization invitation objects' }, + identifiers: { type: 'json', description: 'Array of allowlist/blocklist identifier objects' }, + templates: { type: 'json', description: 'Array of JWT template objects' }, + accessTokens: { type: 'json', description: 'Array of OAuth access token objects' }, // Single entity fields (destructured from get/create/update operations) - id: { type: 'string', description: 'Resource ID (user, organization, or session)' }, + id: { type: 'string', description: 'Resource ID (user, organization, session, etc.)' }, name: { type: 'string', description: 'Organization name' }, slug: { type: 'string', description: 'Organization slug' }, username: { type: 'string', description: 'Username' }, @@ -404,23 +820,70 @@ export const ClerkBlock: BlockConfig = { hasImage: { type: 'boolean', description: 'Whether resource has an image' }, emailAddresses: { type: 'json', description: 'User email addresses' }, phoneNumbers: { type: 'json', description: 'User phone numbers' }, + emailAddress: { type: 'string', description: 'Email address (for invitations)' }, primaryEmailAddressId: { type: 'string', description: 'Primary email address ID' }, primaryPhoneNumberId: { type: 'string', description: 'Primary phone number ID' }, + primaryWeb3WalletId: { type: 'string', description: 'Primary Web3 wallet ID' }, externalId: { type: 'string', description: 'External system ID' }, passwordEnabled: { type: 'boolean', description: 'Whether password is enabled' }, twoFactorEnabled: { type: 'boolean', description: 'Whether 2FA is enabled' }, + totpEnabled: { type: 'boolean', description: 'Whether TOTP is enabled' }, + backupCodeEnabled: { type: 'boolean', description: 'Whether backup codes are enabled' }, + deleteSelfEnabled: { type: 'boolean', description: 'Whether user can delete themselves' }, + createOrganizationEnabled: { + type: 'boolean', + description: 'Whether user can create organizations', + }, banned: { type: 'boolean', description: 'Whether user is banned' }, locked: { type: 'boolean', description: 'Whether user is locked' }, - userId: { type: 'string', description: 'User ID (for sessions)' }, + lockoutExpiresInSeconds: { type: 'number', description: 'Seconds until lockout expires' }, + userId: { type: 'string', description: 'User ID (for sessions and memberships)' }, clientId: { type: 'string', description: 'Client ID (for sessions)' }, - status: { type: 'string', description: 'Session status' }, + status: { type: 'string', description: 'Session or invitation status' }, lastActiveAt: { type: 'number', description: 'Last activity timestamp' }, + lastActiveOrganizationId: { + type: 'string', + description: 'Last active organization ID (for sessions)', + }, lastSignInAt: { type: 'number', description: 'Last sign-in timestamp' }, membersCount: { type: 'number', description: 'Number of members' }, + pendingInvitationsCount: { type: 'number', description: 'Number of pending invitations' }, maxAllowedMemberships: { type: 'number', description: 'Max allowed memberships' }, adminDeleteEnabled: { type: 'boolean', description: 'Whether admin delete is enabled' }, createdBy: { type: 'string', description: 'Creator user ID' }, publicMetadata: { type: 'json', description: 'Public metadata' }, + privateMetadata: { type: 'json', description: 'Private metadata' }, + unsafeMetadata: { type: 'json', description: 'Unsafe metadata' }, + organizationId: { + type: 'string', + description: 'Organization ID (for memberships/invitations)', + }, + role: { type: 'string', description: 'Organization membership role' }, + roleName: { type: 'string', description: 'Human-readable role name' }, + permissions: { type: 'json', description: 'Permissions granted by the role' }, + identifier: { type: 'string', description: 'Allowlist/blocklist identifier value' }, + identifierType: { type: 'string', description: 'Identifier type (email, phone, web3 wallet)' }, + invitationId: { type: 'string', description: 'Allowlist invitation ID' }, + inviterId: { type: 'string', description: 'User ID of the invitation inviter' }, + inviterEmail: { type: 'string', description: "Inviter's email address" }, + inviterFirstName: { type: 'string', description: "Inviter's first name" }, + inviterLastName: { type: 'string', description: "Inviter's last name" }, + expiresAt: { type: 'number', description: 'Expiration timestamp (invitation, actor token)' }, + expireAt: { type: 'number', description: 'Expiration timestamp (session)' }, + abandonAt: { type: 'number', description: 'Session abandon timestamp' }, + url: { type: 'string', description: 'Invitation or actor token URL' }, + token: { type: 'string', description: 'OAuth access token or actor token' }, + provider: { type: 'string', description: 'OAuth provider' }, + scopes: { type: 'json', description: 'OAuth scopes granted to the token' }, + claims: { type: 'json', description: 'JWT template claims' }, + lifetime: { type: 'number', description: 'JWT template lifetime in seconds' }, + allowedClockSkew: { type: 'number', description: 'JWT template allowed clock skew in seconds' }, + customSigningKey: { + type: 'boolean', + description: 'Whether the JWT template uses a custom signing key', + }, + signingAlgorithm: { type: 'string', description: 'JWT template signing algorithm' }, + actor: { type: 'json', description: 'Actor object identifying who is impersonating' }, // Common outputs totalCount: { type: 'number', description: 'Total count for paginated results' }, deleted: { type: 'boolean', description: 'Whether the resource was deleted' }, @@ -469,7 +932,7 @@ export const ClerkBlockMeta = { icon: ClerkIcon, title: 'Clerk org-management automator', prompt: - 'Create a workflow that on a new enterprise plan via Stripe creates a Clerk organization, invites the admin, and writes the Clerk org ID back to the Stripe customer.', + 'Create a workflow that on a new enterprise plan via Stripe creates a Clerk organization, invites the admin by email, and writes the Clerk org ID back to the Stripe customer.', modules: ['agent', 'workflows'], category: 'operations', tags: ['enterprise', 'automation'], @@ -479,7 +942,7 @@ export const ClerkBlockMeta = { icon: ClerkIcon, title: 'Clerk inactive-user cleaner', prompt: - 'Build a scheduled workflow that finds Clerk users with no sign-ins in 180 days, sends a re-engagement email, and removes accounts after a grace period.', + 'Build a scheduled workflow that finds Clerk users with no sign-ins in 180 days, sends a re-engagement email, and bans accounts that stay inactive after a grace period.', modules: ['scheduled', 'agent', 'workflows'], category: 'operations', tags: ['automation', 'enterprise'], @@ -489,7 +952,7 @@ export const ClerkBlockMeta = { icon: ClerkIcon, title: 'Clerk access-review automator', prompt: - 'Create a scheduled quarterly workflow that lists Clerk organizations and their users, requires owner re-attestation, and writes the review trail to a compliance table.', + 'Create a scheduled quarterly workflow that lists Clerk organizations and their memberships, requires owner re-attestation, and writes the review trail to a compliance table.', modules: ['scheduled', 'tables', 'agent', 'workflows'], category: 'operations', tags: ['legal', 'enterprise'], @@ -511,7 +974,7 @@ export const ClerkBlockMeta = { description: 'Look up a Clerk user by email, username, or name and return their profile. Use to resolve a user before acting on their account or syncing them elsewhere.', content: - '# Find User\n\nLocate a Clerk user account.\n\n## Steps\n1. Use List Users with a Search Query (matches email, phone, username, or name), or the email/username filters for an exact match.\n2. If you already have the Clerk user id (user_...), use Get User instead for the full record.\n3. Review the returned profile: id, primary email, name, externalId, and flags like banned, locked, and twoFactorEnabled.\n\n## Output\nReturn the matched user id, primary email, name, and key status flags. If multiple users match, list the candidates with their emails so the right one can be confirmed; if none match, say so.', + '# Find User\n\nLocate a Clerk user account.\n\n## Steps\n1. Use List Users with a Search Query (matches email, phone, username, or name), or the email/username/phone/external ID filters for an exact match.\n2. If you already have the Clerk user id (user_...), use Get User instead for the full record.\n3. Review the returned profile: id, primary email, name, externalId, and flags like banned, locked, and twoFactorEnabled.\n\n## Output\nReturn the matched user id, primary email, name, and key status flags. If multiple users match, list the candidates with their emails so the right one can be confirmed; if none match, say so.', }, { name: 'provision-user', @@ -520,6 +983,13 @@ export const ClerkBlockMeta = { content: '# Provision User\n\nCreate or update a Clerk user.\n\n## Steps\n1. To create, use Create User with at least an email address (and optionally phone, username, password, first/last name).\n2. To set application roles or app data, pass Public Metadata (visible to the frontend) and Private Metadata (server-only) as JSON.\n3. Set External ID to link the Clerk user to your own system id.\n4. To modify an existing user, use Update User with the user id and only the fields that change.\n\n## Output\nReturn the user id, primary email, and the metadata that was set. Confirm whether the user was created or updated. If a required field is missing or the email already exists, report it clearly.', }, + { + name: 'moderate-user-access', + description: + 'Ban, unban, lock, or unlock a Clerk user to control their ability to sign in. Use for abuse response, suspicious-activity containment, or manual account recovery.', + content: + '# Moderate User Access\n\nControl whether a Clerk user can sign in.\n\n## Steps\n1. Resolve the target user id first (see find-user) if you only have an email or name.\n2. Use Ban User to immediately block all sign-in attempts (e.g. for confirmed abuse); use Unban User to lift it once resolved.\n3. Use Lock User for a temporary, reversible hold (e.g. suspicious login pattern under review); use Unlock User to restore access.\n4. For a full audit trail, use Audit User Sessions afterward to revoke any sessions that should not continue.\n\n## Output\nReturn the user id and the resulting banned/locked flags after the action. State clearly which control was applied and why, so the moderation trail is auditable.', + }, { name: 'audit-user-sessions', description: @@ -530,9 +1000,9 @@ export const ClerkBlockMeta = { { name: 'manage-organization', description: - 'Create a Clerk organization or look up its details and membership. Use when provisioning a new team or tenant in a multi-tenant app.', + 'Create or update a Clerk organization, manage its memberships, and invite new members. Use when provisioning a new team or tenant in a multi-tenant app, or when onboarding/offboarding members.', content: - '# Manage Organization\n\nCreate or inspect a Clerk organization.\n\n## Steps\n1. To create, use Create Organization with the organization name and the Creator User ID (that user becomes the admin); optionally set a slug and max members.\n2. To inspect, use Get Organization by org id or slug, or List Organizations with a search query and include members count.\n3. Read back the org id, slug, members count, and limits.\n\n## Output\nReturn the organization id, name, slug, and member count. When creating, confirm the admin user and echo the org id so it can be linked back to your billing or CRM record.', + "# Manage Organization\n\nCreate, inspect, and staff a Clerk organization.\n\n## Steps\n1. To create, use Create Organization with the organization name and the Creator User ID (that user becomes the admin); optionally set a slug and max members. Use Update Organization to rename, re-slug, or change membership limits later.\n2. To inspect, use Get Organization by org id or slug, or List Organizations with a search query and include members count.\n3. To staff the org, use Add Organization Member with an existing user id and role, or Create Organization Invitation to invite someone by email who does not have an account yet.\n4. Use List Organization Memberships to see current members and their roles, Update Organization Membership to change a member's role, and Remove Organization Member to offboard someone.\n5. Use List Organization Invitations to check on pending invites.\n\n## Output\nReturn the organization id, name, slug, and member count. When adding or inviting a member, confirm the user id or email and the assigned role. When creating, confirm the admin user and echo the org id so it can be linked back to your billing or CRM record.", }, ], } as const satisfies BlockMeta diff --git a/apps/sim/tools/clerk/add_organization_member.ts b/apps/sim/tools/clerk/add_organization_member.ts new file mode 100644 index 00000000000..fdf55734496 --- /dev/null +++ b/apps/sim/tools/clerk/add_organization_member.ts @@ -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' }, + }, +} diff --git a/apps/sim/tools/clerk/ban_user.ts b/apps/sim/tools/clerk/ban_user.ts new file mode 100644 index 00000000000..be902a68758 --- /dev/null +++ b/apps/sim/tools/clerk/ban_user.ts @@ -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 = { + 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' }, + }, +} diff --git a/apps/sim/tools/clerk/create_actor_token.ts b/apps/sim/tools/clerk/create_actor_token.ts new file mode 100644 index 00000000000..02c936c8820 --- /dev/null +++ b/apps/sim/tools/clerk/create_actor_token.ts @@ -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 = { + 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' }, + }, +} diff --git a/apps/sim/tools/clerk/create_allowlist_identifier.ts b/apps/sim/tools/clerk/create_allowlist_identifier.ts new file mode 100644 index 00000000000..bd7d4746e68 --- /dev/null +++ b/apps/sim/tools/clerk/create_allowlist_identifier.ts @@ -0,0 +1,101 @@ +import { createLogger } from '@sim/logger' +import type { + ClerkAllowlistIdentifier, + ClerkApiError, + ClerkCreateAllowlistIdentifierParams, + ClerkCreateAllowlistIdentifierResponse, +} from '@/tools/clerk/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ClerkCreateAllowlistIdentifier') + +export const clerkCreateAllowlistIdentifierTool: ToolConfig< + ClerkCreateAllowlistIdentifierParams, + ClerkCreateAllowlistIdentifierResponse +> = { + id: 'clerk_create_allowlist_identifier', + name: 'Create Allowlist Identifier in Clerk', + description: 'Add an email, phone number, or web3 wallet to your Clerk instance allowlist', + version: '1.0.0', + + params: { + secretKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Clerk Secret Key for API authentication', + }, + identifier: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Email address, phone number, or web3 wallet to allow (wildcards like *@example.com supported for email)', + }, + notify: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether to notify the identifier owner by email (default false)', + }, + }, + + request: { + url: () => 'https://api.clerk.com/v1/allowlist_identifiers', + 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 = { + identifier: params.identifier?.trim(), + } + + if (params.notify !== undefined) body.notify = params.notify + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data: ClerkAllowlistIdentifier | 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 allowlist identifier in Clerk' + ) + } + + const identifier = data as ClerkAllowlistIdentifier + return { + success: true, + output: { + id: identifier.id, + identifier: identifier.identifier, + identifierType: identifier.identifier_type, + invitationId: identifier.invitation_id ?? null, + createdAt: identifier.created_at, + updatedAt: identifier.updated_at, + success: true, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Allowlist identifier ID' }, + identifier: { type: 'string', description: 'Email, phone, or web3 wallet identifier' }, + identifierType: { type: 'string', description: 'Type of identifier' }, + invitationId: { type: 'string', description: 'Associated invitation ID', optional: true }, + createdAt: { type: 'number', description: 'Creation timestamp' }, + updatedAt: { type: 'number', description: 'Last update timestamp' }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/clerk/create_blocklist_identifier.ts b/apps/sim/tools/clerk/create_blocklist_identifier.ts new file mode 100644 index 00000000000..ad1da860253 --- /dev/null +++ b/apps/sim/tools/clerk/create_blocklist_identifier.ts @@ -0,0 +1,87 @@ +import { createLogger } from '@sim/logger' +import type { + ClerkApiError, + ClerkBlocklistIdentifier, + ClerkCreateBlocklistIdentifierParams, + ClerkCreateBlocklistIdentifierResponse, +} from '@/tools/clerk/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ClerkCreateBlocklistIdentifier') + +export const clerkCreateBlocklistIdentifierTool: ToolConfig< + ClerkCreateBlocklistIdentifierParams, + ClerkCreateBlocklistIdentifierResponse +> = { + id: 'clerk_create_blocklist_identifier', + name: 'Create Blocklist Identifier in Clerk', + description: + 'Add an email, phone number, or web3 wallet to your Clerk instance blocklist to prevent sign-ups', + version: '1.0.0', + + params: { + secretKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Clerk Secret Key for API authentication', + }, + identifier: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Email address, phone number, or web3 wallet to block', + }, + }, + + request: { + url: () => 'https://api.clerk.com/v1/blocklist_identifiers', + 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) => ({ + identifier: params.identifier?.trim(), + }), + }, + + transformResponse: async (response: Response) => { + const data: ClerkBlocklistIdentifier | 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 blocklist identifier in Clerk' + ) + } + + const identifier = data as ClerkBlocklistIdentifier + return { + success: true, + output: { + id: identifier.id, + identifier: identifier.identifier, + identifierType: identifier.identifier_type, + createdAt: identifier.created_at, + updatedAt: identifier.updated_at, + success: true, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Blocklist identifier ID' }, + identifier: { type: 'string', description: 'Email, phone, or web3 wallet identifier' }, + identifierType: { type: 'string', description: 'Type of identifier' }, + createdAt: { type: 'number', description: 'Creation timestamp' }, + updatedAt: { type: 'number', description: 'Last update timestamp' }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/clerk/create_organization_invitation.ts b/apps/sim/tools/clerk/create_organization_invitation.ts new file mode 100644 index 00000000000..cc33019257c --- /dev/null +++ b/apps/sim/tools/clerk/create_organization_invitation.ts @@ -0,0 +1,167 @@ +import { createLogger } from '@sim/logger' +import type { + ClerkApiError, + ClerkCreateOrganizationInvitationParams, + ClerkCreateOrganizationInvitationResponse, + ClerkOrganizationInvitation, +} from '@/tools/clerk/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ClerkCreateOrganizationInvitation') + +export const clerkCreateOrganizationInvitationTool: ToolConfig< + ClerkCreateOrganizationInvitationParams, + ClerkCreateOrganizationInvitationResponse +> = { + id: 'clerk_create_organization_invitation', + name: 'Create Organization Invitation in Clerk', + description: 'Invite a user by email to join 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)', + }, + emailAddress: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Email address of the user to invite', + }, + role: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Role to assign on acceptance, e.g. org:admin or org:member', + }, + inviterUserId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'User ID of the inviter', + }, + redirectUrl: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'URL to redirect to after the invitation is accepted', + }, + expiresInDays: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Days until the invitation expires (1-365, default 30)', + }, + publicMetadata: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Public metadata (JSON object)', + }, + privateMetadata: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Private metadata (JSON object)', + }, + notify: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether Clerk sends the invitation email (default true)', + }, + }, + + request: { + url: (params) => + `https://api.clerk.com/v1/organizations/${params.organizationId?.trim()}/invitations`, + 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 = { + email_address: params.emailAddress, + role: params.role, + } + + if (params.inviterUserId !== undefined) body.inviter_user_id = params.inviterUserId + if (params.redirectUrl !== undefined) body.redirect_url = params.redirectUrl + if (params.expiresInDays !== undefined) body.expires_in_days = params.expiresInDays + if (params.publicMetadata !== undefined) body.public_metadata = params.publicMetadata + if (params.privateMetadata !== undefined) body.private_metadata = params.privateMetadata + if (params.notify !== undefined) body.notify = params.notify + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data: ClerkOrganizationInvitation | 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 organization invitation in Clerk' + ) + } + + const invitation = data as ClerkOrganizationInvitation + return { + success: true, + output: { + id: invitation.id, + emailAddress: invitation.email_address, + role: invitation.role, + roleName: invitation.role_name ?? null, + organizationId: invitation.organization_id, + inviterId: invitation.inviter_id ?? null, + inviterEmail: invitation.public_inviter_data?.identifier ?? null, + inviterFirstName: invitation.public_inviter_data?.first_name ?? null, + inviterLastName: invitation.public_inviter_data?.last_name ?? null, + status: invitation.status, + url: invitation.url ?? null, + expiresAt: invitation.expires_at ?? null, + publicMetadata: invitation.public_metadata ?? {}, + createdAt: invitation.created_at, + updatedAt: invitation.updated_at, + success: true, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Invitation ID' }, + emailAddress: { type: 'string', description: 'Invited email address' }, + role: { type: 'string', description: 'Role to assign on acceptance' }, + roleName: { type: 'string', description: 'Human-readable role name', optional: true }, + organizationId: { type: 'string', description: 'Organization ID' }, + inviterId: { type: 'string', description: 'User ID of the inviter', optional: true }, + inviterEmail: { type: 'string', description: "Inviter's email address", optional: true }, + inviterFirstName: { type: 'string', description: "Inviter's first name", optional: true }, + inviterLastName: { type: 'string', description: "Inviter's last name", optional: true }, + status: { type: 'string', description: 'Invitation status' }, + url: { type: 'string', description: 'Invitation URL', optional: true }, + expiresAt: { type: 'number', description: 'Expiration timestamp', optional: true }, + 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' }, + }, +} diff --git a/apps/sim/tools/clerk/delete_allowlist_identifier.ts b/apps/sim/tools/clerk/delete_allowlist_identifier.ts new file mode 100644 index 00000000000..a23606557b5 --- /dev/null +++ b/apps/sim/tools/clerk/delete_allowlist_identifier.ts @@ -0,0 +1,80 @@ +import { createLogger } from '@sim/logger' +import type { + ClerkApiError, + ClerkDeleteAllowlistIdentifierParams, + ClerkDeleteAllowlistIdentifierResponse, + ClerkDeleteResponse, +} from '@/tools/clerk/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ClerkDeleteAllowlistIdentifier') + +export const clerkDeleteAllowlistIdentifierTool: ToolConfig< + ClerkDeleteAllowlistIdentifierParams, + ClerkDeleteAllowlistIdentifierResponse +> = { + id: 'clerk_delete_allowlist_identifier', + name: 'Delete Allowlist Identifier from Clerk', + description: 'Remove an identifier from your Clerk instance allowlist', + version: '1.0.0', + + params: { + secretKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Clerk Secret Key for API authentication', + }, + identifierId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the allowlist identifier to delete', + }, + }, + + request: { + url: (params) => + `https://api.clerk.com/v1/allowlist_identifiers/${params.identifierId?.trim()}`, + method: 'DELETE', + 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: ClerkDeleteResponse | 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 delete allowlist identifier from Clerk' + ) + } + + const deleteData = data as ClerkDeleteResponse + return { + success: true, + output: { + id: deleteData.id, + object: deleteData.object ?? 'allowlist_identifier', + deleted: deleteData.deleted ?? true, + success: true, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Deleted allowlist identifier ID' }, + object: { type: 'string', description: 'Object type (allowlist_identifier)' }, + deleted: { type: 'boolean', description: 'Whether the identifier was deleted' }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/clerk/delete_blocklist_identifier.ts b/apps/sim/tools/clerk/delete_blocklist_identifier.ts new file mode 100644 index 00000000000..a915de0e6d1 --- /dev/null +++ b/apps/sim/tools/clerk/delete_blocklist_identifier.ts @@ -0,0 +1,80 @@ +import { createLogger } from '@sim/logger' +import type { + ClerkApiError, + ClerkDeleteBlocklistIdentifierParams, + ClerkDeleteBlocklistIdentifierResponse, + ClerkDeleteResponse, +} from '@/tools/clerk/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ClerkDeleteBlocklistIdentifier') + +export const clerkDeleteBlocklistIdentifierTool: ToolConfig< + ClerkDeleteBlocklistIdentifierParams, + ClerkDeleteBlocklistIdentifierResponse +> = { + id: 'clerk_delete_blocklist_identifier', + name: 'Delete Blocklist Identifier from Clerk', + description: 'Remove an identifier from your Clerk instance blocklist', + version: '1.0.0', + + params: { + secretKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Clerk Secret Key for API authentication', + }, + identifierId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the blocklist identifier to delete', + }, + }, + + request: { + url: (params) => + `https://api.clerk.com/v1/blocklist_identifiers/${params.identifierId?.trim()}`, + method: 'DELETE', + 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: ClerkDeleteResponse | 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 delete blocklist identifier from Clerk' + ) + } + + const deleteData = data as ClerkDeleteResponse + return { + success: true, + output: { + id: deleteData.id, + object: deleteData.object ?? 'blocklist_identifier', + deleted: deleteData.deleted ?? true, + success: true, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Deleted blocklist identifier ID' }, + object: { type: 'string', description: 'Object type (blocklist_identifier)' }, + deleted: { type: 'boolean', description: 'Whether the identifier was deleted' }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/clerk/delete_organization.ts b/apps/sim/tools/clerk/delete_organization.ts new file mode 100644 index 00000000000..e14aaca1b40 --- /dev/null +++ b/apps/sim/tools/clerk/delete_organization.ts @@ -0,0 +1,78 @@ +import { createLogger } from '@sim/logger' +import type { + ClerkApiError, + ClerkDeleteOrganizationParams, + ClerkDeleteOrganizationResponse, + ClerkDeleteResponse, +} from '@/tools/clerk/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ClerkDeleteOrganization') + +export const clerkDeleteOrganizationTool: ToolConfig< + ClerkDeleteOrganizationParams, + ClerkDeleteOrganizationResponse +> = { + id: 'clerk_delete_organization', + name: 'Delete Organization from Clerk', + description: 'Delete an organization from your Clerk application', + 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 to delete (e.g., org_2NNEqL2nrIRdJ194ndJqAHwEfxC)', + }, + }, + + request: { + url: (params) => `https://api.clerk.com/v1/organizations/${params.organizationId?.trim()}`, + method: 'DELETE', + 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: ClerkDeleteResponse | 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 delete organization from Clerk' + ) + } + + const deleteData = data as ClerkDeleteResponse + return { + success: true, + output: { + id: deleteData.id, + object: deleteData.object ?? 'organization', + deleted: deleteData.deleted ?? true, + success: true, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Deleted organization ID' }, + object: { type: 'string', description: 'Object type (organization)' }, + deleted: { type: 'boolean', description: 'Whether the organization was deleted' }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/clerk/get_jwt_template.ts b/apps/sim/tools/clerk/get_jwt_template.ts new file mode 100644 index 00000000000..311e21aa61e --- /dev/null +++ b/apps/sim/tools/clerk/get_jwt_template.ts @@ -0,0 +1,94 @@ +import { createLogger } from '@sim/logger' +import type { + ClerkApiError, + ClerkGetJwtTemplateParams, + ClerkGetJwtTemplateResponse, + ClerkJwtTemplate, +} from '@/tools/clerk/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ClerkGetJwtTemplate') + +export const clerkGetJwtTemplateTool: ToolConfig< + ClerkGetJwtTemplateParams, + ClerkGetJwtTemplateResponse +> = { + id: 'clerk_get_jwt_template', + name: 'Get JWT Template from Clerk', + description: 'Retrieve a single custom JWT template by ID from Clerk', + version: '1.0.0', + + params: { + secretKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Clerk Secret Key for API authentication', + }, + templateId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the JWT template to retrieve', + }, + }, + + request: { + url: (params) => `https://api.clerk.com/v1/jwt_templates/${params.templateId?.trim()}`, + method: 'GET', + 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: ClerkJwtTemplate | 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 get JWT template from Clerk' + ) + } + + const template = data as ClerkJwtTemplate + + return { + success: true, + output: { + id: template.id, + name: template.name, + claims: template.claims ?? {}, + lifetime: template.lifetime, + allowedClockSkew: template.allowed_clock_skew, + customSigningKey: template.custom_signing_key ?? false, + signingAlgorithm: template.signing_algorithm, + createdAt: template.created_at, + updatedAt: template.updated_at, + success: true, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'JWT template ID' }, + name: { type: 'string', description: 'JWT template name' }, + claims: { type: 'json', description: 'Custom claims defined on the template' }, + lifetime: { type: 'number', description: 'Token lifetime in seconds' }, + allowedClockSkew: { type: 'number', description: 'Allowed clock skew in seconds' }, + customSigningKey: { + type: 'boolean', + description: 'Whether a custom signing key is configured', + }, + signingAlgorithm: { type: 'string', description: 'Signing algorithm used' }, + createdAt: { type: 'number', description: 'Creation timestamp' }, + updatedAt: { type: 'number', description: 'Last update timestamp' }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/clerk/get_organization.ts b/apps/sim/tools/clerk/get_organization.ts index 501f221eade..14b3f1a6ffb 100644 --- a/apps/sim/tools/clerk/get_organization.ts +++ b/apps/sim/tools/clerk/get_organization.ts @@ -35,7 +35,7 @@ export const clerkGetOrganizationTool: ToolConfig< }, request: { - url: (params) => `https://api.clerk.com/v1/organizations/${params.organizationId}`, + url: (params) => `https://api.clerk.com/v1/organizations/${params.organizationId?.trim()}`, method: 'GET', headers: (params) => { if (!params.secretKey) { diff --git a/apps/sim/tools/clerk/get_session.ts b/apps/sim/tools/clerk/get_session.ts index 7d3b58499da..ed271e09d16 100644 --- a/apps/sim/tools/clerk/get_session.ts +++ b/apps/sim/tools/clerk/get_session.ts @@ -31,7 +31,7 @@ export const clerkGetSessionTool: ToolConfig `https://api.clerk.com/v1/sessions/${params.sessionId}`, + url: (params) => `https://api.clerk.com/v1/sessions/${params.sessionId?.trim()}`, method: 'GET', headers: (params) => { if (!params.secretKey) { diff --git a/apps/sim/tools/clerk/get_user_oauth_token.ts b/apps/sim/tools/clerk/get_user_oauth_token.ts new file mode 100644 index 00000000000..2c6e8602f8f --- /dev/null +++ b/apps/sim/tools/clerk/get_user_oauth_token.ts @@ -0,0 +1,116 @@ +import { createLogger } from '@sim/logger' +import type { + ClerkApiError, + ClerkGetUserOauthTokenParams, + ClerkGetUserOauthTokenResponse, + ClerkOAuthAccessToken, +} from '@/tools/clerk/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ClerkGetUserOauthToken') + +export const clerkGetUserOauthTokenTool: ToolConfig< + ClerkGetUserOauthTokenParams, + ClerkGetUserOauthTokenResponse +> = { + id: 'clerk_get_user_oauth_token', + name: 'Get User OAuth Access Token from Clerk', + description: + "Retrieve a user's OAuth access token for a connected external provider (e.g. Google, GitHub, Microsoft) obtained via Clerk SSO", + 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 (e.g., user_2NNEqL2nrIRdJ194ndJqAHwEfxC)', + }, + provider: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'OAuth provider slug, e.g. google, github, microsoft, discord (without the oauth_ prefix)', + }, + }, + + request: { + url: (params) => { + const providerSlug = params.provider?.trim().replace(/^oauth_/, '') + return `https://api.clerk.com/v1/users/${params.userId?.trim()}/oauth_access_tokens/oauth_${providerSlug}` + }, + method: 'GET', + 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: ClerkOAuthAccessToken[] | 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 get user OAuth access token from Clerk' + ) + } + + const tokens = data as ClerkOAuthAccessToken[] + return { + success: true, + output: { + accessTokens: tokens.map((token) => ({ + externalAccountId: token.external_account_id, + token: token.token, + expiresAt: token.expires_at ?? null, + provider: token.provider, + label: token.label ?? null, + scopes: token.scopes ?? [], + publicMetadata: token.public_metadata ?? {}, + })), + success: true, + }, + } + }, + + outputs: { + accessTokens: { + type: 'array', + description: 'OAuth access tokens for the connected provider', + items: { + type: 'object', + properties: { + externalAccountId: { type: 'string', description: 'External account ID' }, + token: { type: 'string', description: 'OAuth access token' }, + expiresAt: { type: 'number', description: 'Expiration timestamp', optional: true }, + provider: { type: 'string', description: 'OAuth provider slug' }, + label: { type: 'string', description: 'Token label', optional: true }, + scopes: { + type: 'array', + description: 'OAuth scopes granted to the token', + items: { type: 'string' }, + }, + publicMetadata: { + type: 'json', + description: 'Public metadata associated with the token', + }, + }, + }, + }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/clerk/index.ts b/apps/sim/tools/clerk/index.ts index eda1978ee43..7c81a840012 100644 --- a/apps/sim/tools/clerk/index.ts +++ b/apps/sim/tools/clerk/index.ts @@ -1,11 +1,34 @@ +export { clerkAddOrganizationMemberTool } from './add_organization_member' +export { clerkBanUserTool } from './ban_user' +export { clerkCreateActorTokenTool } from './create_actor_token' +export { clerkCreateAllowlistIdentifierTool } from './create_allowlist_identifier' +export { clerkCreateBlocklistIdentifierTool } from './create_blocklist_identifier' export { clerkCreateOrganizationTool } from './create_organization' +export { clerkCreateOrganizationInvitationTool } from './create_organization_invitation' export { clerkCreateUserTool } from './create_user' +export { clerkDeleteAllowlistIdentifierTool } from './delete_allowlist_identifier' +export { clerkDeleteBlocklistIdentifierTool } from './delete_blocklist_identifier' +export { clerkDeleteOrganizationTool } from './delete_organization' export { clerkDeleteUserTool } from './delete_user' +export { clerkGetJwtTemplateTool } from './get_jwt_template' export { clerkGetOrganizationTool } from './get_organization' export { clerkGetSessionTool } from './get_session' export { clerkGetUserTool } from './get_user' +export { clerkGetUserOauthTokenTool } from './get_user_oauth_token' +export { clerkListAllowlistIdentifiersTool } from './list_allowlist_identifiers' +export { clerkListBlocklistIdentifiersTool } from './list_blocklist_identifiers' +export { clerkListJwtTemplatesTool } from './list_jwt_templates' +export { clerkListOrganizationInvitationsTool } from './list_organization_invitations' +export { clerkListOrganizationMembershipsTool } from './list_organization_memberships' export { clerkListOrganizationsTool } from './list_organizations' export { clerkListSessionsTool } from './list_sessions' export { clerkListUsersTool } from './list_users' +export { clerkLockUserTool } from './lock_user' +export { clerkRemoveOrganizationMemberTool } from './remove_organization_member' +export { clerkRevokeActorTokenTool } from './revoke_actor_token' export { clerkRevokeSessionTool } from './revoke_session' +export { clerkUnbanUserTool } from './unban_user' +export { clerkUnlockUserTool } from './unlock_user' +export { clerkUpdateOrganizationTool } from './update_organization' +export { clerkUpdateOrganizationMembershipTool } from './update_organization_membership' export { clerkUpdateUserTool } from './update_user' diff --git a/apps/sim/tools/clerk/list_allowlist_identifiers.ts b/apps/sim/tools/clerk/list_allowlist_identifiers.ts new file mode 100644 index 00000000000..0f5f1ce0986 --- /dev/null +++ b/apps/sim/tools/clerk/list_allowlist_identifiers.ts @@ -0,0 +1,119 @@ +import { createLogger } from '@sim/logger' +import type { + ClerkAllowlistIdentifier, + ClerkApiError, + ClerkListAllowlistIdentifiersParams, + ClerkListAllowlistIdentifiersResponse, +} from '@/tools/clerk/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ClerkListAllowlistIdentifiers') + +export const clerkListAllowlistIdentifiersTool: ToolConfig< + ClerkListAllowlistIdentifiersParams, + ClerkListAllowlistIdentifiersResponse +> = { + id: 'clerk_list_allowlist_identifiers', + name: 'List Allowlist Identifiers from Clerk', + description: 'List email/phone/web3-wallet identifiers on your Clerk instance allowlist', + version: '1.0.0', + + params: { + secretKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Clerk Secret Key for API authentication', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (e.g., 10, 50, 100; range: 1-500, default: 10)', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results to skip for pagination (e.g., 0, 10, 20)', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + + if (params.limit) queryParams.append('limit', params.limit.toString()) + if (params.offset) queryParams.append('offset', params.offset.toString()) + + const queryString = queryParams.toString() + return queryString + ? `https://api.clerk.com/v1/allowlist_identifiers?${queryString}` + : 'https://api.clerk.com/v1/allowlist_identifiers' + }, + method: 'GET', + 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: ClerkAllowlistIdentifier[] | 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 list allowlist identifiers from Clerk' + ) + } + + const identifiers = (data as ClerkAllowlistIdentifier[]).map((identifier) => ({ + id: identifier.id, + identifier: identifier.identifier, + identifierType: identifier.identifier_type, + invitationId: identifier.invitation_id ?? null, + createdAt: identifier.created_at, + updatedAt: identifier.updated_at, + })) + + return { + success: true, + output: { + identifiers, + totalCount: identifiers.length, + success: true, + }, + } + }, + + outputs: { + identifiers: { + type: 'array', + description: 'Array of Clerk allowlist identifier objects', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Allowlist identifier ID' }, + identifier: { type: 'string', description: 'Email, phone, or web3 wallet identifier' }, + identifierType: { type: 'string', description: 'Type of identifier' }, + invitationId: { + type: 'string', + description: 'Associated invitation ID', + optional: true, + }, + createdAt: { type: 'number', description: 'Creation timestamp' }, + updatedAt: { type: 'number', description: 'Last update timestamp' }, + }, + }, + }, + totalCount: { type: 'number', description: 'Total number of allowlist identifiers' }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/clerk/list_blocklist_identifiers.ts b/apps/sim/tools/clerk/list_blocklist_identifiers.ts new file mode 100644 index 00000000000..ddc06ab6df6 --- /dev/null +++ b/apps/sim/tools/clerk/list_blocklist_identifiers.ts @@ -0,0 +1,94 @@ +import { createLogger } from '@sim/logger' +import type { + ClerkApiError, + ClerkBlocklistIdentifier, + ClerkListBlocklistIdentifiersParams, + ClerkListBlocklistIdentifiersResponse, +} from '@/tools/clerk/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ClerkListBlocklistIdentifiers') + +export const clerkListBlocklistIdentifiersTool: ToolConfig< + ClerkListBlocklistIdentifiersParams, + ClerkListBlocklistIdentifiersResponse +> = { + id: 'clerk_list_blocklist_identifiers', + name: 'List Blocklist Identifiers from Clerk', + description: 'List email/phone/web3-wallet identifiers on your Clerk instance blocklist', + version: '1.0.0', + + params: { + secretKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Clerk Secret Key for API authentication', + }, + }, + + request: { + url: () => 'https://api.clerk.com/v1/blocklist_identifiers', + method: 'GET', + 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 json: { data: ClerkBlocklistIdentifier[]; total_count: number } | ClerkApiError = + await response.json() + + if (!response.ok) { + logger.error('Clerk API request failed', { data: json, status: response.status }) + throw new Error( + (json as ClerkApiError).errors?.[0]?.message || + 'Failed to list blocklist identifiers from Clerk' + ) + } + + const responseData = json as { data: ClerkBlocklistIdentifier[]; total_count: number } + + const identifiers = responseData.data.map((identifier) => ({ + id: identifier.id, + identifier: identifier.identifier, + identifierType: identifier.identifier_type, + createdAt: identifier.created_at, + updatedAt: identifier.updated_at, + })) + + return { + success: true, + output: { + identifiers, + totalCount: responseData.total_count ?? identifiers.length, + success: true, + }, + } + }, + + outputs: { + identifiers: { + type: 'array', + description: 'Array of Clerk blocklist identifier objects', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Blocklist identifier ID' }, + identifier: { type: 'string', description: 'Email, phone, or web3 wallet identifier' }, + identifierType: { type: 'string', description: 'Type of identifier' }, + createdAt: { type: 'number', description: 'Creation timestamp' }, + updatedAt: { type: 'number', description: 'Last update timestamp' }, + }, + }, + }, + totalCount: { type: 'number', description: 'Total number of blocklist identifiers' }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/clerk/list_jwt_templates.ts b/apps/sim/tools/clerk/list_jwt_templates.ts new file mode 100644 index 00000000000..7be7a58c5da --- /dev/null +++ b/apps/sim/tools/clerk/list_jwt_templates.ts @@ -0,0 +1,101 @@ +import { createLogger } from '@sim/logger' +import type { + ClerkApiError, + ClerkJwtTemplate, + ClerkListJwtTemplatesParams, + ClerkListJwtTemplatesResponse, +} from '@/tools/clerk/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ClerkListJwtTemplates') + +export const clerkListJwtTemplatesTool: ToolConfig< + ClerkListJwtTemplatesParams, + ClerkListJwtTemplatesResponse +> = { + id: 'clerk_list_jwt_templates', + name: 'List JWT Templates from Clerk', + description: 'List custom JWT templates configured on your Clerk instance', + version: '1.0.0', + + params: { + secretKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Clerk Secret Key for API authentication', + }, + }, + + request: { + url: () => 'https://api.clerk.com/v1/jwt_templates', + method: 'GET', + 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: ClerkJwtTemplate[] | 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 list JWT templates from Clerk' + ) + } + + const templates = (data as ClerkJwtTemplate[]).map((template) => ({ + id: template.id, + name: template.name, + claims: template.claims ?? {}, + lifetime: template.lifetime, + allowedClockSkew: template.allowed_clock_skew, + customSigningKey: template.custom_signing_key ?? false, + signingAlgorithm: template.signing_algorithm, + createdAt: template.created_at, + updatedAt: template.updated_at, + })) + + return { + success: true, + output: { + templates, + totalCount: templates.length, + success: true, + }, + } + }, + + outputs: { + templates: { + type: 'array', + description: 'Array of Clerk JWT template objects', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'JWT template ID' }, + name: { type: 'string', description: 'JWT template name' }, + claims: { type: 'json', description: 'Custom claims defined on the template' }, + lifetime: { type: 'number', description: 'Token lifetime in seconds' }, + allowedClockSkew: { type: 'number', description: 'Allowed clock skew in seconds' }, + customSigningKey: { + type: 'boolean', + description: 'Whether a custom signing key is configured', + }, + signingAlgorithm: { type: 'string', description: 'Signing algorithm used' }, + createdAt: { type: 'number', description: 'Creation timestamp' }, + updatedAt: { type: 'number', description: 'Last update timestamp' }, + }, + }, + }, + totalCount: { type: 'number', description: 'Total number of JWT templates' }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/clerk/list_organization_invitations.ts b/apps/sim/tools/clerk/list_organization_invitations.ts new file mode 100644 index 00000000000..9aa02429a45 --- /dev/null +++ b/apps/sim/tools/clerk/list_organization_invitations.ts @@ -0,0 +1,162 @@ +import { createLogger } from '@sim/logger' +import type { + ClerkApiError, + ClerkListOrganizationInvitationsParams, + ClerkListOrganizationInvitationsResponse, + ClerkOrganizationInvitation, +} from '@/tools/clerk/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ClerkListOrganizationInvitations') + +export const clerkListOrganizationInvitationsTool: ToolConfig< + ClerkListOrganizationInvitationsParams, + ClerkListOrganizationInvitationsResponse +> = { + id: 'clerk_list_organization_invitations', + name: 'List Organization Invitations from Clerk', + description: 'List pending and past invitations for a Clerk organization', + 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)', + }, + status: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by status: pending, accepted, revoked, or expired', + }, + emailAddress: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by invited email address', + }, + orderBy: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort field (created_at, email_address) with +/- prefix (default: -created_at)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (e.g., 10, 50, 100; range: 1-500, default: 10)', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results to skip for pagination (e.g., 0, 10, 20)', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + + if (params.status) queryParams.append('status', params.status) + if (params.emailAddress) queryParams.append('email_address', params.emailAddress) + if (params.orderBy) queryParams.append('order_by', params.orderBy) + if (params.limit) queryParams.append('limit', params.limit.toString()) + if (params.offset) queryParams.append('offset', params.offset.toString()) + + const queryString = queryParams.toString() + const base = `https://api.clerk.com/v1/organizations/${params.organizationId?.trim()}/invitations` + return queryString ? `${base}?${queryString}` : base + }, + method: 'GET', + 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 json: { data: ClerkOrganizationInvitation[]; total_count: number } | ClerkApiError = + await response.json() + + if (!response.ok) { + logger.error('Clerk API request failed', { data: json, status: response.status }) + throw new Error( + (json as ClerkApiError).errors?.[0]?.message || + 'Failed to list organization invitations from Clerk' + ) + } + + const responseData = json as { data: ClerkOrganizationInvitation[]; total_count: number } + + const invitations = responseData.data.map((invitation) => ({ + id: invitation.id, + emailAddress: invitation.email_address, + role: invitation.role, + roleName: invitation.role_name ?? null, + organizationId: invitation.organization_id, + inviterId: invitation.inviter_id ?? null, + inviterEmail: invitation.public_inviter_data?.identifier ?? null, + inviterFirstName: invitation.public_inviter_data?.first_name ?? null, + inviterLastName: invitation.public_inviter_data?.last_name ?? null, + status: invitation.status, + url: invitation.url ?? null, + expiresAt: invitation.expires_at ?? null, + publicMetadata: invitation.public_metadata ?? {}, + createdAt: invitation.created_at, + updatedAt: invitation.updated_at, + })) + + return { + success: true, + output: { + invitations, + totalCount: responseData.total_count ?? invitations.length, + success: true, + }, + } + }, + + outputs: { + invitations: { + type: 'array', + description: 'Array of Clerk organization invitation objects', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Invitation ID' }, + emailAddress: { type: 'string', description: 'Invited email address' }, + role: { type: 'string', description: 'Role to assign on acceptance' }, + roleName: { type: 'string', description: 'Human-readable role name', optional: true }, + organizationId: { type: 'string', description: 'Organization ID' }, + inviterId: { type: 'string', description: 'User ID of the inviter', optional: true }, + inviterEmail: { type: 'string', description: "Inviter's email address", optional: true }, + inviterFirstName: { type: 'string', description: "Inviter's first name", optional: true }, + inviterLastName: { type: 'string', description: "Inviter's last name", optional: true }, + status: { type: 'string', description: 'Invitation status' }, + url: { type: 'string', description: 'Invitation URL', optional: true }, + expiresAt: { type: 'number', description: 'Expiration timestamp', optional: true }, + publicMetadata: { type: 'json', description: 'Public metadata' }, + createdAt: { type: 'number', description: 'Creation timestamp' }, + updatedAt: { type: 'number', description: 'Last update timestamp' }, + }, + }, + }, + totalCount: { type: 'number', description: 'Total number of invitations' }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/clerk/list_organization_memberships.ts b/apps/sim/tools/clerk/list_organization_memberships.ts new file mode 100644 index 00000000000..5c1bac5f3b9 --- /dev/null +++ b/apps/sim/tools/clerk/list_organization_memberships.ts @@ -0,0 +1,167 @@ +import { createLogger } from '@sim/logger' +import type { + ClerkApiError, + ClerkListOrganizationMembershipsParams, + ClerkListOrganizationMembershipsResponse, + ClerkOrganizationMembership, +} from '@/tools/clerk/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ClerkListOrganizationMemberships') + +export const clerkListOrganizationMembershipsTool: ToolConfig< + ClerkListOrganizationMembershipsParams, + ClerkListOrganizationMembershipsResponse +> = { + id: 'clerk_list_organization_memberships', + name: 'List Organization Memberships from Clerk', + description: 'List members of a Clerk organization with optional filtering and pagination', + 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)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results per page (e.g., 10, 50, 100; range: 1-500, default: 10)', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Number of results to skip for pagination (e.g., 0, 10, 20)', + }, + orderBy: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Sort field (e.g., created_at) with +/- prefix for direction', + }, + role: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by role, comma-separated for multiple (e.g., org:admin,org:member)', + }, + }, + + request: { + url: (params) => { + const queryParams = new URLSearchParams() + + if (params.limit) queryParams.append('limit', params.limit.toString()) + if (params.offset) queryParams.append('offset', params.offset.toString()) + if (params.orderBy) queryParams.append('order_by', params.orderBy) + if (params.role) { + params.role.split(',').forEach((role) => { + queryParams.append('role', role.trim()) + }) + } + + const queryString = queryParams.toString() + const base = `https://api.clerk.com/v1/organizations/${params.organizationId?.trim()}/memberships` + return queryString ? `${base}?${queryString}` : base + }, + method: 'GET', + 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 json: { data: ClerkOrganizationMembership[]; total_count: number } | ClerkApiError = + await response.json() + + if (!response.ok) { + logger.error('Clerk API request failed', { data: json, status: response.status }) + throw new Error( + (json as ClerkApiError).errors?.[0]?.message || + 'Failed to list organization memberships from Clerk' + ) + } + + const responseData = json as { data: ClerkOrganizationMembership[]; total_count: number } + + const memberships = responseData.data.map((membership) => ({ + 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, + })) + + return { + success: true, + output: { + memberships, + totalCount: responseData.total_count ?? memberships.length, + success: true, + }, + } + }, + + outputs: { + memberships: { + type: 'array', + description: 'Array of Clerk organization membership objects', + items: { + type: 'object', + properties: { + 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' }, + }, + }, + }, + totalCount: { type: 'number', description: 'Total number of memberships' }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/clerk/lock_user.ts b/apps/sim/tools/clerk/lock_user.ts new file mode 100644 index 00000000000..290cbc496e4 --- /dev/null +++ b/apps/sim/tools/clerk/lock_user.ts @@ -0,0 +1,89 @@ +import { createLogger } from '@sim/logger' +import type { + ClerkApiError, + ClerkLockUserParams, + ClerkLockUserResponse, + ClerkUser, +} from '@/tools/clerk/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ClerkLockUser') + +export const clerkLockUserTool: ToolConfig = { + id: 'clerk_lock_user', + name: 'Lock User in Clerk', + description: 'Lock a user account, blocking sign-in attempts', + 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 lock (e.g., user_2NNEqL2nrIRdJ194ndJqAHwEfxC)', + }, + }, + + request: { + url: (params) => `https://api.clerk.com/v1/users/${params.userId?.trim()}/lock`, + 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 lock 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' }, + }, +} diff --git a/apps/sim/tools/clerk/remove_organization_member.ts b/apps/sim/tools/clerk/remove_organization_member.ts new file mode 100644 index 00000000000..9b35396661d --- /dev/null +++ b/apps/sim/tools/clerk/remove_organization_member.ts @@ -0,0 +1,114 @@ +import { createLogger } from '@sim/logger' +import type { + ClerkApiError, + ClerkOrganizationMembership, + ClerkRemoveOrganizationMemberParams, + ClerkRemoveOrganizationMemberResponse, +} from '@/tools/clerk/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ClerkRemoveOrganizationMember') + +export const clerkRemoveOrganizationMemberTool: ToolConfig< + ClerkRemoveOrganizationMemberParams, + ClerkRemoveOrganizationMemberResponse +> = { + id: 'clerk_remove_organization_member', + name: 'Remove Organization Member from Clerk', + description: 'Remove a member from a Clerk organization', + 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 member to remove', + }, + }, + + request: { + url: (params) => + `https://api.clerk.com/v1/organizations/${params.organizationId?.trim()}/memberships/${params.userId?.trim()}`, + method: 'DELETE', + 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: 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 remove organization member from 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' }, + }, +} diff --git a/apps/sim/tools/clerk/revoke_actor_token.ts b/apps/sim/tools/clerk/revoke_actor_token.ts new file mode 100644 index 00000000000..2457dbcb330 --- /dev/null +++ b/apps/sim/tools/clerk/revoke_actor_token.ts @@ -0,0 +1,89 @@ +import { createLogger } from '@sim/logger' +import type { + ClerkActorToken, + ClerkApiError, + ClerkRevokeActorTokenParams, + ClerkRevokeActorTokenResponse, +} from '@/tools/clerk/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ClerkRevokeActorToken') + +export const clerkRevokeActorTokenTool: ToolConfig< + ClerkRevokeActorTokenParams, + ClerkRevokeActorTokenResponse +> = { + id: 'clerk_revoke_actor_token', + name: 'Revoke Actor Token in Clerk', + description: 'Revoke an actor token before it is used or expires', + version: '1.0.0', + + params: { + secretKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The Clerk Secret Key for API authentication', + }, + actorTokenId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the actor token to revoke', + }, + }, + + request: { + url: (params) => `https://api.clerk.com/v1/actor_tokens/${params.actorTokenId?.trim()}/revoke`, + 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: 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 revoke 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 (should be revoked)' }, + 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' }, + }, +} diff --git a/apps/sim/tools/clerk/revoke_session.ts b/apps/sim/tools/clerk/revoke_session.ts index 3a1e9a00c5d..311ead427e8 100644 --- a/apps/sim/tools/clerk/revoke_session.ts +++ b/apps/sim/tools/clerk/revoke_session.ts @@ -34,7 +34,7 @@ export const clerkRevokeSessionTool: ToolConfig< }, request: { - url: (params) => `https://api.clerk.com/v1/sessions/${params.sessionId}/revoke`, + url: (params) => `https://api.clerk.com/v1/sessions/${params.sessionId?.trim()}/revoke`, method: 'POST', headers: (params) => { if (!params.secretKey) { diff --git a/apps/sim/tools/clerk/types.ts b/apps/sim/tools/clerk/types.ts index 7d09f59a8a9..72ea1e8a23d 100644 --- a/apps/sim/tools/clerk/types.ts +++ b/apps/sim/tools/clerk/types.ts @@ -537,6 +537,544 @@ export interface ClerkRevokeSessionResponse extends ToolResponse { } } +// Update Organization +export interface ClerkUpdateOrganizationParams { + secretKey: string + organizationId: string + name?: string + slug?: string + maxAllowedMemberships?: number + adminDeleteEnabled?: boolean +} + +export interface ClerkUpdateOrganizationResponse extends ToolResponse { + output: { + id: string + name: string + slug: string | null + imageUrl: string | null + hasImage: boolean + membersCount: number | null + pendingInvitationsCount: number | null + maxAllowedMemberships: number + adminDeleteEnabled: boolean + createdBy: string | null + createdAt: number + updatedAt: number + publicMetadata: Record + success: boolean + } +} + +// Delete Organization +export interface ClerkDeleteOrganizationParams { + secretKey: string + organizationId: string +} + +export interface ClerkDeleteOrganizationResponse extends ToolResponse { + output: { + id: string + object: string + deleted: boolean + success: boolean + } +} + +/** + * Clerk Organization Membership object. + * `public_user_data` mirrors the OpenAPI spec (richer than the @clerk/backend SDK's + * resource class, which omits `username`/`banned`/the deprecated `profile_image_url`). + */ +export interface ClerkOrganizationMembershipPublicUserData { + user_id: string + first_name: string | null + last_name: string | null + image_url: string + has_image: boolean + identifier: string | null + username?: string | null + banned?: boolean +} + +export interface ClerkOrganizationMembership { + id: string + object: 'organization_membership' + role: string + role_name?: string + permissions: string[] + public_metadata: Record + private_metadata?: Record + organization: ClerkOrganization + public_user_data: ClerkOrganizationMembershipPublicUserData + created_at: number + updated_at: number +} + +interface ClerkOrganizationMembershipOutput { + id: string + role: string + roleName: string | null + permissions: string[] + organizationId: string + userId: string + firstName: string | null + lastName: string | null + imageUrl: string | null + identifier: string | null + username: string | null + banned: boolean + publicMetadata: Record + createdAt: number + updatedAt: number +} + +// List Organization Memberships +export interface ClerkListOrganizationMembershipsParams { + secretKey: string + organizationId: string + limit?: number + offset?: number + orderBy?: string + role?: string +} + +export interface ClerkListOrganizationMembershipsResponse extends ToolResponse { + output: { + memberships: ClerkOrganizationMembershipOutput[] + totalCount: number + success: boolean + } +} + +// Add Organization Member (create membership) +export interface ClerkAddOrganizationMemberParams { + secretKey: string + organizationId: string + userId: string + role: string +} + +export interface ClerkAddOrganizationMemberResponse extends ToolResponse { + output: ClerkOrganizationMembershipOutput & { success: boolean } +} + +// Update Organization Membership (change role) +export interface ClerkUpdateOrganizationMembershipParams { + secretKey: string + organizationId: string + userId: string + role: string +} + +export interface ClerkUpdateOrganizationMembershipResponse extends ToolResponse { + output: ClerkOrganizationMembershipOutput & { success: boolean } +} + +// Remove Organization Member (delete membership) +export interface ClerkRemoveOrganizationMemberParams { + secretKey: string + organizationId: string + userId: string +} + +export interface ClerkRemoveOrganizationMemberResponse extends ToolResponse { + output: ClerkOrganizationMembershipOutput & { success: boolean } +} + +/** + * Clerk Organization Invitation object. + */ +export interface ClerkOrganizationInvitationPublicUserData { + user_id: string + first_name: string | null + last_name: string | null + image_url: string + has_image: boolean + identifier: string +} + +export interface ClerkOrganizationInvitation { + id: string + object: 'organization_invitation' + email_address: string + role: string + role_name?: string + organization_id: string + inviter_id: string | null + public_inviter_data: ClerkOrganizationInvitationPublicUserData | null + status: 'pending' | 'accepted' | 'revoked' | 'expired' + public_metadata: Record + private_metadata?: Record + url: string | null + expires_at: number | null + created_at: number + updated_at: number +} + +interface ClerkOrganizationInvitationOutput { + id: string + emailAddress: string + role: string + roleName: string | null + organizationId: string + inviterId: string | null + inviterEmail: string | null + inviterFirstName: string | null + inviterLastName: string | null + status: string + url: string | null + expiresAt: number | null + publicMetadata: Record + createdAt: number + updatedAt: number +} + +// Create Organization Invitation +export interface ClerkCreateOrganizationInvitationParams { + secretKey: string + organizationId: string + emailAddress: string + role: string + inviterUserId?: string + redirectUrl?: string + expiresInDays?: number + publicMetadata?: Record + privateMetadata?: Record + notify?: boolean +} + +export interface ClerkCreateOrganizationInvitationResponse extends ToolResponse { + output: ClerkOrganizationInvitationOutput & { success: boolean } +} + +// List Organization Invitations +export interface ClerkListOrganizationInvitationsParams { + secretKey: string + organizationId: string + status?: 'pending' | 'accepted' | 'revoked' | 'expired' + emailAddress?: string + orderBy?: string + limit?: number + offset?: number +} + +export interface ClerkListOrganizationInvitationsResponse extends ToolResponse { + output: { + invitations: ClerkOrganizationInvitationOutput[] + totalCount: number + success: boolean + } +} + +interface ClerkUserModerationOutput { + id: string + username: string | null + firstName: string | null + lastName: string | null + banned: boolean + locked: boolean + lockoutExpiresInSeconds: number | null + updatedAt: number +} + +// Ban User +export interface ClerkBanUserParams { + secretKey: string + userId: string +} + +export interface ClerkBanUserResponse extends ToolResponse { + output: ClerkUserModerationOutput & { success: boolean } +} + +// Unban User +export interface ClerkUnbanUserParams { + secretKey: string + userId: string +} + +export interface ClerkUnbanUserResponse extends ToolResponse { + output: ClerkUserModerationOutput & { success: boolean } +} + +// Lock User +export interface ClerkLockUserParams { + secretKey: string + userId: string +} + +export interface ClerkLockUserResponse extends ToolResponse { + output: ClerkUserModerationOutput & { success: boolean } +} + +// Unlock User +export interface ClerkUnlockUserParams { + secretKey: string + userId: string +} + +export interface ClerkUnlockUserResponse extends ToolResponse { + output: ClerkUserModerationOutput & { success: boolean } +} + +/** + * Clerk OAuth Access Token object. + */ +export interface ClerkOAuthAccessToken { + object: 'oauth_access_token' + external_account_id: string + token: string + expires_at: number | null + provider: string + public_metadata: Record + label: string | null + scopes?: string[] +} + +// Get User OAuth Access Token +export interface ClerkGetUserOauthTokenParams { + secretKey: string + userId: string + provider: string +} + +export interface ClerkGetUserOauthTokenResponse extends ToolResponse { + output: { + accessTokens: { + externalAccountId: string + token: string + expiresAt: number | null + provider: string + label: string | null + scopes: string[] + publicMetadata: Record + }[] + success: boolean + } +} + +/** + * Clerk Allowlist/Blocklist Identifier objects. + */ +export interface ClerkAllowlistIdentifier { + id: string + object: 'allowlist_identifier' + identifier: string + identifier_type: string + invitation_id?: string | null + instance_id?: string + created_at: number + updated_at: number +} + +export interface ClerkBlocklistIdentifier { + id: string + object: 'blocklist_identifier' + identifier: string + identifier_type: string + instance_id?: string + created_at: number + updated_at: number +} + +interface ClerkAllowlistIdentifierOutput { + id: string + identifier: string + identifierType: string + invitationId: string | null + createdAt: number + updatedAt: number +} + +interface ClerkBlocklistIdentifierOutput { + id: string + identifier: string + identifierType: string + createdAt: number + updatedAt: number +} + +// List Allowlist Identifiers +export interface ClerkListAllowlistIdentifiersParams { + secretKey: string + limit?: number + offset?: number +} + +export interface ClerkListAllowlistIdentifiersResponse extends ToolResponse { + output: { + identifiers: ClerkAllowlistIdentifierOutput[] + totalCount: number + success: boolean + } +} + +// Create Allowlist Identifier +export interface ClerkCreateAllowlistIdentifierParams { + secretKey: string + identifier: string + notify?: boolean +} + +export interface ClerkCreateAllowlistIdentifierResponse extends ToolResponse { + output: ClerkAllowlistIdentifierOutput & { success: boolean } +} + +// Delete Allowlist Identifier +export interface ClerkDeleteAllowlistIdentifierParams { + secretKey: string + identifierId: string +} + +export interface ClerkDeleteAllowlistIdentifierResponse extends ToolResponse { + output: { + id: string + object: string + deleted: boolean + success: boolean + } +} + +// List Blocklist Identifiers +export interface ClerkListBlocklistIdentifiersParams { + secretKey: string +} + +export interface ClerkListBlocklistIdentifiersResponse extends ToolResponse { + output: { + identifiers: ClerkBlocklistIdentifierOutput[] + totalCount: number + success: boolean + } +} + +// Create Blocklist Identifier +export interface ClerkCreateBlocklistIdentifierParams { + secretKey: string + identifier: string +} + +export interface ClerkCreateBlocklistIdentifierResponse extends ToolResponse { + output: ClerkBlocklistIdentifierOutput & { success: boolean } +} + +// Delete Blocklist Identifier +export interface ClerkDeleteBlocklistIdentifierParams { + secretKey: string + identifierId: string +} + +export interface ClerkDeleteBlocklistIdentifierResponse extends ToolResponse { + output: { + id: string + object: string + deleted: boolean + success: boolean + } +} + +/** + * Clerk JWT Template object. The signing key is write-only and never echoed back. + */ +export interface ClerkJwtTemplate { + id: string + object: 'jwt_template' + name: string + claims: Record + lifetime: number + allowed_clock_skew: number + custom_signing_key: boolean + signing_algorithm: string + created_at: number + updated_at: number +} + +interface ClerkJwtTemplateOutput { + id: string + name: string + claims: Record + lifetime: number + allowedClockSkew: number + customSigningKey: boolean + signingAlgorithm: string + createdAt: number + updatedAt: number +} + +// List JWT Templates +export interface ClerkListJwtTemplatesParams { + secretKey: string +} + +export interface ClerkListJwtTemplatesResponse extends ToolResponse { + output: { + templates: ClerkJwtTemplateOutput[] + totalCount: number + success: boolean + } +} + +// Get JWT Template +export interface ClerkGetJwtTemplateParams { + secretKey: string + templateId: string +} + +export interface ClerkGetJwtTemplateResponse extends ToolResponse { + output: ClerkJwtTemplateOutput & { success: boolean } +} + +/** + * Clerk Actor Token object. `token`/`url` are only present on creation, + * not once the token has been consumed or revoked. + */ +export interface ClerkActorToken { + id: string + object: 'actor_token' + status: 'pending' | 'accepted' | 'revoked' + user_id: string + actor: Record + token?: string | null + url?: string | null + created_at: number + updated_at: number +} + +interface ClerkActorTokenOutput { + id: string + status: string + userId: string + actor: Record + token: string | null + url: string | null + createdAt: number + updatedAt: number +} + +// Create Actor Token +export interface ClerkCreateActorTokenParams { + secretKey: string + userId: string + actor: Record + expiresInSeconds?: number + sessionMaxDurationInSeconds?: number +} + +export interface ClerkCreateActorTokenResponse extends ToolResponse { + output: ClerkActorTokenOutput & { success: boolean } +} + +// Revoke Actor Token +export interface ClerkRevokeActorTokenParams { + secretKey: string + actorTokenId: string +} + +export interface ClerkRevokeActorTokenResponse extends ToolResponse { + output: ClerkActorTokenOutput & { success: boolean } +} + // Generic response type for the block export type ClerkResponse = | ClerkListUsersResponse @@ -547,6 +1085,29 @@ export type ClerkResponse = | ClerkListOrganizationsResponse | ClerkGetOrganizationResponse | ClerkCreateOrganizationResponse + | ClerkUpdateOrganizationResponse + | ClerkDeleteOrganizationResponse | ClerkListSessionsResponse | ClerkGetSessionResponse | ClerkRevokeSessionResponse + | ClerkListOrganizationMembershipsResponse + | ClerkAddOrganizationMemberResponse + | ClerkUpdateOrganizationMembershipResponse + | ClerkRemoveOrganizationMemberResponse + | ClerkCreateOrganizationInvitationResponse + | ClerkListOrganizationInvitationsResponse + | ClerkBanUserResponse + | ClerkUnbanUserResponse + | ClerkLockUserResponse + | ClerkUnlockUserResponse + | ClerkGetUserOauthTokenResponse + | ClerkListAllowlistIdentifiersResponse + | ClerkCreateAllowlistIdentifierResponse + | ClerkDeleteAllowlistIdentifierResponse + | ClerkListBlocklistIdentifiersResponse + | ClerkCreateBlocklistIdentifierResponse + | ClerkDeleteBlocklistIdentifierResponse + | ClerkListJwtTemplatesResponse + | ClerkGetJwtTemplateResponse + | ClerkCreateActorTokenResponse + | ClerkRevokeActorTokenResponse diff --git a/apps/sim/tools/clerk/unban_user.ts b/apps/sim/tools/clerk/unban_user.ts new file mode 100644 index 00000000000..f025775e1c6 --- /dev/null +++ b/apps/sim/tools/clerk/unban_user.ts @@ -0,0 +1,89 @@ +import { createLogger } from '@sim/logger' +import type { + ClerkApiError, + ClerkUnbanUserParams, + ClerkUnbanUserResponse, + ClerkUser, +} from '@/tools/clerk/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ClerkUnbanUser') + +export const clerkUnbanUserTool: ToolConfig = { + id: 'clerk_unban_user', + name: 'Unban User in Clerk', + description: 'Remove a ban from a user, allowing them to sign in again', + 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 unban (e.g., user_2NNEqL2nrIRdJ194ndJqAHwEfxC)', + }, + }, + + request: { + url: (params) => `https://api.clerk.com/v1/users/${params.userId?.trim()}/unban`, + 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 unban 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' }, + }, +} diff --git a/apps/sim/tools/clerk/unlock_user.ts b/apps/sim/tools/clerk/unlock_user.ts new file mode 100644 index 00000000000..9c0ef1b8018 --- /dev/null +++ b/apps/sim/tools/clerk/unlock_user.ts @@ -0,0 +1,89 @@ +import { createLogger } from '@sim/logger' +import type { + ClerkApiError, + ClerkUnlockUserParams, + ClerkUnlockUserResponse, + ClerkUser, +} from '@/tools/clerk/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ClerkUnlockUser') + +export const clerkUnlockUserTool: ToolConfig = { + id: 'clerk_unlock_user', + name: 'Unlock User in Clerk', + description: 'Unlock a previously locked user account', + 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 unlock (e.g., user_2NNEqL2nrIRdJ194ndJqAHwEfxC)', + }, + }, + + request: { + url: (params) => `https://api.clerk.com/v1/users/${params.userId?.trim()}/unlock`, + 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 unlock 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' }, + }, +} diff --git a/apps/sim/tools/clerk/update_organization.ts b/apps/sim/tools/clerk/update_organization.ts new file mode 100644 index 00000000000..201fc650b22 --- /dev/null +++ b/apps/sim/tools/clerk/update_organization.ts @@ -0,0 +1,138 @@ +import { createLogger } from '@sim/logger' +import type { + ClerkApiError, + ClerkOrganization, + ClerkUpdateOrganizationParams, + ClerkUpdateOrganizationResponse, +} from '@/tools/clerk/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ClerkUpdateOrganization') + +export const clerkUpdateOrganizationTool: ToolConfig< + ClerkUpdateOrganizationParams, + ClerkUpdateOrganizationResponse +> = { + id: 'clerk_update_organization', + name: 'Update Organization in Clerk', + description: 'Update an existing organization in your Clerk application', + 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 to update (e.g., org_2NNEqL2nrIRdJ194ndJqAHwEfxC)', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Name of the organization', + }, + slug: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Slug identifier for the organization', + }, + maxAllowedMemberships: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum member capacity (0 for unlimited)', + }, + adminDeleteEnabled: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Whether admins can delete the organization', + }, + }, + + request: { + url: (params) => `https://api.clerk.com/v1/organizations/${params.organizationId?.trim()}`, + method: 'PATCH', + 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 = {} + + if (params.name !== undefined) body.name = params.name + if (params.slug !== undefined) body.slug = params.slug + if (params.maxAllowedMemberships !== undefined) + body.max_allowed_memberships = params.maxAllowedMemberships + if (params.adminDeleteEnabled !== undefined) + body.admin_delete_enabled = params.adminDeleteEnabled + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data: ClerkOrganization | 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 update organization in Clerk' + ) + } + + const org = data as ClerkOrganization + return { + success: true, + output: { + id: org.id, + name: org.name, + slug: org.slug ?? null, + imageUrl: org.image_url ?? null, + hasImage: org.has_image ?? false, + membersCount: org.members_count ?? null, + pendingInvitationsCount: org.pending_invitations_count ?? null, + maxAllowedMemberships: org.max_allowed_memberships ?? 0, + adminDeleteEnabled: org.admin_delete_enabled ?? false, + createdBy: org.created_by ?? null, + createdAt: org.created_at, + updatedAt: org.updated_at, + publicMetadata: org.public_metadata ?? {}, + success: true, + }, + } + }, + + outputs: { + id: { type: 'string', description: 'Organization ID' }, + name: { type: 'string', description: 'Organization name' }, + slug: { type: 'string', description: 'Organization slug', optional: true }, + imageUrl: { type: 'string', description: 'Organization image URL', optional: true }, + hasImage: { type: 'boolean', description: 'Whether organization has an image' }, + membersCount: { type: 'number', description: 'Number of members', optional: true }, + pendingInvitationsCount: { + type: 'number', + description: 'Number of pending invitations', + optional: true, + }, + maxAllowedMemberships: { type: 'number', description: 'Max allowed memberships' }, + adminDeleteEnabled: { type: 'boolean', description: 'Whether admin delete is enabled' }, + createdBy: { type: 'string', description: 'Creator user ID', optional: true }, + createdAt: { type: 'number', description: 'Creation timestamp' }, + updatedAt: { type: 'number', description: 'Last update timestamp' }, + publicMetadata: { type: 'json', description: 'Public metadata' }, + success: { type: 'boolean', description: 'Operation success status' }, + }, +} diff --git a/apps/sim/tools/clerk/update_organization_membership.ts b/apps/sim/tools/clerk/update_organization_membership.ts new file mode 100644 index 00000000000..d7cb8f8ae21 --- /dev/null +++ b/apps/sim/tools/clerk/update_organization_membership.ts @@ -0,0 +1,123 @@ +import { createLogger } from '@sim/logger' +import type { + ClerkApiError, + ClerkOrganizationMembership, + ClerkUpdateOrganizationMembershipParams, + ClerkUpdateOrganizationMembershipResponse, +} from '@/tools/clerk/types' +import type { ToolConfig } from '@/tools/types' + +const logger = createLogger('ClerkUpdateOrganizationMembership') + +export const clerkUpdateOrganizationMembershipTool: ToolConfig< + ClerkUpdateOrganizationMembershipParams, + ClerkUpdateOrganizationMembershipResponse +> = { + id: 'clerk_update_organization_membership', + name: 'Update Organization Membership in Clerk', + description: "Change a member's role within a Clerk organization", + 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 member whose role is being changed', + }, + role: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'New role to assign, e.g. org:admin or org:member', + }, + }, + + request: { + url: (params) => + `https://api.clerk.com/v1/organizations/${params.organizationId?.trim()}/memberships/${params.userId?.trim()}`, + method: 'PATCH', + headers: (params) => { + if (!params.secretKey) { + throw new Error('Clerk Secret Key is required') + } + return { + Authorization: `Bearer ${params.secretKey}`, + 'Content-Type': 'application/json', + } + }, + body: (params) => ({ + 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 update organization membership 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' }, + }, +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 2bb910c4eef..facc130273b 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -394,16 +394,39 @@ import { } from '@/tools/calendly' import { clayPopulateTool } from '@/tools/clay' import { + clerkAddOrganizationMemberTool, + clerkBanUserTool, + clerkCreateActorTokenTool, + clerkCreateAllowlistIdentifierTool, + clerkCreateBlocklistIdentifierTool, + clerkCreateOrganizationInvitationTool, clerkCreateOrganizationTool, clerkCreateUserTool, + clerkDeleteAllowlistIdentifierTool, + clerkDeleteBlocklistIdentifierTool, + clerkDeleteOrganizationTool, clerkDeleteUserTool, + clerkGetJwtTemplateTool, clerkGetOrganizationTool, clerkGetSessionTool, + clerkGetUserOauthTokenTool, clerkGetUserTool, + clerkListAllowlistIdentifiersTool, + clerkListBlocklistIdentifiersTool, + clerkListJwtTemplatesTool, + clerkListOrganizationInvitationsTool, + clerkListOrganizationMembershipsTool, clerkListOrganizationsTool, clerkListSessionsTool, clerkListUsersTool, + clerkLockUserTool, + clerkRemoveOrganizationMemberTool, + clerkRevokeActorTokenTool, clerkRevokeSessionTool, + clerkUnbanUserTool, + clerkUnlockUserTool, + clerkUpdateOrganizationMembershipTool, + clerkUpdateOrganizationTool, clerkUpdateUserTool, } from '@/tools/clerk' import { @@ -7001,12 +7024,35 @@ export const tools: Record = { clerk_create_user: clerkCreateUserTool, clerk_update_user: clerkUpdateUserTool, clerk_delete_user: clerkDeleteUserTool, + clerk_ban_user: clerkBanUserTool, + clerk_unban_user: clerkUnbanUserTool, + clerk_lock_user: clerkLockUserTool, + clerk_unlock_user: clerkUnlockUserTool, + clerk_get_user_oauth_token: clerkGetUserOauthTokenTool, clerk_list_organizations: clerkListOrganizationsTool, clerk_get_organization: clerkGetOrganizationTool, clerk_create_organization: clerkCreateOrganizationTool, + clerk_update_organization: clerkUpdateOrganizationTool, + clerk_delete_organization: clerkDeleteOrganizationTool, + clerk_list_organization_memberships: clerkListOrganizationMembershipsTool, + clerk_add_organization_member: clerkAddOrganizationMemberTool, + clerk_update_organization_membership: clerkUpdateOrganizationMembershipTool, + clerk_remove_organization_member: clerkRemoveOrganizationMemberTool, + clerk_create_organization_invitation: clerkCreateOrganizationInvitationTool, + clerk_list_organization_invitations: clerkListOrganizationInvitationsTool, clerk_list_sessions: clerkListSessionsTool, clerk_get_session: clerkGetSessionTool, clerk_revoke_session: clerkRevokeSessionTool, + clerk_list_allowlist_identifiers: clerkListAllowlistIdentifiersTool, + clerk_create_allowlist_identifier: clerkCreateAllowlistIdentifierTool, + clerk_delete_allowlist_identifier: clerkDeleteAllowlistIdentifierTool, + clerk_list_blocklist_identifiers: clerkListBlocklistIdentifiersTool, + clerk_create_blocklist_identifier: clerkCreateBlocklistIdentifierTool, + clerk_delete_blocklist_identifier: clerkDeleteBlocklistIdentifierTool, + clerk_list_jwt_templates: clerkListJwtTemplatesTool, + clerk_get_jwt_template: clerkGetJwtTemplateTool, + clerk_create_actor_token: clerkCreateActorTokenTool, + clerk_revoke_actor_token: clerkRevokeActorTokenTool, cloudflare_list_zones: cloudflareListZonesTool, cloudflare_get_zone: cloudflareGetZoneTool, cloudflare_create_zone: cloudflareCreateZoneTool, diff --git a/apps/sim/triggers/clerk/index.ts b/apps/sim/triggers/clerk/index.ts index 88eb2a8acd3..55f149730e8 100644 --- a/apps/sim/triggers/clerk/index.ts +++ b/apps/sim/triggers/clerk/index.ts @@ -1,6 +1,13 @@ export { clerkOrganizationCreatedTrigger } from './organization_created' +export { clerkOrganizationDeletedTrigger } from './organization_deleted' export { clerkOrganizationMembershipCreatedTrigger } from './organization_membership_created' +export { clerkOrganizationMembershipDeletedTrigger } from './organization_membership_deleted' +export { clerkOrganizationMembershipUpdatedTrigger } from './organization_membership_updated' +export { clerkOrganizationUpdatedTrigger } from './organization_updated' export { clerkSessionCreatedTrigger } from './session_created' +export { clerkSessionEndedTrigger } from './session_ended' +export { clerkSessionRemovedTrigger } from './session_removed' +export { clerkSessionRevokedTrigger } from './session_revoked' export { clerkUserCreatedTrigger } from './user_created' export { clerkUserDeletedTrigger } from './user_deleted' export { clerkUserUpdatedTrigger } from './user_updated' diff --git a/apps/sim/triggers/clerk/organization_deleted.ts b/apps/sim/triggers/clerk/organization_deleted.ts new file mode 100644 index 00000000000..3ce87d4442d --- /dev/null +++ b/apps/sim/triggers/clerk/organization_deleted.ts @@ -0,0 +1,38 @@ +import { ClerkIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildClerkExtraFields, + buildOrganizationDeletedOutputs, + clerkSetupInstructions, + clerkTriggerOptions, +} from '@/triggers/clerk/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Clerk Organization Deleted Trigger. + * Triggers when an organization is deleted. + */ +export const clerkOrganizationDeletedTrigger: TriggerConfig = { + id: 'clerk_organization_deleted', + name: 'Clerk Organization Deleted', + provider: 'clerk', + description: 'Trigger workflow when a Clerk organization is deleted', + version: '1.0.0', + icon: ClerkIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'clerk_organization_deleted', + triggerOptions: clerkTriggerOptions, + setupInstructions: clerkSetupInstructions('organization.deleted'), + extraFields: buildClerkExtraFields('clerk_organization_deleted'), + }), + + outputs: buildOrganizationDeletedOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/clerk/organization_membership_deleted.ts b/apps/sim/triggers/clerk/organization_membership_deleted.ts new file mode 100644 index 00000000000..9f8462edffc --- /dev/null +++ b/apps/sim/triggers/clerk/organization_membership_deleted.ts @@ -0,0 +1,38 @@ +import { ClerkIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildClerkExtraFields, + buildOrganizationMembershipDeletedOutputs, + clerkSetupInstructions, + clerkTriggerOptions, +} from '@/triggers/clerk/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Clerk Organization Membership Deleted Trigger. + * Triggers when a member is removed from an organization. + */ +export const clerkOrganizationMembershipDeletedTrigger: TriggerConfig = { + id: 'clerk_organization_membership_deleted', + name: 'Clerk Organization Membership Deleted', + provider: 'clerk', + description: 'Trigger workflow when a Clerk organization membership is deleted', + version: '1.0.0', + icon: ClerkIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'clerk_organization_membership_deleted', + triggerOptions: clerkTriggerOptions, + setupInstructions: clerkSetupInstructions('organizationMembership.deleted'), + extraFields: buildClerkExtraFields('clerk_organization_membership_deleted'), + }), + + outputs: buildOrganizationMembershipDeletedOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/clerk/organization_membership_updated.ts b/apps/sim/triggers/clerk/organization_membership_updated.ts new file mode 100644 index 00000000000..5e41e9e336c --- /dev/null +++ b/apps/sim/triggers/clerk/organization_membership_updated.ts @@ -0,0 +1,38 @@ +import { ClerkIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildClerkExtraFields, + buildOrganizationMembershipOutputs, + clerkSetupInstructions, + clerkTriggerOptions, +} from '@/triggers/clerk/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Clerk Organization Membership Updated Trigger. + * Triggers when a member's role within an organization changes. + */ +export const clerkOrganizationMembershipUpdatedTrigger: TriggerConfig = { + id: 'clerk_organization_membership_updated', + name: 'Clerk Organization Membership Updated', + provider: 'clerk', + description: 'Trigger workflow when a Clerk organization membership is updated', + version: '1.0.0', + icon: ClerkIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'clerk_organization_membership_updated', + triggerOptions: clerkTriggerOptions, + setupInstructions: clerkSetupInstructions('organizationMembership.updated'), + extraFields: buildClerkExtraFields('clerk_organization_membership_updated'), + }), + + outputs: buildOrganizationMembershipOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/clerk/organization_updated.ts b/apps/sim/triggers/clerk/organization_updated.ts new file mode 100644 index 00000000000..feda7b46f92 --- /dev/null +++ b/apps/sim/triggers/clerk/organization_updated.ts @@ -0,0 +1,38 @@ +import { ClerkIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildClerkExtraFields, + buildOrganizationOutputs, + clerkSetupInstructions, + clerkTriggerOptions, +} from '@/triggers/clerk/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Clerk Organization Updated Trigger. + * Triggers when an organization's details are updated. + */ +export const clerkOrganizationUpdatedTrigger: TriggerConfig = { + id: 'clerk_organization_updated', + name: 'Clerk Organization Updated', + provider: 'clerk', + description: 'Trigger workflow when a Clerk organization is updated', + version: '1.0.0', + icon: ClerkIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'clerk_organization_updated', + triggerOptions: clerkTriggerOptions, + setupInstructions: clerkSetupInstructions('organization.updated'), + extraFields: buildClerkExtraFields('clerk_organization_updated'), + }), + + outputs: buildOrganizationOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/clerk/session_ended.ts b/apps/sim/triggers/clerk/session_ended.ts new file mode 100644 index 00000000000..faa59ddd245 --- /dev/null +++ b/apps/sim/triggers/clerk/session_ended.ts @@ -0,0 +1,38 @@ +import { ClerkIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildClerkExtraFields, + buildSessionOutputs, + clerkSetupInstructions, + clerkTriggerOptions, +} from '@/triggers/clerk/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Clerk Session Ended Trigger. + * Triggers when a user signs out and the session ends. + */ +export const clerkSessionEndedTrigger: TriggerConfig = { + id: 'clerk_session_ended', + name: 'Clerk Session Ended', + provider: 'clerk', + description: 'Trigger workflow when a Clerk session ends', + version: '1.0.0', + icon: ClerkIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'clerk_session_ended', + triggerOptions: clerkTriggerOptions, + setupInstructions: clerkSetupInstructions('session.ended'), + extraFields: buildClerkExtraFields('clerk_session_ended'), + }), + + outputs: buildSessionOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/clerk/session_removed.ts b/apps/sim/triggers/clerk/session_removed.ts new file mode 100644 index 00000000000..c19c8fad1e2 --- /dev/null +++ b/apps/sim/triggers/clerk/session_removed.ts @@ -0,0 +1,38 @@ +import { ClerkIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildClerkExtraFields, + buildSessionOutputs, + clerkSetupInstructions, + clerkTriggerOptions, +} from '@/triggers/clerk/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Clerk Session Removed Trigger. + * Triggers when a session is removed, e.g. because the associated user was deleted. + */ +export const clerkSessionRemovedTrigger: TriggerConfig = { + id: 'clerk_session_removed', + name: 'Clerk Session Removed', + provider: 'clerk', + description: 'Trigger workflow when a Clerk session is removed', + version: '1.0.0', + icon: ClerkIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'clerk_session_removed', + triggerOptions: clerkTriggerOptions, + setupInstructions: clerkSetupInstructions('session.removed'), + extraFields: buildClerkExtraFields('clerk_session_removed'), + }), + + outputs: buildSessionOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/clerk/session_revoked.ts b/apps/sim/triggers/clerk/session_revoked.ts new file mode 100644 index 00000000000..0f5d1b661ab --- /dev/null +++ b/apps/sim/triggers/clerk/session_revoked.ts @@ -0,0 +1,38 @@ +import { ClerkIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildClerkExtraFields, + buildSessionOutputs, + clerkSetupInstructions, + clerkTriggerOptions, +} from '@/triggers/clerk/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Clerk Session Revoked Trigger. + * Triggers when a session is revoked, e.g. via the Revoke Session API or Dashboard. + */ +export const clerkSessionRevokedTrigger: TriggerConfig = { + id: 'clerk_session_revoked', + name: 'Clerk Session Revoked', + provider: 'clerk', + description: 'Trigger workflow when a Clerk session is revoked', + version: '1.0.0', + icon: ClerkIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'clerk_session_revoked', + triggerOptions: clerkTriggerOptions, + setupInstructions: clerkSetupInstructions('session.revoked'), + extraFields: buildClerkExtraFields('clerk_session_revoked'), + }), + + outputs: buildSessionOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/clerk/utils.ts b/apps/sim/triggers/clerk/utils.ts index e1a4f733288..38772722027 100644 --- a/apps/sim/triggers/clerk/utils.ts +++ b/apps/sim/triggers/clerk/utils.ts @@ -12,8 +12,15 @@ export const CLERK_TRIGGER_TO_EVENT_TYPE: Record = { clerk_user_updated: 'user.updated', clerk_user_deleted: 'user.deleted', clerk_session_created: 'session.created', + clerk_session_ended: 'session.ended', + clerk_session_removed: 'session.removed', + clerk_session_revoked: 'session.revoked', clerk_organization_created: 'organization.created', + clerk_organization_updated: 'organization.updated', + clerk_organization_deleted: 'organization.deleted', clerk_organization_membership_created: 'organizationMembership.created', + clerk_organization_membership_updated: 'organizationMembership.updated', + clerk_organization_membership_deleted: 'organizationMembership.deleted', } /** @@ -24,8 +31,15 @@ export const clerkTriggerOptions = [ { label: 'User Updated', id: 'clerk_user_updated' }, { label: 'User Deleted', id: 'clerk_user_deleted' }, { label: 'Session Created', id: 'clerk_session_created' }, + { label: 'Session Ended', id: 'clerk_session_ended' }, + { label: 'Session Removed', id: 'clerk_session_removed' }, + { label: 'Session Revoked', id: 'clerk_session_revoked' }, { label: 'Organization Created', id: 'clerk_organization_created' }, + { label: 'Organization Updated', id: 'clerk_organization_updated' }, + { label: 'Organization Deleted', id: 'clerk_organization_deleted' }, { label: 'Organization Membership Created', id: 'clerk_organization_membership_created' }, + { label: 'Organization Membership Updated', id: 'clerk_organization_membership_updated' }, + { label: 'Organization Membership Deleted', id: 'clerk_organization_membership_deleted' }, { label: 'Generic Webhook (All Events)', id: 'clerk_webhook' }, ] @@ -159,7 +173,7 @@ export function buildOrganizationOutputs(): Record { } /** - * Build outputs for `organizationMembership.created` events. + * Build outputs for `organizationMembership.created` and `.updated` events. * The `data` object is the Clerk OrganizationMembership object. */ export function buildOrganizationMembershipOutputs(): Record { @@ -179,6 +193,33 @@ export function buildOrganizationMembershipOutputs(): Record { + return { + ...commonEventOutputs, + organizationId: { type: 'string', description: 'Deleted Clerk organization ID (data.id)' }, + deleted: { + type: 'boolean', + description: 'Whether the organization was deleted (data.deleted)', + }, + } +} + +/** + * Build outputs for `organizationMembership.deleted` events. + * The `data` object is a deleted-object marker: `{ id, deleted, object }`. + */ +export function buildOrganizationMembershipDeletedOutputs(): Record { + return { + ...commonEventOutputs, + membershipId: { type: 'string', description: 'Deleted membership ID (data.id)' }, + deleted: { type: 'boolean', description: 'Whether the membership was deleted (data.deleted)' }, + } +} + /** * Build outputs for the generic webhook (all events). * Only the fields common to every Clerk event are guaranteed; use `data` diff --git a/apps/sim/triggers/registry.ts b/apps/sim/triggers/registry.ts index c13704ed89f..67416d77974 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -60,8 +60,15 @@ import { } from '@/triggers/circleback' import { clerkOrganizationCreatedTrigger, + clerkOrganizationDeletedTrigger, clerkOrganizationMembershipCreatedTrigger, + clerkOrganizationMembershipDeletedTrigger, + clerkOrganizationMembershipUpdatedTrigger, + clerkOrganizationUpdatedTrigger, clerkSessionCreatedTrigger, + clerkSessionEndedTrigger, + clerkSessionRemovedTrigger, + clerkSessionRevokedTrigger, clerkUserCreatedTrigger, clerkUserDeletedTrigger, clerkUserUpdatedTrigger, @@ -724,8 +731,15 @@ export const TRIGGER_REGISTRY: TriggerRegistry = { clerk_user_updated: clerkUserUpdatedTrigger, clerk_user_deleted: clerkUserDeletedTrigger, clerk_session_created: clerkSessionCreatedTrigger, + clerk_session_ended: clerkSessionEndedTrigger, + clerk_session_removed: clerkSessionRemovedTrigger, + clerk_session_revoked: clerkSessionRevokedTrigger, clerk_organization_created: clerkOrganizationCreatedTrigger, + clerk_organization_updated: clerkOrganizationUpdatedTrigger, + clerk_organization_deleted: clerkOrganizationDeletedTrigger, clerk_organization_membership_created: clerkOrganizationMembershipCreatedTrigger, + clerk_organization_membership_updated: clerkOrganizationMembershipUpdatedTrigger, + clerk_organization_membership_deleted: clerkOrganizationMembershipDeletedTrigger, clerk_webhook: clerkWebhookTrigger, incidentio_incident_created: incidentioIncidentCreatedTrigger, incidentio_incident_updated: incidentioIncidentUpdatedTrigger,