IlmHamroh
JavaScript Full-stack/21-qism. React: media, integratsiyalar, PWA23/32-dars22 daqiqa
Mundarija (34)

Telegram Mini Apps auth va to'lov: initData imzosi, startapp, Stars, CloudStorage va production

Qisqacha: Mini App foydalanuvchini o'zi "tanimaydi": u xom initData satrini serverga yuboradi, server esa Telegram imzosini bot tokeni bilan tekshiradi (HMAC-SHA256, kalit — "WebAppData") va auth_date eskirmaganini ko'radi. Shundan keyingina user.id ga ishoniladi. t.me/<bot>/<ilova>?startapp=taom-42 — ilovani kerakli sahifada ochadi (start_param, tekshirib). Raqamli mahsulot uchun to'lov — Telegram Stars: server createInvoiceLink (valyuta XTR), frontend openInvoice, tasdiq — bot serveriga. CloudStorage — foydalanuvchi uchun 1024 tagacha kalit. Lokal sinov — HTTPS tunnel yoki test serveri. Bot va token biz yaratmadik: imzo tekshiruvi Node'da soxta token bilan jonli.

Bu darsda

  • initData imzosini Node'da tekshirasiz: data-check-string, maxfiy kalit, hash, auth_date, soxtalashtirishga urinish.
  • Frontend'dan initData ni sarlavhada yuborib, server sessiyasini olasiz.
  • startapp havolasidan sahifa yo'lini xavfsiz chiqarasiz.
  • Telegram Stars to'lov oqimini (createInvoiceLink → openInvoice → pre_checkout_query → successful_payment) tushuntirasiz.
  • CloudStorage, to'liq ekran va xavfsiz zona, test serveri, HTTPS tunnel va Telegram Login Widget bilan ishlaysiz.

Oldin bilishingiz kerak: Telegram Mini Apps I, To'lov UI, Dev server proxy, HTTPS va CORS, React'da auth holati, URLSearchParams.

1. Nega bu kerak?

O'tgan darsda «Bahor» menyusi Telegram ichida ochildi: ism, tema, ko'k tugma. Endi uchta haqiqiy savol:

  1. Mehmon buyurtma berdi. Server uni kimning nomiga yozadi? initDataUnsafe.user.id ni hujumchi o'zgartira oladi — buni ko'rdik.
  2. Jasur aka kanalga "Bugun manti 10% arzon" postini qo'ydi. Tugma Mini App'ni bosh sahifada emas, aynan manti sahifasida ochishi kerak.
  3. «Bahor» elektron retseptlar kitobini sotmoqchi. Telegram ichida qanday to'lov olinadi?

Bu darsning javoblari: imzo tekshiruvi, startapp havolasi va Telegram Stars. Yana — production'ga chiqish uchun kerakli narsalar: foydalanuvchi sozlamalarini saqlash, to'liq ekran, lokal sinash.

Hayotiy o'xshatish: konvertdagi muhr. Telegram mehmon haqidagi ma'lumotni (initData) konvertga solib, muhr (hash) bosadi. Muhrni faqat Telegram va bot egasi biladigan "pechat" (bot tokeni) bilan yasash mumkin. Konvert mehmonning qo'lidan o'tadi — u ichini o'zgartirsa, muhr mos kelmay qoladi. Server konvertni ochishdan oldin muhrni tekshiradi.

2. initData imzosi

2.1 Imzo qanday yasaladi

Telegram hujjati (core.telegram.org/bots/webapps, "Validating data received via the Mini App") algoritmni shunday beradi:

  1. initData dagi barcha maydonlar (hash dan tashqari) alifbo tartibida, kalit=qiymat ko'rinishida, \n bilan qo'shiladi — bu data-check-string.
  2. Maxfiy kalit: HMAC_SHA256(bot_token, "WebAppData") — "WebAppData" kalit sifatida, bot tokeni — xabar.
  3. hex(HMAC_SHA256(data_check_string, maxfiy_kalit)) — hash ga teng bo'lsa, ma'lumot Telegram'dan.

HMAC (hash-based message authentication code) — xabar va maxfiy kalitdan yasaladigan "muhr". Kalitni bilmasdan to'g'ri HMAC yasash amalda mumkin emas. Kalitni bilgan esa xabarning bitta harfi o'zgarganini ham sezadi. To'liq — crypto: hash va HMAC darsida.

flowchart LR
  A["initData<br/>(hash'siz)"] --> B["tartiblash<br/>kalit=qiymat"]
  T["bot tokeni"] --> K["HMAC('WebAppData',<br/>token)"]
  B --> H["HMAC(maxfiy kalit,<br/>data-check-string)"]
  K --> H
  H --> C{"= hash ?"}
  C -- ha --> OK["ishonamiz"]
  C -- yo'q --> NO["rad"]

Nimaga qarang: bot tokeni faqat serverda ishlatiladi. Brauzerga u hech qachon kelmaydi — aks holda har kim imzo yasay olardi.

2.2 Jonli: Node'da tekshirish

Bu — backend kodi (Node, node:crypto). Token soxta — biz bot yaratmadik. Lekin algoritm haqiqiy: Telegram'ning haqiqiy initData si haqiqiy token bilan xuddi shunday tekshiriladi. To'liq backend — Telegram Login va initData tekshiruvi darsida.

js
import { createHmac, timingSafeEqual } from "node:crypto";

// SOXTA token — faqat dars uchun. Haqiqiysi: process.env.BOT_TOKEN
const BOT_TOKEN = "123456:SOXTA-TOKEN-faqat-dars-uchun";

function sign(fields, token) {
  const dataCheck = Object.keys(fields)
    .sort()
    .map((key) => `${key}=${fields[key]}`)
    .join("\n");
  const secret = createHmac("sha256", "WebAppData")
    .update(token)
    .digest();
  return createHmac("sha256", secret).update(dataCheck).digest("hex");
}

function validate(initData, token, maxAgeSec, now) {
  const params = new URLSearchParams(initData);
  const hash = params.get("hash");
  if (!hash) return "hash yo'q";
  params.delete("hash");
  const fields = Object.fromEntries(params);
  const expected = sign(fields, token);
  const same =
    hash.length === expected.length &&
    timingSafeEqual(Buffer.from(hash), Buffer.from(expected));
  if (!same) return "imzo noto'g'ri";
  if (now - Number(fields.auth_date) > maxAgeSec) return "eskirgan";
  return "to'g'ri: " + JSON.parse(fields.user).first_name;
}

// Telegram'ning ishini taqlid qilamiz: imzolangan initData
const fields = {
  auth_date: "1760000000",
  query_id: "AAH-sinov",
  user: JSON.stringify({ id: 1, first_name: "Aziz" }),
};
const initData = new URLSearchParams({
  ...fields,
  hash: sign(fields, BOT_TOKEN),
}).toString();

const now = 1760000000 + 60; // bir daqiqadan keyin
console.log(validate(initData, BOT_TOKEN, 3600, now));
const forged = initData.replace("Aziz", "Jasur");
console.log(validate(forged, BOT_TOKEN, 3600, now));
console.log(validate(initData, BOT_TOKEN, 3600, now + 86400));
console.log(validate(initData, "999:BOSHQA-BOT", 3600, now));

Konsolda:

text
to'g'ri: Aziz
imzo noto'g'ri
eskirgan
imzo noto'g'ri

To'rt holat:

  1. Asl initData — imzo mos, bir daqiqa oldin ochilgan: ishonamiz.
  2. "Aziz" → "Jasur" — hujumchi ismni (yoki id ni) o'zgartirdi. Imzo endi mos emas.
  3. Bir kundan keyin — imzo to'g'ri, lekin auth_date eski. Kimdir eski initData ni ushlab olib, qayta ishlatmoqda (replay — qayta o'ynatish hujumi). Muddat (bu yerda 1 soat) — sizning qaroringiz.
  4. Boshqa botning tokeni — boshqa botning Mini App'idan olingan to'g'ri initData sizning botingizda o'tmaydi.

Ikki kichik, lekin muhim detal:

  • timingSafeEqual — oddiy === satrlarni birinchi farq qilgan belgigacha solishtiradi: javob vaqti "nechta belgi to'g'ri" ekanini sizdiradi. timingSafeEqual har doim to'liq solishtiradi. Uzunlik oldin tekshiriladi — u teng bo'lmagan buferlarda xato tashlaydi.
  • auth_date — soniyada (Unix vaqti), Date.now() esa millisoniyada. Haqiqiy kodda Math.floor(Date.now() / 1000).

Bot API 8.0 dan initData da signature maydoni ham bor — bot tokenisiz, Telegram'ning ochiq kaliti (Ed25519) bilan tekshirish uchun. U uchinchi tomon (bot egasi bo'lmagan xizmat) uchun. O'z botingizda hash yetarli; signature boshqa maydonlar qatorida data-check-string'ga kiradi.

Tekshirib ko'ring: Dasturchi tezlik uchun imzoni brauzerda tekshirishni xohlaydi: "SubtleCrypto bor, tokenni VITE_BOT_TOKEN ga qo'yamiz". Nima bo'ladi?

Javob

Token build'ga, ya'ni har bir foydalanuvchining brauzeriga tushadi. Hujumchi uni o'qiydi va istalgan initData ga to'g'ri imzo yasaydi — tekshiruv ma'nosini yo'qotadi. Bundan ham yomoni: bot tokeni bilan botning o'zini boshqarish mumkin (xabar yuborish, webhook o'zgartirish). Bot tokeni — faqat serverda, VITE_ prefiksi bilan hech qachon (env o'zgaruvchilar).

3. Frontend: initData bilan kirish

3.1 Sarlavhada yuborish

Frontend'ning vazifasi oddiy: xom initData ni har so'rov bilan (yoki bir marta — sessiya olish uchun) serverga yuborish. Keng tarqalgan kelishuv (@tma.js hujjatidagi) — Authorization sarlavhasi tma sxemasi bilan: Authorization: tma <initData>. Server har so'rovda imzoni tekshiradi yoki birinchi marta tekshirib, o'z sessiya cookie'sini beradi (React'da auth holati).

jsx
import { useEffect, useState } from "react";

// O'quv modeli: Telegram bergan xom satr va soxta server
const initData =
  "user=%7B%22id%22%3A1%2C%22first_name%22%3A%22Aziz%22%7D" +
  "&auth_date=1760000000&hash=…";

function api(path, { headers }) {
  const auth = headers.Authorization;
  console.log(path, "→", auth.slice(0, 15) + "…");
  return new Promise((resolve) =>
    setTimeout(() => {
      // server imzoni tekshirdi (yuqoridagi validate) va javob berdi
      resolve({ ok: true, user: { id: 1, name: "Aziz" } });
    }, 200),
  );
}

function useTelegramSession() {
  const [state, setState] = useState({ status: "loading" });
  useEffect(() => {
    if (!initData) {
      setState({ status: "outside" }); // Telegram'dan tashqarida
      return;
    }
    api("/api/sessiya", {
      headers: { Authorization: `tma ${initData}` },
    }).then((res) =>
      setState(
        res.ok
          ? { status: "ok", user: res.user }
          : { status: "denied" },
      ),
    );
  }, []);
  return state;
}

export default function App() {
  const session = useTelegramSession();
  if (session.status === "loading") return <p>Kirilmoqda…</p>;
  if (session.status === "outside")
    return <p>Ilovani Telegram orqali oching.</p>;
  if (session.status === "denied") return <p>Kirish rad etildi.</p>;
  return (
    <p>Xush kelibsiz, {session.user.name}! (server tasdiqladi)</p>
  );
}

Konsolda:

text
/api/sessiya → tma user=%7B%22…

Ekranda:

text
Xush kelibsiz, Aziz! (server tasdiqladi)

Endi ekrandagi ism — serverning javobidan, initDataUnsafe dan emas. Server buyurtmani ham, ballni ham shu tasdiqlangan id bilan yozadi.

Uchta holat ham ko'zda tutilgan: ichida (ok), tashqarida (outside — oddiy brauzerda ochilgan, initData bo'sh), rad (denied — imzo yoki muddat o'tmadi). Rad holatida "Ilovani qaytadan oching" — initData eskirgan bo'lishi mumkin: mehmon Mini App'ni kecha ochib, yopmagan.

3.2 Uzoq ochiq turgan ilova

auth_date — Mini App ochilgan vaqt. Mehmon ilovani ochib, ikki soat ishlatsa, initData hamon o'sha. Agar server har so'rovda "1 soatdan eski — rad" desa, ikkinchi soatda ilova "buziladi". Odatiy yechim: birinchi so'rovda initData tekshiriladi va server o'z sessiya tokenini (cookie) beradi. Keyingi so'rovlar — sessiya bilan. initData muddati faqat sessiyani ochishda tekshiriladi.

4. startapp: kerakli sahifada ochish

4.1 Havola va parametr

Kanal postidagi havola: https://t.me/<bot>/<ilova>?startapp=taom-42. Telegram Mini App'ni ochadi va startapp qiymatini beradi:

  • WebApp.initDataUnsafe.start_param — "taom-42";
  • manzilda tgWebAppStartParam=taom-42 (hujjat bo'yicha — birinchi chizishdayoq to'g'ri sahifani ko'rsatish uchun).

Rasmiy hujjat (2026-10 holatiga) qiymat uchun belgi va uzunlik chegarasini aniq yozmaydi. Jamoa hujjati (docs.telegram-mini-apps.com) 64 belgigacha va faqat lotin harflari, raqamlar va _ deydi; bot havolasidagi start uchun Bot API - ga ham ruxsat beradi. Xavfsiz yo'l — 64 belgigacha, faqat A-Z a-z 0-9 _ -. Ya'ni /taom/42 kabi yo'lni to'g'ridan yozib bo'lmaydi — o'z "kodlash" ingizni o'ylab topasiz.

4.2 Jonli: parametrdan yo'l

start_param — foydalanuvchi o'zi yozishi mumkin bo'lgan tashqi qiymat (URL'dagi qiymatni tekshirish). Shuning uchun — faqat ma'lum shakllar, qolgani — bosh sahifa:

js
const RULES = [
  // taom-42 → /taom/42
  [/^taom-(\d{1,6})$/, (m) => `/taom/${m[1]}`],
  // chegirma-BAHOR10 → /?kod=BAHOR10
  [/^chegirma-([A-Z0-9]{3,12})$/, (m) => `/?kod=${m[1]}`],
  // bolim-ichimliklar → /bolim/ichimliklar
  [/^bolim-(osh|ichimliklar|shirinlik)$/, (m) => `/bolim/${m[1]}`],
];

function routeFromStartParam(param) {
  if (typeof param !== "string" || param.length > 64) return "/";
  for (const [pattern, toPath] of RULES) {
    const m = param.match(pattern);
    if (m) return toPath(m);
  }
  return "/";
}

for (const p of [
  "taom-42",
  "chegirma-BAHOR10",
  "bolim-ichimliklar",
  "bolim-admin",
  "taom-42/../../admin",
  undefined,
]) {
  console.log(String(p), "→", routeFromStartParam(p));
}

Konsolda:

text
taom-42 → /taom/42
chegirma-BAHOR10 → /?kod=BAHOR10
bolim-ichimliklar → /bolim/ichimliklar
bolim-admin → /
taom-42/../../admin → /
undefined → /
  • Har qoida — ^…$ bilan: butun satr mos kelishi kerak, "ichida bor" yetmaydi (langarlar).
  • Bo'limlar — ro'yxat (osh|ichimliklar|shirinlik), "istalgan so'z" emas: bolim-admin o'tmaydi.
  • Natija — o'zimiz yasagan yo'l. Router'ga start_param ning o'zi berilmaydi.

React Router'da buni birinchi chizishdan oldin qilish qulay: createBrowserRouter ga initialEntries emas, ilova ochilganda bir marta navigate(routeFromStartParam(…), { replace: true }) (dasturiy navigatsiya). replace — "orqaga" bosilganda bo'sh sahifaga qaytmasin.

Bundan tashqari bot o'zi ham havola yasashi mumkin: t.me/<bot>?start=… — bu Mini App emas, bot chati /start buyrug'i bilan ochiladi. Ikkalasini adashtirmang: Mini App uchun — startapp.

5. To'lov: Telegram Stars

5.1 Qaysi to'lov qachon

Telegram ichida ikki xil narsa sotiladi:

  • Raqamli mahsulot va xizmat (elektron kitob, kurs, o'yin ichidagi narsa, premium funksiya) — Telegram'ning "Bot Payments API for Digital Goods" sahifasiga ko'ra (2026-10 holatiga) faqat Telegram Stars (yulduzlar) bilan. Sababi — Apple va Google do'kon qoidalari: Telegram ilovasi ichidagi raqamli savdo ularning to'lov tizimidan o'tishi kerak.
  • Jismoniy mahsulot (osh yetkazib berish) — boshqa to'lov provayderlari bilan: Telegram Payments (BotFather'da ulanadigan provayderlar) yoki o'tgan darsdagi kabi o'z checkout'ingiz.

Qoidalar o'zgarib turadi — loyihadan oldin Telegram'ning "Bot Developers" shartlarini o'qing.

5.2 Stars oqimi

sequenceDiagram
  participant W as Mini App
  participant S as Bot serveri
  participant T as Telegram
  W->>S: "retseptlar kitobi" ni sotib olaman
  S->>T: createInvoiceLink (XTR, 50 yulduz)
  T-->>S: invoice havolasi
  S-->>W: havola
  W->>T: WebApp.openInvoice(havola)
  T->>S: pre_checkout_query
  S->>T: answerPreCheckoutQuery (ok)
  T->>S: successful_payment
  T-->>W: callback("paid")
  W->>S: polling: buyurtma holati?

Nimaga qarang: tanish rasm. Invoice'ni server yaratadi (narx serverda), to'lov tasdig'i serverga keladi (successful_payment), frontend esa faqat oynani ochadi va natijani so'raydi. Bu — o'tgan darsdagi Payme/Click oqimining Telegram varianti.

Backend tomoni (hujjat bo'yicha): createInvoiceLink da currency: "XTR", narx — yulduzlar soni (butun son), provider_token — bo'sh. pre_checkout_query ga bot 10 soniya ichida javob berishi kerak — aks holda to'lov bekor bo'ladi. Qaytarish — refundStarPayment. Bular — Mini App backendi va Stars darsida.

5.3 Jonli: openInvoice

Frontend tomoni — bitta metod va callback. status — "paid", "cancelled", "failed" yoki "pending":

jsx
import { useState } from "react";

// O'quv modeli: Telegram to'lov oynasi o'rnida —
// 200 ms dan keyin "paid"
const WebApp = {
  openInvoice(url, callback) {
    console.log("invoice:", url);
    setTimeout(() => callback("paid"), 200);
  },
};
function createInvoice() {
  return Promise.resolve("https://t.me/$sinov-invoice");
}

const TEXT = {
  idle: "",
  opening: "To'lov oynasi ochilmoqda…",
  paid: "To'landi! Kitob tasdiqlanishi bilan ochiladi.",
  cancelled: "To'lov bekor qilindi.",
  failed: "To'lov o'tmadi. Qayta urinib ko'ring.",
  pending: "To'lov kutilmoqda…",
};

export default function App() {
  const [status, setStatus] = useState("idle");
  async function buy() {
    setStatus("opening");
    const link = await createInvoice(); // serverdan
    WebApp.openInvoice(link, (result) => {
      console.log("natija:", result);
      setStatus(result);
      // "paid" — faqat xabar. Kitobni server tasdig'idan keyin
      // ochamiz (successful_payment → polling)
    });
  }
  return (
    <>
      <button onClick={buy} disabled={status === "opening"}>
        50 ⭐ ga sotib olish
      </button>
      <p role="status">{TEXT[status]}</p>
    </>
  );
}

Konsolda:

text
invoice: https://t.me/$sinov-invoice
natija: paid

Ekranda:

text
50 ⭐ ga sotib olish
To'landi! Kitob tasdiqlanishi bilan ochiladi.

E'tibor bering: "paid" bo'lsa ham kitob darhol ochilmaydi. Callback brauzerda ishlaydi — uni soxtalashtirish oson (DevTools'da callback("paid")). Kitob — server successful_payment ni olgandan keyin, serverdan keladi. O'tgan darsdagi "holat faqat serverdan" qoidasi.

6. CloudStorage: foydalanuvchi sozlamalari

6.1 Nima va qachon

WebApp.CloudStorage — Telegram serverlarida, shu bot va shu foydalanuvchi uchun kichik kalit-qiymat ombori. Afzalligi — mehmon telefondan ham, kompyuterdagi Telegram'dan ham bir xil sozlamani ko'radi (localStorage esa har qurilmada alohida). Cheklovlar (hujjat bo'yicha):

  • 1024 tagacha kalit;
  • kalit — 1–128 belgi, faqat A-Z, a-z, 0-9, _, -;
  • qiymat — satr, 0–4096 belgi.

Mos keladi: tanlangan til, sevimli taomlar, oxirgi manzil. Mos kelmaydi: buyurtmalar tarixi (bu — server bazasi), maxfiy narsa (bu — umuman brauzer emas).

6.2 Jonli: callback'ni Promise qilish

API callback uslubida: setItem(key, value, (error, ok) => …). React'da async/await qulay — o'ramiz (promisify):

jsx
import { useEffect, useState } from "react";

// O'quv modeli: CloudStorage (haqiqiysi — Telegram serverida)
const store = new Map();
const CloudStorage = {
  setItem(key, value, cb) {
    setTimeout(() => {
      if (!/^[A-Za-z0-9_-]{1,128}$/.test(key)) {
        return cb(new Error("KEY_INVALID"));
      }
      store.set(key, value);
      cb(null, true);
    }, 30);
  },
  getItem(key, cb) {
    setTimeout(() => cb(null, store.get(key) ?? ""), 30);
  },
};

// (xato, natija) callback'ini Promise'ga aylantiradi
const settle = (resolve, reject) => (e, v) =>
  e ? reject(e) : resolve(v);
const cloud = {
  get: (key) =>
    new Promise((res, rej) =>
      CloudStorage.getItem(key, settle(res, rej)),
    ),
  set: (key, value) =>
    new Promise((res, rej) =>
      CloudStorage.setItem(key, value, settle(res, rej)),
    ),
};

export default function App() {
  const [favorites, setFavorites] = useState([]);
  useEffect(() => {
    cloud
      .get("sevimli")
      .then((v) => setFavorites(v ? v.split(",") : []));
  }, []);
  async function add() {
    const next = [...favorites, "manti"];
    await cloud.set("sevimli", next.join(","));
    setFavorites(next);
    try {
      await cloud.set("sevimli taomlar", "x"); // bo'sh joy — taqiq
    } catch (e) {
      console.log("xato:", e.message);
    }
  }
  return (
    <>
      <p>Sevimlilar: {favorites.join(", ") || "yo'q"}</p>
      <button onClick={add}>Sevimliga qo'shish</button>
    </>
  );
}

Konsolda:

text
xato: KEY_INVALID

Ekranda:

text
Sevimlilar: manti
Sevimliga qo'shish

Qiymat — faqat satr, shuning uchun ro'yxat join(",") bilan saqlandi (murakkabroq ma'lumot — JSON.stringify, lekin 4096 belgi chegarasini unutmang). Kalitda bo'sh joy — xato. Bizning mock xato matnini o'ylab topdi; haqiqiy SDK'ning xato matnini hujjat aniq bermaydi — xatoni ushlang, matniga tayanmang.

Bot API 9.0 dan yana ikki ombor bor: DeviceStorage (qurilmada, katta hajm) va SecureStorage (qurilmaning himoyalangan xotirasi — token kabi narsalar uchun, kam hajm).

7. Production: ekran, sinov, tunnel

7.1 To'liq ekran va xavfsiz zona

Bot API 8.0 dan Mini App butun ekranni egallashi mumkin — o'yinlar va media uchun. Lekin endi kontent telefonning "teshik"i va tizim paneli ostiga tushishi mumkin. SDK xavfsiz zonalarni beradi (mobil qurilma tuzoqlari):

js
if (WebApp.isVersionAtLeast("8.0")) {
  WebApp.requestFullscreen();
  WebApp.onEvent("fullscreenChanged", () => {
    console.log("to'liq ekran:", WebApp.isFullscreen);
  });
}
css
.sarlavha {
  /* telefon tizim paneli va Telegram tugmalari ostida qolmasin */
  padding-top: calc(
    var(--tg-safe-area-inset-top, 0px) +
      var(--tg-content-safe-area-inset-top, 0px)
  );
}

safeAreaInset — tizim (kamera teshigi, holat paneli), contentSafeAreaInset — Telegram'ning o'z elementlari (yopish tugmasi). Eski Telegram'da metod yo'q — isVersionAtLeast shuning uchun.

7.2 Lokal sinash: HTTPS tunnel

Telegram Mini App manzilini faqat HTTPS bilan ochadi. Sizning Vite server'ingiz esa http://localhost:5173. Ikki yo'l:

1. Tunnel — lokal portni internetga HTTPS manzil bilan chiqaradigan dastur. Masalan, Cloudflare'ning cloudflared (hisobsiz "tez tunnel" rejimi bor) yoki ngrok (hisob kerak):

bash
cloudflared tunnel --url http://localhost:5173
# → https://<tasodifiy-nom>.trycloudflare.com

Shu manzil BotFather'da menyu tugmasiga yoziladi. Vite esa begona host nomini to'sadi — dev server darsidagi xavfsizlik:

ts
// vite.config.ts
export default defineConfig({
  server: {
    // tunnel manzili; faqat dev uchun
    allowedHosts: [".trycloudflare.com"],
  },
});

Usiz brauzerda Vite'ning xabari chiqadi: "Blocked request. This host (…) is not allowed. To allow this host, add … to server.allowedHosts in vite.config.js." — xabarning o'zi davosini aytadi. Tunnel ochiq turganda lokal server'ingiz internetda — ishlab bo'lgach yoping.

2. Test serveri — Telegram'ning alohida sinov muhiti: alohida akkaunt, alohida bot (BotFather u yerda ham bor). Hujjatga ko'ra test muhitida Mini App manzili HTTP (TLS'siz) bo'lishi mumkin — lokal tarmoqdagi IP bilan sinash oson. Test akkauntiga kirish yo'li har ilovada boshqacha (iOS'da sozlamalar belgisini 10 marta bosish va hokazo — hujjatdagi "Testing Mini Apps").

Biz ikkalasini ham sinamadik — bot yo'q. Tunnel buyrug'i va Vite sozlamasi — hujjat bo'yicha.

7.3 Telegram Login Widget (oddiy sayt uchun)

Mini App'dan tashqarida, oddiy saytda "Telegram orqali kirish" kerak bo'lsa — Telegram Login Widget:

html
<script
  async
  src="https://telegram.org/js/telegram-widget.js?22"
  data-telegram-login="sizning_bot"
  data-size="large"
  data-onauth="onTelegramAuth(user)"
  data-request-access="write"
></script>

Tugma bosilganda Telegram foydalanuvchidan ruxsat so'raydi va onTelegramAuth ga { id, first_name, username, photo_url, auth_date, hash } beradi. Bu ham imzolangan, lekin boshqa algoritm: maxfiy kalit — SHA256(bot_token) (HMAC emas), "WebAppData" yo'q. Ikki algoritmni adashtirish — keng tarqalgan xato. Saytning domeni BotFather'da /setdomain bilan bog'lanadi. CSP'da: script-src https://telegram.org, frame-src https://oauth.telegram.org.

Login Widget skripti <iframe> va o'z skriptlarini yuklaydi — vazifalar kabi Trusted Types yoqilgan sahifada CAPTCHA'lardagi kabi muammo bo'lishi mumkin (CAPTCHA darsida o'lchov). Biz Login Widget'ni sinamadik.

7.4 Chiqarishdan oldin

  • BotFather: bot nomi, tavsifi, rasmi, menyu tugmasi, asosiy Mini App (/newapp yoki bot sozlamalari), maxfiylik siyosati havolasi.
  • Hajm va tezlik: WebView, mobil internet — birinchi yuklash kichik (optimallashtirish).
  • CSP: telegram.org (SDK skripti), to'lov bo'lsa — provayder hostlari.
  • Telegram'dan tashqarida ochilganda — tushunarli xabar, yiqilish emas.

7.5 Nega vazifalar da yo'q

Oldingi darsdagi sabab: bot = hisob va token, imzo tekshiruvi = backend. Kanon bu darsda ham o'lchov qilmadi: @twa-dev/sdk va @tma.js/sdk-react raqamlari — Mini Apps I da. Bu darsning jonli bloklari — o'quv modellari, initData imzosi esa Node'da haqiqiy algoritm bilan.

8. Hujumchi nigohi

1. Soxta initData. Hujumchi hash'ga o'z user ini yozadi. Himoya — server imzo tekshiruvi (validate). Imzosiz yoki noto'g'ri imzoli so'rov — 401.

2. Qayta o'ynatish (replay). Hujumchi kimningdir haqiqiy initData sini ushlab oldi (masalan, mehmon skrinshot yoki log yubordi). Imzo to'g'ri! Himoya — auth_date muddati va sessiyani faqat birinchi so'rovda ochish. initData ni loglarga yozmang — u vaqtinchalik "parol".

3. Bot tokeni sizib chiqishi. Token frontend'da, ochiq repo'da, .env commit qilingan — har kim imzo yasaydi va botingizni boshqaradi. Himoya: token faqat server env'ida; sizib chiqsa — BotFather'da darhol /revoke.

4. start_param orqali hujum. ?startapp=… — havolani hujumchi ham yasaydi. Uni yo'lga, so'rovga, HTML'ga tekshirmasdan qo'yish — ochiq yo'naltirish yoki XSS. Himoya — «Jonli: parametrdan yo'l» dagi oq ro'yxat.

5. Soxta "paid". openInvoice callback'i — brauzerda. Mahsulot faqat server successful_payment dan keyin beriladi.

9. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Imzo hech qachon mos kelmaydi maydonlar tartiblanmagan, \n o'rniga &, yoki URL-kodlangan qiymat ishlatilgan URLSearchParams (qiymatni o'zi ochadi), sort(), \n
Imzo mos emas (Login Widget kodi bilan) Login Widget va Mini App algoritmi aralashgan Mini App: HMAC("WebAppData", token)
Bir soatdan keyin ilova "buziladi" har so'rovda auth_date tekshiriladi initData bilan sessiya ochish, keyin cookie
Muddat doim "eskirgan" Date.now() (ms) va auth_date (s) Math.floor(Date.now() / 1000)
startapp ishlamaydi /taom/42 — ruxsat etilmagan belgi taom-42 va o'z qoidangiz
KEY_INVALID yoki saqlanmadi kalitda bo'sh joy/nuqta, qiymat 4096 dan uzun A-Z a-z 0-9 _ -, qisqa qiymat
Blocked request. This host … is not allowed Vite tunnel hostini bilmaydi server.allowedHosts
Raqamli mahsulot uchun Payme ulandi, ilova bloklandi Mini App'da raqamli savdo — Stars Stars (XTR)

10. Mashqlar

Mashqlar kurs/mashqlar/21/23-tma-auth/ da. Jonli yechimlarni «Ishga tushir» bilan sinang.

1-mashq (oson): bo'sh joylarni to'ldiring

  • Mini App maxfiy kalitini yasashda HMAC kaliti bo'ladigan satr:
  • Eski initData ni qayta ishlatishga qarshi tekshiriladigan maydon:
  • Mini App'ni kerakli sahifada ochadigan havola parametri:
  • Telegram Stars valyuta kodi:
  • Bot pre_checkout_query ga javob berishi kerak bo'lgan muddat (soniya):
Yechim

HMAC_SHA256(bot_token, "WebAppData") — maxfiy kalit. auth_date — ochilgan vaqt (Unix soniya). startapp → start_param. XTR — Stars, provider_token bo'sh. 10 soniyada javob bo'lmasa — to'lov bekor.

2-mashq (o'rta): data-check-string

Imzo tekshiruvining birinchi qadamini alohida yozing: dataCheckString(initData) — hash ni olib tashlab, qolgan maydonlarni alifbo tartibida kalit=qiymat qilib \n bilan qo'shsin. Sinov:

js
"user=%7B%22id%22%3A1%7D&auth_date=1760000000&query_id=AAH&hash=abc"

Natijada user qiymati ochilgan ({"id":1}) bo'lishi kerak, hash — yo'q. Keyin xuddi shu satrni & bilan ajratib, qo'lda (split("&")) qiling va farqni konsolga chiqaring.

Ishora: URLSearchParams qiymatlarni o'zi ochadi (%7B → {), split("=") esa — yo'q (URLSearchParams).

Yechim
js
function dataCheckString(initData) {
  const params = new URLSearchParams(initData);
  params.delete("hash");
  return [...params]
    .sort(([a], [b]) => a.localeCompare(b))
    .map(([key, value]) => `${key}=${value}`)
    .join("\n");
}

function wrongWay(initData) {
  return initData
    .split("&")
    .filter((pair) => !pair.startsWith("hash="))
    .sort()
    .join("\n");
}

const sample =
  "user=%7B%22id%22%3A1%7D&auth_date=1760000000" +
  "&query_id=AAH&hash=abc";
console.log(dataCheckString(sample));
console.log("---");
console.log(wrongWay(sample));

Konsolda:

text
auth_date=1760000000
query_id=AAH
user={"id":1}
---
auth_date=1760000000
query_id=AAH
user=%7B%22id%22%3A1%7D

Ikkinchi variantda user kodlangan holda qoldi — undan yasalgan HMAC Telegram'nikiga hech qachon teng bo'lmaydi va har bir haqiqiy foydalanuvchi rad etiladi. "Imzo hech qachon mos kelmaydi" xatosining eng ko'p sababi shu. localeCompare o'rniga oddiy sort() ham ishlaydi — kalitlar lotin harflarida.

3-mashq (qiyin): useTelegramSession — qayta urinish bilan

«Jonli: initData bilan kirish» dagi hook'ni kengaytiring. Soxta server birinchi so'rovda { ok: false, reason: "eskirgan" }, ikkinchisida { ok: true, user: { name: "Aziz" } } qaytarsin.

  • denied holatida sababi ko'rsatilsin: «Kirish rad etildi: eskirgan» va «Qayta urinish» tugmasi.
  • Tugma so'rovni qayta yuborsin (effekt attempt state'iga bog'langan).
  • Har so'rov konsolga: so'rov N.
Yechim
jsx
import { useEffect, useState } from "react";

const initData =
  "user=%7B%22id%22%3A1%7D&auth_date=1760000000&hash=…";
const replies = [
  { ok: false, reason: "eskirgan" },
  { ok: true, user: { name: "Aziz" } },
];
let calls = 0;
function api() {
  calls += 1;
  console.log(`so'rov ${calls}`);
  const reply = replies[calls - 1] ?? replies[1];
  return new Promise((resolve) =>
    setTimeout(() => resolve(reply), 150),
  );
}

function useTelegramSession(attempt) {
  const [state, setState] = useState({ status: "loading" });
  useEffect(() => {
    let active = true;
    setState({ status: "loading" });
    api("/api/sessiya", {
      headers: { Authorization: `tma ${initData}` },
    }).then((res) => {
      if (!active) return;
      setState(
        res.ok
          ? { status: "ok", user: res.user }
          : { status: "denied", reason: res.reason },
      );
    });
    return () => {
      active = false;
    };
  }, [attempt]);
  return state;
}

export default function App() {
  const [attempt, setAttempt] = useState(1);
  const session = useTelegramSession(attempt);
  if (session.status === "loading") return <p>Kirilmoqda…</p>;
  if (session.status === "denied")
    return (
      <p>
        Kirish rad etildi: {session.reason}{" "}
        <button onClick={() => setAttempt((a) => a + 1)}>
          Qayta urinish
        </button>
      </p>
    );
  return <p>Xush kelibsiz, {session.user.name}!</p>;
}

Konsolda:

text
so'rov 1
so'rov 2

Ekranda:

text
Xush kelibsiz, Aziz!

active bayrog'i — race condition dan himoya: tez ikki marta «Qayta urinish» bosilsa, eski javob yangisini bosib ketmaydi. Haqiqiy Mini App'da "eskirgan" bo'lsa, qayta so'rov o'sha initData bilan yana rad etiladi — foydalanuvchiga "Ilovani yopib, qayta oching" deyish to'g'riroq (Telegram yangi initData beradi). Bu yerda soxta server ikkinchi marta "ok" deydi — oqimni ko'rsatish uchun.

4-mashq: tajriba qadami (ixtiyoriy)

vazifalar ga tegmang. 2-mashqdagi dataCheckString va darsdagi validate ni bitta Node faylga (kurs/tajriba/tma-imzo/tekshir.mjs) yig'ing va node:test bilan to'rt test yozing: to'g'ri imzo, o'zgartirilgan user, eski auth_date, hash siz satr (node:test). Token — soxta, darsdagidek. Agar o'zingiz bot yaratgan bo'lsangiz (ixtiyoriy) — Mini App'da WebApp.initData ni konsolga chiqarib, uni o'z tokeningiz bilan tekshirib ko'ring va natijani hech qayerga ulashmang.

Yechim

Test tuzilmasi (qisqacha):

js
import { test } from "node:test";
import assert from "node:assert/strict";

test("to'g'ri imzo", () => {
  assert.equal(validate(initData, TOKEN, 3600, now), "to'g'ri: Aziz");
});
test("o'zgartirilgan user", () => {
  const forged = initData.replace("Aziz", "Jasur");
  assert.equal(validate(forged, TOKEN, 3600, now), "imzo noto'g'ri");
});
// … eskirgan, hash yo'q

Haqiqiy initData ni tekshirganda signature maydoni ham keladi — u data-check-string'ga kiradi (faqat hash olib tashlanadi). Uni o'chirib yuborsangiz — imzo mos kelmaydi.

11. Real ishda

  • Mini App backend'i — Telegram'dagi har jiddiy ilovada: initData tekshiruvi, sessiya, buyurtmalar. Ko'p jamoalarda frontend'chi ham shu tekshiruvni o'qiy olishi kutiladi — xatoni qayerdan izlashni bilish uchun.
  • Kanal va bot havolalari — marketing: har postda startapp bilan aniq taom yoki aksiya. Analitikada start_param — "qaysi post olib keldi" (analitika).
  • Stars — kontent sotuvchilar, o'yinlar, premium funksiyalar. Jismoniy savdo — o'z checkout'ingiz yoki Telegram Payments provayderlari.
  • Intervyu savollari: "Mini App'da foydalanuvchini qanday autentifikatsiya qilasiz?", "Nega initDataUnsafe ga ishonib bo'lmaydi?", "Replay hujumidan qanday himoyalanasiz?", "Raqamli mahsulot uchun qaysi to'lov?", "Lokal Mini App'ni qanday sinaysiz?".

Xulosa

  • initData — imzolangan konvert: server data-check-string'dan HMAC(HMAC("WebAppData", token), …) yasab hash bilan solishtiradi (timingSafeEqual) va auth_date muddatini tekshiradi; token — faqat serverda.
  • Frontend initData ni Authorization: tma … bilan yuboradi; server sessiya ochadi; ekrandagi foydalanuvchi — server javobidan.
  • startapp → start_param: xavfsiz chegara — 64 belgi, A-Z a-z 0-9 _ -; yo'l — oq ro'yxatli qoidalar bilan, qolgani — bosh sahifa.
  • Raqamli savdo — Stars (XTR): invoice serverda, openInvoice callback'i — faqat xabar, mahsulot — successful_payment dan keyin.
  • CloudStorage — bot+foydalanuvchi uchun 1024 kalit, qiymat ≤ 4096; callback API'ni Promise'ga o'rash.
  • Production: to'liq ekran va --tg-safe-area-inset-*, isVersionAtLeast, HTTPS tunnel + allowedHosts yoki test serveri; Login Widget — boshqa imzo algoritmi.

Keyingi dars: Tashqi login va SMS kod bilan kirish UI — «Google bilan kirish» tugmasi, OAuth qaytish sahifasi va PKCE'da frontend'ning roli, SMS kod ekrani va qayta yuborish taymeri.

Manbalar

  • Telegram: "Telegram Mini Apps" — "Validating data received via the Mini App", "Validating data for Third-Party Use", startapp ("Direct Link Mini Apps"), openInvoice, CloudStorage, requestFullscreen, safeAreaInset, "Testing Mini Apps" — core.telegram.org/bots/webapps
  • Telegram: "Telegram Stars" va "Bot Payments API for Digital Goods" — core.telegram.org/bots/payments-stars; "Telegram Login Widget" — core.telegram.org/widgets/login
  • Telegram Mini Apps platformasi (jamoa): "Init Data", "Authorizing User" (Authorization: tma) — docs.telegram-mini-apps.com
  • Vite: server.allowedHosts — vite.dev/config/server-options
  • Node.js: crypto.createHmac, crypto.timingSafeEqual — nodejs.org/api/crypto.html
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Telegram Mini Apps auth va to'lov: initData imzosi, startapp, Stars, CloudStorage va production — IlmHamroh