# Подпись посетителя

> Ваш сервер подписывает токен, и чат знает, какой вошедший посетитель пишет.

Если посетители входят в аккаунт на вашем сайте, ваш сервер может сказать чату, кто пишет. Тогда:

- операторы видят аккаунт и поля, которые вы передали;
- посетитель находит свой разговор на любом устройстве и на любом домене проекта.

Без подписи браузер хранит разговор отдельно для каждого сайта. На новом устройстве или зеркале
посетитель начинает заново.

## Как это работает
1. Ваш сервер подписывает токен для вошедшего пользователя секретом подписи проекта.
2. Страница передаёт токен чату: через `data-identity` на теге или `identify(token)`.
3. Виджет отправляет токен в `POST https://api.tolkly.io/v1/projects/<project>/identify`. Сервер проверяет
   его и говорит, какой разговор принадлежит посетителю: тот, что в этом браузере, а если его
   нет — последний открытый разговор посетителя.

Сам виджет ничего не проверяет. Если сервер отклонил токен, ему не доверяют даже частично: чат
работает как анонимный.

## Токен
Токен — это JWT с подписью HS256. Сервер всегда читает алгоритм как HS256, что бы ни стояло в
заголовке. Если в заголовке другой алгоритм, включая `none`, токен отклоняется.

| Поле | Обязательно | Что это |
|---|---|---|
| `sub` | да | ID пользователя у вас, строка (число принимается как строка), до 200 символов. С более длинным токен отклоняется. |
| `name` | нет | Имя, которое видят операторы, до 120 символов. |
| `email` | нет | Отбрасывается, если не похоже на почту. |
| `phone` | нет | Отбрасывается, если не похоже на номер телефона. |
| `fields` | нет | Объект с полями, которые объявил проект ([ниже](#fields)). |
| `exp` | нет | Срок действия в секундах от начала эпохи. Без него токен действует, пока действует секрет. |

`exp` проверяется с запасом в 60 секунд. Так немного отстающие часы не отклонят свежий токен.
Токен длиннее 8192 символов отклоняется без чтения.

Содержимое токена подписано, но не зашифровано: посетитель может прочитать его на странице.
Передавайте только то, что ему можно видеть.

## Поля
Поля у каждого проекта свои: номер счёта, статус KYC, VIP-уровень, ссылка на пользователя в вашей
админке. Каждое поле проект объявляет с ключом и типом. Необъявленные поля в `fields`
отбрасываются, как и значения неверного типа. Поля объявляют владельцы и администраторы компании.

| Тип | Принимает |
|---|---|
| `text` | строку до 500 символов (число принимается как текст) |
| `number` | число или строку с числом |
| `date` | `YYYY-MM-DD` или полное время в ISO 8601 |
| `list` | массив строк или чисел, каждое до 100 символов; сохраняются первые 20. Одно значение считается одним элементом. Если хоть один элемент пустой, слишком длинный или другого типа, отбрасывается весь список. |
| `link` | URL `http:` или `https:`, до 2000 символов |
| `boolean` | `true` или `false` (а также `"true"`, `"false"`, `1`, `0`) |

Поля объявляются в разделе проекта «Установка» › «Дополнительно», блок «Поля посетителя»:
название, которое видят операторы, ключ, который пишет ваш сервер, тип и показывать ли значение
операторам. Ключ после создания не меняется: ваш сервер уже его присылает, поэтому новый ключ —
новое поле. Выключите «Показывать операторам» для значения, которое нужно только правилам,
например оценки риска.

## Секрет
Владельцы и администраторы компании находят его в разделе проекта «Установка»: «Дополнительно» › блок «Подпись
посетителя» › «Показать секрет».

Храните секрет на сервере сайта, в переменной окружения, например `TOLKLY_IDENTITY_SECRET`.
Никогда не кладите его в код для браузера или в страницу: с ним можно войти под любым вашим
клиентом.

## Подпись на вашем сервере
Node.js:

```js
// npm install jsonwebtoken
import jwt from 'jsonwebtoken'

const token = jwt.sign(
  {
    sub: String(user.id),
    name: user.name,
    email: user.email,
  },
  process.env.TOLKLY_IDENTITY_SECRET,
  { algorithm: 'HS256', expiresIn: '12h' },
)
```

PHP:

```php
// composer require firebase/php-jwt
use Firebase\JWT\JWT;

$token = JWT::encode([
  'sub' => (string) $user->id,
  'name' => $user->name,
  'email' => $user->email,
  'exp' => time() + 12 * 3600,
], getenv('TOLKLY_IDENTITY_SECRET'), 'HS256');
```

Python:

```python
# pip install pyjwt
import os, time, jwt

token = jwt.encode({
    "sub": str(user.id),
    "name": user.name,
    "email": user.email,
    "exp": int(time.time()) + 12 * 3600,
}, os.environ["TOLKLY_IDENTITY_SECRET"], algorithm="HS256")
```

Добавьте `fields` рядом с остальными полями, например `fields: { vip: user.vip }`.

## Передайте токен чату
Если страница собирается для вошедшего пользователя, поставьте токен на тег:

```html
<tolkly-chat endpoint="https://api.tolkly.io" project="your-project"
  data-identity="<token>"></tolkly-chat>
```

Если пользователь вошёл без перезагрузки страницы, передайте токен из скрипта:

```js
await customElements.whenDefined('tolkly-chat')
document.querySelector('tolkly-chat').identify(token)
```

Элемент также следит за `data-identity`: если поменять атрибут, чат заново определит посетителя.

При выходе вызовите `reset()` и перестаньте выводить `data-identity`. Чат забудет посетителя и
разговор, сохранённый в этом браузере, и следующий человек за этим компьютером начнёт заново. Если
в браузере лежит разговор другого подписанного посетителя, сервер тоже выдаст новый разговор.

## Поручить ИИ-агенту
У владельцев и администраторов есть кнопка «Поручить ИИ-агенту»: «Установка» › «Дополнительно», блок «Подпись
посетителя». Она даёт промпт ниже, с уже подставленным проектом, для агента, который правит сайт
(Claude Code, Cursor). По этому промпту агент:

- подписывает токен на сервере секретом из `TOLKLY_IDENTITY_SECRET`;
- передаёт токен через `data-identity` или `identify(token)`;
- вызывает `reset()` при выходе;
- проверяет результат в карточке посетителя в инбоксе.

Промпт называет переменную окружения, но не сам секрет. Здесь вместо идентификатора проекта стоит
`your-project`. Скопировать промпт: «Открыть в…» › «Промпт для ИИ-агента».

```text
Добавь подпись посетителя в чат поддержки Tolkly на этом сайте, чтобы операторы видели, какой клиент пишет.

Проект: your-project
Endpoint виджета: https://api.tolkly.io
Секрет: переменная окружения TOLKLY_IDENTITY_SECRET на сервере сайта. Владелец копирует его в Tolkly: «Установка» › «Дополнительно» › «Подпись посетителя» › «Показать секрет».

1. Прочитай https://tolkly.io/docs/en/identity.md. Если страница не открывается, хватит этого промпта.
2. Найди, где сервер сайта знает вошедшего пользователя (сессия, middleware авторизации), и шаблон, который выводит тег <tolkly-chat>. Если тега <tolkly-chat> на сайте ещё нет, остановись и скажи мне: сначала ставится сам чат.
3. На сервере подпиши для вошедшего пользователя JWT с алгоритмом HS256 секретом из TOLKLY_IDENTITY_SECRET, обычной для платформы библиотекой JWT. Никогда не подписывай в коде страницы и не передавай секрет в страницу. Если переменная не задана, остановись и попроси меня её задать; секрет в репозиторий не записывай.
4. Поля: "sub" — id пользователя строкой (обязательно, до 200 символов); "name" — имя, которое видят операторы (до 120); "email" и "phone", если они есть на сайте; "exp" — срок действия в секундах от начала эпохи, например на 12 часов вперёд. Дополнительные данные — в "fields": {…}, только ключи, которые объявил проект (они перечислены в том же блоке под «Поля, которые может передать сайт»); всё остальное отбрасывается. Содержимое токена видно в странице: передавай только то, что можно видеть посетителю.
5. Передай токен чату. Когда страница выводится для вошедшего посетителя, поставь его на тег: data-identity="<token>". После входа без перезагрузки страницы вызови identify(token) у элемента: await customElements.whenDefined('tolkly-chat'); document.querySelector('tolkly-chat').identify(token). Анонимному посетителю data-identity не выводи.
6. При выходе вызови document.querySelector('tolkly-chat').reset() и перестань выводить data-identity, чтобы следующий человек за этим компьютером начал с чистого листа.
7. Проверь в браузере: войди на сайте, открой чат и напиши сообщение. В инбоксе Tolkly, во вкладке «Все», в карточке посетителя «Аккаунт на сайте» виден id пользователя с отметкой «Проверено». В консоли браузера нет строки "tolkly: the identity token was refused (…)"; если она есть, слово в скобках говорит почему (malformed, algorithm, signature, expired). Потом выйди и снова открой чат: он анонимный, с новым разговором. Потом перечисли изменённые файлы.
```

## Без подписи
`identify({ id, name, email, phone, fields })` называет посетителя без токена. Операторы видят эти
данные с пометкой «Не проверено». Они сохраняются только в этом разговоре. Они не попадают в
историю между устройствами и не забирают разговор подписанного посетителя.

## Отклонённый токен
Чат работает как анонимный и пишет причину в консоль браузера:

```text
tolkly: the identity token was refused (signature)
```

| Код | Почему |
|---|---|
| `malformed` | Не три части base64url с JSON, нет `sub` или он длиннее 200 символов, `exp` не число, или токен длиннее 8192 символов. |
| `algorithm` | В заголовке другой алгоритм, не HS256. |
| `signature` | Подписан другим секретом: старым или от другого проекта. |
| `expired` | Срок `exp` истёк, даже с учётом допустимого расхождения часов. |

## Смена секрета
Нажмите «Выпустить новый секрет» в приложении (только для владельцев и администраторов). Старый секрет сразу
перестаёт работать. Подписанные им токены отклоняются с кодом `signature`, пока ваш сервер не
начнёт подписывать новым. Поэтому меняйте секрет вместе с выкладкой.
