Skip to content

Commit 70fd75d

Browse files
committed
Add webhook verification support
1 parent ae692ad commit 70fd75d

File tree

10 files changed

+947
-0
lines changed

10 files changed

+947
-0
lines changed

.fernignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Specify files that shouldn't be modified by Fern
22

33
README.md
4+
scripts/
45
src/cache.test.ts
56
src/cache.ts
67
src/core/fetcher/custom.ts
@@ -9,4 +10,6 @@ src/events.ts
910
src/index.ts
1011
src/logger.ts
1112
src/version.ts
13+
src/webhooks.ts
1214
src/wrapper.ts
15+
tests/unit/webhooks.test.ts

README.md

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,187 @@ client.close();
270270

271271
The Schematic API supports many operations beyond these, accessible via the API modules on the client, `Accounts`, `Billing`, `Companies`, `Entitlements`, `Events`, `Features`, and `Plans`.
272272

273+
## Webhook Verification
274+
275+
Schematic can send webhooks to notify your application of events. To ensure the security of these webhooks, Schematic signs each request using HMAC-SHA256. The SDK provides utility functions to verify these signatures.
276+
277+
### Verifying Webhook Signatures
278+
279+
When your application receives a webhook request from Schematic, you should verify its signature to ensure it's authentic. The SDK provides simple functions to verify webhook signatures. Here's how to use them in different frameworks:
280+
281+
#### Express
282+
283+
```ts
284+
import express from "express";
285+
import {
286+
verifyWebhookSignature,
287+
WebhookSignatureError,
288+
WEBHOOK_SIGNATURE_HEADER,
289+
WEBHOOK_TIMESTAMP_HEADER,
290+
} from "@schematichq/schematic-typescript-node";
291+
292+
// Note: Schematic webhooks use these headers:
293+
// - X-Schematic-Webhook-Signature: Contains the HMAC-SHA256 signature
294+
// - X-Schematic-Webhook-Timestamp: Contains the timestamp when the webhook was sent
295+
296+
const app = express();
297+
298+
// Use a middleware that captures raw body for signature verification
299+
app.use(
300+
"/webhooks/schematic",
301+
express.json({
302+
verify: (req, res, buf) => {
303+
if (buf && buf.length) {
304+
(req as any).rawBody = buf;
305+
}
306+
},
307+
})
308+
);
309+
310+
app.post("/webhooks/schematic", (req, res) => {
311+
try {
312+
const webhookSecret = "your-webhook-secret"; // Get this from the Schematic app
313+
314+
// Verify the webhook signature using the captured raw body
315+
verifyWebhookSignature(req, webhookSecret);
316+
317+
// Process the webhook payload
318+
const data = req.body;
319+
console.log("Webhook verified:", data);
320+
321+
res.status(200).end();
322+
} catch (error) {
323+
if (error instanceof WebhookSignatureError) {
324+
console.error("Webhook verification failed:", error.message);
325+
return res.status(400).json({ error: error.message });
326+
}
327+
328+
console.error("Error processing webhook:", error);
329+
res.status(500).json({ error: "Internal server error" });
330+
}
331+
});
332+
333+
const PORT = 3000;
334+
app.listen(PORT, () => {
335+
console.log(`Server running on port ${PORT}`);
336+
});
337+
```
338+
339+
#### Node HTTP Server
340+
341+
```ts
342+
import http from "http";
343+
import {
344+
verifySignature,
345+
WebhookSignatureError,
346+
WEBHOOK_SIGNATURE_HEADER,
347+
WEBHOOK_TIMESTAMP_HEADER,
348+
} from "@schematichq/schematic-typescript-node";
349+
350+
const webhookSecret = "your-webhook-secret"; // Get this from the Schematic app
351+
352+
const server = http.createServer(async (req, res) => {
353+
if (req.url === "/webhooks/schematic" && req.method === "POST") {
354+
// Collect the request body
355+
let body = "";
356+
for await (const chunk of req) {
357+
body += chunk.toString();
358+
}
359+
360+
try {
361+
// Get the headers
362+
const signature = req.headers[WEBHOOK_SIGNATURE_HEADER.toLowerCase()] as string;
363+
const timestamp = req.headers[WEBHOOK_TIMESTAMP_HEADER.toLowerCase()] as string;
364+
365+
// Verify the signature
366+
verifySignature(body, signature, timestamp, webhookSecret);
367+
368+
// Process the webhook payload
369+
const data = JSON.parse(body);
370+
console.log("Webhook verified:", data);
371+
372+
res.statusCode = 200;
373+
res.end();
374+
} catch (error) {
375+
if (error instanceof WebhookSignatureError) {
376+
console.error("Webhook verification failed:", error.message);
377+
res.statusCode = 400;
378+
res.end(JSON.stringify({ error: error.message }));
379+
return;
380+
}
381+
382+
console.error("Error processing webhook:", error);
383+
res.statusCode = 500;
384+
res.end(JSON.stringify({ error: "Internal server error" }));
385+
}
386+
} else {
387+
res.statusCode = 404;
388+
res.end();
389+
}
390+
});
391+
392+
const PORT = 3000;
393+
server.listen(PORT, () => {
394+
console.log(`Server running on port ${PORT}`);
395+
});
396+
```
397+
398+
#### Next.js API Routes
399+
400+
```ts
401+
// pages/api/webhooks/schematic.ts
402+
import type { NextApiRequest, NextApiResponse } from "next";
403+
import {
404+
verifyWebhookSignature,
405+
WebhookSignatureError,
406+
WEBHOOK_SIGNATURE_HEADER,
407+
WEBHOOK_TIMESTAMP_HEADER,
408+
} from "@schematichq/schematic-typescript-node";
409+
import { buffer } from "micro";
410+
411+
// Schematic webhooks use these headers:
412+
// - X-Schematic-Webhook-Signature: Contains the HMAC-SHA256 signature
413+
// - X-Schematic-Webhook-Timestamp: Contains the timestamp when the webhook was sent
414+
415+
// Disable body parsing to get the raw body
416+
export const config = {
417+
api: {
418+
bodyParser: false,
419+
},
420+
};
421+
422+
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
423+
if (req.method !== "POST") {
424+
return res.status(405).end("Method not allowed");
425+
}
426+
427+
try {
428+
const webhookSecret = process.env.SCHEMATIC_WEBHOOK_SECRET!;
429+
const rawBody = await buffer(req);
430+
431+
// Verify the webhook signature
432+
verifyWebhookSignature(req, webhookSecret, rawBody);
433+
434+
// Parse the webhook payload
435+
const payload = JSON.parse(rawBody.toString());
436+
console.log("Webhook verified:", payload);
437+
438+
// Process the webhook event
439+
// ...
440+
441+
res.status(200).end();
442+
} catch (error) {
443+
if (error instanceof WebhookSignatureError) {
444+
console.error("Webhook verification failed:", error.message);
445+
return res.status(400).json({ error: error.message });
446+
}
447+
448+
console.error("Error processing webhook:", error);
449+
res.status(500).json({ error: "Internal server error" });
450+
}
451+
}
452+
```
453+
273454
## Testing
274455

275456
### Offline mode
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Webhook Test Server
2+
3+
A simple test server to verify Schematic webhook signatures.
4+
5+
## Usage
6+
7+
```bash
8+
# Set your webhook secret
9+
export SCHEMATIC_WEBHOOK_SECRET="your-webhook-secret"
10+
11+
# Optionally set port (default: 8080)
12+
export PORT=9000
13+
14+
# Run the server
15+
npm start
16+
```
17+
18+
Use a tool like ngrok to expose the server to the internet:
19+
20+
```bash
21+
ngrok http 8080
22+
```
23+
24+
Then configure the webhook in Schematic with your ngrok URL.

0 commit comments

Comments
 (0)