// Express example
const crypto = require("crypto");
const express = require("express");
function verifyGroundSignature(rawBody, signatureHeader, signingSecret, toleranceSeconds = 300) {
if (!signatureHeader) throw new Error("Missing Ground-Signature header");
const parts = signatureHeader.split(",");
const tPart = parts.find((p) => p.startsWith("t="));
const v1Part = parts.find((p) => p.startsWith("v1="));
if (!tPart || !v1Part) throw new Error("Invalid Ground-Signature header format");
const timestamp = Number(tPart.split("=")[1]);
const signature = v1Part.split("=")[1];
// Optional replay protection
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > toleranceSeconds) {
throw new Error("Ground-Signature timestamp outside allowed window");
}
const payloadToSign = `${timestamp}.${rawBody}`;
const computed = crypto.createHmac("sha256", signingSecret).update(payloadToSign).digest("hex");
const expected = Buffer.from(computed, "utf8");
const actual = Buffer.from(signature, "utf8");
if (expected.length !== actual.length || !crypto.timingSafeEqual(expected, actual)) {
throw new Error("Invalid Ground-Signature");
}
}
const app = express();
app.post("/ground/webhook", express.raw({ type: "application/json" }), (req, res) => {
const rawBody = req.body.toString("utf8");
const signatureHeader = req.get("Ground-Signature");
const signingSecret = process.env.GROUND_WEBHOOK_SECRET;
try {
verifyGroundSignature(rawBody, signatureHeader, signingSecret);
} catch (err) {
return res.status(400).json({ error: "Invalid signature" });
}
const event = JSON.parse(rawBody);
// Use event.event to route handling:
// - "portfolio_wallet.withdrawal.status_changed"
// - "portfolio_wallet.deposit.status_changed"
return res.status(200).json({ received: true });
});