Files
alrahma_sunday_school_api/apps/api/dist/security/webhookIdempotency.js
T
2026-06-11 03:22:12 -04:00

87 lines
3.0 KiB
JavaScript

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.processWebhookOnce = processWebhookOnce;
exports.getWebhookEventId = getWebhookEventId;
const crypto_1 = __importDefault(require("crypto"));
function payloadHash(rawBody) {
return crypto_1.default.createHash('sha256').update(rawBody).digest('hex');
}
function getWebhookEventModel() {
try {
// Lazy load keeps isolated unit tests from requiring a generated Prisma client
// before `prisma generate` has run in the local sandbox.
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { prisma } = require('../lib/prisma');
return prisma.webhookEvent;
}
catch {
return undefined;
}
}
async function findExisting(model, provider, providerEventId) {
return model.findUnique({
where: { provider_providerEventId: { provider, providerEventId } },
});
}
async function mark(model, id, status, errorMessage) {
await model.update({
where: { id },
data: {
status,
processedAt: status === 'PROCESSED' ? new Date() : undefined,
errorMessage: errorMessage?.slice(0, 1000),
},
});
}
async function processWebhookOnce({ provider, providerEventId, eventType, rawBody, handle, }) {
const model = getWebhookEventModel();
if (!model) {
const result = await handle();
return { duplicate: false, result };
}
const hash = payloadHash(rawBody);
let eventRecord;
try {
eventRecord = await model.create({
data: {
provider,
providerEventId,
eventType,
status: 'PROCESSING',
payloadHash: hash,
},
});
}
catch (err) {
const existing = await findExisting(model, provider, providerEventId);
if (existing?.status === 'PROCESSED')
return { duplicate: true };
if (existing?.payloadHash && existing.payloadHash !== hash) {
throw new Error('Webhook event ID replayed with a different payload hash');
}
if (existing?.status === 'PROCESSING')
return { duplicate: true };
eventRecord = existing;
}
try {
const result = await handle();
if (eventRecord?.id)
await mark(model, eventRecord.id, 'PROCESSED');
return { duplicate: false, result };
}
catch (err) {
if (eventRecord?.id)
await mark(model, eventRecord.id, 'FAILED', err?.message);
throw err;
}
}
function getWebhookEventId(provider, payload) {
if (provider === 'paypal') {
return String(payload.id ?? payload.event_id ?? `${payload.event_type ?? 'unknown'}:${payload.resource?.id ?? 'missing'}`);
}
return String(payload.event_id ?? payload.id ?? payload.transaction_id ?? 'missing');
}
//# sourceMappingURL=webhookIdempotency.js.map