Enable LLMs to handle webhooks with plaintext files
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

add optional prefix stripping to hmac verification

+76 -8
+2
README.md
··· 29 29 verify: 30 30 hmac: 31 31 header: X-My-Header-Signature 32 + prefix: "sha256=" # optional: stripped before comparing the digest 32 33 secret: $MY_WEBHOOK_SECRET 33 34 payload: 34 35 contentType: json ··· 118 119 verify: 119 120 hmac: 120 121 header: X-Hub-Signature-256 # or query: param-name 122 + prefix: "sha256=" # optional: stripped before comparing the digest 121 123 secret: $ENV_VAR_NAME # must be an environment variable reference 122 124 ``` 123 125
+45
packages/core/src/__tests__/verify.test.ts
··· 108 108 expect(await verifyRequest(lure, req)).toBe(false); 109 109 }); 110 110 111 + it("returns true for valid HMAC with matching prefix stripped", async () => { 112 + const secret = "mysecret"; 113 + process.env["TEST_SECRET"] = secret; 114 + const body = new Uint8Array(Buffer.from("hello world")); 115 + const sig = "sha256=" + computeHmac(secret, body); 116 + 117 + const lure = makeLure({ 118 + frontmatter: { 119 + verify: { hmac: { secret: "$TEST_SECRET", header: "X-Sig", prefix: "sha256=" } }, 120 + }, 121 + }); 122 + const req = makeRequest({ headers: new Headers({ "X-Sig": sig }), rawBody: body }); 123 + expect(await verifyRequest(lure, req)).toBe(true); 124 + }); 125 + 126 + it("returns false when prefix is declared but absent from signature", async () => { 127 + const secret = "mysecret"; 128 + process.env["TEST_SECRET"] = secret; 129 + const body = new Uint8Array(Buffer.from("hello world")); 130 + const sig = computeHmac(secret, body); // no prefix 131 + 132 + const lure = makeLure({ 133 + frontmatter: { 134 + verify: { hmac: { secret: "$TEST_SECRET", header: "X-Sig", prefix: "sha256=" } }, 135 + }, 136 + }); 137 + const req = makeRequest({ headers: new Headers({ "X-Sig": sig }), rawBody: body }); 138 + expect(await verifyRequest(lure, req)).toBe(false); 139 + }); 140 + 141 + it("returns false when prefix does not match", async () => { 142 + const secret = "mysecret"; 143 + process.env["TEST_SECRET"] = secret; 144 + const body = new Uint8Array(Buffer.from("hello world")); 145 + const sig = "sha1=" + computeHmac(secret, body); 146 + 147 + const lure = makeLure({ 148 + frontmatter: { 149 + verify: { hmac: { secret: "$TEST_SECRET", header: "X-Sig", prefix: "sha256=" } }, 150 + }, 151 + }); 152 + const req = makeRequest({ headers: new Headers({ "X-Sig": sig }), rawBody: body }); 153 + expect(await verifyRequest(lure, req)).toBe(false); 154 + }); 155 + 111 156 it("returns true for valid HMAC via query param", async () => { 112 157 const secret = "mysecret"; 113 158 process.env["TEST_SECRET"] = secret;
+9 -2
packages/core/src/schema.ts
··· 4 4 5 5 const SECRET_PATTERN = /^\$[A-Z_][A-Z0-9_]*$/; 6 6 7 - const verifyParamsShape = { 7 + const hmacParamsShape = { 8 + secret: "string", 9 + "header?": "string", 10 + "query?": "string", 11 + "prefix?": "string", 12 + } as const; 13 + 14 + const literalParamsShape = { 8 15 secret: "string", 9 16 "header?": "string", 10 17 "query?": "string", 11 18 } as const; 12 19 13 20 const frontmatterValidator = type({ 14 - "verify?": type({ hmac: verifyParamsShape }).or({ literal: verifyParamsShape }), 21 + "verify?": type({ hmac: hmacParamsShape }).or({ literal: literalParamsShape }), 15 22 "payload?": { 16 23 "contentType?": '"json"', 17 24 },
+10 -3
packages/core/src/types.ts
··· 7 7 rawBody: Uint8Array; 8 8 } 9 9 10 - export interface LureVerifyParams { 10 + export interface LureHmacParams { 11 + secret: string; 12 + header?: string; 13 + query?: string; 14 + prefix?: string; 15 + } 16 + 17 + export interface LureLiteralParams { 11 18 secret: string; 12 19 header?: string; 13 20 query?: string; 14 21 } 15 22 16 23 export type LureVerify = 17 - | { hmac: LureVerifyParams } 18 - | { literal: LureVerifyParams }; 24 + | { hmac: LureHmacParams } 25 + | { literal: LureLiteralParams }; 19 26 20 27 export interface LurePayload { 21 28 contentType?: "json";
+10 -3
packages/core/src/verify.ts
··· 1 1 import { createHmac, timingSafeEqual } from "node:crypto"; 2 - import type { ParsedLure, LureRequest, LureVerifyParams } from "./types.js"; 2 + import type { ParsedLure, LureRequest, LureHmacParams, LureLiteralParams } from "./types.js"; 3 3 4 4 function safeEqual(a: string, b: string): boolean { 5 5 const aBuf = Buffer.from(a); ··· 11 11 } 12 12 13 13 function extractSignature( 14 - params: LureVerifyParams, 14 + params: LureHmacParams | LureLiteralParams, 15 15 req: LureRequest, 16 16 ): string | null { 17 17 if (params.header !== undefined) { ··· 31 31 secretValue: string, 32 32 signature: string, 33 33 rawBody: Uint8Array, 34 + prefix: string | undefined, 34 35 ): boolean { 36 + if (prefix !== undefined) { 37 + if (!signature.startsWith(prefix)) { 38 + return false; 39 + } 40 + signature = signature.slice(prefix.length); 41 + } 35 42 const digest = createHmac("sha256", secretValue).update(rawBody).digest("hex"); 36 43 return safeEqual(signature, digest); 37 44 } ··· 63 70 } 64 71 65 72 if ("hmac" in verify) { 66 - return verifyHmac(secretValue, signature, req.rawBody); 73 + return verifyHmac(secretValue, signature, req.rawBody, verify.hmac.prefix); 67 74 } else { 68 75 return verifyLiteral(secretValue, signature); 69 76 }