1.**UUID v4 достаточен для идентификации (#1013).**В Baton нет protected resources. UUID — это просто "кто нажал кнопку". Утечка UUID безвредна — атакующий может отправить ложный сигнал, но это решается на уровне модерации Telegram-группы, а не криптографии.
2.**localStorage проверяется реальной записью, не `typeof` (#1024):**
```javascript
function isStorageAvailable(storage) {
try {
const key = '__baton_test__';
storage.setItem(key, '1');
storage.removeItem(key);
return true;
} catch (e) {
return false;
}
}
```
`typeof localStorage !== 'undefined'` вернёт `true` в iOS private mode, но `.setItem()` бросит `SecurityError`. Только реальная запись выявляет проблему.
3.**Цепочка fallback (#1025) гарантирует работу на любой платформе:**
- **localStorage** (нормальный режим): UUID живёт бессрочно*
- **sessionStorage** (iOS private mode fallback): UUID живёт до закрытия вкладки
- **in-memory** (крайний случай, CSP/iframe restrictions): UUID живёт до перезагрузки страницы
*На iOS — до 7 дней неактивности или ручной очистки.
4.**JWT отклонён** — избыточная сложность. Baton не имеет ролей, permissions, или данных требующих авторизации. `run_in_executor` для bcrypt (#1004) — это overhead ради несуществующей потребности.
5.**Session cookies отклонены** — серверное состояние противоречит stateless архитектуре. SameSite и iOS cookie policies создают edge cases без пользы.
---
## Реализация (спецификация для dev-агента)
### Клиентская сторона (app.js)
```javascript
let _memoryStore = {};
function getStorage() {
if (isStorageAvailable(localStorage)) return localStorage;
if (isStorageAvailable(sessionStorage)) return sessionStorage;
return {
getItem: (k) => _memoryStore[k] || null,
setItem: (k, v) => { _memoryStore[k] = v; },
removeItem: (k) => { delete _memoryStore[k]; }
};
}
function getOrCreateUserId() {
const storage = getStorage();
let id = storage.getItem('baton_user_id');
if (!id) {
id = crypto.randomUUID();
storage.setItem('baton_user_id', id);
}
return id;
}
```
### Серверная сторона (backend)
-`POST /api/register` принимает `{uuid, name}`, возвращает `{user_id, uuid}`
- Idempotent: `INSERT OR IGNORE INTO users (uuid, name)` — повторный вызов с тем же UUID возвращает существующую запись
- UUID в таблице `users` — UNIQUE constraint
### Поток первого визита
```
1. PWA открыт → getOrCreateUserId() → UUID сгенерирован
2. Показать форму ввода имени
3. POST /api/register {uuid, name} → 200 OK
4. UUID сохранён в storage → следующий визит: UUID уже есть, форма не показывается
1.**UUID = устройство, не человек.** Один пользователь на двух устройствах = два UUID. Это приемлемо: цель — знать ИМЯ нажавшего, а не объединять профили.
2.**Потеря UUID → повторная регистрация.** При очистке данных или 7-дневной iOS-очистке пользователь получит новый UUID и должен будет заново ввести имя. Частота: низкая (пользователи экстренного приложения открывают его при установке и при экстренных ситуациях).
3.**Приватный режим = временный UUID.** Это ожидаемое поведение, не баг. Пользователь в private mode осознанно отказывается от персистентности.
4.**Нет миграции данных.** UUID не переносится между устройствами. Если потребуется в будущем — добавить Telegram user ID как secondary identifier (уже есть endpoint `/api/webhook/telegram` для `/start`).
5.**crypto.randomUUID() требует Secure Context.** HTTPS обязателен. На HTTP `crypto.randomUUID` вернёт `undefined` в некоторых браузерах → дополнительный fallback через `URL.createObjectURL(new Blob()).slice(-36)`НЕ нужен, т.к. HTTPS обязателен для PWA (#1011).