"use client"; /** * EventExplorer (TASK-0130) — the developer-hub "Events" tab. * * Driven ENTIRELY by `WEBHOOK_EVENT_CATALOG` in @factory/shared (the single * source of truth that also drives dispatcher matching, API validation and * the completeness test) — never a hand-kept list. Each event renders its * description, a rendered payload schema (zod → display) and a copyable * example payload. * * A11y: each event is a labelled group with a copy button that announces via * an aria-live region. Mobile: rows stack; the schema/example toggle is a * per-row expand. */ import { useState } from "react"; import { z } from "zod"; import { Check, ChevronDown, ChevronRight, Copy } from "lucide-react"; // Browser-safe subpath (the root barrel re-exports node:crypto — the // Developers page 500'd bundling the barrel; catalog.ts is zod-only). import { WEBHOOK_EVENT_CATALOG, type WebhookEventCatalogEntry, } from "@factory/shared/events/catalog"; import { cn } from "@/lib/utils"; /** A rendered schema line: `name: type (optional)`. */ function zodToLines(schema: z.ZodTypeAny, depth = 0): { name: string; type: string; optional: boolean }[] { const out: { name: string; type: string; optional: boolean }[] = []; const push = (name: string, type: string, optional: boolean) => out.push({ name: depth === 0 ? name : `${name}`, type, optional }); let s = schema as z.ZodTypeAny; const isOptional = (t: z.ZodTypeAny): boolean => t instanceof z.ZodOptional || (typeof t._def !== "undefined" && (t as unknown as { _def?: { typeName?: string } })._def?.typeName === "ZodOptional"); if (isOptional(s)) { const inner = (s as z.ZodOptional)._def.innerType; return zodToLines(inner, depth).map((l) => ({ ...l, optional: true })); } if (s instanceof z.ZodObject) { const entries = Object.entries(s.shape as Record); for (const [key, value] of entries) { if (value instanceof z.ZodObject) { // Inline nested objects as `key: object { … }` (one level) so the // explorer stays scannable. const nested = Object.entries(value.shape as Record) .map(([k, v]) => `${k}: ${typeOf(v)}`) .join(", "); push(key, `object { ${nested} }`, isOptional(value)); } else { push(key, typeOf(value), isOptional(value)); } } } else { push("", typeOf(s), false); } return out; } function typeOf(t: z.ZodTypeAny): string { if (t instanceof z.ZodString) return "string"; if (t instanceof z.ZodNumber) return "number"; if (t instanceof z.ZodBoolean) return "boolean"; if (t instanceof z.ZodArray) return `${typeOf(t.element)}[]`; if (t instanceof z.ZodNullable) return `${typeOf(t._def.innerType)} | null`; if (t instanceof z.ZodOptional) return `${typeOf(t._def.innerType)}?`; if (t instanceof z.ZodEnum) return t.options.join(" | "); if (t instanceof z.ZodUnion) return (t.options as z.ZodTypeAny[]).map((o) => typeOf(o)).join(" | "); if (t instanceof z.ZodLiteral) return JSON.stringify(t._def.value); if (t instanceof z.ZodRecord) return "object"; if (t instanceof z.ZodAny || t instanceof z.ZodUnknown) return "any"; return "object"; } async function copyText(text: string): Promise { try { await navigator.clipboard.writeText(text); return true; } catch { try { const ta = document.createElement("textarea"); ta.value = text; ta.setAttribute("readonly", ""); ta.style.position = "fixed"; ta.style.opacity = "0"; document.body.appendChild(ta); ta.select(); const ok = document.execCommand("copy"); document.body.removeChild(ta); return ok; } catch { return false; } } } function EventRow({ entry }: { entry: WebhookEventCatalogEntry }) { const [open, setOpen] = useState(false); const [copied, setCopied] = useState(false); const lines = zodToLines(entry.payloadSchema); const doCopy = async () => { const ok = await copyText(JSON.stringify(entry.example, null, 2)); setCopied(ok); setTimeout(() => setCopied(false), 2000); }; return (
  • {open ? (

    Payload schema (data)

    {lines.map((l, i) => ( ))}
    Payload fields for {entry.name}
    {l.name} {l.type} {l.optional ? "optional" : ""}

    Example

    {copied ? `${entry.name} example copied` : ""}
                  {JSON.stringify(entry.example, null, 2)}
                

    Fired by: {entry.firedBy}

    ) : null}
  • ); } export function EventExplorer() { // Group by the leading segment (page.*, record.*, …) for scannability. const groups = new Map(); for (const e of WEBHOOK_EVENT_CATALOG) { const g = e.name.split(".")[0] ?? "other"; const arr = groups.get(g) ?? []; arr.push(e); groups.set(g, arr); } return (
    {Array.from(groups.entries()).map(([group, events]) => (

    {group}

      {events.map((e) => ( ))}
    ))}

    Subscriptions accept exact event names or the * wildcard. Payloads are JSON, signed with{" "} X-Factory-Signature: t=…,v1=HMAC-SHA256(secret, "<t>.<body>").

    ); }