const { parse, validate } = require('@telegram-apps/init-data-node'); const crypto = require('crypto'); const config = require('../config'); const MAX_AUTH_AGE_SECONDS = 60 * 60; // 1 час /** * Manual validation with base64 padding fix * Based on: https://docs.telegram-mini-apps.com/platform/init-data */ function manualValidateInitData(initDataRaw, botToken) { const params = new URLSearchParams(initDataRaw); const hash = params.get('hash'); if (!hash) { throw new Error('Отсутствует hash в initData'); } // Remove hash from params params.delete('hash'); // Create data check string const dataCheckArr = Array.from(params.entries()) .sort(([a], [b]) => a.localeCompare(b)) .map(([key, value]) => `${key}=${value}`); const dataCheckString = dataCheckArr.join('\n'); // Create secret key const secretKey = crypto .createHmac('sha256', 'WebAppData') .update(botToken) .digest(); // Create signature const signature = crypto .createHmac('sha256', secretKey) .update(dataCheckString) .digest('hex'); // Compare signatures return signature === hash; } function validateAndParseInitData(initDataRaw, botToken = null) { const tokenToUse = botToken || config.telegramBotToken; console.log('[Telegram] validateAndParseInitData called:', { hasInitData: !!initDataRaw, type: typeof initDataRaw, length: initDataRaw?.length || 0, preview: initDataRaw?.substring(0, 100) + '...', usingModerationToken: !!botToken }); if (!tokenToUse) { throw new Error('Bot token не настроен'); } if (!initDataRaw || typeof initDataRaw !== 'string') { throw new Error('initData не передан'); } const trimmed = initDataRaw.trim(); if (!trimmed.length) { throw new Error('initData пуст'); } console.log('[Telegram] Validating initData with bot token...'); // Try library validation first let valid = false; try { validate(trimmed, tokenToUse); valid = true; console.log('[Telegram] Library validation successful'); } catch (libError) { console.log('[Telegram] Library validation failed, trying manual validation:', libError.message); // Fallback to manual validation with base64 padding fix try { valid = manualValidateInitData(trimmed, tokenToUse); if (valid) { console.log('[Telegram] Manual validation successful'); } } catch (manualError) { console.error('[Telegram] Manual validation also failed:', manualError.message); } } if (!valid) { console.error('[Telegram] All validation attempts failed'); throw new Error('Неверная подпись initData'); } console.log('[Telegram] initData validation successful, parsing...'); const payload = parse(trimmed); console.log('[Telegram] Parsed payload:', { hasUser: !!payload?.user, userId: payload?.user?.id, auth_date: payload?.auth_date, authDate: payload?.authDate, allKeys: Object.keys(payload) }); if (!payload || !payload.user) { throw new Error('Отсутствует пользователь в initData'); } // Check for authDate (camelCase from library) or auth_date (snake_case) const authDate = Number(payload.authDate || payload.auth_date); if (!authDate) { console.error('[Telegram] Missing authDate in payload:', payload); throw new Error('Отсутствует auth_date в initData'); } const now = Math.floor(Date.now() / 1000); const age = Math.abs(now - authDate); console.log('[Telegram] Auth date check:', { authDate, now, age, maxAge: MAX_AUTH_AGE_SECONDS, expired: age > MAX_AUTH_AGE_SECONDS }); if (age > MAX_AUTH_AGE_SECONDS) { throw new Error(`Данные авторизации устарели (возраст: ${age}с, макс: ${MAX_AUTH_AGE_SECONDS}с)`); } console.log('[Telegram] initData validation complete'); return payload; } module.exports = { validateAndParseInitData };