// routes/privacyLawWebhooks.js require('dotenv').config(); const express = require('express'); const crypto = require('crypto'); const router = express.Router(); // Use raw body ONLY for this router so HMAC works router.use(express.raw({ type: '*/*' })); const SHOPIFY_API_SECRET = process.env.SHOPIFY_API_SECRET; // ---- helpers ---- function verifyHmac(rawBody, hmacHeader) { if (!SHOPIFY_API_SECRET || !hmacHeader || !rawBody) return false; const digest = crypto .createHmac('sha256', SHOPIFY_API_SECRET) .update(rawBody) // raw Buffer .digest('base64'); const generated = Buffer.from(digest, 'utf8'); const received = Buffer.from(hmacHeader, 'utf8'); if (generated.length !== received.length) return false; return crypto.timingSafeEqual(generated, received); } function parseJsonSafe(buf) { try { return JSON.parse(buf.toString('utf8')); } catch { return null; } } function handleWebhook(req, res, topicName) { const hmacHeader = req.header('x-shopify-hmac-sha256'); const shop = req.header('x-shopify-shop-domain'); const topic = req.header('x-shopify-topic') || topicName; const isValid = verifyHmac(req.body, hmacHeader); if (!isValid) { // You asked for 500 on invalid HMAC. (Best practice is 401.) console.error(`[WEBHOOK:${topic}] INVALID HMAC from shop=${shop}`); return res.status(401).send('Invalid HMAC'); } const payload = parseJsonSafe(req.body) || {}; // Log minimally; avoid logging full PII in real apps console.log(`[WEBHOOK:${topic}] shop=${shop}`, payload); // Echo back for your debugging return res.status(200).json({ status: 'ok', topic, shop, received: payload }); } // ---- endpoints ---- // 1) customers/data_request router.post('/customers/data_request', (req, res) => handleWebhook(req, res, 'customers/data_request') ); // 2) customers/redact router.post('/customers/redact', (req, res) => handleWebhook(req, res, 'customers/redact') ); // 3) shop/redact router.post('/shop/redact', (req, res) => handleWebhook(req, res, 'shop/redact') ); module.exports = router;