hook-sign 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,193 @@
1
+ # hook-sign
2
+
3
+ A lightweight webhook signature verification utility for Node.js — with ready-to-use Express middleware.
4
+ Supports Shopify webhook verification (HMAC SHA256) and can be extended for other providers.
5
+
6
+ ---
7
+
8
+ ## Features
9
+
10
+ - ✅ Verify webhook signature using raw request body (Buffer)
11
+ - ✅ Shopify webhook verification (`X-Shopify-Hmac-Sha256`)
12
+ - ✅ Express middleware included
13
+ - ✅ Timing-safe signature comparison (`crypto.timingSafeEqual`)
14
+ - ✅ TypeScript support
15
+
16
+ ---
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install hook-sign
22
+ ```
23
+
24
+ Or with Yarn:
25
+
26
+ ```bash
27
+ yarn add hook-sign
28
+ ```
29
+
30
+ ### ⚠️ Important Note (Must Read)
31
+
32
+ Webhook providers sign the raw request body bytes, not the parsed JSON object. You must capture the raw request body in Express using:
33
+
34
+ ```javascript
35
+ express.json({ verify: rawBodySaver })
36
+ ```
37
+
38
+ If you skip this, signature verification will fail.
39
+
40
+ ---
41
+
42
+ ## Quick Start (Shopify + Express)
43
+
44
+ ### 1. Setup Express raw body capture
45
+
46
+ ```javascript
47
+ import express from "express";
48
+ import { rawBodySaver, shopifyWebhookMiddleware } from "hook-sign";
49
+
50
+ const app = express();
51
+
52
+ // Capture raw body (IMPORTANT)
53
+ app.use(
54
+ express.json({
55
+ verify: rawBodySaver,
56
+ })
57
+ );
58
+ ```
59
+
60
+ ### 2. Add Shopify webhook route
61
+
62
+ ```javascript
63
+ app.post(
64
+ "/webhooks/shopify",
65
+ shopifyWebhookMiddleware({
66
+ secret: process.env.SHOPIFY_SECRET, // Your Shopify App API Secret Key
67
+ }),
68
+ (req, res) => {
69
+ // ✅ Signature verified successfully
70
+
71
+ // Your webhook payload
72
+ console.log("Shopify Webhook Payload:", req.body);
73
+
74
+ res.status(200).send("OK");
75
+ }
76
+ );
77
+
78
+ app.listen(5000, () => console.log("Server running on http://localhost:5000"));
79
+ ```
80
+
81
+ ### 3. Environment Variable
82
+
83
+ Your secret should be stored in `.env`:
84
+
85
+ ```env
86
+ SHOPIFY_SECRET=your_shopify_app_api_secret_key
87
+ ```
88
+
89
+ > ✅ **Shopify secret** = Shopify App API Secret Key (not access token)
90
+
91
+ ---
92
+
93
+ ## Manual Verification (Without Express Middleware)
94
+
95
+ If you don't want middleware, you can manually verify the signature:
96
+
97
+ ```javascript
98
+ import { verifyShopifyWebhook } from "hook-sign";
99
+
100
+ const isValid = verifyShopifyWebhook({
101
+ rawBody: req.rawBody, // Buffer
102
+ secret: process.env.SHOPIFY_SECRET,
103
+ hmacHeader: req.headers["x-shopify-hmac-sha256"],
104
+ });
105
+
106
+ if (!isValid) return res.status(401).send("Invalid signature");
107
+ ```
108
+
109
+ ---
110
+
111
+ ## API Reference
112
+
113
+ ### `verifyShopifyWebhook({ rawBody, secret, hmacHeader })`
114
+
115
+ Returns `true` if Shopify HMAC signature is valid.
116
+
117
+ **Example:**
118
+
119
+ ```javascript
120
+ verifyShopifyWebhook({
121
+ rawBody: Buffer.from("..."),
122
+ secret: "my_secret",
123
+ hmacHeader: "base64_signature",
124
+ });
125
+ ```
126
+
127
+ ### `shopifyWebhookMiddleware({ secret, headerName?, onError? })`
128
+
129
+ Express middleware that automatically validates Shopify signature.
130
+
131
+ **Options:**
132
+
133
+ - `secret` (required): Shopify App Secret Key
134
+ - `headerName` (optional): Custom header name (default: `x-shopify-hmac-sha256`)
135
+ - `onError` (optional): Custom error handler
136
+
137
+ ### `rawBodySaver(req, res, buf)`
138
+
139
+ Express verify function used in `express.json()` to store raw body buffer:
140
+
141
+ ```javascript
142
+ app.use(express.json({ verify: rawBodySaver }));
143
+ ```
144
+
145
+ ---
146
+
147
+ ## Example: Custom Error Response
148
+
149
+ ```javascript
150
+ app.post(
151
+ "/webhooks/shopify",
152
+ shopifyWebhookMiddleware({
153
+ secret: process.env.SHOPIFY_SECRET,
154
+ onError: (req, res) =>
155
+ res.status(401).json({ ok: false, message: "Invalid Shopify signature" }),
156
+ }),
157
+ (req, res) => res.status(200).send("OK")
158
+ );
159
+ ```
160
+
161
+ ---
162
+
163
+ ## Troubleshooting
164
+
165
+ ### ❌ Error: `RAW_BODY_MISSING`
166
+
167
+ **✅ Fix:** Ensure you added raw body capture before your routes:
168
+
169
+ ```javascript
170
+ app.use(express.json({ verify: rawBodySaver }));
171
+ ```
172
+
173
+ ### ❌ Signature mismatch even with correct secret
174
+
175
+ **Most common causes:**
176
+
177
+ - Using parsed JSON instead of raw body
178
+ - Running a second body parser that modifies the payload
179
+ - Using wrong Shopify secret (must be App API Secret Key)
180
+
181
+ ---
182
+
183
+ ## Security Tips
184
+
185
+ - Never store Shopify secret in database
186
+ - Keep secret only in `.env` / server environment variables
187
+ - Always respond quickly to webhooks (Shopify expects 200 OK)
188
+
189
+ ---
190
+
191
+ ## License
192
+
193
+ MIT
@@ -0,0 +1,3 @@
1
+ export type HmacEncoding = "hex" | "base64";
2
+ export declare function computeHmac(rawBody: Buffer, secret: string, algorithm?: string, encoding?: HmacEncoding): string;
3
+ export declare function timingSafeEqual(a: string, b: string): boolean;
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.computeHmac = computeHmac;
37
+ exports.timingSafeEqual = timingSafeEqual;
38
+ const crypto = __importStar(require("crypto"));
39
+ function computeHmac(rawBody, secret, algorithm = "sha256", encoding = "base64") {
40
+ return crypto.createHmac(algorithm, secret).update(rawBody).digest(encoding);
41
+ }
42
+ function timingSafeEqual(a, b) {
43
+ const aBuf = Buffer.from(a);
44
+ const bBuf = Buffer.from(b);
45
+ if (aBuf.length !== bBuf.length)
46
+ return false;
47
+ return crypto.timingSafeEqual(aBuf, bBuf);
48
+ }
@@ -0,0 +1,2 @@
1
+ import type { Request } from "express";
2
+ export declare function rawBodySaver(req: Request, _res: any, buf: Buffer): void;
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.rawBodySaver = rawBodySaver;
4
+ function rawBodySaver(req, _res, buf) {
5
+ // attach raw buffer to req for later verification
6
+ req.rawBody = buf;
7
+ }
@@ -0,0 +1,7 @@
1
+ import type { Request, Response, NextFunction } from "express";
2
+ export type ShopifyMiddlewareOptions = {
3
+ secret: string;
4
+ headerName?: string;
5
+ onError?: (req: Request, res: Response) => void;
6
+ };
7
+ export declare function shopifyWebhookMiddleware(options: ShopifyMiddlewareOptions): (req: Request, res: Response, next: NextFunction) => void | Response<any, Record<string, any>>;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.shopifyWebhookMiddleware = shopifyWebhookMiddleware;
4
+ const shopify_1 = require("../providers/shopify");
5
+ function shopifyWebhookMiddleware(options) {
6
+ const headerName = options.headerName ?? "x-shopify-hmac-sha256";
7
+ return (req, res, next) => {
8
+ const rawBody = req.rawBody;
9
+ if (!rawBody) {
10
+ // If raw body missing, dev configured body parser wrong
11
+ return res.status(400).json({
12
+ ok: false,
13
+ error: "RAW_BODY_MISSING",
14
+ message: "Raw body is missing. Configure express.json({ verify }) using rawBodySaver.",
15
+ });
16
+ }
17
+ const hmacHeader = req.headers[headerName];
18
+ const ok = (0, shopify_1.verifyShopifyWebhook)({
19
+ rawBody,
20
+ secret: options.secret,
21
+ hmacHeader,
22
+ });
23
+ if (!ok) {
24
+ if (options.onError)
25
+ return options.onError(req, res);
26
+ return res.status(401).json({ ok: false, error: "INVALID_SIGNATURE" });
27
+ }
28
+ return next();
29
+ };
30
+ }
@@ -0,0 +1,4 @@
1
+ export { computeHmac, timingSafeEqual } from "./core/hmac";
2
+ export { verifyShopifyWebhook } from "./providers/shopify";
3
+ export { rawBodySaver } from "./express/rawBody";
4
+ export { shopifyWebhookMiddleware } from "./express/shopifyMiddleware";
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.shopifyWebhookMiddleware = exports.rawBodySaver = exports.verifyShopifyWebhook = exports.timingSafeEqual = exports.computeHmac = void 0;
4
+ var hmac_1 = require("./core/hmac");
5
+ Object.defineProperty(exports, "computeHmac", { enumerable: true, get: function () { return hmac_1.computeHmac; } });
6
+ Object.defineProperty(exports, "timingSafeEqual", { enumerable: true, get: function () { return hmac_1.timingSafeEqual; } });
7
+ var shopify_1 = require("./providers/shopify");
8
+ Object.defineProperty(exports, "verifyShopifyWebhook", { enumerable: true, get: function () { return shopify_1.verifyShopifyWebhook; } });
9
+ var rawBody_1 = require("./express/rawBody");
10
+ Object.defineProperty(exports, "rawBodySaver", { enumerable: true, get: function () { return rawBody_1.rawBodySaver; } });
11
+ var shopifyMiddleware_1 = require("./express/shopifyMiddleware");
12
+ Object.defineProperty(exports, "shopifyWebhookMiddleware", { enumerable: true, get: function () { return shopifyMiddleware_1.shopifyWebhookMiddleware; } });
@@ -0,0 +1,6 @@
1
+ export type ShopifyVerifyParams = {
2
+ rawBody: Buffer;
3
+ secret: string;
4
+ hmacHeader: string | undefined;
5
+ };
6
+ export declare function verifyShopifyWebhook({ rawBody, secret, hmacHeader, }: ShopifyVerifyParams): boolean;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.verifyShopifyWebhook = verifyShopifyWebhook;
4
+ const hmac_1 = require("../core/hmac");
5
+ function verifyShopifyWebhook({ rawBody, secret, hmacHeader, }) {
6
+ if (!hmacHeader)
7
+ return false;
8
+ const expected = (0, hmac_1.computeHmac)(rawBody, secret, "sha256", "base64");
9
+ return (0, hmac_1.timingSafeEqual)(expected, hmacHeader.trim());
10
+ }
@@ -0,0 +1,3 @@
1
+ export type HmacEncoding = "hex" | "base64";
2
+ export declare function computeHmac(rawBody: Buffer, secret: string, algorithm?: string, encoding?: HmacEncoding): string;
3
+ export declare function timingSafeEqual(a: string, b: string): boolean;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.computeHmac = computeHmac;
4
+ exports.timingSafeEqual = timingSafeEqual;
5
+ const crypto = require("crypto");
6
+ function computeHmac(rawBody, secret, algorithm = "sha256", encoding = "base64") {
7
+ return crypto.createHmac(algorithm, secret).update(rawBody).digest(encoding);
8
+ }
9
+ function timingSafeEqual(a, b) {
10
+ const aBuf = Buffer.from(a);
11
+ const bBuf = Buffer.from(b);
12
+ if (aBuf.length !== bBuf.length)
13
+ return false;
14
+ return crypto.timingSafeEqual(aBuf, bBuf);
15
+ }
@@ -0,0 +1,2 @@
1
+ import type { Request } from "express";
2
+ export declare function rawBodySaver(req: Request, _res: any, buf: Buffer): void;
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.rawBodySaver = rawBodySaver;
4
+ function rawBodySaver(req, _res, buf) {
5
+ // attach raw buffer to req for later verification
6
+ req.rawBody = buf;
7
+ }
@@ -0,0 +1,7 @@
1
+ import type { Request, Response, NextFunction } from "express";
2
+ export type ShopifyMiddlewareOptions = {
3
+ secret: string;
4
+ headerName?: string;
5
+ onError?: (req: Request, res: Response) => void;
6
+ };
7
+ export declare function shopifyWebhookMiddleware(options: ShopifyMiddlewareOptions): (req: Request, res: Response, next: NextFunction) => void | Response<any, Record<string, any>>;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.shopifyWebhookMiddleware = shopifyWebhookMiddleware;
4
+ const shopify_1 = require("../providers/shopify");
5
+ function shopifyWebhookMiddleware(options) {
6
+ const headerName = options.headerName ?? "x-shopify-hmac-sha256";
7
+ return (req, res, next) => {
8
+ const rawBody = req.rawBody;
9
+ if (!rawBody) {
10
+ // If raw body missing, dev configured body parser wrong
11
+ return res.status(400).json({
12
+ ok: false,
13
+ error: "RAW_BODY_MISSING",
14
+ message: "Raw body is missing. Configure express.json({ verify }) using rawBodySaver.",
15
+ });
16
+ }
17
+ const hmacHeader = req.headers[headerName];
18
+ const ok = (0, shopify_1.verifyShopifyWebhook)({
19
+ rawBody,
20
+ secret: options.secret,
21
+ hmacHeader,
22
+ });
23
+ if (!ok) {
24
+ if (options.onError)
25
+ return options.onError(req, res);
26
+ return res.status(401).json({ ok: false, error: "INVALID_SIGNATURE" });
27
+ }
28
+ return next();
29
+ };
30
+ }
@@ -0,0 +1,6 @@
1
+ export type ShopifyVerifyParams = {
2
+ rawBody: Buffer;
3
+ secret: string;
4
+ hmacHeader: string | undefined;
5
+ };
6
+ export declare function verifyShopifyWebhook({ rawBody, secret, hmacHeader, }: ShopifyVerifyParams): boolean;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.verifyShopifyWebhook = verifyShopifyWebhook;
4
+ const hmac_1 = require("../core/hmac");
5
+ function verifyShopifyWebhook({ rawBody, secret, hmacHeader, }) {
6
+ if (!hmacHeader)
7
+ return false;
8
+ const expected = (0, hmac_1.computeHmac)(rawBody, secret, "sha256", "base64");
9
+ return (0, hmac_1.timingSafeEqual)(expected, hmacHeader.trim());
10
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const supertest_1 = require("supertest");
5
+ const express_1 = require("express");
6
+ const crypto_1 = require("crypto");
7
+ const rawBody_1 = require("../src/express/rawBody");
8
+ const shopifyMiddleware_1 = require("../src/express/shopifyMiddleware");
9
+ (0, vitest_1.describe)("Shopify Express middleware", () => {
10
+ (0, vitest_1.it)("should allow request with valid signature", async () => {
11
+ const secret = "my_secret";
12
+ const app = (0, express_1.default)();
13
+ app.use(express_1.default.json({ verify: rawBody_1.rawBodySaver }));
14
+ app.post("/webhooks/shopify", (0, shopifyMiddleware_1.shopifyWebhookMiddleware)({ secret }), (req, res) => res.status(200).send("ok"));
15
+ const payload = { order_id: 999 };
16
+ // IMPORTANT: sign the exact JSON string
17
+ const raw = Buffer.from(JSON.stringify(payload), "utf8");
18
+ const hmac = crypto_1.default
19
+ .createHmac("sha256", secret)
20
+ .update(raw)
21
+ .digest("base64");
22
+ const res = await (0, supertest_1.default)(app)
23
+ .post("/webhooks/shopify")
24
+ .set("X-Shopify-Hmac-Sha256", hmac)
25
+ .send(payload);
26
+ (0, vitest_1.expect)(res.statusCode).toBe(200);
27
+ (0, vitest_1.expect)(res.text).toBe("ok");
28
+ });
29
+ (0, vitest_1.it)("should reject request with invalid signature", async () => {
30
+ const secret = "my_secret";
31
+ const app = (0, express_1.default)();
32
+ app.use(express_1.default.json({ verify: rawBody_1.rawBodySaver }));
33
+ app.post("/webhooks/shopify", (0, shopifyMiddleware_1.shopifyWebhookMiddleware)({ secret }), (req, res) => res.status(200).send("ok"));
34
+ const res = await (0, supertest_1.default)(app)
35
+ .post("/webhooks/shopify")
36
+ .set("X-Shopify-Hmac-Sha256", "INVALID")
37
+ .send({ order_id: 999 });
38
+ (0, vitest_1.expect)(res.statusCode).toBe(401);
39
+ });
40
+ });
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "hook-sign",
3
+ "version": "1.0.0",
4
+ "description": "Webhook signature verification utilities + Express middleware",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "prepublishOnly": "npm run build",
13
+ "test": "vitest"
14
+ },
15
+ "keywords": [
16
+ "webhook",
17
+ "shopify",
18
+ "hmac",
19
+ "signature",
20
+ "express"
21
+ ],
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/rakib-dev1/webhook-sign"
26
+ },
27
+ "peerDependencies": {
28
+ "express": ">=4"
29
+ },
30
+ "devDependencies": {
31
+ "@types/express": "^4.17.25",
32
+ "@types/node": "^20.19.30",
33
+ "@types/supertest": "^2.0.0",
34
+ "express": "^4.22.1",
35
+ "supertest": "^6.3.0",
36
+ "typescript": "^5.9.3",
37
+ "vitest": "^1.0.0"
38
+ }
39
+ }