UNPKG

skills-ws

Version:

87 agent skills for AI coding assistants — marketing, growth, web3, dev, design & operations. Built for OpenClaw, Claude Code, Cursor, and Codex.

103 lines (84 loc) • 3.35 kB
## Contents - Security - Webhook Signature Verification (Mandatory) - Timing-Safe Comparison for API Keys - Rate Limiting - Secure Key Storage ## Security ### Webhook Signature Verification (Mandatory) Already covered above. **Never skip this.** Without it, anyone can POST fake events to your webhook endpoint. ### Timing-Safe Comparison for API Keys ```js const crypto = require('crypto'); // WRONG — vulnerable to timing attacks // if (providedKey === storedKey) { ... } // RIGHT — constant-time comparison function secureCompare(a, b) { if (typeof a !== 'string' || typeof b !== 'string') return false; const bufA = Buffer.from(a); const bufB = Buffer.from(b); if (bufA.length !== bufB.length) return false; return crypto.timingSafeEqual(bufA, bufB); } // For hashed keys (what you should actually do): // Hash the incoming key, then compare hashes. SHA-256 is fixed-length, // so timingSafeEqual works perfectly. function validateKeyHash(providedKey, storedHash) { const providedHash = crypto.createHash('sha256').update(providedKey).digest('hex'); return secureCompare(providedHash, storedHash); } ``` ### Rate Limiting ```js const rateLimit = require('express-rate-limit'); // Global rate limit const globalLimiter = rateLimit({ windowMs: 60 * 1000, max: 100, standardHeaders: true, legacyHeaders: false, message: { error: 'Too many requests' }, }); // Per-plan rate limit — pre-create one limiter per plan to avoid // creating a new rateLimit instance on every request (which resets // the window each time, making it nonfunctional). const planLimiters = Object.fromEntries( Object.entries(PLAN_LIMITS).map(([plan, limits]) => [ plan, rateLimit({ windowMs: 60 * 1000, max: limits.rpm, keyGenerator: (req) => req.userId, standardHeaders: true, message: { error: 'rate_limit_exceeded', limit: limits.rpm, window: '1m', }, }), ]) ); function planRateLimiter(req, res, next) { const limiter = planLimiters[req.plan]; if (!limiter) return res.status(403).json({ error: 'No plan' }); return limiter(req, res, next); } // ⚠️ Do NOT rate-limit the Stripe webhook endpoint by request volume before // verifying the signature. Stripe legitimately bursts events (backfills, // migrations, incident recovery) and a 429 just triggers retries, growing a // backlog and risking dropped events past Stripe's retry window. // // Instead: (1) verify the signature first — that IS your authentication and // rejects forged/replayed payloads; (2) keep the handler cheap by enqueuing // work and returning 200 fast; (3) protect the box with a generous infra-level // connection/QPS cap (LB/WAF), not an app-level per-window cap that drops valid // events. If you must cap in-app, cap AFTER verification and only on bodies that // fail signature checks (i.e. throttle attackers, never Stripe). app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), handleStripeWebhook); ``` ### Secure Key Storage - **Never log full API keys.** Log only the prefix (`myapp_live_a1b2...`). - **Never store plaintext keys.** Always hash with SHA-256. - **Rotate webhook secrets** periodically via Stripe Dashboard. - **Use separate restricted API keys** for different services (read-only for analytics, write for billing). ---