Skip to content

Commit 65d87bc

Browse files
committed
Add support for validating webhooks
1 parent cafb2a5 commit 65d87bc

File tree

3 files changed

+106
-1
lines changed

3 files changed

+106
-1
lines changed

index.js

+9
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const hardware = require("./lib/hardware");
99
const models = require("./lib/models");
1010
const predictions = require("./lib/predictions");
1111
const trainings = require("./lib/trainings");
12+
const webhooks = require("./lib/webhooks");
1213

1314
const packageJSON = require("./package.json");
1415

@@ -85,6 +86,14 @@ class Replicate {
8586
cancel: trainings.cancel.bind(this),
8687
list: trainings.list.bind(this),
8788
};
89+
90+
this.webhooks = {
91+
default: {
92+
secrets: {
93+
get: webhooks.default.secrets.get.bind(this),
94+
},
95+
},
96+
};
8897
}
8998

9099
/**

lib/util.js

+77-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,81 @@
1+
const crypto = require("node:crypto");
2+
13
const ApiError = require("./error");
24

5+
/**
6+
* @see {@link validateWebhook}
7+
* @overload
8+
* @param {object} requestData - The request data
9+
* @param {string} requestData.id - The webhook ID header from the incoming request.
10+
* @param {string} requestData.timestamp - The webhook timestamp header from the incoming request.
11+
* @param {string} requestData.body - The raw body of the incoming webhook request.
12+
* @param {string} requestData.secret - The webhook secret, obtained from `replicate.webhooks.defaul.secret` method.
13+
* @param {string} requestData.signature - The webhook signature header from the incoming request, comprising one or more space-delimited signatures.
14+
*/
15+
16+
/**
17+
* @see {@link validateWebhook}
18+
* @overload
19+
* @param {object} requestData - The request object
20+
* @param {object} requestData.headers - The request headers
21+
* @param {string} requestData.headers["webhook-id"] - The webhook ID header from the incoming request
22+
* @param {string} requestData.headers["webhook-timestamp"] - The webhook timestamp header from the incoming request
23+
* @param {string} requestData.headers["webhook-signature"] - The webhook signature header from the incoming request, comprising one or more space-delimited signatures
24+
* @param {string} requestData.body - The raw body of the incoming webhook request
25+
* @param {string} secret - The webhook secret, obtained from `replicate.webhooks.defaul.secret` method
26+
*/
27+
28+
/**
29+
* Validate a webhook signature
30+
*
31+
* @returns {boolean} - True if the signature is valid
32+
* @throws {Error} - If the request is missing required headers, body, or secret
33+
*/
34+
function validateWebhook(requestData, secret) {
35+
let { id, timestamp, body, signature } = requestData;
36+
const signingSecret = secret || requestData.secret;
37+
38+
if (
39+
requestData &&
40+
requestData.headers &&
41+
typeof requestData.body === "string"
42+
) {
43+
id = requestData.headers["webhook-id"];
44+
timestamp = requestData.headers["webhook-timestamp"];
45+
signature = requestData.headers["webhook-signature"];
46+
body = requestData.body;
47+
}
48+
49+
if (!id || !timestamp || !signature) {
50+
throw new Error("Missing required webhook headers");
51+
}
52+
53+
if (!body) {
54+
throw new Error("Missing required body");
55+
}
56+
57+
if (!signingSecret) {
58+
throw new Error("Missing required secret");
59+
}
60+
61+
const signedContent = `${id}.${timestamp}.${body}`;
62+
63+
const secretBytes = Buffer.from(signingSecret.split("_")[1], "base64");
64+
65+
const computedSignature = crypto
66+
.createHmac("sha256", secretBytes)
67+
.update(signedContent)
68+
.digest("base64");
69+
70+
const expectedSignatures = signature
71+
.split(" ")
72+
.map((sig) => sig.split(",")[1]);
73+
74+
return expectedSignatures.some(
75+
(expectedSignature) => expectedSignature === computedSignature
76+
);
77+
}
78+
379
/**
480
* Automatically retry a request if it fails with an appropriate status code.
581
*
@@ -68,4 +144,4 @@ async function withAutomaticRetries(request, options = {}) {
68144
return request();
69145
}
70146

71-
module.exports = { withAutomaticRetries };
147+
module.exports = { validateWebhook, withAutomaticRetries };

lib/webhooks.js

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Get the default webhook signing secret
3+
*
4+
* @returns {Promise<object>} Resolves with the signing secret for the default webhook
5+
*/
6+
async function getDefaultWebhookSecret() {
7+
const response = await this.request("/webhooks/default/secret", {
8+
method: "GET",
9+
});
10+
11+
return response.json();
12+
}
13+
14+
module.exports = {
15+
default: {
16+
secrets: {
17+
get: getDefaultWebhookSecret,
18+
},
19+
},
20+
};

0 commit comments

Comments
 (0)