diff --git a/plugins/webhooks/README.md b/plugins/webhooks/README.md index d7f8cce6..8d292a06 100644 --- a/plugins/webhooks/README.md +++ b/plugins/webhooks/README.md @@ -29,6 +29,7 @@ const plugins = [ options: { // Add here the subcribers you will define subscriptions: ["product.created", "product.updated"], + secretKey:"Your-webhook-secret" }, }, ]; diff --git a/plugins/webhooks/src/modules/webhooks/service.ts b/plugins/webhooks/src/modules/webhooks/service.ts index 7919b123..5fd1afaa 100644 --- a/plugins/webhooks/src/modules/webhooks/service.ts +++ b/plugins/webhooks/src/modules/webhooks/service.ts @@ -1,12 +1,13 @@ -import { MedusaError, MedusaService } from "@medusajs/framework/utils"; -import { Webhook } from "./models/webhooks"; -import { LoaderOptions, Logger } from "@medusajs/framework/types"; -import { WebhookModel } from "../../common"; +import { MedusaError, MedusaService } from '@medusajs/framework/utils'; +import { Webhook } from './models/webhooks'; +import type { LoaderOptions, Logger } from '@medusajs/framework/types'; +import type { WebhookModel } from '../../common'; +import Crypto from 'node:crypto'; export type WebhookSendResponse = { event_type: string; target_url: string; - result: "success" | "error"; + result: 'success' | 'error'; data?: any; message?: string; err?: any; @@ -23,45 +24,49 @@ type ConstructorParams = { logger: Logger; }; +type WebhookOptions = LoaderOptions & { subscriptions: string[] } & { secretKey: string }; + class WebhooksService extends MedusaService({ Webhook, }) { public subscriptions: string[] = []; private logger: Logger; - - constructor( - container: ConstructorParams, - options: LoaderOptions & { subscriptions: string[] } - ) { + private options: WebhookOptions; + constructor(container: ConstructorParams, options: WebhookOptions) { super(container, options); + this.options = options; this.subscriptions = options.subscriptions; + if (!this.options.secretKey) { + this.logger.warn('No secretKey provided for webhook signatures. Webhook security will be compromised.'); + this.options.secretKey = 'No-Secret-Key'; // Default string to prevent runtime errors + } this.logger = container.logger; } - public async send( - subscription: WebhookModel, - payload: any - ): Promise { + createHmacSignature(payload: Record) { + return Crypto.createHmac('sha256', this.options.secretKey).update(JSON.stringify(payload)).digest('hex'); + } + + public async send(subscription: WebhookModel, payload: Record): Promise { const { event_type, target_url } = subscription; try { const response = await fetch(target_url, { - method: "POST", + method: 'POST', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', + 'x-webhook-signature': this.createHmacSignature(payload), }, body: JSON.stringify(payload), }); - const contentType = response.headers.get("content-type"); - const data = contentType?.includes("json") - ? await response.json() - : await response.text(); + const contentType = response.headers.get('content-type'); + const data = contentType?.includes('json') ? await response.json() : await response.text(); return { event_type, target_url, - result: "success", + result: 'success', data, }; } catch (err) { @@ -69,19 +74,15 @@ class WebhooksService extends MedusaService({ } } - public async sendWebhooksEvents(webhooks: WebhookModel[], payload: any) { - console.log("webhooks", webhooks); + public async sendWebhooksEvents(webhooks: WebhookModel[], payload: Record) { const results = (await Promise.allSettled( webhooks?.map((webhook) => this.send(webhook, payload)) )) as PromiseFulfilledResult[]; results.forEach((result) => { - const resultMessage = - result.value?.result === "error" ? "failed" : "succeeded"; + const resultMessage = result.value?.result === 'error' ? 'failed' : 'succeeded'; - this.logger.info( - `Webhook ${result.value?.event_type} -> ${result.value?.target_url} ${resultMessage}.` - ); + this.logger.info(`Webhook ${result.value?.event_type} -> ${result.value?.target_url} ${resultMessage}.`); }); return results; @@ -89,19 +90,13 @@ class WebhooksService extends MedusaService({ public async testWebhookSubscription(testData?: WebhookModel) { if (!testData) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Test data is required." - ); + throw new MedusaError(MedusaError.Types.INVALID_DATA, 'Test data is required.'); } const eventType = this.detectTypeOfEvent(testData.event_type); if (!eventType) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Event type ${testData?.event_type} is not supported.` - ); + throw new MedusaError(MedusaError.Types.INVALID_DATA, `Event type ${testData?.event_type} is not supported.`); } const response = await this.send(testData, { @@ -124,10 +119,10 @@ class WebhooksService extends MedusaService({ private onSendError( err: WebhookSendResponseError, subscription: WebhookModel, - payload: any + payload: Record ): WebhookSendResponse { this.logger.error( - "Error sending webhook", + 'Error sending webhook', new Error( `Error sending webhook: ${subscription.event_type} -> ${ subscription.target_url @@ -138,8 +133,8 @@ class WebhooksService extends MedusaService({ return { event_type: subscription.event_type, target_url: subscription.target_url, - result: "error", - message: err?.message ?? err?.cause?.code ?? "Unknown error", + result: 'error', + message: err?.message ?? err?.cause?.code ?? 'Unknown error', err: err?.cause ?? err, }; }