archetecture security fix
This commit is contained in:
+87
@@ -0,0 +1,87 @@
|
||||
"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
|
||||
Reference in New Issue
Block a user