ThunderPhone 2.0 on nyt julkaistu.Ota käyttöön itse – alkaen 2¢/min.Lue lisää julkistuksesta

Operations

Vahvista webhook-allekirjoitukset

Jokainen ThunderPhonen lähettämä webhook- ja työkalupyyntö allekirjoitetaan. Vahvista allekirjoitus kerran tämän ohjeen avulla ja käytä sitten samaa tarkistusta jokaisessa käyttämässäsi päätepisteessä.

Jokaisessa palvelimellesi lähettämässämme pyynnössä — webhook-toimituksissa ja työkalupäätepisteiden kutsuissa — on HMAC-SHA256-allekirjoitus X-ThunderPhone-Signature-otsakkeessa. Toteuta vahvistus kerran oikein ja käytä samaa apufunktiota jokaisessa käsittelijässä.

Algoritmi

  1. Lue pyynnön raaka runko — täsmälleen ne tavut, jotka POSTasimme sinulle.
  2. Laske hmac_sha256(secret, body).hexdigest().
  3. Vertaa sitä vakioaikaisesti arvoon X-ThunderPhone-Signature. (Yksinkertainen merkkijonovertailu vuotaa ajoitustietoa.)

Allekirjoitamme täsmälleen lähettämämme tavut, joten raakarungon vahvistaminen toimii aina. Nämä tavut ovat myös hyötykuorman kanoninen JSON-sarjoitus — avaimet aakkosjärjestyksessä, tiiviit erottimet (, ja : ilman välilyöntejä), UTF-8. Tämä tarjoaa toisen, täysin vastaavan tavan, kun kehys tarjoaa vain jäsennetyn JSONin: sarjoita kanonisesti uudelleen ja laske siitä HMAC.

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

Suosi raakarunkoa — siinä on yksi vaihe vähemmän, eikä se altistu joidenkin kielten JSON-lukujen edestakaisen muunnoksen erikoisuuksille.

Mikä salaisuus?

LähdeSalaisuus
Webhook-päätepiste (/v1/developer/webhook-endpoints)Päätepistekohtainen secret (48 heksamerkkiä), joka palautetaan kerran luotaessa
Vanha yhden URL-osoitteen webhookOrganisaatiokohtainen secret, joka palautetaan pyynnössä GET /v1/webhook
Työkalupäätepisteen kutsu (suora kutsu osoitteeseen endpoint.url)Organisaatiotason webhook-salaisuus (sama kuin vanhassa yhden URL-osoitteen webhookissa) — ei päätepistekohtainen salaisuus

Tallenna salaisuus salaisuuksien hallintaan tai ympäristömuuttujaan — älä koskaan commitoi sitä.

Viitetoteutukset

Kaikki neljä vahvistavat pyynnön raakarungon:

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

Kehyskohtainen kytkentä

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})

Työkalukutsujen varmistaminen

Kun agentti kutsuu jotakin sinun funktiotyökaluistasi suoraan (työkalulla on endpoint), pyyntö sisältää kaksi ThunderPhone-otsaketta määrittämiesi endpoint.headers-otsakkeiden lisäksi:

  • X-ThunderPhone-Call-ID — käynnissä olevan puhelun numeerinen tunniste.
  • X-ThunderPhone-Signature — HMAC-SHA256, joka käyttää avaimena organisaatiotason webhook-salaisuutta ja lasketaan pyynnön täsmällisistä rungon tavuista.

Sama verify()-apuohjelma toimii sellaisenaan, mutta huomioi kaksi poikkeusta:

  1. GET / DELETE-työkaluilla ei ole runkoa. Argumentit välitetään kyselyparametreina, ja allekirjoitus lasketaan tyhjästä tavujonosta — siis verify(b"", sig, secret) (Python) tai verify(Buffer.alloc(0), sig, secret) (Node). Älä tiivistä kyselymerkkijonoa.
  2. Organisaatioilla, joilla ei ole määritettyä vanhaa webhookia, ei ole organisaatiosalaisuutta. Tällöin työkalukutsut sisältävät vain X-ThunderPhone-Call-ID-otsakkeen, eivätkä allekirjoitusotsaketta. Määritä vanha webhook (PUT /v1/webhook) saadaksesi allekirjoitussalaisuuden tai todenna työkalukutsut omalla otsakkeellasi endpoint.headers-asetuksen kautta.
@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)
    ...

Webhook-tilan työkalujen välitys (työkalut ilman endpoint-asetusta, jotka toimitetaan organisaatiosi webhookiin tyyppeinä telephony.tool / web.tool) on tavallinen allekirjoitettu webhook — yllä oleva vakiomenettely pätee. Katso molemmat pyyntömuodot kohdasta Funktiotyökalut.

Yleiset sudenkuopat

Uudelleensarjoittaminen oletusmuotoilulla

Rungon jäsentäminen ja sen uudelleenkirjoittaminen JSON-kirjastosi oletusasetuksilla (välilyönnit merkkien , / : jälkeen, lisäysjärjestystä noudattavat avaimet) tuottaa eri tavut ja rikkoo HMACin. Vahvista raakarunko — tai jos sinun on sarjoitettava uudelleen, vastaa tarkasti kanonista muotoamme: lajitellut avaimet, tiiviit erottimet, UTF-8.

Kehys jäsentää JSONin automaattisesti

Expressin express.json()-middleware kuluttaa rungon tietovirran ja menetät raakatavut. Käytä express.raw()-middlewarea nimenomaan webhook-reitillä tai puskuroi raakarunko esiväliohjelmassa. Sama koskee NestJS:ää / Koaa — tarkista niiden "raaka runko" -dokumentaatio.

Ajoitusturvaton vertailu

expected === signature JS:ssä tai expected == signature Pythonissa ovat ajoituksesta riippuvia vertailuja. Käytä vastaavasti crypto.timingSafeEqual- tai hmac.compare_digest-funktiota. Suorituskykyero on olematon.

Väärä salaisuus työkalupäätepisteille

Suorat työkalupäätepistekutsut allekirjoitetaan organisaatiotason webhook-salaisuudella (GET /v1/webhook) — ei millään päätepistekohtaisella salaisuudella polusta /v1/developer/webhook-endpoints. Käytä samaa verify()- funktiota uudelleen, mutta varmista, että syötät sille organisaation salaisuuden työkalureiteillä.

Kyselymerkkijonon tiivistäminen GET/DELETE-työkaluissa

Rungottomissa työkalumenetelmissä allekirjoitus kattaa tyhjän tavujonon, joten käytössä on yksi yleispätevä menettely: laske HMAC pyynnön raakarungolle, mikä se sitten onkin. URL-osoitteen tai kyselymerkkijonon tiiviste ei koskaan täsmää.

401-vastauksen palauttamatta jättäminen, kun allekirjoitus ei täsmää

200-vastauksen palauttaminen epäonnistuneen vahvistuksen jälkeen tekee käsittelijästä uudelleenlähetyshyökkäyksen kohteen. Palauta aina muu kuin 2xx-vastaus, jos vahvistus epäonnistuu.


Seuraavat vaiheet