Авторизация в приложении на Black Hole

Black Hole — это закрытый режим сервера приложения: снаружи он недоступен напрямую, единственный вход — через платформенный шлюз Gateway. Когда сотрудник открывает приложение внутри Битрикс24, Gateway проверяет, кто это, и кладёт данные пользователя прямо в заголовки запроса к серверу приложения. Приложению не нужны ни своя форма входа, ни хранение паролей: оно доверяет заголовкам от Gateway, а когда нужны данные портала — обращается к API за этого пользователя. Кто может открыть приложение, задаёт политика доступа — Доступ и режимы.

Gateway сам аутентифицирует пользователя и пробрасывает к приложению уже подписанный запрос. Браузер не видит и не хранит vibe_session_* — токен живёт только между Gateway и сервером приложения. Приложение получает заголовок X-Vibe-Authorization: Bearer vibe_session_<…> и читает данные пользователя через GET /v1/me. Это BFF-паттерн (Backend-for-Frontend) — тот же, что у Cloudflare Access, Google IAP и AWS API Gateway.

Скоуп: vibe:apps (на стороне V1 API), внутри туннеля — без явного скоупа, Gateway сам управляет доступом.

Заголовки запроса · Параметры iframe · Данные пользователя · Примеры кода · Жизненный цикл сессии

Жизненный цикл запроса

Полный обмен — от открытия приложения в Битрикс24 до проксирования запроса на сервер приложения — показан ниже по шагам. Разбор ключевых моментов идёт сразу после схемы.

Битрикс24 placement                       Backend (vibecode.bitrix24.tech)
   │                                         │
   ├── POST /v1/bitrix-handler ──────────────►│  exchange OAuth code,
   │     (B24 placement triple)               │  upsert AppUserToken,
   │                                          │  createAppSession (mint vibe_session_*),
   │                                          │  mint signed __init JWT (TTL 5 min)
   │                                          │
   │◄── 302 https://app-XXX/?placement=…&member_id=…[&placement_options=…]&__init=<jwt> ──┤
   │
Browser
   │
   ├── GET https://app-XXX/?placement=…&member_id=…[&placement_options=…]&__init=<jwt> ───►│ Gateway
   │                                                                     │  Priority 0 redeem:
   │                                                                     │   • verify JWT (HMAC-SHA256, "gateway-init")
   │                                                                     │   • subdomain binding
   │                                                                     │   • mark jti used (one-time)
   │                                                                     │   • store (sub, subdomain) → raw in cache
   │                                                                     │   • mint _vibe_gw cookie
   │                                                                     │
   │◄────────── 302 https://app-XXX/?placement=…&member_id=…[&placement_options=…] ───────┤
   │                                          Set-Cookie: _vibe_gw=…
   │
   ├── GET https://app-XXX/?placement=…&member_id=…[&placement_options=…] ──────────────►│ Gateway
   │   Cookie: _vibe_gw=…                                                │  Priority 1 (cookie):
   │                                                                     │   • resolve raw from cache
   │                                                                     │   • strip incoming X-Vibe-Authorization (anti-spoof)
   │                                                                     │   • inject X-Vibe-Authorization: Bearer vibe_session_…
   │                                                                     │
   │                                                                     ├── GET / ─────► App server
   │                                                                     │   X-Vibe-Authorization: Bearer vibe_session_…
   │                                                                     │   Cookie: _vibe_gw=…  (Gateway-only; не для app)
   │                                                                     │
   │◄────────────────────── HTTP response ───────────────────────────────┤

Ключевая разница от стандартного OAuth:

  • Браузер никогда не видит vibe_session_* — даже в момент первого открытия. Токен передаётся только в подписанном одноразовом коде в URL, который Gateway сразу обменивает и стирает.
  • __init — одноразовый. Повторный обмен того же jti отклоняется. Время жизни — 5 минут. На медленных сетях загрузки iframe этого хватает с запасом — если не уложитесь в 5 минут, проверьте журналы Gateway init code replay rejected / init code verify failed.
  • Cookie _vibe_gw живёт 10 минут — а у placement-приложений (открытых через встройку в Битрикс24) около 40 минут — и продлевается при активности. Не пытайтесь её читать в JS — она HttpOnly.
  • Пробуждение из сна: если сервер в SLEEPING, Gateway обменивает __init до страницы пробуждения, кладёт исходные данные в кэш и cookie в ответ. После переподключения агента браузер делает location.replace на чистый URL — кэш ещё не истёк, и X-Vibe-Authorization проставляется на первый же GET.

Что приходит в каждый запрос

GET /api/tasks
Host: app-9edac24091ba.vibecode.bitrix24.tech
Cookie: _vibe_gw=<…>                       ← платформенная cookie, к приложению пробрасывается прозрачно

X-Vibe-Request-Id:   req_<16 hex chars>
X-Vibe-User-Id:      42
X-Vibe-Portal-Id:    f2342f7a-b1c5-4d50-8a3e-30219c5ae0c1
X-Vibe-User-Name:         Иван Иванов
X-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
X-Vibe-User-Role:    MEMBER
X-Vibe-Authorization: Bearer vibe_session_<43 chars>

Gateway проставляет семь заголовков на каждый проксируемый запрос. X-Vibe-Request-Id приходит на любой запрос, остальные — только когда пользователь аутентифицирован (пройдена авторизация через placement или по Bearer-токену):

Заголовок Когда Значение
X-Vibe-Request-Id всегда Идентификатор запроса для корреляции в журналах. Префикс req_ + 16 hex-символов.
X-Vibe-User-Id аутентифицированный запрос Числовой ID пользователя в Битрикс24 (например, 42). Совпадает с currentUser.bitrixUserId в ответе /v1/me.
X-Vibe-Portal-Id аутентифицированный запрос UUID портала во внутренней БД Вайбкод. Не путать с доменом — он отдельно.
X-Vibe-User-Name аутентифицированный запрос Отображаемое имя пользователя в виде сырых UTF-8 байтов. ⚠️ Большинство HTTP-стеков (Express и др.) читают значения заголовков как latin1 (RFC 7230 §3.2.4), поэтому локализованные имена отображаются нечитаемыми символами (Ðван вместо Иван). Для корректного имени используйте X-Vibe-User-Name-Encoded (ниже). Заголовок сохранён для обратной совместимости.
X-Vibe-User-Name-Encoded аутентифицированный запрос То же отображаемое имя, в процентном кодировании по RFC 3986 (UTF-8). Декодируется любым стандартным декодером: в JS — decodeURIComponent(req.headers['x-vibe-user-name-encoded']). Рекомендуемый источник имени вместо сырого X-Vibe-User-Name.
X-Vibe-User-Role аутентифицированный запрос ADMIN или MEMBER — роль текущего пользователя (того, кто открыл приложение) на портале. ADMIN означает право управлять настройками приложения (предикат Битрикс24 user.admin) — в том числе у владельца портала. Это надёжный способ определить, администратор ли текущий пользователь. Признака администратора для произвольного пользователя из GET /v1/users Битрикс24 не отдаёт (см. поле isAdmin в доках сущности users).
X-Vibe-Authorization аутентифицированный запрос Bearer vibe_session_<…> — токен доступа для серверных вызовов V1 API.
  • X-Vibe-Authorization — единственный источник Bearer-токена. Платформенный префикс X-Vibe- выбран, чтобы не конфликтовать с собственной Authorization-цепочкой приложения (например, если оно проксирует к своему вышестоящему API). Любой входящий X-Vibe-Authorization от клиента Gateway срезает до проброса — приложение никогда не получит подделанный токен.
  • X-Vibe-User-Id / -Portal-Id / -User-Name / -User-Role — быстрый способ опознать пользователя без дополнительного вызова /v1/me. Подходят для логов, аналитики и простого ACL. Для полного контекста (scopes, capabilities, tariff) всё равно делайте один вызов /v1/me и кэшируйте его на токен.
  • Имя пользователя — читайте 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').
  • X-Vibe-Request-Id — кладите в свой структурированный лог, чтобы трассировать запрос между Gateway и приложением. При обращении в поддержку Вайбкод присылайте этот ID — он есть и в журналах платформы.
  • Cookie: _vibe_gw — оставляется как есть, но это не токен доступа к V1 API. Это сессионный токен Gateway для быстрой авторизации последующих запросов. Приложению она не нужна и помечена HttpOnly — JS её даже не увидит.
  • access_token / auth[*] в теле запроса — не приходят. Не пытайтесь их читать.

URL-параметры iframe после placement-редиректа

После POST /v1/bitrix-handler платформа перенаправляет (302) браузер на <appUrl> и добавляет в строку запроса:

Параметр Когда Значение
placement всегда Код placement-а (LEFT_MENU, CRM_DEAL_DETAIL_TAB, IM_TEXTAREA, …). Совпадает с PLACEMENT в POST от B24.
member_id всегда Идентификатор портала Bitrix24 (тот же member_id, что в B24 OAuth).
placement_options когда B24 присылает PLACEMENT_OPTIONS — типично для DETAIL_TAB / DETAIL_TOOLBAR / LIST_MENU и других placement'ов, где есть контекст сущности JSON-строка с контекстом сущности — например {"ID":"42"} для CRM_DEAL_DETAIL_TAB. Сюда B24 кладёт ID текущей карточки (сделки/лида/контакта/компании).
__init первый запрос Одноразовый JWT для Gateway, обмениваемый на _vibe_gw cookie. После обмена — исчезает (замена на чистый URL).

Заметка по именованию: B24 шлёт это поле как PLACEMENT_OPTIONS (в верхнем регистре) в POST к /v1/bitrix-handler. Мы пробрасываем его под именем placement_options в нижнем регистре в URL iframe. Это одно и то же поле — ищите в спецификации B24 по имени в верхнем регистре.

Как читать placement_options — забирайте из URLSearchParams и парсите JSON:

JavaScript
const params = new URLSearchParams(window.location.search)
const placement = params.get('placement')                  // "CRM_DEAL_DETAIL_TAB"
const options = JSON.parse(params.get('placement_options') || '{}')
const entityId = options.ID                                // "42" — текущая карточка

Не используйте BX24.placement.info() — SDK не видит контекст при перенаправлении через платформенный обработчик и вернёт {placement:null, options:null}. Всё, что вам нужно про placement и его параметры, уже лежит в URL.

Получение данных пользователя (`GET /v1/me`)

X-Vibe-Authorization несёт только токен доступа, не данные пользователя. Чтобы узнать пользователя, портал, скоупы и capabilities — один раз вызываем /v1/me на сервере приложения и кэшируем в собственной сессии:

GET https://vibecode.bitrix24.tech/v1/me
X-Api-Key: <ключ-вашего-приложения>
Authorization: Bearer vibe_session_<…>     ← забрали из X-Vibe-Authorization

200 OK
{
  "success": true,
  "data": {
    "type": "oauth_app",
    "portal": "company.bitrix24.ru",
    "scopes": ["crm", "tasks", "imbot", ...],
    "currentUser": {
      "bitrixUserId": "42",
      "_note": "B24 numeric user id of the OAuth-authorized end user (from Bearer session)."
    },
    "capabilities": { "servers": { "create": { "available": true, "reason": "TRIAL_ACTIVE" }, ... }, ... },
    "tariff": { ... },
    "app": { "title": "My App", "id": "<uuid>" }
  }
}

Показаны характерные поля. В ответе также есть api (список доступных эндпоинтов и сущностей), oauth, oauthTutorial, deployment, infra, feedback и другие — модель использует их, чтобы построить запросы без чтения остальной документации.

Идентификатор пользователя — data.currentUser.bitrixUserId (строка с числовым ID Битрикс24). Не userId, не user.id, не верхнеуровневое поле — именно currentUser.bitrixUserId. Совпадает с заголовком X-Vibe-User-Id (см. секцию «Что приходит в каждый запрос»), который Gateway уже проставил в запрос. Для простой идентификации заголовка достаточно. /v1/me нужен, когда требуются scopes, capabilities, tariff или информация о приложении.

Без Authorization: Bearer тот же /v1/me отвечает с тем же набором полей, только currentUser будет null. Это рабочий режим: используется для серверной инициализации приложения до OAuth — портал, скоупы и capabilities уже видны. Не пытайтесь распознавать «нет сессии» по 401 — его не будет, проверяйте currentUser !== null.

Кэш — 24 часа TTL. После 401 на V1 — отправляйте пользователя обратно на placement (см. ниже).

Примеры кода

Node.js / Express

JavaScript
import express from 'express'
const app = express()

const VIBE_API = 'https://vibecode.bitrix24.tech'
const VIBE_APP_KEY = process.env.VIBE_APP_KEY  // ваш vibe_app_<…> ключ

app.use(async (req, _res, next) => {
  // Gateway уже всё проверил — но на всякий случай подстрахуемся
  const bearer = req.headers['x-vibe-authorization']?.toString().replace(/^Bearer /, '')
  if (!bearer) return next() // anonymous (PUBLIC policy) — кэшируем без identity

  // Кэшируем identity in-process per token (одно обращение к /v1/me на сессию).
  // /v1/me возвращает { success, data: { currentUser, portal, scopes, ... } } —
  // разворачиваем envelope, чтобы дальше работать с самими полями.
  let me = identityCache.get(bearer)
  if (!me) {
    const res = await fetch(`${VIBE_API}/v1/me`, {
      headers: { 'X-Api-Key': VIBE_APP_KEY, Authorization: `Bearer ${bearer}` },
    })
    if (!res.ok) return next() // 401 → показать экран повторной авторизации
    const json = await res.json()
    me = json.data
    identityCache.set(bearer, me)
  }
  req.user = me           // me.currentUser?.bitrixUserId, me.portal, me.scopes, me.capabilities
  req.bearer = bearer
  next()
})

app.get('/api/tasks', async (req, res) => {
  // currentUser может быть null, если запрос без Bearer пришёл (PUBLIC-политика).
  // Альтернатива — прочитать заголовок X-Vibe-User-Id (Gateway уже его проставил).
  const userId = req.user?.currentUser?.bitrixUserId ?? req.headers['x-vibe-user-id']
  const r = await fetch(`${VIBE_API}/v1/tasks?filter[RESPONSIBLE_ID]=${userId}`, {
    headers: { 'X-Api-Key': VIBE_APP_KEY, Authorization: `Bearer ${req.bearer}` },
  })
  res.json(await r.json())
})

Python / FastAPI

Python
from fastapi import FastAPI, Request, HTTPException
import httpx

VIBE_API = "https://vibecode.bitrix24.tech"
VIBE_APP_KEY = os.environ["VIBE_APP_KEY"]
app = FastAPI()
identity_cache: dict[str, dict] = {}

async def resolve_identity(request: Request):
    raw = request.headers.get("x-vibe-authorization", "")
    bearer = raw.removeprefix("Bearer ")
    if not bearer:
        return None
    if bearer in identity_cache:
        return identity_cache[bearer]
    async with httpx.AsyncClient() as c:
        r = await c.get(
            f"{VIBE_API}/v1/me",
            headers={"X-Api-Key": VIBE_APP_KEY, "Authorization": f"Bearer {bearer}"},
        )
    if r.status_code != 200:
        return None
    # /v1/me возвращает {"success": True, "data": {...}} — кладём в кэш именно data.
    me = r.json()["data"]
    identity_cache[bearer] = me
    return me

@app.get("/api/tasks")
async def tasks(request: Request):
    me = await resolve_identity(request)
    if not me:
        raise HTTPException(401, "Re-authentication required")
    # currentUser может быть None при запросе без Bearer (PUBLIC-политика); откат на заголовок Gateway.
    user_id = (me.get("currentUser") or {}).get("bitrixUserId") or request.headers.get("x-vibe-user-id")
    async with httpx.AsyncClient() as c:
        r = await c.get(
            f"{VIBE_API}/v1/tasks",
            params={"filter[RESPONSIBLE_ID]": user_id},
            headers={"X-Api-Key": VIBE_APP_KEY,
                     "Authorization": request.headers["x-vibe-authorization"]},
        )
    return r.json()

Go / net-http

go
package main

import (
    "encoding/json"
    "net/http"
    "os"
    "strings"
    "sync"
)

const vibeAPI = "https://vibecode.bitrix24.tech"

var vibeAppKey = os.Getenv("VIBE_APP_KEY")
var identityCache sync.Map // bearer string -> *Identity

// CurrentUser совпадает по полям с объектом data.currentUser в ответе /v1/me.
// Когда запрос пришёл без Bearer (PUBLIC-политика) — data.currentUser приходит как null,
// и поле CurrentUser в раскодированной структуре остаётся пустым (BitrixUserID == "").
type CurrentUser struct {
    BitrixUserID string `json:"bitrixUserId"`
}

type Identity struct {
    Portal      string       `json:"portal"`       // домен портала, например "company.bitrix24.ru"
    Scopes      []string     `json:"scopes"`
    CurrentUser *CurrentUser `json:"currentUser"`  // nil без Bearer
}

type meEnvelope struct {
    Success bool     `json:"success"`
    Data    Identity `json:"data"`
}

func resolveIdentity(r *http.Request) (*Identity, string, error) {
    raw := strings.TrimPrefix(r.Header.Get("X-Vibe-Authorization"), "Bearer ")
    if raw == "" {
        return nil, "", nil
    }
    if v, ok := identityCache.Load(raw); ok {
        return v.(*Identity), raw, nil
    }
    req, _ := http.NewRequest("GET", vibeAPI+"/v1/me", nil)
    req.Header.Set("X-Api-Key", vibeAppKey)
    req.Header.Set("Authorization", "Bearer "+raw)
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, "", err
    }
    defer resp.Body.Close()
    if resp.StatusCode != 200 {
        return nil, "", nil
    }
    var env meEnvelope
    if err := json.NewDecoder(resp.Body).Decode(&env); err != nil {
        return nil, "", err
    }
    identityCache.Store(raw, &env.Data)
    return &env.Data, raw, nil
}

// Пример использования: r.User.CurrentUser.BitrixUserID — числовой ID пользователя Битрикс24.
// Альтернатива — заголовок r.Header.Get("X-Vibe-User-Id"), Gateway проставил его на входе.

Жизненный цикл сессии

Одновременно действуют два срока, и путать их не нужно:

  • Токен vibe_session_* живёт 24 часа (AppSession.expiresAt). Продление не делается — после истечения пользователь заново открывает placement.
  • Сессия Gateway (_vibe_gw) живёт 10 минут, а у placement-сессий (приложение открыто через встройку в Битрикс24) — около 40 минут. Она продлевается при активности: запрос к серверу приложения во второй половине жизни сессии (для обычной — когда до истечения осталось меньше 5 минут, для placement — примерно после 20-й минуты) получает свежую cookie. Пока приложение получает запросы, сессия держится и заголовок X-Vibe-Authorization проставляется автоматически.

Отсюда следствие, которое стоит учитывать: 401 может прийти раньше, чем истекут 24 часа токена — если сервер приложения не получает запросов дольше жизни Gateway-сессии (для placement-приложений это около 40 минут, для остальных — 10). Например, пользователь долго заполняет форму целиком на стороне браузера (поиск сотрудников, перетаскивание, привязка отделов), не обращаясь к серверу. По истечении сессии следующий запрос приходит к приложению уже без заголовка X-Vibe-Authorization, и вызов к V1 возвращает 401, хотя сам токен ещё действителен. Для placement-приложений расширенное окно (~40 минут) покрывает типичные паузы в 10–15 минут — 401 после короткого простоя больше не возникает.

Как с этим работать:

  • На 401 от V1 — попросите пользователя заново открыть приложение из меню Битрикс24. Важно: это должно быть именно повторное открытие из меню (оно ре-POST-ит placement, и Gateway выдаёт свежую сессию), а не возврат на уже открытую вкладку — повторный показ кэшированной вкладки идёт обычным GET без нового POST и сессию не восстановит. Серверный редирект здесь не поможет: из iframe он выйти не может. Самостоятельно обновлять токен не нужно.
  • Для длинных форм и редакторов — сохраняйте изменения по частям или держите сессию активной периодическим лёгким запросом к своему серверу, пока пользователь работает (любой запрос к приложению проходит через Gateway и продлевает сессию). Учтите: браузер тротлит фоновые вкладки, поэтому keepalive надёжен только пока вкладка приложения активна.
  • Серверная фоновая работа без пользователя (крон, интеграция) — отдельный случай: vibe_session_* привязана к конкретному пользователю и на сервере без его браузера не продлевается. Если нужен долгоживущий серверный доступ ОТ ИМЕНИ ВЛАДЕЛЬЦА сервера, используйте api-bearer-токен и его продление — POST /v1/infra/servers/:id/access-tokens/:tokenId/refresh (требует включённого администратором платформы accessTokensEnabled). Такой токен действует от владельца, а не от текущего пользователя, — не подменяйте им пер-юзерные вызовы V1.
JavaScript
// На 401 от V1 сессия Gateway истекла. Серверный редирект не выйдет из iframe —
// верните 401 своему SPA и предложите пользователю заново открыть приложение из
// меню Битрикс24 (это ре-POST-ит placement и выдаст свежую сессию). Токен сам не обновляйте.
if (vibeResponse.status === 401) {
  return res.status(401).json({ error: 'session_expired', reopen: true })
}

Что не делать

  • sessionStorage.setItem('vibe_token', …) — токена в браузере физически нет, и не должно быть. OAuth 2.0 Security BCP § 4.1 явно запрещает хранение access-токенов в sessionStorage / localStorage. Не пытайтесь воссоздать его на стороне клиента.
  • Возвращать токен в JSON-ответе SPA. X-Vibe-Authorization живёт исключительно между Gateway и сервером приложения. Раскрытие в ответе браузеру уничтожает всё преимущество BFF-паттерна (XSS → утечка токена).
  • Читать _vibe_gw cookie руками. Cookie помечена HttpOnly — JS даже не увидит её. Если хочется session ID для логирования — генерируйте свой.
  • Использовать Authorization вместо X-Vibe-Authorization. Платформенный префикс выбран специально, чтобы приложение могло пользоваться своей собственной Authorization-цепочкой (например, для проксирования к своему вышестоящему сервису).
  • Хранить vibe_session_* где-либо за пределами одной HTTP-сессии. Это токен доступа, который действует 24 часа и охватывает весь V1 API в скоупе приложения. Любая утечка — компрометация.
  • Пытаться декодировать __init или _vibe_gw. Это HMAC-подписанные JWT с одним получателем — Gateway. Содержимое внутреннее, в будущем поменяется.

Аналогия для понимания

Модель для понимания: Cloudflare Access / Google IAP / AWS API Gateway. Прокси (Gateway) делает всю аутентификацию и кладёт данные пользователя в заголовки на входе. Приложение читает заголовки и доверяет им, потому что:

  1. Заголовок может прийти только от Gateway (всё остальное срезается).
  2. Туннель защищён mTLS / WebSocket на уровне инфраструктуры.
  3. Если приложению нужна возможность — оно делает серверный вызов на платформенное API, неся токен как Bearer.

Идиоматическое чтение — req.headers['x-vibe-authorization'] (или эквивалент вашего фреймворка). Идиоматический ответ на 401 от V1 — перенаправление пользователя на placement, не самостоятельное обновление токена.

Входящие запросы извне: вебхуки и события Битрикс24

По умолчанию приложение на Black Hole — приватное: Gateway аутентифицирует каждый запрос (цепочка выше). Внешний вызов без платформенной cookie / Bearer — например исходящий вебхук Битрикс24 или POST от обработчика события — получает 401 BH_LOGIN_REQUIRED. Формат ответа зависит от запроса: для Content-Type: application/json или пути /v1/… — JSON-ошибка, для запроса из браузера — HTML-страница входа. Поэтому «настроил вебхук на адрес приложения → приходит 401» — ожидаемое поведение приватного режима, а не сбой.

Чтобы принимать запросы извне, переведите сервер в публичный режим:

Terminal
curl -X PATCH https://vibecode.bitrix24.tech/v1/infra/servers/SERVER_ID/access-policy \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "accessPolicy": "PUBLIC" }'

В режиме PUBLIC Gateway пробрасывает внешний запрос прямо в приложение с заголовком X-Vibe-Public: true — аутентификация не обязательна. Доступ открыт всем, кто знает адрес субдомена, поэтому проверяйте входящие данные на стороне приложения (для событий Битрикс24 — поле auth.application_token в теле запроса). Если же у посетителя уже есть сессия, Gateway дополнительно прокидывает заголовки идентичности X-Vibe-User-*X-Vibe-Authorization, когда у пользователя есть токен приложения) — так же, как на приватных политиках. Анонимный запрос приходит без них. Опознан ли посетитель — определяйте по наличию X-Vibe-User-Id, а не X-Vibe-Authorization: последний несёт только токен доступа для /v1 и законно пуст у гостевых ссылок и у пользователей без токена приложения, даже когда идентичность известна. Подробнее о режимах — Политика доступа.

Таймаут — 30 секунд. Gateway ждёт ответа приложения до 30 секунд. Внутренний 504 при этом перехватывается самим шлюзом и до вызывающего в исходном виде не доходит: обычный запрос получает HTML-страницу запуска или пробуждения приложения со статусом 503, поэтому распознать таймаут по телу ответа нельзя. Для долгих задач (анализ, генерация, импорт) не держите соединение открытым: ответьте сразу (202 + идентификатор задачи), выполняйте работу в фоне, а вызывающий опрашивает статус отдельным запросом. Синхронный POST дольше 30 секунд через публичный субдомен не дойдёт до конца.

Спящий сервер теряет первый запрос. Если сервер в SLEEPING, входящий запрос его будит, но вызывающий в этот момент получает HTML-страницу пробуждения (со статусом 503), а не ответ приложения — то есть полезная нагрузка первого вебхука/события до приложения не доходит. Битрикс24 события отправляет без гарантии повторной доставки. Поэтому для надёжного приёма событий отключите авто-сон (PATCH /v1/infra/servers/:id/sleep со значением «никогда») — иначе первое событие после засыпания будет потеряно.

Можно ли направить событие Битрикс24 (`event.bind`) на субдомен приложения

Да. Битрикс24 event.bind принимает в качестве обработчика произвольный HTTPS-URL, поэтому адрес субдомена Black Hole (https://app-<hex>.vibecode.bitrix24.tech/<путь>) подходит — это разблокирует событийную архитектуру (событие в Битрикс24 → действие в приложении) без опроса каждые N минут. Условия:

  1. HTTPS обязателен — субдомены Black Hole всегда по HTTPS, условие выполнено.
  2. Публичный режимaccessPolicy: PUBLIC (см. выше), иначе POST от Битрикс24 упрётся в 401.
  3. Сервер не должен засыпать — иначе первое событие потеряется (см. выше). Отключите авто-сон.
  4. Ответ за 30 секунд — обработчик должен подтвердить приём быстро, тяжёлую обработку выносите в фон.
  5. event.bind вызывается через REST API самого Битрикс24 в контексте приложения. События доступны на коммерческих тарифах Битрикс24. В теле запроса Битрикс24 присылает auth.application_token — проверяйте его.

Если держать сервер постоянно активным нельзя, надёжнее остаётся опрос (polling) на стороне приложения.

Placement открывается с экраном входа вместо приложения

Симптом: сотрудник открывает вкладку с placement в Битрикс24, но в iframe показывается экран входа, а не приложение. Либо приложение пускает только владельца ключа, а другим сотрудникам из списка доступа по-прежнему предлагает войти. Две причины конфигурации.

Обработчик placement указывает на субдомен, а не на платформу

Битрикс24 должен открывать placement через обработчик платформы https://vibecode.bitrix24.tech/v1/bitrix-handler, а не напрямую на субдомен приложения (https://app-<hex>.vibecode.bitrix24.tech). Только через обработчик платформа создаёт сессию vibe_session_* и одноразовый код для Gateway (см. «Жизненный цикл запроса» выше). Если placement привязан напрямую к субдомену, Битрикс24 открывает iframe в обход платформы — сессия не создаётся, и Gateway показывает экран входа.

Адрес обработчика задаёт платформа, указывать субдомен приложения вручную не нужно. Если placement уже привязан к субдомену — переопубликуйте приложение: публикация заново привязывает обработчик к платформенному адресу.

Список доступа применяется только при политике `NAMED_USERS`

В режиме OWNER_ONLY доступ есть только у владельца ключа — записи списка доступа, добавленные через POST /access, не применяются. Чтобы пустить конкретных сотрудников портала, переключите политику на NAMED_USERS и добавьте пользователей:

Terminal
curl -X PATCH https://vibecode.bitrix24.tech/v1/infra/servers/SERVER_ID/access-policy \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "accessPolicy": "NAMED_USERS" }'

Подробнее о режимах и списке доступа — Политика доступа.

После обоих исправлений сотрудники из списка проходят в iframe автоматически. Ручной вход на субдомене в отдельной вкладке не требуется — если вы пробовали этот обходной путь, после исправления обработчика он не нужен.

Self-hosted приложение (не Black Hole)

Заголовок X-Vibe-Authorization проставляется только за Gateway, который стоит перед Black Hole-серверами. Если файлы приложения лежат на вашем собственном сервере, заголовок не подставляется — приложение само хранит токен сессии (свой BFF) и само вызывает GET /v1/me.

Как получить токен сессии текущего пользователя в placement (iframe):

  • Приложение Вайбкод с appUrl на вашем сервере. Placement приходит на обработчик Вайбкод, который перенаправляет на <appUrl>/?code=<одноразовый код>&placement=…&member_id=…. Ваш сервер обменивает код: POST /v1/oauth/token { app_key, code, redirect_uri } (значение redirect_uri — ровно настроенный appUrl). В браузер попадает только одноразовый код, не токен.
  • Собственное приложение Битрикс24 со своим обработчиком. Placement приходит на ваш сервер с токеном пользователя (AUTH_ID). Обменяйте его сервер-к-серверу: POST /v1/oauth/placement-session { app_key, access_token, member_id, domain }.

В обоих случаях ответ — { access_token: "vibe_session_…", user, expires_in }. Дальше приложение работает как BFF: хранит vibe_session_* на своей стороне, читает личность через GET /v1/me с Authorization: Bearer <vibe_session_*> и не отдаёт токен в браузер. Возможность встроить свою страницу в iframe — заголовки X-Frame-Options и CSP frame-ancestors — обеспечиваете вы, платформа за неё не отвечает. Стандартный GET /v1/oauth/authorize в iframe не открывается: страница согласия Битрикс24 запрещает встраивание. Открывайте его только в отдельной вкладке.

Смотрите также