@girardmedia/bootspring 3.3.2 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/assets/agents/accessibility-auditor.md +39 -0
  2. package/assets/agents/api-designer.md +40 -0
  3. package/assets/agents/auth-implementer.md +64 -0
  4. package/assets/agents/bug-hunter.md +42 -0
  5. package/assets/agents/bundle-analyzer.md +40 -0
  6. package/assets/agents/cache-optimizer.md +55 -0
  7. package/assets/agents/changelog-writer.md +55 -0
  8. package/assets/agents/ci-cd-builder.md +40 -0
  9. package/assets/agents/code-explainer.md +39 -0
  10. package/assets/agents/code-reviewer.md +39 -0
  11. package/assets/agents/cost-optimizer.md +57 -0
  12. package/assets/agents/cron-scheduler.md +51 -0
  13. package/assets/agents/data-seeder.md +56 -0
  14. package/assets/agents/database-architect.md +40 -0
  15. package/assets/agents/dependency-updater.md +40 -0
  16. package/assets/agents/deploy-checker.md +40 -0
  17. package/assets/agents/docker-optimizer.md +40 -0
  18. package/assets/agents/documentation-writer.md +40 -0
  19. package/assets/agents/email-builder.md +55 -0
  20. package/assets/agents/env-setup.md +40 -0
  21. package/assets/agents/error-handler.md +40 -0
  22. package/assets/agents/eslint-fixer.md +46 -0
  23. package/assets/agents/feature-flagger.md +69 -0
  24. package/assets/agents/git-detective.md +39 -0
  25. package/assets/agents/graphql-builder.md +60 -0
  26. package/assets/agents/incident-responder.md +59 -0
  27. package/assets/agents/log-analyzer.md +39 -0
  28. package/assets/agents/migration-planner.md +41 -0
  29. package/assets/agents/monorepo-navigator.md +39 -0
  30. package/assets/agents/nextjs-expert.md +57 -0
  31. package/assets/agents/notification-builder.md +56 -0
  32. package/assets/agents/onboarding-guide.md +39 -0
  33. package/assets/agents/performance-profiler.md +40 -0
  34. package/assets/agents/prisma-expert.md +57 -0
  35. package/assets/agents/rate-limiter.md +58 -0
  36. package/assets/agents/react-expert.md +58 -0
  37. package/assets/agents/refactorer.md +42 -0
  38. package/assets/agents/regex-builder.md +46 -0
  39. package/assets/agents/release-manager.md +40 -0
  40. package/assets/agents/s3-manager.md +58 -0
  41. package/assets/agents/schema-validator.md +40 -0
  42. package/assets/agents/search-builder.md +62 -0
  43. package/assets/agents/security-auditor.md +39 -0
  44. package/assets/agents/sitemap-generator.md +53 -0
  45. package/assets/agents/stripe-integrator.md +59 -0
  46. package/assets/agents/tailwind-expert.md +55 -0
  47. package/assets/agents/tech-debt-tracker.md +39 -0
  48. package/assets/agents/test-writer.md +42 -0
  49. package/assets/agents/type-fixer.md +45 -0
  50. package/assets/agents/webhook-builder.md +54 -0
  51. package/assets/rules/cpp.md +53 -0
  52. package/assets/rules/css.md +52 -0
  53. package/assets/rules/go.md +50 -0
  54. package/assets/rules/html.md +52 -0
  55. package/assets/rules/java.md +51 -0
  56. package/assets/rules/kotlin.md +50 -0
  57. package/assets/rules/php.md +51 -0
  58. package/assets/rules/python.md +51 -0
  59. package/assets/rules/ruby.md +51 -0
  60. package/assets/rules/rust.md +49 -0
  61. package/assets/rules/shell.md +52 -0
  62. package/assets/rules/sql.md +49 -0
  63. package/assets/rules/swift.md +50 -0
  64. package/assets/rules/typescript.md +52 -0
  65. package/assets/rules/yaml-json.md +51 -0
  66. package/assets/skills/accessibility.md +210 -0
  67. package/assets/skills/agent-patterns.md +387 -0
  68. package/assets/skills/ai-integration.md +263 -0
  69. package/assets/skills/animation-patterns.md +224 -0
  70. package/assets/skills/api-design.md +218 -0
  71. package/assets/skills/api-gateway.md +341 -0
  72. package/assets/skills/api-versioning.md +226 -0
  73. package/assets/skills/astro-patterns.md +233 -0
  74. package/assets/skills/auth-patterns.md +248 -0
  75. package/assets/skills/aws-patterns.md +171 -0
  76. package/assets/skills/background-jobs.md +162 -0
  77. package/assets/skills/browser-extensions.md +309 -0
  78. package/assets/skills/caching-patterns.md +253 -0
  79. package/assets/skills/ci-cd.md +251 -0
  80. package/assets/skills/cli-development.md +296 -0
  81. package/assets/skills/code-review.md +185 -0
  82. package/assets/skills/cron-patterns.md +327 -0
  83. package/assets/skills/data-fetching.md +231 -0
  84. package/assets/skills/database-migrations.md +346 -0
  85. package/assets/skills/database-patterns.md +219 -0
  86. package/assets/skills/debugging.md +281 -0
  87. package/assets/skills/design-system.md +289 -0
  88. package/assets/skills/django-patterns.md +182 -0
  89. package/assets/skills/docker-patterns.md +235 -0
  90. package/assets/skills/e2e-testing.md +287 -0
  91. package/assets/skills/edge-computing.md +268 -0
  92. package/assets/skills/electron-patterns.md +266 -0
  93. package/assets/skills/email-templates.md +206 -0
  94. package/assets/skills/error-handling.md +265 -0
  95. package/assets/skills/event-driven.md +232 -0
  96. package/assets/skills/express-patterns.md +239 -0
  97. package/assets/skills/fastapi-patterns.md +198 -0
  98. package/assets/skills/feature-flags.md +212 -0
  99. package/assets/skills/figma-to-code.md +298 -0
  100. package/assets/skills/file-upload.md +228 -0
  101. package/assets/skills/forms-patterns.md +264 -0
  102. package/assets/skills/gcp-patterns.md +189 -0
  103. package/assets/skills/git-workflow.md +187 -0
  104. package/assets/skills/golang-patterns.md +185 -0
  105. package/assets/skills/graphql-patterns.md +244 -0
  106. package/assets/skills/i18n-patterns.md +172 -0
  107. package/assets/skills/image-processing.md +350 -0
  108. package/assets/skills/java-springboot.md +226 -0
  109. package/assets/skills/kotlin-patterns.md +207 -0
  110. package/assets/skills/kubernetes-patterns.md +326 -0
  111. package/assets/skills/laravel-patterns.md +261 -0
  112. package/assets/skills/llm-fine-tuning.md +335 -0
  113. package/assets/skills/load-testing.md +303 -0
  114. package/assets/skills/logging-observability.md +228 -0
  115. package/assets/skills/markdown-processing.md +318 -0
  116. package/assets/skills/mcp-server-patterns.md +292 -0
  117. package/assets/skills/microservices.md +272 -0
  118. package/assets/skills/migration-patterns.md +239 -0
  119. package/assets/skills/mongodb-patterns.md +189 -0
  120. package/assets/skills/monorepo-patterns.md +287 -0
  121. package/assets/skills/nextjs-app-router.md +237 -0
  122. package/assets/skills/notification-patterns.md +348 -0
  123. package/assets/skills/oauth-patterns.md +246 -0
  124. package/assets/skills/payment-integration.md +222 -0
  125. package/assets/skills/pdf-generation.md +307 -0
  126. package/assets/skills/performance-optimization.md +277 -0
  127. package/assets/skills/php-patterns.md +210 -0
  128. package/assets/skills/prisma-patterns.md +241 -0
  129. package/assets/skills/prompt-engineering.md +193 -0
  130. package/assets/skills/pwa-patterns.md +247 -0
  131. package/assets/skills/python-patterns.md +158 -0
  132. package/assets/skills/python-testing.md +172 -0
  133. package/assets/skills/queue-patterns.md +295 -0
  134. package/assets/skills/rag-patterns.md +159 -0
  135. package/assets/skills/rate-limiting.md +319 -0
  136. package/assets/skills/react-components.md +201 -0
  137. package/assets/skills/react-native-patterns.md +299 -0
  138. package/assets/skills/real-time-patterns.md +181 -0
  139. package/assets/skills/redis-patterns.md +188 -0
  140. package/assets/skills/refactoring.md +218 -0
  141. package/assets/skills/regex-patterns.md +191 -0
  142. package/assets/skills/remix-patterns.md +262 -0
  143. package/assets/skills/responsive-design.md +199 -0
  144. package/assets/skills/ruby-rails-patterns.md +178 -0
  145. package/assets/skills/rust-patterns.md +211 -0
  146. package/assets/skills/search-patterns.md +227 -0
  147. package/assets/skills/security-hardening.md +237 -0
  148. package/assets/skills/seo-patterns.md +179 -0
  149. package/assets/skills/serverless-patterns.md +223 -0
  150. package/assets/skills/sql-optimization.md +154 -0
  151. package/assets/skills/state-management.md +254 -0
  152. package/assets/skills/storybook-patterns.md +330 -0
  153. package/assets/skills/svelte-patterns.md +258 -0
  154. package/assets/skills/swift-patterns.md +227 -0
  155. package/assets/skills/tailwind-patterns.md +272 -0
  156. package/assets/skills/tdd-workflow.md +199 -0
  157. package/assets/skills/terraform-patterns.md +270 -0
  158. package/assets/skills/testing-react.md +240 -0
  159. package/assets/skills/testing-vitest.md +232 -0
  160. package/assets/skills/typescript-strict.md +159 -0
  161. package/assets/skills/video-processing.md +340 -0
  162. package/assets/skills/vue-patterns.md +247 -0
  163. package/assets/skills/web-workers.md +327 -0
  164. package/assets/skills/webhooks-patterns.md +283 -0
  165. package/assets/skills/websocket-patterns.md +306 -0
  166. package/dist/cli/index.js +941 -958
  167. package/dist/core/index.d.ts +341 -11
  168. package/dist/core.js +58 -95
  169. package/dist/mcp/index.d.ts +33 -1
  170. package/dist/mcp-server.js +177 -255
  171. package/package.json +4 -1
@@ -0,0 +1,327 @@
1
+ ---
2
+ name: web-workers
3
+ description: Web Worker patterns for shared workers, service workers, Comlink RPC, transferable objects, and WebAssembly integration.
4
+ ---
5
+
6
+ # Web Worker Patterns
7
+
8
+ ## When to Use
9
+ Use Web Workers when you need to run CPU-intensive tasks without blocking the main thread. This includes image processing, data parsing, cryptographic operations, sorting large datasets, and running WebAssembly modules. Workers keep the UI responsive by moving computation off the main thread. Use Comlink to simplify the messaging API, and transferable objects to avoid expensive data copies.
10
+
11
+ ## How It Works
12
+
13
+ ### Basic Dedicated Worker
14
+
15
+ ```typescript
16
+ // src/workers/data-processor.worker.ts
17
+ interface ProcessRequest {
18
+ type: 'sort' | 'filter' | 'aggregate';
19
+ data: number[];
20
+ options?: { ascending?: boolean; threshold?: number };
21
+ }
22
+
23
+ self.addEventListener('message', (event: MessageEvent<ProcessRequest>) => {
24
+ const { type, data, options } = event.data;
25
+
26
+ let result: unknown;
27
+
28
+ switch (type) {
29
+ case 'sort':
30
+ result = [...data].sort((a, b) => options?.ascending ? a - b : b - a);
31
+ break;
32
+ case 'filter':
33
+ result = data.filter((n) => n >= (options?.threshold ?? 0));
34
+ break;
35
+ case 'aggregate':
36
+ result = {
37
+ sum: data.reduce((a, b) => a + b, 0),
38
+ avg: data.reduce((a, b) => a + b, 0) / data.length,
39
+ min: Math.min(...data),
40
+ max: Math.max(...data),
41
+ };
42
+ break;
43
+ }
44
+
45
+ self.postMessage({ type, result });
46
+ });
47
+ ```
48
+
49
+ ```typescript
50
+ // src/use-worker.ts
51
+ export function createDataProcessor() {
52
+ const worker = new Worker(
53
+ new URL('./workers/data-processor.worker.ts', import.meta.url),
54
+ { type: 'module' }
55
+ );
56
+
57
+ return {
58
+ process(request: ProcessRequest): Promise<unknown> {
59
+ return new Promise((resolve) => {
60
+ const handler = (event: MessageEvent) => {
61
+ if (event.data.type === request.type) {
62
+ worker.removeEventListener('message', handler);
63
+ resolve(event.data.result);
64
+ }
65
+ };
66
+ worker.addEventListener('message', handler);
67
+ worker.postMessage(request);
68
+ });
69
+ },
70
+ terminate: () => worker.terminate(),
71
+ };
72
+ }
73
+ ```
74
+
75
+ ### Comlink RPC (Simplified Messaging)
76
+
77
+ ```typescript
78
+ // src/workers/image-processor.worker.ts
79
+ import * as Comlink from 'comlink';
80
+
81
+ class ImageProcessor {
82
+ async resize(imageData: ImageData, width: number, height: number): Promise<ImageData> {
83
+ const canvas = new OffscreenCanvas(width, height);
84
+ const ctx = canvas.getContext('2d')!;
85
+ const bitmap = await createImageBitmap(imageData);
86
+ ctx.drawImage(bitmap, 0, 0, width, height);
87
+ return ctx.getImageData(0, 0, width, height);
88
+ }
89
+
90
+ async grayscale(imageData: ImageData): Promise<ImageData> {
91
+ const data = new Uint8ClampedArray(imageData.data);
92
+ for (let i = 0; i < data.length; i += 4) {
93
+ const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
94
+ data[i] = data[i + 1] = data[i + 2] = avg;
95
+ }
96
+ return new ImageData(data, imageData.width, imageData.height);
97
+ }
98
+
99
+ async blur(imageData: ImageData, radius: number): Promise<ImageData> {
100
+ const { width, height, data } = imageData;
101
+ const output = new Uint8ClampedArray(data);
102
+ // Simple box blur
103
+ for (let y = radius; y < height - radius; y++) {
104
+ for (let x = radius; x < width - radius; x++) {
105
+ for (let c = 0; c < 3; c++) {
106
+ let sum = 0, count = 0;
107
+ for (let dy = -radius; dy <= radius; dy++) {
108
+ for (let dx = -radius; dx <= radius; dx++) {
109
+ sum += data[((y + dy) * width + (x + dx)) * 4 + c];
110
+ count++;
111
+ }
112
+ }
113
+ output[(y * width + x) * 4 + c] = sum / count;
114
+ }
115
+ }
116
+ }
117
+ return new ImageData(output, width, height);
118
+ }
119
+ }
120
+
121
+ Comlink.expose(new ImageProcessor());
122
+ ```
123
+
124
+ ```typescript
125
+ // src/image-client.ts
126
+ import * as Comlink from 'comlink';
127
+
128
+ type ImageProcessor = {
129
+ resize(data: ImageData, w: number, h: number): Promise<ImageData>;
130
+ grayscale(data: ImageData): Promise<ImageData>;
131
+ blur(data: ImageData, radius: number): Promise<ImageData>;
132
+ };
133
+
134
+ const worker = new Worker(
135
+ new URL('./workers/image-processor.worker.ts', import.meta.url),
136
+ { type: 'module' }
137
+ );
138
+ const processor = Comlink.wrap<ImageProcessor>(worker);
139
+
140
+ // Usage — call worker methods like local async functions
141
+ const resized = await processor.resize(imageData, 800, 600);
142
+ const gray = await processor.grayscale(imageData);
143
+ ```
144
+
145
+ ### Transferable Objects (Zero-Copy)
146
+
147
+ ```typescript
148
+ // src/workers/transfer-worker.ts
149
+ self.addEventListener('message', (event: MessageEvent) => {
150
+ const buffer: ArrayBuffer = event.data;
151
+ const view = new Float64Array(buffer);
152
+
153
+ // Process in-place
154
+ for (let i = 0; i < view.length; i++) {
155
+ view[i] = Math.sqrt(view[i]);
156
+ }
157
+
158
+ // Transfer back — zero copy
159
+ self.postMessage(buffer, [buffer]);
160
+ });
161
+ ```
162
+
163
+ ```typescript
164
+ // Main thread — transfer ownership to worker
165
+ const data = new Float64Array(1_000_000);
166
+ for (let i = 0; i < data.length; i++) data[i] = Math.random() * 1000;
167
+
168
+ // Transfer ownership — main thread loses access, no copy made
169
+ worker.postMessage(data.buffer, [data.buffer]);
170
+ // data.buffer.byteLength === 0 after transfer
171
+
172
+ worker.addEventListener('message', (event) => {
173
+ const result = new Float64Array(event.data);
174
+ console.log('Processed:', result.length, 'items');
175
+ });
176
+ ```
177
+
178
+ ### Shared Worker (Multiple Tabs)
179
+
180
+ ```typescript
181
+ // src/workers/shared-state.worker.ts
182
+ const connections: MessagePort[] = [];
183
+ let sharedState: Record<string, unknown> = {};
184
+
185
+ self.addEventListener('connect', (event: MessageEvent) => {
186
+ const port = (event as any).ports[0] as MessagePort;
187
+ connections.push(port);
188
+
189
+ port.addEventListener('message', (msg: MessageEvent) => {
190
+ const { action, key, value } = msg.data;
191
+
192
+ if (action === 'set') {
193
+ sharedState[key] = value;
194
+ // Broadcast to all connected tabs
195
+ connections.forEach((p) => p.postMessage({ type: 'update', state: sharedState }));
196
+ }
197
+
198
+ if (action === 'get') {
199
+ port.postMessage({ type: 'state', state: sharedState });
200
+ }
201
+ });
202
+
203
+ port.start();
204
+ port.postMessage({ type: 'state', state: sharedState });
205
+ });
206
+ ```
207
+
208
+ ```typescript
209
+ // Main thread — connect to shared worker
210
+ const shared = new SharedWorker(
211
+ new URL('./workers/shared-state.worker.ts', import.meta.url),
212
+ { type: 'module' }
213
+ );
214
+
215
+ shared.port.addEventListener('message', (event) => {
216
+ if (event.data.type === 'update') {
217
+ console.log('State updated across tabs:', event.data.state);
218
+ }
219
+ });
220
+ shared.port.start();
221
+
222
+ // Set value (broadcasts to all tabs)
223
+ shared.port.postMessage({ action: 'set', key: 'user', value: { name: 'Alice' } });
224
+ ```
225
+
226
+ ### WASM Integration in Worker
227
+
228
+ ```typescript
229
+ // src/workers/wasm-worker.ts
230
+ import * as Comlink from 'comlink';
231
+
232
+ let wasmInstance: WebAssembly.Instance | null = null;
233
+
234
+ async function initWasm(): Promise<void> {
235
+ if (wasmInstance) return;
236
+ const response = await fetch('/compute.wasm');
237
+ const { instance } = await WebAssembly.instantiateStreaming(response);
238
+ wasmInstance = instance;
239
+ }
240
+
241
+ const wasmApi = {
242
+ async fibonacci(n: number): Promise<number> {
243
+ await initWasm();
244
+ return (wasmInstance!.exports.fibonacci as (n: number) => number)(n);
245
+ },
246
+
247
+ async processBuffer(data: ArrayBuffer): Promise<ArrayBuffer> {
248
+ await initWasm();
249
+ const memory = wasmInstance!.exports.memory as WebAssembly.Memory;
250
+ const alloc = wasmInstance!.exports.alloc as (size: number) => number;
251
+ const process = wasmInstance!.exports.process as (ptr: number, len: number) => number;
252
+
253
+ const inputPtr = alloc(data.byteLength);
254
+ new Uint8Array(memory.buffer, inputPtr, data.byteLength).set(new Uint8Array(data));
255
+
256
+ const outputPtr = process(inputPtr, data.byteLength);
257
+ const output = new Uint8Array(memory.buffer, outputPtr, data.byteLength).slice();
258
+
259
+ return output.buffer;
260
+ },
261
+ };
262
+
263
+ Comlink.expose(wasmApi);
264
+ ```
265
+
266
+ ### Worker Pool
267
+
268
+ ```typescript
269
+ // src/worker-pool.ts
270
+ export class WorkerPool<T> {
271
+ private workers: Worker[] = [];
272
+ private queue: Array<{ resolve: (v: T) => void; args: unknown }> = [];
273
+ private busy = new Set<Worker>();
274
+
275
+ constructor(private workerUrl: URL, private size: number = navigator.hardwareConcurrency) {
276
+ for (let i = 0; i < this.size; i++) {
277
+ this.workers.push(new Worker(workerUrl, { type: 'module' }));
278
+ }
279
+ }
280
+
281
+ async execute(args: unknown): Promise<T> {
282
+ const available = this.workers.find((w) => !this.busy.has(w));
283
+ if (available) return this.dispatch(available, args);
284
+
285
+ return new Promise((resolve) => {
286
+ this.queue.push({ resolve, args });
287
+ });
288
+ }
289
+
290
+ private dispatch(worker: Worker, args: unknown): Promise<T> {
291
+ this.busy.add(worker);
292
+ return new Promise((resolve) => {
293
+ worker.onmessage = (event: MessageEvent<T>) => {
294
+ this.busy.delete(worker);
295
+ resolve(event.data);
296
+ const next = this.queue.shift();
297
+ if (next) this.dispatch(worker, next.args).then(next.resolve);
298
+ };
299
+ worker.postMessage(args);
300
+ });
301
+ }
302
+
303
+ terminate() {
304
+ this.workers.forEach((w) => w.terminate());
305
+ }
306
+ }
307
+ ```
308
+
309
+ ## Examples
310
+
311
+ | Pattern | Overhead | Use Case |
312
+ |---------|----------|----------|
313
+ | Dedicated Worker | ~1ms message | Image processing, sorting |
314
+ | Shared Worker | ~1ms message | Cross-tab state sync |
315
+ | Comlink | ~2ms message | Complex API, many methods |
316
+ | Transferable | Zero-copy | Large ArrayBuffer operations |
317
+ | Worker Pool | Pool management | Parallel batch processing |
318
+ | WASM in Worker | Init once | Heavy compute (crypto, codec) |
319
+
320
+ ## Checklist
321
+ - [ ] Workers created with `new URL()` for bundler compatibility
322
+ - [ ] Comlink used for complex worker APIs to avoid manual message handling
323
+ - [ ] Large buffers transferred, not copied, using `[buffer]` transfer list
324
+ - [ ] Worker pool size matches `navigator.hardwareConcurrency`
325
+ - [ ] Workers terminated when no longer needed to free resources
326
+ - [ ] Error handler (`onerror`) set on every worker instance
327
+ - [ ] Feature detection before using SharedWorker or WebAssembly
@@ -0,0 +1,283 @@
1
+ ---
2
+ name: webhooks-patterns
3
+ description: Webhook patterns with signature verification, retry logic, idempotency keys, event ordering, and testing.
4
+ ---
5
+
6
+ # Webhook Patterns
7
+
8
+ ## When to Use
9
+ Apply when building webhook consumers (receiving events from Stripe, GitHub, etc.) or webhook producers (sending events to your users' endpoints). Webhooks are HTTP callbacks -- the sender POSTs to your URL when an event occurs. The key challenges are verification, reliability, idempotency, and ordering.
10
+
11
+ ## How It Works
12
+
13
+ ### Receiving Webhooks -- Signature Verification
14
+
15
+ Always verify the webhook signature. Never trust the payload without it:
16
+
17
+ ```typescript
18
+ import crypto from "crypto";
19
+ import express from "express";
20
+
21
+ // Stripe signature verification
22
+ app.post(
23
+ "/webhooks/stripe",
24
+ express.raw({ type: "application/json" }),
25
+ (req, res) => {
26
+ const sig = req.headers["stripe-signature"] as string;
27
+ try {
28
+ const event = stripe.webhooks.constructEvent(
29
+ req.body,
30
+ sig,
31
+ process.env.STRIPE_WEBHOOK_SECRET!
32
+ );
33
+ processEvent(event);
34
+ res.json({ received: true });
35
+ } catch (err) {
36
+ res.status(400).send("Invalid signature");
37
+ }
38
+ }
39
+ );
40
+
41
+ // Generic HMAC-SHA256 verification (GitHub, custom webhooks)
42
+ function verifyWebhookSignature(
43
+ payload: Buffer,
44
+ signature: string,
45
+ secret: string
46
+ ): boolean {
47
+ const expected = crypto
48
+ .createHmac("sha256", secret)
49
+ .update(payload)
50
+ .digest("hex");
51
+ const received = signature.replace("sha256=", "");
52
+ return crypto.timingSafeEqual(
53
+ Buffer.from(expected, "hex"),
54
+ Buffer.from(received, "hex")
55
+ );
56
+ }
57
+
58
+ app.post(
59
+ "/webhooks/github",
60
+ express.raw({ type: "application/json" }),
61
+ (req, res) => {
62
+ const sig = req.headers["x-hub-signature-256"] as string;
63
+ if (!verifyWebhookSignature(req.body, sig, process.env.GITHUB_WEBHOOK_SECRET!)) {
64
+ return res.status(401).send("Invalid signature");
65
+ }
66
+ const event = JSON.parse(req.body.toString());
67
+ processGitHubEvent(event);
68
+ res.status(200).send("OK");
69
+ }
70
+ );
71
+ ```
72
+
73
+ ### Idempotent Processing
74
+
75
+ Webhooks may be delivered more than once. Make handlers safe to re-run:
76
+
77
+ ```typescript
78
+ async function processWebhookIdempotent(
79
+ eventId: string,
80
+ handler: () => Promise<void>
81
+ ): Promise<void> {
82
+ // Atomic check-and-set in Redis
83
+ const acquired = await redis.set(
84
+ `webhook:processed:${eventId}`, "1", "NX", "EX", 86400
85
+ );
86
+ if (!acquired) {
87
+ logger.info({ eventId }, "Duplicate webhook, skipping");
88
+ return;
89
+ }
90
+
91
+ try {
92
+ await handler();
93
+ } catch (err) {
94
+ // Remove the key so the webhook can be retried
95
+ await redis.del(`webhook:processed:${eventId}`);
96
+ throw err;
97
+ }
98
+ }
99
+
100
+ // Or use database constraints
101
+ await db.query(
102
+ `INSERT INTO webhook_events (event_id, type, processed_at)
103
+ VALUES ($1, $2, NOW())
104
+ ON CONFLICT (event_id) DO NOTHING`,
105
+ [event.id, event.type]
106
+ );
107
+ ```
108
+
109
+ ### Sending Webhooks -- Producer Side
110
+
111
+ ```typescript
112
+ interface WebhookDelivery {
113
+ id: string;
114
+ endpoint: string;
115
+ event: string;
116
+ payload: object;
117
+ attempt: number;
118
+ nextRetryAt: Date | null;
119
+ deliveredAt: Date | null;
120
+ statusCode: number | null;
121
+ }
122
+
123
+ async function sendWebhook(
124
+ endpoint: string,
125
+ secret: string,
126
+ event: string,
127
+ payload: object
128
+ ): Promise<void> {
129
+ const body = JSON.stringify(payload);
130
+ const timestamp = Math.floor(Date.now() / 1000);
131
+ const signature = crypto
132
+ .createHmac("sha256", secret)
133
+ .update(`${timestamp}.${body}`)
134
+ .digest("hex");
135
+
136
+ const response = await fetch(endpoint, {
137
+ method: "POST",
138
+ headers: {
139
+ "Content-Type": "application/json",
140
+ "X-Webhook-Signature": `t=${timestamp},v1=${signature}`,
141
+ "X-Webhook-Event": event,
142
+ "X-Webhook-ID": crypto.randomUUID(),
143
+ },
144
+ body,
145
+ signal: AbortSignal.timeout(10000), // 10s timeout
146
+ });
147
+
148
+ if (!response.ok) {
149
+ throw new Error(`Webhook delivery failed: ${response.status}`);
150
+ }
151
+ }
152
+ ```
153
+
154
+ ### Retry Logic with Exponential Backoff
155
+
156
+ ```typescript
157
+ const RETRY_SCHEDULE = [
158
+ 60, // 1 minute
159
+ 300, // 5 minutes
160
+ 1800, // 30 minutes
161
+ 7200, // 2 hours
162
+ 28800, // 8 hours
163
+ 86400, // 24 hours
164
+ ];
165
+
166
+ async function deliverWithRetry(delivery: WebhookDelivery): Promise<void> {
167
+ try {
168
+ await sendWebhook(
169
+ delivery.endpoint,
170
+ delivery.secret,
171
+ delivery.event,
172
+ delivery.payload
173
+ );
174
+ await db.webhookDeliveries.update(delivery.id, {
175
+ deliveredAt: new Date(),
176
+ statusCode: 200,
177
+ });
178
+ } catch (err) {
179
+ const nextAttempt = delivery.attempt + 1;
180
+ if (nextAttempt >= RETRY_SCHEDULE.length) {
181
+ await db.webhookDeliveries.update(delivery.id, {
182
+ statusCode: 0,
183
+ failedPermanently: true,
184
+ });
185
+ logger.error({ deliveryId: delivery.id }, "Webhook permanently failed");
186
+ return;
187
+ }
188
+
189
+ const delaySeconds = RETRY_SCHEDULE[nextAttempt];
190
+ await db.webhookDeliveries.update(delivery.id, {
191
+ attempt: nextAttempt,
192
+ nextRetryAt: new Date(Date.now() + delaySeconds * 1000),
193
+ lastError: (err as Error).message,
194
+ });
195
+ }
196
+ }
197
+ ```
198
+
199
+ ### Event Ordering
200
+
201
+ Webhooks may arrive out of order. Handle this gracefully:
202
+
203
+ ```typescript
204
+ // Include a timestamp or sequence number in the event
205
+ interface WebhookEvent {
206
+ id: string;
207
+ type: string;
208
+ createdAt: string; // ISO timestamp
209
+ data: Record<string, unknown>;
210
+ }
211
+
212
+ // For order-sensitive events, check the timestamp
213
+ async function handleOrderStatusChange(event: WebhookEvent): Promise<void> {
214
+ const order = await db.orders.findById(event.data.orderId as string);
215
+
216
+ // Ignore events older than the last processed event
217
+ if (order.lastWebhookAt && new Date(event.createdAt) < order.lastWebhookAt) {
218
+ logger.warn({ eventId: event.id }, "Out-of-order webhook, skipping");
219
+ return;
220
+ }
221
+
222
+ await db.orders.update(order.id, {
223
+ status: event.data.status as string,
224
+ lastWebhookAt: new Date(event.createdAt),
225
+ });
226
+ }
227
+ ```
228
+
229
+ ### Testing Webhooks Locally
230
+
231
+ ```bash
232
+ # Stripe CLI -- forward webhooks to localhost
233
+ stripe listen --forward-to localhost:3000/webhooks/stripe
234
+
235
+ # ngrok -- expose local server to the internet
236
+ ngrok http 3000
237
+
238
+ # Use the ngrok URL as your webhook endpoint in development
239
+ ```
240
+
241
+ ```typescript
242
+ // Integration test for webhook handler
243
+ describe("POST /webhooks/stripe", () => {
244
+ it("processes checkout.session.completed", async () => {
245
+ const payload = { id: "evt_123", type: "checkout.session.completed", data: { /* ... */ } };
246
+ const body = JSON.stringify(payload);
247
+ const sig = stripe.webhooks.generateTestHeaderString({
248
+ payload: body,
249
+ secret: process.env.STRIPE_WEBHOOK_SECRET!,
250
+ });
251
+
252
+ const res = await request(app)
253
+ .post("/webhooks/stripe")
254
+ .set("Content-Type", "application/json")
255
+ .set("stripe-signature", sig)
256
+ .send(body);
257
+
258
+ expect(res.status).toBe(200);
259
+ const order = await db.orders.findById(payload.data.object.metadata.orderId);
260
+ expect(order.status).toBe("fulfilled");
261
+ });
262
+ });
263
+ ```
264
+
265
+ ## Examples
266
+
267
+ | Pattern | When | Result |
268
+ |---------|------|--------|
269
+ | HMAC signature | Every webhook | Prevents spoofed payloads |
270
+ | Idempotency key | Duplicate deliveries | Process each event exactly once |
271
+ | Retry with backoff | Network failures | Reliable delivery over 24 hours |
272
+ | Timestamp ordering | Status updates | Ignore stale events |
273
+ | Local forwarding | Development | Test webhooks without deploying |
274
+
275
+ ## Checklist
276
+ - [ ] Webhook signatures verified on every incoming request
277
+ - [ ] Raw request body used for signature verification (not parsed JSON)
278
+ - [ ] Handlers are idempotent -- safe to process duplicate events
279
+ - [ ] Respond 200 quickly, process asynchronously if slow
280
+ - [ ] Retry schedule uses exponential backoff (1min to 24hrs)
281
+ - [ ] Failed deliveries logged with attempt count and error
282
+ - [ ] Out-of-order events handled via timestamp comparison
283
+ - [ ] Webhook endpoint tested with local forwarding (Stripe CLI, ngrok)