UNPKG

hook-engine

Version:

Production-grade webhook engine with comprehensive adapter support, security, reliability, structured logging, and CLI tools.

261 lines (260 loc) 10.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const crypto_1 = __importDefault(require("crypto")); /** * Shopify webhook adapter * Supports: orders, products, customers, inventory, app events, and more * Documentation: https://shopify.dev/docs/apps/webhooks */ const shopify = { getSignature(req) { // Shopify sends signature in X-Shopify-Hmac-Sha256 header return req.headers["x-shopify-hmac-sha256"]; }, verifySignature(rawBody, signature, secret) { if (!signature) return false; try { const expected = crypto_1.default .createHmac("sha256", secret) .update(rawBody) .digest("base64"); return crypto_1.default.timingSafeEqual(Buffer.from(expected), Buffer.from(signature)); } catch (error) { console.error('Shopify signature verification error:', error); return false; } }, parsePayload(body) { return JSON.parse(body.toString("utf8")); }, normalize(event, options) { let type = 'unknown'; let id = ''; let timestamp = Math.floor(Date.now() / 1000); let payload = {}; // Determine event type from the webhook topic (usually passed in headers or inferred from structure) if (event.id) { id = event.id.toString(); } // Handle different Shopify event types based on structure if (event.line_items && event.total_price !== undefined) { // Order events type = 'orders.created'; if (event.cancelled_at) type = 'orders.cancelled'; if (event.fulfilled_at) type = 'orders.fulfilled'; if (event.financial_status) { switch (event.financial_status) { case 'paid': type = 'orders.paid'; break; case 'refunded': type = 'orders.refunded'; break; case 'partially_refunded': type = 'orders.partially_refunded'; break; } } timestamp = event.created_at ? Math.floor(new Date(event.created_at).getTime() / 1000) : timestamp; payload = { order_id: event.id, order_number: event.order_number, name: event.name, email: event.email, total_price: event.total_price, subtotal_price: event.subtotal_price, total_tax: event.total_tax, currency: event.currency, financial_status: event.financial_status, fulfillment_status: event.fulfillment_status, line_items: event.line_items, customer: event.customer, billing_address: event.billing_address, shipping_address: event.shipping_address, payment_gateway_names: event.payment_gateway_names, tags: event.tags, note: event.note, created_at: event.created_at, updated_at: event.updated_at }; } else if (event.title && event.handle && event.vendor !== undefined) { // Product events type = 'products.created'; if (event.updated_at && new Date(event.updated_at) > new Date(event.created_at)) { type = 'products.updated'; } timestamp = event.created_at ? Math.floor(new Date(event.created_at).getTime() / 1000) : timestamp; payload = { product_id: event.id, title: event.title, handle: event.handle, vendor: event.vendor, product_type: event.product_type, status: event.status, tags: event.tags, variants: event.variants, images: event.images, options: event.options, created_at: event.created_at, updated_at: event.updated_at, published_at: event.published_at }; } else if (event.first_name !== undefined && event.last_name !== undefined && event.email) { // Customer events type = 'customers.created'; if (event.updated_at && new Date(event.updated_at) > new Date(event.created_at)) { type = 'customers.updated'; } timestamp = event.created_at ? Math.floor(new Date(event.created_at).getTime() / 1000) : timestamp; payload = { customer_id: event.id, email: event.email, first_name: event.first_name, last_name: event.last_name, phone: event.phone, addresses: event.addresses, orders_count: event.orders_count, total_spent: event.total_spent, tags: event.tags, accepts_marketing: event.accepts_marketing, created_at: event.created_at, updated_at: event.updated_at, last_order_id: event.last_order_id, last_order_name: event.last_order_name }; } else if (event.order_id && event.line_items) { // Fulfillment events type = 'fulfillments.created'; if (event.status === 'success') type = 'fulfillments.fulfilled'; if (event.status === 'cancelled') type = 'fulfillments.cancelled'; timestamp = event.created_at ? Math.floor(new Date(event.created_at).getTime() / 1000) : timestamp; payload = { fulfillment_id: event.id, order_id: event.order_id, status: event.status, tracking_company: event.tracking_company, tracking_number: event.tracking_number, tracking_url: event.tracking_url, line_items: event.line_items, created_at: event.created_at, updated_at: event.updated_at }; } else if (event.inventory_item_id) { // Inventory events type = 'inventory_levels.updated'; payload = { inventory_item_id: event.inventory_item_id, location_id: event.location_id, available: event.available, updated_at: event.updated_at }; } else if (event.order_id && event.amount) { // Refund events type = 'refunds.created'; timestamp = event.created_at ? Math.floor(new Date(event.created_at).getTime() / 1000) : timestamp; payload = { refund_id: event.id, order_id: event.order_id, amount: event.amount, currency: event.currency, reason: event.reason, refund_line_items: event.refund_line_items, transactions: event.transactions, created_at: event.created_at }; } else if (event.collection_id || event.product_id) { // Collection events if (event.collection_id) { type = 'collections.updated'; payload = { collection_id: event.collection_id, product_id: event.product_id, position: event.position, featured: event.featured, created_at: event.created_at, updated_at: event.updated_at }; } } else if (event.checkout_id || event.abandoned_checkout_url) { // Checkout events type = 'checkouts.created'; if (event.completed_at) type = 'checkouts.completed'; timestamp = event.created_at ? Math.floor(new Date(event.created_at).getTime() / 1000) : timestamp; payload = { checkout_id: event.id, token: event.token, cart_token: event.cart_token, email: event.email, total_price: event.total_price, subtotal_price: event.subtotal_price, line_items: event.line_items, customer: event.customer, abandoned_checkout_url: event.abandoned_checkout_url, created_at: event.created_at, updated_at: event.updated_at, completed_at: event.completed_at }; } else if (event.api_client_id) { // App events type = 'app.uninstalled'; payload = { api_client_id: event.api_client_id, shop_id: event.shop_id, shop_domain: event.shop_domain }; } else if (event.domain) { // Shop events type = 'shop.updated'; payload = { shop_id: event.id, name: event.name, domain: event.domain, email: event.email, currency: event.currency, country_code: event.country_code, province_code: event.province_code, plan_name: event.plan_name, created_at: event.created_at, updated_at: event.updated_at }; } else { // Generic Shopify event type = 'shopify.unknown'; id = event.id?.toString() || `shopify_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; payload = event; } // Use fallback ID if not set if (!id) { id = `${type}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } return { id, type, source: "shopify", timestamp, payload, raw: options?.includeRaw ? JSON.stringify(event) : '', }; }, }; exports.default = shopify;