Тема
Свой сервер и прокси
Remote-апп — это твой https-сервер, который отдаёт экраны (Stac JSON) и API-ответы. Клиент никогда не ходит к нему напрямую: весь трафик идёт через защищённый прокси платформы.
Нет своего хостинга?
Готовый пример с нуля — деплой бэкенда на Yandex Cloud (Cloud Functions + API Gateway).
Как ходит трафик
Контракт твоего сервера
Экраны
На GET <origin><path> сервер отвечает телом-объектом — Stac JSON экрана. path — это путь, указанный как стартовый или запрошенный экшеном openPage.
GET https://my-app.example.com/ → JSON стартового экрана
GET https://my-app.example.com/stats → JSON страницы /statsAPI-вызовы
Экшен networkRequest с относительным url превращается в запрос к твоему origin с тем же методом, query и телом:
{ "actionType": "networkRequest", "url": "/api/vote", "method": "post", "body": {} }
→ POST https://my-app.example.com/api/voteКто пришёл: identity по разрешениям
Сессия и JWT пользователя не передаются никогда. Какие данные видит твой сервер, решает сам пользователь: при первом запуске он подтверждает запрошенные тобой разрешения (см. публикацию) и может изменить выбор в любой момент. Прокси шлёт только разрешённое:
| Заголовок | Скоуп | Значение |
|---|---|---|
X-MireaNinja-App-User | всегда | Псевдонимный ID — стабилен для твоего аппа, но в разных аппах разный (нельзя сопоставить аудитории) |
X-MireaNinja-Scopes | всегда | Список выданных скоупов через запятую |
X-MireaNinja-User | identity | Реальный UUID пользователя |
X-MireaNinja-Email | email | Университетская почта |
X-MireaNinja-Name | profile | ФИО, base64(utf-8) |
X-MireaNinja-Course | profile | Номер курса |
X-MireaNinja-Group | group | Учебная группа, base64(utf-8) |
X-MireaNinja-Timestamp | — | Unix-время подписи (сек) |
X-MireaNinja-Signature | — | HMAC-SHA256(секрет, "<appUserId>.<timestamp>"), hex |
Проектируй под отказ
Каждый скоуп пользователь может не выдать или отозвать позже — апп обязан работать без них. Скоупы, не заявленные при публикации, не будут переданы даже при желании пользователя.
Имя и группа могут содержать кириллицу, поэтому передаются как base64 от UTF-8: Buffer.from(value, "base64").toString("utf8").
Проверка подписи
Подпись доказывает, что запрос пришёл именно от прокси Mirea Ninja, а не от кого-то, кто подставил заголовки руками. Секрет выдаётся команде платформы по запросу (контакты — в приложении). Проверка на Node.js:
js
// express middleware
import crypto from "node:crypto";
function verifyNinja(req, res, next) {
const user = req.header("X-MireaNinja-App-User");
const ts = req.header("X-MireaNinja-Timestamp");
const sig = req.header("X-MireaNinja-Signature");
if (!user || !ts || !sig) return res.status(401).end();
// окно в 5 минут против replay
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
return res.status(401).end();
}
const expected = crypto
.createHmac("sha256", process.env.NINJA_SECRET)
.update(`${user}.${ts}`)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).end();
}
req.ninjaAppUserId = user; // псевдонимный ID — используй как ключ аккаунта
next();
}Нет секрета — нет проблемы
Если подпись тебе не нужна (например, апп без аккаунтов), просто игнорируй заголовки — они придут без Signature, пока секрет не настроен.
Жёсткие ограничения прокси
- Только
https; IP-адреса,localhost,*.local,*.internalи внутренние хосты платформы запрещены. - Редиректы не выполняются — ответ 3xx считается ошибкой.
- Ответ обязан быть валидным JSON и не больше 512 КБ.
- Таймаут запроса — 10 секунд.
- Методы: GET, POST, PUT, PATCH, DELETE.
- Rate limit: 180 запросов в минуту на пользователя (на все мини-аппы суммарно). При превышении — 429.
- Путь не может выйти за пределы origin (никаких
..). - Query-параметры прокидываются с лимитом: ключ ≤ 64, значение ≤ 512 символов; длиннее — отбрасывается.
Картинки ходят напрямую
Виджет image загружает URL без прокси. Используй стабильный https-хостинг и не выкладывай через картинки чувствительные данные.
Чек-лист перед публикацией
- Сервер отвечает JSON-объектом на стартовый путь за < 10 c.
- Все ответы меньше 512 КБ (пагинация, а не «всё сразу»).
- Нет редиректов (включая
http→httpsи слэш-редиректы). - CDN/хостинг не отдаёт HTML-страницы ошибок на JSON-роуты.
- Если используешь identity — проверяешь подпись и timestamp.
Сервисные мини-аппы (внутренние)
Помимо hosted и remote есть третий вид источника — service. Это первопартийные аппы самой платформы (например, «Свободные аудитории»): у них нет внешнего origin и нет владельца-пользователя, а экраны отдаёт внутренняя edge-функция с префиксом miniapp-svc-<slug>.
- Прокси узнаёт такой апп по
source_kind = 'service'и зовёт функциюminiapp-svc-<slug>напрямую (доверенная внутренняя цель), передавая ейslug,path,kindиuserId. - Функция авторизует вызов по service-role ключу — снаружи её не дёрнуть.
- Завести сервисный апп может только платформа (миграцией): обычные пользователи не могут выставить
source_kind = 'service'.
Для авторов сторонних аппов это закрытый путь — используйте hosted или remote.