import crypto from "crypto";
import jwt from "jsonwebtoken";
// Capture the raw body: signing is over bytes, not re-serialised JSON.
app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf.toString(); } }));
function verifyPatchwork(req, res, next) {
const header = req.get("Patchwork-Signature");
if (!header) return res.status(401).end();
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.status(401).end();
const digest = crypto.createHash("sha256").update(req.rawBody ?? "").digest("base64");
const expected = crypto
.createHmac("sha256", process.env.PATCHWORK_REQUEST_SECRET)
.update(`${t}.${req.method}.${req.path}.${digest}`)
.digest("hex");
const a = Buffer.from(expected), b = Buffer.from(v1 ?? "");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.status(401).end();
// On a minted connection, the subject token rides alongside the signature.
const bearer = req.get("Authorization")?.replace(/^Bearer /, "");
if (bearer) {
const claims = jwt.verify(bearer, process.env.YOUR_PUBLIC_KEY, {
algorithms: ["RS256"], audience: "your-api",
});
req.subject = claims.sub;
}
next();
}
app.post("/tools/orders/:id/refund", verifyPatchwork, async (req, res) => {
// Your normal authorization runs unchanged, against the subject.
const [userId, tenantId] = req.subject.split(":");
await authorizeRefund({ userId, tenantId, orderId: req.params.id });
const refund = await refundOrder(req.params.id, req.body.amount);
res.json({ reference: refund.reference });
});