# التحقق من التوقيعات (/ar/integrate/webhooks/verifying-signatures)

اتأكد إن كل webhook جاي من XPay فعلًا قبل ما تثق فيه. HMAC-SHA256 على المحتوى الخام، بنافذة إعادة إرسال 5 دقايق.

أي حد معاه رابط نقطتك يقدر يعمل POST عليها. سر التوقيع هو اللي بيقولّك أنهي طلبات POST هي XPay فعلًا. كل webhook XPay بيسلّمه بيحمل ترويسة `XPay-Signature`. المعالِج بتاعك بيعيد حساب التوقيع باستخدام سر `whsec_*` بتاع النقطة، وبيرفض أي طلب القيمة فيه ما تطابقش.

مفيش مكتبة من XPay لده حاليًا. التحقق صغير لدرجة إنه ممكن يعيش في المعالِج بتاعك في حوالي 20 سطر.

## الترويسة [#الترويسة]

XPay بيبعت ترويسة توقيع واحدة على كل تسليم:

```
XPay-Signature: t=1730000000,v1=a1b2c3d4...
```

| الحقل | المعنى                                                                              |
| ----- | ----------------------------------------------------------------------------------- |
| `t`   | طابع وقت Unix (بالثواني) في اللحظة اللي XPay حسب فيها التوقيع.                      |
| `v1`  | تشفير HMAC-SHA256 بترميز hex للطابع الزمني، و`.`، والمحتوى الخام، موقّع بسر النقطة. |

الحقلين الاتنين مطلوبين. الترويسة المشوّهة، أو الطلب اللي مفيهوش ترويسة خالص، المفروض يترفض على طول.

## إزاي XPay بيوقّع [#إزاي-xpay-بيوقّع]

XPay بيحسب التوقيع كالآتي:

```
signedPayload = `${timestamp}.${rawRequestBody}`
signature     = HMAC-SHA256(endpointSecret, signedPayload)
```

حيث:

* `timestamp` هي نفس القيمة المبعوتة في حقل `t`.
* `rawRequestBody` هي **بايتات** محتوى الـ JSON اللي XPay بعته. مش شكل موحّد. إعادة تسلسل الـ JSON المحلَّل من جهتك مش هتنتج نفس البايتات.

اعمل ترميز hex للملخّص وقارنه، في وقت ثابت، مع قيمة `v1`.

## إزاي تتحقق [#إزاي-تتحقق]

التحقق خمس خطوات صغيرة:

1. اقرا **المحتوى الخام** للطلب كسلسلة نصية أو `Buffer`. ما تحلّلهوش كـ JSON لسه.
2. اقرا ترويسة `XPay-Signature` وقسّمها على `,` لـ `t={...}` و`v1={...}`.
3. ارفض لو `abs(now - t) > 300` ثانية. دي نافذة إعادة الإرسال.
4. احسب `HMAC-SHA256(secret, "${t}.${rawBody}")` واعمل ترميز hex للملخّص.
5. قارن الملخّص في وقت ثابت مع قيمة `v1`.

بس بعد ما الخمسة كلهم ينجحوا تعمل `JSON.parse(rawBody)` وتتصرّف على الحدث.

<Tabs items="[&#x22;Node.js&#x22;, &#x22;Python&#x22;]">
  <Tab value="Node.js">
    ```typescript
    import crypto from "node:crypto";

    const TOLERANCE_SECONDS = 300;

    export function verifyXPaySignature(
      rawBody: string,
      header: string | undefined,
      secret: string,
    ): { valid: boolean; event?: unknown } {
      if (!header) return { valid: false };

      const parts = Object.fromEntries(
        header.split(",").map((p) => {
          const [k, ...rest] = p.split("=");
          return [k, rest.join("=")];
        }),
      );

      const timestamp = Number.parseInt(parts.t ?? "", 10);
      const received = parts.v1;
      if (!Number.isFinite(timestamp) || !received) return { valid: false };

      // Replay protection
      if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > TOLERANCE_SECONDS) {
        return { valid: false };
      }

      const computed = crypto
        .createHmac("sha256", secret)
        .update(`${timestamp}.${rawBody}`)
        .digest("hex");

      const a = Buffer.from(computed);
      const b = Buffer.from(received);
      if (a.length !== b.length) return { valid: false };
      if (!crypto.timingSafeEqual(a, b)) return { valid: false };

      return { valid: true, event: JSON.parse(rawBody) };
    }
    ```
  </Tab>

  <Tab value="Python">
    ```python
    import hmac, hashlib, time, json

    TOLERANCE_SECONDS = 300

    def verify_xpay_signature(raw_body: str, header: str, secret: str):
        if not header:
            return None

        parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
        try:
            timestamp = int(parts["t"])
        except (KeyError, ValueError):
            return None

        received = parts.get("v1", "")
        if not received:
            return None

        # Replay protection
        if abs(time.time() - timestamp) > TOLERANCE_SECONDS:
            return None

        computed = hmac.new(
            secret.encode(),
            f"{timestamp}.{raw_body}".encode(),
            hashlib.sha256,
        ).hexdigest()

        if not hmac.compare_digest(computed, received):
            return None

        return json.loads(raw_body)
    ```
  </Tab>
</Tabs>

الدالة بترجّع الحدث المحلَّل لما التحقق ينجح، وإشارة "غير صالح" غير كده. دايمًا حلّل الـ JSON جوّه المُتحقق (بعد فحص التوقيع)، مش قبله.

## توصيل المحتوى الخام في إطار العمل بتاعك [#توصيل-المحتوى-الخام-في-إطار-العمل-بتاعك]

أكتر سبب شائع لفشل التحقق هو إن إطار العمل حلّل المحتوى لـ JSON وأعاد تسلسله قبل ما المعالِج بتاعك يشتغل. المحتوى المعاد تسلسله بيبقى ليه مسافات وترتيب مفاتيح مختلفين عن البايتات اللي XPay وقّعها، فالـ HMAC مش هيطابق.

شوية وصفات حسب إطار العمل:

<Tabs items="[&#x22;Express&#x22;, &#x22;Fastify&#x22;, &#x22;Next.js (App Router)&#x22;]">
  <Tab value="Express">
    ```typescript
    import express from "express";

    const app = express();

    // IMPORTANT: use express.raw on this exact route, before any global JSON parser.
    app.post("/webhooks/xpay", express.raw({ type: "application/json" }), (req, res) => {
      const rawBody = (req.body as Buffer).toString("utf8");
      const result = verifyXPaySignature(
        rawBody,
        req.header("XPay-Signature"),
        process.env.XPAY_WEBHOOK_SECRET!,
      );
      if (!result.valid) return res.status(400).send("invalid signature");

      handleEvent(result.event);
      res.status(200).send();
    });
    ```
  </Tab>

  <Tab value="Fastify">
    ```typescript
    import Fastify from "fastify";

    const fastify = Fastify();

    // Register a content-type parser that hands you the raw bytes.
    fastify.addContentTypeParser("application/json", { parseAs: "string" }, (_req, body, done) =>
      done(null, body),
    );

    fastify.post("/webhooks/xpay", async (req, reply) => {
      const rawBody = req.body as string;
      const result = verifyXPaySignature(
        rawBody,
        req.headers["xpay-signature"] as string,
        process.env.XPAY_WEBHOOK_SECRET!,
      );
      if (!result.valid) return reply.code(400).send("invalid signature");

      handleEvent(result.event);
      return reply.code(200).send();
    });
    ```
  </Tab>

  <Tab value="Next.js (App Router)">
    ```typescript
    // app/api/webhooks/xpay/route.ts
    import { NextRequest, NextResponse } from "next/server";

    export async function POST(req: NextRequest) {
      const rawBody = await req.text(); // read once, before any .json()
      const result = verifyXPaySignature(
        rawBody,
        req.headers.get("XPay-Signature") ?? undefined,
        process.env.XPAY_WEBHOOK_SECRET!,
      );
      if (!result.valid) {
        return new NextResponse("invalid signature", { status: 400 });
      }

      handleEvent(result.event);
      return new NextResponse(null, { status: 200 });
    }
    ```
  </Tab>
</Tabs>

## تعمل إيه لما التحقق يفشل [#تعمل-إيه-لما-التحقق-يفشل]

عدم تطابق التوقيع مش دايمًا هجوم. نفس مسار الكود بيمسك كتير من حالات الإعداد الغلط العادية.

| السبب                                | العَرَض                                                                                               |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------- |
| إطار العمل حلّل المحتوى وأعاد تسلسله | التحقق بيفشل على كل طلب. أكتر سبب شائع.                                                               |
| سر نقطة غلط                          | التحقق بيفشل على كل طلب. اختلاط بين سر الاختبار والحساب الفعلي، أو بتستخدم قيمة قديمة من نقطة محذوفة. |
| النقطة اتحذفت واتعملت تاني           | النقطة الجديدة ليها `whsec_*` جديد. حدّث متغيّر البيئة بتاعك.                                         |
| انحراف ساعة السيرفر بتاعك            | رفض بسبب نافذة إعادة الإرسال. زامن ساعة السيرفر عن طريق NTP.                                          |
| الترويسة اتعدّلت                     | `t` أو `v1` ناقص. بروكسي عكسي أو ingress عدّل الترويسة. سيب `XPay-Signature` يعدّي من غير تعديل.      |

لما التحقق يفشل، ارجع رد غير 2xx (`400` هو الاختيار الصح). تدفق التسليم بتاع XPay بيعتبر ده محاولة فاشلة وبيعيد المحاولة على الجدول العادي. شوف [إعادة الإرسال والمحاولات](/integrate/webhooks/replaying-and-retries).

المعالِج اللي بيرجع 200 على توقيع غلط بيبقى معطوب بصمت: عمرك ما هتلاحظ إن التوقيع اتغير، والمهاجم اللي يلاقي رابطك يقدر يزوّر أحداث.

## عدم التكرار [#عدم-التكرار]

نفس الحدث ممكن يتسلّم أكتر من مرة. XPay بيعيد المحاولة على كل رد غير 2xx وعلى كل مهلة منتهية، وممكن كمان تشوف نسخ مكررة لو رد ناجح ما وصلش لـ XPay. لازم المعالِج بتاعك يكون بلا تكرار (idempotent). دي ناحية الـ webhook من عدم التكرار؛ علشان الـ `Idempotency-Key` اللي بتبعته على عمليات الكتابة، شوف [عدم التكرار](/integrate/idempotency).

استخدم `event.id` على المستوى الأعلى كمفتاح إزالة التكرار:

```typescript
async function handleEvent(event: { id: string; type: string; data: unknown }) {
  // Skip if we've seen this id before
  const inserted = await db.processedEvents.insertIfNew(event.id);
  if (!inserted) return;

  switch (event.type) {
    case "checkout.session.completed":
      await fulfillOrder(event.data);
      break;
    // other cases
  }
}
```

الـ `event.id` فريد لكل حدث وثابت عبر كل محاولات الإعادة.

## أقرّ بسرعة، واشتغل في الخلفية [#أقرّ-بسرعة-واشتغل-في-الخلفية]

XPay بيعتبر التسليم ناجح بس لو المعالِج بتاعك رجّع 2xx خلال 30 ثانية. أي حاجة أبطأ بتطلق محاولة إعادة، حتى لو شغلك خلص في الآخر.

الشكل الصح هو إنك تتحقق، تزيل التكرار، تحط الحدث في طابور، وترجع 200 بأسرع ما يمكن. شغّل تنفيذ الطلبات، وإرسال الإيميلات، واستدعاءات الـ API اللاحقة، وكتابات قاعدة البيانات من عامل الطابور، مش من جوّه معالِج الطلب.

```typescript
const result = verifyXPaySignature(rawBody, header, secret);
if (!result.valid) return res.status(400).send();

await queue.add("xpay-event", result.event);
return res.status(200).send();
```

## رايح فين بعد كده [#رايح-فين-بعد-كده]

<Cards>
  <Card icon="<Cable />" title="إعداد نقطة نهاية" href="/integrate/webhooks/setting-up-an-endpoint">
    أنشئ النقطة وامسك سر `whsec_*` اللي هتتحقق بيه.
  </Card>

  <Card icon="<RefreshCw />" title="إعادة الإرسال والمحاولات" href="/integrate/webhooks/replaying-and-retries">
    جدول إعادة المحاولة لما المعالِج بتاعك يرجع غير 2xx، وإزاي تعيد إرسال حدث يدويًا من
    لوحة التحكم.
  </Card>

  <Card icon="<Terminal />" title="التطوير المحلي" href="/integrate/webhooks/local-development">
    حوّل تسليمات الـ webhook لجهازك وأنت بتبني المعالِج.
  </Card>

  <Card icon="<Boxes />" title="مرجع الأحداث" href="/integrate/webhooks/event-reference">
    كل حدث تقدر تشترك فيه، إمتى بيتطلق، والكائن اللي بيحمله.
  </Card>

  <Card icon="<Network />" title="نموذج الكائنات" href="/integrate/object-model">
    إيه اللي بيحتويه `data.object` بتاع كل حدث، والمعرّفات اللي تحتفظ بيها في سجل الطلب بتاعك.
  </Card>
</Cards>