--- компонент: Айфрейм — иконка в правом меню Кобры статус: черновик v2 обновлено: 2026-08-28 источники: @ai-cobra/webapp-sdk@0.2.0, док ядра «Права в ai-cobra», канон COBRA (person-iframe) связано: [[chesuyka]] — поверхности и права --- # Айфрейм ## Что это **Айфрейм — дверь для нового: iframe-интеграции внутри Кобры.** Ваше веб-приложение живёт на своём домене, а Кобра открывает его в рамке прямо у себя — с темой, языком и подписанным контекстом. Так внутрь встраиваются и наши чешуйки, и внешние сервисы. Оператор попадает в него **иконкой в правом меню**: нажал — рядом с перепиской раскрылся чужой интерфейс, окна переключать не надо. Канон формулирует это как дверь в стене: «за ней комната, которую строил кто-то другой: своя мебель, свои правила. Но наличник подогнан под ваши обои, ручка на привычной высоте, и вы не выходите на улицу, чтобы туда попасть». Правило двери — **гость входит внутрь, а не вы выходите наружу**. Ядро при этом не переписывается. В этом весь смысл: у платформы появляется новая способность, а её код не меняется ни на строку. ## Что сюда не относится Механизмов показа во вселенной несколько, и путать их не надо — у каждого своё имя и свой файл: | Механизм | Где живёт | Где описан | |---|---|---| | **Айфрейм** | иконка в правом меню Кобры | этот файл | | **Карманный ВебАпп** | в переписке мессенджера, на телефоне | [chesuyka.md](manifest-chesuyka.md), раздел про карман | | **Капюшон** | МастерАпп: одна дверь в мессенджере, за ней всё остальное | [master-app.md](manifest-master-app.md) | | **Страница-артефакт** | внутри папки-проекта | [chesuyka.md](manifest-chesuyka.md) | Всё, что ниже, — **только про Айфрейм**. У кармана своя авторизация, свой бэкенд и свои правила; смешивать их — самая дорогая ошибка в этой теме. --- # Из чего состоит Айфрейм — это протокол между двумя сторонами. Обе половины лежат в одном пакете: ```bash npm i @ai-cobra/webapp-sdk ``` | Сторона | Импорт | Кто её пишет | |---|---|---| | **Гость** — ваша чешуйка внутри рамки | `@ai-cobra/webapp-sdk` | вы | | **Хост** — рабочее место, которое рамку открывает | `@ai-cobra/webapp-sdk/host` | ядро (или вы, если встраиваете у себя) | ## Три вещи, которые решает протокол 1. **Достать подписанный контекст** — кто оператор, из какого чата открыли, что известно о клиенте. 2. **Поговорить с рабочим местом** — вставить текст в поле ответа, закрыть вкладку, объявить свою кнопку в чатах. 3. **Держать тему в согласии с хостом** — светлая или тёмная, и перерисоваться, когда оператор переключил. ## Сообщения Ходят через `postMessage` в обе стороны и всегда несут три поля: `source` (кто отправитель), `v` (версия протокола, сейчас `1`), `event` (что произошло). | Константа | Значение | |---|---| | `GUEST_SOURCE` | `cobra-webapp` — говорит встроенное приложение | | `HOST_SOURCE` | `cobra-host` — говорит рабочее место | | `LEGACY_GUEST_SOURCE` | `motorland-webapp` — первый модуль, написанный до появления SDK. Хост принимает и его | Версия нужна, чтобы хост мог узнать модуль, собранный на старом SDK, и не сломать его. Без неё менять протокол было бы нельзя никогда. ## Где стоит иконка — и что от этого меняется Основное место Айфрейма — **иконка в правом меню**, рядом с перепиской. Именно там его и открывает оператор в обычной работе. По документу ядра у модуля есть и вторая точка показа — иконка в левом рейле, открывающая его полноэкранно. Разница не косметическая: **от места зависит, что приедет в подписи**. | Откуда открыт | Как выглядит | Что в подписи | |---|---|---| | **Иконка в правом меню** (основное) | рядом с перепиской | `operator` + `chat{kind, id}` + `client{chat_id, customer_id, avito_id, crm_id, phone}` | | Иконка в левом рейле | во весь экран | только `operator`, `auth_date`, `hash`. **Ни `chat`, ни `client`** | Отсюда практическое следствие: экран, открытый из рейла, **не имеет права рассчитывать на чат**. Если чешуйка живёт только рядом с перепиской — это нормально, но тогда полноэкранный вход должен показывать не пустоту, а список или поиск. --- # Как происходит авторизация Схема ровно как в Telegram Mini Apps — и это не совпадение: имена полей выбраны такими же намеренно, чтобы о правилах нельзя было забыть. ## Шаг 1. Кобра подписывает контекст При **каждом открытии** (не при подключении модуля) ядро собирает контекст, подписывает его секретом интеграции и кладёт в адрес параметром `initData`. ``` secret_key = HMAC_SHA256(key="WebAppData", msg=<секрет интеграции>) data_check_string = поля, кроме hash, как "k=v", отсортированные по k, склеенные "\n" hash = HMAC_SHA256(key=secret_key, msg=data_check_string).hex() ``` Секрет хранится зашифрованным, показывается только владельцу и админам пространства (остальные видят лишь признак «токен есть»), наружу в публичное API не отдаётся вообще. ## Шаг 2. Приложение забирает подпись и отправляет её своему серверу ```ts import { createWebapp } from '@ai-cobra/webapp-sdk' const cobra = createWebapp() if (cobra.initData) { const session = await fetch('/api/auth/webapp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ init_data: cobra.initData }), }).then((r) => r.json()) cobra.clearInitDataFromUrl() // подписи в истории браузера делать нечего } ``` `cobra.initDataUnsafe` — та же подпись, разобранная в объект. **Читать можно, верить нельзя.** Имя выбрано пугающим специально. Годится только на то, чтобы отрисовать скелет экрана до ответа сервера; ни одно решение о правах на нём строиться не должно. ## Шаг 3. Сервер проверяет Пересчитать `hash` тем же алгоритмом и сверить. Отдельно проверить **свежесть** `auth_date` — обычно не старше суток. И только после этого выдавать сессию. Проверяет **ваш** сервер, потому что секрет интеграции есть только у него. SDK этим не занимается намеренно: эндпоинт у каждого приложения свой, и зашивать чужой адрес в пакет значило бы сделать его непригодным для всех остальных. ## Ключевая мысль: подпись — это удостоверение, а не ключ **Открытие чешуйки само по себе не даёт ей никаких прав в Кобре.** Оно даёт доказательство, кто на неё смотрит. Через SDK модуль может ровно три вещи: вставить текст в поле ввода оператора, закрыть свою вкладку и сообщить, что загрузился. Ни прочитать переписку, ни написать клиенту он так не может. ## Как получить доступ к данным Читать переписку и писать в неё через рамку **нельзя**. Есть ровно два пути, и оба заводятся отдельно от айфрейма: **1. Токен публичного API** (`wat_…`) со списком скоупов, выпускает владелец или админ. Модуль ходит в `/api/public/v1` и может ровно то, что в его скоупах. Типичный набор карточки клиента: `chats:read` плюс чтение «своих полей». **2. Свой MCP-сервер.** При подключении модуля указывается его адрес — заводится строка `mcp_servers`, и инструменты чешуйки становятся доступны агентам и терминалам пространства через штатный прокси. Отдельного пути нет намеренно. Токен подписи и токен публичного API — **два разных секрета**. Первый удостоверяет человека, второй даёт права. --- # Что умеет гость | Член | Что делает | |---|---| | `initData` | подпись как есть — её и отправляют своему серверу | | `initDataUnsafe` | разобранный объект. Читать можно, верить нельзя | | `isEmbedded` | открыто внутри рабочего места или напрямую в браузере | | `canTalkToHost` | встроено **и** знает, кому адресовать. По нему решают, показывать ли кнопки «в чат» | | `theme` | `'light' \| 'dark'` | | `ready()` | сказать хосту, что загрузились. Возвращает `false`, если сообщение не ушло | | `insertText(text)` | вставить текст в поле ответа оператора. **Не отправляет** сообщение клиенту | | `close()` | попросить закрыть вкладку | | `defineChatButton({ icon, label, path })` | объявить кнопку модуля во всех чатах у всех операторов. Хост сохраняет её на сервере. `label: null` — чистая иконка. В README не описана, есть в типах | | `applyTheme()` / `onThemeChange(fn)` | проставить `data-theme` на `` / подписка на смену | | `clearInitDataFromUrl()` | убрать подпись из адресной строки после обмена на сессию | | `dispose()` | снять обработчики: тесты и размонтирование | `insertText` не отправляет сообщение осознанно: текст попадает в поле ввода, отправляет его оператор, посмотрев, что вставилось. Модуль не должен писать клиенту от имени человека. ## Тема ```ts cobra.applyTheme() cobra.onThemeChange(() => cobra.applyTheme()) ``` До первого сообщения хоста тема берётся из параметра `?theme=`, а если его нет — из системной настройки. **Переключателя темы внутри модуля не делать**: он живёт в чужом окне и должен выглядеть его частью. ## Внешний вид Пакет отдаёт UI-кит, чтобы один и тот же компонент выглядел одинаково в любом интерфейсе Кобры: ```ts import '@ai-cobra/webapp-sdk/tokens.css' // переменные: цвета, --ck-* отступы, тени, размеры import '@ai-cobra/webapp-sdk/ui.css' // .ck-btn, .ck-card, .ck-badge, .ck-chip, .ck-combo… import '@ai-cobra/webapp-sdk/ui-x.css' // .ck-alert, .ck-avatar, .ck-calendar, .ck-carousel… ``` `tokens.css` — зеркало `ai-cobra/frontend/src/index.css` (акцент `#0e9d7d`, тёплый фон `#efeee9`). Нужен, чтобы модуль выглядел правильно при автономной разработке, когда оболочка ещё ничего не прислала. --- # Что делает хост ```ts import { createWebappHost } from '@ai-cobra/webapp-sdk/host' const host = createWebappHost({ // перечитывается на каждое сообщение: вкладки открываются и закрываются allowedOrigins: () => openTabs.map((tab) => new URL(tab.url).origin), onInsertText: (text) => composer.insert(text), onClose: (context) => closeTabByOrigin(context.origin), }) host.sendTheme(iframe.contentWindow, appOrigin, 'dark') ``` ## Безопасность рамки Три правила, и каждое закрывает конкретную атаку: 1. **Гость шлёт только на origin хоста.** Он приезжает в адресе параметром `cobra_host`. Нет параметра — **SDK молчит**. Лучше молчащая кнопка, чем текст оператора, отправленный чужому окну, которое открыло вас в своём фрейме. 2. **Слать в `'*'` нельзя.** Никогда. Страницу может открыть в своём фрейме кто угодно. 3. **Хост отбрасывает сообщения с чужого origin.** Иначе любая страница в любой рамке могла бы подсунуть оператору текст в ответ клиенту. --- # Обязательный первый экран: дашборд **У каждой чешуйки первым экраном идёт дашборд состояния.** Не «главная», не список — именно экран, по которому за пять секунд видно, что подключено, что работает и почему не работает. Причина простая: айфрейм — это стык четырёх невидимых вещей (подпись, права, своя база, чужой сервис). Когда что-то не работает, снаружи это выглядит одинаково — пустой экран. Дашборд превращает невидимое в видимое, и разбор проблемы занимает минуту вместо дня. Он полезен всем троим: разработчику при отладке, внедренцу при установке у заказчика, оператору — когда «ничего не грузится» и надо понять, звать ли поддержку. ## Что на нём должно быть | Блок | Что показывает | Зачем | |---|---|---| | **Кто я** | оператор, почта, пространство, роль | сразу видно, если открыли не тем аккаунтом | | **Откуда открыт** | рейл или чат; если чат — его id и что известно о клиенте | половина багов «карточка пустая» — это открытие из рейла | | **Подпись** | проверена или нет, когда выдана, сколько осталось до протухания | отвечает на «почему меня выкинуло» | | **Хост** | origin из `cobra_host`, версия протокола, `isEmbedded`, `canTalkToHost` | видно, что мы вообще в рамке, а не в голом браузере | | **Права** | какие скоупы у токена, чего не хватает под текущий экран | вместо загадочного `insufficient_scope` | | **Своя база** | последняя синхронизация, размер очереди изменений, «мёртвые» записи | видно, что данные отстают, ещё до жалобы | | **Чужой сервис** | доступен ли Б24/CRM, когда отвечал в последний раз | сразу отделяет «мы сломались» от «у них лежит» | | **MCP** | сколько инструментов отдано, когда последний вызов | видно, что агенты вообще пользуются | | **SSI** | настройки заполнены или нет, каких не хватает | самая частая причина «не работает» | ## Кнопки самопроверки Три штуки, которые экономят часы: - **«Проверить подпись»** — сходить на свой сервер и показать вердикт целиком. - **«Вставить тестовый текст»** — доказывает, что связь с хостом живая, и `insertText` доходит. - **«Пинг MCP»** — дёрнуть свой самый безобидный инструмент и показать ответ. ## Правила дашборда 1. **Каждая строка — с отметкой времени.** Состояние без времени не отличается от протухшего. 2. **Красное объясняет себя.** Не «ошибка», а «нет скоупа `chats:read`, попросите у админа пространства». 3. **Ничего секретного.** Токены и ключи не показывать даже частично — только «есть/нет» и когда выдан. 4. **Он не заменяет работу.** Дашборд — первый экран, а не единственный: со второго начинается то, ради чего чешуйку и делали. 5. **Живёт и в проде.** Соблазн выключить его «для чистоты» велик, но именно в проде он и нужен. Спрятать за пунктом меню можно, удалять нельзя. --- # Чек-лист - [ ] подпись уходит на свой сервер, `hash` и свежесть `auth_date` проверяются - [ ] `clearInitDataFromUrl()` вызывается после обмена на сессию - [ ] ни одно решение о правах не принято на `initDataUnsafe` - [ ] `ready()` вызывается после загрузки - [ ] раскладка рейла работает без `chat` и `client` - [ ] тема берётся от хоста, своего переключателя нет - [ ] `canTalkToHost` проверяется перед показом кнопок «в чат» - [ ] нет ни одного `postMessage` в `'*'` - [ ] хост-сторона отбрасывает чужие origin - [ ] **первый экран — дашборд состояния, и он остаётся в проде** - [ ] на дашборде есть три кнопки самопроверки - [ ] дашборд не показывает секретов # Где обычно ломается 1. **Права на `initDataUnsafe`** — роль берётся из неподписанных данных, и «клиент» открывает панель руководителя, поправив одно поле. 2. **Подпись осталась в адресе** — она лежит в истории браузера и в реферерах. 3. **Экран рейла, которому нужен чат** — в полноэкранном режиме `chat` и `client` не приезжают. 4. **`postMessage('*')`** — текст оператора уходит чужому окну. 5. **Свой переключатель темы** — модуль спорит с оболочкой, выглядит инородно. 6. **Нет дашборда** — любая поломка выглядит как пустой экран, и разбор занимает день. 7. **Дашборд выключили в проде** — ровно там, где он и нужен. 8. **Спутали Айфрейм с карманным вебаппом** — у них разные бэкенды, разная авторизация и разная модель ролей. Код, написанный «как в Айфрейме», в кармане не заработает. # Источники - `@ai-cobra/webapp-sdk@0.2.0` — README, `dist/index.d.ts`, `dist/host.d.ts`, `protocol.d.ts` - Док ядра «Права в ai-cobra: чешуйки (модули), MCP и интерфейс» - Канон COBRA: `person-iframe.html` — Айфрейм как герой вселенной - [Спецификация чешуйки](manifest-chesuyka.md) — поверхности, права, мерка - [МастерАпп «Капюшон»](manifest-master-app.md) и [ADA](manifest-ada.md) — другие механизмы, не путать --- _Источник: `cobra-components/iframe.md` в репозитории audio-talks. Эта копия обновляется скриптом `sync-components.mjs` — правки вносить в источник, не здесь._