Skip to content

Що потрібно на сервері

Один порт, п'ять змінних оточення і квиток, який живе п'ятнадцять хвилин. Ця сторінка — усе, що лежить між «у нас є агент» і «на сайті клієнта з'явився чат».

Один порт, і тільки він

Браузер відвідувача розмовляє не з самим api. Опублікувати назовні всі маршрути, Swagger і канал інструментів заради чату в кутку сторінки — це величезні двері, відчинені з маленького приводу.

Тому хаб слухає власний окремий порт, і на цьому порту лежить рівно одне:

PORT самого apiBRIDLE_PUBLIC_PORT
що слухаєувесь apiодин простір імен WebSocket
маршрутиусі ендпоінти, Swagger, канал інструментівPOST /visitor і більше нічого
дотягується добази, ключа секретів, кластераходу агента, і тільки
опублікованийнітак, і тільки він

На цьому порту немає маршруту, який можна опублікувати недбало. Це і є шукана властивість, заради неї поділ і зроблено.

П'ять змінних

bash
# Яка половина пари — цей процес. `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. Перевірку навішено руками на один обробник, тому маршрут, доданий завтра, закритий для ключа за замовчуванням, а не за підсумками рев'ю, — і вона не заводить принципала, тож маршрут, якому її причепили б помилково, відмовив би, а не розширив права.
  • Чужу команду. Команда береться з ключа, агент — зі шляху; якщо вони не збіглися, відповідь така сама «немає такого агента», яку отримує сторонній, — тобто ключем не можна з'ясувати, які ідентифікатори агентів існують.
  • Кабінет. Випуск і відкликання ключів як і раніше потребують сесії учасника.

І тоді сторінка не носить жодного секрету

ts
init({
  apiUrl,
  agentId,
  token: () => fetch('/api/agent-token').then((r) => r.json()).then((t) => t.token),
})

/api/agent-token — це ВАШ маршрут, і особистість він має брати з сесії покупця на вашому сервері, а не з того, що надіслав браузер. У цьому й уся різниця між «сайт каже, хто це» і «браузер каже, хто він».

Коли спливає вставлений руками квиток

Є один випадок, де вставлений квиток — єдиний вихід: статичний сайт без бекенда — зібрана документація, лендинг на файловому хостингу. Покликати випуск там нікому, а ключ команди класти у збірку не можна: збірка публікується.

У такого сайту чат перестає відповідати разом із квитком, і віджет повідомляє INVALID_TOKEN. Це строк робить свою роботу, а не поломка. Готуйтеся повторювати: п'ятнадцять хвилин — це п'ятнадцять хвилин.

Демо працює в обох режимах і каже, у якому воно зараз: із ключем команди квитка в ньому немає взагалі, а зі вставленим — воно читає власний квиток, показує залишок, а коли час вийшов, каже саме це, а не замовкає.

Що ніколи не опиняється на вашому сайті

  • BRIDLE_JWT_SECRET і будь-який інший секрет установки.
  • Ключ команди. Він живе на вашому сервері. У джерелі сторінки або в опублікованій збірці це облікові дані для того, щоб назвати будь-якого покупця вашої команди ким завгодно.
  • Облікові дані, якими входять ваші оператори. У вбудовування свій секрет і свій квиток, і поділ зроблено навмисно.
  • Власні секрети, ключі та підключення агента. Розмова відвідувача їх не читає, і жодне налаштування цього не змінює.