Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
9 changes: 8 additions & 1 deletion scripts/skill-generator/prompts/generate-skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,21 @@ 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/<framework>/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

**Before finalizing, verify these are consistent across all files:**

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:**
Expand Down
101 changes: 19 additions & 82 deletions skills/github-webhooks/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<hex>`. 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

Expand Down
93 changes: 20 additions & 73 deletions skills/shopify-webhooks/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
84 changes: 21 additions & 63 deletions skills/stripe-webhooks/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading