# SINGULAR SERVER v1.1.0 — сервер связей СИНГУЛЯРА

Опциональный сервер по ТЗ «СОБЫТИЕ / СОВМЕСТНОЕ ПЕНИЕ / ПАМЯТЬ»: он нужен только для
«моих людей» (·19), живых приглашений и центрального хранения записей. Без него СИНГУЛЯР
полноценен: зал ·18 работает по коду/QR (сигналинг через публичные реле), личность живёт
на устройстве. **Сеть — функция, не фундаментальная зависимость.**

**v1.40.0 «РЕВИЗИЯ» — НОЛЬ ВНЕШНИХ ЗАВИСИМОСТЕЙ.** WebSocket (RFC 6455) встроен в
`server.mjs` своими руками — склонировал дом → запустил → работает. `npm install` больше
НЕ нужен ни для запуска, ни для тестов. Пакет `web-push` остаётся опциональным: без него
push просто пропускается (graceful), с ним — работает (нужны VAPID-ключи в окружении).

## Запуск

```bash
cd server
node server.mjs      # http://localhost:8787 — корень отдаёт СИНГУЛЯР_19_ЧЕЛОВЕК.html
node --test ../ИНСТРУМЕНТЫ/test_s19_human.js   # инварианты + интеграционный тест GC
# или: npm start / npm test — то же самое, без единой установки
```

Требование одно: node ≥ 22 (нативный WebSocket-клиент для тестов; сам сервер работает
и на более старых). Переменные окружения — см. `.env.example`. Для продакшена держите
за HTTPS-прокси (Caddy/nginx), иначе браузеры не дадут микрофон и push с домена.

## Контракт (SINGULAR SERVER v1)

| Метод | Путь | Что делает |
|---|---|---|
| GET | `/health` | живость, счётчики, TTL |
| GET | `/api/vapid-public` | публичный ключ Web Push |
| POST | `/api/register` | `{name, publicKey: ECDSA JWK}` → пользователь (UUID) |
| POST | `/api/auth/challenge` | `{userId}` → одноразовый вызов (5 мин) |
| POST | `/api/auth/verify` | подпись WebCrypto (IEEE P1363) → токен 12 ч |
| GET | `/api/me` | свои данные + друзья + входящие/исходящие запросы |
| GET | `/api/people?q=` | поиск людей по имени |
| POST | `/api/friends/request` | позвать в люди (решает получатель) |
| POST | `/api/friends/accept` | принять связь |
| POST | `/api/friends/remove` | убрать связь (можно позвать снова) |
| POST | `/api/friends/block` | закрыть дверь окончательно |
| POST | `/api/sessions` | пригласить спеть: `{invitees, hallCode, song}` — код зала ·18 внутри |
| POST | `/api/sessions/:id/join·leave·start` | жизнь события |
| POST | `/api/recordings` | финальный микс (header `x-session-id`) → TEMPORARY |
| POST | `/api/recordings/:id/manifest` | `manifest.json` (участники, песня, тайминги) |
| POST | `/api/recordings/:id/participants/:n` | голоса по-отдельности `pN.webm` (перемикширование) |
| POST | `/api/recordings/:id/agree` | согласие на сохранение (политика «все») |
| POST | `/api/recordings/:id/save` | человек решает: ◈ → MEMORY (заметка/фото) |
| GET | `/api/recordings/:id` | аудио сохранённого воспоминания |
| GET | `/api/memories` | список воспоминаний владельца |
| POST | `/api/ws/ticket` | одноразовый билет (30 с) на вход в ws — только под Bearer |
| WS | `/ws?ticket=` | presence + ретрансляция offer/answer/ice (только сигналинг, не звук) |

Путь `/ws?token=` закрыт НАВСЕГДА (v1.25.1, ревью S1): долгий секрет не ездит в URL.

## Принципы (не нарушать)

- **Временная запись ≠ вечная память.** `expiresAt = now + TTL_TEMP_MS` (7 дней по ТЗ).
  Никто не нажал ◈ → GC удаляет метаданные И блоб. Проверено тестом
  `CREATE → TEMPORARY → EXPIRE → GC → absent + absent`.
- **Сервер не решает за людей.** Память создаёт человек (политика `any` — владелец;
  политика `all` — согласие всех участников через `/agree`).
- **Аудио не в базе.** Метаданные — JSON с атомарной записью (интерфейс один —
  замена на SQLite не переписывает сервер); блобы — файлы
  `data/recordings/{id}/{final.webm, manifest.json, participants/pN.webm}`.
- **Личность без паролей.** Ключ ECDSA P-256 non-exportable остаётся на устройстве,
  сервер хранит публичный JWK. Технический UUID — не пользовательский код.
- **Живые события не теряются.** Если получатель офлайн — приглашение ждёт его в
  очереди (сутки) и доставляется при подключении.

## Что это НЕ (честно)

- Не соцсеть: нет лент, лайков, подписчиков, публичных профилей, рекомендаций.
- Не SFU: голос идёт P2P между браузерами (mesh ≤ 4–5); сервер только сигналинг.
- Не production-deploy: UNKNOWN до реальной установки — TURN/брандмауэр,
  фоновый push на iOS/Android, нагрузка, бэкапы. См. `ДОКУМЕНТЫ/ОТЧЁТ_ПОЛНЫЙ.md`.
