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

Telegram Mini App backendi va Stars: initData va to'lov oqimi

Qisqacha: Mini App — Telegram ichida ochiladigan web sahifa. U serverga foydalanuvchi haqida initData yuboradi; server unga ishonishdan oldin imzoni tekshiradi: secret = HMAC_SHA256("WebAppData", bot_token), hash = HMAC_SHA256(secret, data_check_string). Imzo to'g'ri va auth_date yangi bo'lsa — foydalanuvchi haqiqiy. Raqamli tovar uchun to'lov — Telegram Stars (XTR): provider_token bo'sh, narx bitta; pre_checkout_query ga 10 soniya ichida javob; successful_payment takror kelsa — telegram_payment_charge_id bo'yicha e'tiborsiz qoldiring.

Bu darsda

  • Mini App va bot qanday bog'lanishini tushunasiz.
  • initData imzosini Web Crypto bilan o'zingiz hisoblaysiz va soxtasini aniqlaysiz.
  • Imzo tekshiruvidan tashqari auth_date, timingSafeEqual va Express middleware'ni yozasiz.
  • answerWebAppQuery va web_app tugmasi haqida bilasiz.
  • Telegram Stars bilan hisob-faktura, tekshiruv, idempotent to'lov va refund oqimini kanon tajribasida ko'rasiz.
  • Loyiha uchun miniapp.ts modulini va uning 4 ta testini yozasiz.

Oldin bilishingiz kerak: Bot prodakshnda, crypto 1: tasodif, hash va HMAC, Middleware va request lifecycle, Kiruvchi webhook'larni qabul qilish g'oyasi (imzo, idempotentlik) — keyinroq to'liq.

Telegram Login va Mini App autentifikatsiyasi 28-qismda Telegram Login va Mini App initData tekshiruvi darsida auth tomonidan ko'riladi. Bugun — backend amaliyoti va to'lov.

1. Nega bu kerak?

Bot interfeysi — tugma va matn. Ba'zi ishlar uchun bu tor: vazifalar kalendari, filtrli ro'yxat, rasm muharriri. Mini App (avval Web App deyilgan) — oddiy web sahifa, lekin Telegram ichida, foydalanuvchi tizimga kirmasdan ochiladi.

"Kirmasdan" — eng qiziq joyi. Sahifa Telegram'dan foydalanuvchi ma'lumotini oladi: id, ism, til. Lekin sahifa — brauzer, brauzerdagi hamma narsani foydalanuvchi o'zgartira oladi. Hujumchi sahifa o'rniga o'z so'rovini yuboradi: "men — foydalanuvchi 1 (admin)". Server buni qanday ajratadi?

Javob — imzo. Telegram ma'lumotni bot tokeni asosidagi kalit bilan imzolaydi. Tokenni faqat Telegram va sizning serveringiz biladi. Imzo mos kelsa — ma'lumotni Telegram bergan, hech kim o'zgartirmagan.

O'xshatish: muhrli ma'lumotnoma. Odam "men shu tashkilot xodimiman" desa, ishonmaysiz. Muhrli ma'lumotnoma ko'rsatsa va muhr haqiqiy bo'lsa — ishonasiz. Muhrni soxtalashtirib bo'lmaydi, chunki muhr faqat tashkilotda. Lekin ma'lumotnoma sanasiga ham qaraysiz: o'tgan yilgi ma'lumotnoma yaroqsiz.

2. Mini App va bot

2.1 Qanday ochiladi

Mini App bir necha joydan ochiladi: inline tugma (web_app), reply klaviatura tugmasi, bot menyusi tugmasi, havola (hujjat bo'yicha). grammY'da inline tugma:

js
import { InlineKeyboard } from "grammy";

await ctx.reply("Vazifalar kalendari:", {
  reply_markup: new InlineKeyboard()
    .webApp("Ochish", "https://example.com/miniapp"),
});

Sahifa Telegram'ning telegram-web-app.js skriptini yuklaydi va window.Telegram.WebApp.initData dan satrni oladi. Shu satr serverga yuboriladi.

2.2 initData tuzilishi

initData — URL so'rov qatori (query string). Maydonlar (hujjat bo'yicha, asosiylari):

Maydon Nima
user foydalanuvchi, JSON: id, first_name, language_code …
auth_date Mini App ochilgan vaqt, Unix soniya
query_id answerWebAppQuery uchun (inline tugmadan ochilganda)
hash imzo, 64 ta hex belgi
signature uchinchi tomon uchun Ed25519 imzo

2.3 Bot va Mini App aloqasi

Mini App botga ma'lumotni uch yo'l bilan qaytaradi:

  1. Serveringizga fetch — eng ko'p ishlatiladigan yo'l: initData bilan so'rov, server tekshiradi va javob beradi.
  2. answerWebAppQuery — hujjat: Web App bilan o'zaro ta'sir natijasini o'rnatadi va so'rov kelgan chatga foydalanuvchi nomidan xabar yuboradi. Server initData dagi query_id bilan chaqiradi. Hujjatga ko'ra web_app inline tugmasi faqat bot bilan shaxsiy chatda mavjud.
  3. Telegram.WebApp.sendData — faqat reply klaviatura tugmasidan ochilganda; bot web_app_data xizmat xabarini oladi.

Kanon mock'ida answerWebAppQuery bor, lekin u alohida sinalmagan. Bu darsda birinchi yo'l asosiy.

3. initData imzosi

3.1 Algoritm

Telegram hujjati (2026-10-28 da o'qildi):

  1. data_check_string — hash dan boshqa barcha maydonlar, kalit bo'yicha alifbo tartibida, kalit=qiymat ko'rinishida, orasida yangi qator (\n).
  2. secret_key = HMAC-SHA256, kalit — "WebAppData" satri, ma'lumot — bot tokeni.
  3. hex(HMAC-SHA256(kalit = secret_key, ma'lumot = data_check_string)) hash ga teng bo'lsa — ma'lumot Telegram'dan.

E'tibor bering: 2-qadamda kalit — doimiy "WebAppData", token esa ma'lumot. Ko'p xato aynan shu yerda — ikkalasini almashtirib qo'yish.

HMAC — crypto 1 darsida ko'rgansiz: kalitli hash. Kalitni bilmasdan to'g'ri HMAC yasab bo'lmaydi.

3.2 Brauzerda o'zingiz hisoblang

Web Crypto (crypto.subtle) bilan butun algoritm — Node'da ham, brauzerda ham ishlaydi. Token — soxta, faqat namuna uchun:

js
const enc = new TextEncoder();
const hex = (b) => [...new Uint8Array(b)]
  .map((x) => x.toString(16).padStart(2, "0")).join("");

async function hmac(kalit, matn) {
  const k = await crypto.subtle.importKey(
    "raw", typeof kalit === "string" ? enc.encode(kalit) : kalit,
    { name: "HMAC", hash: "SHA-256" }, false, ["sign"],
  );
  return crypto.subtle.sign("HMAC", k, enc.encode(matn));
}

const tekshiruvSatri = (p) => [...p.entries()]
  .filter(([k]) => k !== "hash")
  .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
  .map(([k, v]) => `${k}=${v}`)
  .join("\n");

async function imzola(maydonlar, token) {
  const p = new URLSearchParams(maydonlar);
  const sir = await hmac("WebAppData", token);
  p.set("hash", hex(await hmac(sir, tekshiruvSatri(p))));
  return p.toString();
}

async function tekshir(initData, token) {
  const p = new URLSearchParams(initData);
  const sir = await hmac("WebAppData", token);
  const kutilgan = hex(await hmac(sir, tekshiruvSatri(p)));
  return kutilgan === p.get("hash");
}

const TOKEN = "123456:SOXTA-faqat-lokal-mock-uchun";
const user = JSON.stringify({ id: 501, first_name: "Aziz" });
const init = await imzola(
  { query_id: "AAH-soxta", user, auth_date: "1793095140" }, TOKEN,
);
console.log(new URLSearchParams(init).get("hash"));
console.log(tekshiruvSatri(new URLSearchParams(init)));
console.log(await tekshir(init, TOKEN));
const p = new URLSearchParams(init);
p.set("user", JSON.stringify({ id: 1, first_name: "Admin" }));
console.log(await tekshir(p.toString(), TOKEN));
console.log(await tekshir(init, "999:BOSHQA"));
const teskari = new URLSearchParams(
  [...new URLSearchParams(init)].reverse(),
);
console.log(await tekshir(teskari.toString(), TOKEN));

Konsolda:

text
646e75bb3783235b588c4dde49befd4f5ae022b7b1d5ed66c13c11abdd9f3f8a
auth_date=1793095140
query_id=AAH-soxta
user={"id":501,"first_name":"Aziz"}
true
false
false
true

Qatorma-qator:

  1. hash — 64 ta hex belgi (32 bayt). Kirish bir xil bo'lsa — har doim shu qiymat. Biz uni kanon modulining Node createHmac versiyasi bilan solishtirdik: bir xil chiqdi.
  2. data_check_string — uch qator, alifbo tartibida (auth_date, query_id, user). user — URL-kodlangan emas, ochilgan JSON.
  3. To'g'ri imzo — true.
  4. Hujumchi user ni "Admin" ga almashtirdi — false. hash ni qayta hisoblay olmaydi: token yo'q.
  5. Boshqa bot tokeni — false. Bitta botning initData si boshqa bot serverida o'tmaydi.
  6. Maydonlar tartibi teskari — true. Tartib tekshiruv satrida alifbo bo'yicha tiklanadi.

Tekshirib ko'ring: Nega tekshiruv satri URLSearchParams qiymatlaridan (user={"id":501,…}) yasaladi, xom satr bo'lagidan (user=%7B%22id%22…) emas?

Javob

Hujjat kalit=<qiymat> deydi — ochilgan qiymat. Telegram imzoni ochilgan qiymatlar ustidan hisoblaydi. Mijoz satrni boshqacha kodlashi mumkin (%20 yoki +). Ochilgan qiymat ustidan tekshirsangiz, kodlash farqi imzoni buzmaydi. Kanon testi buni ham tekshiradi: "maydonlar tartibi va URL-kodlash imzoga ta'sir qilmaydi".

3.3 Imzodan keyingi tekshiruvlar

To'g'ri imzo — yetarli emas.

  • auth_date — eskirganmi? Hujjat qo'shimcha tekshiruvni tavsiya qiladi. Imzo vaqt o'tsa ham to'g'ri qoladi. Kimdir bir marta foydalanuvchining initData sini qo'lga kiritsa (log, ekran yozuvi), uni abadiy qayta ishlatadi — takroriy hujum (replay). Kanon modul 1 soatdan eskisini rad etadi.
  • Kelajak vaqt. auth_date hozirdan 60 s dan ko'p oldinda bo'lsa — soat sozlanmagan yoki soxta. Kanon kelajak sababini beradi.
  • hash formati. Faqat ^[0-9a-f]{64}$ — aks holda Buffer.from(hash, "hex") qisqa bufer beradi.
  • Doimiy vaqtli solishtirish. === birinchi farqli belgida to'xtaydi; vaqtni o'lchab, imzoni bayt-bayt topish nazariy jihatdan mumkin. Node'da timingSafeEqual (crypto 2). Bizning Web Crypto misolimiz soddalik uchun === ishlatdi — serverda kanon moduli kabi yozing.
  • user JSON. JSON.parse xato bersa yoki id butun son bo'lmasa — format.

3.4 Uchinchi tomon: Ed25519 signature

Hujjatda ikkinchi usul bor: ma'lumotni bot tokenisiz tekshirish (masalan, boshqa xizmat). Buning uchun signature maydoni — Ed25519 imzo, Telegram'ning ochiq kaliti bilan tekshiriladi; tekshiruv satri bot_id:WebAppData qatoridan boshlanadi. Bu kanonda sinalmagan — hujjat bo'yicha. O'z serveringizda tokeningiz bor, HMAC usuli yetarli.

4. Sessiya: keyingi so'rovlar

Har so'rovda initData ni yuborib tekshirish mumkin. Mini App hamjamiyati hujjatlarida Authorization: tma <initData> sarlavhasi uchraydi — kanon modul shu shaklni oladi. Bu Telegram'ning rasmiy hujjatida yo'q (biz 2026-10-28 da core.telegram.org/bots/webapps da qidirdik — topilmadi); bu shunchaki kelishuv.

Ikkinchi yo'l — initData ni bir marta tekshirib, o'z sessiyangizni berish: cookie yoki token. Bu 28-qismdagi auth mavzusi (Session-based auth, JWT anatomiyasi). Muhimi tartib:

  1. initData imzosi va auth_date tekshiriladi.
  2. user.id bo'yicha bazadan foydalanuvchi topiladi (yoki yaratiladi).
  3. O'z sessiyangiz beriladi.

Shundan keyin user.id — ishonchli. Mijoz yuborgan boshqa userId maydoniga hech qachon ishonmang.

sequenceDiagram
  participant M as Mini App (brauzer)
  participant S as Server
  participant D as Baza
  M->>S: GET /me, Authorization tma initData
  S->>S: hash, auth_date tekshiruvi
  alt imzo yoki vaqt noto'g'ri
    S-->>M: 401 Problem Details
  else to'g'ri
    S->>D: user.id bo'yicha foydalanuvchi
    D-->>S: foydalanuvchi
    S-->>M: 200 ma'lumot (yoki sessiya)
  end

Diagrammaga qarang: server initData ni tekshirmaguncha bazaga bormaydi. 401 javobi — Problem Details formatida.

5. Telegram Stars

5.1 Nima va qachon

Telegram Stars — Telegram ichidagi virtual valyuta, kodi XTR. Telegram hujjati (Bot Payments API for Digital Goods, 2026-10-28): raqamli tovar va xizmatlar Stars bilan sotiladi. Jismoniy tovar — boshqa sahifa, to'lov provayderlari orqali.

vazifalar misolida "Premium — 30 kun" (cheksiz eslatmalar) — raqamli xizmat, demak Stars. Do'kondagi non — jismoniy, u O'zbekiston to'lov tizimlari orqali (Payme Merchant API, Click integratsiyasi).

Stars'ni pulga aylantirish, komissiya va soliq — hujjat bo'yicha, kanonda sinalmagan. Haqiqiy Stars to'lovi ham sinalmagan: kanon lokal mock'da oqimni taqlid qildi.

5.2 Oqim

Hujjatdagi qadamlar:

  1. sendInvoice (yoki createInvoiceLink) — currency: "XTR".
  2. pre_checkout_query update'ini kutish.
  3. answerPreCheckoutQuery bilan tasdiqlash yoki rad etish — 10 soniya ichida.
  4. successful_payment xabarini kutish.
  5. telegram_payment_charge_id ni saqlash — refund uchun kerak.
  6. Tovarni yetkazish.
sequenceDiagram
  participant F as Foydalanuvchi
  participant T as Telegram
  participant B as Bot
  B->>T: sendInvoice (XTR, 50, payload)
  F->>T: "To'lash"
  T->>B: pre_checkout_query
  B->>B: buyurtma bor, egasi, summa, holat?
  B->>T: answerPreCheckoutQuery (ok yoki sabab)
  T->>T: Stars yechiladi
  T->>B: successful_payment (charge_id)
  B->>B: charge_id yangi? → Premium yoqiladi

Diagrammaga qarang: pre_checkout_query paytida pul hali yechilmagan. Bu oxirgi tekshiruv nuqtasi. Pul successful_payment dan oldin yechiladi va tovar faqat shundan keyin beriladi.

5.3 Invoice

js
bot.command("premium", (ctx) => ctx.replyWithInvoice(
  "Premium — 30 kun", "Cheksiz eslatmalar va fayllar",
  "prem-501-1", "XTR",
  [{ label: "Premium", amount: 50 }],
  { provider_token: "" },
));

payload (prem-501-1) — sizning buyurtma id'ngiz: u keyingi update'larda qaytadi. Hujjat: Stars uchun provider_token — bo'sh satr, prices — aynan bitta element. Kanon mock'i shu qoidalarni tekshirdi:

text
sendInvoice: {"title":"Premium — 30 kun","currency":"XTR","total_amount":50,"payload":"prem-501-1"}
XTR + provider_token: Bad Request: provider_token must be empty for Telegram Stars
XTR + 2 narx: Bad Request: exactly one price must be specified for Telegram Stars

Xato matnlari — mock'niki (haqiqiy server matni solishtirilmagan); qoidalar — hujjatdagi.

5.4 pre_checkout_query: tekshiruv

Bu bosqichda bot buyurtmani bazadan tekshiradi. Kanon tekshiruvi — toza mantiq, uni alohida funksiya qilamiz:

js
function tekshirTolov(buyurtmalar, q) {
  const b = buyurtmalar.get(q.invoice_payload);
  if (!b) return "Buyurtma topilmadi";
  if (b.foydalanuvchi !== q.from.id) return "Begona buyurtma";
  if (b.holat !== "kutilmoqda") return "Buyurtma faol emas";
  if (b.summa !== q.total_amount || q.currency !== "XTR") {
    return "Summa mos emas";
  }
  return null;
}

const buyurtmalar = new Map([
  ["prem-501-1",
    { foydalanuvchi: 501, summa: 50, holat: "kutilmoqda" }],
  ["prem-501-2",
    { foydalanuvchi: 501, summa: 50, holat: "bekor" }],
]);
const holatlar = [
  [501, "prem-501-1", 50], [501, "prem-501-1", 1],
  [777, "prem-501-1", 50], [501, "prem-501-2", 50],
  [501, "yoq", 50],
];
for (const [kim, payload, summa] of holatlar) {
  const q = {
    from: { id: kim }, invoice_payload: payload,
    total_amount: summa, currency: "XTR",
  };
  const xato = tekshirTolov(buyurtmalar, q);
  console.log(`${kim} ${payload} ${summa} → ${xato ?? "ok"}`);
}

Konsolda:

text
501 prem-501-1 50 → ok
501 prem-501-1 1 → Summa mos emas
777 prem-501-1 50 → Begona buyurtma
501 prem-501-2 50 → Buyurtma faol emas
501 yoq 50 → Buyurtma topilmadi

Kurs tajribasi haqiqiy grammY va mock bilan xuddi shu besh holatni berdi:

text
chat 501, prem-501-1, 50 XTR → ok=true
chat 501, prem-501-1, 1 XTR → ok=false, "Summa mos emas"
chat 777, prem-501-1, 50 XTR → ok=false, "Begona buyurtma"
chat 501, prem-501-2, 50 XTR → ok=false, "Buyurtma faol emas"
chat 501, yoq, 50 XTR → ok=false, "Buyurtma topilmadi"

Bot rad etganda answerPreCheckoutQuery(false, { error_message }) — sabab foydalanuvchiga ko'rsatiladi. 10 soniya chegarasi sabab bu yerda og'ir ish qilmang: bitta indeksli so'rov — yetarli.

Nega summa ham tekshiriladi? payload ni bot yaratadi, lekin invoice'ni qayta yuborish yoki eski invoice orqali to'lash holatlari bo'lishi mumkin: narx o'zgargan, eski xabar qolgan. Summa va valyuta — bazadagi qiymat bilan solishtiriladi, mijozga ishonilmaydi.

5.5 successful_payment: idempotentlik

To'lov o'tdi. Telegram successful_payment xabarini yuboradi. Lekin update ikki marta kelishi mumkin — Bot prodakshnda darsida ko'rganingizdek, webhook javobi kechiksa Telegram qayta yuboradi. Ikki marta "Premium yoqildi" — ikki oy bepul Premium.

Himoya — telegram_payment_charge_id bo'yicha idempotentlik: shu id bilan to'lov bazada bormi? Bor bo'lsa — e'tiborsiz. Bazada UNIQUE cheklov (CREATE TABLE va cheklovlar).

js
bot.on("message:successful_payment", async (ctx) => {
  const p = ctx.message.successful_payment;
  if (tolovlar.has(p.telegram_payment_charge_id)) return;
  tolovlar.set(p.telegram_payment_charge_id, p);
  buyurtmalar.get(p.invoice_payload).holat = "tolangan";
  await ctx.reply("Rahmat! Premium yoqildi.");
});

Kurs tajribasi to'lovni yakunladi, keyin o'sha charge_id bilan update'ni qo'lda qayta yubordi:

text
to'landi prem-501-1 50 XTR
takror successful_payment stxSOXTAfc… — e'tiborsiz
buyurtma holati: tolangan | to'lovlar: 1

Bitta to'lov, bitta yozuv. stxSOXTA… — mock yaratgan id, har ishga tushishda boshqa.

Prodakshnda has va set orasida ikki parallel update o'tib ketishi mumkin (poyga holati). Shuning uchun tekshiruvni bazaning o'ziga bering: INSERT … ON CONFLICT DO NOTHING va qo'shilgan qatorlar soniga qarab qaror (UPSERT va MERGE).

5.6 Refund

Hujjat: refundStarPayment — Stars'dagi muvaffaqiyatli to'lovni qaytaradi; unga user_id va telegram_payment_charge_id kerak. Stars sotadigan bot /paysupport buyrug'iga javob berishi kerak (hujjat: to'lov muammolari bo'yicha murojaatlar).

text
refundStarPayment: true
bo'sh charge_id: Bad Request: CHARGE_ID_INVALID

Kurs tajribasi refund qildi va bo'sh id bilan xatoni ko'rdi. Refunddan keyin buyurtma holatini o'zingiz o'zgartiring (Premium o'chadi) — Telegram buni siz uchun qilmaydi. Moliyaviy hisob haqida — Refund, reconciliation va moliyaviy hisob.

Tekshirib ko'ring: Nega Premium'ni pre_checkout_query da emas, successful_payment da yoqamiz?

Javob

pre_checkout_query — "to'lashga tayyormisiz?" savoli. Undan keyin ham to'lov bajarilmasligi mumkin (Stars yetmaydi, foydalanuvchi bekor qildi). Pul yechilgani faqat successful_payment bilan ma'lum. Oldin yoqsangiz — tekin Premium.

6. Hujumchi nigohi

Hujum Himoya Kanon tekshiruvi
user ni o'zgartirib yuborish HMAC imzo test: imzo
boshqa botning initData si token bo'yicha kalit test: 999:BOSHQA → imzo
eski initData ni qayta ishlatish auth_date ≤ 1 soat test: eskirgan
kelajak auth_date 60 s chegara test: kelajak
hash siz so'rov 401 test: hash-yoq
begona buyurtmani to'lash from.id = egasi kurs tajribasi: "Begona buyurtma"
summani 1 Star qilish bazadagi summa kurs tajribasi: "Summa mos emas"
takroriy successful_payment charge_id UNIQUE kurs tajribasi: "e'tiborsiz"

Bot tokeni — ham bot API kaliti, ham initData imzosi kaliti. U oqsa, hujumchi istalgan foydalanuvchi nomidan initData yasaydi. Token faqat serverda.

7. Ko'p uchraydigan xatolar

Xato Natija To'g'risi
HMAC(token, "WebAppData") — kalit va ma'lumot almashgan imzo hech qachon mos kelmaydi kalit "WebAppData", ma'lumot token
initDataUnsafe dagi user ga ishonish soxta foydalanuvchi server initData ni tekshiradi
xom (kodlangan) qiymatlardan tekshiruv satri imzo mos kelmaydi ochilgan qiymatlar
auth_date tekshirilmaydi takroriy hujum maksimal yosh
=== bilan solishtirish (serverda) vaqt orqali sizish timingSafeEqual
provider_token Stars'da 400 bo'sh satr
Premium pre_checkout da tekin Premium successful_payment da
charge_id saqlanmaydi takror to'lov ikki marta; refund qilib bo'lmaydi UNIQUE yozuv

8. Mashqlar

Mashqlar kurs/mashqlar/29/26-miniapp/ da.

1-mashq (oson): tekshiruv satri

initData: user=%7B%22id%22%3A7%7D&query_id=Q1&auth_date=1700000000&hash=ab12. data_check_string ni qo'lda yozing (qatorlar tartibi va ko'rinishi).

Yechim
text
auth_date=1700000000
query_id=Q1
user={"id":7}

hash chiqarib tashlanadi, qolganlari alifbo bo'yicha, user qiymati URL-kodlashdan ochilgan.

2-mashq (o'rta): auth_date tekshiruvi

vaqtniTekshir(authDate, hozirSoniya, maks = 3600) funksiyasini yozing: butun son bo'lmasa "format", hozirdan 60 s dan ko'p oldinda bo'lsa "kelajak", maks dan eski bo'lsa "eskirgan", aks holda "ok". Hozir — 1793095200. Sinang: "1793095140", "1793091000", "1793095300", "abc", "1793095259". Ishora: Number.isInteger (Matn ↔ son va sonni tekshirish).

Yechim
js
function vaqtniTekshir(authDate, hozir, maks = 3600) {
  const a = Number(authDate);
  if (!Number.isInteger(a)) return "format";
  if (a > hozir + 60) return "kelajak";
  if (hozir - a > maks) return "eskirgan";
  return "ok";
}

const hozir = 1793095200;
for (const a of ["1793095140", "1793091000", "1793095300",
  "abc", "1793095259"]) {
  console.log(a, vaqtniTekshir(a, hozir));
}
text
1793095140 ok
1793091000 eskirgan
1793095300 kelajak
abc format
1793095259 ok

1793091000 — 4 200 s oldin, 1 soatdan ko'p. 1793095259 — 59 s oldinda, soatlar farqi uchun ruxsat.

3-mashq (qiyin): idempotent to'lov qabul qiluvchi

tolovQabul(holat, p) yozing: holat — { tolovlar: Map, buyurtmalar: Map }, p — { invoice_payload, total_amount, telegram_payment_charge_id }. Qaytarsin: "takror" (charge_id bor), "noma'lum buyurtma", "summa mos emas" (pul olindi, lekin summa farq — qo'lda ko'rib chiqish), yoki "yoqildi" (to'lov yoziladi, buyurtma tolangan). To'rt update: to'g'ri, o'sha takror, boshqa charge_id bilan noma'lum payload, summasi farqli. Oxirida to'lovlar sonini chiqaring.

Yechim
js
function tolovQabul(holat, p) {
  const id = p.telegram_payment_charge_id;
  if (holat.tolovlar.has(id)) return "takror";
  const b = holat.buyurtmalar.get(p.invoice_payload);
  holat.tolovlar.set(id, p);
  if (!b) return "noma'lum buyurtma";
  if (b.summa !== p.total_amount) return "summa mos emas";
  b.holat = "tolangan";
  return "yoqildi";
}

const holat = {
  tolovlar: new Map(),
  buyurtmalar: new Map([
    ["prem-1", { summa: 50, holat: "kutilmoqda" }],
    ["prem-2", { summa: 50, holat: "kutilmoqda" }],
  ]),
};
const updatelar = [
  { invoice_payload: "prem-1", total_amount: 50,
    telegram_payment_charge_id: "c1" },
  { invoice_payload: "prem-1", total_amount: 50,
    telegram_payment_charge_id: "c1" },
  { invoice_payload: "yoq", total_amount: 50,
    telegram_payment_charge_id: "c2" },
  { invoice_payload: "prem-2", total_amount: 5,
    telegram_payment_charge_id: "c3" },
];
for (const u of updatelar) console.log(tolovQabul(holat, u));
console.log("to'lovlar:", holat.tolovlar.size);
text
yoqildi
takror
noma'lum buyurtma
summa mos emas
to'lovlar: 3

Muhim qaror: noma'lum va summasi farqli to'lov ham yoziladi — pul allaqachon olingan, u yo'qolmasligi kerak (refund yoki qo'lda hal qilish uchun charge_id kerak). Faqat takror yozilmaydi.

9. Loyiha qadami: miniapp.ts moduli

Kanon modullar/src/telegram/miniapp.ts — initData tekshiruvi va Express middleware (node:crypto, Express 5.3.0, TypeScript 6.0.3). Telefon uchun formatladik va izohlarni qisqartirdik; kod ma'nosi kanondagidek, nusxa tsc, eslint va testlardan o'tdi (biz tekshirdik):

ts
// miniapp.ts — Telegram Mini App backend: `initData` imzosini
// tekshirish (core.telegram.org/bots/webapps):
//   secret = HMAC_SHA256(key = "WebAppData", data = bot_token);
//   hash = hex(HMAC_SHA256(key = secret, data = data_check_string)),
//   data_check_string — `hash` dan boshqa barcha maydonlar, kalit
//   bo'yicha alifbo tartibida, "kalit=qiymat", "\n" bilan.
// Haqiqiy bot tokeni bilan SINALMAGAN — soxta token bilan imzolangan.
import { createHmac, timingSafeEqual } from "node:crypto";
import type { NextFunction, Request, Response } from "express";

export type MiniAppFoydalanuvchi = {
  id: number; first_name: string; last_name?: string;
  username?: string; language_code?: string;
};
export type InitNatija =
  | {
      ok: true; foydalanuvchi: MiniAppFoydalanuvchi; authDate: Date;
      maydonlar: Record<string, string>;
    }
  | {
      ok: false;
      sabab: "hash-yoq" | "imzo" | "eskirgan" | "kelajak"
        | "foydalanuvchi-yoq" | "format";
    };

const dataCheckString = (p: URLSearchParams) =>
  [...p.entries()]
    .filter(([k]) => k !== "hash")
    .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
    .map(([k, v]) => `${k}=${v}`)
    .join("\n");

const secretKey = (botToken: string) =>
  createHmac("sha256", "WebAppData").update(botToken).digest();

export function initDataTekshir(
  initData: string,
  botToken: string,
  o: { maksYoshSoniya?: number; hozir?: Date } = {},
): InitNatija {
  let p: URLSearchParams;
  try {
    p = new URLSearchParams(initData);
  } catch {
    return { ok: false, sabab: "format" };
  }
  const hash = p.get("hash");
  if (!hash || !/^[0-9a-f]{64}$/.test(hash)) {
    return { ok: false, sabab: "hash-yoq" };
  }
  const kutilgan = createHmac("sha256", secretKey(botToken))
    .update(dataCheckString(p)).digest();
  if (!timingSafeEqual(kutilgan, Buffer.from(hash, "hex"))) {
    return { ok: false, sabab: "imzo" };
  }
  const auth = Number(p.get("auth_date"));
  const hozir = (o.hozir ?? new Date()).getTime() / 1000;
  if (!Number.isInteger(auth)) return { ok: false, sabab: "format" };
  if (auth > hozir + 60) return { ok: false, sabab: "kelajak" };
  // eski initData — takroriy hujum (replay) oynasi
  if (hozir - auth > (o.maksYoshSoniya ?? 3600)) {
    return { ok: false, sabab: "eskirgan" };
  }
  const u = p.get("user");
  if (!u) return { ok: false, sabab: "foydalanuvchi-yoq" };
  let foydalanuvchi: MiniAppFoydalanuvchi;
  try {
    foydalanuvchi = JSON.parse(u) as MiniAppFoydalanuvchi;
  } catch {
    return { ok: false, sabab: "format" };
  }
  if (!Number.isSafeInteger(foydalanuvchi.id)) {
    return { ok: false, sabab: "format" };
  }
  return {
    ok: true, foydalanuvchi, authDate: new Date(auth * 1000),
    maydonlar: Object.fromEntries(p.entries()),
  };
}

/** Test/mock uchun: Telegram qiladigan imzoni o'zimiz qo'yamiz
 * (SOXTA token bilan). */
export function initDataImzola(
  maydonlar: Record<string, string>, botToken: string,
): string {
  const p = new URLSearchParams(maydonlar);
  p.delete("hash");
  p.set("hash", createHmac("sha256", secretKey(botToken))
    .update(dataCheckString(p)).digest("hex"));
  return p.toString();
}

/** Express: `Authorization: tma <initData>` → `res.locals.tg`. */
export function miniAppAuth(botToken: string, maksYoshSoniya = 3600) {
  return (req: Request, res: Response, next: NextFunction): void => {
    const m = /^tma (.+)$/.exec(req.get("authorization") ?? "");
    const n = m?.[1]
      ? initDataTekshir(m[1], botToken, { maksYoshSoniya })
      : ({ ok: false, sabab: "hash-yoq" } as const);
    if (!n.ok) {
      res.status(401).type("application/problem+json").json({
        type: "about:blank", title: "Unauthorized", status: 401,
        detail: `initData: ${n.sabab}`,
      });
      return;
    }
    res.locals["tg"] = n;
    next();
  };
}

Darsdagi Web Crypto misolidan farqlar bor. Solishtirish — timingSafeEqual (misolda === edi), hash formati regex bilan tekshiriladi. Natija — ok bo'yicha discriminated union tur, sabab bilan. timingSafeEqual ikkala bufer uzunligi teng bo'lmasa xato tashlaydi — regex aynan 64 hex (32 bayt) ni kafolatlaydi, shuning uchun bu holat bo'lmaydi.

Kanon izohida tma sarlavhasi "Telegram hujjatidagi tavsiya shakli" deb yozilgan edi. Biz uni rasmiy hujjatda topmadik, shuning uchun darsdagi nusxada bu so'zlarni olib tashladik: tma — hamjamiyat kelishuvi, Telegram talabi emas.

Testlar — modullar/test/miniapp.test.ts, to'rtta. Imzolangan initData ni initDataImzola soxta token bilan yasaydi. Parcha (formatlangan):

ts
test("o'zgartirilgan user, boshqa token, hash'siz, eski, kelajak",
  () => {
    const p = new URLSearchParams(toza);
    p.set("user", JSON.stringify({ id: 1, first_name: "Admin" }));
    assert.deepEqual(initDataTekshir(p.toString(), TOKEN, { hozir }),
      { ok: false, sabab: "imzo" });
    assert.deepEqual(initDataTekshir(toza, "999:BOSHQA", { hozir }),
      { ok: false, sabab: "imzo" });
    // ... hash'siz → hash-yoq; 2 soatdan keyin → eskirgan;
    // auth_date + 1 soat → kelajak
  });

Testlarni biz ishga tushirdik (kanon nusxasi, Node 24.21.0):

bash
node --test test/miniapp.test.ts
text
✔ to'g'ri imzo — foydalanuvchi olinadi (2.4341ms)
✔ o'zgartirilgan user, boshqa token, hash'siz, eski, kelajak (17.7159ms)
✔ maydonlar tartibi va URL-kodlash imzoga ta'sir qilmaydi (qiymatlar decode qilinib tekshiriladi) (6.7875ms)
✔ Express middleware: Authorization: tma … (203.9404ms)
ℹ tests 4
ℹ suites 0
ℹ pass 4
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 889.4201

Millisekundlar har safar boshqacha. To'rtinchi test haqiqiy Express serverni listen(0) bilan ochadi: to'g'ri sarlavha — 200 va id: 501; bitta harfi o'zgargan initData — 401 va application/problem+json; charset=utf-8.

Stars oqimi modulga kirmadi: u kurs tajribasi. To'lovlar tolov/ alohida modulida va 38-qism bozor loyihasida ishlatiladi; vazifalar da Premium yo'q.

Commit xabari:

bash
git commit -m "feat(telegram): Mini App initData tekshiruvi"

10. Real ishda

  • Telegram ichidagi do'konlar, o'yinlar, bron qilish xizmatlari — Mini App; backend har so'rovni initData yoki undan olingan sessiya bilan tekshiradi.
  • Raqamli obunalar (kurs, premium funksiya, kontent) Telegram'da Stars bilan sotiladi; jismoniy tovar — mahalliy to'lov tizimlari.
  • Xavfsizlik auditlarida "initData tekshirilmagan, initDataUnsafe ga ishonilgan" — keng tarqalgan topilma.
  • Intervyuda: "Mini App foydalanuvchisini serverda qanday aniqlaysiz?", "To'lov xabari ikki marta kelsa nima qilasiz?"

Xulosa

  • Mini App — Telegram ichidagi web sahifa; initData ga imzo tekshirilmaguncha ishonilmaydi.
  • secret = HMAC("WebAppData", token), hash = HMAC(secret, data_check_string) — ochilgan qiymatlar, alifbo tartibi, \n.
  • Imzodan keyin: auth_date (eskirgan, kelajak), timingSafeEqual, user formati.
  • Stars (XTR): provider_token bo'sh, bitta narx; pre_checkout_query — 10 s ichida tekshiruv.
  • Tovar successful_payment da; telegram_payment_charge_id — idempotentlik va refund kaliti.

Keyingi dars: Pul bilan ishlash JS'da — to'lov tizimlariga o'tishdan oldin summani tiyinda, butun son sifatida saqlashni o'rganamiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

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