## Вход в приложение из десктопа

`POST /v1/cowork/app-login`

Выдаёт десктопу Cowork/Code адрес задеплоенного приложения со ссылкой для входа, чтобы встроенный браузер открыл приложение сразу от имени владельца ключа, без формы авторизации.

Платформа сама решает, пускать ли человека: приложение должно принадлежать аккаунту Битрикс24, к которому привязан ключ, а его политика доступа должна пускать владельца ключа. Шлюз меняет токен из адреса на свою куку поддомена приложения и перенаправляет на адрес без токена. Приложение получает номер пользователя в Битрикс24, как при обычном входе. Токен открывает только это приложение — ни кабинет платформы, ни соседние приложения по нему не открываются.

Вызвать эндпоинт может только ключ десктопа Cowork/Code со скоупом `vibe:cowork`. Ключ агентского места и ключ стороннего агента несут тот же скоуп, но получают `403 COWORK_DESKTOP_KEY_REQUIRED`.

**Скоуп:** `vibe:cowork`. Ключ должен относиться к классу десктопа Cowork/Code.

## Поля запроса (body)

| Поле | Тип | Обяз. | Описание |
|------|-----|-------|----------|
| `appUrl` | string | да | Адрес приложения, которое нужно открыть, с любым путём, параметрами запроса и якорем. Схема, хост и порт должны точно совпадать с адресом приложения. Данные пользователя в адресе и параметр `__gw_token` не допускаются |

## Примеры

### curl

```bash
curl -X POST https://vibecode.bitrix24.tech/v1/cowork/app-login \
  -H "X-Api-Key: YOUR_COWORK_KEY" \
  -H "Content-Type: application/json" \
  -d '{"appUrl": "https://app-3f9c2a1b7e.vibecode.bitrix24.tech/orders?status=new"}'
```

### JavaScript — получить адрес входа и проверить его

```javascript
// Адрес приложения: https, поддомен app-* домена приложений, без порта, данных пользователя и токена
function isAppAddress(appUrl) {
  try {
    const u = new URL(appUrl)
    return u.protocol === 'https:' && !u.port && !u.username && !u.password
      && !u.searchParams.has('__gw_token')
      && /^app-[a-z0-9-]+\.vibecode\.bitrix24\.tech$/.test(u.hostname)
  } catch {
    return false
  }
}

async function getAppLoginUrl(appUrl) {
  // Не адрес приложения — ручку не зовём и адрес не открываем
  if (!isAppAddress(appUrl)) return null
  const res = await fetch('https://vibecode.bitrix24.tech/v1/cowork/app-login', {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_COWORK_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ appUrl }),
  })
  if (!res.ok) {
    const { error } = await res.json().catch(() => ({}))
    // Платформа не признала адрес приложением — не открываем его совсем
    if (error?.code === 'INVALID_APP_URL') return null
    return appUrl // прочие отказы — адрес уже проверен выше, открываем его с обычным входом
  }

  const { url } = await res.json() // { url, expiresIn } — без обёртки success/data
  // Ответ отличается от запроса ровно одним параметром токена
  const stripped = url.replace(/[?&]__gw_token=[^&#]*/, '')
  return stripped === appUrl ? url : null // расхождение — не открываем ничего
}
```

## Поля ответа

| Поле | Тип | Описание |
|------|-----|----------|
| `url` | string | Исходный `appUrl` с одним добавленным параметром `__gw_token`. Параметр стоит последним в строке запроса, перед якорем. Токен не разбирайте и не пишите в журналы |
| `expiresIn` | number | Срок жизни токена в секундах. Платформа берёт его из самого подписанного токена |

## Пример ответа

```json
{
  "url": "https://app-3f9c2a1b7e.vibecode.bitrix24.tech/orders?status=new&__gw_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXAiOiJndyJ9.c2lnbmF0dXJl",
  "expiresIn": 600
}
```

## Пример ответа при ошибке

```json
{
  "success": false,
  "error": {
    "code": "APP_NOT_AVAILABLE",
    "message": "The app is not available."
  }
}
```

## Ошибки

| HTTP | Код | Описание |
|------|-----|----------|
| 400 | `INVALID_APP_URL` | Поле `appUrl` не передано, не строка или не является точным адресом приложения: другая схема или порт, лишний уровень поддомена, данные пользователя, управляющие символы, уже присутствующий параметр `__gw_token`. Повтор с тем же телом не поможет |
| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |
| 401 | `INVALID_API_KEY` | Ключ не опознан: такой строки на платформе нет |
| 401 | `KEY_INACTIVE` | Ключ отозван — например, устройство отключили через [`DELETE /v1/cowork/key`](/docs/cowork/key) |
| 401 | `KEY_EXPIRED` | Срок действия ключа истёк |
| 402 | `ACCOUNT_FROZEN` | Баланс аккаунта исчерпан — пополните счёт |
| 403 | `INSUFFICIENT_SCOPE` | У ключа нет скоупа `vibe:cowork` |
| 403 | `COWORK_DESKTOP_KEY_REQUIRED` | Ключ не относится к классу десктопа Cowork/Code — так отвечает и ключ агентского места, и ключ стороннего агента |
| 403 | `PORTAL_REQUIRED` | Ключ не привязан к человеку на аккаунте Битрикс24 |
| 403 | `WRITE_BLOCKED_READONLY_KEY` | Ключ выпущен в режиме «только чтение» |
| 404 | `APP_NOT_AVAILABLE` | Один ответ на все исходы проверки доступа: приложения нет, оно относится к другому аккаунту, его политика доступа не пускает владельца ключа, либо платформа не смогла принять решение |
| 409 | `B24_USER_UNKNOWN` | Не удалось определить номер владельца ключа в Битрикс24, поэтому подтвердить его личность нечем |
| 413 | `PAYLOAD_TOO_LARGE` | Тело больше 8 КБ |
| 415 | `FST_ERR_CTP_INVALID_MEDIA_TYPE` | Тело прислано с типом содержимого, который этот маршрут не разбирает. Отправляйте тело с заголовком `Content-Type: application/json` |
| 429 | `RATE_LIMITED` | Превышен предел частоты на связку аккаунт и человек — несколько ключей одного человека делят общий предел. Суммарный лимит платформы — 30 запросов в минуту. Действующее для вашего ключа значение приходит в заголовке `x-ratelimit-limit` — оно ниже суммарного, поскольку лимит делится между репликами |
| 503 | `user_self_deletion_pending` | Владелец ключа ожидает удаления учётной записи на этом аккаунте. Запрос отклоняется до выдачи токена; повторяйте после задержки из `Retry-After` |

Полный список общих ошибок API — [Ошибки](/docs/errors).

## Известные особенности

**Успешный ответ (200) — это сам объект, без обёртки `success`.** Ошибки приходят в конверте `{ success: false, error: { code, message } }`. Определяйте успех по HTTP-статусу (`res.ok`).

**Адрес ответа совпадает с запросом побайтно, кроме параметра токена.** Платформа не пересобирает адрес: кодировка параметров, пустые значения и якорь остаются как в `appUrl`. Сравнивайте ответ с запросом после удаления параметра `__gw_token` и не открывайте адрес, если осталось расхождение. После обмена токена на куку шлюз перенаправляет на адрес без токена и собирает строку запроса заново: порядок параметров и запись пустых значений у приложения могут отличаться от `appUrl`.

**Токен действует до конца срока, а не один раз.** Повторный переход по тому же адресу в пределах `expiresIn` снова пускает в приложение. После первого перехода шлюз ставит куку и перенаправляет на адрес без токена, поэтому следующие открытия того же приложения идут по куке, без нового вызова.

**Отзыв ключа останавливает выдачу новых входов.** Кука, которую шлюз уже поставил, живёт свой срок. Политику доступа приложения шлюз перепроверяет сам, поэтому закрытие доступа к приложению действует и для такой куки.

**Как реагировать на отказы.** После `400 INVALID_APP_URL` адрес не открывайте совсем: платформа не признала его адресом приложения, покажите пользователю ошибку. Любой другой отказ означает, что приложение нужно открыть по исходному адресу с обычным входом — но только если адрес прошёл вашу собственную проверку до запроса: `https`, поддомен вида `app-*` домена приложений, без порта, данных пользователя и параметра `__gw_token`. Отказ `401` или `403` приходит до разбора адреса и не подтверждает, что это адрес приложения. `401` — попросите пользователя войти в десктоп заново. `400` и `403` — ошибка клиента, повторять запрос бессмысленно. `429` — повторяйте с растущей паузой.

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

- [Cowork/Code](/docs/cowork)
- [Отключить это устройство](/docs/cowork/key)
- [Ошибки](/docs/errors)
