import { ApiError } from "../errors"; import { readIdempotency, writeIdempotency } from "../idempotency/idempotencyStore"; import { env } from "../../config/env"; import { successResponse } from "./errorMiddleware"; export function idempotency(options) { return function idempotencyMiddleware(req, _res, next) { const idempotencyKey = req.header("idempotency-key"); if (!idempotencyKey) { if (options.required === false) { return next(); } return next(new ApiError("DUPLICATE_REQUEST", "Missing Idempotency-Key", 400)); } const cached = readIdempotency(options.scope, idempotencyKey); if (cached) { const cachedPayload = cached.response ?? cached; const cachedStatus = cached.statusCode || 200; const payload = (() => { if (cachedPayload && typeof cachedPayload === "object" && "data" in cachedPayload && "request_id" in cachedPayload && "timestamp" in cachedPayload) { const typed = cachedPayload; typed.request_id = req.requestId; typed.timestamp = new Date().toISOString(); return typed; } return cachedPayload; })(); return _res.status(cachedStatus).json(payload); } req.body = { ...(req.body || {}), __idempotencyKey: idempotencyKey }; const originalJson = _res.json.bind(_res); const originalStatus = _res.status.bind(_res); let statusCode = 200; _res.status = function statusWithStore(code) { statusCode = code; return originalStatus(code); }; _res.json = function jsonWithStore(payload) { const responsePayload = payload && typeof payload === "object" && "data" in payload && "request_id" in payload && "timestamp" in payload ? successResponse(req, payload.data) : payload; writeIdempotency(options.scope, idempotencyKey, { response: responsePayload, statusCode, at: Date.now() }, options.ttlMs || env.IDEMPOTENCY_TTL_MS); return originalJson(responsePayload); }; next(); }; }