nuxt-webhook-validators
Advanced tools
| import { type H3Event } from 'h3'; | ||
| /** | ||
| * Validates MailChannels webhooks on the Edge | ||
| * @see {@link https://docs.mailchannels.net/email-api/advanced/delivery-events/#verifying-message-signatures} | ||
| * @param event H3Event | ||
| * @returns {boolean} `true` if the webhook is valid, `false` otherwise | ||
| */ | ||
| export declare const isValidMailChannelsWebhook: (event: H3Event) => Promise<boolean>; |
| import { getRequestHeaders, readRawBody } from "h3"; | ||
| import { verifyPublicSignature, ED25519, validateSha256, stripPemHeaders } from "../helpers.js"; | ||
| import { useRuntimeConfig } from "#imports"; | ||
| const MAILCHANNELS_CONTENT_DIGEST = "content-digest"; | ||
| const MAILCHANNELS_SIGNATURE = "signature"; | ||
| const MAILCHANNELS_SIGNATURE_INPUT = "signature-input"; | ||
| const DEFAULT_TOLERANCE = 300; | ||
| const validateContentDigest = async (header, body) => { | ||
| const match = header.match(/^(.*?)=:(.*?):$/); | ||
| if (!match) return false; | ||
| const [, algorithm, hash] = match; | ||
| const normalizedAlgorithm = algorithm.replace("-", "").toLowerCase(); | ||
| if (!["sha256"].includes(normalizedAlgorithm)) return false; | ||
| return validateSha256(hash, body, { encoding: "base64" }); | ||
| }; | ||
| const extractSignature = (signatureHeader) => { | ||
| const signatureMatch = signatureHeader.match(/sig_\d+=:([^:]+):/); | ||
| return signatureMatch ? signatureMatch[1] : null; | ||
| }; | ||
| const extractInputValues = (header) => { | ||
| const regex = /^(\w+)=\(([^)]+)\);created=(\d+);alg="([^"]+)";keyid="([^"]+)"$/; | ||
| const match = header.match(regex); | ||
| if (!match) return null; | ||
| return { | ||
| name: match[1], | ||
| timestamp: Number.parseInt(match[3], 10), | ||
| algorithm: match[4], | ||
| keyId: match[5] | ||
| }; | ||
| }; | ||
| export const isValidMailChannelsWebhook = async (event) => { | ||
| const config = useRuntimeConfig(event).webhook.mailchannels; | ||
| const headers = getRequestHeaders(event); | ||
| const body = await readRawBody(event); | ||
| const contentDigest = headers[MAILCHANNELS_CONTENT_DIGEST]; | ||
| const messageSignature = headers[MAILCHANNELS_SIGNATURE]; | ||
| const signatureInput = headers[MAILCHANNELS_SIGNATURE_INPUT]; | ||
| if (!body || !contentDigest || !messageSignature || !signatureInput) return false; | ||
| if (!await validateContentDigest(contentDigest, body)) return false; | ||
| const signature = extractSignature(messageSignature); | ||
| if (!signature) return false; | ||
| const values = extractInputValues(signatureInput); | ||
| if (!values) return false; | ||
| const now = Math.floor(Date.now() / 1e3); | ||
| if (now - values.timestamp > DEFAULT_TOLERANCE) return false; | ||
| const signingString = `"content-digest": ${contentDigest} | ||
| "@signature-params": ("content-digest");created=${values.timestamp};alg="${values.algorithm}";keyid="${values.keyId}"`; | ||
| let publicKey = config.publicKey; | ||
| if (!publicKey) { | ||
| const publicKeyResponse = await $fetch("/tx/v1/webhook/public-key", { | ||
| baseURL: "https://api.mailchannels.net", | ||
| query: { id: values.keyId } | ||
| }).catch(() => null); | ||
| if (!publicKeyResponse) return false; | ||
| publicKey = publicKeyResponse.key; | ||
| } | ||
| publicKey = stripPemHeaders(publicKey); | ||
| const isValid = await verifyPublicSignature(publicKey, ED25519, signingString, signature, { | ||
| encoding: "base64", | ||
| format: "spki" | ||
| }); | ||
| return isValid; | ||
| }; |
@@ -7,2 +7,3 @@ import * as _nuxt_schema from '@nuxt/schema'; | ||
| export { type ModuleOptions, _default as default }; | ||
| export { _default as default }; | ||
| export type { ModuleOptions }; |
+2
-1
@@ -7,2 +7,3 @@ import * as _nuxt_schema from '@nuxt/schema'; | ||
| export { type ModuleOptions, _default as default }; | ||
| export { _default as default }; | ||
| export type { ModuleOptions }; |
+1
-1
@@ -7,3 +7,3 @@ { | ||
| }, | ||
| "version": "0.1.10", | ||
| "version": "0.1.11", | ||
| "builder": { | ||
@@ -10,0 +10,0 @@ "@nuxt/module-builder": "0.8.4", |
+3
-0
@@ -36,2 +36,5 @@ import { defineNuxtModule, createResolver, addServerImportsDir } from '@nuxt/kit'; | ||
| }); | ||
| runtimeConfig.webhook.mailchannels = defu(runtimeConfig.webhook.mailchannels, { | ||
| publicKey: "" | ||
| }); | ||
| runtimeConfig.webhook.meta = defu(runtimeConfig.webhook.meta, { | ||
@@ -38,0 +41,0 @@ appSecret: "" |
@@ -32,1 +32,5 @@ import { type webcrypto } from 'node:crypto'; | ||
| export declare const stripPemHeaders: (pem: string) => string; | ||
| export declare const sha256: (payload: string | object, encoding?: BufferEncoding) => Promise<string>; | ||
| export declare const validateSha256: (hash: string, payload: string, options?: Partial<{ | ||
| encoding: BufferEncoding; | ||
| }>) => Promise<boolean>; |
@@ -44,1 +44,9 @@ import { subtle } from "node:crypto"; | ||
| export const stripPemHeaders = (pem) => pem.replace(/-----[^-]+-----|\s/g, ""); | ||
| export const sha256 = async (payload, encoding) => { | ||
| const buffer = typeof payload === "object" ? Buffer.from(JSON.stringify(payload)) : encoder.encode(payload); | ||
| const signatureBuffer = await subtle.digest(HMAC_SHA256.hash, buffer); | ||
| return Buffer.from(signatureBuffer).toString(encoding ?? "hex"); | ||
| }; | ||
| export const validateSha256 = async (hash, payload, options) => { | ||
| return hash === await sha256(payload, options?.encoding); | ||
| }; |
@@ -1,5 +0,3 @@ | ||
| import { subtle } from "node:crypto"; | ||
| import { Buffer } from "node:buffer"; | ||
| import { getRequestHeaders, readRawBody } from "h3"; | ||
| import { encoder, HMAC_SHA256, ensureConfiguration } from "../helpers.js"; | ||
| import { ensureConfiguration, sha256 } from "../helpers.js"; | ||
| const NUXTHUB_SIGNATURE = "x-nuxthub-signature"; | ||
@@ -13,6 +11,5 @@ export const isValidNuxtHubWebhook = async (event) => { | ||
| const payload = body + config.secretKey; | ||
| const signatureBuffer = await subtle.digest(HMAC_SHA256.hash, encoder.encode(payload)); | ||
| const signature = Buffer.from(signatureBuffer).toString("hex"); | ||
| const signature = await sha256(payload); | ||
| return signature === webhookSignature; | ||
| }; | ||
| export const isValidNuxthubWebhook = isValidNuxtHubWebhook; |
+7
-1
@@ -1,1 +0,7 @@ | ||
| export { type ModuleOptions, default } from './module.js' | ||
| import type { NuxtModule } from '@nuxt/schema' | ||
| import type { default as Module } from './module.js' | ||
| export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any> | ||
| export { default } from './module.js' |
+7
-1
@@ -1,1 +0,7 @@ | ||
| export { type ModuleOptions, default } from './module' | ||
| import type { NuxtModule } from '@nuxt/schema' | ||
| import type { default as Module } from './module' | ||
| export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any> | ||
| export { default } from './module' |
+10
-10
| { | ||
| "name": "nuxt-webhook-validators", | ||
| "version": "0.1.10", | ||
| "version": "0.1.11", | ||
| "description": "A simple nuxt module that works on the edge to easily validate incoming webhooks from different services.", | ||
@@ -47,3 +47,3 @@ "keywords": [ | ||
| "dependencies": { | ||
| "@nuxt/kit": "^3.16.0", | ||
| "@nuxt/kit": "^3.16.1", | ||
| "defu": "^6.1.4", | ||
@@ -53,16 +53,16 @@ "scule": "^1.3.0" | ||
| "devDependencies": { | ||
| "@nuxt/devtools": "^2.2.1", | ||
| "@nuxt/eslint-config": "^1.1.0", | ||
| "@nuxt/devtools": "^2.3.1", | ||
| "@nuxt/eslint-config": "^1.2.0", | ||
| "@nuxt/module-builder": "^0.8.4", | ||
| "@nuxt/schema": "^3.16.0", | ||
| "@nuxt/test-utils": "^3.17.1", | ||
| "@types/node": "^22.13.9", | ||
| "@nuxt/schema": "^3.16.1", | ||
| "@nuxt/test-utils": "^3.17.2", | ||
| "@types/node": "^22.13.10", | ||
| "changelogen": "^0.6.1", | ||
| "eslint": "^9.22.0", | ||
| "nuxt": "^3.16.0", | ||
| "nuxt": "^3.16.1", | ||
| "typescript": "^5.8.2", | ||
| "vitest": "^3.0.8", | ||
| "vitest": "^3.0.9", | ||
| "vue-tsc": "^2.2.8" | ||
| }, | ||
| "packageManager": "pnpm@10.6.1", | ||
| "packageManager": "pnpm@10.6.5", | ||
| "changelog": { | ||
@@ -69,0 +69,0 @@ "repo": { |
+2
-1
@@ -17,3 +17,3 @@  | ||
| - 17 [Webhook validators](#supported-webhook-validators) | ||
| - 18 [Webhook validators](#supported-webhook-validators) | ||
| - Works on the edge | ||
@@ -89,2 +89,3 @@ - Exposed [Server utils](#server-utils) | ||
| - Kick | ||
| - MailChannels | ||
| - Meta | ||
@@ -91,0 +92,0 @@ - NuxtHub |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
40784
11.37%48
4.35%737
13.38%166
0.61%2
100%Updated