К содержанию
TelegaFirst

Для AI-агентов: markdown этой страницы — /docs/curl-device-grant.mdиндекс документации — /llms.txt

Самостоятельная авторизация через curl

Обновлено

Этот сценарий регистрирует самостоятельный public diagnostic client, получает согласие человека в Telegram и сохраняет собственный access token локально. Нужны curl, jq, приватный локальный каталог и доступ человека к платформенному Telegram-боту. Бот вашего бизнеса для самого входа не требуется.

Используйте только регистрацию этого диагностического клиента. Токены из ChatGPT, Claude, Codex или другого AI-хоста не извлекают и не переиспользуют. Owner OAuth, API key и сессии покупателя — разные способы доступа; create_session и surface JWT не заменяют этот вход. Подробности — в авторизации.

Ниже приведены обезличенные контрактные ответы самостоятельного клиента. Это не запись ручного подключения AI-хоста; успешный curl не подтверждает поддержку или проверку какого-либо AI-клиента.

Discovery и локальный каталог

Machine endpoint — https://mcp.telegafirst.com/api/v1/mcp. Публичная документация доступна на обеих зонах .ru и .com; домен документации не меняет её язык.

Выполняйте команды в одной локальной shell-сессии. Каталог получает права 700, создаваемые файлы — 600 благодаря umask 077. Не включайте shell tracing и не передавайте содержимое секретных файлов в промпт, чат или публичный отчёт.

set -eu
umask 077
TGF_DIAGNOSTIC_DIR=$(mktemp -d "${TMPDIR:-/tmp}/tgf-device.XXXXXX")
chmod 700 "$TGF_DIAGNOSTIC_DIR"
TGF_OAUTH_BASE=https://mcp.telegafirst.com/api/v1/oauth
TGF_MCP_URL=https://mcp.telegafirst.com/api/v1/mcp

curl --silent --show-error --fail-with-body \
  --output "$TGF_DIAGNOSTIC_DIR/resource-metadata.json" \
  https://mcp.telegafirst.com/.well-known/oauth-protected-resource/api/v1/mcp
curl --silent --show-error --fail-with-body \
  --output "$TGF_DIAGNOSTIC_DIR/authorization-server.json" \
  https://mcp.telegafirst.com/.well-known/oauth-authorization-server
jq '{resource,authorization_servers}' "$TGF_DIAGNOSTIC_DIR/resource-metadata.json"
jq '{registration_endpoint,device_authorization_endpoint,token_endpoint,grant_types_supported}' \
  "$TGF_DIAGNOSTIC_DIR/authorization-server.json"

Проверьте resource и endpoints в возвращённой metadata: регистрация — /api/v1/oauth/register, device authorization — /api/v1/oauth/device_authorization, token — /api/v1/oauth/token. Поддержанный grant type — urn:ietf:params:oauth:grant-type:device_code. Если metadata другого сервера отличается, не отправляйте туда диагностические коды из этого сценария.

Регистрация собственного public client

Запрос POST /api/v1/oauth/register использует JSON и token_endpoint_auth_method: "none"; client secret не нужен. Регистрация требует redirect_uris. Здесь используется разрешённый loopback URI; device grant открывает возвращённый Telegram-deeplink и не запускает локальный callback listener.

{"client_name":"TelegaFirst curl diagnostic","redirect_uris":["http://127.0.0.1:8765/callback"],"token_endpoint_auth_method":"none"}
printf '%s\n' '{"client_name":"TelegaFirst curl diagnostic","redirect_uris":["http://127.0.0.1:8765/callback"],"token_endpoint_auth_method":"none"}' \
  > "$TGF_DIAGNOSTIC_DIR/register-request.json"
curl --silent --show-error --fail-with-body --request POST \
  --header 'Content-Type: application/json' \
  --data-binary @"$TGF_DIAGNOSTIC_DIR/register-request.json" \
  --output "$TGF_DIAGNOSTIC_DIR/register.json" "$TGF_OAUTH_BASE/register"

Обезличенный успешный ответ, HTTP 201:

{"client_id":"<CLIENT_ID>","client_name":"TelegaFirst curl diagnostic","redirect_uris":["http://127.0.0.1:8765/callback"],"token_endpoint_auth_method":"none","client_id_issued_at":1790769600}

Используйте полученный client_id, а не идентификатор зарегистрированного AI-хоста.

Device authorization и согласие в Telegram

Полный запрос POST /api/v1/oauth/device_authorization:

{"client_id":"<CLIENT_ID>","resource":"https://mcp.telegafirst.com/api/v1/mcp"}
jq --arg resource "$TGF_MCP_URL" '{client_id:.client_id,resource:$resource}' \
  "$TGF_DIAGNOSTIC_DIR/register.json" > "$TGF_DIAGNOSTIC_DIR/device-request.json"
curl --silent --show-error --fail-with-body --request POST \
  --header 'Content-Type: application/json' \
  --data-binary @"$TGF_DIAGNOSTIC_DIR/device-request.json" \
  --output "$TGF_DIAGNOSTIC_DIR/device.json" "$TGF_OAUTH_BASE/device_authorization"
jq '{verification_uri_complete,user_code,expires_in,interval}' "$TGF_DIAGNOSTIC_DIR/device.json"

Обезличенный успешный ответ, HTTP 200:

{"device_code":"<DEVICE_CODE>","user_code":"<USER_CODE>","verification_uri":"https://t.me/platform_bot","verification_uri_complete":"https://t.me/platform_bot?start=s_<NONCE>","expires_in":300,"interval":5}

platform_bot в этом ответе — имя бота тестового fixture. В реальном запуске откройте возвращённый сервером verification_uri_complete; не подставляйте тестовое имя или другой nonce. Ссылка содержит одноразовый код входа: открывайте её локально и не публикуйте.

Человек проверяет имя TelegaFirst curl diagnostic, совпадение user_code и каждое показанное право с обозначением риска, затем подтверждает карточку в Telegram. Текущий device flow запрашивает все девять consent bundles, включая операции с деньгами. Здесь нет обещания выбрать более узкий пакет через параметр scope. Не подтверждайте карточку, если права не подходят вашей задаче.

Выданный OAuthGrant сохраняет точные expanded capability scopes принятого согласия. Поле scope ответа ниже перечисляет bundle IDs; оно не заменяет проверки текущей роли, доступа к бизнесу, тарифа и прав при каждом вызове. Права и ограничения.

Polling: interval, slow_down и expiry

Полный запрос POST /api/v1/oauth/token:

{"grant_type":"urn:ietf:params:oauth:grant-type:device_code","client_id":"<CLIENT_ID>","device_code":"<DEVICE_CODE>"}

Первый poll и каждый последующий выполняются не раньше текущего interval. Начальное значение — 5 секунд. authorization_pending сохраняет интервал; каждый slow_down увеличивает его на 5 секунд для всех следующих попыток. Окно подтверждения — 300 секунд от device authorization; polling его не продлевает.

Сервер сообщает ожидание и слишком частый polling через HTTP 400, поэтому token-запрос ниже сохраняет тело без --fail-with-body и отдельно проверяет HTTP status.

{"error":"authorization_pending","error_description":"waiting for Telegram approval"}
{"error":"slow_down","error_description":"increase the polling interval by five seconds"}
jq -s '{grant_type:"urn:ietf:params:oauth:grant-type:device_code",client_id:.[0].client_id,device_code:.[1].device_code}' \
  "$TGF_DIAGNOSTIC_DIR/register.json" "$TGF_DIAGNOSTIC_DIR/device.json" \
  > "$TGF_DIAGNOSTIC_DIR/token-request.json"
TGF_POLL_INTERVAL=$(jq -r '.interval' "$TGF_DIAGNOSTIC_DIR/device.json")
while :; do
  sleep "$TGF_POLL_INTERVAL"
  TGF_HTTP_STATUS=$(curl --silent --show-error --request POST \
    --header 'Content-Type: application/json' \
    --data-binary @"$TGF_DIAGNOSTIC_DIR/token-request.json" \
    --output "$TGF_DIAGNOSTIC_DIR/token.json" --write-out '%{http_code}' \
    "$TGF_OAUTH_BASE/token")
  if [ "$TGF_HTTP_STATUS" = 200 ]; then
    jq -e '.token_type == "Bearer" and (.access_token | type == "string")' \
      "$TGF_DIAGNOSTIC_DIR/token.json" >/dev/null
    break
  fi
  TGF_OAUTH_ERROR=$(jq -r '.error // "unexpected_response"' "$TGF_DIAGNOSTIC_DIR/token.json")
  case "$TGF_OAUTH_ERROR" in
    authorization_pending) ;;
    slow_down) TGF_POLL_INTERVAL=$((TGF_POLL_INTERVAL + 5)) ;;
    *) printf '%s\n' 'Authorization stopped; inspect the private response locally and restart authorization if needed.' >&2; exit 1 ;;
  esac
done
chmod 600 "$TGF_DIAGNOSTIC_DIR"/*.json

Успех, HTTP 200; токен здесь заменён placeholder:

{"access_token":"<ACCESS_TOKEN>","token_type":"Bearer","scope":"workspace-read customer-conversations-read bot-setup conversations storefront-and-marketing ai-and-knowledge access-and-integrations platform-support money-operations"}

Текущий device token response не содержит refresh_token или expires_in. Значение 300 относится к окну device authorization, а не к сроку жизни access token. После успеха прекратите polling: authorization уже потреблена.

Терминальные ошибки token endpoint, HTTP 400:

СитуацияПолный ответДействие
Человек отказал{"error":"access_denied","error_description":"the user denied authorization"}Остановить polling; учитывать решение человека.
Device code истёк{"error":"expired_token","error_description":"device authorization expired"}Получить новую device authorization и новое согласие.
Сессия истекла или уже потреблена{"error":"expired_token","error_description":"device authorization expired or was consumed"}Не повторять обмен после успеха; для нового входа начать новое согласие.
Чужой client_id{"error":"invalid_grant","error_description":"device authorization belongs to a different client"}Проверить локальную пару registration/device; не использовать чужие коды.

При любой другой ошибке цикл также завершается; ответ остаётся в приватном файле. Device authorization до создания кодов отклоняет неизвестный client или неверный resource, HTTP 400:

{"error":"invalid_client","error_description":"unknown client_id"}
{"error":"invalid_target","error_description":"resource does not match this authorization server"}

Локальный токен и JSON-RPC curl

Создайте приватный файл заголовка из своего token response. Токен не появится в аргументах curl, shell history или выводе команды:

jq -r '"Authorization: Bearer " + .access_token' "$TGF_DIAGNOSTIC_DIR/token.json" \
  > "$TGF_DIAGNOSTIC_DIR/authorization-header.txt"
chmod 600 "$TGF_DIAGNOSTIC_DIR/authorization-header.txt"

MCP использует Streamable HTTP на том же endpoint. У tools/list и describe_tool нет самостоятельного REST twin в этом сценарии; вызывайте их JSON-RPC. Эти примеры читают список и схему, не создают бизнес и не меняют деньги. Доступ требует действующего OAuth grant; meta-discovery не требует отдельного operation scope, но показывает только допустимый текущему caller inventory.

Для поддержанного legacy handshake отправьте initialize, затем прочитайте актуальный список:

printf '%s\n' '{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {},
    "clientInfo": {
      "name": "TelegaFirst curl diagnostic",
      "version": "1.0"
    }
  }
}' \
  > "$TGF_DIAGNOSTIC_DIR/initialize-request.json"
curl --silent --show-error --fail-with-body --request POST \
  --header @"$TGF_DIAGNOSTIC_DIR/authorization-header.txt" \
  --header 'Content-Type: application/json' --header 'Accept: application/json, text/event-stream' \
  --data-binary @"$TGF_DIAGNOSTIC_DIR/initialize-request.json" \
  --output "$TGF_DIAGNOSTIC_DIR/initialize-response.txt" "$TGF_MCP_URL"

printf '%s\n' '{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list",
  "params": {}
}' \
  > "$TGF_DIAGNOSTIC_DIR/tools-list-request.json"
curl --silent --show-error --fail-with-body --request POST \
  --header @"$TGF_DIAGNOSTIC_DIR/authorization-header.txt" \
  --header 'Content-Type: application/json' --header 'Accept: application/json, text/event-stream' \
  --data-binary @"$TGF_DIAGNOSTIC_DIR/tools-list-request.json" \
  --output "$TGF_DIAGNOSTIC_DIR/tools-list-response.txt" "$TGF_MCP_URL"

В stateless transport не нужно переносить session ID между этими запросами. Сохраняйте ответ как текст: HTTP transport может вернуть JSON или SSE; не считайте весь SSE-файл одним JSON-документом. Успешный JSON-RPC ответ имеет соответствующий id и result; для tools/list прочитайте возвращённые tools и их schemas, а не фиксированное число инструментов из примера.

Чтобы получить схему конкретного доступного инструмента, вызовите describe_tool с tool_id, например:

{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"describe_tool","arguments":{"tool_id":"get_onboarding_state"}}}
printf '%s\n' '{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "describe_tool",
    "arguments": {
      "tool_id": "get_onboarding_state"
    }
  }
}' \
  > "$TGF_DIAGNOSTIC_DIR/describe-request.json"
curl --silent --show-error --fail-with-body --request POST \
  --header @"$TGF_DIAGNOSTIC_DIR/authorization-header.txt" \
  --header 'Content-Type: application/json' --header 'Accept: application/json, text/event-stream' \
  --data-binary @"$TGF_DIAGNOSTIC_DIR/describe-request.json" \
  --output "$TGF_DIAGNOSTIC_DIR/describe-response.txt" "$TGF_MCP_URL"

Полученная схема не устанавливает dedicated callable tool и не доказывает разрешение выполнить операцию. Проверяйте tools/list после изменения readiness или активного бизнеса; следующий шаг описан в руководстве по началу работы и MCP-гайде.

HTTP 401 с WWW-Authenticate требует проверить или повторить авторизацию. Ошибка выполнения инструмента может прийти в HTTP-успешном JSON-RPC result с isError: true; наличие HTTP 200 само по себе не означает успех инструмента. Отказ прав, тарифного окна или readiness не обходят API key либо surface JWT. Здесь не приведён выдуманный MCP success/failure fixture: actual result зависит от текущих прав и состояния вашего бизнеса. Коды ошибок.

Завершение диагностики

Файлы token.json, authorization-header.txt, device request/response и ответы MCP остаются только локально. Не добавляйте их в репозиторий, evidence, URL или промпт. После завершения удалите созданный приватный каталог; удаление файлов само по себе не отзывает уже выданный grant. Для прекращения доступа отзовите подключение через существующее управление доступом, описанное в авторизации.