import { Input } from "@/src/components/ui/input"; import { Button } from "@/src/components/ui/button"; import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/src/components/ui/form"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/src/components/ui/select"; import { X, Plus, RefreshCw, Lock, LockOpen } from "lucide-react"; import { useFieldArray, type UseFormReturn } from "react-hook-form"; import { z } from "zod/v4"; import { type ActionDomain, type ActionDomainWithSecrets, AvailableWebhookApiSchema, type SafeWebhookActionConfig, WebhookDefaultHeaders, } from "@langfuse/shared"; import { api } from "@/src/utils/api"; import { useState } from "react"; import { Dialog, DialogBody, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/src/components/ui/dialog"; import { Popover, PopoverContent, PopoverTrigger, } from "@/src/components/ui/popover"; import { WebhookSecretRender } from "../WebhookSecretRender"; import { CodeView } from "@/src/components/ui/CodeJsonViewer"; import { showSuccessToast } from "@/src/features/notifications/showSuccessToast"; export const webhookSchema = z.object({ url: z.url(), headers: z.array( z.object({ name: z.string().refine( (name) => { if (!name.trim()) return true; // Allow empty names (will be filtered out) const defaultHeaderKeys = Object.keys(WebhookDefaultHeaders); return !defaultHeaderKeys.includes(name.trim().toLowerCase()); }, { message: "This header is automatically added by Langfuse and cannot be customized", }, ), value: z.string(), displayValue: z.string(), isSecret: z.boolean(), wasSecret: z.boolean(), }), ), apiVersion: AvailableWebhookApiSchema, }); export type WebhookFormValues = z.infer; interface WebhookActionFormProps { form: UseFormReturn; disabled: boolean; projectId: string; action?: ActionDomain | ActionDomainWithSecrets; } export const WebhookActionForm: React.FC = ({ form, disabled, projectId, action, }) => { const { fields: headerFields, append: appendHeader, remove: removeHeader, } = useFieldArray({ control: form.control, name: "webhook.headers", }); // Get default header keys to filter them out const defaultHeaderKeys = Object.keys(WebhookDefaultHeaders); // Filter out default headers from the user-editable headers const customHeaderFields = headerFields.filter((field, index) => { const headerName = form.watch(`webhook.headers.${index}.name`); return !defaultHeaderKeys.includes(headerName?.toLowerCase()); }); // Function to add a new header pair const addHeader = () => { appendHeader({ name: "", value: "", displayValue: "", isSecret: false, wasSecret: false, }); }; // Function to toggle secret status of a header const toggleHeaderSecret = (index: number) => { const currentValue = form.watch(`webhook.headers.${index}.isSecret`); form.setValue(`webhook.headers.${index}.isSecret`, !currentValue); }; return (
( Webhook URL * The HTTP URL to call when the trigger fires. We will send a POST request to this URL. Only HTTPS URLs are allowed for security. )} /> ( API Version The API version to use for the webhook payload format when prompt events are triggered. )} />
Headers {/* Default Headers Section */}
Default headers (automatically added by Langfuse): {Object.entries({ ...WebhookDefaultHeaders, "x-langfuse-signature": `t=,v1=`, }).map(([key, value]) => (
))}
{/* Custom Headers Section */} Optional custom headers to include in the webhook request: {customHeaderFields.map((field) => { // Find the original index in the headerFields array const originalIndex = headerFields.findIndex( (f) => f.id === field.id, ); const isSecret = form.watch( `webhook.headers.${originalIndex}.isSecret`, ); const displayValue = form.watch( `webhook.headers.${originalIndex}.displayValue`, ); return (
( )} /> ( )} />
); })}
{/* Webhook Secret Section */}
Webhook Secret Use this secret to verify webhook signatures for security. The secret is automatically included in the x-langfuse-signature header. {action?.id ? (
Secret is encrypted and can only be viewed when generated or regenerated
) : (
Webhook secret will be generated when the automation is created.
)}
); }; export const RegenerateWebhookSecretButton = ({ projectId, action, }: { projectId: string; action: ActionDomain | ActionDomainWithSecrets; }) => { const [showConfirmPopover, setShowConfirmPopover] = useState(false); const [showRegenerateDialog, setShowRegenerateDialog] = useState(false); const [regeneratedSecret, setRegeneratedSecret] = useState( null, ); const utils = api.useUtils(); const regenerateSecretMutation = api.automations.regenerateWebhookSecret.useMutation({ onSuccess: (data) => { showSuccessToast({ title: "Webhook Secret Regenerated", description: "Your webhook secret has been successfully regenerated.", }); setRegeneratedSecret(data.webhookSecret); setShowRegenerateDialog(true); utils.automations.invalidate(); }, }); // Function to regenerate webhook secret const handleRegenerateSecret = async () => { if (!action?.id) return; try { await regenerateSecretMutation.mutateAsync({ projectId, actionId: action.id, }); setShowConfirmPopover(false); } catch (error) { console.error("Failed to regenerate webhook secret:", error); } }; return ( <>

Please confirm

This action will invalidate the current webhook secret and generate a new one. Any existing integrations using the old secret will stop working until updated.

{/* Regenerate Secret Dialog */} Webhook Secret Regenerated Your webhook secret has been regenerated. Please copy the new secret below - it will only be shown once. {regeneratedSecret && ( )} ); }; // Function to convert the array of header objects to a Record for API export const formatWebhookHeaders = ( headers: { name: string; value: string; displayValue: string; isSecret: boolean; wasSecret: boolean; }[], ): Record => { const requestHeaders: Record = {}; const defaultHeaderKeys = Object.keys(WebhookDefaultHeaders); headers.forEach((header) => { if (header.name.trim()) { // Exclude default headers - they will be added automatically by the API if (!defaultHeaderKeys.includes(header.name.trim().toLowerCase())) { requestHeaders[header.name.trim()] = { secret: header.isSecret || false, value: header.value.trim(), }; } } }); return requestHeaders; };