diff --git a/AGENTS.md b/AGENTS.md index 3202ab2..9c66716 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -147,6 +147,17 @@ metadata: - Keep SKILL.md under 500 lines / < 5,000 tokens - Put detailed reference material in `references/` files +- **Inline code in `SKILL.md` is for the verification core only** — keep it short (typically under 25 lines) and limit it to the algorithm/header/comparison essentials an agent needs to answer "how do I verify X?" without loading another file. The full Express/Next.js/FastAPI handler (route wiring, event dispatch, error responses) lives in `examples/`. Point to the example by path rather than duplicating the handler: + ```markdown + ## Verification (core) + + ```javascript + // ~10–20 lines: HMAC compute + timing-safe compare + ``` + + > **For complete handlers with tests**, see [examples/express/](examples/express/), [examples/nextjs/](examples/nextjs/), [examples/fastapi/](examples/fastapi/). + ``` + This keeps `SKILL.md` focused, lets agents copy the canonical verification fast, and avoids drift between `SKILL.md` and the runnable (CI-tested) examples. - **Links within the same skill:** Use relative paths (e.g. `references/verification.md`, `examples/express/`). - **Links to another skill:** Use absolute GitHub URLs so links resolve when only one skill is installed. Use the `main` branch: `https://github.com/hookdeck/webhook-skills/blob/main/skills/{skill-name}/…` for a file, or `https://github.com/hookdeck/webhook-skills/tree/main/skills/{skill-name}` for the skill root. diff --git a/scripts/skill-generator/prompts/generate-skill.md b/scripts/skill-generator/prompts/generate-skill.md index f6ecd17..96777f1 100644 --- a/scripts/skill-generator/prompts/generate-skill.md +++ b/scripts/skill-generator/prompts/generate-skill.md @@ -72,6 +72,13 @@ Read the AGENTS.md file in this repository to understand the full skill creation - FastAPI: `npx hookdeck-cli listen 8000 {{PROVIDER_KEBAB}} --path /webhooks/{{PROVIDER_KEBAB}}` Do **not** write `hookdeck listen ...` (assumes a global install), and do **not** omit the source positional arg (the CLI would otherwise prompt interactively). See `AGENTS.md` → "Local Development" for the rationale. +7. **`SKILL.md` inline code is for the verification core only** — keep it short (typically under 25 lines) and scoped to the algorithm/header/comparison essentials. The full Express/Next.js/FastAPI handler (route wiring, event dispatch, error responses) belongs in `examples/`, not in `SKILL.md`. After the verification snippet, link the example directories so agents can pull the runnable, CI-tested version: + + ```markdown + > **For complete handlers with tests**, see [examples/express/](examples/express/), [examples/nextjs/](examples/nextjs/), [examples/fastapi/](examples/fastapi/). + ``` + + Do **not** duplicate the full handler from `examples//src/...` into `SKILL.md`. Drift between the two is a recurring source of bugs (e.g. SDK API changes captured only in the example). ## CRITICAL: Consistency Checks @@ -79,7 +86,7 @@ Read the AGENTS.md file in this repository to understand the full skill creation 1. **Event names** - SKILL.md, overview.md, and all three example handlers must use identical event names 2. **Header names** - All files must reference the same signature header(s) -3. **Verification algorithm** - SKILL.md inline code must match example implementations exactly +3. **Verification algorithm** - the verification core snippet in `SKILL.md` (algorithm + header parse + timing-safe compare) must match how the example handlers verify. Keep the SKILL.md snippet short and focused; the full handler lives in `examples/` 4. **Environment variable names** - Consistent across .env.example and code files **Check for these common mistakes:** diff --git a/skills/github-webhooks/SKILL.md b/skills/github-webhooks/SKILL.md index ed55d30..82957ce 100644 --- a/skills/github-webhooks/SKILL.md +++ b/skills/github-webhooks/SKILL.md @@ -20,107 +20,44 @@ metadata: - Understanding GitHub event types and payloads - Handling push, pull request, or issue events -## Essential Code (USE THIS) +## Verification (core) -### GitHub Signature Verification (JavaScript) +GitHub signs the raw body with HMAC-SHA256 keyed on your webhook secret and sends the digest in `X-Hub-Signature-256` formatted as `sha256=`. Use `X-Hub-Signature-256` (not the legacy SHA-1 `X-Hub-Signature`), pass the **raw** body, and compare timing-safe. + +Node: ```javascript const crypto = require('crypto'); -function verifyGitHubWebhook(rawBody, signatureHeader, secret) { - if (!signatureHeader || !secret) return false; - - // GitHub sends: sha256=xxxx - const [algorithm, signature] = signatureHeader.split('='); - if (algorithm !== 'sha256') return false; - - const expected = crypto - .createHmac('sha256', secret) - .update(rawBody) - .digest('hex'); - +function verify(rawBody, signatureHeader, secret) { + const [algo, sig] = (signatureHeader || '').split('='); + if (algo !== 'sha256' || !sig) return false; + const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex'); try { - return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); + return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)); } catch { return false; } } ``` -### Express Webhook Handler - -```javascript -const express = require('express'); -const app = express(); - -// CRITICAL: Use express.raw() - GitHub requires raw body for signature verification -app.post('/webhooks/github', - express.raw({ type: 'application/json' }), - (req, res) => { - const signature = req.headers['x-hub-signature-256']; // Use sha256, not sha1 - const event = req.headers['x-github-event']; - const delivery = req.headers['x-github-delivery']; - - // Verify signature - if (!verifyGitHubWebhook(req.body, signature, process.env.GITHUB_WEBHOOK_SECRET)) { - console.error('GitHub signature verification failed'); - return res.status(401).send('Invalid signature'); - } - - // Parse payload after verification - const payload = JSON.parse(req.body.toString()); - - console.log(`Received ${event} (delivery: ${delivery})`); - - // Handle by event type - switch (event) { - case 'push': - console.log(`Push to ${payload.ref}:`, payload.head_commit?.message); - break; - case 'pull_request': - console.log(`PR #${payload.number} ${payload.action}:`, payload.pull_request?.title); - break; - case 'issues': - console.log(`Issue #${payload.issue?.number} ${payload.action}:`, payload.issue?.title); - break; - case 'ping': - console.log('Ping:', payload.zen); - break; - default: - console.log('Received event:', event); - } - - res.json({ received: true }); - } -); -``` - -### Python Signature Verification (FastAPI) +Python: ```python -import hmac -import hashlib +import hmac, hashlib -def verify_github_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool: - if not signature_header or not secret: - return False - - # GitHub sends: sha256=xxxx - try: - algorithm, signature = signature_header.split('=') - if algorithm != 'sha256': - return False - except ValueError: +def verify(raw_body: bytes, signature_header: str, secret: str) -> bool: + algo, _, sig = (signature_header or "").partition("=") + if algo != "sha256" or not sig: return False - expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() - return hmac.compare_digest(signature, expected) + return hmac.compare_digest(sig, expected) ``` -> **For complete working examples with tests**, see: -> - [examples/express/](examples/express/) - Full Express implementation -> - [examples/nextjs/](examples/nextjs/) - Next.js App Router implementation -> - [examples/fastapi/](examples/fastapi/) - Python FastAPI implementation +> **For complete handlers with route wiring, event dispatch, and tests**, see: +> - [examples/express/](examples/express/) +> - [examples/nextjs/](examples/nextjs/) +> - [examples/fastapi/](examples/fastapi/) ## Common Event Types diff --git a/skills/shopify-webhooks/SKILL.md b/skills/shopify-webhooks/SKILL.md index af66520..6ede577 100644 --- a/skills/shopify-webhooks/SKILL.md +++ b/skills/shopify-webhooks/SKILL.md @@ -20,99 +20,46 @@ metadata: - Understanding Shopify event types and payloads - Handling order, product, or customer events -## Essential Code (USE THIS) +## Verification (core) -### Shopify Signature Verification (JavaScript) +Shopify signs the raw body with HMAC-SHA256 keyed on the app's API secret and sends the digest in `X-Shopify-Hmac-SHA256` as **base64** (not hex). Pass the **raw** body, decode base64, and compare timing-safe. The topic is in `X-Shopify-Topic`; the shop domain in `X-Shopify-Shop-Domain`. + +Node: ```javascript const crypto = require('crypto'); -function verifyShopifyWebhook(rawBody, hmacHeader, secret) { - if (!hmacHeader || !secret) return false; - - const hash = crypto - .createHmac('sha256', secret) - .update(rawBody) - .digest('base64'); - +function verify(rawBody, hmacHeader, secret) { + if (!hmacHeader) return false; + const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('base64'); try { - return crypto.timingSafeEqual(Buffer.from(hmacHeader), Buffer.from(hash)); + return crypto.timingSafeEqual(Buffer.from(hmacHeader), Buffer.from(expected)); } catch { return false; } } ``` -### Express Webhook Handler - -```javascript -const express = require('express'); -const app = express(); - -// CRITICAL: Use express.raw() - Shopify requires raw body for HMAC verification -app.post('/webhooks/shopify', - express.raw({ type: 'application/json' }), - (req, res) => { - const hmac = req.headers['x-shopify-hmac-sha256']; - const topic = req.headers['x-shopify-topic']; - const shop = req.headers['x-shopify-shop-domain']; - - // Verify signature - if (!verifyShopifyWebhook(req.body, hmac, process.env.SHOPIFY_API_SECRET)) { - console.error('Shopify signature verification failed'); - return res.status(400).send('Invalid signature'); - } - - // Parse payload after verification - const payload = JSON.parse(req.body.toString()); - - console.log(`Received ${topic} from ${shop}`); - - // Handle by topic - switch (topic) { - case 'orders/create': - console.log('New order:', payload.id); - break; - case 'orders/paid': - console.log('Order paid:', payload.id); - break; - case 'products/create': - console.log('New product:', payload.id); - break; - case 'customers/create': - console.log('New customer:', payload.id); - break; - default: - console.log('Received:', topic); - } - - res.status(200).send('OK'); - } -); -``` - -> **Important**: Shopify requires webhook endpoints to respond within 5 seconds with a 200 OK status. Process webhooks asynchronously if your handler logic takes longer. - -### Python Signature Verification (FastAPI) +Python: ```python -import hmac -import hashlib -import base64 +import hmac, hashlib, base64 -def verify_shopify_webhook(raw_body: bytes, hmac_header: str, secret: str) -> bool: - if not hmac_header or not secret: +def verify(raw_body: bytes, hmac_header: str, secret: str) -> bool: + if not hmac_header: return False - calculated = base64.b64encode( + expected = base64.b64encode( hmac.new(secret.encode(), raw_body, hashlib.sha256).digest() ).decode() - return hmac.compare_digest(hmac_header, calculated) + return hmac.compare_digest(hmac_header, expected) ``` -> **For complete working examples with tests**, see: -> - [examples/express/](examples/express/) - Full Express implementation -> - [examples/nextjs/](examples/nextjs/) - Next.js App Router implementation -> - [examples/fastapi/](examples/fastapi/) - Python FastAPI implementation +> **Important**: Shopify requires the endpoint to respond with 200 within 5 seconds. Process work asynchronously if the handler is slow. + +> **For complete handlers with route wiring, event dispatch, and tests**, see: +> - [examples/express/](examples/express/) +> - [examples/nextjs/](examples/nextjs/) +> - [examples/fastapi/](examples/fastapi/) ## Common Event Types (Topics) diff --git a/skills/stripe-webhooks/SKILL.md b/skills/stripe-webhooks/SKILL.md index 5023feb..f49f9cd 100644 --- a/skills/stripe-webhooks/SKILL.md +++ b/skills/stripe-webhooks/SKILL.md @@ -20,82 +20,40 @@ metadata: - Understanding Stripe event types and payloads - Handling payment, subscription, or invoice events -## Essential Code (USE THIS) +## Verification (core) -### Express Webhook Handler +Stripe ships official SDK helpers that verify the `Stripe-Signature` header (HMAC-SHA256 over `timestamp.body`) and parse the event in one call. Pass the **raw** request body — don't `JSON.parse` first. + +Node: ```javascript -const express = require('express'); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); -const app = express(); - -// CRITICAL: Use express.raw() for webhook endpoint - Stripe needs raw body -app.post('/webhooks/stripe', - express.raw({ type: 'application/json' }), - async (req, res) => { - const signature = req.headers['stripe-signature']; - - let event; - try { - // Verify signature using Stripe SDK - event = stripe.webhooks.constructEvent( - req.body, - signature, - process.env.STRIPE_WEBHOOK_SECRET // whsec_xxxxx from Stripe dashboard - ); - } catch (err) { - console.error('Stripe signature verification failed:', err.message); - return res.status(400).send(`Webhook Error: ${err.message}`); - } - - // Handle the event - switch (event.type) { - case 'payment_intent.succeeded': - console.log('Payment succeeded:', event.data.object.id); - break; - case 'customer.subscription.created': - console.log('Subscription created:', event.data.object.id); - break; - case 'invoice.paid': - console.log('Invoice paid:', event.data.object.id); - break; - default: - console.log('Unhandled event:', event.type); - } - - res.json({ received: true }); - } +const event = stripe.webhooks.constructEvent( + rawBody, // Buffer or string of the raw HTTP body + req.headers['stripe-signature'], + process.env.STRIPE_WEBHOOK_SECRET // whsec_… from the webhook endpoint settings ); +// Throws Stripe.errors.SignatureVerificationError on tampering or stale timestamp ``` -### Python (FastAPI) Webhook Handler +Python: ```python import stripe -from fastapi import FastAPI, Request, HTTPException - -stripe.api_key = os.environ.get("STRIPE_SECRET_KEY") -webhook_secret = os.environ.get("STRIPE_WEBHOOK_SECRET") - -@app.post("/webhooks/stripe") -async def stripe_webhook(request: Request): - payload = await request.body() - signature = request.headers.get("stripe-signature") - - try: - event = stripe.Webhook.construct_event(payload, signature, webhook_secret) - except stripe.error.SignatureVerificationError: - raise HTTPException(status_code=400, detail="Invalid signature") - - # Handle event... - return {"received": True} + +event = stripe.Webhook.construct_event( + raw_body, # bytes of the raw HTTP body + request.headers["stripe-signature"], + os.environ["STRIPE_WEBHOOK_SECRET"], +) +# Raises stripe.error.SignatureVerificationError on tampering or stale timestamp ``` -> **For complete working examples with tests**, see: -> - [examples/express/](examples/express/) - Full Express implementation -> - [examples/nextjs/](examples/nextjs/) - Next.js App Router implementation -> - [examples/fastapi/](examples/fastapi/) - Python FastAPI implementation +> **For complete handlers with route wiring, event dispatch, and tests**, see: +> - [examples/express/](examples/express/) +> - [examples/nextjs/](examples/nextjs/) +> - [examples/fastapi/](examples/fastapi/) ## Common Event Types