Skip to content

Commit ac572c2

Browse files
authored
Merge pull request #56 from hookdeck/chore/inline-code-trim-convention
docs: trim SKILL.md inline handlers to verification core; point to examples
2 parents 25c0c34 + a200905 commit ac572c2

5 files changed

Lines changed: 79 additions & 219 deletions

File tree

AGENTS.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,17 @@ metadata:
147147

148148
- Keep SKILL.md under 500 lines / < 5,000 tokens
149149
- Put detailed reference material in `references/` files
150+
- **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:
151+
```markdown
152+
## Verification (core)
153+
154+
```javascript
155+
// ~10–20 lines: HMAC compute + timing-safe compare
156+
```
157+
158+
> **For complete handlers with tests**, see [examples/express/](examples/express/), [examples/nextjs/](examples/nextjs/), [examples/fastapi/](examples/fastapi/).
159+
```
160+
This keeps `SKILL.md` focused, lets agents copy the canonical verification fast, and avoids drift between `SKILL.md` and the runnable (CI-tested) examples.
150161
- **Links within the same skill:** Use relative paths (e.g. `references/verification.md`, `examples/express/`).
151162
- **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.
152163

scripts/skill-generator/prompts/generate-skill.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,21 @@ Read the AGENTS.md file in this repository to understand the full skill creation
7272
- FastAPI: `npx hookdeck-cli listen 8000 {{PROVIDER_KEBAB}} --path /webhooks/{{PROVIDER_KEBAB}}`
7373

7474
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.
75+
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:
76+
77+
```markdown
78+
> **For complete handlers with tests**, see [examples/express/](examples/express/), [examples/nextjs/](examples/nextjs/), [examples/fastapi/](examples/fastapi/).
79+
```
80+
81+
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).
7582

7683
## CRITICAL: Consistency Checks
7784

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

8087
1. **Event names** - SKILL.md, overview.md, and all three example handlers must use identical event names
8188
2. **Header names** - All files must reference the same signature header(s)
82-
3. **Verification algorithm** - SKILL.md inline code must match example implementations exactly
89+
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/`
8390
4. **Environment variable names** - Consistent across .env.example and code files
8491

8592
**Check for these common mistakes:**

skills/github-webhooks/SKILL.md

Lines changed: 19 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -20,107 +20,44 @@ metadata:
2020
- Understanding GitHub event types and payloads
2121
- Handling push, pull request, or issue events
2222

23-
## Essential Code (USE THIS)
23+
## Verification (core)
2424

25-
### GitHub Signature Verification (JavaScript)
25+
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.
26+
27+
Node:
2628

2729
```javascript
2830
const crypto = require('crypto');
2931

30-
function verifyGitHubWebhook(rawBody, signatureHeader, secret) {
31-
if (!signatureHeader || !secret) return false;
32-
33-
// GitHub sends: sha256=xxxx
34-
const [algorithm, signature] = signatureHeader.split('=');
35-
if (algorithm !== 'sha256') return false;
36-
37-
const expected = crypto
38-
.createHmac('sha256', secret)
39-
.update(rawBody)
40-
.digest('hex');
41-
32+
function verify(rawBody, signatureHeader, secret) {
33+
const [algo, sig] = (signatureHeader || '').split('=');
34+
if (algo !== 'sha256' || !sig) return false;
35+
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
4236
try {
43-
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
37+
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
4438
} catch {
4539
return false;
4640
}
4741
}
4842
```
4943

50-
### Express Webhook Handler
51-
52-
```javascript
53-
const express = require('express');
54-
const app = express();
55-
56-
// CRITICAL: Use express.raw() - GitHub requires raw body for signature verification
57-
app.post('/webhooks/github',
58-
express.raw({ type: 'application/json' }),
59-
(req, res) => {
60-
const signature = req.headers['x-hub-signature-256']; // Use sha256, not sha1
61-
const event = req.headers['x-github-event'];
62-
const delivery = req.headers['x-github-delivery'];
63-
64-
// Verify signature
65-
if (!verifyGitHubWebhook(req.body, signature, process.env.GITHUB_WEBHOOK_SECRET)) {
66-
console.error('GitHub signature verification failed');
67-
return res.status(401).send('Invalid signature');
68-
}
69-
70-
// Parse payload after verification
71-
const payload = JSON.parse(req.body.toString());
72-
73-
console.log(`Received ${event} (delivery: ${delivery})`);
74-
75-
// Handle by event type
76-
switch (event) {
77-
case 'push':
78-
console.log(`Push to ${payload.ref}:`, payload.head_commit?.message);
79-
break;
80-
case 'pull_request':
81-
console.log(`PR #${payload.number} ${payload.action}:`, payload.pull_request?.title);
82-
break;
83-
case 'issues':
84-
console.log(`Issue #${payload.issue?.number} ${payload.action}:`, payload.issue?.title);
85-
break;
86-
case 'ping':
87-
console.log('Ping:', payload.zen);
88-
break;
89-
default:
90-
console.log('Received event:', event);
91-
}
92-
93-
res.json({ received: true });
94-
}
95-
);
96-
```
97-
98-
### Python Signature Verification (FastAPI)
44+
Python:
9945

10046
```python
101-
import hmac
102-
import hashlib
47+
import hmac, hashlib
10348

104-
def verify_github_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
105-
if not signature_header or not secret:
106-
return False
107-
108-
# GitHub sends: sha256=xxxx
109-
try:
110-
algorithm, signature = signature_header.split('=')
111-
if algorithm != 'sha256':
112-
return False
113-
except ValueError:
49+
def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
50+
algo, _, sig = (signature_header or "").partition("=")
51+
if algo != "sha256" or not sig:
11452
return False
115-
11653
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
117-
return hmac.compare_digest(signature, expected)
54+
return hmac.compare_digest(sig, expected)
11855
```
11956

120-
> **For complete working examples with tests**, see:
121-
> - [examples/express/](examples/express/) - Full Express implementation
122-
> - [examples/nextjs/](examples/nextjs/) - Next.js App Router implementation
123-
> - [examples/fastapi/](examples/fastapi/) - Python FastAPI implementation
57+
> **For complete handlers with route wiring, event dispatch, and tests**, see:
58+
> - [examples/express/](examples/express/)
59+
> - [examples/nextjs/](examples/nextjs/)
60+
> - [examples/fastapi/](examples/fastapi/)
12461
12562
## Common Event Types
12663

skills/shopify-webhooks/SKILL.md

Lines changed: 20 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -20,99 +20,46 @@ metadata:
2020
- Understanding Shopify event types and payloads
2121
- Handling order, product, or customer events
2222

23-
## Essential Code (USE THIS)
23+
## Verification (core)
2424

25-
### Shopify Signature Verification (JavaScript)
25+
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`.
26+
27+
Node:
2628

2729
```javascript
2830
const crypto = require('crypto');
2931

30-
function verifyShopifyWebhook(rawBody, hmacHeader, secret) {
31-
if (!hmacHeader || !secret) return false;
32-
33-
const hash = crypto
34-
.createHmac('sha256', secret)
35-
.update(rawBody)
36-
.digest('base64');
37-
32+
function verify(rawBody, hmacHeader, secret) {
33+
if (!hmacHeader) return false;
34+
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('base64');
3835
try {
39-
return crypto.timingSafeEqual(Buffer.from(hmacHeader), Buffer.from(hash));
36+
return crypto.timingSafeEqual(Buffer.from(hmacHeader), Buffer.from(expected));
4037
} catch {
4138
return false;
4239
}
4340
}
4441
```
4542

46-
### Express Webhook Handler
47-
48-
```javascript
49-
const express = require('express');
50-
const app = express();
51-
52-
// CRITICAL: Use express.raw() - Shopify requires raw body for HMAC verification
53-
app.post('/webhooks/shopify',
54-
express.raw({ type: 'application/json' }),
55-
(req, res) => {
56-
const hmac = req.headers['x-shopify-hmac-sha256'];
57-
const topic = req.headers['x-shopify-topic'];
58-
const shop = req.headers['x-shopify-shop-domain'];
59-
60-
// Verify signature
61-
if (!verifyShopifyWebhook(req.body, hmac, process.env.SHOPIFY_API_SECRET)) {
62-
console.error('Shopify signature verification failed');
63-
return res.status(400).send('Invalid signature');
64-
}
65-
66-
// Parse payload after verification
67-
const payload = JSON.parse(req.body.toString());
68-
69-
console.log(`Received ${topic} from ${shop}`);
70-
71-
// Handle by topic
72-
switch (topic) {
73-
case 'orders/create':
74-
console.log('New order:', payload.id);
75-
break;
76-
case 'orders/paid':
77-
console.log('Order paid:', payload.id);
78-
break;
79-
case 'products/create':
80-
console.log('New product:', payload.id);
81-
break;
82-
case 'customers/create':
83-
console.log('New customer:', payload.id);
84-
break;
85-
default:
86-
console.log('Received:', topic);
87-
}
88-
89-
res.status(200).send('OK');
90-
}
91-
);
92-
```
93-
94-
> **Important**: Shopify requires webhook endpoints to respond within 5 seconds with a 200 OK status. Process webhooks asynchronously if your handler logic takes longer.
95-
96-
### Python Signature Verification (FastAPI)
43+
Python:
9744

9845
```python
99-
import hmac
100-
import hashlib
101-
import base64
46+
import hmac, hashlib, base64
10247

103-
def verify_shopify_webhook(raw_body: bytes, hmac_header: str, secret: str) -> bool:
104-
if not hmac_header or not secret:
48+
def verify(raw_body: bytes, hmac_header: str, secret: str) -> bool:
49+
if not hmac_header:
10550
return False
106-
calculated = base64.b64encode(
51+
expected = base64.b64encode(
10752
hmac.new(secret.encode(), raw_body, hashlib.sha256).digest()
10853
).decode()
109-
return hmac.compare_digest(hmac_header, calculated)
54+
return hmac.compare_digest(hmac_header, expected)
11055
```
11156

112-
> **For complete working examples with tests**, see:
113-
> - [examples/express/](examples/express/) - Full Express implementation
114-
> - [examples/nextjs/](examples/nextjs/) - Next.js App Router implementation
115-
> - [examples/fastapi/](examples/fastapi/) - Python FastAPI implementation
57+
> **Important**: Shopify requires the endpoint to respond with 200 within 5 seconds. Process work asynchronously if the handler is slow.
58+
59+
> **For complete handlers with route wiring, event dispatch, and tests**, see:
60+
> - [examples/express/](examples/express/)
61+
> - [examples/nextjs/](examples/nextjs/)
62+
> - [examples/fastapi/](examples/fastapi/)
11663
11764
## Common Event Types (Topics)
11865

skills/stripe-webhooks/SKILL.md

Lines changed: 21 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -20,82 +20,40 @@ metadata:
2020
- Understanding Stripe event types and payloads
2121
- Handling payment, subscription, or invoice events
2222

23-
## Essential Code (USE THIS)
23+
## Verification (core)
2424

25-
### Express Webhook Handler
25+
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.
26+
27+
Node:
2628

2729
```javascript
28-
const express = require('express');
2930
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
3031

31-
const app = express();
32-
33-
// CRITICAL: Use express.raw() for webhook endpoint - Stripe needs raw body
34-
app.post('/webhooks/stripe',
35-
express.raw({ type: 'application/json' }),
36-
async (req, res) => {
37-
const signature = req.headers['stripe-signature'];
38-
39-
let event;
40-
try {
41-
// Verify signature using Stripe SDK
42-
event = stripe.webhooks.constructEvent(
43-
req.body,
44-
signature,
45-
process.env.STRIPE_WEBHOOK_SECRET // whsec_xxxxx from Stripe dashboard
46-
);
47-
} catch (err) {
48-
console.error('Stripe signature verification failed:', err.message);
49-
return res.status(400).send(`Webhook Error: ${err.message}`);
50-
}
51-
52-
// Handle the event
53-
switch (event.type) {
54-
case 'payment_intent.succeeded':
55-
console.log('Payment succeeded:', event.data.object.id);
56-
break;
57-
case 'customer.subscription.created':
58-
console.log('Subscription created:', event.data.object.id);
59-
break;
60-
case 'invoice.paid':
61-
console.log('Invoice paid:', event.data.object.id);
62-
break;
63-
default:
64-
console.log('Unhandled event:', event.type);
65-
}
66-
67-
res.json({ received: true });
68-
}
32+
const event = stripe.webhooks.constructEvent(
33+
rawBody, // Buffer or string of the raw HTTP body
34+
req.headers['stripe-signature'],
35+
process.env.STRIPE_WEBHOOK_SECRET // whsec_… from the webhook endpoint settings
6936
);
37+
// Throws Stripe.errors.SignatureVerificationError on tampering or stale timestamp
7038
```
7139

72-
### Python (FastAPI) Webhook Handler
40+
Python:
7341

7442
```python
7543
import stripe
76-
from fastapi import FastAPI, Request, HTTPException
77-
78-
stripe.api_key = os.environ.get("STRIPE_SECRET_KEY")
79-
webhook_secret = os.environ.get("STRIPE_WEBHOOK_SECRET")
80-
81-
@app.post("/webhooks/stripe")
82-
async def stripe_webhook(request: Request):
83-
payload = await request.body()
84-
signature = request.headers.get("stripe-signature")
85-
86-
try:
87-
event = stripe.Webhook.construct_event(payload, signature, webhook_secret)
88-
except stripe.error.SignatureVerificationError:
89-
raise HTTPException(status_code=400, detail="Invalid signature")
90-
91-
# Handle event...
92-
return {"received": True}
44+
45+
event = stripe.Webhook.construct_event(
46+
raw_body, # bytes of the raw HTTP body
47+
request.headers["stripe-signature"],
48+
os.environ["STRIPE_WEBHOOK_SECRET"],
49+
)
50+
# Raises stripe.error.SignatureVerificationError on tampering or stale timestamp
9351
```
9452

95-
> **For complete working examples with tests**, see:
96-
> - [examples/express/](examples/express/) - Full Express implementation
97-
> - [examples/nextjs/](examples/nextjs/) - Next.js App Router implementation
98-
> - [examples/fastapi/](examples/fastapi/) - Python FastAPI implementation
53+
> **For complete handlers with route wiring, event dispatch, and tests**, see:
54+
> - [examples/express/](examples/express/)
55+
> - [examples/nextjs/](examples/nextjs/)
56+
> - [examples/fastapi/](examples/fastapi/)
9957
10058
## Common Event Types
10159

0 commit comments

Comments
 (0)