import { createHmac, timingSafeEqual } from "node:crypto"; export interface TelegramInitDataUser { id: string; username?: string; firstName?: string; lastName?: string; } /** * Validates Telegram Mini App initData against the bot token. * https://core.telegram.org/bots/webapps#validating-data-received-via-the-mini-app */ export function verifyTelegramInitData( initData: string, botToken: string, maxAgeSeconds = 86400, ): TelegramInitDataUser | null { if (!initData || initData.length > 4096 || !botToken) return null; const params = new URLSearchParams(initData); if (new Set(params.keys()).size !== [...params.keys()].length) return null; const hash = params.get("hash"); if (!hash || !/^[a-f0-9]{64}$/.test(hash)) return null; params.delete("hash"); const checkString = [...params.keys()] .sort() .map((key) => `${key}=${params.get(key)}`) .join("\n"); const secret = createHmac("sha256", "WebAppData").update(botToken).digest(); const expected = createHmac("sha256", secret).update(checkString).digest("hex"); const valid = hash.length === expected.length && timingSafeEqual(Buffer.from(hash), Buffer.from(expected)); if (!valid) return null; const authDate = Number(params.get("auth_date")); if (!Number.isFinite(authDate) || Math.abs(Date.now() / 1000 - authDate) > maxAgeSeconds) return null; const raw = params.get("user"); if (!raw) return null; try { const user = JSON.parse(raw) as { id?: unknown; username?: unknown; first_name?: unknown; last_name?: unknown; }; if (typeof user.id !== "number" && typeof user.id !== "string") return null; return { id: String(user.id), ...(typeof user.username === "string" ? { username: user.username } : {}), ...(typeof user.first_name === "string" ? { firstName: user.first_name } : {}), ...(typeof user.last_name === "string" ? { lastName: user.last_name } : {}), }; } catch { return null; } }