From a20090528ce9a502052bc7189362b5e9dac9ba90 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 11 May 2026 15:08:20 +0000 Subject: [PATCH] docs: trim SKILL.md inline handlers to verification core; point to examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a normative rule in AGENTS.md "Content Guidelines" and the generator prompt: `SKILL.md` should carry only the verification core (algorithm + header parse + timing-safe compare, ~10-20 lines per language). The full Express/Next.js/FastAPI handler — route wiring, event dispatch, error responses — lives in `examples/`, which are CI-tested. `SKILL.md` then links to the example directories instead of duplicating their code. Why: - Two recent bug classes in this repo trace back to inline-handler drift. The Stripe SDK 15+ break (stripe.Webhook.construct_event now requiring an `object` field on the event) was caught only by the example's test suite; if the example had been duplicated into SKILL.md, the inline copy would have rotted silently. The Hookdeck CLI convention shift touched 93 files because the same command line was repeated across every skill's docs. - A pointer-only SKILL.md would force agents to load an example file for every "how do I verify X?" query, which is the most common skill query. Keeping the verification snippet inline preserves that fast-path while removing the duplicated handler that's the real drift surface. Applies the trim to the three reference skills the generator prompt explicitly tells new generations to mimic — stripe-webhooks, github-webhooks, shopify-webhooks — so the visible pattern is consistent with the spec. The remaining existing skills (~16) and the 13 currently open feat PRs (#41-#52, #55) can be swept in a follow-up if review of this pattern goes well. No lint script: a length-only threshold (e.g. "no code fence > 25 lines") is too coarse to be useful — legitimate verification cores can hit that limit while compact-but-duplicated handlers slip under. A content-match lint ("SKILL.md snippet appears verbatim in examples/*") would be a better signal, but is enough scope to defer until we see whether this spec + the generator prompt rule actually holds in practice. https://claude.ai/code/session_01NNTgQRJss1V7gyzzJ9rjnB --- AGENTS.md | 11 ++ .../skill-generator/prompts/generate-skill.md | 9 +- skills/github-webhooks/SKILL.md | 101 ++++-------------- skills/shopify-webhooks/SKILL.md | 93 ++++------------ skills/stripe-webhooks/SKILL.md | 84 ++++----------- 5 files changed, 79 insertions(+), 219 deletions(-) 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