IlmHamroh
JavaScript Full-stack/29-qism. Backend integratsiyalar: fayl, to'lov, bot, realtime34/69-dars17 daqiqa
Mundarija (18)

Kiruvchi webhook'larni qabul qilish: imzo, vaqt oynasi va takrorlar

Qisqacha: Webhook — tashqi xizmat sizning URL'ingizga yuboradigan HTTP so'rov: "hodisa bo'ldi". Bu URL ommaviy, shuning uchun har so'rovga beshta savol beriladi. Imzo to'g'rimi (HMAC, xom tana ustida, timingSafeEqual bilan)? Eski emasmi (vaqt oynasi — replay himoyasi)? Bu hodisani avval ko'rganmizmi (hodisa id bo'yicha dedup)? Tez javob beryapmizmi (200, og'ir ish — navbatga)? Tartibga tayanmayapmizmi? Kanon Standard Webhooks formatini paketsiz yozdi va rasmiy standardwebhooks 1.1.1 paketi bilan ikki tomonlama mosligini test bilan isbotladi.

Bu darsda

  • Webhook nima va nega u alohida xavfsizlik talab qilishini bilasiz.
  • Standard Webhooks formatini (webhook-id, webhook-timestamp, webhook-signature) o'qiysiz.
  • HMAC-SHA256 imzoni Web Crypto bilan hisoblaysiz.
  • Vaqt oynasi, kalit almashtirish (bir nechta imzo) va doimiy vaqtli solishtirishni qo'llaysiz.
  • Takror hodisalarni bazada filtrlaysiz va tartibsiz hodisalarga chidamli kod yozasiz.
  • webhook/imzo.ts modulini yozasiz.

Oldin bilishingiz kerak: Stripe (Stripe-Signature, express.raw), crypto 1: tasodif, hash va HMAC, Idempotency-Key, UPSERT va MERGE (ON CONFLICT).

Stripe darsida bitta provayderning webhook'ini qabul qildik. Endi umumiy qoidalarni yig'amiz — ular Telegram, GitHub, to'lov tizimlari va har qanday boshqa webhook uchun bir xil.

1. Nega bu kerak?

Webhook manzilingiz — masalan, https://bozor.uz/webhook/tolov — ommaviy. Uni topgan har kim POST yubora oladi: "buyurtma 42 to'landi". Agar server bunga ishonsa — tekin tovar. Bundan tashqari:

  • provayder bir hodisani bir necha marta yuboradi (javobingiz kechikdi, tarmoq uzildi) — ikki marta ishlov;
  • hodisalar tartibsiz keladi — "bekor qilindi" "yaratildi" dan oldin;
  • javobingiz sekin bo'lsa, provayder timeout deb qayta yuboradi — yuk ko'payadi.

Pochtachi o'xshatishi: eshigingizga xat keldi. Muhrni tekshirasiz (imzo), sanasiga qaraysiz (eski xat emasmi), bu xatni avval olganmisiz (raqami), "oldim" deb imzo qo'yib darhol jo'natib yuborasiz (tez 200), xatni esa keyin, xotirjam o'qiysiz (navbat).

2. Standard Webhooks formati

Har provayder o'z imzo formatini o'ylab topgan: Stripe — t=…,v1=… hex, GitHub — sha256=…, Click — MD5. Standard Webhooks — bir guruh kompaniyalar (Svix boshchiligida) taklif qilgan umumiy spetsifikatsiya. Kanon uni ham kiruvchi, ham chiquvchi (keyingi dars) webhook'lar uchun tanladi.

Uchta sarlavha:

Sarlavha Qiymati
webhook-id hodisaning noyob id'si (takrorni filtrlash uchun)
webhook-timestamp yuborilgan vaqt, unix soniya
webhook-signature v1,<base64 HMAC>; bir nechtasi bo'shliq bilan

Imzo: HMAC-SHA256(kalit, "<id>.<timestamp>.<xom tana>"), base64. Sir whsec_ + base64 ko'rinishida beriladi — kalit baytlarini olish uchun whsec_ dan keyingi qism base64'dan dekodlanadi. Stripe'dan farqi shu: u yerda butun satr kalit edi.

Web Crypto bilan (brauzerda ham ishlaydi):

js
const enc = new TextEncoder();
const b64 = (u8) => btoa(String.fromCharCode(...u8));
const unb64 = (s) => Uint8Array.from(atob(s), (c) => c.charCodeAt(0));

async function imzola(sir, id, vaqt, tana) {
  const kalit = await crypto.subtle.importKey(
    "raw", unb64(sir.slice("whsec_".length)),
    { name: "HMAC", hash: "SHA-256" }, false, ["sign"],
  );
  const matn = enc.encode(`${id}.${vaqt}.${tana}`);
  const imzo = await crypto.subtle.sign("HMAC", kalit, matn);
  return `v1,${b64(new Uint8Array(imzo))}`;
}

(async () => {
  const sir = "whsec_c294dGEtbG9rYWwta2FsaXQtMjQtYmF5dA==";
  const tana = '{"type":"vazifa.bajarildi","data":{"id":42}}';
  console.log(await imzola(sir, "msg_1", 1793095200, tana));
  const boshqa = tana.replace("42", "43");
  console.log(await imzola(sir, "msg_1", 1793095200, boshqa));
})();

Konsolda:

text
v1,tx0uT3FdOfYHiqFxw/sS11c4TJxUXRJQI0KP84jtumc=
v1,Fb6uBrkjUOHgEbsA8ZRdtsEHayuAP7Qgv+PM9zySBXs=

Bu sir — soxta, faqat dars uchun (soxta-lokal-kalit-24-bayt matnining base64'i). Birinchi imzoni biz uch usulda hisobladik (biz ishga tushirdik): Web Crypto, kanon imzola (node:crypto) va rasmiy standardwebhooks 1.1.1 paketining sign metodi — uchala natija bir xil. Tanadagi bitta raqam o'zgardi — imzo butunlay boshqa.

id va timestamp imzo ichida — bu muhim. Hujumchi eski imzoli so'rovni olib, sarlavhadagi vaqtni yangilasa, imzo mos kelmaydi.

3. Tekshiruv: beshta qadam

Kanon tekshir funksiyasi tartibi:

  1. Sarlavhalar bormi, vaqt raqammi (^\d{1,12}$). Yo'q — sarlavha.
  2. Vaqt oynasi: hozir − 300 dan eski — eski, hozir + 300 dan keyingi — kelajak. Bu replay himoyasi: o'g'irlangan so'rovni 5 daqiqadan keyin qayta yuborib bo'lmaydi. Kelajak chegarasi soatlar farqiga chidaydi, lekin "kelajakdagi" so'rovni ertaga ishlatishni to'xtatadi.
  3. Kutilgan imzo — o'zimiz hisoblaymiz, xom tana bilan.
  4. Har imzo bilan solishtirish: sarlavhada bo'shliq bilan bir nechta imzo bo'lishi mumkin — kalit almashtirish (rotatsiya, rotation) paytida yuboruvchi eski va yangi kalit bilan imzolaydi. Birortasi mos kelsa yetarli. Noma'lum versiyalar (v1a, v2) o'tkaziladi.
  5. Doimiy vaqtli solishtirish — timingSafeEqual, uzunlik avval.

Kanon kodi (formatlangan, ma'nosi kanondagidek):

ts
export type TekshiruvNatija =
  | { ok: true; id: string; vaqt: number }
  | { ok: false; sabab: "sarlavha" | "eski" | "kelajak" | "imzo" };

export function tekshir(
  sir: string,
  sarlavha: (nom: string) => string | undefined,
  xomTana: string | Buffer,
  o: { toleransSoniya?: number; hozirSoniya?: number } = {},
): TekshiruvNatija {
  const id = sarlavha("webhook-id");
  const ts = sarlavha("webhook-timestamp");
  const imzolar = sarlavha("webhook-signature");
  if (!id || !ts || !imzolar || !/^\d{1,12}$/.test(ts))
    return { ok: false, sabab: "sarlavha" };
  const vaqt = Number(ts);
  const hozir = o.hozirSoniya ?? Math.floor(Date.now() / 1000);
  const tol = o.toleransSoniya ?? 300;
  if (vaqt < hozir - tol) return { ok: false, sabab: "eski" };
  if (vaqt > hozir + tol) return { ok: false, sabab: "kelajak" };
  const kutilgan = Buffer.from(
    imzola(
      sir,
      id,
      vaqt,
      typeof xomTana === "string"
        ? xomTana
        : xomTana.toString("utf8"),
    ).slice(3),
    "base64",
  );
  for (const bolak of imzolar.split(" ")) {
    const [ver, qiymat] = bolak.split(",");
    if (ver !== "v1" || !qiymat) continue;
    const b = Buffer.from(qiymat, "base64");
    if (
      b.length === kutilgan.length &&
      timingSafeEqual(b, kutilgan)
    )
      return { ok: true, id, vaqt };
  }
  return { ok: false, sabab: "imzo" };
}

Natija — discriminated union (ajratuvchi maydonli union, TypeScript qismida o'tilgan): ok: true bo'lsa id va vaqt, aks holda sabab. Sabab logga yoziladi (qaysi xato ko'payganini bilish uchun), javobga esa faqat 400 — hujumchiga qaysi qadamda yiqilganini aytmaymiz.

Tekshirib ko'ring: Nega imzoni === bilan solishtirish xavfli, axir imzo 32 baytlik tasodifiy qiymat-ku?

Javob

=== satrlarni boshidan solishtiradi va birinchi farqli belgida to'xtaydi. Javob vaqtini millionlab marta o'lchab, hujumchi imzoning birinchi baytini, keyin ikkinchisini topishi mumkin (timing attack). Tarmoq shovqini buni qiyinlashtiradi, lekin imkonsiz qilmaydi. timingSafeEqual har doim hamma baytni solishtiradi — vaqt farq qilmaydi.

4. Xom tana

Stripe darsidagi saboq umumiy: imzo baytlar ustida. express.json() dan keyin qayta JSON.stringify qilingan tana boshqacha bo'lishi mumkin (bo'shliqlar, kalitlar tartibi, \u ekranlash). Kanon testi buni Stripe uchun isbotladi: qayta formatlangan JSON — 400.

Express'da qoida: webhook marshrutiga express.raw({ type: "application/json" }) (yoki express.text) va u global express.json() dan oldin ulanadi. Imzo tekshirilgandan keyin o'zingiz JSON.parse qilasiz. Tanaga hajm chegarasi ham qo'ying (limit) — imzo tekshiruvidan oldin 1 GB tanani o'qish o'zi DoS.

5. Takrorlar: hodisa id bo'yicha dedup

Webhook yetkazish — "kamida bir marta" (at-least-once): yuboruvchi javobni olmasa, qayta yuboradi. Sizning vazifangiz — "aynan bir marta" ishlov berish. Usul — hodisa id'sini noyob kalit bilan saqlash. Kanon kiruvchi_hodisalar jadvali (tolov bazasi):

sql
CREATE TABLE kiruvchi_hodisalar (
  provayder      text        NOT NULL,
  hodisa_id      text        NOT NULL,
  tur            text        NOT NULL,
  tana           jsonb       NOT NULL,
  qabul_vaqt     timestamptz NOT NULL DEFAULT now(),
  ishlangan_vaqt timestamptz,
  PRIMARY KEY (provayder, hodisa_id)
);

Takror qanday ko'rinishini o'z Postgres 17.6 klasterimizda kanon sxemasi ustida ko'ramiz (biz ishga tushirdik):

sql
INSERT INTO kiruvchi_hodisalar (provayder, hodisa_id, tur, tana)
VALUES ('stripe', 'evt_1', 'payment_intent.succeeded', '{}')
ON CONFLICT DO NOTHING;
INSERT INTO kiruvchi_hodisalar (provayder, hodisa_id, tur, tana)
VALUES ('stripe', 'evt_1', 'payment_intent.succeeded', '{}')
ON CONFLICT DO NOTHING;
text
INSERT 0 1
INSERT 0 0

INSERT 0 0 — qator qo'shilmadi. Kodda bu rowCount === 0 — "takror", ishlov to'xtaydi, javob 200. Muhim: dedup yozuvi va ishlov (buyurtmani to'langan qilish) bitta ma'lumotlar bazasi tranzaksiyasida. Aks holda hodisa "ko'rilgan" deb yoziladi, ishlov esa yiqiladi — va qayta yuborilganda "takror" deb tashlanadi. Hodisa yo'qoladi.

Har manba o'z id'si bilan:

Manba Dedup kaliti
Standard Webhooks webhook-id
Stripe event.id (evt_…)
Telegram Stars telegram_payment_charge_id
Payme, Click, Uzum provayder tranzaksiya id'si (UNIQUE (provayder, tashqi_id))

Telegram Stars darsida kanon xuddi shu hodisani ikki marta yuborganda ikkinchisi e'tiborsiz qolganini ko'rgansiz (Telegram Mini App backendi va Stars).

6. Tez 200 va navbat

Yuboruvchilar javobni cheklangan vaqt kutadi. Agar webhook ichida email yuborsangiz, PDF chek yasasangiz, boshqa API'ni chaqirsangiz — timeout, qayta yuborish, yana timeout. Naqsh:

flowchart LR
  W[Webhook POST] --> I{Imzo va vaqt}
  I -->|xato| R400[400]
  I -->|to'g'ri| T[Tranzaksiya: dedup + holat]
  T -->|takror| R200a[200 takror]
  T -->|yangi| Q[Navbatga vazifa]
  Q --> R200[200]
  Q -.-> WK[Worker: email, chek, bildirishnoma]

Diagrammaga qarang: so'rov ichida faqat tez va zarur ish — imzo, dedup, buyurtma holati. Qolgani navbatga (Nega navbat kerak, BullMQ asoslari). Kanon Stripe webhook'ida hozircha og'ir ish yo'q; navbat bilan ulanishi BullMQ darsida.

Qachon 200 emas: imzo xato — 400 (qayta yuborish foydasiz); bizning ichki xatomiz (baza ishlamayapti) — 500, shunda yuboruvchi keyinroq qayta urinadi va hodisa yo'qolmaydi. Uchala javob yonma-yon:

Holat Javobimiz Yuboruvchi nima qiladi
yangi hodisa, ishlandi 200 yetkazildi deb belgilaydi
takror hodisa 200 yetkazildi deb belgilaydi
imzo yoki vaqt xato 400 xato deb yozadi
bizda ichki xato 500 keyinroq qayta yuboradi
javob kechikdi (timeout) — keyinroq qayta yuboradi

Oxirgi qatorga e'tibor bering: sekin javob ham 500 kabi ta'sir qiladi. Shuning uchun so'rov ichida faqat tez ish qoladi.

7. Tartib kafolatlanmaydi

Ikki hodisa: buyurtma.yangilandi (10:00:01) va buyurtma.bekor (10:00:02). Tarmoqda ikkinchisi birinchi yetib keldi. Agar "kelgan tartibda qo'llash" qilsak, bekor qilingan buyurtma "yangilandi" bo'lib tiriladi.

Ikki himoya:

  1. Holat mashinasi: bekor — yakuniy holat, undan chiqish yo'q (To'lov ekotizimi va umumiy oqim). Kech kelgan "yangilandi" rad etiladi.
  2. Versiya yoki vaqt: har obyektda oxirgi qo'llangan hodisa vaqtini saqlang va eskisini e'tiborsiz qoldiring:
js
const saqlangan = { id: 42, holat: "yangi", oxirgiVaqt: 0 };

function qolla(obyekt, hodisa) {
  if (hodisa.vaqt <= obyekt.oxirgiVaqt) {
    return `e'tiborsiz: ${hodisa.tur} (eski)`;
  }
  obyekt.holat = hodisa.holat;
  obyekt.oxirgiVaqt = hodisa.vaqt;
  return `qo'llandi: ${hodisa.tur}`;
}

const kelishTartibi = [
  { tur: "bekor", holat: "bekor", vaqt: 1002 },
  { tur: "yangilandi", holat: "kutilmoqda", vaqt: 1001 },
  { tur: "bekor", holat: "bekor", vaqt: 1002 },
];
for (const h of kelishTartibi) console.log(qolla(saqlangan, h));
console.log(saqlangan.holat);

Konsolda:

text
qo'llandi: bekor
e'tiborsiz: yangilandi (eski)
e'tiborsiz: bekor (eski)
bekor

Uchinchi qator — takror ham shu tekshiruvdan o'tmadi. Lekin vaqt bo'yicha filtr dedup'ning o'rnini bosmaydi: bir xil vaqtli ikki xil hodisa bo'lishi mumkin. Ikkalasi birga.

Stripe kabi provayderlar hujjatida ham aytiladi: hodisa tartibiga tayanmang; kerak bo'lsa obyektning joriy holatini API'dan so'rang. Kanon Stripe testida takror va kech hodisalar shunday sinaldi.

8. Hujumchi nigohi

Hujum Himoya Kanon tekshiruvi
soxta POST "to'landi" HMAC imzo test: boshqa sir → imzo
tanani o'zgartirish imzo xom tana ustida test: {"a":2} → imzo
eski so'rovni qayta yuborish vaqt oynasi ±300 s test: 301 s → eski
"kelajak" vaqtli so'rov yuqori chegara test: kelajak
imzoni vaqt bo'yicha topish timingSafeEqual kodda
takror yuborish webhook-id / event.id dedup Stripe testi: takror
ulkan tana limit imzodan oldin express.raw({ limit })

9. Ko'p uchraydigan xatolar

Xato Oqibati To'g'risi
imzosiz qabul qilish soxta hodisalar HMAC, har so'rovda
express.json() dan keyin imzo haqiqiy so'rovlar rad xom tana
vaqtni tekshirmaslik replay ±300 s
=== bilan solishtirish timing attack timingSafeEqual
dedup va ishlov alohida hodisa yo'qoladi bitta tranzaksiya
webhook ichida og'ir ish timeout, takrorlar tez 200 + navbat
ichki xatoda 200 hodisa yo'qoladi 500 — qayta yuborilsin
kelish tartibiga ishonish bekor buyurtma tiriladi holat mashinasi + vaqt

10. Mashqlar

Mashqlar kurs/mashqlar/29/34-kiruvchi/ da.

1-mashq (oson): qaysi sabab?

Kanon tekshir qaytaradigan sababni ayting (sarlavha, eski, kelajak, imzo yoki ok):

  1. webhook-timestamp = hozir − 400.
  2. Sarlavhada v1,yolgon v2,x v1,<to'g'ri imzo>.
  3. webhook-id yo'q.
  4. Tana {"a":1} imzolangan, {"a":2} keldi.
  5. webhook-timestamp = hozir + 600.
Yechim
  1. eski. 2. ok — uchinchi imzo mos keldi, v2 o'tkazildi (kanon testida aynan shu holat). 3. sarlavha. 4. imzo. 5. kelajak.

2-mashq (o'rta): sarlavhadagi imzolar

imzolarniAjrat(s) yozing: webhook-signature qiymatidan faqat v1 imzolarining base64 qismlarini ro'yxat qilib qaytarsin. Bo'shliq bilan ajratilgan; vergulsiz yoki bo'sh qiymatli bo'laklar o'tkaziladi. Uch holatni sinang.

Yechim
js
function imzolarniAjrat(s) {
  const natija = [];
  for (const bolak of s.split(" ")) {
    const i = bolak.indexOf(",");
    if (i < 0) continue;
    const [ver, qiymat] = [bolak.slice(0, i), bolak.slice(i + 1)];
    if (ver === "v1" && qiymat) natija.push(qiymat);
  }
  return natija;
}

console.log(imzolarniAjrat("v1,abc="));
console.log(imzolarniAjrat("v1,yolgon v2,x v1,abc= buzuq v1,"));
console.log(imzolarniAjrat(""));

Konsolda:

text
[ 'abc=' ]
[ 'yolgon', 'abc=' ]
[]

indexOf(",") — birinchi vergul. Base64 ichida vergul bo'lmaydi, lekin = bo'ladi — shuning uchun split("=") emas. Kanon split(",") ishlatadi va ikkinchi qismni oladi — natija shu.

3-mashq (qiyin): xotiradagi qabul qiluvchi

Bazasiz qabulQiluvchi() yozing: ichida Set (ko'rilgan id'lar) va ishlov hisoblagichi. qabul({ id, vaqt, hozir }) qaytarsin: vaqt oynasidan tashqarida — 400 eski yoki 400 kelajak (300 s), takror — 200 takror, yangi — 200 yangi (va hisoblagich oshadi). Imzoni tekshirilgan deb hisoblang. Besh so'rovni sinang va oxirida nechta ishlov bo'lganini chiqaring.

Yechim
js
function qabulQiluvchi(tolerans = 300) {
  const korilgan = new Set();
  let ishlov = 0;
  return {
    qabul({ id, vaqt, hozir }) {
      if (vaqt < hozir - tolerans) return "400 eski";
      if (vaqt > hozir + tolerans) return "400 kelajak";
      if (korilgan.has(id)) return "200 takror";
      korilgan.add(id);
      ishlov++;
      return "200 yangi";
    },
    get ishlov() {
      return ishlov;
    },
  };
}

const q = qabulQiluvchi();
const hozir = 1_793_095_200;
console.log(q.qabul({ id: "msg_1", vaqt: hozir, hozir }));
console.log(q.qabul({ id: "msg_1", vaqt: hozir, hozir: hozir + 5 }));
console.log(q.qabul({ id: "msg_2", vaqt: hozir - 301, hozir }));
console.log(q.qabul({ id: "msg_3", vaqt: hozir + 900, hozir }));
console.log(q.qabul({ id: "msg_4", vaqt: hozir - 299, hozir }));
console.log("ishlov:", q.ishlov);

Konsolda:

text
200 yangi
200 takror
400 eski
400 kelajak
200 yangi
ishlov: 2

Xotiradagi Set server qayta ishga tushganda tozalanadi va bir nechta server nusxasi orasida bo'linmaydi — shuning uchun haqiqiy kodda dedup bazada. Yana bir nozik joy: vaqt oynasi tashqarisidagi takrorni ham rad etamiz — bu yaxshi, lekin yuboruvchi 5 daqiqadan keyin yangi vaqt bilan qayta imzolab yuboradi, shuning uchun dedup oynadan uzoqroq saqlanadi.

11. Loyiha qadami: webhook/imzo.ts

Modul modullar/src/webhook/imzo.ts — paketsiz, faqat node:crypto. Kiruvchi (tekshirish) va chiquvchi (imzolash, keyingi dars) webhook'lar uchun bitta fayl. To'liq (formatlangan nusxa, ma'nosi kanondagidek):

ts
// imzo.ts — Standard Webhooks (standardwebhooks.com) imzosi: kiruvchi
// va chiquvchi webhook'lar uchun bitta format.
// Sarlavhalar: webhook-id, webhook-timestamp (unix soniya),
// webhook-signature: "v1,<base64(HMAC-SHA256(kalit,
// `${id}.${timestamp}.${tana}`))>" (bo'shliq bilan bir nechta).
// Sir: "whsec_" + base64(kalit baytlari). Tekshiruv: imzo
// (timingSafeEqual), vaqt oynasi (sukut ±5 daqiqa — replay himoyasi),
// XOM tana.
// Paketsiz yozilgan; `standardwebhooks` 1.1.1 bilan moslik testda
// tekshiriladi.
import {
  createHmac,
  randomBytes,
  timingSafeEqual,
} from "node:crypto";

export const sirYarat = (): string =>
  `whsec_${randomBytes(24).toString("base64")}`;
const kalit = (sir: string) =>
  Buffer.from(
    sir.startsWith("whsec_") ? sir.slice(6) : sir,
    "base64",
  );

export function imzola(
  sir: string,
  id: string,
  vaqtSoniya: number,
  tana: string,
): string {
  return `v1,${createHmac("sha256", kalit(sir))
    .update(`${id}.${String(vaqtSoniya)}.${tana}`)
    .digest("base64")}`;
}

export function sarlavhalar(
  sir: string,
  id: string,
  tana: string,
  vaqtSoniya = Math.floor(Date.now() / 1000),
): Record<string, string> {
  return {
    "webhook-id": id,
    "webhook-timestamp": String(vaqtSoniya),
    "webhook-signature": imzola(sir, id, vaqtSoniya, tana),
  };
}

export type TekshiruvNatija =
  | { ok: true; id: string; vaqt: number }
  | { ok: false; sabab: "sarlavha" | "eski" | "kelajak" | "imzo" };

export function tekshir(
  sir: string,
  sarlavha: (nom: string) => string | undefined,
  xomTana: string | Buffer,
  o: { toleransSoniya?: number; hozirSoniya?: number } = {},
): TekshiruvNatija {
  const id = sarlavha("webhook-id");
  const ts = sarlavha("webhook-timestamp");
  const imzolar = sarlavha("webhook-signature");
  if (!id || !ts || !imzolar || !/^\d{1,12}$/.test(ts))
    return { ok: false, sabab: "sarlavha" };
  const vaqt = Number(ts);
  const hozir = o.hozirSoniya ?? Math.floor(Date.now() / 1000);
  const tol = o.toleransSoniya ?? 300;
  if (vaqt < hozir - tol) return { ok: false, sabab: "eski" };
  if (vaqt > hozir + tol) return { ok: false, sabab: "kelajak" };
  const kutilgan = Buffer.from(
    imzola(
      sir,
      id,
      vaqt,
      typeof xomTana === "string"
        ? xomTana
        : xomTana.toString("utf8"),
    ).slice(3),
    "base64",
  );
  for (const bolak of imzolar.split(" ")) {
    const [ver, qiymat] = bolak.split(",");
    if (ver !== "v1" || !qiymat) continue;
    const b = Buffer.from(qiymat, "base64");
    if (
      b.length === kutilgan.length &&
      timingSafeEqual(b, kutilgan)
    )
      return { ok: true, id, vaqt };
  }
  return { ok: false, sabab: "imzo" };
}

sirYarat — 24 tasodifiy bayt, whsec_ + base64: har webhook obunasi uchun alohida sir.

Testlar — modullar/test/webhook.test.ts. Imzo bo'yicha ikkitasi. Birinchisi — rasmiy standardwebhooks 1.1.1 bilan ikki tomonlama moslik (ularning verify bizning imzoni qabul qiladi, ularning sign bizning imzola bilan bir xil, uch imzoli sarlavhada to'g'risi topiladi). Ikkinchisi — xatolar (o'zgartirilgan tana, boshqa sir, eski, kelajak, sarlavhasiz). Parcha:

ts
test("imzo: standardwebhooks 1.1.1 bilan ikki tomonlama mos", () => {
  const sir = sirYarat();
  const tana = JSON.stringify({
    type: "vazifa.bajarildi", data: { id: 42, matn: "Oʻqish" },
  });
  const h = sarlavhalar(sir, "msg_1", tana);
  const wh = new Webhook(sir);
  assert.deepEqual(wh.verify(tana, h), JSON.parse(tana));
  const t = Math.floor(Date.now() / 1000);
  const ularniki = wh.sign("msg_2", new Date(t * 1000), tana);
  assert.equal(ularniki, imzola(sir, "msg_2", t, tana));
});

Butun webhook test faylini biz ishga tushirdik (kanon nusxasi, Node 24.21.0, o'z Postgres 17.6 klasterimizda kanon sxemasi sql/001–204). Birinchi ikkitasi — shu darsniki, qolganlari — keyingi darsniki. Avval eng yuqoridagi ikki imzo: qatoriga, keyin fail 0 ga qarang:

text
✔ imzo: standardwebhooks 1.1.1 bilan ikki tomonlama mos (5.4168ms)
✔ imzo: o'zgartirilgan tana, boshqa sir, eski/kelajak vaqt, sarlavhasiz (0.5404ms)
✔ SSRF: IP ro'yxati va URL qoidalari (6.2427ms)
✔ kechikish jadvali: jitter ±20 %, oxirida undefined (DLQ) (0.2222ms)
▶ chiquvchi yetkazish (Postgres + lokal qabul qiluvchi)
  ✔ yuborildi: imzo qabul qiluvchida tekshiriladi; hodisa_id bir xil bo'lsa takror navbatga tushmaydi (119.3479ms)
  ✔ 500 → qayta (urinishlar, keyingi vaqt); 302 → muvaffaqiyat EMAS; 410 → obuna o'chadi; jadval tugasa — DLQ (34.6685ms)
  ✔ SSRF: localhost URL (ruxsat ro'yxatisiz agent) — ulanishdan oldin to'xtatiladi (11.2884ms)
✔ chiquvchi yetkazish (Postgres + lokal qabul qiluvchi) (3221.3033ms)
ℹ tests 7
ℹ suites 1
ℹ pass 7
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 7927.0156

Nima bo'ldi? Ikkala imzo testi o'tdi: bizning paketsiz imzola rasmiy paket bilan bir xil imzo beradi, buzilgan so'rovlarning har biri o'z sababi bilan rad etiladi. Qolgan beshta test — chiquvchi tomonniki, ularni keyingi darsda ochamiz.

Commit xabari:

bash
M='feat(webhook): Standard Webhooks imzosi — tekshirish,'
M+=' vaqt oynasi, kalit almashtirish'
git commit -m "$M"

12. Real ishda

  • Svix, Hookdeck — webhook'larni qabul qilish va yuborish xizmatlari; Standard Webhooks spetsifikatsiyasi Svix tajribasidan chiqqan. Kanon ularni sinamagan.
  • GitHub, Shopify, Telegram — har biri o'z formati: GitHub X-Hub-Signature-256: sha256=<hex>, Telegram bot webhook'ida — secret_token sarlavhasi (imzo emas, maxfiy so'z). Har birini hujjatdan o'qing; tamoyillar shu darsdagi.
  • Lokal sinash — tunnel (ngrok, Cloudflare Tunnel) yoki provayder CLI'si; kanonda — test ichida imzolangan so'rovlar, tarmoqsiz.
  • Intervyuda: "Webhook'ni qanday himoyalaysiz?", "Takror webhook'ni qanday aniqlaysiz?", "Webhook tartibiga tayansa bo'ladimi?"

Xulosa

  • Webhook URL'i ommaviy — har so'rov tekshiriladi: imzo, vaqt, takror.
  • Standard Webhooks: webhook-id, webhook-timestamp, webhook-signature: v1,<base64>; HMAC-SHA256 "id.timestamp.tana", sir whsec_ + base64.
  • Imzo xom tana ustida, timingSafeEqual bilan; bir nechta imzo — kalit almashtirish.
  • Vaqt oynasi ±300 s — replay himoyasi.
  • Dedup — hodisa id'si noyob kalit bilan, ishlov bilan bitta tranzaksiyada; takrorga 200.
  • Tez 200, og'ir ish navbatda; ichki xatoda 500; tartibga tayanmang.

Keyingi dars: Chiquvchi webhook'larni yuborish — endi biz yuboruvchimiz: yetkazish jurnali, qayta urinish jadvali, DLQ va SSRF himoyasi.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!