ThunderPhone 2.0 เปิดให้ใช้งานแล้วเริ่มใช้งานได้ด้วยตัวเอง ราคาเริ่มต้น 2 เซนต์ต่อนาที.อ่านประกาศเปิดตัว

Operations

ตรวจสอบลายเซ็น Webhook

Webhook และคำขอเครื่องมือทุกรายการที่ ThunderPhone ส่งจะมีการลงลายเซ็น ตรวจสอบลายเซ็นหนึ่งครั้งด้วยแนวทางในหน้านี้ แล้วนำการตรวจสอบเดียวกันนี้ไปใช้ซ้ำกับทุกเอนด์พอยต์ที่คุณใช้งาน

ทุกคำขอที่เราส่งไปยังเซิร์ฟเวอร์ของคุณ — ทั้งการส่ง webhook และการเรียกใช้ tool endpoint — จะมีลายเซ็น HMAC-SHA256 อยู่ในส่วนหัว X-ThunderPhone-Signature ตรวจสอบให้ถูกต้องเพียงครั้งเดียว แล้วใช้ตัวช่วยเดียวกันกับ handler ทุกตัว

อัลกอริทึม

  1. อ่านเนื้อหาคำขอ แบบดิบ — ไบต์ที่ตรงตามที่เรา POST ให้คุณทุกประการ
  2. คำนวณ hmac_sha256(secret, body).hexdigest()
  3. เปรียบเทียบกับ X-ThunderPhone-Signature แบบ เวลาคงที่ (การเปรียบเทียบสตริงแบบทั่วไปจะเปิดเผยข้อมูลด้านเวลา)

เราลงลายเซ็นบนไบต์ที่ส่งจริงทุกประการ ดังนั้นการตรวจสอบเนื้อหาแบบดิบจึงใช้ได้เสมอ ไบต์เหล่านั้นยังเป็น การทำให้ JSON เป็นรูปแบบมาตรฐาน ของ payload ด้วย — คีย์เรียงตามตัวอักษร ตัวคั่นแบบกระชับ (, และ : โดยไม่มีช่องว่าง) และ UTF-8 ซึ่งให้วิธีที่สองที่เทียบเท่ากันโดยสมบูรณ์ เมื่อเฟรมเวิร์กของคุณเปิดเผยเฉพาะ JSON ที่แยกวิเคราะห์แล้ว: ทำให้เป็นรูปแบบมาตรฐานอีกครั้ง แล้วคำนวณ HMAC จากผลลัพธ์นั้น

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

ควรใช้เนื้อหาแบบดิบ — มีขั้นตอนน้อยกว่า และไม่ถูกรบกวนจากลักษณะเฉพาะของการแปลงตัวเลข JSON ไปกลับในบางภาษา

ใช้ secret ใด

แหล่งที่มาSecret
Webhook endpoint (/v1/developer/webhook-endpoints)secret เฉพาะ endpoint (อักขระ hex 48 ตัว) ที่ส่งกลับเพียงครั้งเดียวเมื่อสร้าง
Webhook URL เดียวแบบเดิมsecret เฉพาะองค์กรที่ส่งกลับจาก GET /v1/webhook
การเรียกใช้ Tool endpoint (เรียกไปยัง endpoint.url ของคุณโดยตรง)webhook secret ระดับองค์กร (ค่าเดียวกับ webhook URL เดียวแบบเดิม) — ไม่ใช่ secret เฉพาะ endpoint

จัดเก็บ secret ไว้ในตัวจัดการ secret หรือตัวแปรสภาพแวดล้อม — ห้าม commit โดยเด็ดขาด

ตัวอย่างการติดตั้งใช้งานอ้างอิง

ทั้งสี่ตัวอย่างตรวจสอบเนื้อหาคำขอแบบดิบ:

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.json() ของ Express จะใช้สตรีมเนื้อหา ทำให้คุณสูญเสียไบต์ดิบ ใช้ 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 เสมอหากการตรวจสอบล้มเหลว


ขั้นตอนถัดไป