Skip to content
Merged
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
6 changes: 6 additions & 0 deletions backend/src/utils/backoff.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export function calculateBackoff(attempt: number): number {
// Exponential backoff with jitter
const base = Math.pow(2, attempt) * 1000;
const jitter = Math.random() * 500;
return base + jitter;
}
18 changes: 18 additions & 0 deletions backend/src/utils/observability.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import winston from "winston";

export const logger = winston.createLogger({
level: "info",
format: winston.format.json(),
transports: [new winston.transports.Console()],
});

export function logDelivery(url: string, status: string, attempt: number, error?: string) {
logger.info({
event: "webhook_delivery",
url,
status,
attempt,
error,
timestamp: new Date().toISOString(),
});
}
14 changes: 14 additions & 0 deletions backend/src/utils/signing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import crypto from "crypto";

export function generateSecret(): string {
return crypto.randomBytes(32).toString("hex");
}

export function signPayload(secret: string, payload: string): string {
return crypto.createHmac("sha256", secret).update(payload).digest("hex");
}

export function verifySignature(secret: string, payload: string, signature: string): boolean {
const expected = signPayload(secret, payload);
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
12 changes: 12 additions & 0 deletions backend/src/webhooks/webhooks.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { AppDataSource } from "../data-source";
import { WebhookDelivery } from "../entities/webhook.entity";

export const webhookRepo = AppDataSource.getRepository(WebhookDelivery);

export async function saveDelivery(delivery: Partial<WebhookDelivery>) {
return webhookRepo.save(delivery);
}

export async function getDeliveryHistory(url: string) {
return webhookRepo.find({ where: { webhookUrl: url }, order: { attemptedAt: "DESC" } });
}
15 changes: 15 additions & 0 deletions backend/src/webhooks/webhooks.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { Webhook, WebhookEventType } from './webhook.entity';
import { CreateWebhookDto } from './dto/create-webhook.dto';
import { UpdateWebhookDto } from './dto/update-webhook.dto';
import { NotFoundException, BadRequestException } from '@nestjs/common';
import { deliverWebhook } from "../webhooks.service";
import { webhookRepo } from "../webhooks.repository";

describe('WebhooksService', () => {
let service: WebhooksService;
Expand Down Expand Up @@ -225,4 +227,17 @@ describe('WebhooksService', () => {
expect(webhook.isActive).toBe(false);
});
});

describe("Webhooks Service", () => {
it("should save successful delivery", async () => {
await deliverWebhook("http://localhost:4000/test", { foo: "bar" }, "secret");
const history = await webhookRepo.find();
expect(history[0].status).toBe("success");
});

it("should retry on failure", async () => {
await deliverWebhook("http://invalid-url", { foo: "bar" }, "secret");
const history = await webhookRepo.find();
expect(history[0].status).toBe("failed");
});
});
Loading