Skip to content

Instantly share code, notes, and snippets.

@narenmanoharan
Last active February 23, 2023 16:36
Show Gist options
  • Save narenmanoharan/5083d1998c80586c8f470b21d15206eb to your computer and use it in GitHub Desktop.
Save narenmanoharan/5083d1998c80586c8f470b21d15206eb to your computer and use it in GitHub Desktop.
Node example for a POST endpoint to consume wolfia webhooks
var express = require('express');
var crypto = require('crypto');
var router = express.Router();
router.post('/', function (req, res, next) {
const isExpired = isWebhookExpired(req);
const isVerified = isWebhookVerified(req);
if (isVerified && !isExpired) {
processWebhook(req.body);
res.status(200).send('OK');
} else if (isVerified && isExpired) {
res.status(400).send("Webhook expired");
} else if (!isVerified) {
res.status(400).send("Invalid signature");
} else {
res.status(400).send("Unknown error");
}
});
// Prevents man-in-the-middle attacks by verifying the signature
function isWebhookVerified(req) {
const body = req.body;
const validUntil = req.header("X-Wolfia-Until");
const signature = req.header("X-Wolfia-Signature");
return verifySignature(body, validUntil, signature);
}
// Prevents replay attacks by verifying the expiration date
function isWebhookExpired(req) {
const validUntil = req.header("X-Wolfia-Until");
const validDateTime = new Date(validUntil * 1000);
const currentTime = new Date();
return validDateTime < currentTime;
}
function verifySignature(body, validUntil, signature) {
const secret = "secret you entered when creating the webhook goes here"
const data = JSON.stringify(body) + "." + validUntil;
const hash = crypto.createHmac('sha256', secret).update(data).digest('hex');
return hash === signature
}
function processWebhook(body) {
console.log("Received webhook: " + JSON.stringify(body, null, 2));
switch (body.event) {
case "ios.appstore.review.started":
console.log(`Started review for version: ${body.cfBundleShortVersionString}.${body.cfBundleVersion}`)
// handle review start
break;
case "ios.appstore.review.completed":
console.log(`Completed review for version: ${body.cfBundleShortVersionString}.${body.cfBundleVersion}`)
// handle review complete
break;
default:
console.log("Unknown event: " + body.event)
}
}
module.exports = router;
@narenmanoharan
Copy link
Author

Here's the schema of the webhook events:

data class IOSAppStoreReviewStarted(
    val event: String = "ios.appstore.review.started",
    val buildId: String,
    val cfBundleIdentifier: String,
    val cfBundleVersion: String,
    val cfBundleShortVersionString: String,
    val submittedAt: Instant,
) : WebhookEvent()

data class IOSAppStoreReviewCompleted(
    val event: String = "ios.appstore.review.completed",
    val buildId: String,
    val cfBundleIdentifier: String,
    val cfBundleVersion: String,
    val cfBundleShortVersionString: String,
    val completedAt: Instant,
) : WebhookEvent()

And an example in json:

{
  "type": "app.wolfia.common.webhooks.WebhookEvent.IOSAppStoreReviewStarted",
  "event": "ios.appstore.review.started",
  "buildId": "17127a98-0989-46a8-a69a-b3a6378f653d",
  "cfBundleIdentifier": "app.wolfia",
  "cfBundleVersion": "1",
  "cfBundleShortVersionString": "1.0.0",
  "submittedAt": "2023-02-22T17:19:25.503847Z"
}

{
  "type": "app.wolfia.common.webhooks.WebhookEvent.IOSAppStoreReviewCompleted",
  "event": "ios.appstore.review.completed",
  "buildId": "17127a98-0989-46a8-a69a-b3a6378f653d",
  "cfBundleIdentifier": "app.wolfia",
  "cfBundleVersion": "1",
  "cfBundleShortVersionString": "1.0.0",
  "completedAt": "2023-02-22T17:03:53.539592Z"
}

Also the associated headers for the post request:

-> X-Wolfia-Idempotency-Key: 01GSG9X387JZMZ35SWAJRF03V2
-> X-Wolfia-Signature: 803e54b491c71feab826a928991794401ac5e404095826fd46b4474bf128d0d7
-> X-Wolfia-Until: 1677086673

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment