import { createHash, createHmac, timingSafeEqual } from 'node:crypto'; import { config } from '../config.js'; import { prisma } from '../prisma.js'; type TelegramInitDataUser = { id: number; username?: string; first_name?: string; last_name?: string; photo_url?: string; language_code?: string; }; export type TelegramLoginData = { id: number; first_name?: string; last_name?: string; username?: string; photo_url?: string; auth_date: number; hash: string; }; type TelegramUserPayload = { id: number; username?: string; firstName?: string; lastName?: string; photoUrl?: string; languageCode?: string; }; type TelegramAuthData = { telegramInitData?: string; telegramLoginData?: string; }; function hmacSha256(key: string | Buffer, value: string) { return createHmac('sha256', key).update(value).digest(); } function sha256(value: string) { return createHash('sha256').update(value).digest(); } function assertValidHash(receivedHash: string, expectedHash: Buffer, dataType: string) { const received = Buffer.from(receivedHash, 'hex'); if (received.length !== expectedHash.length) { throw new Error(`Telegram ${dataType} hash is invalid.`); } if (!timingSafeEqual(received, expectedHash)) { throw new Error(`Telegram ${dataType} hash is invalid.`); } } function assertConfiguredBotToken() { if (!config.telegramMiniAppBotToken) { throw new Error('TELEGRAM_MINI_APP_BOT_TOKEN is required.'); } } function assertFreshAuthDate(authDate: number, dataType: string) { if (!Number.isFinite(authDate)) { throw new Error(`Telegram ${dataType} auth_date is required.`); } const ageSeconds = Math.floor(Date.now() / 1000) - authDate; if (ageSeconds > config.telegramAuthMaxAgeSeconds) { throw new Error(`Telegram ${dataType} is expired.`); } } function parseTelegramInitData(initData: string): TelegramUserPayload { assertConfiguredBotToken(); const params = new URLSearchParams(initData); const receivedHash = params.get('hash'); if (!receivedHash) { throw new Error('Telegram init data hash is required.'); } assertFreshAuthDate(Number(params.get('auth_date')), 'init data'); const dataCheckString = [...params.entries()] .filter(([key]) => key !== 'hash') .sort(([left], [right]) => left.localeCompare(right)) .map(([key, value]) => `${key}=${value}`) .join('\n'); const secretKey = hmacSha256('WebAppData', config.telegramMiniAppBotToken); const expectedHash = hmacSha256(secretKey, dataCheckString); assertValidHash(receivedHash, expectedHash, 'init data'); const rawUser = params.get('user'); if (!rawUser) { throw new Error('Telegram user is required.'); } const user = JSON.parse(rawUser) as TelegramInitDataUser; return { id: user.id, username: user.username, firstName: user.first_name, lastName: user.last_name, photoUrl: user.photo_url, languageCode: user.language_code, }; } function parseTelegramLoginData(loginData: TelegramLoginData): TelegramUserPayload { assertConfiguredBotToken(); assertFreshAuthDate(Number(loginData.auth_date), 'login data'); const dataCheckString = Object.entries(loginData) .filter(([key, value]) => key !== 'hash' && value !== undefined && value !== null) .sort(([left], [right]) => left.localeCompare(right)) .map(([key, value]) => `${key}=${value}`) .join('\n'); const expectedHash = hmacSha256(sha256(config.telegramMiniAppBotToken), dataCheckString); assertValidHash(loginData.hash, expectedHash, 'login data'); return { id: Number(loginData.id), username: loginData.username, firstName: loginData.first_name, lastName: loginData.last_name, photoUrl: loginData.photo_url, }; } function parseTelegramLoginJson(loginData: string) { return parseTelegramLoginData(JSON.parse(loginData) as TelegramLoginData); } async function upsertTelegramUser(user: TelegramUserPayload) { return prisma.user.upsert({ where: { telegramId: String(user.id) }, create: { telegramId: String(user.id), username: user.username, firstName: user.firstName, lastName: user.lastName, photoUrl: user.photoUrl, languageCode: user.languageCode, }, update: { username: user.username, firstName: user.firstName, lastName: user.lastName, photoUrl: user.photoUrl, languageCode: user.languageCode, }, }); } export async function getOrCreateTelegramUser(initData: string) { return upsertTelegramUser(parseTelegramInitData(initData)); } export async function getOrCreateTelegramLoginUser(loginData: TelegramLoginData) { return upsertTelegramUser(parseTelegramLoginData(loginData)); } export async function requireTelegramUser(authData: TelegramAuthData) { if (authData.telegramInitData) { return upsertTelegramUser(parseTelegramInitData(authData.telegramInitData)); } if (authData.telegramLoginData) { return upsertTelegramUser(parseTelegramLoginJson(authData.telegramLoginData)); } throw new Error('Telegram authorization is required.'); }