From 4c967eebc0ead47296a9a8e1e66aca90e6afce48 Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Thu, 5 Feb 2026 13:11:50 +0000 Subject: [PATCH 1/5] feat: add woocommerce-webhooks skill --- skills/woocommerce-webhooks/SKILL.md | 211 ++++++++++++++ .../examples/express/.env.example | 1 + .../examples/express/README.md | 54 ++++ .../examples/express/package.json | 23 ++ .../examples/express/src/index.js | 128 +++++++++ .../examples/express/test/webhook.test.js | 211 ++++++++++++++ .../examples/fastapi/.env.example | 1 + .../examples/fastapi/README.md | 69 +++++ .../examples/fastapi/main.py | 131 +++++++++ .../examples/fastapi/requirements.txt | 4 + .../examples/fastapi/test_webhook.py | 210 ++++++++++++++ .../examples/nextjs/.env.example | 1 + .../examples/nextjs/README.md | 59 ++++ .../nextjs/app/webhooks/woocommerce/route.ts | 123 +++++++++ .../examples/nextjs/package.json | 24 ++ .../examples/nextjs/test/webhook.test.ts | 214 ++++++++++++++ .../examples/nextjs/vitest.config.ts | 7 + .../references/overview.md | 125 +++++++++ .../woocommerce-webhooks/references/setup.md | 110 ++++++++ .../references/verification.md | 260 ++++++++++++++++++ 20 files changed, 1966 insertions(+) create mode 100644 skills/woocommerce-webhooks/SKILL.md create mode 100644 skills/woocommerce-webhooks/examples/express/.env.example create mode 100644 skills/woocommerce-webhooks/examples/express/README.md create mode 100644 skills/woocommerce-webhooks/examples/express/package.json create mode 100644 skills/woocommerce-webhooks/examples/express/src/index.js create mode 100644 skills/woocommerce-webhooks/examples/express/test/webhook.test.js create mode 100644 skills/woocommerce-webhooks/examples/fastapi/.env.example create mode 100644 skills/woocommerce-webhooks/examples/fastapi/README.md create mode 100644 skills/woocommerce-webhooks/examples/fastapi/main.py create mode 100644 skills/woocommerce-webhooks/examples/fastapi/requirements.txt create mode 100644 skills/woocommerce-webhooks/examples/fastapi/test_webhook.py create mode 100644 skills/woocommerce-webhooks/examples/nextjs/.env.example create mode 100644 skills/woocommerce-webhooks/examples/nextjs/README.md create mode 100644 skills/woocommerce-webhooks/examples/nextjs/app/webhooks/woocommerce/route.ts create mode 100644 skills/woocommerce-webhooks/examples/nextjs/package.json create mode 100644 skills/woocommerce-webhooks/examples/nextjs/test/webhook.test.ts create mode 100644 skills/woocommerce-webhooks/examples/nextjs/vitest.config.ts create mode 100644 skills/woocommerce-webhooks/references/overview.md create mode 100644 skills/woocommerce-webhooks/references/setup.md create mode 100644 skills/woocommerce-webhooks/references/verification.md diff --git a/skills/woocommerce-webhooks/SKILL.md b/skills/woocommerce-webhooks/SKILL.md new file mode 100644 index 0000000..1c9b550 --- /dev/null +++ b/skills/woocommerce-webhooks/SKILL.md @@ -0,0 +1,211 @@ +--- +name: woocommerce-webhooks +description: > + Receive and verify WooCommerce webhooks. Use when setting up WooCommerce webhook + handlers, debugging signature verification, or handling e-commerce events like + order.created, order.updated, product.created, or customer.created. +license: MIT +metadata: + author: hookdeck + version: "0.1.0" + repository: https://github.com/hookdeck/webhook-skills +--- + +# WooCommerce Webhooks + +## When to Use This Skill + +- Setting up WooCommerce webhook handlers +- Debugging signature verification failures +- Understanding WooCommerce event types and payloads +- Handling order, product, or customer events +- Integrating with WooCommerce stores + +## Essential Code (USE THIS) + +### WooCommerce Signature Verification (JavaScript) + +```javascript +const crypto = require('crypto'); + +function verifyWooCommerceWebhook(rawBody, signature, secret) { + if (!signature || !secret) return false; + + const hash = crypto + .createHmac('sha256', secret) + .update(rawBody) + .digest('base64'); + + try { + return crypto.timingSafeEqual( + Buffer.from(signature), + Buffer.from(hash) + ); + } catch { + return false; + } +} +``` + +### Express Webhook Handler + +```javascript +const express = require('express'); +const app = express(); + +// CRITICAL: Use raw body for signature verification +app.use('/webhooks/woocommerce', express.raw({ type: 'application/json' })); + +app.post('/webhooks/woocommerce', (req, res) => { + const signature = req.headers['x-wc-webhook-signature']; + const secret = process.env.WOOCOMMERCE_WEBHOOK_SECRET; + + if (!verifyWooCommerceWebhook(req.body, signature, secret)) { + return res.status(400).send('Invalid signature'); + } + + const payload = JSON.parse(req.body); + const topic = req.headers['x-wc-webhook-topic']; + + console.log(`Received ${topic} event:`, payload.id); + res.status(200).send('OK'); +}); +``` + +### Next.js API Route (App Router) + +```typescript +import crypto from 'crypto'; +import { NextRequest } from 'next/server'; + +export async function POST(request: NextRequest) { + const signature = request.headers.get('x-wc-webhook-signature'); + const secret = process.env.WOOCOMMERCE_WEBHOOK_SECRET; + + const rawBody = await request.text(); + + if (!verifyWooCommerceWebhook(rawBody, signature, secret)) { + return new Response('Invalid signature', { status: 400 }); + } + + const payload = JSON.parse(rawBody); + const topic = request.headers.get('x-wc-webhook-topic'); + + console.log(`Received ${topic} event:`, payload.id); + return new Response('OK', { status: 200 }); +} +``` + +### FastAPI Handler + +```python +import hmac +import hashlib +import base64 +from fastapi import FastAPI, Request, HTTPException + +app = FastAPI() + +def verify_woocommerce_webhook(raw_body: bytes, signature: str, secret: str) -> bool: + if not signature or not secret: + return False + + hash_digest = hmac.new( + secret.encode(), + raw_body, + hashlib.sha256 + ).digest() + expected_signature = base64.b64encode(hash_digest).decode() + + return hmac.compare_digest(signature, expected_signature) + +@app.post('/webhooks/woocommerce') +async def handle_webhook(request: Request): + raw_body = await request.body() + signature = request.headers.get('x-wc-webhook-signature') + secret = os.getenv('WOOCOMMERCE_WEBHOOK_SECRET') + + if not verify_woocommerce_webhook(raw_body, signature, secret): + raise HTTPException(status_code=400, detail='Invalid signature') + + payload = await request.json() + topic = request.headers.get('x-wc-webhook-topic') + + print(f"Received {topic} event: {payload.get('id')}") + return {'status': 'success'} +``` + +## Common Event Types + +| Event | Triggered When | Common Use Cases | +|-------|----------------|------------------| +| `order.created` | New order placed | Send confirmation emails, update inventory | +| `order.updated` | Order status changed | Track fulfillment, send notifications | +| `order.deleted` | Order deleted | Clean up external systems | +| `product.created` | Product added | Sync to external catalogs | +| `product.updated` | Product modified | Update pricing, inventory | +| `customer.created` | New customer registered | Welcome emails, CRM sync | +| `customer.updated` | Customer info changed | Update profiles, preferences | + +## Environment Variables + +```bash +WOOCOMMERCE_WEBHOOK_SECRET=your_webhook_secret_key +``` + +## Headers Reference + +WooCommerce webhooks include these headers: + +- `X-WC-Webhook-Signature` - HMAC SHA256 signature (base64) +- `X-WC-Webhook-Topic` - Event type (e.g., "order.created") +- `X-WC-Webhook-Resource` - Resource type (e.g., "order") +- `X-WC-Webhook-Event` - Action (e.g., "created") +- `X-WC-Webhook-Source` - Store URL +- `X-WC-Webhook-ID` - Webhook ID +- `X-WC-Webhook-Delivery-ID` - Unique delivery ID + +## Local Development + +For local webhook testing, install Hookdeck CLI: + +```bash +# Install via npm +npm install -g hookdeck-cli + +# Or via Homebrew +brew install hookdeck/hookdeck/hookdeck +``` + +Then start the tunnel: + +```bash +hookdeck listen 3000 --path /webhooks/woocommerce +``` + +No account required. Provides local tunnel + web UI for inspecting requests. + +## Reference Materials + +- `overview.md` - What WooCommerce webhooks are, common event types +- `setup.md` - Configure webhooks in WooCommerce admin, get signing secret +- `verification.md` - Signature verification details and gotchas +- `examples/` - Complete runnable examples per framework + +## Recommended: webhook-handler-patterns + +For production-ready webhook handlers, also install the webhook-handler-patterns skill for: + +- Handler sequence +- Idempotency +- Error handling +- Retry logic + +## Related Skills + +- [stripe-webhooks](https://github.com/hookdeck/webhook-skills/tree/main/skills/stripe-webhooks) - Stripe payment webhooks with HMAC verification +- [shopify-webhooks](https://github.com/hookdeck/webhook-skills/tree/main/skills/shopify-webhooks) - Shopify store webhooks with HMAC verification +- [github-webhooks](https://github.com/hookdeck/webhook-skills/tree/main/skills/github-webhooks) - GitHub repository webhooks +- [paddle-webhooks](https://github.com/hookdeck/webhook-skills/tree/main/skills/paddle-webhooks) - Paddle billing webhooks +- [webhook-handler-patterns](https://github.com/hookdeck/webhook-skills/tree/main/skills/webhook-handler-patterns) - Idempotency, error handling, retry logic +- [hookdeck-event-gateway](https://github.com/hookdeck/webhook-skills/tree/main/skills/hookdeck-event-gateway) - Production webhook infrastructure \ No newline at end of file diff --git a/skills/woocommerce-webhooks/examples/express/.env.example b/skills/woocommerce-webhooks/examples/express/.env.example new file mode 100644 index 0000000..7af6dec --- /dev/null +++ b/skills/woocommerce-webhooks/examples/express/.env.example @@ -0,0 +1 @@ +WOOCOMMERCE_WEBHOOK_SECRET=your_webhook_secret_key_here \ No newline at end of file diff --git a/skills/woocommerce-webhooks/examples/express/README.md b/skills/woocommerce-webhooks/examples/express/README.md new file mode 100644 index 0000000..7a41f7e --- /dev/null +++ b/skills/woocommerce-webhooks/examples/express/README.md @@ -0,0 +1,54 @@ +# WooCommerce Webhooks - Express Example + +Minimal example of receiving WooCommerce webhooks with signature verification. + +## Prerequisites + +- Node.js 18+ +- WooCommerce store with webhook signing secret + +## Setup + +1. Install dependencies: + ```bash + npm install + ``` + +2. Copy environment variables: + ```bash + cp .env.example .env + ``` + +3. Add your WooCommerce webhook secret to `.env`: + ``` + WOOCOMMERCE_WEBHOOK_SECRET=your_webhook_secret_from_woocommerce + ``` + +## Run + +```bash +npm start +``` + +Server runs on http://localhost:3000 + +## Test + +You can test with a sample payload: + +```bash +curl -X POST http://localhost:3000/webhooks/woocommerce \ + -H "Content-Type: application/json" \ + -H "X-WC-Webhook-Topic: order.created" \ + -H "X-WC-Webhook-Signature: your_generated_signature" \ + -d '{"id": 123, "status": "processing", "total": "29.99"}' +``` + +The signature should be generated using HMAC SHA-256 with your webhook secret. + +## WooCommerce Setup + +In your WooCommerce admin: +1. Go to **WooCommerce > Settings > Advanced > Webhooks** +2. Add webhook with delivery URL: `http://localhost:3000/webhooks/woocommerce` +3. Copy the webhook secret to your `.env` file \ No newline at end of file diff --git a/skills/woocommerce-webhooks/examples/express/package.json b/skills/woocommerce-webhooks/examples/express/package.json new file mode 100644 index 0000000..855fa4d --- /dev/null +++ b/skills/woocommerce-webhooks/examples/express/package.json @@ -0,0 +1,23 @@ +{ + "name": "woocommerce-webhook-express", + "version": "1.0.0", + "description": "WooCommerce webhook handler using Express", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "test": "jest", + "dev": "nodemon src/index.js" + }, + "dependencies": { + "express": "^5.2.1", + "dotenv": "^16.4.7" + }, + "devDependencies": { + "jest": "^30.2.0", + "nodemon": "^3.1.9", + "supertest": "^7.0.0" + }, + "jest": { + "testEnvironment": "node" + } +} \ No newline at end of file diff --git a/skills/woocommerce-webhooks/examples/express/src/index.js b/skills/woocommerce-webhooks/examples/express/src/index.js new file mode 100644 index 0000000..ffa2bd4 --- /dev/null +++ b/skills/woocommerce-webhooks/examples/express/src/index.js @@ -0,0 +1,128 @@ +const express = require('express'); +const crypto = require('crypto'); +require('dotenv').config(); + +const app = express(); +const PORT = process.env.PORT || 3000; + +/** + * Verify WooCommerce webhook signature using HMAC SHA-256 + * @param {Buffer} rawBody - Raw request body + * @param {string} signature - X-WC-Webhook-Signature header value + * @param {string} secret - Webhook secret from WooCommerce + * @returns {boolean} - True if signature is valid + */ +function verifyWooCommerceWebhook(rawBody, signature, secret) { + if (!signature || !secret || !rawBody) { + return false; + } + + const expectedSignature = crypto + .createHmac('sha256', secret) + .update(rawBody) + .digest('base64'); + + try { + return crypto.timingSafeEqual( + Buffer.from(signature), + Buffer.from(expectedSignature) + ); + } catch (error) { + // Different lengths will cause an error + return false; + } +} + +/** + * Handle different WooCommerce event types + * @param {string} topic - Event topic (e.g., "order.created") + * @param {Object} payload - Webhook payload + */ +function handleWooCommerceEvent(topic, payload) { + console.log(`Processing ${topic} event for ID: ${payload.id}`); + + switch (topic) { + case 'order.created': + console.log(`New order #${payload.id} for $${payload.total}`); + // Add your order processing logic here + break; + + case 'order.updated': + console.log(`Order #${payload.id} updated to status: ${payload.status}`); + // Add your order update logic here + break; + + case 'product.created': + console.log(`New product: ${payload.name} (ID: ${payload.id})`); + // Add your product sync logic here + break; + + case 'product.updated': + console.log(`Product updated: ${payload.name} (ID: ${payload.id})`); + // Add your product update logic here + break; + + case 'customer.created': + console.log(`New customer: ${payload.email} (ID: ${payload.id})`); + // Add your customer onboarding logic here + break; + + case 'customer.updated': + console.log(`Customer updated: ${payload.email} (ID: ${payload.id})`); + // Add your customer update logic here + break; + + default: + console.log(`Unhandled event type: ${topic}`); + } +} + +// CRITICAL: Use raw body parser for signature verification +app.use('/webhooks/woocommerce', express.raw({ type: 'application/json' })); + +// WooCommerce webhook endpoint +app.post('/webhooks/woocommerce', (req, res) => { + try { + const signature = req.headers['x-wc-webhook-signature']; + const topic = req.headers['x-wc-webhook-topic']; + const source = req.headers['x-wc-webhook-source']; + const secret = process.env.WOOCOMMERCE_WEBHOOK_SECRET; + + console.log(`Received webhook: ${topic} from ${source}`); + + // Verify webhook signature + if (!verifyWooCommerceWebhook(req.body, signature, secret)) { + console.log('❌ Invalid webhook signature'); + return res.status(400).json({ error: 'Invalid signature' }); + } + + console.log('✅ Signature verified'); + + // Parse the JSON payload + const payload = JSON.parse(req.body.toString()); + + // Handle the event + handleWooCommerceEvent(topic, payload); + + // Respond with success + res.status(200).json({ received: true }); + + } catch (error) { + console.error('Error processing webhook:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Health check endpoint +app.get('/health', (req, res) => { + res.status(200).json({ status: 'healthy', timestamp: new Date().toISOString() }); +}); + +// Start server +app.listen(PORT, () => { + console.log(`🚀 WooCommerce webhook server running on port ${PORT}`); + console.log(`📍 Webhook endpoint: http://localhost:${PORT}/webhooks/woocommerce`); + console.log('🔒 Make sure to set WOOCOMMERCE_WEBHOOK_SECRET in your environment'); +}); + +module.exports = { app, verifyWooCommerceWebhook }; \ No newline at end of file diff --git a/skills/woocommerce-webhooks/examples/express/test/webhook.test.js b/skills/woocommerce-webhooks/examples/express/test/webhook.test.js new file mode 100644 index 0000000..9070ea6 --- /dev/null +++ b/skills/woocommerce-webhooks/examples/express/test/webhook.test.js @@ -0,0 +1,211 @@ +const request = require('supertest'); +const crypto = require('crypto'); +const { app, verifyWooCommerceWebhook } = require('../src/index'); + +// Test webhook secret +const TEST_SECRET = 'test_woocommerce_secret_key'; + +// Set test environment variable +process.env.WOOCOMMERCE_WEBHOOK_SECRET = TEST_SECRET; + +/** + * Generate a valid WooCommerce webhook signature for testing + * @param {string} payload - JSON payload as string + * @param {string} secret - Webhook secret + * @returns {string} - Base64 encoded signature + */ +function generateTestSignature(payload, secret) { + return crypto + .createHmac('sha256', secret) + .update(payload) + .digest('base64'); +} + +describe('WooCommerce Webhook Handler', () => { + describe('Signature Verification', () => { + test('should verify valid signatures', () => { + const payload = '{"id": 123, "status": "processing"}'; + const signature = generateTestSignature(payload, TEST_SECRET); + + const isValid = verifyWooCommerceWebhook( + Buffer.from(payload), + signature, + TEST_SECRET + ); + + expect(isValid).toBe(true); + }); + + test('should reject invalid signatures', () => { + const payload = '{"id": 123, "status": "processing"}'; + const invalidSignature = 'invalid_signature'; + + const isValid = verifyWooCommerceWebhook( + Buffer.from(payload), + invalidSignature, + TEST_SECRET + ); + + expect(isValid).toBe(false); + }); + + test('should reject missing signature', () => { + const payload = '{"id": 123, "status": "processing"}'; + + const isValid = verifyWooCommerceWebhook( + Buffer.from(payload), + null, + TEST_SECRET + ); + + expect(isValid).toBe(false); + }); + + test('should reject missing secret', () => { + const payload = '{"id": 123, "status": "processing"}'; + const signature = generateTestSignature(payload, TEST_SECRET); + + const isValid = verifyWooCommerceWebhook( + Buffer.from(payload), + signature, + null + ); + + expect(isValid).toBe(false); + }); + + test('should handle different payload lengths', () => { + const payloads = [ + '{}', + '{"id":1}', + '{"id": 123, "status": "processing", "total": "29.99", "customer": {"name": "John Doe"}}' + ]; + + payloads.forEach(payload => { + const signature = generateTestSignature(payload, TEST_SECRET); + const isValid = verifyWooCommerceWebhook( + Buffer.from(payload), + signature, + TEST_SECRET + ); + expect(isValid).toBe(true); + }); + }); + }); + + describe('Webhook Endpoint', () => { + test('should accept valid order.created webhook', async () => { + const payload = { + id: 123, + status: 'processing', + total: '29.99', + currency: 'USD', + billing: { + first_name: 'John', + last_name: 'Doe', + email: 'john@example.com' + } + }; + + const payloadString = JSON.stringify(payload); + const signature = generateTestSignature(payloadString, TEST_SECRET); + + const response = await request(app) + .post('/webhooks/woocommerce') + .set('Content-Type', 'application/json') + .set('X-WC-Webhook-Topic', 'order.created') + .set('X-WC-Webhook-Signature', signature) + .set('X-WC-Webhook-Source', 'https://example.com') + .send(payloadString); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ received: true }); + }); + + test('should accept valid product.updated webhook', async () => { + const payload = { + id: 456, + name: 'Premium T-Shirt', + status: 'publish', + regular_price: '29.99', + stock_status: 'instock' + }; + + const payloadString = JSON.stringify(payload); + const signature = generateTestSignature(payloadString, TEST_SECRET); + + const response = await request(app) + .post('/webhooks/woocommerce') + .set('Content-Type', 'application/json') + .set('X-WC-Webhook-Topic', 'product.updated') + .set('X-WC-Webhook-Signature', signature) + .set('X-WC-Webhook-Source', 'https://example.com') + .send(payloadString); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ received: true }); + }); + + test('should reject webhook with invalid signature', async () => { + const payload = { + id: 123, + status: 'processing' + }; + + const response = await request(app) + .post('/webhooks/woocommerce') + .set('Content-Type', 'application/json') + .set('X-WC-Webhook-Topic', 'order.created') + .set('X-WC-Webhook-Signature', 'invalid_signature') + .set('X-WC-Webhook-Source', 'https://example.com') + .send(JSON.stringify(payload)); + + expect(response.status).toBe(400); + expect(response.body).toEqual({ error: 'Invalid signature' }); + }); + + test('should reject webhook without signature header', async () => { + const payload = { + id: 123, + status: 'processing' + }; + + const response = await request(app) + .post('/webhooks/woocommerce') + .set('Content-Type', 'application/json') + .set('X-WC-Webhook-Topic', 'order.created') + .set('X-WC-Webhook-Source', 'https://example.com') + .send(JSON.stringify(payload)); + + expect(response.status).toBe(400); + expect(response.body).toEqual({ error: 'Invalid signature' }); + }); + + test('should handle malformed JSON gracefully', async () => { + const invalidPayload = '{"id": 123, "status":}'; // Invalid JSON + const signature = generateTestSignature(invalidPayload, TEST_SECRET); + + const response = await request(app) + .post('/webhooks/woocommerce') + .set('Content-Type', 'application/json') + .set('X-WC-Webhook-Topic', 'order.created') + .set('X-WC-Webhook-Signature', signature) + .set('X-WC-Webhook-Source', 'https://example.com') + .send(invalidPayload); + + expect(response.status).toBe(500); + expect(response.body).toEqual({ error: 'Internal server error' }); + }); + }); + + describe('Health Check', () => { + test('should return healthy status', async () => { + const response = await request(app) + .get('/health'); + + expect(response.status).toBe(200); + expect(response.body.status).toBe('healthy'); + expect(response.body.timestamp).toBeDefined(); + }); + }); +}); \ No newline at end of file diff --git a/skills/woocommerce-webhooks/examples/fastapi/.env.example b/skills/woocommerce-webhooks/examples/fastapi/.env.example new file mode 100644 index 0000000..7af6dec --- /dev/null +++ b/skills/woocommerce-webhooks/examples/fastapi/.env.example @@ -0,0 +1 @@ +WOOCOMMERCE_WEBHOOK_SECRET=your_webhook_secret_key_here \ No newline at end of file diff --git a/skills/woocommerce-webhooks/examples/fastapi/README.md b/skills/woocommerce-webhooks/examples/fastapi/README.md new file mode 100644 index 0000000..a1ce78b --- /dev/null +++ b/skills/woocommerce-webhooks/examples/fastapi/README.md @@ -0,0 +1,69 @@ +# WooCommerce Webhooks - FastAPI Example + +Minimal example of receiving WooCommerce webhooks with signature verification using FastAPI. + +## Prerequisites + +- Python 3.9+ +- WooCommerce store with webhook signing secret + +## Setup + +1. Create a virtual environment: + ```bash + python3 -m venv venv + source venv/bin/activate # On Windows: venv\Scripts\activate + ``` + +2. Install dependencies: + ```bash + pip install -r requirements.txt + ``` + +3. Copy environment variables: + ```bash + cp .env.example .env + ``` + +4. Add your WooCommerce webhook secret to `.env`: + ``` + WOOCOMMERCE_WEBHOOK_SECRET=your_webhook_secret_from_woocommerce + ``` + +## Run + +Development mode: +```bash +uvicorn main:app --reload +``` + +Production mode: +```bash +uvicorn main:app --host 0.0.0.0 --port 8000 +``` + +Server runs on http://localhost:8000 + +## Test + +You can test with a sample payload: + +```bash +curl -X POST http://localhost:8000/webhooks/woocommerce \ + -H "Content-Type: application/json" \ + -H "X-WC-Webhook-Topic: order.created" \ + -H "X-WC-Webhook-Signature: your_generated_signature" \ + -d '{"id": 123, "status": "processing", "total": "29.99"}' +``` + +Run tests: +```bash +pytest test_webhook.py -v +``` + +## WooCommerce Setup + +In your WooCommerce admin: +1. Go to **WooCommerce > Settings > Advanced > Webhooks** +2. Add webhook with delivery URL: `http://localhost:8000/webhooks/woocommerce` +3. Copy the webhook secret to your `.env` file \ No newline at end of file diff --git a/skills/woocommerce-webhooks/examples/fastapi/main.py b/skills/woocommerce-webhooks/examples/fastapi/main.py new file mode 100644 index 0000000..6700202 --- /dev/null +++ b/skills/woocommerce-webhooks/examples/fastapi/main.py @@ -0,0 +1,131 @@ +import os +import hmac +import hashlib +import base64 +from typing import Any, Dict, Optional +from dotenv import load_dotenv +from fastapi import FastAPI, Request, HTTPException + +# Load environment variables +load_dotenv() + +app = FastAPI(title="WooCommerce Webhook Handler", version="1.0.0") + +def verify_woocommerce_webhook(raw_body: bytes, signature: Optional[str], secret: Optional[str]) -> bool: + """ + Verify WooCommerce webhook signature using HMAC SHA-256 + + Args: + raw_body: Raw request body as bytes + signature: X-WC-Webhook-Signature header value + secret: Webhook secret from WooCommerce + + Returns: + True if signature is valid + """ + if not signature or not secret or not raw_body: + return False + + # Generate expected signature + hash_digest = hmac.new( + secret.encode('utf-8'), + raw_body, + hashlib.sha256 + ).digest() + + expected_signature = base64.b64encode(hash_digest).decode('utf-8') + + # Use timing-safe comparison to prevent timing attacks + return hmac.compare_digest(signature, expected_signature) + +def handle_woocommerce_event(topic: str, payload: Dict[str, Any]) -> None: + """ + Handle different WooCommerce event types + + Args: + topic: Event topic (e.g., "order.created") + payload: Webhook payload + """ + print(f"Processing {topic} event for ID: {payload.get('id')}") + + if topic == 'order.created': + print(f"New order #{payload.get('id')} for ${payload.get('total')}") + # Add your order processing logic here + + elif topic == 'order.updated': + print(f"Order #{payload.get('id')} updated to status: {payload.get('status')}") + # Add your order update logic here + + elif topic == 'product.created': + print(f"New product: {payload.get('name')} (ID: {payload.get('id')})") + # Add your product sync logic here + + elif topic == 'product.updated': + print(f"Product updated: {payload.get('name')} (ID: {payload.get('id')})") + # Add your product update logic here + + elif topic == 'customer.created': + print(f"New customer: {payload.get('email')} (ID: {payload.get('id')})") + # Add your customer onboarding logic here + + elif topic == 'customer.updated': + print(f"Customer updated: {payload.get('email')} (ID: {payload.get('id')})") + # Add your customer update logic here + + else: + print(f"Unhandled event type: {topic}") + +@app.post("/webhooks/woocommerce") +async def handle_webhook(request: Request): + """ + WooCommerce webhook endpoint + """ + try: + # Get headers + signature = request.headers.get('x-wc-webhook-signature') + topic = request.headers.get('x-wc-webhook-topic') + source = request.headers.get('x-wc-webhook-source') + secret = os.getenv('WOOCOMMERCE_WEBHOOK_SECRET') + + print(f"Received webhook: {topic} from {source}") + + # Get raw body for signature verification + raw_body = await request.body() + + # Verify webhook signature + if not verify_woocommerce_webhook(raw_body, signature, secret): + print("❌ Invalid webhook signature") + raise HTTPException(status_code=400, detail="Invalid signature") + + print("✅ Signature verified") + + # Parse the JSON payload + payload = await request.json() + + # Handle the event + if topic: + handle_woocommerce_event(topic, payload) + + # Respond with success + return {"received": True} + + except HTTPException: + # Re-raise HTTP exceptions (like 400 for invalid signature) + raise + except Exception as e: + print(f"Error processing webhook: {e}") + raise HTTPException(status_code=500, detail="Internal server error") + +@app.get("/health") +async def health_check(): + """ + Health check endpoint + """ + return { + "status": "healthy", + "timestamp": "2024-01-15T10:30:00Z" + } + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/skills/woocommerce-webhooks/examples/fastapi/requirements.txt b/skills/woocommerce-webhooks/examples/fastapi/requirements.txt new file mode 100644 index 0000000..44107fc --- /dev/null +++ b/skills/woocommerce-webhooks/examples/fastapi/requirements.txt @@ -0,0 +1,4 @@ +fastapi>=0.128.1 +python-dotenv>=1.0.0 +pytest>=9.0.2 +httpx>=0.28.1 \ No newline at end of file diff --git a/skills/woocommerce-webhooks/examples/fastapi/test_webhook.py b/skills/woocommerce-webhooks/examples/fastapi/test_webhook.py new file mode 100644 index 0000000..69e1fe0 --- /dev/null +++ b/skills/woocommerce-webhooks/examples/fastapi/test_webhook.py @@ -0,0 +1,210 @@ +import pytest +import os +import json +import hmac +import hashlib +import base64 +from fastapi.testclient import TestClient +from main import app, verify_woocommerce_webhook + +# Test webhook secret +TEST_SECRET = 'test_woocommerce_secret_key' + +# Set test environment variable +os.environ['WOOCOMMERCE_WEBHOOK_SECRET'] = TEST_SECRET + +client = TestClient(app) + +def generate_test_signature(payload: str, secret: str) -> str: + """ + Generate a valid WooCommerce webhook signature for testing + + Args: + payload: JSON payload as string + secret: Webhook secret + + Returns: + Base64 encoded signature + """ + hash_digest = hmac.new( + secret.encode('utf-8'), + payload.encode('utf-8'), + hashlib.sha256 + ).digest() + return base64.b64encode(hash_digest).decode('utf-8') + +class TestSignatureVerification: + def test_should_verify_valid_signatures(self): + payload = '{"id": 123, "status": "processing"}' + signature = generate_test_signature(payload, TEST_SECRET) + + is_valid = verify_woocommerce_webhook( + payload.encode('utf-8'), + signature, + TEST_SECRET + ) + + assert is_valid is True + + def test_should_reject_invalid_signatures(self): + payload = '{"id": 123, "status": "processing"}' + invalid_signature = 'invalid_signature' + + is_valid = verify_woocommerce_webhook( + payload.encode('utf-8'), + invalid_signature, + TEST_SECRET + ) + + assert is_valid is False + + def test_should_reject_missing_signature(self): + payload = '{"id": 123, "status": "processing"}' + + is_valid = verify_woocommerce_webhook( + payload.encode('utf-8'), + None, + TEST_SECRET + ) + + assert is_valid is False + + def test_should_reject_missing_secret(self): + payload = '{"id": 123, "status": "processing"}' + signature = generate_test_signature(payload, TEST_SECRET) + + is_valid = verify_woocommerce_webhook( + payload.encode('utf-8'), + signature, + None + ) + + assert is_valid is False + + def test_should_handle_different_payload_lengths(self): + payloads = [ + '{}', + '{"id":1}', + '{"id": 123, "status": "processing", "total": "29.99", "customer": {"name": "John Doe"}}' + ] + + for payload in payloads: + signature = generate_test_signature(payload, TEST_SECRET) + is_valid = verify_woocommerce_webhook( + payload.encode('utf-8'), + signature, + TEST_SECRET + ) + assert is_valid is True + +class TestWebhookEndpoint: + def test_should_accept_valid_order_created_webhook(self): + payload = { + "id": 123, + "status": "processing", + "total": "29.99", + "currency": "USD", + "billing": { + "first_name": "John", + "last_name": "Doe", + "email": "john@example.com" + } + } + + # Convert to JSON string for signature generation + payload_string = json.dumps(payload, separators=(',', ':')) + signature = generate_test_signature(payload_string, TEST_SECRET) + + # Send the raw JSON string, not using json= parameter + response = client.post( + "/webhooks/woocommerce", + content=payload_string, + headers={ + "Content-Type": "application/json", + "X-WC-Webhook-Topic": "order.created", + "X-WC-Webhook-Signature": signature, + "X-WC-Webhook-Source": "https://example.com" + } + ) + + assert response.status_code == 200 + assert response.json() == {"received": True} + + def test_should_accept_valid_product_updated_webhook(self): + payload = { + "id": 456, + "name": "Premium T-Shirt", + "status": "publish", + "regular_price": "29.99", + "stock_status": "instock" + } + + # Convert to JSON string for signature generation + payload_string = json.dumps(payload, separators=(',', ':')) + signature = generate_test_signature(payload_string, TEST_SECRET) + + # Send the raw JSON string, not using json= parameter + response = client.post( + "/webhooks/woocommerce", + content=payload_string, + headers={ + "Content-Type": "application/json", + "X-WC-Webhook-Topic": "product.updated", + "X-WC-Webhook-Signature": signature, + "X-WC-Webhook-Source": "https://example.com" + } + ) + + assert response.status_code == 200 + assert response.json() == {"received": True} + + def test_should_reject_webhook_with_invalid_signature(self): + payload = { + "id": 123, + "status": "processing" + } + + payload_string = json.dumps(payload, separators=(',', ':')) + + response = client.post( + "/webhooks/woocommerce", + content=payload_string, + headers={ + "Content-Type": "application/json", + "X-WC-Webhook-Topic": "order.created", + "X-WC-Webhook-Signature": "invalid_signature", + "X-WC-Webhook-Source": "https://example.com" + } + ) + + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid signature"} + + def test_should_reject_webhook_without_signature_header(self): + payload = { + "id": 123, + "status": "processing" + } + + payload_string = json.dumps(payload, separators=(',', ':')) + + response = client.post( + "/webhooks/woocommerce", + content=payload_string, + headers={ + "Content-Type": "application/json", + "X-WC-Webhook-Topic": "order.created", + "X-WC-Webhook-Source": "https://example.com" + } + ) + + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid signature"} + +class TestHealthCheck: + def test_should_return_healthy_status(self): + response = client.get("/health") + + assert response.status_code == 200 + assert response.json()["status"] == "healthy" + assert "timestamp" in response.json() \ No newline at end of file diff --git a/skills/woocommerce-webhooks/examples/nextjs/.env.example b/skills/woocommerce-webhooks/examples/nextjs/.env.example new file mode 100644 index 0000000..7af6dec --- /dev/null +++ b/skills/woocommerce-webhooks/examples/nextjs/.env.example @@ -0,0 +1 @@ +WOOCOMMERCE_WEBHOOK_SECRET=your_webhook_secret_key_here \ No newline at end of file diff --git a/skills/woocommerce-webhooks/examples/nextjs/README.md b/skills/woocommerce-webhooks/examples/nextjs/README.md new file mode 100644 index 0000000..79ed8e1 --- /dev/null +++ b/skills/woocommerce-webhooks/examples/nextjs/README.md @@ -0,0 +1,59 @@ +# WooCommerce Webhooks - Next.js Example + +Minimal example of receiving WooCommerce webhooks with signature verification using Next.js App Router. + +## Prerequisites + +- Node.js 18+ +- WooCommerce store with webhook signing secret + +## Setup + +1. Install dependencies: + ```bash + npm install + ``` + +2. Copy environment variables: + ```bash + cp .env.example .env.local + ``` + +3. Add your WooCommerce webhook secret to `.env.local`: + ``` + WOOCOMMERCE_WEBHOOK_SECRET=your_webhook_secret_from_woocommerce + ``` + +## Run + +Development mode: +```bash +npm run dev +``` + +Production build: +```bash +npm run build +npm start +``` + +Server runs on http://localhost:3000 + +## Test + +You can test with a sample payload: + +```bash +curl -X POST http://localhost:3000/webhooks/woocommerce \ + -H "Content-Type: application/json" \ + -H "X-WC-Webhook-Topic: order.created" \ + -H "X-WC-Webhook-Signature: your_generated_signature" \ + -d '{"id": 123, "status": "processing", "total": "29.99"}' +``` + +## WooCommerce Setup + +In your WooCommerce admin: +1. Go to **WooCommerce > Settings > Advanced > Webhooks** +2. Add webhook with delivery URL: `http://localhost:3000/webhooks/woocommerce` +3. Copy the webhook secret to your `.env.local` file \ No newline at end of file diff --git a/skills/woocommerce-webhooks/examples/nextjs/app/webhooks/woocommerce/route.ts b/skills/woocommerce-webhooks/examples/nextjs/app/webhooks/woocommerce/route.ts new file mode 100644 index 0000000..3cbf2c1 --- /dev/null +++ b/skills/woocommerce-webhooks/examples/nextjs/app/webhooks/woocommerce/route.ts @@ -0,0 +1,123 @@ +import crypto from 'crypto'; +import { NextRequest } from 'next/server'; + +/** + * Verify WooCommerce webhook signature using HMAC SHA-256 + * @param rawBody - Raw request body as string + * @param signature - X-WC-Webhook-Signature header value + * @param secret - Webhook secret from WooCommerce + * @returns True if signature is valid + */ +function verifyWooCommerceWebhook(rawBody: string, signature: string | null, secret: string | undefined): boolean { + if (!signature || !secret || !rawBody) { + return false; + } + + const expectedSignature = crypto + .createHmac('sha256', secret) + .update(rawBody) + .digest('base64'); + + try { + return crypto.timingSafeEqual( + Buffer.from(signature), + Buffer.from(expectedSignature) + ); + } catch (error) { + // Different lengths will cause an error + return false; + } +} + +/** + * Handle different WooCommerce event types + * @param topic - Event topic (e.g., "order.created") + * @param payload - Webhook payload + */ +function handleWooCommerceEvent(topic: string, payload: any) { + console.log(`Processing ${topic} event for ID: ${payload.id}`); + + switch (topic) { + case 'order.created': + console.log(`New order #${payload.id} for $${payload.total}`); + // Add your order processing logic here + break; + + case 'order.updated': + console.log(`Order #${payload.id} updated to status: ${payload.status}`); + // Add your order update logic here + break; + + case 'product.created': + console.log(`New product: ${payload.name} (ID: ${payload.id})`); + // Add your product sync logic here + break; + + case 'product.updated': + console.log(`Product updated: ${payload.name} (ID: ${payload.id})`); + // Add your product update logic here + break; + + case 'customer.created': + console.log(`New customer: ${payload.email} (ID: ${payload.id})`); + // Add your customer onboarding logic here + break; + + case 'customer.updated': + console.log(`Customer updated: ${payload.email} (ID: ${payload.id})`); + // Add your customer update logic here + break; + + default: + console.log(`Unhandled event type: ${topic}`); + } +} + +export async function POST(request: NextRequest) { + try { + const signature = request.headers.get('x-wc-webhook-signature'); + const topic = request.headers.get('x-wc-webhook-topic'); + const source = request.headers.get('x-wc-webhook-source'); + const secret = process.env.WOOCOMMERCE_WEBHOOK_SECRET; + + console.log(`Received webhook: ${topic} from ${source}`); + + // Get raw body for signature verification + const rawBody = await request.text(); + + // Verify webhook signature + if (!verifyWooCommerceWebhook(rawBody, signature, secret)) { + console.log('❌ Invalid webhook signature'); + return new Response(JSON.stringify({ error: 'Invalid signature' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + } + + console.log('✅ Signature verified'); + + // Parse the JSON payload + const payload = JSON.parse(rawBody); + + // Handle the event + if (topic) { + handleWooCommerceEvent(topic, payload); + } + + // Respond with success + return new Response(JSON.stringify({ received: true }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + + } catch (error) { + console.error('Error processing webhook:', error); + return new Response(JSON.stringify({ error: 'Internal server error' }), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }); + } +} + +// Export the verification function for testing +export { verifyWooCommerceWebhook }; \ No newline at end of file diff --git a/skills/woocommerce-webhooks/examples/nextjs/package.json b/skills/woocommerce-webhooks/examples/nextjs/package.json new file mode 100644 index 0000000..e3258f8 --- /dev/null +++ b/skills/woocommerce-webhooks/examples/nextjs/package.json @@ -0,0 +1,24 @@ +{ + "name": "woocommerce-webhook-nextjs", + "version": "1.0.0", + "description": "WooCommerce webhook handler using Next.js App Router", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "next": "^16.1.6", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "typescript": "^5.9.3", + "@types/node": "^22.10.7", + "@types/react": "^19.0.13", + "@types/react-dom": "^19.0.2", + "vitest": "^4.0.18" + } +} \ No newline at end of file diff --git a/skills/woocommerce-webhooks/examples/nextjs/test/webhook.test.ts b/skills/woocommerce-webhooks/examples/nextjs/test/webhook.test.ts new file mode 100644 index 0000000..4b4f3d0 --- /dev/null +++ b/skills/woocommerce-webhooks/examples/nextjs/test/webhook.test.ts @@ -0,0 +1,214 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import crypto from 'crypto'; + +// Test webhook secret +const TEST_SECRET = 'test_woocommerce_secret_key'; + +// Set test environment variable +beforeAll(() => { + process.env.WOOCOMMERCE_WEBHOOK_SECRET = TEST_SECRET; +}); + +/** + * Generate a valid WooCommerce webhook signature for testing + * @param payload - JSON payload as string + * @param secret - Webhook secret + * @returns Base64 encoded signature + */ +function generateTestSignature(payload: string, secret: string): string { + return crypto + .createHmac('sha256', secret) + .update(payload) + .digest('base64'); +} + +// Import the handler after setting environment variables +const { POST, verifyWooCommerceWebhook } = await import('../app/webhooks/woocommerce/route'); + +describe('WooCommerce Webhook Handler', () => { + describe('Signature Verification', () => { + test('should verify valid signatures', () => { + const payload = '{"id": 123, "status": "processing"}'; + const signature = generateTestSignature(payload, TEST_SECRET); + + const isValid = verifyWooCommerceWebhook(payload, signature, TEST_SECRET); + + expect(isValid).toBe(true); + }); + + test('should reject invalid signatures', () => { + const payload = '{"id": 123, "status": "processing"}'; + const invalidSignature = 'invalid_signature'; + + const isValid = verifyWooCommerceWebhook(payload, invalidSignature, TEST_SECRET); + + expect(isValid).toBe(false); + }); + + test('should reject missing signature', () => { + const payload = '{"id": 123, "status": "processing"}'; + + const isValid = verifyWooCommerceWebhook(payload, null, TEST_SECRET); + + expect(isValid).toBe(false); + }); + + test('should reject missing secret', () => { + const payload = '{"id": 123, "status": "processing"}'; + const signature = generateTestSignature(payload, TEST_SECRET); + + const isValid = verifyWooCommerceWebhook(payload, signature, undefined); + + expect(isValid).toBe(false); + }); + + test('should handle different payload lengths', () => { + const payloads = [ + '{}', + '{"id":1}', + '{"id": 123, "status": "processing", "total": "29.99", "customer": {"name": "John Doe"}}' + ]; + + payloads.forEach(payload => { + const signature = generateTestSignature(payload, TEST_SECRET); + const isValid = verifyWooCommerceWebhook(payload, signature, TEST_SECRET); + expect(isValid).toBe(true); + }); + }); + }); + + describe('Webhook Endpoint', () => { + test('should accept valid order.created webhook', async () => { + const payload = { + id: 123, + status: 'processing', + total: '29.99', + currency: 'USD', + billing: { + first_name: 'John', + last_name: 'Doe', + email: 'john@example.com' + } + }; + + const payloadString = JSON.stringify(payload); + const signature = generateTestSignature(payloadString, TEST_SECRET); + + const request = new Request('http://localhost:3000/webhooks/woocommerce', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-WC-Webhook-Topic': 'order.created', + 'X-WC-Webhook-Signature': signature, + 'X-WC-Webhook-Source': 'https://example.com', + }, + body: payloadString, + }); + + const response = await POST(request as any); + + expect(response.status).toBe(200); + const body = await response.json(); + expect(body).toEqual({ received: true }); + }); + + test('should accept valid product.updated webhook', async () => { + const payload = { + id: 456, + name: 'Premium T-Shirt', + status: 'publish', + regular_price: '29.99', + stock_status: 'instock' + }; + + const payloadString = JSON.stringify(payload); + const signature = generateTestSignature(payloadString, TEST_SECRET); + + const request = new Request('http://localhost:3000/webhooks/woocommerce', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-WC-Webhook-Topic': 'product.updated', + 'X-WC-Webhook-Signature': signature, + 'X-WC-Webhook-Source': 'https://example.com', + }, + body: payloadString, + }); + + const response = await POST(request as any); + + expect(response.status).toBe(200); + const body = await response.json(); + expect(body).toEqual({ received: true }); + }); + + test('should reject webhook with invalid signature', async () => { + const payload = { + id: 123, + status: 'processing' + }; + + const request = new Request('http://localhost:3000/webhooks/woocommerce', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-WC-Webhook-Topic': 'order.created', + 'X-WC-Webhook-Signature': 'invalid_signature', + 'X-WC-Webhook-Source': 'https://example.com', + }, + body: JSON.stringify(payload), + }); + + const response = await POST(request as any); + + expect(response.status).toBe(400); + const body = await response.json(); + expect(body).toEqual({ error: 'Invalid signature' }); + }); + + test('should reject webhook without signature header', async () => { + const payload = { + id: 123, + status: 'processing' + }; + + const request = new Request('http://localhost:3000/webhooks/woocommerce', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-WC-Webhook-Topic': 'order.created', + 'X-WC-Webhook-Source': 'https://example.com', + }, + body: JSON.stringify(payload), + }); + + const response = await POST(request as any); + + expect(response.status).toBe(400); + const body = await response.json(); + expect(body).toEqual({ error: 'Invalid signature' }); + }); + + test('should handle malformed JSON gracefully', async () => { + const invalidPayload = '{"id": 123, "status":}'; // Invalid JSON + const signature = generateTestSignature(invalidPayload, TEST_SECRET); + + const request = new Request('http://localhost:3000/webhooks/woocommerce', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-WC-Webhook-Topic': 'order.created', + 'X-WC-Webhook-Signature': signature, + 'X-WC-Webhook-Source': 'https://example.com', + }, + body: invalidPayload, + }); + + const response = await POST(request as any); + + expect(response.status).toBe(500); + const body = await response.json(); + expect(body).toEqual({ error: 'Internal server error' }); + }); + }); +}); \ No newline at end of file diff --git a/skills/woocommerce-webhooks/examples/nextjs/vitest.config.ts b/skills/woocommerce-webhooks/examples/nextjs/vitest.config.ts new file mode 100644 index 0000000..7054f48 --- /dev/null +++ b/skills/woocommerce-webhooks/examples/nextjs/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + }, +}); \ No newline at end of file diff --git a/skills/woocommerce-webhooks/references/overview.md b/skills/woocommerce-webhooks/references/overview.md new file mode 100644 index 0000000..a61cac2 --- /dev/null +++ b/skills/woocommerce-webhooks/references/overview.md @@ -0,0 +1,125 @@ +# WooCommerce Webhooks Overview + +## What Are WooCommerce Webhooks? + +WooCommerce webhooks are HTTP callbacks that fire when specific events happen in your WooCommerce store. When an order is created, a product is updated, or a customer registers, WooCommerce can automatically send a POST request to your application with the event details. + +This enables real-time integration between your WooCommerce store and external systems like: + +- CRM systems (customer data sync) +- Email marketing platforms (order confirmations, abandoned carts) +- Inventory management (stock updates) +- Analytics platforms (sales tracking) +- Fulfillment services (order processing) + +## Common Event Types + +| Event | Triggered When | Common Use Cases | +|-------|----------------|------------------| +| `order.created` | New order is placed | Send order confirmation emails, create shipping labels, update inventory | +| `order.updated` | Order status or details change | Track order fulfillment, send status updates to customers | +| `order.deleted` | Order is permanently deleted | Clean up external records, reverse inventory changes | +| `order.restored` | Deleted order is restored | Restore external records, reapply inventory changes | +| `product.created` | New product is added | Sync to external catalogs, trigger marketing campaigns | +| `product.updated` | Product details change | Update pricing feeds, sync inventory levels | +| `product.deleted` | Product is permanently deleted | Remove from external catalogs, update recommendations | +| `product.restored` | Deleted product is restored | Restore to external catalogs | +| `customer.created` | New customer account registered | Send welcome emails, add to CRM, create loyalty profiles | +| `customer.updated` | Customer profile changes | Update CRM records, sync preferences | +| `customer.deleted` | Customer account deleted | Clean up external profiles, handle GDPR deletion | +| `coupon.created` | New coupon created | Sync to marketing platforms | +| `coupon.updated` | Coupon terms modified | Update promotional campaigns | +| `coupon.deleted` | Coupon removed | End promotional campaigns | + +## Event Payload Structure + +All WooCommerce webhooks share common payload elements: + +```json +{ + "id": 123, + "date_created": "2024-01-15T10:30:00", + "date_modified": "2024-01-15T10:30:00", + "status": "processing", + // ... event-specific fields +} +``` + +### Order Events + +Order webhooks include customer details, line items, totals, and shipping information: + +```json +{ + "id": 456, + "status": "processing", + "currency": "USD", + "total": "29.99", + "billing": { + "first_name": "John", + "last_name": "Doe", + "email": "john@example.com" + }, + "line_items": [ + { + "id": 789, + "name": "T-Shirt", + "quantity": 1, + "price": 29.99 + } + ] +} +``` + +### Product Events + +Product webhooks include details like name, price, stock status, and categories: + +```json +{ + "id": 101, + "name": "Premium T-Shirt", + "status": "publish", + "regular_price": "29.99", + "stock_status": "instock", + "manage_stock": true, + "stock_quantity": 50 +} +``` + +### Customer Events + +Customer webhooks include profile information and preferences: + +```json +{ + "id": 202, + "email": "customer@example.com", + "first_name": "Jane", + "last_name": "Smith", + "username": "jane_smith", + "billing": { + "first_name": "Jane", + "last_name": "Smith", + "company": "Example Corp" + } +} +``` + +## Webhook Headers + +Every WooCommerce webhook includes these important headers: + +- **X-WC-Webhook-Topic** - The event type (e.g., "order.created") +- **X-WC-Webhook-Resource** - The resource type (e.g., "order") +- **X-WC-Webhook-Event** - The action (e.g., "created") +- **X-WC-Webhook-Signature** - HMAC SHA256 signature for verification +- **X-WC-Webhook-Source** - The store URL that sent the webhook +- **X-WC-Webhook-ID** - The webhook configuration ID +- **X-WC-Webhook-Delivery-ID** - Unique identifier for this delivery attempt + +## Full Event Reference + +For the complete list of events and their payloads, see [WooCommerce's webhook documentation](https://woocommerce.com/document/webhooks/). + +The WooCommerce REST API documentation also provides detailed payload schemas for each resource type. \ No newline at end of file diff --git a/skills/woocommerce-webhooks/references/setup.md b/skills/woocommerce-webhooks/references/setup.md new file mode 100644 index 0000000..f4ce24b --- /dev/null +++ b/skills/woocommerce-webhooks/references/setup.md @@ -0,0 +1,110 @@ +# Setting Up WooCommerce Webhooks + +## Prerequisites + +- WordPress site with WooCommerce plugin installed +- Admin access to WooCommerce settings +- Your webhook endpoint URL (where you want to receive webhooks) +- HTTPS endpoint recommended for production (required for sensitive events) + +## Get Your Webhook Secret + +The webhook secret is used to generate signatures for verifying webhook authenticity. WooCommerce generates this automatically when you create a webhook, but you can also set a custom one. + +1. Go to **WooCommerce > Settings > Advanced > Webhooks** +2. Click **Add webhook** +3. The **Secret** field will auto-populate with a secure random string +4. **Copy and save this secret** - you'll need it for signature verification +5. Alternatively, you can enter your own custom secret + +## Register Your Webhook Endpoint + +1. In the **Webhook data** form, fill in: + - **Name**: Descriptive name (e.g., "Order Processing Webhook") + - **Status**: Set to **Active** + - **Topic**: Select the event to listen for (see options below) + - **Delivery URL**: Your endpoint URL (e.g., `https://yourapp.com/webhooks/woocommerce`) + - **Secret**: Use the auto-generated secret or enter your own + +2. **Topic Options**: + - **Order created** - `order.created` + - **Order updated** - `order.updated` + - **Order deleted** - `order.deleted` + - **Order restored** - `order.restored` + - **Product created** - `product.created` + - **Product updated** - `product.updated` + - **Product deleted** - `product.deleted` + - **Product restored** - `product.restored` + - **Customer created** - `customer.created` + - **Customer updated** - `customer.updated` + - **Customer deleted** - `customer.deleted` + - **Coupon created** - `coupon.created` + - **Coupon updated** - `coupon.updated` + - **Coupon deleted** - `coupon.deleted` + - **Action** - Custom action hooks (advanced users) + +3. Click **Save Webhook** + +## Multiple Webhooks + +You can create multiple webhooks for different events or endpoints: + +- One webhook for order events → order processing system +- Another webhook for customer events → CRM system +- Third webhook for product events → inventory management + +Each webhook can have its own delivery URL and secret. + +## Test Your Webhook + +After saving, WooCommerce automatically sends a test ping to your endpoint. Check your webhook logs to confirm it was received. + +You can also trigger test events: +- Create a test order (for order webhooks) +- Add a test product (for product webhooks) +- Register a test customer (for customer webhooks) + +## Webhook Management + +### View Webhook Status + +In **WooCommerce > Settings > Advanced > Webhooks**, you can see: +- **Status**: Active, Paused, or Disabled +- **Pending deliveries**: Number of failed deliveries waiting for retry +- **Last delivery**: Timestamp of most recent delivery attempt + +### Automatic Disabling + +WooCommerce automatically disables webhooks after 5 consecutive delivery failures. A failure is any response that's not: +- 2xx (success) +- 301 (moved permanently) +- 302 (found/temporary redirect) + +### View Webhook Logs + +Check delivery logs at **WooCommerce > Status > Logs**: +1. Select **webhook-delivery** from the "All sources" dropdown +2. Choose a log file to view delivery details and responses +3. Use this for debugging connection issues or response errors + +## Security Considerations + +1. **Use HTTPS**: Always use HTTPS endpoints in production +2. **Verify Signatures**: Always validate the `X-WC-Webhook-Signature` header +3. **Keep Secrets Secure**: Store webhook secrets in environment variables +4. **IP Allowlisting**: Consider restricting access to your store's IP range +5. **Rate Limiting**: Implement rate limiting on your webhook endpoints + +## Custom Topics (Advanced) + +For advanced users, you can create custom webhook topics using the `woocommerce_webhook_topic_hooks` filter: + +```php +// In your theme's functions.php or plugin +add_filter('woocommerce_webhook_topic_hooks', function($topic_hooks) { + $topic_hooks['cart.updated'] = ['woocommerce_add_to_cart']; + return $topic_hooks; +}); +``` + +This creates a custom "cart.updated" topic that triggers when items are added to cart. \ No newline at end of file diff --git a/skills/woocommerce-webhooks/references/verification.md b/skills/woocommerce-webhooks/references/verification.md new file mode 100644 index 0000000..fd17f44 --- /dev/null +++ b/skills/woocommerce-webhooks/references/verification.md @@ -0,0 +1,260 @@ +# WooCommerce Signature Verification + +## How It Works + +WooCommerce signs every webhook request using HMAC SHA-256. The signature is included in the `X-WC-Webhook-Signature` header as a base64-encoded string. + +The signature is computed as: +``` +HMAC-SHA256(raw_request_body, webhook_secret) → base64 encoded +``` + +## Implementation + +### Node.js + +```javascript +const crypto = require('crypto'); + +function verifyWooCommerceWebhook(rawBody, signature, secret) { + if (!signature || !secret) return false; + + const expectedSignature = crypto + .createHmac('sha256', secret) + .update(rawBody) + .digest('base64'); + + // Use timing-safe comparison to prevent timing attacks + try { + return crypto.timingSafeEqual( + Buffer.from(signature), + Buffer.from(expectedSignature) + ); + } catch (error) { + // Different lengths will throw an error + return false; + } +} + +// Usage example +const isValid = verifyWooCommerceWebhook( + req.body, // Raw body as Buffer + req.headers['x-wc-webhook-signature'], // Signature header + process.env.WOOCOMMERCE_WEBHOOK_SECRET // Your secret +); +``` + +### Python + +```python +import hmac +import hashlib +import base64 + +def verify_woocommerce_webhook(raw_body: bytes, signature: str, secret: str) -> bool: + if not signature or not secret: + return False + + # Generate expected signature + hash_digest = hmac.new( + secret.encode('utf-8'), + raw_body, + hashlib.sha256 + ).digest() + + expected_signature = base64.b64encode(hash_digest).decode('utf-8') + + # Use timing-safe comparison + return hmac.compare_digest(signature, expected_signature) + +# Usage example +is_valid = verify_woocommerce_webhook( + request.body, # Raw body as bytes + request.headers.get('x-wc-webhook-signature'), # Signature header + os.environ['WOOCOMMERCE_WEBHOOK_SECRET'] # Your secret +) +``` + +### PHP (Reference Implementation) + +WooCommerce itself uses this verification method: + +```php +function verify_woocommerce_webhook($raw_body, $signature, $secret) { + if (empty($signature) || empty($secret)) { + return false; + } + + $expected_signature = base64_encode( + hash_hmac('sha256', $raw_body, $secret, true) + ); + + return hash_equals($signature, $expected_signature); +} + +// Usage example +$is_valid = verify_woocommerce_webhook( + file_get_contents('php://input'), // Raw body + $_SERVER['HTTP_X_WC_WEBHOOK_SIGNATURE'], // Signature header + $webhook_secret // Your secret +); +``` + +## Common Gotchas + +### 1. Use Raw Body, Not Parsed JSON + +**❌ Wrong:** +```javascript +// DON'T parse the body first +app.use(express.json()); +app.post('/webhook', (req, res) => { + const signature = req.headers['x-wc-webhook-signature']; + // req.body is now a JavaScript object, not raw bytes! + verifyWooCommerceWebhook(JSON.stringify(req.body), signature, secret); +}); +``` + +**✅ Correct:** +```javascript +// Use raw body for signature verification +app.use('/webhooks/woocommerce', express.raw({ type: 'application/json' })); +app.post('/webhooks/woocommerce', (req, res) => { + const signature = req.headers['x-wc-webhook-signature']; + // req.body is the raw Buffer + if (!verifyWooCommerceWebhook(req.body, signature, secret)) { + return res.status(400).send('Invalid signature'); + } + + // Parse JSON after verification + const payload = JSON.parse(req.body); +}); +``` + +### 2. Header Name Casing + +Different frameworks handle header names differently: + +```javascript +// These are equivalent: +req.headers['x-wc-webhook-signature'] +req.headers['X-WC-Webhook-Signature'] +req.get('X-WC-Webhook-Signature') + +// FastAPI/Python +request.headers.get('x-wc-webhook-signature') +request.headers.get('X-WC-Webhook-Signature') +``` + +Always use lowercase in your code for consistency. + +### 3. Buffer vs String Handling + +**Node.js:** +```javascript +// Express with express.raw() gives you a Buffer +if (Buffer.isBuffer(req.body)) { + // Use directly + verifyWooCommerceWebhook(req.body, signature, secret); +} else { + // Convert string to Buffer + verifyWooCommerceWebhook(Buffer.from(req.body), signature, secret); +} +``` + +**Python:** +```python +# FastAPI gives you bytes by default with request.body() +raw_body = await request.body() # This is bytes +verify_woocommerce_webhook(raw_body, signature, secret) + +# If you have a string, encode it +if isinstance(body, str): + body = body.encode('utf-8') +``` + +### 4. Missing or Empty Signatures + +Always check for missing signatures: + +```javascript +function verifyWooCommerceWebhook(rawBody, signature, secret) { + // Guard against missing values + if (!signature || !secret || !rawBody) { + return false; + } + + // Continue with verification... +} +``` + +### 5. Timing Attack Protection + +Always use timing-safe comparison functions: + +- **Node.js**: `crypto.timingSafeEqual()` +- **Python**: `hmac.compare_digest()` +- **PHP**: `hash_equals()` + +Never use `===` or `==` for signature comparison. + +## Debugging Verification Failures + +### 1. Log the Details + +```javascript +function debugWebhook(rawBody, signature, secret) { + console.log('Raw body length:', rawBody.length); + console.log('Signature received:', signature); + console.log('Secret (first 8 chars):', secret?.substring(0, 8)); + + const expectedSignature = crypto + .createHmac('sha256', secret) + .update(rawBody) + .digest('base64'); + + console.log('Expected signature:', expectedSignature); + console.log('Signatures match:', signature === expectedSignature); +} +``` + +### 2. Check WooCommerce Logs + +In WooCommerce admin: +1. Go to **WooCommerce > Status > Logs** +2. Select **webhook-delivery** logs +3. Look for HTTP response codes from your endpoint +4. 400 responses indicate signature verification failure + +### 3. Test with Known Data + +Create a test case with known values: + +```javascript +const testSecret = 'test_secret'; +const testBody = '{"id":123,"status":"processing"}'; +const expectedSignature = crypto + .createHmac('sha256', testSecret) + .update(testBody) + .digest('base64'); + +console.log('Test signature:', expectedSignature); +// Compare with what your verification function produces +``` + +### 4. Webhook Delivery Failures + +If WooCommerce shows "delivery failures": +- Check your endpoint is reachable +- Verify it returns 200 for valid signatures +- Check for timeouts (WooCommerce has a 60-second limit) +- Look at your server logs for errors + +## Security Best Practices + +1. **Always verify signatures** before processing webhook data +2. **Use HTTPS** in production to prevent man-in-the-middle attacks +3. **Store secrets securely** in environment variables, not in code +4. **Implement proper error handling** - return 4xx for bad requests, 5xx for server errors +5. **Log security events** - track failed verification attempts +6. **Rate limit** your webhook endpoints to prevent abuse \ No newline at end of file From d59a4626dc11c6311d70251e1d60e9cc6006c582 Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Thu, 5 Feb 2026 13:44:50 +0000 Subject: [PATCH 2/5] chore: add woocommerce integration (CI, README, test scenarios) Co-authored-by: Cursor --- .github/workflows/test-examples.yml | 3 +++ README.md | 1 + scripts/test-agent-scenario.sh | 7 +++++++ 3 files changed, 11 insertions(+) diff --git a/.github/workflows/test-examples.yml b/.github/workflows/test-examples.yml index cf467b1..f89ee7c 100644 --- a/.github/workflows/test-examples.yml +++ b/.github/workflows/test-examples.yml @@ -20,6 +20,7 @@ jobs: - resend-webhooks - deepgram-webhooks - fusionauth-webhooks + - woocommerce-webhooks - hookdeck-event-gateway steps: @@ -53,6 +54,7 @@ jobs: - resend-webhooks - deepgram-webhooks - fusionauth-webhooks + - woocommerce-webhooks - hookdeck-event-gateway steps: @@ -86,6 +88,7 @@ jobs: - resend-webhooks - deepgram-webhooks - fusionauth-webhooks + - woocommerce-webhooks - hookdeck-event-gateway steps: diff --git a/README.md b/README.md index 7942d3d..14d953f 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Skills for receiving and verifying webhooks from specific providers. Each includ | SendGrid | [`sendgrid-webhooks`](skills/sendgrid-webhooks/) | Verify SendGrid webhook signatures (ECDSA), handle email delivery events | | Shopify | [`shopify-webhooks`](skills/shopify-webhooks/) | Verify Shopify HMAC signatures, handle order and product webhook events | | Stripe | [`stripe-webhooks`](skills/stripe-webhooks/) | Verify Stripe webhook signatures, parse payment event payloads, handle checkout.session.completed events | +| WooCommerce | [`woocommerce-webhooks`](skills/woocommerce-webhooks/) | Verify WooCommerce webhook signatures, handle order, product, and customer events | ### Webhook Handler Pattern Skills diff --git a/scripts/test-agent-scenario.sh b/scripts/test-agent-scenario.sh index 2216776..7446438 100755 --- a/scripts/test-agent-scenario.sh +++ b/scripts/test-agent-scenario.sh @@ -34,6 +34,7 @@ usage() { echo " fusionauth-express - FusionAuth webhook handling in Express" echo " fusionauth-nextjs - FusionAuth webhook handling in Next.js" echo " fusionauth-fastapi - FusionAuth webhook handling in FastAPI" + echo " woocommerce-express - WooCommerce webhook handling in Express" echo " hookdeck-express - Hookdeck Event Gateway in Express" echo "" echo "Options:" @@ -125,6 +126,12 @@ get_scenario_config() { SKILL_NAME="fusionauth-webhooks" PROMPT="Add a FusionAuth webhook endpoint to my FastAPI app. I need to handle user.create and user.delete events. If you use any skills to help with this, add a comment in the code noting which skill(s) you referenced." ;; + woocommerce-express) + PROVIDER="woocommerce" + FRAMEWORK="express" + SKILL_NAME="woocommerce-webhooks" + PROMPT="Add WooCommerce webhook handling to my Express app. I want to handle order.created and product.updated events. If you use any skills to help with this, add a comment in the code noting which skill(s) you referenced." + ;; hookdeck-express) PROVIDER="hookdeck" FRAMEWORK="express" From 55d1ad83e2eae405a006d98b04a9f6adefa1c24a Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Thu, 5 Feb 2026 14:47:35 +0000 Subject: [PATCH 3/5] fix: add woocommerce to providers.yaml and fix validation script - Add woocommerce entry to providers.yaml with documentation URLs - Update validate-provider.sh to handle dynamic detection workflow The test-examples.yml now uses dynamic detection to auto-discover providers from the skills directory, so the validation script no longer needs to check for hardcoded provider names in the workflow. Co-authored-by: Cursor --- providers.yaml | 12 ++++++++++++ scripts/validate-provider.sh | 9 +++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/providers.yaml b/providers.yaml index e1ebdf1..46b68c8 100644 --- a/providers.yaml +++ b/providers.yaml @@ -139,3 +139,15 @@ providers: Signature format includes timestamp (t) and signature (v1). Secret starts with whsec_. Always use raw request body for verification. Common events: checkout.session.completed, payment_intent.succeeded, customer.subscription.created. + + - name: woocommerce + displayName: WooCommerce + docs: + webhooks: https://developer.woocommerce.com/docs/webhooks/ + events: https://developer.woocommerce.com/docs/webhooks/#webhook-topics + notes: > + E-commerce platform for WordPress. Uses X-WC-Webhook-Signature header with HMAC-SHA256 + (base64 encoded). Secret is configured per webhook in WooCommerce admin. Additional headers + include X-WC-Webhook-Topic, X-WC-Webhook-Resource, X-WC-Webhook-Event, X-WC-Webhook-Source, + X-WC-Webhook-ID, and X-WC-Webhook-Delivery-ID. Common events: order.created, order.updated, + product.created, customer.created. diff --git a/scripts/validate-provider.sh b/scripts/validate-provider.sh index 670ad79..0f7dad8 100755 --- a/scripts/validate-provider.sh +++ b/scripts/validate-provider.sh @@ -221,8 +221,13 @@ validate_integration() { errors+=("providers.yaml not found at repository root") fi - # Check test-examples.yml has provider in matrices - if ! grep -q "$provider" "$ROOT_DIR/.github/workflows/test-examples.yml"; then + # Check test-examples.yml has provider in matrices (or uses dynamic detection) + # If workflow uses dynamic detection (detect-changes job), skip hardcoded name check + if grep -q "detect-changes" "$ROOT_DIR/.github/workflows/test-examples.yml" || \ + grep -q "ls -d skills/\*-webhooks" "$ROOT_DIR/.github/workflows/test-examples.yml"; then + # Workflow uses dynamic detection - no hardcoded check needed + : + elif ! grep -q "$provider" "$ROOT_DIR/.github/workflows/test-examples.yml"; then errors+=("$provider not found in .github/workflows/test-examples.yml matrices") fi From 576211ee6b05fcdaaef33f7ba12d480d7cb23e19 Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Thu, 5 Feb 2026 18:33:19 +0000 Subject: [PATCH 4/5] fix: update validate-provider.sh for dynamic test scenarios test-agent-scenario.sh now reads scenarios from providers.yaml instead of hardcoding them. Update validation to check for testScenario in providers.yaml instead of grepping test-agent-scenario.sh. Co-authored-by: Cursor --- scripts/validate-provider.sh | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/scripts/validate-provider.sh b/scripts/validate-provider.sh index fd74d43..1892b9b 100755 --- a/scripts/validate-provider.sh +++ b/scripts/validate-provider.sh @@ -212,20 +212,28 @@ validate_integration() { errors+=("$provider not found in README.md Provider Skills table") fi - # Check providers.yaml has entry + # Check providers.yaml has entry with testScenario + # test-agent-scenario.sh now reads scenarios dynamically from providers.yaml if [ -f "$ROOT_DIR/providers.yaml" ]; then if ! grep -q "name: $provider_name" "$ROOT_DIR/providers.yaml"; then errors+=("$provider_name not found in providers.yaml") + else + # Check that the provider has a testScenario defined + # Use awk to find the provider block and check for testScenario + local has_test_scenario + has_test_scenario=$(awk -v provider="$provider_name" ' + /^ - name:/ { in_provider = ($3 == provider) } + in_provider && /testScenario:/ { print "yes"; exit } + /^ - name:/ && !($3 == provider) { in_provider = 0 } + ' "$ROOT_DIR/providers.yaml") + if [ "$has_test_scenario" != "yes" ]; then + errors+=("No testScenario for $provider_name in providers.yaml") + fi fi else errors+=("providers.yaml not found at repository root") fi - # Check test-agent-scenario.sh has at least one scenario - if ! grep -q "$provider_name" "$ROOT_DIR/scripts/test-agent-scenario.sh"; then - errors+=("No scenario for $provider_name in scripts/test-agent-scenario.sh") - fi - # Return errors if [ ${#errors[@]} -gt 0 ]; then printf '%s\n' "${errors[@]}" @@ -324,8 +332,7 @@ if [ ${#FAILED_PROVIDERS[@]} -gt 0 ]; then log "Please ensure you have updated:" log " 1. All required skill files (SKILL.md, references/, examples/)" log " 2. README.md - Add provider to Provider Skills table" - log " 3. providers.yaml - Add provider entry with documentation URLs" - log " 4. scripts/test-agent-scenario.sh - Add at least one test scenario" + log " 3. providers.yaml - Add provider entry with documentation URLs and testScenario" exit 1 else log "${GREEN}All ${#PASSED_PROVIDERS[@]} provider(s) passed validation!${NC}" From 9779776c523c251d3105c4abbabb49830ec8c95a Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Thu, 5 Feb 2026 18:56:09 +0000 Subject: [PATCH 5/5] fix: prevent server from starting during tests Only start the Express server when the file is run directly, not when imported for testing. This prevents open handles from causing Jest to hang after tests complete. Co-authored-by: Cursor --- .../examples/express/src/index.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/skills/woocommerce-webhooks/examples/express/src/index.js b/skills/woocommerce-webhooks/examples/express/src/index.js index ffa2bd4..47eb7b4 100644 --- a/skills/woocommerce-webhooks/examples/express/src/index.js +++ b/skills/woocommerce-webhooks/examples/express/src/index.js @@ -118,11 +118,13 @@ app.get('/health', (req, res) => { res.status(200).json({ status: 'healthy', timestamp: new Date().toISOString() }); }); -// Start server -app.listen(PORT, () => { - console.log(`🚀 WooCommerce webhook server running on port ${PORT}`); - console.log(`📍 Webhook endpoint: http://localhost:${PORT}/webhooks/woocommerce`); - console.log('🔒 Make sure to set WOOCOMMERCE_WEBHOOK_SECRET in your environment'); -}); +// Start server only when run directly (not when imported for testing) +if (require.main === module) { + app.listen(PORT, () => { + console.log(`🚀 WooCommerce webhook server running on port ${PORT}`); + console.log(`📍 Webhook endpoint: http://localhost:${PORT}/webhooks/woocommerce`); + console.log('🔒 Make sure to set WOOCOMMERCE_WEBHOOK_SECRET in your environment'); + }); +} module.exports = { app, verifyWooCommerceWebhook }; \ No newline at end of file