ThunderPhone 2.0 متاح الآن.خدمة ذاتية، ابتداءً من 2 سنت/دقيقة.اقرأ الإعلان

Operations

التحقق من توقيعات webhook

يتم توقيع كل طلب webhook وأداة يرسله ThunderPhone. تحقّق من التوقيع مرة واحدة باستخدام الوصفة هنا، ثم أعد استخدام التحقق نفسه على كل نقطة نهاية تشغّلها.

يحمل كل طلب نرسله إلى خادمك — عمليات تسليم webhook واستدعاءات نقاط نهاية الأدوات — توقيع HMAC-SHA256 في الترويسة X-ThunderPhone-Signature. اضبط التحقق مرة واحدة، ثم أضف المساعد نفسه إلى كل معالج.

الخوارزمية

  1. اقرأ متن الطلب الخام — البايتات الدقيقة التي أرسلناها إليك عبر POST.
  2. احسب hmac_sha256(secret, body).hexdigest().
  3. قارن في وقت ثابت مع X-ThunderPhone-Signature. (تُسرّب مقارنة السلاسل النصية البسيطة معلومات التوقيت.)

نوقّع البايتات التي نرسلها بالضبط، لذا ينجح التحقق من المتن الخام دائمًا. وهذه البايتات هي أيضًا تسلسل JSON الأساسي للحمولة — مفاتيح مرتبة أبجديًا، وفواصل مضغوطة (, و : بلا مسافات)، وترميز UTF-8. يمنحك ذلك طريقة ثانية مكافئة تمامًا عندما لا يكشف إطار العمل لديك إلا JSON المحلّل: أعِد تسلسله بصيغة أساسية وطبّق HMAC عليه.

# Equivalent to hashing the raw body:
import json
canonical = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")

فضّل المتن الخام — فهو خطوة أقل ومحمي من مشكلات إعادة تحويل أرقام JSON في بعض اللغات.

أي سر؟

المصدرالسر
نقطة نهاية Webhook (/v1/developer/webhook-endpoints)secret لكل نقطة نهاية (48 حرفًا سداسيًا عشريًا) يُعاد مرة واحدة عند الإنشاء
Webhook القديم بعنوان URL واحدsecret لكل مؤسسة يُعاد عند GET /v1/webhook
استدعاء نقطة نهاية أداة (استدعاء مباشر إلى endpoint.url لديك)سر webhook على مستوى المؤسسة (وهو نفسه المستخدم في webhook القديم بعنوان URL واحد) — وليس سرًا لكل نقطة نهاية

خزّن السر في مدير الأسرار أو متغير البيئة لديك — ولا تلتزم به في المستودع مطلقًا.

تطبيقات مرجعية

تتحقق التطبيقات الأربعة جميعها من متن الطلب الخام:

Python
import hashlib
import hmac
 
 
def verify(body: bytes, signature: str, secret: str) -> bool:
    """Constant-time HMAC-SHA256 verification."""
    expected = hmac.new(
        secret.encode("utf-8"),
        body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature or "")
Node.js
import crypto from "node:crypto";
 
export function verify(body, signature, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(body)
    .digest("hex");
  if (!signature || expected.length !== signature.length) return false;
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature),
  );
}
Go
package webhook
 
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
)
 
func Verify(body []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(signature))
}
Ruby
require "openssl"
 
def verify(body, signature, secret)
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
  Rack::Utils.secure_compare(expected, signature.to_s)
end

التهيئة الخاصة بالأطر

FastAPI
from fastapi import FastAPI, HTTPException, Request
 
app = FastAPI()
 
@app.post("/thunderphone-webhook")
async def hook(request: Request):
    body = await request.body()           # raw bytes, NOT request.json()
    sig = request.headers.get("X-ThunderPhone-Signature", "")
    if not verify(body, sig, SECRET):
        raise HTTPException(status_code=401)
 
    import json
    event = json.loads(body)
    # … dispatch on event["type"] …
    return {"ok": True}
Express
import express from "express";
 
const app = express();
 
app.post(
  "/thunderphone-webhook",
  // IMPORTANT: parse as raw; do NOT use express.json() here.
  express.raw({ type: "application/json" }),
  (req, res) => {
    const sig = req.header("X-ThunderPhone-Signature") || "";
    if (!verify(req.body, sig, process.env.WEBHOOK_SECRET)) {
      return res.sendStatus(401);
    }
    const event = JSON.parse(req.body.toString("utf8"));
    // … dispatch on event.type …
    res.sendStatus(204);
  },
);
Django
import json
 
from django.http import JsonResponse, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
 
 
@csrf_exempt
@require_POST
def hook(request):
    body = request.body  # raw bytes
    sig = request.headers.get("X-ThunderPhone-Signature", "")
    if not verify(body, sig, SECRET):
        return HttpResponseForbidden("invalid signature")
    event = json.loads(body)
    # … dispatch on event["type"] …
    return JsonResponse({"ok": True})

التحقق من استدعاءات الأدوات

عندما يستدعي الوكيل إحدى أدوات الدوال مباشرةً (تحتوي الأداة على endpoint)، يحمل الطلب رأسي ThunderPhone التاليين إلى جانب endpoint.headers التي أعددتها:

  • X-ThunderPhone-Call-ID — المعرّف الرقمي للمكالمة المباشرة.
  • X-ThunderPhone-Signature — HMAC-SHA256، باستخدام سر خطاف الويب على مستوى المؤسسة كمفتاح، على بايتات نص الطلب الدقيقة.

تعمل دالة المساعدة verify() نفسها دون تغيير، مع نقطتين يجب مراعاتهما:

  1. أدوات GET / DELETE لا تحتوي على نص طلب. تنتقل الوسائط كمعلمات استعلام، ويُحسب التوقيع على سلسلة البايتات الفارغة — لذا استخدم verify(b"", sig, secret) ‏(Python) أو verify(Buffer.alloc(0), sig, secret) ‏(Node). لا تُجزّئ سلسلة الاستعلام.
  2. المؤسسات التي ليس لديها خطاف ويب قديم مُعدّ لا تملك سرًا للمؤسسة. في هذه الحالة، تحمل استدعاءات الأدوات X-ThunderPhone-Call-ID فقط دون رأس توقيع. أعدّ خطاف الويب القديم (PUT /v1/webhook) للحصول على سر توقيع، أو صادِق استدعاءات الأدوات باستخدام رأسك الخاص عبر endpoint.headers.
@app.post("/tools/search-appointments")
async def tool(request: Request):
    body = await request.body()  # b"" for GET/DELETE tools
    sig = request.headers.get("X-ThunderPhone-Signature", "")
    call_id = request.headers.get("X-ThunderPhone-Call-ID", "")
    if not verify(body, sig, ORG_WEBHOOK_SECRET):
        raise HTTPException(status_code=401)
    args = json.loads(body)
    ...

إرسال الأدوات في وضع خطاف الويب (الأدوات التي لا تحتوي على endpoint، والتي تصل إلى خطاف ويب مؤسستك بصيغة telephony.tool / web.tool) هو خطاف ويب موقّع عادي — تنطبق الوصفة القياسية أعلاه. راجع أدوات الدوال لكلا شكلي الطلب.

الأخطاء الشائعة

إعادة التسلسل بالتنسيق الافتراضي

يؤدي تحليل النص وإعادة تفريغه باستخدام الإعدادات الافتراضية لمكتبة JSON لديك (مسافات بعد , / :، ومفاتيح مرتبة حسب الإدراج) إلى إنتاج بايتات مختلفة وتعطيل HMAC. تحقّق من النص الخام — أو إذا اضطررت إلى إعادة التسلسل، فطابق صيغتنا القياسية تمامًا: مفاتيح مرتبة، وفواصل مضغوطة، وترميز UTF-8.

إطار العمل يحلل JSON تلقائيًا

تستهلك برمجية Express الوسيطة express.json() تدفق النص وتفقد البايتات الخام. استخدم express.raw() تحديدًا في مسار webhook، أو خزّن النص الخام مؤقتًا في برمجية وسيطة تسبقها. وينطبق الأمر نفسه على NestJS / Koa — راجع وثائقهما حول "النص الخام".

مقارنة غير آمنة زمنيًا

تُعد expected === signature في JS أو expected == signature في Python مقارنات متغيرة التوقيت. استخدم crypto.timingSafeEqual أو hmac.compare_digest على التوالي. فرق الأداء معدوم.

سر خاطئ لنقاط نهاية الأدوات

تُوقَّع استدعاءات نقاط نهاية الأدوات المباشرة باستخدام سر webhook على مستوى المؤسسة (GET /v1/webhook) — وليس باستخدام أي سر لكل نقطة نهاية من /v1/developer/webhook-endpoints. أعد استخدام الدالة verify() نفسها، ولكن تأكد من تزويدها بسر المؤسسة في مسارات الأدوات.

تجزئة سلسلة الاستعلام في أدوات GET/DELETE

بالنسبة إلى أساليب الأدوات التي لا تحتوي على نص، يغطي التوقيع سلسلة البايتات الفارغة، مما يحافظ على وصفة عامة واحدة: طبّق HMAC على نص الطلب الخام، مهما كان. لن تتطابق تجزئة عنوان URL أو سلسلة الاستعلام أبدًا.

عدم إرجاع 401 عند عدم التطابق

إن إرجاع 200 عند فشل التحقق يجعل المعالج هدفًا لإعادة التشغيل. استجب دائمًا برمز غير 2xx إذا فشل التحقق.


الخطوات التالية