commit 282d2cbece8a024e109339aa8a2997329d9ecf74 Author: Zhongwei Li Date: Sat Nov 29 18:52:47 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..c743758 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,15 @@ +{ + "name": "webhook-handler-creator", + "description": "Create secure webhook endpoints with signature verification and retry logic", + "version": "1.0.0", + "author": { + "name": "Jeremy Longshore", + "email": "[email protected]" + }, + "skills": [ + "./skills" + ], + "commands": [ + "./commands" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..90786a4 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# webhook-handler-creator + +Create secure webhook endpoints with signature verification and retry logic diff --git a/commands/create-webhook-handler.md b/commands/create-webhook-handler.md new file mode 100644 index 0000000..43291e6 --- /dev/null +++ b/commands/create-webhook-handler.md @@ -0,0 +1,1104 @@ +--- +description: Create secure webhook endpoints with validation and resilience +shortcut: webhook +--- + +# Create Webhook Handler + +Automatically generate production-ready webhook endpoints with signature verification, idempotency, retry handling, event processing, and comprehensive security measures for reliable third-party integrations. + +## When to Use This Command + +Use `/create-webhook-handler` when you need to: +- Receive real-time events from external services (Stripe, GitHub, Slack) +- Build event-driven architectures with external triggers +- Implement payment processing notifications +- Handle CI/CD pipeline events +- Process incoming data from IoT devices +- Create serverless event receivers + +DON'T use this when: +- Building internal service communication (use message queues) +- Need guaranteed ordering (webhooks are best-effort) +- Handling large payloads (>10MB typically) +- Bidirectional communication needed (consider WebSockets) + +## Design Decisions + +This command implements **signature-based verification** as the primary approach because: +- Industry-standard security practice (HMAC-SHA256) +- Prevents replay attacks with timestamp validation +- Ensures message integrity and authenticity +- Widely supported by webhook providers +- Simple to implement and verify +- Minimal overhead for validation + +**Alternative considered: OAuth token validation** +- More complex setup required +- Additional network calls for validation +- Higher latency +- Recommended for user-authenticated webhooks + +**Alternative considered: mTLS (mutual TLS)** +- Strongest security option +- Complex certificate management +- Not widely supported +- Recommended for enterprise B2B integrations + +## Prerequisites + +Before running this command: +1. Webhook URL endpoint defined +2. Shared secret or signing key from provider +3. Event types to handle documented +4. Database for idempotency tracking +5. Queue system for async processing (optional) + +## Implementation Process + +### Step 1: Create Webhook Endpoint +Define secure POST endpoint with proper routing and middleware. + +### Step 2: Implement Signature Verification +Validate webhook authenticity using HMAC signatures. + +### Step 3: Add Idempotency Handling +Prevent duplicate processing of retried events. + +### Step 4: Route Events to Handlers +Dispatch events to appropriate processing functions. + +### Step 5: Configure Monitoring +Set up logging, metrics, and alerting for webhook health. + +## Output Format + +The command generates: +- `webhooks/handlers/` - Event-specific handler functions +- `webhooks/middleware/` - Signature verification, rate limiting +- `webhooks/routes.js` - Webhook endpoint definitions +- `webhooks/processors/` - Async event processors +- `webhooks/schemas/` - Event validation schemas +- `config/webhooks.json` - Provider configurations +- `tests/webhooks/` - Integration tests with mock events + +## Code Examples + +### Example 1: Multi-Provider Webhook System with Express + +```javascript +// webhooks/middleware/signature.js +const crypto = require('crypto'); +const { WebhookError } = require('../errors'); + +class SignatureVerifier { + constructor(config) { + this.providers = config.providers; + this.timestampTolerance = config.timestampTolerance || 300; // 5 minutes + } + + // Stripe signature verification + verifyStripe(payload, signature, secret) { + const elements = signature.split(','); + const timestamp = elements.find(e => e.startsWith('t=')).substring(2); + const signatures = elements + .filter(e => e.startsWith('v1=')) + .map(e => e.substring(3)); + + // Check timestamp to prevent replay attacks + const currentTime = Math.floor(Date.now() / 1000); + if (currentTime - parseInt(timestamp) > this.timestampTolerance) { + throw new WebhookError('Webhook timestamp too old', 'TIMESTAMP_TOO_OLD'); + } + + // Calculate expected signature + const signedPayload = `${timestamp}.${payload}`; + const expectedSignature = crypto + .createHmac('sha256', secret) + .update(signedPayload) + .digest('hex'); + + // Compare signatures + const validSignature = signatures.some(sig => + crypto.timingSafeEqual( + Buffer.from(sig), + Buffer.from(expectedSignature) + ) + ); + + if (!validSignature) { + throw new WebhookError('Invalid signature', 'INVALID_SIGNATURE'); + } + + return true; + } + + // GitHub signature verification + verifyGitHub(payload, signature, secret) { + if (!signature.startsWith('sha256=')) { + throw new WebhookError('Invalid signature format', 'INVALID_FORMAT'); + } + + const expectedSignature = 'sha256=' + crypto + .createHmac('sha256', secret) + .update(payload) + .digest('hex'); + + const isValid = crypto.timingSafeEqual( + Buffer.from(signature), + Buffer.from(expectedSignature) + ); + + if (!isValid) { + throw new WebhookError('Invalid signature', 'INVALID_SIGNATURE'); + } + + return true; + } + + // Shopify signature verification + verifyShopify(payload, signature, secret) { + const hash = crypto + .createHmac('sha256', secret) + .update(payload, 'utf8') + .digest('base64'); + + const isValid = crypto.timingSafeEqual( + Buffer.from(signature), + Buffer.from(hash) + ); + + if (!isValid) { + throw new WebhookError('Invalid signature', 'INVALID_SIGNATURE'); + } + + return true; + } + + // Generic HMAC verification + verifyHMAC(payload, signature, secret, algorithm = 'sha256') { + const expectedSignature = crypto + .createHmac(algorithm, secret) + .update(payload) + .digest('hex'); + + const isValid = crypto.timingSafeEqual( + Buffer.from(signature), + Buffer.from(expectedSignature) + ); + + if (!isValid) { + throw new WebhookError('Invalid signature', 'INVALID_SIGNATURE'); + } + + return true; + } + + // Middleware factory + createMiddleware(provider) { + return async (req, res, next) => { + try { + const config = this.providers[provider]; + if (!config) { + throw new WebhookError('Unknown provider', 'UNKNOWN_PROVIDER'); + } + + const rawBody = req.rawBody || JSON.stringify(req.body); + const signature = req.headers[config.headerName]; + + if (!signature) { + throw new WebhookError('Missing signature', 'MISSING_SIGNATURE'); + } + + // Verify based on provider + switch (provider) { + case 'stripe': + this.verifyStripe(rawBody, signature, config.secret); + break; + case 'github': + this.verifyGitHub(rawBody, signature, config.secret); + break; + case 'shopify': + this.verifyShopify(rawBody, signature, config.secret); + break; + default: + this.verifyHMAC(rawBody, signature, config.secret, config.algorithm); + } + + // Add provider info to request + req.webhookProvider = provider; + req.webhookVerified = true; + + next(); + } catch (error) { + console.error(`Webhook verification failed: ${error.message}`); + res.status(401).json({ + error: 'Webhook verification failed', + code: error.code || 'VERIFICATION_FAILED' + }); + } + }; + } +} + +// webhooks/middleware/idempotency.js +class IdempotencyHandler { + constructor(store) { + this.store = store; // Redis or database + this.ttl = 86400; // 24 hours + } + + async middleware(req, res, next) { + try { + // Extract idempotency key + const idempotencyKey = + req.headers['idempotency-key'] || + req.body.id || + req.body.event_id || + this.generateKey(req); + + // Check if already processed + const existing = await this.store.get(idempotencyKey); + if (existing) { + console.log(`Duplicate webhook detected: ${idempotencyKey}`); + const result = JSON.parse(existing); + return res.status(result.status).json(result.body); + } + + // Store the key immediately to prevent race conditions + await this.store.setNX(idempotencyKey, 'PROCESSING', this.ttl); + + // Attach key to request + req.idempotencyKey = idempotencyKey; + + // Capture response + const originalSend = res.json; + res.json = function(body) { + // Store the response + const response = { + status: res.statusCode, + body: body + }; + this.store.set( + idempotencyKey, + JSON.stringify(response), + this.ttl + ).catch(err => console.error('Failed to store response:', err)); + + // Send the original response + return originalSend.call(res, body); + }.bind(this); + + next(); + } catch (error) { + console.error('Idempotency check failed:', error); + next(); // Continue processing even if idempotency fails + } + } + + generateKey(req) { + const content = JSON.stringify({ + provider: req.webhookProvider, + body: req.body, + timestamp: Math.floor(Date.now() / 60000) // 1-minute window + }); + + return crypto + .createHash('sha256') + .update(content) + .digest('hex'); + } +} + +// webhooks/handlers/stripe.js +const { Queue } = require('bull'); +const { EventEmitter } = require('events'); + +class StripeWebhookHandler extends EventEmitter { + constructor(config) { + super(); + this.config = config; + this.queue = new Queue('stripe-events', config.redis); + this.setupProcessors(); + } + + async handleWebhook(req, res) { + const event = req.body; + + // Log webhook receipt + console.log(`Received Stripe webhook: ${event.type} (${event.id})`); + + try { + // Validate event data + this.validateEvent(event); + + // Queue for async processing + const job = await this.queue.add(event.type, event, { + attempts: 3, + backoff: { + type: 'exponential', + delay: 2000 + }, + removeOnComplete: false, + removeOnFail: false + }); + + // Emit event for real-time processing if needed + this.emit(event.type, event); + + // Respond quickly to acknowledge receipt + res.status(200).json({ + received: true, + jobId: job.id, + eventId: event.id + }); + + } catch (error) { + console.error('Webhook processing error:', error); + + // Still return 200 to prevent retries if it's our error + if (error.code === 'VALIDATION_ERROR') { + res.status(200).json({ + received: true, + error: 'Validation failed, event ignored' + }); + } else { + // Return 500 for Stripe to retry + res.status(500).json({ + error: 'Processing failed', + message: error.message + }); + } + } + } + + validateEvent(event) { + if (!event.type || !event.id || !event.data) { + throw new Error('Invalid event structure'); + } + + // Additional validation based on event type + switch (event.type) { + case 'payment_intent.succeeded': + if (!event.data.object.amount) { + throw new Error('Missing amount in payment_intent'); + } + break; + case 'customer.subscription.deleted': + if (!event.data.object.customer) { + throw new Error('Missing customer in subscription'); + } + break; + } + } + + setupProcessors() { + // Payment succeeded + this.queue.process('payment_intent.succeeded', async (job) => { + const event = job.data; + const payment = event.data.object; + + // Update order status + await this.updateOrderStatus(payment.metadata.orderId, 'paid'); + + // Send confirmation email + await this.sendPaymentConfirmation(payment); + + // Update inventory + await this.updateInventory(payment.metadata.orderId); + + // Analytics + await this.trackRevenue(payment.amount / 100, payment.currency); + + return { processed: true, orderId: payment.metadata.orderId }; + }); + + // Subscription canceled + this.queue.process('customer.subscription.deleted', async (job) => { + const event = job.data; + const subscription = event.data.object; + + // Update user account + await this.updateUserSubscription(subscription.customer, null); + + // Send cancellation email + await this.sendCancellationEmail(subscription); + + // Clean up resources + await this.cleanupUserResources(subscription.customer); + + return { processed: true, customerId: subscription.customer }; + }); + + // Payment failed + this.queue.process('payment_intent.payment_failed', async (job) => { + const event = job.data; + const payment = event.data.object; + + // Notify customer + await this.sendPaymentFailedNotification(payment); + + // Update order status + await this.updateOrderStatus(payment.metadata.orderId, 'payment_failed'); + + // Alert operations team if high-value + if (payment.amount > 100000) { // $1000+ + await this.alertOperationsTeam(payment); + } + + return { processed: true, orderId: payment.metadata.orderId }; + }); + + // Handle job failures + this.queue.on('failed', (job, err) => { + console.error(`Job ${job.id} failed:`, err); + this.alertOnFailure(job, err); + }); + + // Monitor job completion + this.queue.on('completed', (job, result) => { + console.log(`Job ${job.id} completed:`, result); + }); + } + + // Helper methods + async updateOrderStatus(orderId, status) { + // Implementation + } + + async sendPaymentConfirmation(payment) { + // Implementation + } + + async updateInventory(orderId) { + // Implementation + } + + async trackRevenue(amount, currency) { + // Implementation + } + + async alertOnFailure(job, error) { + // Send to monitoring service + } +} + +// webhooks/routes.js +const express = require('express'); +const router = express.Router(); +const { SignatureVerifier } = require('./middleware/signature'); +const { IdempotencyHandler } = require('./middleware/idempotency'); +const { RateLimiter } = require('./middleware/rateLimiter'); +const { StripeWebhookHandler } = require('./handlers/stripe'); +const { GitHubWebhookHandler } = require('./handlers/github'); +const Redis = require('ioredis'); + +// Initialize middleware +const redis = new Redis(process.env.REDIS_URL); +const signatureVerifier = new SignatureVerifier({ + providers: { + stripe: { + secret: process.env.STRIPE_WEBHOOK_SECRET, + headerName: 'stripe-signature' + }, + github: { + secret: process.env.GITHUB_WEBHOOK_SECRET, + headerName: 'x-hub-signature-256' + }, + shopify: { + secret: process.env.SHOPIFY_WEBHOOK_SECRET, + headerName: 'x-shopify-hmac-sha256' + } + }, + timestampTolerance: 300 +}); + +const idempotencyHandler = new IdempotencyHandler(redis); +const rateLimiter = new RateLimiter({ + windowMs: 60000, + maxRequests: 100, + keyGenerator: (req) => req.webhookProvider +}); + +// Initialize handlers +const stripeHandler = new StripeWebhookHandler({ redis }); +const githubHandler = new GitHubWebhookHandler({ redis }); + +// Stripe webhooks +router.post('/stripe', + express.raw({ type: 'application/json' }), + signatureVerifier.createMiddleware('stripe'), + idempotencyHandler.middleware.bind(idempotencyHandler), + rateLimiter.middleware(), + stripeHandler.handleWebhook.bind(stripeHandler) +); + +// GitHub webhooks +router.post('/github', + express.json({ limit: '10mb' }), + signatureVerifier.createMiddleware('github'), + idempotencyHandler.middleware.bind(idempotencyHandler), + rateLimiter.middleware(), + githubHandler.handleWebhook.bind(githubHandler) +); + +// Health check endpoint +router.get('/health', (req, res) => { + res.json({ + status: 'healthy', + providers: ['stripe', 'github', 'shopify'], + timestamp: new Date().toISOString() + }); +}); + +module.exports = router; +``` + +### Example 2: Serverless Webhook Handler with AWS Lambda + +```python +# webhook_handler.py - AWS Lambda function +import json +import hmac +import hashlib +import time +import boto3 +from typing import Dict, Any, Optional +from dataclasses import dataclass +from enum import Enum + +dynamodb = boto3.resource('dynamodb') +sqs = boto3.client('sqs') +sns = boto3.client('sns') + +class WebhookProvider(Enum): + STRIPE = "stripe" + GITHUB = "github" + SLACK = "slack" + SENDGRID = "sendgrid" + +@dataclass +class WebhookConfig: + provider: WebhookProvider + secret: str + header_name: str + algorithm: str = "sha256" + timestamp_tolerance: int = 300 + +class WebhookValidator: + """Validates webhook signatures from various providers.""" + + @staticmethod + def validate_stripe(body: str, signature: str, secret: str) -> bool: + """Validate Stripe webhook signature.""" + elements = signature.split(',') + timestamp = None + signatures = [] + + for element in elements: + key, value = element.split('=') + if key == 't': + timestamp = value + elif key == 'v1': + signatures.append(value) + + if not timestamp: + raise ValueError("No timestamp in signature") + + # Check timestamp + current_time = int(time.time()) + if current_time - int(timestamp) > 300: # 5 minutes + raise ValueError("Timestamp too old") + + # Calculate expected signature + signed_payload = f"{timestamp}.{body}" + expected_sig = hmac.new( + secret.encode('utf-8'), + signed_payload.encode('utf-8'), + hashlib.sha256 + ).hexdigest() + + # Compare signatures + return any( + hmac.compare_digest(sig, expected_sig) + for sig in signatures + ) + + @staticmethod + def validate_github(body: str, signature: str, secret: str) -> bool: + """Validate GitHub webhook signature.""" + if not signature.startswith('sha256='): + raise ValueError("Invalid signature format") + + expected = 'sha256=' + hmac.new( + secret.encode('utf-8'), + body.encode('utf-8'), + hashlib.sha256 + ).hexdigest() + + return hmac.compare_digest(signature, expected) + + @staticmethod + def validate_generic( + body: str, + signature: str, + secret: str, + algorithm: str = "sha256" + ) -> bool: + """Generic HMAC validation.""" + hash_func = getattr(hashlib, algorithm) + expected = hmac.new( + secret.encode('utf-8'), + body.encode('utf-8'), + hash_func + ).hexdigest() + + return hmac.compare_digest(signature, expected) + +class IdempotencyManager: + """Manages idempotent processing of webhooks.""" + + def __init__(self, table_name: str): + self.table = dynamodb.Table(table_name) + + async def check_and_record(self, event_id: str) -> bool: + """Check if event was already processed and record it.""" + try: + # Try to create item with conditional check + self.table.put_item( + Item={ + 'event_id': event_id, + 'processed_at': int(time.time()), + 'ttl': int(time.time()) + 86400 # 24 hour TTL + }, + ConditionExpression='attribute_not_exists(event_id)' + ) + return False # Not processed before + except Exception as e: + if 'ConditionalCheckFailedException' in str(e): + return True # Already processed + raise + +class WebhookProcessor: + """Processes webhook events asynchronously.""" + + def __init__(self, queue_url: str, topic_arn: str): + self.queue_url = queue_url + self.topic_arn = topic_arn + + async def process_stripe_event(self, event: Dict[str, Any]) -> Dict[str, Any]: + """Process Stripe webhook events.""" + event_type = event.get('type') + event_data = event.get('data', {}).get('object', {}) + + handlers = { + 'payment_intent.succeeded': self.handle_payment_success, + 'payment_intent.failed': self.handle_payment_failure, + 'customer.subscription.created': self.handle_subscription_created, + 'customer.subscription.deleted': self.handle_subscription_deleted, + 'invoice.payment_failed': self.handle_invoice_failed + } + + handler = handlers.get(event_type) + if handler: + return await handler(event_data) + else: + print(f"No handler for event type: {event_type}") + return {'processed': False, 'reason': 'No handler'} + + async def handle_payment_success(self, payment: Dict[str, Any]) -> Dict[str, Any]: + """Handle successful payment.""" + # Queue order fulfillment + await self.queue_message({ + 'action': 'fulfill_order', + 'order_id': payment.get('metadata', {}).get('order_id'), + 'amount': payment.get('amount'), + 'currency': payment.get('currency') + }) + + # Send notification + await self.send_notification({ + 'type': 'payment_success', + 'customer_email': payment.get('receipt_email'), + 'amount': payment.get('amount') / 100 + }) + + return {'processed': True, 'action': 'payment_processed'} + + async def queue_message(self, message: Dict[str, Any]): + """Queue message for async processing.""" + sqs.send_message( + QueueUrl=self.queue_url, + MessageBody=json.dumps(message), + MessageAttributes={ + 'Type': { + 'StringValue': message.get('action', 'unknown'), + 'DataType': 'String' + } + } + ) + + async def send_notification(self, notification: Dict[str, Any]): + """Send SNS notification.""" + sns.publish( + TopicArn=self.topic_arn, + Message=json.dumps(notification), + Subject=f"Webhook Event: {notification.get('type')}" + ) + +def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]: + """AWS Lambda handler for webhook processing.""" + + # Parse request + body = event.get('body', '') + headers = event.get('headers', {}) + path = event.get('path', '') + + # Determine provider from path + provider = path.split('/')[-1] # /webhooks/stripe -> stripe + + try: + # Get configuration + config = WebhookConfig( + provider=WebhookProvider(provider), + secret=os.environ[f'{provider.upper()}_WEBHOOK_SECRET'], + header_name=headers.get('x-webhook-header', 'signature') + ) + + # Validate signature + validator = WebhookValidator() + signature = headers.get(config.header_name) + + if not signature: + return { + 'statusCode': 401, + 'body': json.dumps({'error': 'Missing signature'}) + } + + is_valid = False + if config.provider == WebhookProvider.STRIPE: + is_valid = validator.validate_stripe(body, signature, config.secret) + elif config.provider == WebhookProvider.GITHUB: + is_valid = validator.validate_github(body, signature, config.secret) + else: + is_valid = validator.validate_generic( + body, signature, config.secret, config.algorithm + ) + + if not is_valid: + return { + 'statusCode': 401, + 'body': json.dumps({'error': 'Invalid signature'}) + } + + # Parse body + webhook_data = json.loads(body) + + # Check idempotency + idempotency = IdempotencyManager('webhook-events') + event_id = webhook_data.get('id') or webhook_data.get('event_id') + + if await idempotency.check_and_record(event_id): + return { + 'statusCode': 200, + 'body': json.dumps({'message': 'Already processed'}) + } + + # Process webhook + processor = WebhookProcessor( + queue_url=os.environ['SQS_QUEUE_URL'], + topic_arn=os.environ['SNS_TOPIC_ARN'] + ) + + if config.provider == WebhookProvider.STRIPE: + result = await processor.process_stripe_event(webhook_data) + else: + # Queue for processing + await processor.queue_message({ + 'provider': provider, + 'event': webhook_data + }) + result = {'queued': True} + + return { + 'statusCode': 200, + 'body': json.dumps(result) + } + + except Exception as e: + print(f"Webhook processing error: {str(e)}") + + # Return 200 to prevent retries for our errors + if 'Validation' in str(e): + return { + 'statusCode': 200, + 'body': json.dumps({'error': 'Validation failed'}) + } + + # Return 500 for provider to retry + return { + 'statusCode': 500, + 'body': json.dumps({'error': 'Processing failed'}) + } +``` + +### Example 3: Testing Webhook Handlers + +```javascript +// tests/webhook.test.js +const request = require('supertest'); +const crypto = require('crypto'); +const app = require('../app'); +const Redis = require('ioredis-mock'); + +describe('Webhook Handler Tests', () => { + let redis; + + beforeEach(() => { + redis = new Redis(); + }); + + describe('Stripe Webhooks', () => { + const secret = 'whsec_test_secret'; + + function generateStripeSignature(payload, secret) { + const timestamp = Math.floor(Date.now() / 1000); + const signedPayload = `${timestamp}.${payload}`; + const signature = crypto + .createHmac('sha256', secret) + .update(signedPayload) + .digest('hex'); + + return `t=${timestamp},v1=${signature}`; + } + + it('should accept valid webhook with correct signature', async () => { + const payload = JSON.stringify({ + id: 'evt_test_123', + type: 'payment_intent.succeeded', + data: { + object: { + id: 'pi_test_123', + amount: 2000, + currency: 'usd', + metadata: { + order_id: 'order_123' + } + } + } + }); + + const signature = generateStripeSignature(payload, secret); + + const response = await request(app) + .post('/webhooks/stripe') + .set('stripe-signature', signature) + .set('Content-Type', 'application/json') + .send(payload) + .expect(200); + + expect(response.body.received).toBe(true); + expect(response.body.eventId).toBe('evt_test_123'); + }); + + it('should reject webhook with invalid signature', async () => { + const payload = JSON.stringify({ + id: 'evt_test_123', + type: 'payment_intent.succeeded' + }); + + await request(app) + .post('/webhooks/stripe') + .set('stripe-signature', 'invalid_signature') + .send(payload) + .expect(401); + }); + + it('should handle idempotent requests', async () => { + const payload = JSON.stringify({ + id: 'evt_test_duplicate', + type: 'payment_intent.succeeded', + data: { object: { amount: 1000 } } + }); + + const signature = generateStripeSignature(payload, secret); + + // First request + const response1 = await request(app) + .post('/webhooks/stripe') + .set('stripe-signature', signature) + .send(payload) + .expect(200); + + // Duplicate request + const response2 = await request(app) + .post('/webhooks/stripe') + .set('stripe-signature', signature) + .send(payload) + .expect(200); + + // Both should return same response + expect(response1.body).toEqual(response2.body); + }); + + it('should reject old timestamps', async () => { + const oldTimestamp = Math.floor(Date.now() / 1000) - 400; // 6+ minutes old + const payload = JSON.stringify({ + id: 'evt_old', + type: 'payment_intent.succeeded' + }); + + const signedPayload = `${oldTimestamp}.${payload}`; + const signature = crypto + .createHmac('sha256', secret) + .update(signedPayload) + .digest('hex'); + + const signatureHeader = `t=${oldTimestamp},v1=${signature}`; + + await request(app) + .post('/webhooks/stripe') + .set('stripe-signature', signatureHeader) + .send(payload) + .expect(401); + }); + }); + + describe('GitHub Webhooks', () => { + const secret = 'github_secret'; + + function generateGitHubSignature(payload, secret) { + return 'sha256=' + crypto + .createHmac('sha256', secret) + .update(payload) + .digest('hex'); + } + + it('should process push event', async () => { + const payload = JSON.stringify({ + ref: 'refs/heads/main', + repository: { + name: 'test-repo', + full_name: 'user/test-repo' + }, + commits: [{ + id: 'abc123', + message: 'Test commit' + }] + }); + + const signature = generateGitHubSignature(payload, secret); + + const response = await request(app) + .post('/webhooks/github') + .set('x-hub-signature-256', signature) + .set('x-github-event', 'push') + .send(payload) + .expect(200); + + expect(response.body.received).toBe(true); + }); + }); + + describe('Rate Limiting', () => { + it('should rate limit excessive requests', async () => { + const payload = JSON.stringify({ + id: 'evt_rate_test', + type: 'test.event' + }); + + const signature = generateStripeSignature(payload, 'test_secret'); + + // Make many requests + const requests = []; + for (let i = 0; i < 150; i++) { + requests.push( + request(app) + .post('/webhooks/stripe') + .set('stripe-signature', signature) + .send(payload) + ); + } + + const responses = await Promise.all(requests); + const rateLimited = responses.filter(r => r.status === 429); + + expect(rateLimited.length).toBeGreaterThan(0); + }); + }); +}); +``` + +## Error Handling + +| Error | Cause | Solution | +|-------|-------|----------| +| "Invalid signature" | Wrong secret or tampered payload | Verify secret key configuration | +| "Timestamp too old" | Webhook delivered late | Increase tolerance or check delays | +| "Duplicate event" | Webhook retried by provider | Idempotency working correctly | +| "Processing timeout" | Long-running handler | Move to async queue processing | +| "Rate limit exceeded" | Too many webhooks | Increase limits or optimize | + +## Configuration Options + +**Security Options** +- `signatureAlgorithm`: HMAC-SHA256, HMAC-SHA512 +- `timestampTolerance`: Maximum age of webhook (seconds) +- `ipAllowlist`: Restrict to provider IPs +- `rateLimits`: Per-provider rate limiting + +**Processing Options** +- `asyncProcessing`: Queue events for background processing +- `retryAttempts`: Number of processing retries +- `deadLetterQueue`: Failed event storage +- `parallelProcessing`: Concurrent event handling + +## Best Practices + +DO: +- Always verify signatures before processing +- Respond quickly (< 5 seconds) to webhook +- Implement idempotency for all handlers +- Log all webhook events for audit +- Use async processing for heavy operations +- Monitor webhook health and success rates + +DON'T: +- Process webhooks synchronously if slow +- Trust webhook data without validation +- Ignore timestamp verification +- Return errors for duplicate events +- Skip rate limiting +- Store sensitive webhook data in logs + +## Performance Considerations + +- Use connection pooling for database operations +- Queue heavy processing asynchronously +- Implement circuit breakers for downstream services +- Cache frequently accessed data +- Use bulk operations where possible +- Monitor processing latency and queue depth + +## Security Considerations + +- Always use HTTPS endpoints +- Rotate webhook secrets regularly +- Implement IP allowlisting for critical webhooks +- Never log full webhook payloads with sensitive data +- Use separate secrets per environment +- Monitor for replay attacks +- Implement webhook firewall rules + +## Related Commands + +- `/api-event-emitter` - Create event systems +- `/api-gateway-builder` - Build API gateways +- `/message-queue-setup` - Configure message queues +- `/serverless-function` - Create Lambda handlers +- `/monitoring-setup` - Add monitoring + +## Version History + +- v1.0.0 (2024-10): Initial implementation with Stripe, GitHub, Shopify support +- Planned v1.1.0: Add Twilio, SendGrid, and Slack webhook handlers \ No newline at end of file diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..5018d92 --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,97 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:jeremylongshore/claude-code-plugins-plus:plugins/api-development/webhook-handler-creator", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "a0e5a661bb6e68164c56d97190c482af0228432d", + "treeHash": "9fe2ac6f8302ba61bc68427db8c4de6513a4ac949c5c55d8b48beb9b138712f6", + "generatedAt": "2025-11-28T10:18:52.349343Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "webhook-handler-creator", + "description": "Create secure webhook endpoints with signature verification and retry logic", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "01991447744dea84981782d47f05ad82d51eccdeeba158c04f4b8a6f15da5086" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "ea2241a65c59cfc8d62fcfc0ce2d6989d6ce55f9f32219dc17027dd0c7614f95" + }, + { + "path": "commands/create-webhook-handler.md", + "sha256": "3545b87b71616aefd4559c38946aac51f1a02c0a39d74102e272b21eab9d3dc8" + }, + { + "path": "skills/skill-adapter/references/examples.md", + "sha256": "922bbc3c4ebf38b76f515b5c1998ebde6bf902233e00e2c5a0e9176f975a7572" + }, + { + "path": "skills/skill-adapter/references/best-practices.md", + "sha256": "c8f32b3566252f50daacd346d7045a1060c718ef5cfb07c55a0f2dec5f1fb39e" + }, + { + "path": "skills/skill-adapter/references/README.md", + "sha256": "efc5da262c37e37da6a7e2cc862e0c4e4beae3a01e770de1d4d982e3ab55638c" + }, + { + "path": "skills/skill-adapter/scripts/helper-template.sh", + "sha256": "0881d5660a8a7045550d09ae0acc15642c24b70de6f08808120f47f86ccdf077" + }, + { + "path": "skills/skill-adapter/scripts/validation.sh", + "sha256": "92551a29a7f512d2036e4f1fb46c2a3dc6bff0f7dde4a9f699533e446db48502" + }, + { + "path": "skills/skill-adapter/scripts/README.md", + "sha256": "69f93625d3baf6c6febd597368201e63c24a2ea1b6973e46cfad7194406645c6" + }, + { + "path": "skills/skill-adapter/assets/test-data.json", + "sha256": "ac17dca3d6e253a5f39f2a2f1b388e5146043756b05d9ce7ac53a0042eee139d" + }, + { + "path": "skills/skill-adapter/assets/webhook_handler_template.py", + "sha256": "e1271855ed5cf351b9c2d4394ff1d1337eb3fdd28ce606508db773ff19517191" + }, + { + "path": "skills/skill-adapter/assets/README.md", + "sha256": "2dfcb62af4c2a1873dd67e673d071a9a55d0640dc1404df57bec6d7f8961c271" + }, + { + "path": "skills/skill-adapter/assets/example_event_schema.json", + "sha256": "0a9106035babcff2db5b63f3ef10d439ccdea92505ee47fcbbd36796507e3064" + }, + { + "path": "skills/skill-adapter/assets/example_webhook_payload.json", + "sha256": "df8f93e7d0da91fb2482ad7e3a0b0ba727615bd86c9c7d7a80a6ce5d3b57615f" + }, + { + "path": "skills/skill-adapter/assets/skill-schema.json", + "sha256": "f5639ba823a24c9ac4fb21444c0717b7aefde1a4993682897f5bf544f863c2cd" + }, + { + "path": "skills/skill-adapter/assets/config-template.json", + "sha256": "0c2ba33d2d3c5ccb266c0848fc43caa68a2aa6a80ff315d4b378352711f83e1c" + } + ], + "dirSha256": "9fe2ac6f8302ba61bc68427db8c4de6513a4ac949c5c55d8b48beb9b138712f6" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/skill-adapter/assets/README.md b/skills/skill-adapter/assets/README.md new file mode 100644 index 0000000..a6aad5b --- /dev/null +++ b/skills/skill-adapter/assets/README.md @@ -0,0 +1,7 @@ +# Assets + +Bundled resources for webhook-handler-creator skill + +- [ ] webhook_handler_template.py: A basic Python template for a webhook handler with signature verification and idempotency. +- [ ] example_webhook_payload.json: Example JSON payload for a webhook. +- [ ] example_event_schema.json: Example JSON schema for a webhook event. diff --git a/skills/skill-adapter/assets/config-template.json b/skills/skill-adapter/assets/config-template.json new file mode 100644 index 0000000..16f1712 --- /dev/null +++ b/skills/skill-adapter/assets/config-template.json @@ -0,0 +1,32 @@ +{ + "skill": { + "name": "skill-name", + "version": "1.0.0", + "enabled": true, + "settings": { + "verbose": false, + "autoActivate": true, + "toolRestrictions": true + } + }, + "triggers": { + "keywords": [ + "example-trigger-1", + "example-trigger-2" + ], + "patterns": [] + }, + "tools": { + "allowed": [ + "Read", + "Grep", + "Bash" + ], + "restricted": [] + }, + "metadata": { + "author": "Plugin Author", + "category": "general", + "tags": [] + } +} diff --git a/skills/skill-adapter/assets/example_event_schema.json b/skills/skill-adapter/assets/example_event_schema.json new file mode 100644 index 0000000..5289f72 --- /dev/null +++ b/skills/skill-adapter/assets/example_event_schema.json @@ -0,0 +1,125 @@ +{ + "_comment": "Example JSON schema for a webhook event related to order processing.", + "type": "object", + "properties": { + "event_type": { + "type": "string", + "description": "Type of event. e.g., order.created, order.updated, order.cancelled", + "example": "order.created" + }, + "event_id": { + "type": "string", + "format": "uuid", + "description": "Unique identifier for the event.", + "example": "a1b2c3d4-e5f6-7890-1234-567890abcdef" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "Timestamp of when the event occurred (ISO 8601 format).", + "example": "2024-01-27T12:00:00Z" + }, + "data": { + "type": "object", + "description": "The actual data associated with the event.", + "properties": { + "order_id": { + "type": "string", + "description": "Unique identifier for the order.", + "example": "ORD-2024-001" + }, + "customer_id": { + "type": "string", + "description": "Unique identifier for the customer.", + "example": "CUST-001" + }, + "amount": { + "type": "number", + "format": "float", + "description": "Total order amount.", + "example": 99.99 + }, + "currency": { + "type": "string", + "description": "Currency code (ISO 4217).", + "example": "USD" + }, + "status": { + "type": "string", + "description": "Current status of the order.", + "example": "pending", + "enum": ["pending", "processing", "shipped", "delivered", "cancelled"] + }, + "shipping_address": { + "type": "object", + "description": "Shipping address details.", + "properties": { + "street": { + "type": "string", + "example": "123 Main St" + }, + "city": { + "type": "string", + "example": "Anytown" + }, + "state": { + "type": "string", + "example": "CA" + }, + "zip": { + "type": "string", + "example": "91234" + }, + "country": { + "type": "string", + "example": "USA" + } + }, + "required": ["street", "city", "state", "zip", "country"] + }, + "items": { + "type": "array", + "description": "List of items in the order.", + "items": { + "type": "object", + "properties": { + "product_id": { + "type": "string", + "example": "PROD-001" + }, + "quantity": { + "type": "integer", + "example": 2 + }, + "price": { + "type": "number", + "format": "float", + "example": 49.99 + } + }, + "required": ["product_id", "quantity", "price"] + } + } + }, + "required": ["order_id", "customer_id", "amount", "currency", "status", "shipping_address", "items"] + }, + "metadata": { + "type": "object", + "description": "Additional metadata about the event.", + "properties": { + "source": { + "type": "string", + "description": "Source of the event (e.g., 'payment_gateway', 'crm').", + "example": "payment_gateway" + }, + "version": { + "type": "string", + "description": "Version of the event schema.", + "example": "1.0" + } + }, + "required": ["source", "version"] + } + }, + "required": ["event_type", "event_id", "timestamp", "data", "metadata"] +} \ No newline at end of file diff --git a/skills/skill-adapter/assets/example_webhook_payload.json b/skills/skill-adapter/assets/example_webhook_payload.json new file mode 100644 index 0000000..315f1f9 --- /dev/null +++ b/skills/skill-adapter/assets/example_webhook_payload.json @@ -0,0 +1,53 @@ +{ + "_comment": "Example JSON payload for a webhook. This payload represents a common event: an order being placed.", + "event_type": "order.created", + "_comment": "Type of event that triggered the webhook.", + "event_id": "evt_1234567890abcdef", + "_comment": "Unique identifier for this specific event. Used for idempotency.", + "timestamp": 1678886400, + "_comment": "Unix timestamp of when the event occurred.", + "data": { + "_comment": "The actual data associated with the event. Structure varies depending on event_type.", + "order_id": "ord_abcdef1234567890", + "customer_id": "cust_fedcba0987654321", + "order_total": 99.99, + "currency": "USD", + "items": [ + { + "item_id": "item_11223344", + "quantity": 1, + "price": 49.99, + "name": "Awesome Widget" + }, + { + "item_id": "item_55667788", + "quantity": 1, + "price": 50.00, + "name": "Deluxe Gadget" + } + ], + "shipping_address": { + "name": "John Doe", + "address_line1": "123 Main St", + "address_line2": null, + "city": "Anytown", + "state": "CA", + "zip_code": "91234", + "country": "US" + }, + "billing_address": { + "name": "John Doe", + "address_line1": "123 Main St", + "address_line2": null, + "city": "Anytown", + "state": "CA", + "zip_code": "91234", + "country": "US" + } + }, + "metadata": { + "_comment": "Optional metadata associated with the event. Can be used to pass additional context.", + "source": "web", + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36" + } +} \ No newline at end of file diff --git a/skills/skill-adapter/assets/skill-schema.json b/skills/skill-adapter/assets/skill-schema.json new file mode 100644 index 0000000..8dc154c --- /dev/null +++ b/skills/skill-adapter/assets/skill-schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Claude Skill Configuration", + "type": "object", + "required": ["name", "description"], + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z0-9-]+$", + "maxLength": 64, + "description": "Skill identifier (lowercase, hyphens only)" + }, + "description": { + "type": "string", + "maxLength": 1024, + "description": "What the skill does and when to use it" + }, + "allowed-tools": { + "type": "string", + "description": "Comma-separated list of allowed tools" + }, + "version": { + "type": "string", + "pattern": "^\\d+\\.\\d+\\.\\d+$", + "description": "Semantic version (x.y.z)" + } + } +} diff --git a/skills/skill-adapter/assets/test-data.json b/skills/skill-adapter/assets/test-data.json new file mode 100644 index 0000000..f0cd871 --- /dev/null +++ b/skills/skill-adapter/assets/test-data.json @@ -0,0 +1,27 @@ +{ + "testCases": [ + { + "name": "Basic activation test", + "input": "trigger phrase example", + "expected": { + "activated": true, + "toolsUsed": ["Read", "Grep"], + "success": true + } + }, + { + "name": "Complex workflow test", + "input": "multi-step trigger example", + "expected": { + "activated": true, + "steps": 3, + "toolsUsed": ["Read", "Write", "Bash"], + "success": true + } + } + ], + "fixtures": { + "sampleInput": "example data", + "expectedOutput": "processed result" + } +} diff --git a/skills/skill-adapter/assets/webhook_handler_template.py b/skills/skill-adapter/assets/webhook_handler_template.py new file mode 100644 index 0000000..105e423 --- /dev/null +++ b/skills/skill-adapter/assets/webhook_handler_template.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 + +""" +A basic Python template for a webhook handler with signature verification +and idempotency. Provides a starting point for building robust and secure +webhook integrations. +""" + +import hashlib +import hmac +import json +import logging +import os +import time +from functools import wraps +from http import HTTPStatus +from typing import Callable, Dict + +from flask import Flask, request, jsonify + +# Configure logging +logging.basicConfig(level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s') + +# Environment variables (replace with your actual values) +WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET", "your_secret_key") +MAX_RETRIES = int(os.environ.get("MAX_RETRIES", 3)) +RETRY_DELAY = int(os.environ.get("RETRY_DELAY", 1)) # Initial delay in seconds + + +app = Flask(__name__) + + +class WebhookError(Exception): + """Base class for webhook-related exceptions.""" + pass + + +class SignatureVerificationError(WebhookError): + """Raised when webhook signature verification fails.""" + pass + + +class IdempotencyError(WebhookError): + """Raised when idempotency check fails.""" + pass + + +def verify_signature(request_data: bytes, signature: str, secret: str) -> None: + """ + Verifies the webhook signature against the request body and secret. + + Args: + request_data: The raw bytes of the request body. + signature: The signature sent in the webhook request headers. + secret: The secret key used to generate the signature. + + Raises: + SignatureVerificationError: If the signature does not match. + """ + try: + expected_signature = hmac.new( + secret.encode('utf-8'), + request_data, + hashlib.sha256 + ).hexdigest() + + if not hmac.compare_digest(expected_signature, signature): + raise SignatureVerificationError("Invalid webhook signature.") + except Exception as e: + logging.error(f"Signature verification failed: {e}") + raise SignatureVerificationError("Signature verification failed.") from e + + +def idempotent(func: Callable) -> Callable: + """ + Decorator to ensure idempotency of webhook requests. This example uses a + simple in-memory store. For production, use a persistent database + (e.g., Redis, PostgreSQL). + + Args: + func: The function to decorate (webhook handler). + + Returns: + The decorated function. + + Raises: + IdempotencyError: If the request ID has already been processed. + """ + processed_requests: Dict[str, bool] = {} # In-memory store (replace in production) + + @wraps(func) + def wrapper(*args, **kwargs): + request_id = request.headers.get("X-Request-ID") + if not request_id: + logging.warning("Missing X-Request-ID header. Idempotency check skipped.") + return func(*args, **kwargs) # Skip idempotency check if no request ID + + if request_id in processed_requests: + raise IdempotencyError(f"Request with ID {request_id} already processed.") + + try: + result = func(*args, **kwargs) + processed_requests[request_id] = True # Mark as processed + return result + except Exception as e: + logging.error(f"Error processing request: {e}") + raise + return wrapper + + +def retry(func: Callable, max_retries: int = MAX_RETRIES, delay: int = RETRY_DELAY) -> Callable: + """ + Decorator to add retry logic with exponential backoff. + + Args: + func: The function to decorate. + max_retries: The maximum number of retries. + delay: The initial delay in seconds. + + Returns: + The decorated function. + """ + @wraps(func) + def wrapper(*args, **kwargs): + attempts = 0 + while attempts < max_retries: + try: + return func(*args, **kwargs) + except Exception as e: + attempts += 1 + logging.warning(f"Attempt {attempts} failed: {e}. Retrying in {delay} seconds...") + time.sleep(delay) + delay *= 2 # Exponential backoff + logging.error(f"Max retries reached. Function {func.__name__} failed.") + raise + return wrapper + + +@app.route('/webhook', methods=['POST']) +@idempotent +@retry +def handle_webhook(): + """ + Handles incoming webhook requests. + + This function verifies the signature, processes the event, and returns a + success response. It also includes error handling and retry logic. + """ + signature = request.headers.get('X-Webhook-Signature') + if not signature: + logging.warning("Missing X-Webhook-Signature header.") + return jsonify({"error": "Missing signature"}), HTTPStatus.BAD_REQUEST + + request_data = request.get_data() + + try: + verify_signature(request_data, signature, WEBHOOK_SECRET) + except SignatureVerificationError as e: + logging.warning(f"Signature verification failed: {e}") + return jsonify({"error": str(e)}), HTTPStatus.UNAUTHORIZED + + try: + payload = json.loads(request_data.decode('utf-8')) + event_type = payload.get("type") # Example: Get event type from payload + + # Route the event to the appropriate handler (replace with your logic) + if event_type == "user.created": + process_user_created_event(payload) + elif event_type == "payment.succeeded": + process_payment_succeeded_event(payload) + else: + logging.warning(f"Unhandled event type: {event_type}") + return jsonify({"status": "unhandled"}), HTTPStatus.OK # Acknowledge the event + + return jsonify({"status": "success"}), HTTPStatus.OK + + except json.JSONDecodeError: + logging.error("Invalid JSON payload") + return jsonify({"error": "Invalid JSON payload"}), HTTPStatus.BAD_REQUEST + except Exception as e: + logging.exception("Error processing webhook") + return jsonify({"error": "Internal server error"}), HTTPStatus.INTERNAL_SERVER_ERROR + + +def process_user_created_event(payload: Dict) -> None: + """ + Processes a user.created event. This is a placeholder; replace with + your actual business logic. + + Args: + payload: The event payload as a dictionary. + """ + user_id = payload.get("user_id") + logging.info(f"Processing user.created event for user ID: {user_id}") + # Add your business logic here (e.g., create user in your system) + time.sleep(0.1) # Simulate some processing time + + +def process_payment_succeeded_event(payload: Dict) -> None: + """ + Processes a payment.succeeded event. This is a placeholder; replace with + your actual business logic. + + Args: + payload: The event payload as a dictionary. + """ + payment_id = payload.get("payment_id") + logging.info(f"Processing payment.succeeded event for payment ID: {payment_id}") + # Add your business logic here (e.g., update order status) + time.sleep(0.2) # Simulate some processing time + + +@app.errorhandler(SignatureVerificationError) +def handle_signature_error(error): + """Handles SignatureVerificationError exceptions.""" + return jsonify({"error": str(error)}), HTTPStatus.UNAUTHORIZED + + +@app.errorhandler(IdempotencyError) +def handle_idempotency_error(error): + """Handles IdempotencyError exceptions.""" + return jsonify({"error": str(error)}), HTTPStatus.CONFLICT + + +@app.errorhandler(Exception) +def handle_generic_error(error): + """Handles generic exceptions.""" + logging.exception("Unhandled exception") + return jsonify({"error": "Internal server error"}), HTTPStatus.INTERNAL_SERVER_ERROR + + +if __name__ == '__main__': + # Example Usage: + # + # 1. Set the WEBHOOK_SECRET environment variable. + # 2. Run the Flask app: python webhook_handler_template.py + # 3. Send a POST request to /webhook with a valid X-Webhook-Signature header. + # + # Example request: + # + # POST /webhook HTTP/1.1 + # X-Webhook-Signature: + # X-Request-ID: + # Content-Type: application/json + # + # { + # "type": "user.created", + # "user_id": "123" + # } + + app.run(debug=True, host='0.0.0.0', port=5000) \ No newline at end of file diff --git a/skills/skill-adapter/references/README.md b/skills/skill-adapter/references/README.md new file mode 100644 index 0000000..e7ea418 --- /dev/null +++ b/skills/skill-adapter/references/README.md @@ -0,0 +1,10 @@ +# References + +Bundled resources for webhook-handler-creator skill + +- [ ] webhook_security_best_practices.md: Detailed guide on securing webhooks, including signature verification, rate limiting, and input validation. +- [ ] idempotency_implementation.md: Explanation of idempotency and how to implement it in webhook handlers. +- [ ] retry_logic_implementation.md: Guide on implementing retry logic with exponential backoff for webhook handlers. +- [ ] hmac_signature_verification.md: In-depth explanation of HMAC signature verification. +- [ ] event_routing_strategies.md: Different strategies for routing webhook events to appropriate handlers. +- [ ] dead_letter_queue_implementation.md: Guide on setting up a dead letter queue for failed webhook events. diff --git a/skills/skill-adapter/references/best-practices.md b/skills/skill-adapter/references/best-practices.md new file mode 100644 index 0000000..3505048 --- /dev/null +++ b/skills/skill-adapter/references/best-practices.md @@ -0,0 +1,69 @@ +# Skill Best Practices + +Guidelines for optimal skill usage and development. + +## For Users + +### Activation Best Practices + +1. **Use Clear Trigger Phrases** + - Match phrases from skill description + - Be specific about intent + - Provide necessary context + +2. **Provide Sufficient Context** + - Include relevant file paths + - Specify scope of analysis + - Mention any constraints + +3. **Understand Tool Permissions** + - Check allowed-tools in frontmatter + - Know what the skill can/cannot do + - Request appropriate actions + +### Workflow Optimization + +- Start with simple requests +- Build up to complex workflows +- Verify each step before proceeding +- Use skill consistently for related tasks + +## For Developers + +### Skill Development Guidelines + +1. **Clear Descriptions** + - Include explicit trigger phrases + - Document all capabilities + - Specify limitations + +2. **Proper Tool Permissions** + - Use minimal necessary tools + - Document security implications + - Test with restricted tools + +3. **Comprehensive Documentation** + - Provide usage examples + - Document common pitfalls + - Include troubleshooting guide + +### Maintenance + +- Keep version updated +- Test after tool updates +- Monitor user feedback +- Iterate on descriptions + +## Performance Tips + +- Scope skills to specific domains +- Avoid overlapping trigger phrases +- Keep descriptions under 1024 chars +- Test activation reliability + +## Security Considerations + +- Never include secrets in skill files +- Validate all inputs +- Use read-only tools when possible +- Document security requirements diff --git a/skills/skill-adapter/references/examples.md b/skills/skill-adapter/references/examples.md new file mode 100644 index 0000000..b1d8bd2 --- /dev/null +++ b/skills/skill-adapter/references/examples.md @@ -0,0 +1,70 @@ +# Skill Usage Examples + +This document provides practical examples of how to use this skill effectively. + +## Basic Usage + +### Example 1: Simple Activation + +**User Request:** +``` +[Describe trigger phrase here] +``` + +**Skill Response:** +1. Analyzes the request +2. Performs the required action +3. Returns results + +### Example 2: Complex Workflow + +**User Request:** +``` +[Describe complex scenario] +``` + +**Workflow:** +1. Step 1: Initial analysis +2. Step 2: Data processing +3. Step 3: Result generation +4. Step 4: Validation + +## Advanced Patterns + +### Pattern 1: Chaining Operations + +Combine this skill with other tools: +``` +Step 1: Use this skill for [purpose] +Step 2: Chain with [other tool] +Step 3: Finalize with [action] +``` + +### Pattern 2: Error Handling + +If issues occur: +- Check trigger phrase matches +- Verify context is available +- Review allowed-tools permissions + +## Tips & Best Practices + +- ✅ Be specific with trigger phrases +- ✅ Provide necessary context +- ✅ Check tool permissions match needs +- ❌ Avoid vague requests +- ❌ Don't mix unrelated tasks + +## Common Issues + +**Issue:** Skill doesn't activate +**Solution:** Use exact trigger phrases from description + +**Issue:** Unexpected results +**Solution:** Check input format and context + +## See Also + +- Main SKILL.md for full documentation +- scripts/ for automation helpers +- assets/ for configuration examples diff --git a/skills/skill-adapter/scripts/README.md b/skills/skill-adapter/scripts/README.md new file mode 100644 index 0000000..ce361de --- /dev/null +++ b/skills/skill-adapter/scripts/README.md @@ -0,0 +1,7 @@ +# Scripts + +Bundled resources for webhook-handler-creator skill + +- [ ] create_webhook_handler.py: Generates a basic webhook handler with signature verification and idempotency logic. +- [ ] test_webhook_handler.py: Creates test cases for the generated webhook handler. +- [ ] deploy_webhook_handler.sh: Deploys the webhook handler to a server (e.g., using Flask or FastAPI). diff --git a/skills/skill-adapter/scripts/helper-template.sh b/skills/skill-adapter/scripts/helper-template.sh new file mode 100755 index 0000000..c4aae90 --- /dev/null +++ b/skills/skill-adapter/scripts/helper-template.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# Helper script template for skill automation +# Customize this for your skill's specific needs + +set -e + +function show_usage() { + echo "Usage: $0 [options]" + echo "" + echo "Options:" + echo " -h, --help Show this help message" + echo " -v, --verbose Enable verbose output" + echo "" +} + +# Parse arguments +VERBOSE=false + +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + show_usage + exit 0 + ;; + -v|--verbose) + VERBOSE=true + shift + ;; + *) + echo "Unknown option: $1" + show_usage + exit 1 + ;; + esac +done + +# Your skill logic here +if [ "$VERBOSE" = true ]; then + echo "Running skill automation..." +fi + +echo "✅ Complete" diff --git a/skills/skill-adapter/scripts/validation.sh b/skills/skill-adapter/scripts/validation.sh new file mode 100755 index 0000000..590af58 --- /dev/null +++ b/skills/skill-adapter/scripts/validation.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# Skill validation helper +# Validates skill activation and functionality + +set -e + +echo "🔍 Validating skill..." + +# Check if SKILL.md exists +if [ ! -f "../SKILL.md" ]; then + echo "❌ Error: SKILL.md not found" + exit 1 +fi + +# Validate frontmatter +if ! grep -q "^---$" "../SKILL.md"; then + echo "❌ Error: No frontmatter found" + exit 1 +fi + +# Check required fields +if ! grep -q "^name:" "../SKILL.md"; then + echo "❌ Error: Missing 'name' field" + exit 1 +fi + +if ! grep -q "^description:" "../SKILL.md"; then + echo "❌ Error: Missing 'description' field" + exit 1 +fi + +echo "✅ Skill validation passed"