ThunderPhone 2.0 اب لائیو ہے۔سیلف سرو، قیمت 2¢ فی منٹ سے شروع۔اعلان پڑھیں

Operations

ویب ہک دستخطوں کی تصدیق کریں

ThunderPhone کی بھیجی گئی ہر ویب ہک اور ٹول درخواست پر ٹائم اسٹیمپ کے ساتھ دستخط ہوتے ہیں۔ یہاں موجود recipe یا SDK helper کے ذریعے اس کی تصدیق کریں اور پانچ منٹ سے زیادہ پرانی دوبارہ بھیجی گئی درخواستوں کو مسترد کریں۔

آپ کے سرور کو بھیجی جانے والی ہر درخواست (webhook ڈیلیوریز، پرانا single-URL webhook، اور tool-endpoint کالز) میں دو دستخط ہیڈرز شامل ہوتے ہیں۔ دونوں ایک ہی راز کے ساتھ HMAC-SHA256 ہیں۔

ہیڈرکن چیزوں پر دستخط کرتا ہےری پلے سے تحفظ
X-ThunderPhone-Signature-V2 (تجویز کردہ)ٹائم اسٹیمپ، طریقہ، پاتھ، query اور باڈی5 منٹ سے زیادہ پرانی درخواستیں مسترد کریں
X-ThunderPhone-Signatureصرف باڈیکوئی نہیں: حاصل کی گئی درخواست ہمیشہ تصدیق ہو جاتی ہے

نئی انٹیگریشنز میں X-ThunderPhone-Signature-V2 کی تصدیق کریں اور موجودہ انٹیگریشنز کو اس پر منتقل کریں۔ X-ThunderPhone-Signature اب بھی ہر درخواست کے ساتھ، بغیر تبدیلی کے، بھیجا جاتا ہے، اس لیے اس پر بنے ہوئے ویریفائرز کام کرتے رہیں گے۔ ہم اسے بھیجنا بند کرنے سے پہلے ایک تاریخ کا اعلان کریں گے۔

دستخط V2

X-ThunderPhone-Signature-V2: t=1767225600,v1=03a89bec521f78d55727237ca4713bb0b8dcdf2e09e450865b5b5637b80c1c4e
  • t وہ Unix وقت ہے، سیکنڈز میں، جب ہم نے درخواست بھیجی۔ دوبارہ کوشش کی گئی ترسیل بھیجے جانے پر دوبارہ دستخط ہوتی ہے، اپنے الگ t کے ساتھ۔
  • v1 نیچے دی گئی دستخط شدہ اسٹرنگ کا hex HMAC-SHA256 ہے، جس کے لیے آپ کا secret استعمال ہوتا ہے۔

قدر، کوما سے جدا شدہ key=value جوڑوں کی فہرست ہے۔ جن keys کو آپ نہ پہچانیں انہیں نظر انداز کریں، اور اگر کوئی بھی v1 مماثل ہو تو درخواست قبول کریں: ہم آئندہ کسی تبدیلی کے دوران دوسرا دستخط شامل کر سکتے ہیں۔

دستخط شدہ اسٹرنگ

چار فیلڈز، ہر ایک کے بعد ایک newline (\n, بائٹ 0x0A) سوائے آخری کے:

<t>\n<METHOD>\n<path and query>\n<body>
فیلڈقدر
theader سے t کی قدر
METHODبڑے حروف میں HTTP method: POST، GET، PUT، PATCH یا DELETE
راستہ اور کوئریHTTP درخواست لائن سے درخواست کا ہدف: پہلے راستہ، پھر ? اور کوئری اسٹرنگ اگر موجود ہو۔ کوئی scheme، host یا fragment نہیں۔ اسے بالکل ویسا ہی استعمال کریں جیسا موصول ہوا، اسے decode کیے یا parameters کی ترتیب بدلے بغیر
bodyخام درخواست body۔ GET اور DELETE tool calls کی کوئی body نہیں ہوتی، اس لیے ان کی دستخط شدہ اسٹرنگ راستے کے بعد newline پر ختم ہوتی ہے

تصدیق کرنے کے لیے:

  1. کسی بھی parsing سے پہلے، header، method، خام راستہ اور کوئری، اور خام body پڑھیں۔
  2. header سے t اور ہر v1 لیں۔
  3. اگر t آپ کی گھڑی سے پہلے یا بعد میں 300 سیکنڈ سے زیادہ ہو تو درخواست مسترد کریں۔
  4. اپنے secret کے ساتھ دستخط شدہ اسٹرنگ کا hex HMAC-SHA256 حساب کریں۔
  5. اسے مستقل وقت میں v1 سے موازنہ کریں۔ اگر کچھ بھی مماثل نہ ہو تو 401 جواب دیں۔

عملی مثال

Secret whsec_example_4c1f2a9b۔ ہم یہ درخواست t = 1767225600 (2026-01-01 00:00:00 UTC) پر بھیجتے ہیں:

POST /tools/lookup?region=us HTTP/1.1
Host: api.example.com
Content-Type: application/json
X-ThunderPhone-Signature-V2: t=1767225600,v1=03a89bec521f78d55727237ca4713bb0b8dcdf2e09e450865b5b5637b80c1c4e
 
{"customer_id":"cust_123"}

دستخط شدہ اسٹرنگ یہ چار لائنیں ہیں، body کے بعد کوئی newline نہیں:

1767225600
POST
/tools/lookup?region=us
{"customer_id":"cust_123"}

اس کا HMAC-SHA256 03a89bec521f78d55727237ca4713bb0b8dcdf2e09e450865b5b5637b80c1c4e ہے۔ اس کے خلاف اپنے implementation کو چیک کریں، یا اسے openssl کے ساتھ دوبارہ بنائیں:

printf '1767225600\nPOST\n/tools/lookup?region=us\n{"customer_id":"cust_123"}' \
  | openssl dgst -sha256 -hmac 'whsec_example_4c1f2a9b'

اسی سیکنڈ میں /tools/lookup?customer_id=cust_123 پر ایک GET tool call کی body خالی ہوتی ہے۔ اس کی دستخط شدہ اسٹرنگ 1767225600\nGET\n/tools/lookup?customer_id=cust_123\n ہے اور اس کا دستخط 3ce214cb0b8f2bf39b1d19d4e7265df9dbaff5a5f838d3cc6c753a8f7a4ad280 ہے۔

SDKs کے ساتھ

Python اور TypeScript SDKs میں ایک helper شامل ہے (thunderphone 0.2.0 یا بعد کا ورژن، @thunderphone/sdk 0.2.0 یا بعد کا ورژن)۔ یہ 5 منٹ کی مدت چیک کرتا ہے، مستقل وقت میں موازنہ کرتا ہے، اور secrets کی فہرست قبول کرتا ہے تاکہ آپ downtime کے بغیر rotate کر سکیں۔

Python
from thunderphone import verify_signature
 
ok = verify_signature(
    header,            # the X-ThunderPhone-Signature-V2 value
    secret=SECRET,     # or [NEW_SECRET, OLD_SECRET] while rotating
    method=method,     # "POST"
    path=path,         # "/tools/lookup?region=us", exactly as received
    body=raw_body,     # bytes, before any JSON parsing
    tolerance=300,     # optional; seconds
)
Node.js
import { verifySignature } from "@thunderphone/sdk";
 
const ok = verifySignature({
  header,               // the X-ThunderPhone-Signature-V2 value
  secret: SECRET,       // or [NEW_SECRET, OLD_SECRET] while rotating
  method,               // "POST"
  path,                 // "/tools/lookup?region=us", exactly as received
  body: rawBody,        // Buffer or string, before any JSON parsing
  toleranceSeconds: 300, // optional
});

SDK کے بغیر

Python
import hashlib
import hmac
import time
 
TOLERANCE_SECONDS = 300
 
 
def verify_v2(header: str, secret: str, method: str, path: str, body: bytes) -> bool:
    """path is the path plus ?query exactly as received; body is the raw body."""
    timestamp, signatures = None, []
    for item in (header or "").split(","):
        key, _, value = item.strip().partition("=")
        if key == "t" and value.isdigit() and len(value) <= 15:
            timestamp = int(value)
        elif key == "v1":
            signatures.append(value)
    if timestamp is None or abs(time.time() - timestamp) > TOLERANCE_SECONDS:
        return False
    signed = f"{timestamp}\n{method.upper()}\n{path}\n".encode() + body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected.encode(), s.encode()) for s in signatures)
Node.js
import crypto from "node:crypto";
 
const TOLERANCE_SECONDS = 300;
 
// path is the path plus ?query exactly as received; body is the raw Buffer.
export function verifyV2(header, secret, method, path, body) {
  let timestamp;
  const signatures = [];
  for (const item of (header || "").split(",")) {
    const [key, value = ""] = item.trim().split("=", 2);
    if (key === "t" && /^\d+$/.test(value)) timestamp = Number(value);
    else if (key === "v1") signatures.push(value);
  }
  if (timestamp === undefined) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;
  const expected = Buffer.from(
    crypto
      .createHmac("sha256", secret)
      .update(`${timestamp}\n${method.toUpperCase()}\n${path}\n`)
      .update(body)
      .digest("hex"),
  );
  return signatures.some((sig) => {
    const candidate = Buffer.from(sig);
    return (
      candidate.length === expected.length &&
      crypto.timingSafeEqual(candidate, expected)
    );
  });
}

فریم ورک وائرنگ

ہر مثال خام path اور query، اور خام body پڑھتی ہے، پھر SDK helper کو کال کرتی ہے۔

FastAPI
import json
 
from fastapi import FastAPI, HTTPException, Request
from thunderphone import verify_signature
 
app = FastAPI()
 
 
def raw_path_and_query(request: Request) -> str:
    path = (request.scope.get("raw_path") or request.url.path.encode()).decode("latin-1")
    query = request.scope.get("query_string", b"").decode("latin-1")
    return f"{path}?{query}" if query else path
 
 
@app.post("/thunderphone-webhook")
async def hook(request: Request):
    body = await request.body()  # raw bytes, NOT request.json()
    if not verify_signature(
        request.headers.get("X-ThunderPhone-Signature-V2"),
        secret=SECRET,
        method=request.method,
        path=raw_path_and_query(request),
        body=body,
    ):
        raise HTTPException(status_code=401)
    event = json.loads(body)
    # … dispatch on event["type"] …
    return {"ok": True}
Express
import express from "express";
import { verifySignature } from "@thunderphone/sdk";
 
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 ok = verifySignature({
      header: req.header("X-ThunderPhone-Signature-V2"),
      secret: process.env.WEBHOOK_SECRET,
      method: req.method,
      path: req.originalUrl, // path and query as received
      body: req.body,
    });
    if (!ok) 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 HttpResponseForbidden, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from thunderphone import verify_signature
 
 
@csrf_exempt
@require_POST
def hook(request):
    if not verify_signature(
        request.headers.get("X-ThunderPhone-Signature-V2"),
        secret=SECRET,
        method=request.method,
        path=request.get_full_path(),
        body=request.body,  # raw bytes
    ):
        return HttpResponseForbidden("invalid signature")
    event = json.loads(request.body)
    # … dispatch on event["type"] …
    return JsonResponse({"ok": True})

req.originalUrl (Express) اور ASGI کے raw_path اور query_string (FastAPI) بالکل وہی target ہیں جو موصول ہوا تھا۔ Django کا get_full_path() اور Flask کا request.path پہلے path کو decode کرتے ہیں، جس سے عام paths کے لیے ایک ہی string ملتی ہے۔ اگر آپ کے endpoint path میں percent-encoded یا reserved characters، جیسے %2F، ; یا =، شامل ہیں تو اس کے بجائے اپنے server کی فراہم کردہ خام target پڑھیں (مثلاً Gunicorn پر request.META["RAW_URI"])۔

کون سا secret؟

دونوں ہیڈرز پر ایک ہی secret سے دستخط کیے جاتے ہیں۔

ماخذSecret
ویب ہک اینڈپوائنٹ (/v1/developer/webhook-endpoints)ہر اینڈپوائنٹ کا secret (48 hex حروف) جو تخلیق کے وقت ایک بار واپس کیا جاتا ہے
پرانا سنگل-URL ویب ہکہر تنظیم کا secret جو GET /v1/webhook پر واپس کیا جاتا ہے
ٹول اینڈپوائنٹ انوووکیشن (آپ کے endpoint.url پر براہِ راست کال)تنظیمی سطح کا ویب ہک secret (وہی جو پرانے سنگل-URL ویب ہک کے لیے ہے) — ہر اینڈپوائنٹ کا secret نہیں

secret کو اپنے secret manager یا env var میں محفوظ کریں — اسے کبھی commit نہ کریں۔

ٹول کالز کی تصدیق

جب ایجنٹ آپ کے کسی فنکشن ٹول کو براہ راست استعمال کرتا ہے (ٹول میں endpoint ہوتا ہے)، تو درخواست آپ کے ترتیب شدہ endpoint.headers کے ساتھ یہ ThunderPhone ہیڈرز بھی رکھتی ہے:

  • X-ThunderPhone-Call-ID — جاری کال کی عددی شناخت۔
  • X-ThunderPhone-Signature-V2 — اوپر دیا گیا ٹائم اسٹیمپ والا دستخط، جو آپ کی تنظیم کی سطح کی ویب ہک خفیہ کلید کے ذریعے بنایا جاتا ہے۔
  • X-ThunderPhone-Signature — سابقہ صرف باڈی والا دستخط، اسی کلید کے ساتھ۔

ویب ہکس سے دو باتیں مختلف ہیں:

  1. GET / DELETE ٹولز کی کوئی باڈی نہیں ہوتی۔ آرگیومنٹس کوئری پیرامیٹرز کے طور پر منتقل ہوتے ہیں۔ V2 دستخط انہیں پاتھ اور کوئری کے ذریعے شامل کرتا ہے، اور باڈی فیلڈ خالی ہوتی ہے۔ خام کوئری اسٹرنگ کے ساتھ تصدیق کریں: پیرامیٹرز کو پارس کرکے نئی کوئری اسٹرنگ بنانے سے ان کی ترتیب یا انکوڈنگ بدل سکتی ہے۔ سابقہ ہیڈر خالی بائٹ اسٹرنگ پر دستخط کرتا ہے۔
  2. جن تنظیموں میں سابقہ ویب ہک ترتیب شدہ نہیں ہوتا، ان کے پاس تنظیم کی خفیہ کلید نہیں ہوتی۔ اس صورت میں ٹول کالز میں صرف X-ThunderPhone-Call-ID ہوتا ہے اور کوئی دستخطی ہیڈرز نہیں ہوتے۔ دستخط کی خفیہ کلید حاصل کرنے کے لیے سابقہ ویب ہک (PUT /v1/webhook) ترتیب دیں، یا endpoint.headers کے ذریعے اپنے ہیڈر سے ٹول کالز کی توثیق کریں۔
@app.api_route("/tools/search-appointments", methods=["GET", "POST"])
async def tool(request: Request):
    body = await request.body()  # b"" for GET/DELETE tools
    if not verify_signature(
        request.headers.get("X-ThunderPhone-Signature-V2"),
        secret=ORG_WEBHOOK_SECRET,
        method=request.method,
        path=raw_path_and_query(request),
        body=body,
    ):
        raise HTTPException(status_code=401)
    call_id = request.headers.get("X-ThunderPhone-Call-ID", "")
    args = json.loads(body) if body else dict(request.query_params)
    ...

Express میں، باڈی پارسر GET پر req.body کو خالی آبجیکٹ کے طور پر چھوڑ دیتا ہے، اس لیے Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0) منتقل کریں۔

ویب ہک-موڈ ٹول ڈسپیچ (endpoint کے بغیر ٹولز، جو آپ کے تنظیمی ویب ہک کو telephony.tool / web.tool کے طور پر پہنچائے جاتے ہیں) ایک عام دستخط شدہ ویب ہک ہے — اوپر دیا گیا معیاری طریقہ لاگو ہوتا ہے۔ درخواست کی دونوں ساختوں کے لیے فنکشن ٹولز دیکھیں۔

پرانا دستخط: X-ThunderPhone-Signature

اصل ہیڈر صرف خام باڈی کا hex HMAC-SHA256 ہے، جس میں وہی secret استعمال ہوتا ہے۔ اس میں کوئی timestamp نہیں ہوتا، اس لیے جو کوئی ایک دستخط شدہ درخواست حاصل کر لے وہ اسے دوبارہ چلا سکتا ہے۔ انٹیگریشنز کے V2 پر منتقل ہونے کے دوران یہ اب بھی ہر درخواست کے ساتھ بھیجا جاتا ہے۔

  1. خام درخواست کی باڈی پڑھیں — وہی درست bytes جو ہم نے آپ کو بھیجے ہیں۔
  2. hmac_sha256(secret, body).hexdigest() کا حساب لگائیں۔
  3. X-ThunderPhone-Signature کے مقابلے میں constant time میں موازنہ کریں۔

ہم بالکل انہی bytes پر دستخط کرتے ہیں جو ہم منتقل کرتے ہیں۔ یہ bytes payload کی canonical JSON serialization بھی ہوتی ہیں — keys حروفِ تہجی کے مطابق ترتیب میں، مختصر separators (بغیر خالی جگہ کے , اور :)، UTF-8۔ اگر آپ کا framework صرف parsed JSON فراہم کرتا ہے، تو اسے canonical انداز میں دوبارہ serialize کریں اور اس پر HMAC لگائیں:

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

خام باڈی کو ترجیح دیں — اس میں ایک مرحلہ کم ہوتا ہے اور بعض زبانوں میں JSON نمبروں کے round-tripping کے مسائل سے محفوظ رہتا ہے۔ یہی بات V2 کے body field پر بھی لاگو ہوتی ہے۔

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

عام خامیاں

گھڑی کے وقت کا فرق

5 منٹ کی ونڈو آپ کے سرور کی گھڑی کے مطابق ناپی جاتی ہے۔ اسے NTP کے ساتھ ہم وقت رکھیں؛ 5 منٹ سے زیادہ غلط وقت والی گھڑی ہر درخواست کو مسترد کر دیتی ہے۔

ایک پراکسی راستہ دوبارہ لکھتی ہے

دستخط شدہ راستہ وہی ہے جس کی ہم نے درخواست کی تھی: اس URL کا راستہ جسے آپ نے ThunderPhone میں ترتیب دیا ہے۔ اگر ریورس پراکسی یا API گیٹ وے آپ کی ایپلیکیشن کے درخواست دیکھنے سے پہلے کوئی پری فکس ہٹا یا شامل کر دے، تو تصدیق سے پہلے ہمارے بھیجے ہوئے راستے کو دوبارہ بنائیں (مثلاً، پری فکس واپس شامل کریں)۔

بنیادی فارمیٹنگ کے ساتھ دوبارہ سیریلائز کرنا

باڈی کو پارس کر کے اپنی JSON لائبریری کی بنیادی ترتیبات کے ساتھ دوبارہ ڈمپ کرنے سے ( , / : کے بعد اسپیسز، شامل کرنے کی ترتیب والی کیز) مختلف بائٹس بنتے ہیں اور HMAC خراب ہو جاتا ہے۔ خام باڈی کی تصدیق کریں — یا اگر آپ کو دوبارہ سیریلائز کرنا ضروری ہو تو ہماری معیاری شکل سے بالکل مطابقت رکھیں: ترتیب شدہ کیز، مختصر سیپریٹرز، UTF-8۔

فریم ورک JSON کو خودکار طور پر پارس کرتا ہے

Express کا express.json() middleware باڈی اسٹریم استعمال کر لیتا ہے اور آپ خام بائٹس کھو دیتے ہیں۔ خاص طور پر webhook روٹ پر express.raw() استعمال کریں، یا pre-middleware میں خام باڈی کو بفر کریں۔ NestJS / Koa کے لیے بھی یہی صورتحال ہے — ان کی "raw body" دستاویزات دیکھیں۔

ٹائمنگ کے لحاظ سے غیر محفوظ موازنہ

JS میں expected === signature یا Python میں expected == signature ٹائمنگ کے لحاظ سے متغیر موازنے ہیں۔ بالترتیب crypto.timingSafeEqual یا hmac.compare_digest استعمال کریں۔ کارکردگی میں فرق نہ ہونے کے برابر ہے۔

ٹول اینڈ پوائنٹس کے لیے غلط سیکرٹ

براہ راست ٹول-اینڈ پوائنٹ کالز پر تنظیمی سطح کا ویب ہک سیکرٹ (GET /v1/webhook) استعمال کر کے دستخط کیے جاتے ہیں — نہ کہ /v1/developer/webhook-endpoints کے کسی فی-اینڈ-پوائنٹ سیکرٹ سے۔ وہی ہیلپر دوبارہ استعمال کریں، لیکن یقینی بنائیں کہ ٹول روٹس پر آپ اسے تنظیمی سیکرٹ فراہم کرتے ہیں۔

GET/DELETE ٹولز میں کوئری اسٹرنگز

V2 کوئری اسٹرنگ پر بالکل اسی طرح دستخط کرتا ہے جیسے اسے بھیجا گیا ہو۔ لیگیسی ہیڈر خالی باڈی پر دستخط کرتا ہے اور کبھی کوئری اسٹرنگ پر نہیں، اس لیے لیگیسی ہیڈر کے لیے URL کا ہیش کبھی مطابقت نہیں کرے گا۔

ونڈو کے اندر نقلیں

ٹائم اسٹیمپ 5 منٹ سے پرانے ری پلے روکتا ہے، اس ونڈو کے اندر موجود دوسری نقل کو نہیں۔ اینڈ پوائنٹ ڈیلیوریز کم از کم ایک بار ہوتی ہیں، اس لیے اسی ایونٹ کی دوبارہ کوشش بھی آ سکتی ہے؛ اینویلپ کے event_id کی بنیاد پر نقلیں ہٹا دیں۔

مطابقت نہ ہونے پر 401 واپس نہ کرنا

ناکام تصدیق پر 200 واپس کرنا ہینڈلر کو replay کا ہدف بناتا ہے۔ تصدیق ناکام ہونے پر ہمیشہ 2xx کے علاوہ جواب دیں۔


اگلے مراحل