Skip to content

© 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. Цели и не-цели ​

Цели

  1. Каждый пользователь (или каждая команда) получает изолированную песочницу со своими кредами и своим состоянием.
  2. Песочница создаётся без регистрации (демо) или по токену (команды).
  3. Всё живёт ограниченное время (TTL) и очищается автоматически; есть явный reset.
  4. Self-hosted режим не усложняется: флагом MOCK_TENANCY=off (по умолчанию) поведение остаётся сегодняшним (одна песочница = весь стенд).
  5. Соблюдение протокола: клиент по-прежнему ходит на «как у банка» пути; ничего в контракте не меняется (никаких /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
DoSquota: 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. План внедрения (этапы) ​

  1. Рефакторинг адаптеров в фабрики (createBankMock(ctx), createKassaMock(ctx)), self-hosted поведение не меняется; тесты остаются зелёными. ≈ 1–2 дня.
  2. Слой песочниц: реестр, resolveSandbox, генерация кредов, /sandboxes*. ≈ 2 дня.
  3. Admin-scoped: /admin по ownerToken, действия только внутри песочницы. ≈ 0.5 дня.
  4. Quota/лимиты/TTL/автоочистка. ≈ 1 день.
  5. Лендинг + доки + hosted-конфиг (wildcard DNS/TLS, egress). ≈ 1–2 дня.

10. Открытые вопросы ​

  1. Демо-песочницы без регистрации — как защититься от массового создания (капча, лимит по IP, «очередь»)? Предложение: лимит 3 песочницы/IP/сутки.
  2. Нужен ли «общий read-only стенд» для лендинга (демо без создания), или создание песочницы — единственный вход?
  3. Хотим ли мы поддержать «принеси свой домен» для вебхуков (когда стенд должен звать https://my-app.example/... — это уже работает, но требует egress-разрешений)?
  4. Как быть с браузерным редиректом SuccessURL/FailURL в поддоменной схеме — разрешать только https из соображений безопасности?