diff --git a/bun.lock b/bun.lock index 029d0baf..c546a6b0 100644 --- a/bun.lock +++ b/bun.lock @@ -532,7 +532,7 @@ }, "packages/payment": { "name": "@wrnexus/payment", - "version": "0.8.1", + "version": "0.8.2", "dependencies": { "@wrnexus/db": "workspace:*", "@wrnexus/plugin": "workspace:*", diff --git a/packages/payment/package.json b/packages/payment/package.json index 639a62db..e044c1c9 100644 --- a/packages/payment/package.json +++ b/packages/payment/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/payment", - "version": "0.8.1", + "version": "0.8.2", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/payment/src/adapters.ts b/packages/payment/src/adapters.ts index 33275629..b3b66a16 100644 --- a/packages/payment/src/adapters.ts +++ b/packages/payment/src/adapters.ts @@ -343,6 +343,23 @@ export interface RazorpayConfig { webhookSecret: string; theme?: { color?: string }; } +export interface RazorpayCheckoutResult { + orderId: string; + paymentId: string; + signature: string; +} + +/** Verify the signed result returned by Razorpay Standard Checkout. */ +export async function verifyRazorpayCheckoutSignature( + result: RazorpayCheckoutResult, + keySecret: string, +) { + if (!result.orderId || !result.paymentId || !result.signature || !keySecret) return false; + return equal( + result.signature.toLowerCase(), + await hmac(`${result.orderId}|${result.paymentId}`, keySecret), + ); +} export function razorpayGateway(config: RazorpayConfig): PaymentGateway { const auth = `Basic ${btoa(`${config.keyId}:${config.keySecret}`)}`; const request = (path: string, init: RequestInit = {}) => diff --git a/packages/payment/test/adapter-contract.test.ts b/packages/payment/test/adapter-contract.test.ts index 3db1c357..12740504 100644 --- a/packages/payment/test/adapter-contract.test.ts +++ b/packages/payment/test/adapter-contract.test.ts @@ -3,6 +3,7 @@ import { exerciseGatewayContract, paypalGateway, razorpayGateway, + verifyRazorpayCheckoutSignature, sandboxGateway, signSandboxWebhook, stripeGateway, @@ -180,6 +181,22 @@ test("shared executable contract drives Razorpay through its HTTP adapter", asyn expect(report).toEqual({ ok: true, issues: [] }); }); +test("Razorpay checkout signatures are verified without trusting browser data", async () => { + const secret = "checkout-secret"; + const orderId = "order_1"; + const paymentId = "pay_1"; + const signed = await signature(`${orderId}|${paymentId}`, secret); + expect( + await verifyRazorpayCheckoutSignature({ orderId, paymentId, signature: signed }, secret), + ).toBe(true); + expect( + await verifyRazorpayCheckoutSignature( + { orderId, paymentId: "pay_tampered", signature: signed }, + secret, + ), + ).toBe(false); +}); + test("shared executable contract drives PayPal through its HTTP adapter", async () => { globalThis.fetch = (async (value, init) => { const url = String(value),