Перейти к содержанию

Сессия мини-приложения

После проверки initData сервер знает подтверждённый Telegram ID, время запуска и допустимый контекст. Но функция проверки ещё не создаёт сессию нашего приложения. В этом уроке определим обмен запуска на короткую серверную сессию и отделим его от хранения учебного плана. Результат — точный контракт и полный вспомогательный модуль выдачи случайного токена; HTTP-обработчик и постоянное хранилище остаются отдельной интеграцией.

Мы выберем bearer token в памяти открытой страницы. Это решение удобно для разбора: каждый персональный запрос явно передаёт токен в Authorization. Мы не рассчитываем на одинаковую политику сторонних cookie во всех WebView и не сохраняем токен в localStorage. Выбор не устраняет риск XSS: вредоносный JavaScript того же origin способен прочитать память и вызвать API. Поэтому ограниченный срок, безопасный вывод текста и отсутствие секретов в сторонней аналитике продолжают иметь значение.

Три разных срока

Auth_date относится к подписанному запуску Telegram. Окно нашего проверяющего модуля составляет пять минут с небольшим допуском будущего времени; это выбранная политика, а не универсальная константа платформы. Серверная сессия будет жить пятнадцать минут после собственного создания. Учебный план хранится дольше обеих величин и принадлежит аккаунту, а не конкретному токену. После окончания сессии план не удаляется.

Поле expiresAt в ответе помогает интерфейсу заранее показать необходимость повторного открытия. Источником решения всё равно является серверное время. Клиент не продлевает сессию, добавляя минуту к своему числу, и не изменяет auth_date старого запуска. Если страница оставалась открытой час, исходная initData уже может не пройти новый обмен. Предложите открыть mini app заново, сохранив несекретный draft согласно отдельной политике, вместо бесконечного скрытого повторения того же запроса.

Обмен запуска

Наш договор использует POST /api/session с JSON из одного поля initData. HTTP-адаптер ограничивает тело, проверяет Content-Type и вызывает validateInitData из предыдущего урока. Результат проверки передаётся внутрь серверной функции выдачи. Клиентские поля telegramUserId, authenticated и произвольный expiresAt не участвуют в выборе владельца. Отказ не раскрывает посетителю bot token или точный промежуточный HMAC.

Пример успешного ответа описывает форму данных, а не уже существующий endpoint:

{
  "token": "OPAQUE_SERVER_GENERATED_VALUE",
  "expiresAt": "2026-10-11T12:15:00.000Z"
}

Токен в примере — поясняющая строка, её нельзя использовать как общую константу для всех пользователей. Реальное значение создаётся отдельно для каждой сессии. Ответы обмена и персонального API имеют Cache-Control: no-store. Ни токен, ни initData не помещаются в URL: такие адреса часто попадают в историю, журналы прокси и сведения перехода. Ответ также не нужен системе аналитики целиком.

Случайность и запись хранилища

Полный модуль session-material.mjs формирует материал сессии для Node.js 24. Он не открывает порт, не пишет в базу и не делает проверку Telegram повторно. Его входом является verified context, уже полученный доверенным серверным вызовом. Документация crypto описывает randomBytes и createHash; здесь случайность нужна для непредсказуемого токена, хеш — для поиска записи без хранения исходного значения.

import {randomBytes, createHash} from 'node:crypto';

export function tokenDigest(token) {
  if (typeof token !== 'string' || !/^[A-Za-z0-9_-]{43}$/.test(token)) {
    throw new Error('Invalid session token');
  }
  return createHash('sha256').update(token, 'utf8').digest('hex');
}

export function createSessionMaterial(verified, now = Date.now()) {
  if (!verified || !/^[1-9][0-9]*$/.test(verified.telegramUserId ?? '') ||
      !Number.isSafeInteger(now) || now < 0) {
    throw new Error('Invalid verified context');
  }
  const token = randomBytes(32).toString('base64url');
  const expiresAt = now + 15 * 60 * 1000;
  return {
    response: {token, expiresAt: new Date(expiresAt).toISOString()},
    record: {
      tokenHash: tokenDigest(token),
      ownerId: verified.telegramUserId,
      createdAt: now,
      expiresAt,
      revokedAt: null
    }
  };
}

В базу попадает record, а response отдаётся владельцу через HTTPS. После успешной вставки сервер возвращает токен; до подтверждения записи отвечать успехом нельзя. Уникальный индекс tokenHash и обработка редкого конфликта — обязанности хранилища. Этот модуль не доказывает наличие такого индекса. При чтении Authorization сервер хеширует переданный токен, ищет запись и проверяет expiresAt и revokedAt. Идентификатор ownerId берётся из найденной записи, а не из тела сохранения плана.

Проверка личности и разрешение операции

Сессия отвечает на вопрос «какой аккаунт подтверждён». Она не даёт доступ ко всем планам. Наш /api/plan работает только с планом владельца сессии и не принимает ownerId от клиента. Если появится маршрут чужого ресурса, сервер отдельно сравнит владельца либо применит сформулированное правило доступа. Наличие валидного токена не разрешает присвоить чужой идентификатор в URL.

Повтор одной свежей initData может создать несколько сессий. В текущем контракте это допустимо, но обмен ограничивается по частоте, а число действующих сессий контролируется политикой сервиса. HMAC сам по себе не является защитой от повторного обмена. Если продукт требует одноразового использования запуска, понадобится отдельный атомарный реестр повторов с ограниченным сроком и понятной обработкой сетевого повтора. Мы его не выдаём за свойство функции из урока девять.

Если сервер работает в нескольких процессах, локальный Map каждого процесса не является общим хранилищем сессий. Запрос обмена может попасть в один процесс, а чтение плана — в другой. Тогда человек получит случайные 401 даже с правильным токеном. Для проектной реализации нужен согласованный общий store либо явно ограниченный однопроцессный учебный режим. В серии нет работающего store, поэтому мы фиксируем требование к нему и не обещаем надёжность только на основании случайного значения. Очистка истёкших записей помогает размеру базы, но проверка срока при каждом доступе всё равно обязательна.

Заголовки, origin и выход

Персональные запросы будут использовать credentials: omit и явный Authorization. Сервер не добавляет параллельную cookie-аутентификацию «на всякий случай»: иначе появляется ещё один механизм, который нужно отдельно защищать от CSRF. При выбранном bearer договоре браузер не прикладывает авторизацию автоматически к произвольной форме другого сайта. Проверка origin и аккуратный CORS полезны, но не заменяют сессию и правило владения. Запрос из инструмента вне браузера всё равно должен проходить авторизацию.

Для выхода клиент удаляет токен из памяти, а серверный endpoint отзывает соответствующую запись. Только удаление на экране не отзывает уже скопированное значение. В контракте архива указан DELETE /api/session с текущим bearer и ответом 204 после отзыва. Перезагрузка страницы теряет память, поэтому нужен новый допустимый запуск, а не поиск токена в адресе. Draft и персональные данные очищаются отдельно, чтобы следующий человек не увидел предыдущий план.

Ожидаемые сценарии — свежий запуск создаёт самостоятельную запись; истёкший либо отозванный токен не открывает план; изменение ownerId в клиентском JSON не меняет владельца. Эти сценарии, генератор и HTTP-обмен здесь не выполнялись. Постоянное хранилище, лимиты, очистка записей, адаптер и отзыв ещё требуют реализации. Дальше опишем клиент собственного API, который использует такой контракт и различает отказ, конфликт и отсутствие ответа.

Оглавление серии