Документация / rest

REST API: один ZIP, явные метаданные, серверные ограничения

REST-контракт AIfra: ZIP upload, обязательные headers, status, update без продления TTL и delete.

Статус: Открытая beta. Перед загрузкой прочитайте утверждённые условия и сохраните management token приватно.

01

Endpoints

POST /v1/previews создаёт preview. GET /v1/previews/{id} возвращает публичный status. PUT заменяет revision, DELETE удаляет preview. GET /v1/health и /v1/ready предназначены для platform health checks.

POST и PUT принимают raw application/zip. Максимум проверяется и для upload, и после безопасного извлечения.

POST   /v1/previews
GET    /v1/previews/{id}
PUT    /v1/previews/{id}
DELETE /v1/previews/{id}

02

Как выглядит результат

Готовые index.html и style.css были упакованы в ZIP и отправлены опубликованным ниже примером. Viewer открыл сайт со сроком удаления и кнопкой сообщения о нарушении. Пользовательский JavaScript работает в изолированном iframe.

Это скриншот локальной проверки 9 сентября 2026 года. Он показывает фактический результат REST-сценария, но не подтверждает работу публичного HTTPS или доступность из всех сетей.

Тестовый сайт AIfra: готовая HTML-страница и служебная полоса со временем удаления preview
Реальный результат локального Docker/PostgreSQL-теста; версия для компьютера и телефона.

03

Загрузить подготовленный ZIP

Пример для Bash и curl. В preview.zip должен быть index.html в корне. API_ORIGIN берётся из вашего окружения; до открытия beta используйте только локальный или разрешённый staging API.

INSTALL_ID — ваш случайный локальный installation ID вида aid_<20 или более символов base64url>. Ответ содержит секрет: сохраняйте .ai-deploy приватно и добавьте этот каталог в .gitignore перед запуском.

umask 077
mkdir -p .ai-deploy
curl --fail-with-body --silent --show-error \
  "$API_ORIGIN/v1/previews" \
  -H "Content-Type: application/zip" \
  -H "X-AI-Deploy-Accept-Terms: true" \
  -H "X-AI-Deploy-Anonymous-ID: $INSTALL_ID" \
  -H "X-AI-Deploy-Source: unknown" \
  -H "X-AI-Deploy-Client-Name: curl" \
  -H "X-AI-Deploy-Client-Version: 1.0.0" \
  --data-binary @preview.zip \
  --output .ai-deploy/create-response.json

04

Обязательные deployment headers

При POST и PUT нужны accept terms, анонимный installation ID и честная client attribution. Если источник неизвестен, передавайте unknown.

Content-Type: application/zip
X-AI-Deploy-Accept-Terms: true
X-AI-Deploy-Anonymous-ID: aid_<unguessable>
X-AI-Deploy-Source: unknown
X-AI-Deploy-Client-Name: my-client
X-AI-Deploy-Client-Version: 1.0.0

05

Management token

Create возвращает management token один раз. Update и delete принимают его как Bearer token. Сервис хранит только hash и не включает token в status или логи.

Неверный token не раскрывает наличие preview. Update сохраняет первоначальный expires_at.

Authorization: Bearer pmt_<secret>

06

Рабочий пример для Node.js

Этот пример проверяется автоматическим тестом на настоящем HTTP API в локальном окружении. Публичная доступность API проверяется отдельно. Нужен Node.js 24; дополнительные пакеты не требуются. Сохраните код в upload.mjs.

Подготовьте ZIP с index.html в корне. Задайте API_ORIGIN, ZIP_PATH и STATE_FILE — новый файл вне репозитория в личной папке пользователя. После прочтения условий и согласия задайте ACCEPT_TERMS=true. Запустите node upload.mjs. DEPLOY_SOURCE можно задать как codex, claude-code или cursor только если это настоящий источник запроса; иначе оставьте unknown.

В терминале появятся только ID, ссылка и время удаления. Полный ответ с ключом управления будет записан в STATE_FILE. На Windows храните его в личном профиле с ограниченным доступом; режим 0600 сам по себе не настраивает Windows ACL. Не открывайте этот файл в чате агента.

import { open, readFile, stat } from 'node:fs/promises';
import { randomBytes } from 'node:crypto';

if (process.env.ACCEPT_TERMS !== 'true') throw new Error('Read the terms and set ACCEPT_TERMS=true');
const api = new URL(process.env.API_ORIGIN);
if (api.username || api.password || api.pathname !== '/' || api.search || api.hash ||
    !(api.protocol === 'https:' || (api.protocol === 'http:' && ['localhost', '127.0.0.1'].includes(api.hostname)))) {
  throw new Error('Use HTTPS or a local test API origin');
}
const zipPath = process.env.ZIP_PATH || 'preview.zip';
if ((await stat(zipPath)).size > 50 * 1024 * 1024) throw new Error('ZIP exceeds 50 MiB');
const archive = await readFile(zipPath);
// STATE_FILE must be outside the repository, in your private user directory.
// Exclusive creation prevents overwriting an earlier management token.
const state = await open(process.env.STATE_FILE, 'wx', 0o600);
try {
  const response = await fetch(new URL('/v1/previews', api), {
    method: 'POST', redirect: 'error', signal: AbortSignal.timeout(120_000),
    headers: {
      'Content-Type': 'application/zip',
      'X-AI-Deploy-Accept-Terms': 'true',
      'X-AI-Deploy-Anonymous-ID': 'aid_' + randomBytes(18).toString('base64url'),
      'X-AI-Deploy-Source': process.env.DEPLOY_SOURCE || 'unknown',
      'X-AI-Deploy-Client-Name': 'rest-example',
      'X-AI-Deploy-Client-Version': '1.0.0',
    },
    body: archive,
  });
  const result = await response.json();
  if (!response.ok) {
    const code = String(result.error?.code || 'request_failed');
    throw new Error('HTTP ' + response.status + ': ' + (/^[a-z_]+$/.test(code) ? code : 'request_failed'));
  }
  await state.writeFile(JSON.stringify(result));
  await state.sync();
  console.log(JSON.stringify({ preview_id: result.preview_id, url: result.url, expires_at: result.expires_at }));
} finally {
  await state.close();
}

07

Если запрос не прошёл

terms_not_accepted: явно примите условия перед повтором. archive_path_invalid или archive_link_forbidden: пересоберите ZIP из готовой папки без ссылок и путей за её пределы. phishing_content: удалите запрещённые формы и поля паролей; предупреждение не означает, что сервис вынес юридическое заключение о содержимом.

active_preview_limit: с этого IP уже заняты три места. Удалите свой ненужный preview с его ключом управления или дождитесь окончания срока. У пользователей общего VPN или офиса может быть один внешний IP. Смена IP для обхода лимита не является решением.

upload_too_large или extracted_too_large: уменьшите готовые файлы до 50 MiB. create_rate_limit или update_rate_limit: выдержите указанную сервером паузу, не запускайте повтор в бесконечном цикле. service_unavailable: создание временно закрыто; проверьте объявленный статус сервиса.

Если соединение оборвалось после отправки ZIP, не повторяйте POST автоматически: preview мог уже появиться. Пример оставляет файл состояния и не перезаписывает его при повторном запуске. Если полный ответ не сохранился, ключ нельзя восстановить; обратитесь в поддержку или дождитесь удаления через 48 часов.

08

Деплой через VPN и просмотр без VPN

ZIP отправляет процесс, который запускает HTTP-клиент. Это может быть ваш компьютер, облачное окружение агента или CI. У него должен быть доступ к API_ORIGIN по HTTPS. Доступность модели и доступность API — разные соединения.

Если выбранный VPN не соединяется с API, проверьте маршрут на устройстве, где выполняется загрузка. В собственном VPN-клиенте можно отдельно направить домен API через прямое соединение, оставив соединение с моделью через VPN. Результат зависит от сети; универсальная совместимость пока не подтверждена.

После загрузки другой человек открывает ссылку viewer со своего устройства. Его сеть не участвует в загрузке ZIP. Проверка просмотра в РФ без VPN и проверка загрузки из окружения агента нужны отдельно.

09

Машинная спецификация

Проверяемый OpenAPI 3.1 документ генерируется из того же TypeScript-объекта, который отдаёт API. Статическая копия не поддерживается вручную.

GET /openapi.json