Appearance
© 2026 ООО «Аксиома» ИНН:7842223709 · mock-services · Apache-2.0 SPDX-License-Identifier: Apache-2.0
RFC-001. Изоляция песочниц для публичного мока
Статус: реализовано в #8 (закрывает A-21, F-11—F-15): request-local контекст, hosted tenancy за env-флагом, owner-token, target export/import, TTL/лимиты/квоты (§9.1–9.4) · Дата: 2026-10-08 · Связано: аудит 2026-10-08 (§3.2, дефект A-21; материалы — в приватном репозитории команды)
Вне объёма #8 остались: лендинг/hosted-конфиг (§9.5),
/admin-поддомены и браузерныеSuccessURL/FailURLв поддоменной схеме, per-sandbox сценарные правила (/sandboxes/me/scenarios) — см. §10 «Открытые вопросы».
1. Проблема
Сейчас у стенда одно глобальное состояние: один терминал/касса, один реестр external_id, один /admin. Для self-hosted это правильно. Для публичного хостинга это ломается сразу:
- два интегратора пересекаются на
external_id(второй получает «документ уже существует» — ошибка 12, хотя у него свой чек); - чужой
PaymentId/uuidчитаетсяGetState/report, потому что авторизация — только подпись терминала, а терминал общий; /adminпоказывает чужие платежи, а/admin/actionуправляет чужой кассой;- один пользователь может «засорить» состояние (гигантские чеки, миллион документов) и замедлить всех.
2. Цели и не-цели
Цели
- Каждый пользователь (или каждая команда) получает изолированную песочницу со своими кредами и своим состоянием.
- Песочница создаётся без регистрации (демо) или по токену (команды).
- Всё живёт ограниченное время (TTL) и очищается автоматически; есть явный
reset. - Self-hosted режим не усложняется: флагом
MOCK_TENANCY=off(по умолчанию) поведение остаётся сегодняшним (одна песочница = весь стенд). - Соблюдение протокола: клиент по-прежнему ходит на «как у банка» пути; ничего в контракте не меняется (никаких
/sandbox/abc/v2/Init, иначе сниппеты интеграторов перестают переноситься на бой).
Не-цели
- Биллинг, роли, SSO, квоты «по договору» — вне рамок.
- Долговременное хранение данных пользователя; песочницы по определению эфемерны.
3. Ключевая развилка: как клиент идентифицирует песочницу
| Вариант | Пример | Плюсы | Минусы |
|---|---|---|---|
| A. Поддомен (рекомендуемый) | https://<id>.mock-services.dev/v2/Init | Ноль изменений в путях и параметрах; работает с любой библиотекой; естественно для hosted | Нужен wildcard-DNS и wildcard-сертификат |
| B. Заголовок | X-Mock-Sandbox: <id> + обычный хост | Просто; локально удобно | Заголовок придётся добавлять всем клиентам/сниппетам; не видно в браузерных редиректах |
| C. Базовый путь | /s/<id>/v2/Init | Просто в реализации | Ломает переносимость сниппетов на бой (в проде пути другие) |
| D. Отдельный порт/инстанс на песочницу | :3100X | Полная изоляция процессов | Дорого и неудобно для пользователя |
Решение: A (поддомен) как основной способ на hosted; B — как эквивалент для self-hosted и для тестов (заголовок имеет приоритет над поддоменом). C — не использовать.
4. Модель данных
Sandbox {
id // "swift-fox-42" (человекочитаемый) или ksuid
createdAt, expiresAt
ownerToken // доступ к /admin, reset, экспорту (выдаётся при создании)
profile // "tbank-eacq" | "tbank-multisplit" | "ferma" | ...
creds: { // креды песочницы: интегратор вписывает их в свою конфигурацию
terminalKey, terminalPassword,
fermaLogin, fermaPassword, fermaGroup
}
state // ровно тот же state, что сейчас: payments/deals/notifications/receipts/...
quota: { rps, maxDocuments, maxBodyBytes, ttlMs }
}Хранение: один JSON-файл на песочницу (/data/sandboxes/<id>.json) либо встроенная карта в памяти с периодическим дампом. Внешняя БД для демо-стенда не нужна — состояние маленькое и эфемерное.
Креды генерируются на песочницу. Это важнее, чем кажется: подпись Т-Банка зависит от пароля терминала, поэтому интегратор проходит ровно тот же путь, что в бою («впиши свой ключ и пароль»), а не «используй общий».
5. Маршрутизация
POST /v2/Init (Host: swift-fox-42.mock-services.dev)
→ resolveSandbox(host | X-Mock-Sandbox)
→ если песочницы нет: 404 + подсказка «создайте песочницу: POST /sandboxes»
→ state = sandbox.state, creds = sandbox.creds
→ адаптер обрабатывает запрос как обычно (контракт не меняется)Адаптеры получают sandbox как контекст. Сейчас state, TERMINAL_KEY, TERMINAL_PASSWORD — модульные константы; рефакторинг: «фабрика сервиса» (createServer(ctx)) вместо модульных синглтонов. Ядро уже готово (все механизмы принимают state/persist параметрами), поэтому переделка локализована в services/*/server.js.
6. Публичный API управления песочницами
POST /sandboxes → { id, url, ownerToken, creds, expiresAt }
GET /sandboxes/me → текущая песочница (по ownerToken)
POST /sandboxes/me/reset → очистить состояние (креды сохраняются)
POST /sandboxes/me/reset-creds → сменить креды (проверка «а что если ключ протух»)
POST /sandboxes/me/scenarios → сценарный API (см. §3.3 аудита)
DELETE /sandboxes/me → удалить песочницу немедленно
GET /admin?token=ownerToken → консоль песочницы (единственная точка, где нужен токен)Плюс лендинг (/) с кнопкой «Создать песочницу» и краткой инструкцией «интеграция за 10 минут».
Источники owner-token (порядок закреплён задачей #98): X-Sandbox-Token → X-Owner-Token → Authorization: Bearer → ?sandbox_token=. Явный заголовок приоритетнее Bearer: у адаптеров, чей бизнес-контракт сам использует Authorization: Bearer <токен> (Первый ОФД — токен очереди, ВТБ — OAuth access_token), Bearer иначе затенял бы owner-token владельца и бизнес-вызов отвечал бы 401. Bearer сохранён как fallback для клиентов, не шлющих явных заголовков; источник с пустым значением считается отсутствующим.
7. Безопасность
| Угроза | Мера |
|---|---|
| Перебор чужих песочниц | id ≥ 96 бит энтропии (или ksuid), rate limit на создание; ownerToken — отдельный секрет, не выводимый из id |
| Чтение чужого состояния | Все API-вызовы работают только в контексте песочницы; /admin — по ownerToken |
| SSRF через вебхуки | MOCK_BLOCK_PRIVATE_WEBHOOKS=1 по умолчанию на hosted + egress-фильтр + запрет редиректов на приватные адреса |
| CSRF на админ-действиях | cookie SameSite=Strict/HttpOnly + CSRF-токен, либо только header-token |
| DoS | quota: RPS на песочницу, лимит тела (5 МБ уже есть), лимит документов и очереди вебхуков, TTL состояния, «вес» вебхука (таймаут 10 с, ≤ 24 попытки) |
| Утечка «реальных» данных | Дисклеймер + запрет нереальных карт (список тестовых PAN), PAN не хранится в открытом виде, журнал ограничен по объёму |
| Абьюз как публичный прокси | egress-фильтр инфраструктуры, allowlist портов (80/443), запрет не-HTTP схем |
8. Эксплуатация
- TTL: демо-песочницы — 24 ч, командные — 7 дней (настраивается). По истечении: «заморозка» → удаление. Пользователю — письмо/баннер заранее (если есть e-mail; иначе — только баннер в
/admin). - Автоочистка: фоновый воркер раз в минуту удаляет просроченные песочницы и пустые файлы.
- Метрики: количество песочниц, RPS, размер состояний, ошибки 4xx/5xx, длина очередей вебхуков (Prometheus
/metricsприватно). - Резервные копии: не нужны (данные эфемерны), но конфигурация и лимиты — в env/манифесте.
9. План внедрения (этапы)
- Рефакторинг адаптеров в фабрики (
createBankMock(ctx),createKassaMock(ctx)), self-hosted поведение не меняется; тесты остаются зелёными. ≈ 1–2 дня. - Слой песочниц: реестр,
resolveSandbox, генерация кредов,/sandboxes*. ≈ 2 дня. - Admin-scoped:
/adminпоownerToken, действия только внутри песочницы. ≈ 0.5 дня. - Quota/лимиты/TTL/автоочистка. ≈ 1 день.
- Лендинг + доки + hosted-конфиг (wildcard DNS/TLS, egress). ≈ 1–2 дня.
10. Открытые вопросы
- Демо-песочницы без регистрации — как защититься от массового создания (капча, лимит по IP, «очередь»)? Предложение: лимит 3 песочницы/IP/сутки.
- Нужен ли «общий read-only стенд» для лендинга (демо без создания), или создание песочницы — единственный вход?
- Хотим ли мы поддержать «принеси свой домен» для вебхуков (когда стенд должен звать
https://my-app.example/...— это уже работает, но требует egress-разрешений)? - Как быть с браузерным редиректом
SuccessURL/FailURLв поддоменной схеме — разрешать только https из соображений безопасности?