@editframe/create 0.43.0 → 0.45.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.
- package/README.md +11 -0
- package/dist/index.js +16 -28
- package/dist/index.js.map +1 -1
- package/dist/skills/editframe-brand-video-generator/README.md +155 -0
- package/dist/skills/editframe-brand-video-generator/SKILL.md +207 -0
- package/dist/skills/editframe-brand-video-generator/references/brand-examples.md +178 -0
- package/dist/skills/editframe-brand-video-generator/references/color-psychology.md +227 -0
- package/dist/skills/editframe-brand-video-generator/references/composition-patterns.md +383 -0
- package/dist/skills/editframe-brand-video-generator/references/editing.md +66 -0
- package/dist/skills/editframe-brand-video-generator/references/emotional-arcs.md +496 -0
- package/dist/skills/editframe-brand-video-generator/references/genre-selection.md +135 -0
- package/dist/skills/editframe-brand-video-generator/references/transition-styles.md +611 -0
- package/dist/skills/editframe-brand-video-generator/references/typography-personalities.md +326 -0
- package/dist/skills/editframe-brand-video-generator/references/video-archetypes.md +86 -0
- package/dist/skills/editframe-brand-video-generator/references/video-fundamentals.md +169 -0
- package/dist/skills/editframe-brand-video-generator/references/visual-metaphors.md +50 -0
- package/dist/skills/editframe-composition/SKILL.md +169 -0
- package/dist/skills/editframe-composition/references/audio.md +483 -0
- package/dist/skills/editframe-composition/references/captions.md +844 -0
- package/dist/skills/editframe-composition/references/composition-model.md +73 -0
- package/dist/skills/editframe-composition/references/configuration.md +403 -0
- package/dist/skills/editframe-composition/references/css-parts.md +105 -0
- package/dist/skills/editframe-composition/references/css-variables.md +640 -0
- package/dist/skills/editframe-composition/references/entry-points.md +810 -0
- package/dist/skills/editframe-composition/references/events.md +499 -0
- package/dist/skills/editframe-composition/references/getting-started.md +259 -0
- package/dist/skills/editframe-composition/references/hooks.md +234 -0
- package/dist/skills/editframe-composition/references/image.md +241 -0
- package/dist/skills/editframe-composition/references/r3f.md +580 -0
- package/dist/skills/editframe-composition/references/render-api.md +484 -0
- package/dist/skills/editframe-composition/references/render-strategies.md +119 -0
- package/dist/skills/editframe-composition/references/render-to-video.md +1101 -0
- package/dist/skills/editframe-composition/references/scripting.md +606 -0
- package/dist/skills/editframe-composition/references/sequencing.md +116 -0
- package/dist/skills/editframe-composition/references/server-rendering.md +753 -0
- package/dist/skills/editframe-composition/references/surface.md +329 -0
- package/dist/skills/editframe-composition/references/text.md +627 -0
- package/dist/skills/editframe-composition/references/time-model.md +99 -0
- package/dist/skills/editframe-composition/references/timegroup-modes.md +102 -0
- package/dist/skills/editframe-composition/references/timegroup.md +457 -0
- package/dist/skills/editframe-composition/references/timeline-root.md +398 -0
- package/dist/skills/editframe-composition/references/transcription.md +47 -0
- package/dist/skills/editframe-composition/references/transitions.md +608 -0
- package/dist/skills/editframe-composition/references/use-media-info.md +357 -0
- package/dist/skills/editframe-composition/references/video.md +506 -0
- package/dist/skills/editframe-composition/references/waveform.md +327 -0
- package/dist/skills/editframe-editor-gui/SKILL.md +152 -0
- package/dist/skills/editframe-editor-gui/references/active-root-temporal.md +657 -0
- package/dist/skills/editframe-editor-gui/references/canvas.md +947 -0
- package/dist/skills/editframe-editor-gui/references/controls.md +366 -0
- package/dist/skills/editframe-editor-gui/references/dial.md +756 -0
- package/dist/skills/editframe-editor-gui/references/editor-toolkit.md +587 -0
- package/dist/skills/editframe-editor-gui/references/filmstrip.md +460 -0
- package/dist/skills/editframe-editor-gui/references/fit-scale.md +772 -0
- package/dist/skills/editframe-editor-gui/references/focus-overlay.md +561 -0
- package/dist/skills/editframe-editor-gui/references/hierarchy.md +544 -0
- package/dist/skills/editframe-editor-gui/references/overlay-item.md +634 -0
- package/dist/skills/editframe-editor-gui/references/overlay-layer.md +429 -0
- package/dist/skills/editframe-editor-gui/references/pan-zoom.md +568 -0
- package/dist/skills/editframe-editor-gui/references/pause.md +397 -0
- package/dist/skills/editframe-editor-gui/references/play.md +370 -0
- package/dist/skills/editframe-editor-gui/references/preview.md +391 -0
- package/dist/skills/editframe-editor-gui/references/resizable-box.md +749 -0
- package/dist/skills/editframe-editor-gui/references/scrubber.md +588 -0
- package/dist/skills/editframe-editor-gui/references/thumbnail-strip.md +566 -0
- package/dist/skills/editframe-editor-gui/references/time-display.md +492 -0
- package/dist/skills/editframe-editor-gui/references/timeline-ruler.md +489 -0
- package/dist/skills/editframe-editor-gui/references/timeline.md +604 -0
- package/dist/skills/editframe-editor-gui/references/toggle-loop.md +618 -0
- package/dist/skills/editframe-editor-gui/references/toggle-play.md +526 -0
- package/dist/skills/editframe-editor-gui/references/transform-handles.md +924 -0
- package/dist/skills/editframe-editor-gui/references/trim-handles.md +725 -0
- package/dist/skills/editframe-editor-gui/references/workbench.md +453 -0
- package/dist/skills/editframe-motion-design/SKILL.md +101 -0
- package/dist/skills/editframe-motion-design/references/0-editframe.md +299 -0
- package/dist/skills/editframe-motion-design/references/1-intent.md +201 -0
- package/dist/skills/editframe-motion-design/references/2-physics-model.md +405 -0
- package/dist/skills/editframe-motion-design/references/3-attention.md +350 -0
- package/dist/skills/editframe-motion-design/references/4-process.md +418 -0
- package/dist/skills/editframe-vite-plugin/SKILL.md +75 -0
- package/dist/skills/editframe-vite-plugin/references/file-api.md +111 -0
- package/dist/skills/editframe-vite-plugin/references/getting-started.md +96 -0
- package/dist/skills/editframe-vite-plugin/references/jit-transcoding.md +91 -0
- package/dist/skills/editframe-vite-plugin/references/local-assets.md +75 -0
- package/dist/skills/editframe-vite-plugin/references/visual-testing.md +136 -0
- package/dist/skills/editframe-webhooks/SKILL.md +126 -0
- package/dist/skills/editframe-webhooks/references/events.md +382 -0
- package/dist/skills/editframe-webhooks/references/getting-started.md +232 -0
- package/dist/skills/editframe-webhooks/references/security.md +418 -0
- package/dist/skills/editframe-webhooks/references/testing.md +409 -0
- package/dist/skills/editframe-webhooks/references/troubleshooting.md +457 -0
- package/dist/templates/html/AGENTS.md +13 -0
- package/dist/templates/react/AGENTS.md +13 -0
- package/dist/utils.js +15 -16
- package/dist/utils.js.map +1 -1
- package/package.json +2 -2
- package/tsdown.config.ts +4 -0
- package/dist/detectAgent.js +0 -89
- package/dist/detectAgent.js.map +0 -1
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Webhook Security
|
|
3
|
+
description: Verify HMAC-SHA256 signatures on incoming webhook requests and prevent replay attacks with timestamp validation.
|
|
4
|
+
type: reference
|
|
5
|
+
nav:
|
|
6
|
+
parent: "Security"
|
|
7
|
+
priority: 3
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Webhook Security
|
|
11
|
+
|
|
12
|
+
Secure your webhook endpoints with HMAC signature verification, timestamp validation, and proper secret management.
|
|
13
|
+
|
|
14
|
+
## HMAC Signature Verification
|
|
15
|
+
|
|
16
|
+
Every webhook request includes an `X-Webhook-Signature` header containing an HMAC-SHA256 signature. This signature proves the request originated from Editframe and hasn't been tampered with.
|
|
17
|
+
|
|
18
|
+
### Signature Format
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
X-Webhook-Signature: <hmac-sha256-hex-digest>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The signature is computed as:
|
|
25
|
+
```
|
|
26
|
+
HMAC-SHA256(webhook_secret, request_body)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Where:
|
|
30
|
+
- `webhook_secret` is your API key's webhook secret
|
|
31
|
+
- `request_body` is the raw JSON request body as a string
|
|
32
|
+
|
|
33
|
+
### Verification Implementation
|
|
34
|
+
|
|
35
|
+
#### Node.js / TypeScript
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import crypto from "node:crypto";
|
|
39
|
+
|
|
40
|
+
function verifyWebhookSignature(
|
|
41
|
+
payload: string | Buffer,
|
|
42
|
+
signature: string,
|
|
43
|
+
secret: string
|
|
44
|
+
): boolean {
|
|
45
|
+
// Compute expected signature
|
|
46
|
+
const expectedSignature = crypto
|
|
47
|
+
.createHmac("sha256", secret)
|
|
48
|
+
.update(payload)
|
|
49
|
+
.digest("hex");
|
|
50
|
+
|
|
51
|
+
// Use timing-safe comparison to prevent timing attacks
|
|
52
|
+
return crypto.timingSafeEqual(
|
|
53
|
+
Buffer.from(signature),
|
|
54
|
+
Buffer.from(expectedSignature)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Express middleware
|
|
59
|
+
app.post("/webhooks/editframe", (req, res) => {
|
|
60
|
+
const signature = req.headers["x-webhook-signature"] as string;
|
|
61
|
+
const secret = process.env.EDITFRAME_WEBHOOK_SECRET!;
|
|
62
|
+
|
|
63
|
+
// Get raw body (before JSON parsing)
|
|
64
|
+
const rawBody = JSON.stringify(req.body);
|
|
65
|
+
|
|
66
|
+
if (!verifyWebhookSignature(rawBody, signature, secret)) {
|
|
67
|
+
return res.status(401).send("Invalid signature");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Signature is valid - process event
|
|
71
|
+
const event = req.body;
|
|
72
|
+
res.status(200).send("OK");
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Critical**: Hash the raw request body string, not the parsed JSON object. JSON serialization order can vary, producing different hashes.
|
|
77
|
+
|
|
78
|
+
#### Express with Raw Body
|
|
79
|
+
|
|
80
|
+
To verify signatures correctly, you need access to the raw request body:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import express from "express";
|
|
84
|
+
import bodyParser from "body-parser";
|
|
85
|
+
|
|
86
|
+
const app = express();
|
|
87
|
+
|
|
88
|
+
// Capture raw body for signature verification
|
|
89
|
+
app.use(bodyParser.json({
|
|
90
|
+
verify: (req, res, buf) => {
|
|
91
|
+
(req as any).rawBody = buf.toString('utf8');
|
|
92
|
+
}
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
app.post("/webhooks/editframe", (req, res) => {
|
|
96
|
+
const signature = req.headers["x-webhook-signature"] as string;
|
|
97
|
+
const rawBody = (req as any).rawBody;
|
|
98
|
+
const secret = process.env.EDITFRAME_WEBHOOK_SECRET!;
|
|
99
|
+
|
|
100
|
+
if (!verifyWebhookSignature(rawBody, signature, secret)) {
|
|
101
|
+
return res.status(401).send("Invalid signature");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Process event
|
|
105
|
+
res.status(200).send("OK");
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
#### Next.js API Routes
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { NextApiRequest, NextApiResponse } from "next";
|
|
113
|
+
import crypto from "node:crypto";
|
|
114
|
+
|
|
115
|
+
// Disable body parsing to access raw body
|
|
116
|
+
export const config = {
|
|
117
|
+
api: {
|
|
118
|
+
bodyParser: false,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
async function getRawBody(req: NextApiRequest): Promise<string> {
|
|
123
|
+
return new Promise((resolve, reject) => {
|
|
124
|
+
let data = "";
|
|
125
|
+
req.on("data", (chunk) => { data += chunk; });
|
|
126
|
+
req.on("end", () => resolve(data));
|
|
127
|
+
req.on("error", reject);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export default async function handler(
|
|
132
|
+
req: NextApiRequest,
|
|
133
|
+
res: NextApiResponse
|
|
134
|
+
) {
|
|
135
|
+
if (req.method !== "POST") {
|
|
136
|
+
return res.status(405).send("Method not allowed");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const signature = req.headers["x-webhook-signature"] as string;
|
|
140
|
+
const rawBody = await getRawBody(req);
|
|
141
|
+
const secret = process.env.EDITFRAME_WEBHOOK_SECRET!;
|
|
142
|
+
|
|
143
|
+
const expectedSignature = crypto
|
|
144
|
+
.createHmac("sha256", secret)
|
|
145
|
+
.update(rawBody)
|
|
146
|
+
.digest("hex");
|
|
147
|
+
|
|
148
|
+
if (signature !== expectedSignature) {
|
|
149
|
+
return res.status(401).send("Invalid signature");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const event = JSON.parse(rawBody);
|
|
153
|
+
// Process event
|
|
154
|
+
|
|
155
|
+
res.status(200).send("OK");
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### Python (Flask)
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
import hmac
|
|
163
|
+
import hashlib
|
|
164
|
+
from flask import Flask, request
|
|
165
|
+
|
|
166
|
+
app = Flask(__name__)
|
|
167
|
+
WEBHOOK_SECRET = "your-webhook-secret"
|
|
168
|
+
|
|
169
|
+
@app.route("/webhooks/editframe", methods=["POST"])
|
|
170
|
+
def handle_webhook():
|
|
171
|
+
signature = request.headers.get("X-Webhook-Signature")
|
|
172
|
+
payload = request.get_data()
|
|
173
|
+
|
|
174
|
+
expected_signature = hmac.new(
|
|
175
|
+
WEBHOOK_SECRET.encode(),
|
|
176
|
+
payload,
|
|
177
|
+
hashlib.sha256
|
|
178
|
+
).hexdigest()
|
|
179
|
+
|
|
180
|
+
if not hmac.compare_digest(signature, expected_signature):
|
|
181
|
+
return "Invalid signature", 401
|
|
182
|
+
|
|
183
|
+
event = request.get_json()
|
|
184
|
+
# Process event
|
|
185
|
+
|
|
186
|
+
return "OK", 200
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### Ruby (Rails)
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
require 'openssl'
|
|
193
|
+
|
|
194
|
+
class WebhooksController < ApplicationController
|
|
195
|
+
skip_before_action :verify_authenticity_token
|
|
196
|
+
|
|
197
|
+
def editframe
|
|
198
|
+
signature = request.headers["X-Webhook-Signature"]
|
|
199
|
+
payload = request.raw_post
|
|
200
|
+
secret = ENV["EDITFRAME_WEBHOOK_SECRET"]
|
|
201
|
+
|
|
202
|
+
expected_signature = OpenSSL::HMAC.hexdigest(
|
|
203
|
+
OpenSSL::Digest.new('sha256'),
|
|
204
|
+
secret,
|
|
205
|
+
payload
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
unless ActiveSupport::SecurityUtils.secure_compare(signature, expected_signature)
|
|
209
|
+
render plain: "Invalid signature", status: :unauthorized
|
|
210
|
+
return
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
event = JSON.parse(payload)
|
|
214
|
+
# Process event
|
|
215
|
+
|
|
216
|
+
render plain: "OK", status: :ok
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Secret Management
|
|
222
|
+
|
|
223
|
+
### Retrieving Your Webhook Secret
|
|
224
|
+
|
|
225
|
+
When you create an API key, you receive:
|
|
226
|
+
1. **API Key**: For authenticating API requests
|
|
227
|
+
2. **Webhook Secret**: For verifying webhook signatures
|
|
228
|
+
|
|
229
|
+
**Important**: The webhook secret is only shown once during API key creation. Store it securely immediately.
|
|
230
|
+
|
|
231
|
+
### Storing Secrets
|
|
232
|
+
|
|
233
|
+
Never hardcode secrets in your application code. Use environment variables or a secrets manager:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
# .env
|
|
237
|
+
EDITFRAME_API_KEY=ef_live_...
|
|
238
|
+
EDITFRAME_WEBHOOK_SECRET=abc123...
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
// Load from environment
|
|
243
|
+
const secret = process.env.EDITFRAME_WEBHOOK_SECRET;
|
|
244
|
+
if (!secret) {
|
|
245
|
+
throw new Error("EDITFRAME_WEBHOOK_SECRET not configured");
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Rotating Secrets
|
|
250
|
+
|
|
251
|
+
To rotate your webhook secret:
|
|
252
|
+
|
|
253
|
+
1. Go to your API key detail page
|
|
254
|
+
2. Click "Regenerate Webhook Secret"
|
|
255
|
+
3. Update your application with the new secret
|
|
256
|
+
4. Deploy the updated application
|
|
257
|
+
|
|
258
|
+
**Warning**: Old signatures will fail after secret rotation. Update your application before rotating secrets in production.
|
|
259
|
+
|
|
260
|
+
## Replay Attack Prevention
|
|
261
|
+
|
|
262
|
+
Prevent replay attacks by implementing timestamp validation:
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
interface WebhookEvent {
|
|
266
|
+
topic: string;
|
|
267
|
+
data: {
|
|
268
|
+
created_at: string; // ISO 8601 timestamp
|
|
269
|
+
// ... other fields
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function isWebhookRecent(event: WebhookEvent, maxAgeSeconds: number = 300): boolean {
|
|
274
|
+
const eventTime = new Date(event.data.created_at).getTime();
|
|
275
|
+
const now = Date.now();
|
|
276
|
+
const ageSeconds = (now - eventTime) / 1000;
|
|
277
|
+
|
|
278
|
+
return ageSeconds >= 0 && ageSeconds <= maxAgeSeconds;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// In webhook handler
|
|
282
|
+
app.post("/webhooks/editframe", (req, res) => {
|
|
283
|
+
const event = req.body;
|
|
284
|
+
|
|
285
|
+
// Verify signature first
|
|
286
|
+
if (!verifySignature(req)) {
|
|
287
|
+
return res.status(401).send("Invalid signature");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Check timestamp (5 minute tolerance)
|
|
291
|
+
if (!isWebhookRecent(event, 300)) {
|
|
292
|
+
return res.status(400).send("Webhook timestamp out of range");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Process event
|
|
296
|
+
res.status(200).send("OK");
|
|
297
|
+
});
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Idempotency
|
|
301
|
+
|
|
302
|
+
Webhooks may be delivered multiple times due to retries. Implement idempotency to prevent duplicate processing:
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
const processedEventIds = new Set<string>();
|
|
306
|
+
|
|
307
|
+
app.post("/webhooks/editframe", async (req, res) => {
|
|
308
|
+
const event = req.body;
|
|
309
|
+
const eventId = `${event.topic}:${event.data.id}`;
|
|
310
|
+
|
|
311
|
+
// Check if already processed
|
|
312
|
+
if (processedEventIds.has(eventId)) {
|
|
313
|
+
console.log(`Duplicate webhook ignored: ${eventId}`);
|
|
314
|
+
return res.status(200).send("OK"); // Still return 200 to stop retries
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Verify signature
|
|
318
|
+
if (!verifySignature(req)) {
|
|
319
|
+
return res.status(401).send("Invalid signature");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Process event
|
|
323
|
+
await processWebhookEvent(event);
|
|
324
|
+
|
|
325
|
+
// Mark as processed
|
|
326
|
+
processedEventIds.add(eventId);
|
|
327
|
+
|
|
328
|
+
res.status(200).send("OK");
|
|
329
|
+
});
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
For production, use a database or Redis to track processed events:
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
import { db } from "./database";
|
|
336
|
+
|
|
337
|
+
async function isEventProcessed(eventId: string): Promise<boolean> {
|
|
338
|
+
const record = await db.query(
|
|
339
|
+
"SELECT 1 FROM processed_webhooks WHERE event_id = $1",
|
|
340
|
+
[eventId]
|
|
341
|
+
);
|
|
342
|
+
return record.rows.length > 0;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function markEventProcessed(eventId: string): Promise<void> {
|
|
346
|
+
await db.query(
|
|
347
|
+
"INSERT INTO processed_webhooks (event_id, processed_at) VALUES ($1, NOW())",
|
|
348
|
+
[eventId]
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Security Checklist
|
|
354
|
+
|
|
355
|
+
- [ ] Verify HMAC signature on every webhook request
|
|
356
|
+
- [ ] Use timing-safe comparison for signature verification
|
|
357
|
+
- [ ] Hash the raw request body, not parsed JSON
|
|
358
|
+
- [ ] Store webhook secrets in environment variables or secrets manager
|
|
359
|
+
- [ ] Implement timestamp validation to prevent replay attacks
|
|
360
|
+
- [ ] Implement idempotency to prevent duplicate processing
|
|
361
|
+
- [ ] Use HTTPS for webhook endpoints (required)
|
|
362
|
+
- [ ] Return 200 OK quickly, process events asynchronously
|
|
363
|
+
- [ ] Log signature verification failures for security monitoring
|
|
364
|
+
- [ ] Rotate webhook secrets periodically
|
|
365
|
+
|
|
366
|
+
## Common Security Mistakes
|
|
367
|
+
|
|
368
|
+
### 1. Hashing Parsed JSON
|
|
369
|
+
|
|
370
|
+
**Wrong:**
|
|
371
|
+
```typescript
|
|
372
|
+
const signature = crypto
|
|
373
|
+
.createHmac("sha256", secret)
|
|
374
|
+
.update(JSON.stringify(req.body)) // Different serialization!
|
|
375
|
+
.digest("hex");
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
**Right:**
|
|
379
|
+
```typescript
|
|
380
|
+
const signature = crypto
|
|
381
|
+
.createHmac("sha256", secret)
|
|
382
|
+
.update(req.rawBody) // Raw body as received
|
|
383
|
+
.digest("hex");
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### 2. Non-Timing-Safe Comparison
|
|
387
|
+
|
|
388
|
+
**Wrong:**
|
|
389
|
+
```typescript
|
|
390
|
+
if (signature === expectedSignature) { // Vulnerable to timing attacks
|
|
391
|
+
// ...
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
**Right:**
|
|
396
|
+
```typescript
|
|
397
|
+
if (crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
|
|
398
|
+
// ...
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### 3. Ignoring Signature Verification
|
|
403
|
+
|
|
404
|
+
**Never do this:**
|
|
405
|
+
```typescript
|
|
406
|
+
// DON'T: Process webhooks without verification
|
|
407
|
+
app.post("/webhooks/editframe", (req, res) => {
|
|
408
|
+
// No signature check - anyone can send fake webhooks!
|
|
409
|
+
processEvent(req.body);
|
|
410
|
+
res.status(200).send("OK");
|
|
411
|
+
});
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
## Next Steps
|
|
415
|
+
|
|
416
|
+
- [testing.md](references/testing.md) — Test signature verification locally
|
|
417
|
+
- [troubleshooting.md](references/troubleshooting.md) — Debug signature verification issues
|
|
418
|
+
- [getting-started.md](references/getting-started.md) — Complete webhook setup guide
|