Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions plugins/webhooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const plugins = [
options: {
// Add here the subcribers you will define
subscriptions: ["product.created", "product.updated"],
secretKey:"Your-webhook-secret"
},
},
];
Expand Down
75 changes: 35 additions & 40 deletions plugins/webhooks/src/modules/webhooks/service.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -23,85 +24,79 @@ 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.');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wording here is a little extreme haha. Can we tweak this a bit?

No webhook secret key was found. Please provide a secretKey in the plugin options. A default will be used instead, but this may reduce the security of webhook signatures.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure.. Will fix it

this.options.secretKey = 'No-Secret-Key'; // Default string to prevent runtime errors
}
this.logger = container.logger;
}

public async send(
subscription: WebhookModel,
payload: any
): Promise<WebhookSendResponse> {
createHmacSignature(payload: Record<string, unknown>) {
return Crypto.createHmac('sha256', this.options.secretKey).update(JSON.stringify(payload)).digest('hex');
}

public async send(subscription: WebhookModel, payload: Record<string, unknown>): Promise<WebhookSendResponse> {
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) {
return this.onSendError(err, subscription, payload);
}
}

public async sendWebhooksEvents(webhooks: WebhookModel[], payload: any) {
console.log("webhooks", webhooks);
public async sendWebhooksEvents(webhooks: WebhookModel[], payload: Record<string, unknown>) {
const results = (await Promise.allSettled(
webhooks?.map((webhook) => this.send(webhook, payload))
)) as PromiseFulfilledResult<WebhookSendResponse>[];

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;
}

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, {
Expand All @@ -124,10 +119,10 @@ class WebhooksService extends MedusaService({
private onSendError(
err: WebhookSendResponseError,
subscription: WebhookModel,
payload: any
payload: Record<string, unknown>
): WebhookSendResponse {
this.logger.error(
"Error sending webhook",
'Error sending webhook',
new Error(
`Error sending webhook: ${subscription.event_type} -> ${
subscription.target_url
Expand All @@ -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,
};
}
Expand Down