Mundarija (18)
- Bu darsda
- 1. Nega bu kerak?
- 2. Standard Webhooks formati
- 3. Tekshiruv: beshta qadam
- 4. Xom tana
- 5. Takrorlar: hodisa id bo'yicha dedup
- 6. Tez 200 va navbat
- 7. Tartib kafolatlanmaydi
- 8. Hujumchi nigohi
- 9. Ko'p uchraydigan xatolar
- 10. Mashqlar
- 1-mashq (oson): qaysi sabab?
- 2-mashq (o'rta): sarlavhadagi imzolar
- 3-mashq (qiyin): xotiradagi qabul qiluvchi
- 11. Loyiha qadami: webhook/imzo.ts
- 12. Real ishda
- Xulosa
- Manbalar
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,
timingSafeEqualbilan)? 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 rasmiystandardwebhooks1.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.tsmodulini 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):
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:
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:
- Sarlavhalar bormi, vaqt raqammi (
^\d{1,12}$). Yo'q —sarlavha. - Vaqt oynasi:
hozir − 300dan eski —eski,hozir + 300dan 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. - Kutilgan imzo — o'zimiz hisoblaymiz, xom tana bilan.
- 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. - Doimiy vaqtli solishtirish —
timingSafeEqual, uzunlik avval.
Kanon kodi (formatlangan, ma'nosi kanondagidek):
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):
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):
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;INSERT 0 1
INSERT 0 0INSERT 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:
- Holat mashinasi:
bekor— yakuniy holat, undan chiqish yo'q (To'lov ekotizimi va umumiy oqim). Kech kelgan "yangilandi" rad etiladi. - Versiya yoki vaqt: har obyektda oxirgi qo'llangan hodisa vaqtini saqlang va eskisini e'tiborsiz qoldiring:
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:
qo'llandi: bekor
e'tiborsiz: yangilandi (eski)
e'tiborsiz: bekor (eski)
bekorUchinchi 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):
webhook-timestamp= hozir − 400.- Sarlavhada
v1,yolgon v2,x v1,<to'g'ri imzo>. webhook-idyo'q.- Tana
{"a":1}imzolangan,{"a":2}keldi. webhook-timestamp= hozir + 600.
Yechim
eski. 2.ok— uchinchi imzo mos keldi,v2o'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
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:
[ '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
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:
200 yangi
200 takror
400 eski
400 kelajak
200 yangi
ishlov: 2Xotiradagi 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):
// 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:
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:
✔ 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.0156Nima 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:
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_tokensarlavhasi (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", sirwhsec_+ base64. - Imzo xom tana ustida,
timingSafeEqualbilan; 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
- Standard Webhooks spetsifikatsiyasi: https://www.standardwebhooks.com, https://github.com/standard-webhooks/standard-webhooks
standardwebhooksnpm paketi: https://www.npmjs.com/package/standardwebhooks- Web Crypto
SubtleCrypto.sign(HMAC): https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/sign - Node
crypto.timingSafeEqual: https://nodejs.org/api/crypto.html#cryptotimingsafeequala-b - Stripe webhook eng yaxshi amaliyotlari: https://docs.stripe.com/webhooks#best-practices
- GitHub webhook imzosi: https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries
- Kanon (IlmHamroh, 2026-10-27):
modullar/src/webhook/imzo.ts, standardwebhooks 1.1.1, Node 24.21.0. Imzolar, SQL va testlar — biz ishga tushirdik, PostgreSQL 17.6 (2026-10-28).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!