87 lines
3.0 KiB
JavaScript
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
|