Що потрібно на сервері
Один порт, п'ять змінних оточення і квиток, який живе п'ятнадцять хвилин. Ця сторінка — усе, що лежить між «у нас є агент» і «на сайті клієнта з'явився чат».
Один порт, і тільки він
Браузер відвідувача розмовляє не з самим api. Опублікувати назовні всі маршрути, Swagger і канал інструментів заради чату в кутку сторінки — це величезні двері, відчинені з маленького приводу.
Тому хаб слухає власний окремий порт, і на цьому порту лежить рівно одне:
PORT самого api | BRIDLE_PUBLIC_PORT | |
|---|---|---|
| що слухає | увесь api | один простір імен WebSocket |
| маршрути | усі ендпоінти, Swagger, канал інструментів | POST /visitor і більше нічого |
| дотягується до | бази, ключа секретів, кластера | ходу агента, і тільки |
| опублікований | ні | так, і тільки він |
На цьому порту немає маршруту, який можна опублікувати недбало. Це і є шукана властивість, заради неї поділ і зроблено.
П'ять змінних
# Яка половина пари — цей процес. `hub` (за замовчуванням) означає, що браузери
# підключаються СЮДИ і другий сервіс не потрібен.
BRIDLE_ROLE=hub
# ЄДИНИЙ порт, який ви публікуєте. Значення за замовчуванням немає: порт, що
# відкрився сам через незаповнену змінну, — та випадковість, заради якої варто
# написати один рядок.
BRIDLE_PUBLIC_PORT=3334
# Публічна адреса цього порту. Саме вона потрапить на сторінку як `apiUrl`.
BRIDLE_URL=https://хаб.вашої.установки
# Секрет, яким підписуються квитки вбудовування. НЕ той, яким входять ваші
# оператори: у вбудовування свій, і змішувати їх — помилка зі справжніми зубами.
BRIDLE_JWT_SECRET=<64 випадкових шістнадцяткових символи>
# Звідки береться бандл віджета. Необов'язково; за замовчуванням — стокова збірка.
BRIDLE_SDK_URL=https://bridle.cleanslice.org/sdk/latest.jsНе заповнено — отже, ця установка не публікує агентів. Прив'язки, як і раніше, зберігаються, випуск квитка відмовляє і називає відсутню змінну, порт не відкривається і сокет нікуди не йде. Ніщо не відчиняється наполовину.
BRIDLE_JWT_SECRET не залишає сервер
Це не те значення, яке будь-коли тримає код сайту. Сайт, що вміє підписати квиток сам, міг би назвати будь-якого відвідувача будь-ким іншим. Якщо ви ловите себе на тому, що вставляєте його у фронтенд, — вам насправді потрібен розділ Як випустити квиток на цій самій сторінці.
Друга роль
BRIDLE_ROLE=runtime — давніша схема: браузери розмовляють з окремим хабом Bridle, а api сам додзвонюється до нього ключем BRIDLE_API_KEY. Живою завжди є рівно одна з двох ролей. Агент, опублікований за однієї, лишається опублікованим після перемикання — ідентифікатор, уже вставлений у сторінку, працює далі.
За runtime тут немає POST /visitor: публічні двері чужі. Квитки відвідувачів у такій схемі випускає ваш власний бекенд.
Публікація агента
У кабінеті, на екрані агента «Сайт»: опублікувати агента в канал сайту. До цього навіть правильний квиток нічого не дає — чи опублікований агент, перевіряється при відкритті з'єднання і знову на кожному повідомленні.
Той самий екран каже, чи може установка публікувати взагалі. Якщо бракує BRIDLE_PUBLIC_PORT (або, за runtime, BRIDLE_API_KEY), він скаже це і назве змінну — до того, як щось натиснуто.
Як випустити квиток
Квиток випускає учасник команди агента, який увійшов у систему:
POST /agents/:id/channels/bridle/tokenНеавтентифікованого випуску немає, і немає способу випустити квиток для агента чужої команди.
- Строк за замовчуванням — 15 хвилин. Стеля — 24 години. Попросите більше — отримаєте стелю; попросите менше — отримаєте рівно стільки.
- Квиток прив'язаний до одного агента. Квиток, випущений для агента A і вставлений на сторінку, що вказує на агента B, розмови з B не відкриє. Це перевіряється двічі: коли відкривається сокет і потім на кожному повідомленні — бо вкладка, залишена відкритою на тиждень, переживає квиток у ній.
- Квиток — не особистість, не сесія і не право. Він каже, який агент і до якого моменту. Від чийого імені йде розмова, вирішує запис самого агента — див. Межі та права.
Чому він не може жити довше
Бо він лежить у HTML сторінки відкритим текстом і його бачить кожен, хто відкрив ваш сайт. Покласти його більше нікуди: браузеру треба щось пред'явити раніше, ніж у нього щось є. Настільки публічне — це сесія, а не ключ, і стеля в добу — те місце, де ця різниця закріплена.
Два види квитка
Квиток, що лежить на вашій сторінці, не називає нікого. Якби кожен браузер пред'являв його як є, усі відвідувачі були б однією людиною: одна розмова на всіх, один ліміт потоку і чужі повідомлення у своєму листуванні.
Тому є друга форма і один обмін між ними:
POST https://хаб.вашої.установки/visitor
{ "token": "<квиток сторінки>" }
→ { "token": "<квиток, що називає одного відвідувача>", "expiresAt": "…" }- Обміну не потрібна сесія — звертається браузер незнайомої людини з вашого сайту.
- Він нічого не відкриває наново: той, у кого є квиток сторінки, і так міг говорити з агентом. Це той самий доступ, лише поділений.
- Квиток відвідувача спливає разом із квитком сторінки, до миті.
- Пред'явлений назад квиток відвідувача повертає ту саму людину — це і повертає перезавантажену вкладку в її власну розмову.
Вставка, яку дає кабінет, робить цей обмін за вас, тримає результат у sessionStorage — прив'язаний до вкладки, сам до жодного запиту не додається, зникає разом із вкладкою — і навмисно не відкочується на квиток сторінки, якщо обмін не вдався. Відкат залишив би робочий чат, у якому двоє незнайомих людей тихо сидять в одному листуванні.
Як правильно: квиток випускає ваш бекенд, вставляти нічого
Усе, що вище, описує квиток, який людина робить руками. На справжньому сайті так не треба. П'ятнадцять хвилин — це п'ятнадцять хвилин, а чат, що помер за ніч, виглядає для всіх як зламана програма.
Токен приймається функцією, тому сторінка може питати квиток у вашого власного бекенда, а не носити його в собі. А бекенд отримує квиток за ключем команди: це облікові дані, які живуть на вашому сервері й уміють рівно одне.
1. Випустити ключ
Учасник команди, один раз:
POST /teams/{teamId}/embed-keys
{ "name": "бекенд магазину, прод", "expiresAt": "2027-01-01T00:00:00.000Z" }
→ { "id": "…", "key": "emk.0f3a…c19.Zm9vYmFy…", … }- Секрет показується один раз. Копії тут не лишається. Загубили — відкличте й випустіть новий; це і так правильні ліки.
- Задайте
expiresAt, якщо можете. Відкликання вимагає, щоб хтось пам'ятав про ключ; строк не вимагає нікого. - Відкликання —
DELETE /teams/{teamId}/embed-keys/{keyId}. Негайно, без відтермінування, і запис переживає відкликання, щоб інцидент можна було потім прочитати.
2. Випускати квиток на конкретну людину, сервер-сервер
POST /agents/{agentId}/channels/bridle/visitor
X-Embed-Key: emk.…
{ "externalId": "customer-4471", "name": "Анна", "email": "anna@example.com" }
→ { "token": "…", "expiresAt": "…" }externalId— це особистість, а ім'я, пошта й телефон — те, що про неї ВІДОМО. Той самийexternalIdзавтра — той самий лід, а не другий. Пошта зберігається й показується, але за нею ніколи не шукають: двоє покупців можуть опинитися з однією поштою через помилку самого сайту.- Людина записується ДО того, як квиток видано, тому живий квиток ніколи не називає того, про кого немає рядка. У списку лідів команди вона з'являється з тим, що ви надіслали, а не непрозорим рядком.
- Заголовок
X-Embed-Key, а неAuthorization— навмисно: ключ, надісланий як Bearer, буде відхилено як зіпсований JWT, а сесійний токен, надісланий сюди, — як «це не ключ». Жодна з відмов не залежить від того, чи хтось пам'ятає різницю.
Чого ключ не відкриває
- Жодного іншого маршруту цього api. Перевірку навішено руками на один обробник, тому маршрут, доданий завтра, закритий для ключа за замовчуванням, а не за підсумками рев'ю, — і вона не заводить принципала, тож маршрут, якому її причепили б помилково, відмовив би, а не розширив права.
- Чужу команду. Команда береться з ключа, агент — зі шляху; якщо вони не збіглися, відповідь така сама «немає такого агента», яку отримує сторонній, — тобто ключем не можна з'ясувати, які ідентифікатори агентів існують.
- Кабінет. Випуск і відкликання ключів як і раніше потребують сесії учасника.
І тоді сторінка не носить жодного секрету
init({
apiUrl,
agentId,
token: () => fetch('/api/agent-token').then((r) => r.json()).then((t) => t.token),
})/api/agent-token — це ВАШ маршрут, і особистість він має брати з сесії покупця на вашому сервері, а не з того, що надіслав браузер. У цьому й уся різниця між «сайт каже, хто це» і «браузер каже, хто він».
Коли спливає вставлений руками квиток
Є один випадок, де вставлений квиток — єдиний вихід: статичний сайт без бекенда — зібрана документація, лендинг на файловому хостингу. Покликати випуск там нікому, а ключ команди класти у збірку не можна: збірка публікується.
У такого сайту чат перестає відповідати разом із квитком, і віджет повідомляє INVALID_TOKEN. Це строк робить свою роботу, а не поломка. Готуйтеся повторювати: п'ятнадцять хвилин — це п'ятнадцять хвилин.
Демо працює в обох режимах і каже, у якому воно зараз: із ключем команди квитка в ньому немає взагалі, а зі вставленим — воно читає власний квиток, показує залишок, а коли час вийшов, каже саме це, а не замовкає.
Що ніколи не опиняється на вашому сайті
BRIDLE_JWT_SECRETі будь-який інший секрет установки.- Ключ команди. Він живе на вашому сервері. У джерелі сторінки або в опублікованій збірці це облікові дані для того, щоб назвати будь-якого покупця вашої команди ким завгодно.
- Облікові дані, якими входять ваші оператори. У вбудовування свій секрет і свій квиток, і поділ зроблено навмисно.
- Власні секрети, ключі та підключення агента. Розмова відвідувача їх не читає, і жодне налаштування цього не змінює.