# Webhook हस्ताक्षर की जाँच

भुगतान की सूचना ऐसे पते पर आती है, जिसे सिर्फ़ आपका स्टोर और हमारी सेवा जानते हैं। लेकिन पता लीक हो सकता है — लॉग से, कॉन्फ़िगरेशन से, डेवलपमेंट के इतिहास से। असली सूचना को जालसाज़ी से अलग पहचानने के लिए हर अनुरोध पर Webhook की गुप्त कुंजी से हस्ताक्षर होता है।

ऑर्डर पूरा करने या उसकी स्थिति बदलने से पहले हस्ताक्षर की जाँच करें।

## हस्ताक्षर के हेडर

| हेडर | मान |
| ------------- | ------------------------------------------------ |
| `X-Timestamp` | भेजने का समय, सेकंड में unix time |
| `X-Signature` | `sha256=` उपसर्ग और hex में HMAC-SHA256 |

जिस स्ट्रिंग पर हस्ताक्षर होता है, वह है भेजने का समय, एक बिंदु और अनुरोध की बॉडी:

```
1756901234.{"wallet":{...},"project":{...},"invoice":{...},"payment":{...}}
```

साइनिंग कुंजी प्रोजेक्ट सेटिंग्स का Webhook की गुप्त कुंजी (Webhook secret) फ़ील्ड है। मान लीक हो जाने पर आप उसे वहीं दोबारा जारी कर सकते हैं।

## जाँच के चरण

1. JSON पार्स करने से पहले की raw अनुरोध बॉडी लें।
2. `X-Timestamp` का मान, एक बिंदु और बॉडी को आपस में जोड़ लें।
3. Webhook की गुप्त कुंजी से HMAC-SHA256 की गणना करें।
4. नतीजे की तुलना `X-Signature` से constant-time तुलना के ज़रिए करें।
5. अगर `X-Timestamp` मौजूदा समय से बहुत दूर है, तो अनुरोध ठुकरा दें।

## लागू करने के उदाहरण

**PHP**

```php showLineNumbers
<?php
$secret = 'webhook secret from your project settings';

$body      = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';

// Guards against a replayed request
if (abs(time() - (int)$timestamp) > 300) {
    http_response_code(400);
    exit;
}

$expected = 'sha256='.hash_hmac('sha256', $timestamp.'.'.$body, $secret);

// hash_equals compares in constant time
if (!hash_equals($expected, $signature)) {
    http_response_code(403);
    exit;
}

$data = json_decode($body, true);

// Handle the order

http_response_code(200);
```

**TypeScript**

```typescript showLineNumbers
import { createHmac, timingSafeEqual } from 'node:crypto';
import express, { type Request, type Response } from 'express';

const app = express();
const secret = 'webhook secret from your project settings';

// The body must stay raw, so express.raw instead of express.json
app.post('/webhook', express.raw({ type: 'application/json' }), (req: Request, res: Response) => {
    const timestamp = req.get('X-Timestamp') ?? '';
    const signature = req.get('X-Signature') ?? '';
    const body = (req.body as Buffer).toString('utf8');

    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
        return res.sendStatus(400);
    }

    const expected = 'sha256=' + createHmac('sha256', secret)
        .update(`${timestamp}.${body}`, 'utf8')
        .digest('hex');

    const a = Buffer.from(expected);
    const b = Buffer.from(signature);

    // Check the length first: timingSafeEqual needs buffers of equal size
    if (a.length !== b.length || !timingSafeEqual(a, b)) {
        return res.sendStatus(403);
    }

    const data = JSON.parse(body);

    // Handle the order

    res.sendStatus(200);
});
```

**JavaScript**

वही हैंडलर बिना types के — सादे JavaScript प्रोजेक्ट के लिए।

```js showLineNumbers
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';

const app = express();
const secret = 'webhook secret from your project settings';

// The body must stay raw, so express.raw instead of express.json
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
    const timestamp = req.get('X-Timestamp') ?? '';
    const signature = req.get('X-Signature') ?? '';
    const body = req.body.toString('utf8');

    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
        return res.sendStatus(400);
    }

    const expected = 'sha256=' + createHmac('sha256', secret)
        .update(`${timestamp}.${body}`, 'utf8')
        .digest('hex');

    const a = Buffer.from(expected);
    const b = Buffer.from(signature);

    // Check the length first: timingSafeEqual needs buffers of equal size
    if (a.length !== b.length || !timingSafeEqual(a, b)) {
        return res.sendStatus(403);
    }

    const data = JSON.parse(body);

    // Handle the order

    res.sendStatus(200);
});
```

**Python**

```python showLineNumbers
import hmac
import hashlib
import time

from flask import Flask, request

app = Flask(__name__)
secret = 'webhook secret from your project settings'

@app.post('/webhook')
def webhook():
    timestamp = request.headers.get('X-Timestamp', '')
    signature = request.headers.get('X-Signature', '')
    body = request.get_data(as_text=True)

    if abs(time.time() - int(timestamp or 0)) > 300:
        return '', 400

    expected = 'sha256=' + hmac.new(
        secret.encode(),
        f'{timestamp}.{body}'.encode(),
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(expected, signature):
        return '', 403

    data = request.get_json()

    # Handle the order

    return '', 200
```

## ध्यान रखने की बातें

**हस्ताक्षर raw बॉडी पर होता है।** अगर आप JSON पार्स करके उसे दोबारा बनाते हैं, तो कुंजियों का क्रम और स्पेस बदल जाते हैं — हस्ताक्षर मेल नहीं खाएगा। HMAC की गणना पार्स करने से पहले करें।

**हर पुनः प्रयास का अपना हस्ताक्षर है।** हर पुनः प्रयास पर भेजने का समय नया होता है, इसलिए हस्ताक्षर भी अलग होता है। भविष्य के अनुरोधों से तुलना के लिए हस्ताक्षर सेव न करें।

**गुप्त कुंजी दोबारा जारी की जा सकती है।** दोबारा जारी होते ही पुरानी कुंजी काम करना बंद कर देती है, इसलिए अपने स्टोर में मान उसी समय बदलें।

**हस्ताक्षर की जाँच idempotency की जगह नहीं लेती।** हस्ताक्षर साबित करता है कि अनुरोध असली है, न कि यह कि आप उसे पहली बार देख रहे हैं। पुनः प्रयास मान्य हस्ताक्षर के साथ आता है — `invoice.id` से जाँचें।
