[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"docs-infra\u002Fapp-runtime":3,"docs-tabs-infra\u002Fapp-runtime":6},{"content":4,"lastmod":5},"# Авторизация в приложении на Black Hole\n\nBlack Hole — это закрытый режим сервера приложения: снаружи он недоступен напрямую, единственный вход — через платформенный шлюз Gateway. Когда сотрудник открывает приложение внутри Битрикс24, Gateway проверяет, кто это, и кладёт данные пользователя прямо в заголовки запроса к серверу приложения. Приложению не нужны ни своя форма входа, ни хранение паролей: оно доверяет заголовкам от Gateway, а когда нужны данные портала — обращается к API за этого пользователя. Кто может открыть приложение, задаёт политика доступа — [Доступ и режимы](\u002Fdocs\u002Finfra\u002Faccess).\n\nGateway сам аутентифицирует пользователя и пробрасывает к приложению уже подписанный запрос. Браузер не видит и не хранит `vibe_session_*` — токен живёт только между Gateway и сервером приложения. Приложение получает заголовок `X-Vibe-Authorization: Bearer vibe_session_\u003C…>` и читает данные пользователя через `GET \u002Fv1\u002Fme`. Это BFF-паттерн (Backend-for-Frontend) — тот же, что у Cloudflare Access, Google IAP и AWS API Gateway.\n\nСкоуп: `vibe:apps` (на стороне V1 API), внутри туннеля — без явного скоупа, Gateway сам управляет доступом.\n\n[Заголовки запроса](#что-приходит-в-каждый-запрос) · [Параметры iframe](#url-параметры-iframe-после-placement-редиректа) · [Данные пользователя](#получение-данных-пользователя-get-v1-me) · [Примеры кода](#примеры-кода) · [Жизненный цикл сессии](#жизненный-цикл-сессии)\n\n## Жизненный цикл запроса\n\nПолный обмен — от открытия приложения в Битрикс24 до проксирования запроса на сервер приложения — показан ниже по шагам. Разбор ключевых моментов идёт сразу после схемы.\n\n```\nБитрикс24 placement                       Backend (vibecode.bitrix24.tech)\n   │                                         │\n   ├── POST \u002Fv1\u002Fbitrix-handler ──────────────►│  exchange OAuth code,\n   │     (B24 placement triple)               │  upsert AppUserToken,\n   │                                          │  createAppSession (mint vibe_session_*),\n   │                                          │  mint signed __init JWT (TTL 5 min)\n   │                                          │\n   │◄── 302 https:\u002F\u002Fapp-XXX\u002F?placement=…&member_id=…[&placement_options=…]&__init=\u003Cjwt> ──┤\n   │\nBrowser\n   │\n   ├── GET https:\u002F\u002Fapp-XXX\u002F?placement=…&member_id=…[&placement_options=…]&__init=\u003Cjwt> ───►│ Gateway\n   │                                                                     │  Priority 0 redeem:\n   │                                                                     │   • verify JWT (HMAC-SHA256, \"gateway-init\")\n   │                                                                     │   • subdomain binding\n   │                                                                     │   • mark jti used (one-time)\n   │                                                                     │   • store (sub, subdomain) → raw in cache\n   │                                                                     │   • mint _vibe_gw cookie\n   │                                                                     │\n   │◄────────── 302 https:\u002F\u002Fapp-XXX\u002F?placement=…&member_id=…[&placement_options=…] ───────┤\n   │                                          Set-Cookie: _vibe_gw=…\n   │\n   ├── GET https:\u002F\u002Fapp-XXX\u002F?placement=…&member_id=…[&placement_options=…] ──────────────►│ Gateway\n   │   Cookie: _vibe_gw=…                                                │  Priority 1 (cookie):\n   │                                                                     │   • resolve raw from cache\n   │                                                                     │   • strip incoming X-Vibe-Authorization (anti-spoof)\n   │                                                                     │   • inject X-Vibe-Authorization: Bearer vibe_session_…\n   │                                                                     │\n   │                                                                     ├── GET \u002F ─────► App server\n   │                                                                     │   X-Vibe-Authorization: Bearer vibe_session_…\n   │                                                                     │   Cookie: _vibe_gw=…  (Gateway-only; не для app)\n   │                                                                     │\n   │◄────────────────────── HTTP response ───────────────────────────────┤\n```\n\nКлючевая разница от стандартного OAuth:\n\n- **Браузер никогда не видит `vibe_session_*`** — даже в момент первого открытия. Токен передаётся только в подписанном одноразовом коде в URL, который Gateway сразу обменивает и стирает.\n- **`__init` — одноразовый**. Повторный обмен того же `jti` отклоняется. Время жизни — 5 минут. На медленных сетях загрузки iframe этого хватает с запасом — если не уложитесь в 5 минут, проверьте журналы Gateway `init code replay rejected` \u002F `init code verify failed`.\n- **Cookie `_vibe_gw`** живёт 10 минут — а у placement-приложений (открытых через встройку в Битрикс24) около 40 минут — и продлевается при активности. Не пытайтесь её читать в JS — она `HttpOnly`.\n- **Пробуждение из сна**: если сервер в `SLEEPING`, Gateway обменивает `__init` **до** страницы пробуждения, кладёт исходные данные в кэш и cookie в ответ. После переподключения агента браузер делает `location.replace` на чистый URL — кэш ещё не истёк, и `X-Vibe-Authorization` проставляется на первый же GET.\n\n## Что приходит в каждый запрос\n\n```\nGET \u002Fapi\u002Ftasks\nHost: app-9edac24091ba.vibecode.bitrix24.tech\nCookie: _vibe_gw=\u003C…>                       ← платформенная cookie, к приложению пробрасывается прозрачно\n\nX-Vibe-Request-Id:   req_\u003C16 hex chars>\nX-Vibe-User-Id:      42\nX-Vibe-Portal-Id:    f2342f7a-b1c5-4d50-8a3e-30219c5ae0c1\nX-Vibe-User-Name:         Иван Иванов\nX-Vibe-User-Name-Encoded: %D0%98%D0%B2%D0%B0%D0%BD%20%D0%98%D0%B2%D0%B0%D0%BD%D0%BE%D0%B2\nX-Vibe-User-Role:    MEMBER\nX-Vibe-Authorization: Bearer vibe_session_\u003C43 chars>\n```\n\nGateway проставляет семь заголовков на каждый проксируемый запрос. `X-Vibe-Request-Id` приходит на любой запрос, остальные — только когда пользователь аутентифицирован (пройдена авторизация через placement или по Bearer-токену):\n\n| Заголовок | Когда | Значение |\n|-----------|-------|----------|\n| `X-Vibe-Request-Id` | всегда | Идентификатор запроса для корреляции в журналах. Префикс `req_` + 16 hex-символов. |\n| `X-Vibe-User-Id` | аутентифицированный запрос | Числовой ID пользователя в Битрикс24 (например, `42`). Совпадает с `currentUser.bitrixUserId` в ответе `\u002Fv1\u002Fme`. |\n| `X-Vibe-Portal-Id` | аутентифицированный запрос | UUID портала во внутренней БД Вайбкод. Не путать с доменом — он отдельно. |\n| `X-Vibe-User-Name` | аутентифицированный запрос | Отображаемое имя пользователя в виде сырых UTF-8 байтов. ⚠️ Большинство HTTP-стеков (Express и др.) читают значения заголовков как latin1 (RFC 7230 §3.2.4), поэтому локализованные имена отображаются нечитаемыми символами (`ÐÐ²Ð°Ð½` вместо `Иван`). Для корректного имени используйте `X-Vibe-User-Name-Encoded` (ниже). Заголовок сохранён для обратной совместимости. |\n| `X-Vibe-User-Name-Encoded` | аутентифицированный запрос | То же отображаемое имя, в процентном кодировании по RFC 3986 (UTF-8). Декодируется любым стандартным декодером: в JS — `decodeURIComponent(req.headers['x-vibe-user-name-encoded'])`. **Рекомендуемый источник имени** вместо сырого `X-Vibe-User-Name`. |\n| `X-Vibe-User-Role` | аутентифицированный запрос | `ADMIN` или `MEMBER` — роль текущего пользователя (того, кто открыл приложение) на портале. `ADMIN` означает право управлять настройками приложения (предикат Битрикс24 `user.admin`) — в том числе у владельца портала. Это надёжный способ определить, администратор ли **текущий** пользователь. Признака администратора для произвольного пользователя из `GET \u002Fv1\u002Fusers` Битрикс24 не отдаёт (см. поле `isAdmin` в доках сущности users). |\n| `X-Vibe-Authorization` | аутентифицированный запрос | `Bearer vibe_session_\u003C…>` — токен доступа для серверных вызовов V1 API. |\n\n- **`X-Vibe-Authorization`** — единственный источник Bearer-токена. Платформенный префикс `X-Vibe-` выбран, чтобы не конфликтовать с собственной `Authorization`-цепочкой приложения (например, если оно проксирует к своему вышестоящему API). Любой входящий `X-Vibe-Authorization` от клиента Gateway срезает до проброса — приложение никогда не получит подделанный токен.\n- **`X-Vibe-User-Id` \u002F `-Portal-Id` \u002F `-User-Name` \u002F `-User-Role`** — быстрый способ опознать пользователя без дополнительного вызова `\u002Fv1\u002Fme`. Подходят для логов, аналитики и простого ACL. Для полного контекста (`scopes`, `capabilities`, `tariff`) всё равно делайте один вызов `\u002Fv1\u002Fme` и кэшируйте его на токен.\n- **Имя пользователя — читайте `X-Vibe-User-Name-Encoded`, не `X-Vibe-User-Name`.** Сырой `X-Vibe-User-Name` несёт UTF-8 байты, которые HTTP-стеки трактуют как latin1 → кириллица и локализованные имена отображаются нечитаемыми символами. Корректное имя: `const name = decodeURIComponent(req.headers['x-vibe-user-name-encoded'] ?? '')`. Старый заголовок оставлен как есть, чтобы не ломать приложения, уже использующие обходной путь `Buffer.from(name, 'latin1').toString('utf8')`.\n- **`X-Vibe-Request-Id`** — кладите в свой структурированный лог, чтобы трассировать запрос между Gateway и приложением. При обращении в поддержку Вайбкод присылайте этот ID — он есть и в журналах платформы.\n- **`Cookie: _vibe_gw`** — оставляется как есть, но это не токен доступа к V1 API. Это сессионный токен Gateway для быстрой авторизации последующих запросов. Приложению она не нужна и помечена `HttpOnly` — JS её даже не увидит.\n- **`access_token` \u002F `auth[*]` в теле запроса** — не приходят. Не пытайтесь их читать.\n\n## URL-параметры iframe после placement-редиректа\n\nПосле `POST \u002Fv1\u002Fbitrix-handler` платформа перенаправляет (302) браузер на `\u003CappUrl>` и добавляет в строку запроса:\n\n| Параметр | Когда | Значение |\n|----------|-------|----------|\n| `placement` | всегда | Код placement-а (`LEFT_MENU`, `CRM_DEAL_DETAIL_TAB`, `IM_TEXTAREA`, …). Совпадает с `PLACEMENT` в POST от B24. |\n| `member_id` | всегда | Идентификатор портала Bitrix24 (тот же `member_id`, что в B24 OAuth). |\n| `placement_options` | когда B24 присылает PLACEMENT_OPTIONS — типично для DETAIL_TAB \u002F DETAIL_TOOLBAR \u002F LIST_MENU и других placement'ов, где есть контекст сущности | **JSON-строка с контекстом сущности** — например `{\"ID\":\"42\"}` для CRM_DEAL_DETAIL_TAB. Сюда B24 кладёт ID текущей карточки (сделки\u002Fлида\u002Fконтакта\u002Fкомпании). |\n| `__init` | первый запрос | Одноразовый JWT для Gateway, обмениваемый на `_vibe_gw` cookie. После обмена — исчезает (замена на чистый URL). |\n\n**Заметка по именованию:** B24 шлёт это поле как `PLACEMENT_OPTIONS` (в верхнем регистре) в POST к `\u002Fv1\u002Fbitrix-handler`. Мы пробрасываем его под именем `placement_options` в нижнем регистре в URL iframe. Это одно и то же поле — ищите в спецификации B24 по имени в верхнем регистре.\n\n**Как читать `placement_options`** — забирайте из `URLSearchParams` и парсите JSON:\n\n```js\nconst params = new URLSearchParams(window.location.search)\nconst placement = params.get('placement')                  \u002F\u002F \"CRM_DEAL_DETAIL_TAB\"\nconst options = JSON.parse(params.get('placement_options') || '{}')\nconst entityId = options.ID                                \u002F\u002F \"42\" — текущая карточка\n```\n\n**Не используйте `BX24.placement.info()`** — SDK не видит контекст при перенаправлении через платформенный обработчик и вернёт `{placement:null, options:null}`. Всё, что вам нужно про placement и его параметры, уже лежит в URL.\n\n## Получение данных пользователя (`GET \u002Fv1\u002Fme`)\n\n`X-Vibe-Authorization` несёт только токен доступа, не данные пользователя. Чтобы узнать пользователя, портал, скоупы и `capabilities` — один раз вызываем `\u002Fv1\u002Fme` на сервере приложения и кэшируем в собственной сессии:\n\n```\nGET https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Fme\nX-Api-Key: \u003Cключ-вашего-приложения>\nAuthorization: Bearer vibe_session_\u003C…>     ← забрали из X-Vibe-Authorization\n\n200 OK\n{\n  \"success\": true,\n  \"data\": {\n    \"type\": \"oauth_app\",\n    \"portal\": \"company.bitrix24.ru\",\n    \"scopes\": [\"crm\", \"tasks\", \"imbot\", ...],\n    \"currentUser\": {\n      \"bitrixUserId\": \"42\",\n      \"_note\": \"B24 numeric user id of the OAuth-authorized end user (from Bearer session).\"\n    },\n    \"capabilities\": { \"servers\": { \"create\": { \"available\": true, \"reason\": \"TRIAL_ACTIVE\" }, ... }, ... },\n    \"tariff\": { ... },\n    \"app\": { \"title\": \"My App\", \"id\": \"\u003Cuuid>\" }\n  }\n}\n```\n\nПоказаны характерные поля. В ответе также есть `api` (список доступных эндпоинтов и сущностей), `oauth`, `oauthTutorial`, `deployment`, `infra`, `feedback` и другие — модель использует их, чтобы построить запросы без чтения остальной документации.\n\n**Идентификатор пользователя — `data.currentUser.bitrixUserId`** (строка с числовым ID Битрикс24). Не `userId`, не `user.id`, не верхнеуровневое поле — именно `currentUser.bitrixUserId`. Совпадает с заголовком `X-Vibe-User-Id` (см. секцию «Что приходит в каждый запрос»), который Gateway уже проставил в запрос. Для простой идентификации заголовка достаточно. `\u002Fv1\u002Fme` нужен, когда требуются `scopes`, `capabilities`, `tariff` или информация о приложении.\n\n**Без `Authorization: Bearer`** тот же `\u002Fv1\u002Fme` отвечает с **тем же набором полей**, только `currentUser` будет `null`. Это рабочий режим: используется для серверной инициализации приложения до OAuth — портал, скоупы и capabilities уже видны. Не пытайтесь распознавать «нет сессии» по 401 — его не будет, проверяйте `currentUser !== null`.\n\nКэш — 24 часа TTL. После 401 на V1 — отправляйте пользователя обратно на placement (см. ниже).\n\n## Примеры кода\n\n### Node.js \u002F Express\n\n```js\nimport express from 'express'\nconst app = express()\n\nconst VIBE_API = 'https:\u002F\u002Fvibecode.bitrix24.tech'\nconst VIBE_APP_KEY = process.env.VIBE_APP_KEY  \u002F\u002F ваш vibe_app_\u003C…> ключ\n\napp.use(async (req, _res, next) => {\n  \u002F\u002F Gateway уже всё проверил — но на всякий случай подстрахуемся\n  const bearer = req.headers['x-vibe-authorization']?.toString().replace(\u002F^Bearer \u002F, '')\n  if (!bearer) return next() \u002F\u002F anonymous (PUBLIC policy) — кэшируем без identity\n\n  \u002F\u002F Кэшируем identity in-process per token (одно обращение к \u002Fv1\u002Fme на сессию).\n  \u002F\u002F \u002Fv1\u002Fme возвращает { success, data: { currentUser, portal, scopes, ... } } —\n  \u002F\u002F разворачиваем envelope, чтобы дальше работать с самими полями.\n  let me = identityCache.get(bearer)\n  if (!me) {\n    const res = await fetch(`${VIBE_API}\u002Fv1\u002Fme`, {\n      headers: { 'X-Api-Key': VIBE_APP_KEY, Authorization: `Bearer ${bearer}` },\n    })\n    if (!res.ok) return next() \u002F\u002F 401 → показать экран повторной авторизации\n    const json = await res.json()\n    me = json.data\n    identityCache.set(bearer, me)\n  }\n  req.user = me           \u002F\u002F me.currentUser?.bitrixUserId, me.portal, me.scopes, me.capabilities\n  req.bearer = bearer\n  next()\n})\n\napp.get('\u002Fapi\u002Ftasks', async (req, res) => {\n  \u002F\u002F currentUser может быть null, если запрос без Bearer пришёл (PUBLIC-политика).\n  \u002F\u002F Альтернатива — прочитать заголовок X-Vibe-User-Id (Gateway уже его проставил).\n  const userId = req.user?.currentUser?.bitrixUserId ?? req.headers['x-vibe-user-id']\n  const r = await fetch(`${VIBE_API}\u002Fv1\u002Ftasks?filter[RESPONSIBLE_ID]=${userId}`, {\n    headers: { 'X-Api-Key': VIBE_APP_KEY, Authorization: `Bearer ${req.bearer}` },\n  })\n  res.json(await r.json())\n})\n```\n\n### Python \u002F FastAPI\n\n```python\nfrom fastapi import FastAPI, Request, HTTPException\nimport httpx\n\nVIBE_API = \"https:\u002F\u002Fvibecode.bitrix24.tech\"\nVIBE_APP_KEY = os.environ[\"VIBE_APP_KEY\"]\napp = FastAPI()\nidentity_cache: dict[str, dict] = {}\n\nasync def resolve_identity(request: Request):\n    raw = request.headers.get(\"x-vibe-authorization\", \"\")\n    bearer = raw.removeprefix(\"Bearer \")\n    if not bearer:\n        return None\n    if bearer in identity_cache:\n        return identity_cache[bearer]\n    async with httpx.AsyncClient() as c:\n        r = await c.get(\n            f\"{VIBE_API}\u002Fv1\u002Fme\",\n            headers={\"X-Api-Key\": VIBE_APP_KEY, \"Authorization\": f\"Bearer {bearer}\"},\n        )\n    if r.status_code != 200:\n        return None\n    # \u002Fv1\u002Fme возвращает {\"success\": True, \"data\": {...}} — кладём в кэш именно data.\n    me = r.json()[\"data\"]\n    identity_cache[bearer] = me\n    return me\n\n@app.get(\"\u002Fapi\u002Ftasks\")\nasync def tasks(request: Request):\n    me = await resolve_identity(request)\n    if not me:\n        raise HTTPException(401, \"Re-authentication required\")\n    # currentUser может быть None при запросе без Bearer (PUBLIC-политика); откат на заголовок Gateway.\n    user_id = (me.get(\"currentUser\") or {}).get(\"bitrixUserId\") or request.headers.get(\"x-vibe-user-id\")\n    async with httpx.AsyncClient() as c:\n        r = await c.get(\n            f\"{VIBE_API}\u002Fv1\u002Ftasks\",\n            params={\"filter[RESPONSIBLE_ID]\": user_id},\n            headers={\"X-Api-Key\": VIBE_APP_KEY,\n                     \"Authorization\": request.headers[\"x-vibe-authorization\"]},\n        )\n    return r.json()\n```\n\n### Go \u002F net-http\n\n```go\npackage main\n\nimport (\n    \"encoding\u002Fjson\"\n    \"net\u002Fhttp\"\n    \"os\"\n    \"strings\"\n    \"sync\"\n)\n\nconst vibeAPI = \"https:\u002F\u002Fvibecode.bitrix24.tech\"\n\nvar vibeAppKey = os.Getenv(\"VIBE_APP_KEY\")\nvar identityCache sync.Map \u002F\u002F bearer string -> *Identity\n\n\u002F\u002F CurrentUser совпадает по полям с объектом data.currentUser в ответе \u002Fv1\u002Fme.\n\u002F\u002F Когда запрос пришёл без Bearer (PUBLIC-политика) — data.currentUser приходит как null,\n\u002F\u002F и поле CurrentUser в раскодированной структуре остаётся пустым (BitrixUserID == \"\").\ntype CurrentUser struct {\n    BitrixUserID string `json:\"bitrixUserId\"`\n}\n\ntype Identity struct {\n    Portal      string       `json:\"portal\"`       \u002F\u002F домен портала, например \"company.bitrix24.ru\"\n    Scopes      []string     `json:\"scopes\"`\n    CurrentUser *CurrentUser `json:\"currentUser\"`  \u002F\u002F nil без Bearer\n}\n\ntype meEnvelope struct {\n    Success bool     `json:\"success\"`\n    Data    Identity `json:\"data\"`\n}\n\nfunc resolveIdentity(r *http.Request) (*Identity, string, error) {\n    raw := strings.TrimPrefix(r.Header.Get(\"X-Vibe-Authorization\"), \"Bearer \")\n    if raw == \"\" {\n        return nil, \"\", nil\n    }\n    if v, ok := identityCache.Load(raw); ok {\n        return v.(*Identity), raw, nil\n    }\n    req, _ := http.NewRequest(\"GET\", vibeAPI+\"\u002Fv1\u002Fme\", nil)\n    req.Header.Set(\"X-Api-Key\", vibeAppKey)\n    req.Header.Set(\"Authorization\", \"Bearer \"+raw)\n    resp, err := http.DefaultClient.Do(req)\n    if err != nil {\n        return nil, \"\", err\n    }\n    defer resp.Body.Close()\n    if resp.StatusCode != 200 {\n        return nil, \"\", nil\n    }\n    var env meEnvelope\n    if err := json.NewDecoder(resp.Body).Decode(&env); err != nil {\n        return nil, \"\", err\n    }\n    identityCache.Store(raw, &env.Data)\n    return &env.Data, raw, nil\n}\n\n\u002F\u002F Пример использования: r.User.CurrentUser.BitrixUserID — числовой ID пользователя Битрикс24.\n\u002F\u002F Альтернатива — заголовок r.Header.Get(\"X-Vibe-User-Id\"), Gateway проставил его на входе.\n```\n\n## Жизненный цикл сессии\n\nОдновременно действуют два срока, и путать их не нужно:\n\n- **Токен `vibe_session_*` живёт 24 часа** (`AppSession.expiresAt`). Продление не делается — после истечения пользователь заново открывает placement.\n- **Сессия Gateway (`_vibe_gw`) живёт 10 минут**, а у placement-сессий (приложение открыто через встройку в Битрикс24) — около **40 минут**. Она продлевается при активности: запрос к серверу приложения во второй половине жизни сессии (для обычной — когда до истечения осталось меньше 5 минут, для placement — примерно после 20-й минуты) получает свежую cookie. Пока приложение получает запросы, сессия держится и заголовок `X-Vibe-Authorization` проставляется автоматически.\n\nОтсюда следствие, которое стоит учитывать: **`401` может прийти раньше, чем истекут 24 часа токена** — если сервер приложения не получает запросов дольше жизни Gateway-сессии (для placement-приложений это около 40 минут, для остальных — 10). Например, пользователь долго заполняет форму целиком на стороне браузера (поиск сотрудников, перетаскивание, привязка отделов), не обращаясь к серверу. По истечении сессии следующий запрос приходит к приложению уже без заголовка `X-Vibe-Authorization`, и вызов к V1 возвращает `401`, хотя сам токен ещё действителен. Для placement-приложений расширенное окно (~40 минут) покрывает типичные паузы в 10–15 минут — `401` после короткого простоя больше не возникает.\n\nКак с этим работать:\n\n- **На `401` от V1** — попросите пользователя заново открыть приложение из меню Битрикс24. Важно: это должно быть именно повторное открытие из меню (оно ре-POST-ит placement, и Gateway выдаёт свежую сессию), а не возврат на уже открытую вкладку — повторный показ кэшированной вкладки идёт обычным GET без нового POST и сессию не восстановит. Серверный редирект здесь не поможет: из iframe он выйти не может. Самостоятельно обновлять токен не нужно.\n- **Для длинных форм и редакторов** — сохраняйте изменения по частям или держите сессию активной периодическим лёгким запросом к своему серверу, пока пользователь работает (любой запрос к приложению проходит через Gateway и продлевает сессию). Учтите: браузер тротлит фоновые вкладки, поэтому keepalive надёжен только пока вкладка приложения активна.\n- **Серверная фоновая работа без пользователя** (крон, интеграция) — отдельный случай: `vibe_session_*` привязана к конкретному пользователю и на сервере без его браузера не продлевается. Если нужен долгоживущий серверный доступ ОТ ИМЕНИ ВЛАДЕЛЬЦА сервера, используйте api-bearer-токен и его продление — `POST \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Faccess-tokens\u002F:tokenId\u002Frefresh` (требует включённого администратором платформы `accessTokensEnabled`). Такой токен действует от владельца, а не от текущего пользователя, — не подменяйте им пер-юзерные вызовы V1.\n\n```js\n\u002F\u002F На 401 от V1 сессия Gateway истекла. Серверный редирект не выйдет из iframe —\n\u002F\u002F верните 401 своему SPA и предложите пользователю заново открыть приложение из\n\u002F\u002F меню Битрикс24 (это ре-POST-ит placement и выдаст свежую сессию). Токен сам не обновляйте.\nif (vibeResponse.status === 401) {\n  return res.status(401).json({ error: 'session_expired', reopen: true })\n}\n```\n\n## Что не делать\n\n- ❌ **`sessionStorage.setItem('vibe_token', …)`** — токена в браузере физически нет, и не должно быть. OAuth 2.0 Security BCP § 4.1 явно запрещает хранение access-токенов в `sessionStorage` \u002F `localStorage`. Не пытайтесь воссоздать его на стороне клиента.\n- ❌ **Возвращать токен в JSON-ответе** SPA. `X-Vibe-Authorization` живёт исключительно между Gateway и сервером приложения. Раскрытие в ответе браузеру уничтожает всё преимущество BFF-паттерна (XSS → утечка токена).\n- ❌ **Читать `_vibe_gw` cookie руками**. Cookie помечена `HttpOnly` — JS даже не увидит её. Если хочется session ID для логирования — генерируйте свой.\n- ❌ **Использовать `Authorization` вместо `X-Vibe-Authorization`**. Платформенный префикс выбран специально, чтобы приложение могло пользоваться своей собственной `Authorization`-цепочкой (например, для проксирования к своему вышестоящему сервису).\n- ❌ **Хранить `vibe_session_*` где-либо за пределами одной HTTP-сессии**. Это токен доступа, который действует 24 часа и охватывает весь V1 API в скоупе приложения. Любая утечка — компрометация.\n- ❌ **Пытаться декодировать `__init` или `_vibe_gw`**. Это HMAC-подписанные JWT с одним получателем — Gateway. Содержимое внутреннее, в будущем поменяется.\n\n## Аналогия для понимания\n\nМодель для понимания: **Cloudflare Access \u002F Google IAP \u002F AWS API Gateway**. Прокси (Gateway) делает всю аутентификацию и кладёт данные пользователя в заголовки на входе. Приложение читает заголовки и доверяет им, потому что:\n\n1. Заголовок может прийти только от Gateway (всё остальное срезается).\n2. Туннель защищён mTLS \u002F WebSocket на уровне инфраструктуры.\n3. Если приложению нужна возможность — оно делает серверный вызов на платформенное API, неся токен как Bearer.\n\nИдиоматическое чтение — `req.headers['x-vibe-authorization']` (или эквивалент вашего фреймворка). Идиоматический ответ на 401 от V1 — перенаправление пользователя на placement, не самостоятельное обновление токена.\n\n## Входящие запросы извне: вебхуки и события Битрикс24\n\nПо умолчанию приложение на Black Hole — приватное: Gateway аутентифицирует каждый запрос (цепочка выше). Внешний вызов без платформенной cookie \u002F Bearer — например исходящий вебхук Битрикс24 или POST от обработчика события — получает `401 BH_LOGIN_REQUIRED`. Формат ответа зависит от запроса: для `Content-Type: application\u002Fjson` или пути `\u002Fv1\u002F…` — JSON-ошибка, для запроса из браузера — HTML-страница входа. Поэтому «настроил вебхук на адрес приложения → приходит 401» — ожидаемое поведение приватного режима, а не сбой.\n\nЧтобы принимать запросы извне, переведите сервер в публичный режим:\n\n```bash\ncurl -X PATCH https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Faccess-policy \\\n  -H \"X-Api-Key: YOUR_API_KEY\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{ \"accessPolicy\": \"PUBLIC\" }'\n```\n\nВ режиме `PUBLIC` Gateway пробрасывает внешний запрос прямо в приложение с заголовком `X-Vibe-Public: true` — аутентификация не обязательна. Доступ открыт **всем**, кто знает адрес субдомена, поэтому проверяйте входящие данные на стороне приложения (для событий Битрикс24 — поле `auth.application_token` в теле запроса). Если же у посетителя уже есть сессия, Gateway дополнительно прокидывает заголовки идентичности `X-Vibe-User-*` (и `X-Vibe-Authorization`, когда у пользователя есть токен приложения) — так же, как на приватных политиках. Анонимный запрос приходит без них. **Опознан ли посетитель — определяйте по наличию `X-Vibe-User-Id`, а не `X-Vibe-Authorization`**: последний несёт только токен доступа для `\u002Fv1` и законно пуст у гостевых ссылок и у пользователей без токена приложения, даже когда идентичность известна. Подробнее о режимах — [Политика доступа](\u002Fdocs\u002Finfra\u002Faccess\u002Faccess-policy).\n\n**Таймаут — 30 секунд.** Gateway ждёт ответа приложения до 30 секунд. Внутренний `504` при этом перехватывается самим шлюзом и до вызывающего в исходном виде не доходит: обычный запрос получает HTML-страницу запуска или пробуждения приложения со статусом `503`, поэтому распознать таймаут по телу ответа нельзя. Для долгих задач (анализ, генерация, импорт) не держите соединение открытым: ответьте сразу (`202` + идентификатор задачи), выполняйте работу в фоне, а вызывающий опрашивает статус отдельным запросом. Синхронный POST дольше 30 секунд через публичный субдомен не дойдёт до конца.\n\n**Спящий сервер теряет первый запрос.** Если сервер в SLEEPING, входящий запрос его будит, но вызывающий в этот момент получает HTML-страницу пробуждения (со статусом 503), а не ответ приложения — то есть полезная нагрузка первого вебхука\u002Fсобытия до приложения не доходит. Битрикс24 события отправляет без гарантии повторной доставки. Поэтому для надёжного приёма событий отключите авто-сон (`PATCH \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Fsleep` со значением «никогда») — иначе первое событие после засыпания будет потеряно.\n\n### Можно ли направить событие Битрикс24 (`event.bind`) на субдомен приложения\n\nДа. Битрикс24 `event.bind` принимает в качестве обработчика произвольный HTTPS-URL, поэтому адрес субдомена Black Hole (`https:\u002F\u002Fapp-\u003Chex>.vibecode.bitrix24.tech\u002F\u003Cпуть>`) подходит — это разблокирует событийную архитектуру (событие в Битрикс24 → действие в приложении) без опроса каждые N минут. Условия:\n\n1. **HTTPS обязателен** — субдомены Black Hole всегда по HTTPS, условие выполнено.\n2. **Публичный режим** — `accessPolicy: PUBLIC` (см. выше), иначе POST от Битрикс24 упрётся в `401`.\n3. **Сервер не должен засыпать** — иначе первое событие потеряется (см. выше). Отключите авто-сон.\n4. **Ответ за 30 секунд** — обработчик должен подтвердить приём быстро, тяжёлую обработку выносите в фон.\n5. `event.bind` вызывается через REST API самого Битрикс24 в контексте приложения. События доступны на коммерческих тарифах Битрикс24. В теле запроса Битрикс24 присылает `auth.application_token` — проверяйте его.\n\nЕсли держать сервер постоянно активным нельзя, надёжнее остаётся опрос (polling) на стороне приложения.\n\n## Placement открывается с экраном входа вместо приложения\n\nСимптом: сотрудник открывает вкладку с placement в Битрикс24, но в iframe показывается экран входа, а не приложение. Либо приложение пускает только владельца ключа, а другим сотрудникам из списка доступа по-прежнему предлагает войти. Две причины конфигурации.\n\n### Обработчик placement указывает на субдомен, а не на платформу\n\nБитрикс24 должен открывать placement через обработчик платформы `https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Fbitrix-handler`, а не напрямую на субдомен приложения (`https:\u002F\u002Fapp-\u003Chex>.vibecode.bitrix24.tech`). Только через обработчик платформа создаёт сессию `vibe_session_*` и одноразовый код для Gateway (см. «Жизненный цикл запроса» выше). Если placement привязан напрямую к субдомену, Битрикс24 открывает iframe в обход платформы — сессия не создаётся, и Gateway показывает экран входа.\n\nАдрес обработчика задаёт платформа, указывать субдомен приложения вручную не нужно. Если placement уже привязан к субдомену — переопубликуйте приложение: публикация заново привязывает обработчик к платформенному адресу.\n\n### Список доступа применяется только при политике `NAMED_USERS`\n\nВ режиме `OWNER_ONLY` доступ есть только у владельца ключа — записи списка доступа, добавленные через `POST \u002Faccess`, не применяются. Чтобы пустить конкретных сотрудников портала, переключите политику на `NAMED_USERS` и добавьте пользователей:\n\n```bash\ncurl -X PATCH https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Faccess-policy \\\n  -H \"X-Api-Key: YOUR_API_KEY\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{ \"accessPolicy\": \"NAMED_USERS\" }'\n```\n\nПодробнее о режимах и списке доступа — [Политика доступа](\u002Fdocs\u002Finfra\u002Faccess\u002Faccess-policy).\n\nПосле обоих исправлений сотрудники из списка проходят в iframe автоматически. Ручной вход на субдомене в отдельной вкладке не требуется — если вы пробовали этот обходной путь, после исправления обработчика он не нужен.\n\n## Self-hosted приложение (не Black Hole)\n\nЗаголовок `X-Vibe-Authorization` проставляется только за Gateway, который стоит перед Black Hole-серверами. Если файлы приложения лежат на вашем собственном сервере, заголовок не подставляется — приложение само хранит токен сессии (свой BFF) и само вызывает `GET \u002Fv1\u002Fme`.\n\nКак получить токен сессии текущего пользователя в placement (iframe):\n\n- **Приложение Вайбкод с `appUrl` на вашем сервере.** Placement приходит на обработчик Вайбкод, который перенаправляет на `\u003CappUrl>\u002F?code=\u003Cодноразовый код>&placement=…&member_id=…`. Ваш сервер обменивает код: `POST \u002Fv1\u002Foauth\u002Ftoken { app_key, code, redirect_uri }` (значение `redirect_uri` — ровно настроенный `appUrl`). В браузер попадает только одноразовый код, не токен.\n- **Собственное приложение Битрикс24 со своим обработчиком.** Placement приходит на ваш сервер с токеном пользователя (`AUTH_ID`). Обменяйте его сервер-к-серверу: `POST \u002Fv1\u002Foauth\u002Fplacement-session { app_key, access_token, member_id, domain }`.\n\nВ обоих случаях ответ — `{ access_token: \"vibe_session_…\", user, expires_in }`. Дальше приложение работает как BFF: хранит `vibe_session_*` на своей стороне, читает личность через `GET \u002Fv1\u002Fme` с `Authorization: Bearer \u003Cvibe_session_*>` и не отдаёт токен в браузер. Возможность встроить свою страницу в iframe — заголовки `X-Frame-Options` и CSP `frame-ancestors` — обеспечиваете вы, платформа за неё не отвечает. Стандартный `GET \u002Fv1\u002Foauth\u002Fauthorize` в iframe не открывается: страница согласия Битрикс24 запрещает встраивание. Открывайте его только в отдельной вкладке.\n\n## Смотрите также\n\n- [Black Hole — режимы и доступ](\u002Fdocs\u002Finfra\u002Faccess)\n- [Токены доступа](\u002Fdocs\u002Finfra\u002Faccess-tokens)\n- [Deploy API](\u002Fdocs\u002Finfra\u002Fdeploy)\n","2026-07-10",{}]