diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e7626a2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "json.schemaValidation.enable": true, + "json.schemas": [ + { + "fileMatch": ["package.json"], + "url": "https://json.schemastore.org/package" + } + ], + "json.schemaDownload.enable": true +} diff --git a/app/docs/page.tsx b/app/docs/page.tsx new file mode 100644 index 0000000..a18a77e --- /dev/null +++ b/app/docs/page.tsx @@ -0,0 +1,228 @@ +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Documentation | Tern - Webhook Verification', + description: 'Comprehensive documentation for Tern - Algorithm Agnostic Webhook Verification', +}; + +const DocsPage = () => { + return ( +
+
+
+

Tern Documentation

+ +
+
+

Getting Started

+
+
+

Installation

+
+
+                      npm install @Hookflo/tern
+                    
+
+

+ Or using yarn: +

+
+
+                      yarn add @Hookflo/tern
+                    
+
+
+ +
+

Basic Usage

+
+
+                      {
+`import { verifyWebhook, signWebhook } from '@Hookflo/tern';
+
+// Verify a webhook
+const isValid = verifyWebhook({
+  payload: request.body,
+  signature: request.headers['x-webhook-signature'],
+  secret: process.env.WEBHOOK_SECRET,
+  algorithm: 'sha256' // or 'sha1', 'sha512'
+});
+
+// Generate a signature
+const signature = signWebhook({
+  payload: { event: 'user.created', id: '123' },
+  secret: 'your-secret-key',
+  algorithm: 'sha256'
+});`}
+                      
+                    
+
+
+
+
+ +
+

API Reference

+ +
+
+

verifyWebhook

+

Verifies a webhook signature.

+ +

Parameters

+
    +
  • + payload + - + The webhook payload (object or string) +
  • +
  • + signature + - + The signature from the webhook header +
  • +
  • + secret + - + Your webhook secret key +
  • +
  • + algorithm + - + Hashing algorithm ('sha1', 'sha256', or 'sha512') +
  • +
+ +

Returns

+
+ boolean + Whether the signature is valid +
+
+ +
+

signWebhook

+

Generates a signature for a webhook payload.

+ +

Parameters

+
    +
  • + payload + - + The payload to sign (object or string) +
  • +
  • + secret + - + Your webhook secret key +
  • +
  • + algorithm + - + Hashing algorithm ('sha1', 'sha256', or 'sha512') +
  • +
+ +

Returns

+
+ string + The generated signature +
+
+
+
+ +
+

Examples

+ +
+
+

Express.js Middleware

+
+
+                      {
+`import express from 'express';
+import { verifyWebhook } from '@Hookflo/tern';
+
+const app = express();
+app.use(express.json());
+
+app.post('/webhook', (req, res) => {
+  const signature = req.headers['x-webhook-signature'];
+  
+  if (!signature) {
+    return res.status(401).json({ error: 'No signature provided' });
+  }
+
+  const isValid = verifyWebhook({
+    payload: req.body,
+    signature,
+    secret: process.env.WEBHOOK_SECRET,
+    algorithm: 'sha256'
+  });
+
+  if (!isValid) {
+    return res.status(401).json({ error: 'Invalid signature' });
+  }
+
+  // Process the webhook
+  console.log('Received webhook:', req.body);
+  res.status(200).json({ received: true });
+});
+
+app.listen(3000, () => {
+  console.log('Server is running on port 3000');
+});`}
+                      
+                    
+
+
+ +
+

Next.js API Route

+
+
+                      {
+`import { NextApiRequest, NextApiResponse } from 'next';
+import { verifyWebhook } from '@Hookflo/tern';
+
+export default function handler(req: NextApiRequest, res: NextApiResponse) {
+  if (req.method !== 'POST') {
+    return res.status(405).json({ error: 'Method not allowed' });
+  }
+
+  const signature = req.headers['x-webhook-signature'] as string;
+  
+  if (!signature) {
+    return res.status(401).json({ error: 'No signature provided' });
+  }
+
+  const isValid = verifyWebhook({
+    payload: req.body,
+    signature,
+    secret: process.env.WEBHOOK_SECRET!,
+    algorithm: 'sha256'
+  });
+
+  if (!isValid) {
+    return res.status(401).json({ error: 'Invalid signature' });
+  }
+
+  // Process the webhook
+  console.log('Received webhook:', req.body);
+  res.status(200).json({ received: true });
+}`}
+                      
+                    
+
+
+
+
+
+
+
+
+ ); +}; + +export default DocsPage; diff --git a/app/fonts.css b/app/fonts.css new file mode 100644 index 0000000..6caaab2 --- /dev/null +++ b/app/fonts.css @@ -0,0 +1,21 @@ +/* Font faces */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100 900; + font-display: optional; + src: url('/_next/static/media/e4af272ccee01ff0-s.p.woff2') format('woff2'); +} + +@font-face { + font-family: 'JetBrains Mono'; + font-style: normal; + font-weight: 400 700; + font-display: optional; + src: url('/_next/static/media/bb3ef058b751a6ad-s.p.woff2') format('woff2'); +} + +:root { + --font-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; + --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; +} diff --git a/app/globals.css b/app/globals.css index 08f3578..039add8 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,127 +1,277 @@ -@import "tailwindcss"; -@import "tw-animate-css"; - -@custom-variant dark (&:is(.dark *)); - -:root { - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --destructive-foreground: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --radius: 0.625rem; - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); -} - -.dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.145 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.145 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.985 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.396 0.141 25.723); - --destructive-foreground: oklch(0.637 0.237 25.331); - --border: oklch(0.269 0 0); - --input: oklch(0.269 0 0); - --ring: oklch(0.439 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(0.269 0 0); - --sidebar-ring: oklch(0.439 0 0); -} - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-destructive-foreground: var(--destructive-foreground); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); - - /* Added custom font variables for Inter and JetBrains Mono */ - --font-sans: var(--font-inter); - --font-mono: var(--font-jetbrains-mono); +@import "tailwindcss/preflight"; +@import "tailwindcss/utilities"; + +@layer base { + :root { + /* Light theme */ + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 221.2 83.2% 53.3%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 221.2 83.2% 53.3%; + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 217.2 91.2% 59.8%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 224.3 76.3% 48%; + } } @layer base { * { - @apply border-border outline-ring/50; + @apply border-border; } body { - @apply bg-background text-foreground font-sans; + @apply bg-background text-foreground; + font-feature-settings: "rlig" 1, "calt" 1; } } + +/* Custom scrollbar */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background-color: hsl(var(--muted)); +} + +::-webkit-scrollbar-thumb { + background-color: hsl(var(--muted-foreground) / 0.3); + border-radius: 9999px; +} + +::-webkit-scrollbar-thumb:hover { + background-color: hsl(var(--muted-foreground) / 0.5); +} + +/* Custom selection */ +::selection { + background-color: hsl(var(--primary) / 0.2); + color: hsl(var(--primary-foreground)); +} + +/* Smooth scrolling */ +html { + scroll-behavior: smooth; +} + +/* Custom focus styles */ +*:focus-visible { + outline: none; + box-shadow: 0 0 0 2px hsl(var(--ring)), 0 0 0 4px hsl(var(--ring) / 0.3); + border-radius: 0.125rem; +} + +/* Remove default focus styles for mouse users */ +*:focus:not(:focus-visible) { + outline: none; +} + +/* Custom animation for code blocks */ +@keyframes highlight { + from { background-color: hsl(var(--primary) / 0.1); } + to { background-color: transparent; } +} + +.highlight-animate { + animation: highlight 1.5s ease-in-out; +} + +/* Custom utility classes */ +.text-balance { + text-wrap: balance; +} + +/* Custom typography */ +.prose { + max-width: none; +} + +.prose :where(code):not(:where([class~="not-prose"] *)) { + background-color: hsl(var(--muted)); + padding: 0.25rem 0.375rem; + border-radius: 0.25rem; + font-size: 0.9em; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; +} + +.prose :where(pre):not(:where([class~="not-prose"] *)) { + background-color: hsl(var(--muted)); + padding: 1rem; + border-radius: 0.5rem; + overflow-x: auto; +} + +.prose :where(pre code):not(:where([class~="not-prose"] *)) { + background-color: transparent; + padding: 0; + font-size: 0.875rem; +} + +/* Custom components */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 0.375rem; + font-size: 0.875rem; + font-weight: 500; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.btn:focus-visible { + outline: none; + box-shadow: 0 0 0 2px hsl(var(--ring)), 0 0 0 4px hsl(var(--ring) / 0.3); +} + +.btn:disabled { + opacity: 0.5; + pointer-events: none; +} + +.btn-primary { + background-color: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); +} + +.btn-primary:hover { + background-color: hsl(var(--primary) / 0.9); +} + +.btn-secondary { + background-color: hsl(var(--secondary)); + color: hsl(var(--secondary-foreground)); +} + +.btn-secondary:hover { + background-color: hsl(var(--secondary) / 0.8); +} + +.btn-outline { + border: 1px solid hsl(var(--input)); +} + +.btn-outline:hover { + background-color: hsl(var(--accent)); + color: hsl(var(--accent-foreground)); +} + +.btn-ghost:hover { + background-color: hsl(var(--accent)); + color: hsl(var(--accent-foreground)); +} + +.btn-link { + text-underline-offset: 4px; + color: hsl(var(--primary)); +} + +.btn-link:hover { + text-decoration: underline; +} + +/* Custom form styles */ +.input { + display: flex; + height: 2.5rem; + width: 100%; + border-radius: 0.375rem; + border: 1px solid hsl(var(--input)); + background-color: hsl(var(--background)); + padding: 0.5rem 0.75rem; + font-size: 0.875rem; + line-height: 1.25rem; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.input:focus { + outline: none; + border-color: hsl(var(--ring)); + box-shadow: 0 0 0 1px hsl(var(--ring)); +} + +.input::placeholder { + color: hsl(var(--muted-foreground)); + opacity: 1; +} + +.input:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Custom card styles */ +.card { + border-radius: 0.5rem; + border: 1px solid hsl(var(--border)); + background-color: hsl(var(--card)); + color: hsl(var(--card-foreground)); + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); +} + +.card-header { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 1.5rem; +} + +.card-title { + font-size: 1.5rem; + line-height: 2rem; + font-weight: 600; + letter-spacing: -0.025em; + line-height: 1; +} + +.card-description { + font-size: 0.875rem; + line-height: 1.25rem; + color: hsl(var(--muted-foreground)); +} + +.card-content { + padding: 0 1.5rem 1.5rem 1.5rem; +} + +.card-footer { + display: flex; + align-items: center; + padding: 0 1.5rem 1.5rem 1.5rem; +} diff --git a/app/layout.tsx b/app/layout.tsx index 4a87813..7e9b7b5 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,35 +1,90 @@ -import type React from "react" -import type { Metadata } from "next" -import { Inter, JetBrains_Mono } from "next/font/google" -import "./globals.css" +import type { Metadata, Viewport } from 'next'; +import { Inter, JetBrains_Mono } from 'next/font/google'; +import { ThemeProvider } from '@/components/providers/theme-provider'; +import './globals.css'; +// Load fonts with the new Next.js font system const inter = Inter({ - subsets: ["latin"], - display: "swap", - variable: "--font-inter", -}) + subsets: ['latin'], + display: 'swap', + variable: '--font-sans', + preload: true, + adjustFontFallback: true, +}); const jetbrainsMono = JetBrains_Mono({ - subsets: ["latin"], - display: "swap", - variable: "--font-jetbrains-mono", -}) + subsets: ['latin'], + display: 'swap', + variable: '--font-mono', + preload: true, + adjustFontFallback: true, +}); + +// Viewport configuration +export const viewport: Viewport = { + width: 'device-width', + initialScale: 1, + maximumScale: 5, + userScalable: true, + themeColor: [ + { media: '(prefers-color-scheme: light)', color: 'white' }, + { media: '(prefers-color-scheme: dark)', color: 'black' }, + ], + viewportFit: 'cover', +}; export const metadata: Metadata = { - title: "Tern - Algorithm Agnostic Webhook Verification", - description: - "A robust, scalable webhook verification framework supporting multiple platforms and signature algorithms.", - generator: 'v0.app' -} + title: 'Tern - Algorithm Agnostic Webhook Verification', + description: 'A robust, scalable webhook verification framework supporting multiple platforms and signature algorithms.', + keywords: ['webhook', 'verification', 'security', 'api', 'authentication', 'signing'], + authors: [{ name: 'Tern Team' }], + creator: 'Tern', + openGraph: { + type: 'website', + locale: 'en_US', + url: 'https://tern.dev', + title: 'Tern - Algorithm Agnostic Webhook Verification', + description: 'A robust, scalable webhook verification framework supporting multiple platforms and signature algorithms.', + siteName: 'Tern', + }, + twitter: { + card: 'summary_large_image', + title: 'Tern - Algorithm Agnostic Webhook Verification', + description: 'A robust, scalable webhook verification framework supporting multiple platforms and signature algorithms.', + creator: '@tern', + }, + icons: { + icon: [ + { url: '/favicon.ico' }, + { url: '/favicon-16x16.png', sizes: '16x16', type: 'image/png' }, + { url: '/favicon-32x32.png', sizes: '32x32', type: 'image/png' }, + ], + shortcut: ['favicon.ico'], + apple: [ + { url: 'apple-touch-icon.png' }, + ], + }, + manifest: 'site.webmanifest', + generator: 'Next.js', +}; export default function RootLayout({ children, }: Readonly<{ - children: React.ReactNode + children: React.ReactNode; }>) { return ( - - {children} + + + + {children} + + - ) + ); } diff --git a/app/page.tsx b/app/page.tsx index dad4627..ab83ed2 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,332 +1,259 @@ -import { Button } from "@/components/ui/button" -import { Github, Shield, Zap, Code, Settings, Package, Star, GitBranch, Cpu } from "lucide-react" +'use client'; -export default function HomePage() { +import React from 'react'; +import Link from 'next/link'; +import { Button } from "@/components/ui/button"; +import { Github, Zap, Code, Package, Star, Settings, Shield } from "lucide-react"; + +const features = [ + { + icon: , + title: "Multiple Algorithms", + description: "Supports HMAC-SHA1, HMAC-SHA256, and custom verification methods." + }, + { + icon: , + title: "Easy Integration", + description: "Simple API that works with any web framework or platform." + }, + { + icon: , + title: "Zero Dependencies", + description: "Lightweight with no external dependencies for minimal bundle size." + } +]; + +const HomePage = () => { return ( -
+
{/* Header Banner */} -
+

- Star us on GitHub - {" "} - for the latest updates on Tern - Self-Healing Autonomous Webhook Verification + {' '} + for the latest updates on Tern - Algorithm Agnostic Webhook Verification

{/* Navigation */} -
-
-
- {/* Logo */} -
- - Tern +
+
+
+
+ + Tern
- - {/* Navigation Links */} -
- {/* Hero Section */} -
-
-
- - - Self-Healing • Autonomous • Algorithm-Agnostic - -
- -

- Verify Less, -
- Build More -

- -

- The next-generation webhook verification framework that adapts, heals, and scales automatically. - Zero maintenance, infinite possibilities. -

- - {/* CTA Buttons */} -
- - -
- - {/* Loading Animation */} -
-
-
-
-
-
-
- - {/* Code Demo */} -
-
-
-
-
-
-
-
webhook-verification.ts
+
+ {/* Hero Section */} +
+
+
+ + Algorithm Agnostic Webhook Verification
-
-
import {"{ WebhookVerificationService }"} from '@hookflo/tern';
-
- // Self-healing verification - adapts to platform changes automatically -
-
- const result = await{" "} - WebhookVerificationService -
-
- .verifyWithPlatformConfig( -
-
- request, 'stripe',{" "} - 'whsec_your_secret' -
-
);
-
- if (result.isValid){" "} - {"{"} -
-
- console.log( - 'Webhook verified autonomously!', result. - payload); -
-
{"}"}
-
-
-
- - {/* Features Section */} -
-
-

Why Tern Leads the Future

-

- Most webhook verifiers break when platforms change. Tern evolves with them, - automatically healing and adapting to keep your - systems running. +

+ Secure Webhook Verification, Simplified +

+

+ Tern provides robust, algorithm-agnostic webhook verification for modern applications. + Support multiple platforms and signature algorithms out of the box.

-
- -
-
-
- -
-

Self-Healing Architecture

-

- Automatically detects and adapts to platform signature changes. No more broken webhooks when providers - update their systems. -

-
- -
-
- + + {/* NPM Installation */} +
+
+
+ + npm install @Hookflo/tern +
+
-

Algorithm Agnostic

-

- Supports HMAC-SHA256, HMAC-SHA1, HMAC-SHA512, and custom algorithms. One framework for all signature - types. -

-
-
- -
-

Platform Specific

-

- Battle-tested implementations for Stripe, GitHub, Supabase, Clerk, and 20+ major platforms out of the - box. -

-
- -
-
- -
-

Type Safe

-

- Full TypeScript support with comprehensive type definitions. Catch errors at compile time, not runtime. -

+
+ +
+
+
-
-
- -
-

Framework Agnostic

-

- Works seamlessly with Express.js, Next.js, Cloudflare Workers, Deno, and any JavaScript runtime. + {/* Features Section */} +

+
+
+

Why Choose Tern?

+

+ Built with developers in mind, Tern simplifies webhook verification so you can focus on building great products.

- -
-
- -
-

Zero Dependencies

-

- Lightweight TypeScript framework with no external dependencies. Keep your bundle size minimal and - secure. -

+
+ {features.map((feature, index) => ( +
+
+ {feature.icon} +
+

{feature.title}

+

{feature.description}

+
+ ))}
+
- {/* Supported Platforms */} -
-
-

Trusted by Major Platforms

-

- Battle-tested with the world's leading webhook providers + {/* Interactive Component Showcase */} +

+
+
+

Interactive Component Playground

+

+ Explore Tern's components in action. Edit the code and see changes in real-time.

-
- {[ - { name: "Stripe", icon: "S" }, - { name: "GitHub", icon: "G" }, - { name: "Supabase", icon: "S" }, - { name: "Clerk", icon: "C" }, - { name: "Shopify", icon: "S" }, - { name: "Vercel", icon: "V" }, - ].map((platform) => ( -
-
-
{platform.icon}
+
+ {/* Component 1: Webhook Verification */} +
+

Webhook Verification

+
+
+
+                    {
+`// Verify a webhook signature
+const isValid = verifyWebhook({
+  payload: req.body,
+  signature: req.headers['x-webhook-signature'],
+  secret: process.env.WEBHOOK_SECRET,
+  algorithm: 'sha256' // or 'sha1', 'sha512'
+});`}
+                    
+                  
+
+
+

Try it out:

+
+
+ +