vibemd

Инструкция для AI-агента. Машиночитаемая копия: /agent.md — дай агенту эту ссылку, дальше он справится сам.

# vibemd: инструкция для AI-агента

Ты пишешь сайт, которому нужен бэкенд: хранить заявки, показывать каталог,
пускать владельца в админку. vibemd даёт это без своего сервера — ты объявляешь
сущности, получаешь CRUD REST API и заливаешь готовый статический билд, а он
живёт на `https://{slug}.vibemd.ru` с уже подключённым бэкендом.

Всё делается обычными HTTP-запросами. Отдельную утилиту ставить не нужно —
хватает `curl`, который у тебя уже есть.

Базовый URL API: `https://api.vibemd.ru`. Дальше он называется `$API`.

## Правила, которые нарушают чаще всего

Прочти эти пять пунктов до того, как что-то писать. Остальное — детали.

1. **Admin-ключ никогда не попадает в браузер.** Ни в HTML, ни в JS, ни в
   `.env` фронтенда. Он остаётся у тебя, в файле, который не коммитится.
2. **URL API и public-ключ не хардкодятся в билде.** Они подставляются в HTML
   при отдаче — читай их из `window.VIBEMD` (см. шаг 3).
3. **Форма заявок — это `public_access: "append"`.** Public-ключ виден в
   исходнике страницы: с `write` любой посетитель выгрузил бы все чужие заявки.
4. **`index.html` лежит в корне архива**, а не внутри папки `dist/`.
5. **Ошибку читай целиком.** В ответе есть `code`, список конкретных проблем в
   `details` и часто `hint` со следующим шагом. Там уже написано, что чинить.

## Шаг 1. Проект

Проект — это тенант: свои данные, свои ключи, свой поддомен.

```bash
API=https://api.vibemd.ru

curl -sX POST $API/v1/projects \
  -H "Authorization: Bearer $VIBEMD_PLATFORM_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"name":"Мой лендинг"}'
```

`VIBEMD_PLATFORM_TOKEN` выдаётся человеком — это единственная операция, которую
нельзя выполнить ключом проекта, потому что проекта ещё нет. Если токена нет,
попроси его у пользователя, не выдумывай.

Ответ содержит `slug`, `site_url` и **два ключа — они показываются один раз**:

```json
{
  "project": {"id": "...", "slug": "moi-lending"},
  "site_url": "https://moi-lending.vibemd.ru",
  "keys": [
    {"tier": "admin",  "token": "vmd_a_..."},
    {"tier": "public", "token": "vmd_p_..."}
  ]
}
```

Сразу сохрани их локально и закрой от git:

```bash
cat > .vibemd.json <<EOF
{"api":"$API","slug":"moi-lending","admin_token":"vmd_a_...","public_token":"vmd_p_..."}
EOF
grep -qxF '.vibemd.json' .gitignore || echo '.vibemd.json' >> .gitignore
```

Дальше admin-ключ удобно держать в переменной: `ADMIN=vmd_a_...`.

Если проект уже создан и ключ потерян — новый не получить, ключи не
восстанавливаются. Заведи новый ключ существующим admin-ключом
(`POST $API/v1/project/keys`) или создай проект заново.

## Шаг 2. Сущности

Сущность — это таблица, объявленная на лету. Типы полей: `string`, `number`,
`boolean`, `date`, `json`. Имена сущностей и полей — `snake_case`, с буквы.

```bash
curl -sX POST $API/v1/entities \
  -H "Authorization: Bearer $ADMIN" -H 'Content-Type: application/json' \
  -d '{
    "name": "lead",
    "public_access": "append",
    "fields": [
      {"name": "email",     "type": "string",  "required": true},
      {"name": "message",   "type": "string"},
      {"name": "budget",    "type": "number"},
      {"name": "contacted", "type": "boolean", "default": false}
    ]
  }'
```

`public_access` — что можно делать public-ключом из браузера:

| Значение | Разрешено | Когда |
| --- | --- | --- |
| `none` | ничего | служебные данные |
| `read` | читать | каталог, услуги, цены |
| `append` | **только создавать** | формы заявок, отзывы до модерации |
| `write` | читать, создавать, менять, удалять | публичная гостевая книга |

Правило простое: всё, что public-ключ может прочитать, считай опубликованным.

`auth_access` — то же самое, но для пользователя, вошедшего на сайте. По
умолчанию `none`. Владелец проекта (`owner: true`) видит всё независимо от него.

Изменение схемы — `PUT $API/v1/entities/lead` со **полным** новым списком полей;
в ответе придёт диф. Изменения, от которых уже сохранённые записи стали бы
невалидными (смена типа, новое `required` без `default`), отклоняются с
объяснением — это защита, а не сбой.

## Шаг 3. Фронтенд

В каждый отдаваемый HTML сервер подставляет скрипт с настройками, поэтому билд
не знает ни адреса API, ни ключа:

```js
const { apiUrl, publicKey } = window.VIBEMD;

await fetch(`${apiUrl}/v1/entities/lead/records`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${publicKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ email, message }),
});
```

`window.VIBEMD` — это `{apiUrl, publicKey, projectId, slug}`. Он определён до
твоего бандла. Не копируй значения в код и не клади их в `.env` сборки: ключ
ротируется без передеплоя ровно потому, что подставляется на отдаче.

Чтение списка — обычный GET с фильтрами:

```js
const res  = await fetch(`${apiUrl}/v1/entities/service/records?sort=-created_at&limit=20`,
                         { headers: { Authorization: `Bearer ${publicKey}` } });
const { records, total, has_more } = await res.json();
```

Запись выглядит так: собственные поля лежат в `data`.

```json
{"id": "uuid", "data": {"email": "a@b.ru"}, "created_at": "...", "updated_at": "..."}
```

## Шаг 4. Деплой

Собери фронтенд как обычно и отправь содержимое папки билда одним запросом:

```bash
tar czf - -C ./dist . | curl -sX POST $API/v1/deployments \
  -H "Authorization: Bearer $ADMIN" --data-binary @-
```

Обрати внимание на `-C ./dist .` — архивируется **содержимое** папки, поэтому
`index.html` оказывается в корне. Ошибка `missing index.html in build root`
означает ровно это: ты упаковал папку целиком.

Формат — `tar.gz` или `zip`, определяется по содержимому. Версия становится
живой сразу; `?activate=false` заливает, не включая.

```bash
curl -s  $API/v1/deployments             -H "Authorization: Bearer $ADMIN"   # версии
curl -sX POST $API/v1/deployments/3/activate -H "Authorization: Bearer $ADMIN"   # откат
```

Откат мгновенный: переключается указатель, файлы не двигаются.

Сертификат для поддомена выпускается сам при первом заходе — первый запрос
может занять несколько секунд, дальше обычная скорость.

## Владелец и пользователи сайта

Типовая задача: форма пишет заявки, а человек хочет их читать — но admin-ключ в
браузер отдавать нельзя. Для этого есть вход на самом сайте.

```bash
# 1. Агент заводит владельца. Публичной регистрации по умолчанию нет.
curl -sX POST $API/v1/users \
  -H "Authorization: Bearer $ADMIN" -H 'Content-Type: application/json' \
  -d '{"email":"owner@example.ru","password":"...","owner":true}'
```

```js
// 2. Страница логинит его public-ключом и получает токен.
const r = await fetch(`${apiUrl}/v1/auth/login`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${publicKey}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ email, password }),
});
const { token } = await r.json();

// 3. Дальше запросы идут с двумя заголовками: ключ говорит "какой проект",
//    токен — "кто смотрит".
await fetch(`${apiUrl}/v1/entities/lead/records`, {
  headers: { Authorization: `Bearer ${publicKey}`, 'X-User-Token': token },
});
```

Владелец читает всё в своём проекте, даже сущности с `public_access: "append"`.
Обычный вошедший пользователь ограничен `auth_access` сущности.

Открыть регистрацию посетителей (по умолчанию закрыта):

```bash
curl -sX PATCH $API/v1/project -H "Authorization: Bearer $ADMIN" \
  -H 'Content-Type: application/json' -d '{"auth_registration":"open"}'
```

## Запросы к данным

Фильтры: `?filter.<поле>=<значение>` или `?filter.<поле>.<оператор>=<значение>`.
Операторы: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `contains`, `in`.

```
?filter.contacted=false&filter.budget.gte=30000&sort=-budget&limit=25&offset=0
```

Сортировка — `sort=-budget,email`, минус означает убывание. В ответе есть
`total` и `has_more`. Даты принимаются в RFC3339, `2006-01-02T15:04:05` или
`2006-01-02` и хранятся в UTC.

Записи: `POST` создать, `GET` список или одну по id, `PUT` заменить целиком,
`PATCH` изменить переданные поля, `DELETE` удалить.

## Ошибки

Формат всегда один:

```json
{
  "error": {
    "code": "validation_failed",
    "message": "record does not match the schema of entity \"lead\": 2 problem(s)",
    "details": [
      {"field": "email", "code": "required", "message": "field \"email\" is required but missing"},
      {"field": "budget", "code": "type_mismatch", "expected": "number", "got": "string \"много\""}
    ],
    "hint": "inspect the schema with GET /v1/entities/lead"
  }
}
```

Все нарушения приходят разом — чини запрос за одну попытку, а не по одному.

Коды: `bad_request`, `invalid_json`, `validation_failed`, `unauthorized`,
`forbidden`, `not_found`, `conflict`, `limit_exceeded`, `rate_limited`,
`internal`.

Что означают самые частые:

- `unauthorized` — ключа нет, он отозван или это не тот тир.
- `forbidden` — ключ верный, но политика сущности не разрешает эту операцию.
  Смотри `public_access`/`auth_access`, а не ключ.
- `not_found` на чужой записи выглядит так же, как на несуществующей — это
  специально: чужой UUID не должен ничего подтверждать.
- `rate_limited` — лимит запросов проекта в минуту; в ответе есть `Retry-After`.

## Чеклист перед тем, как сказать «готово»

- [ ] `curl -sI https://{slug}.vibemd.ru` отвечает 200.
- [ ] Форма реально создаёт запись: `GET /v1/entities/lead/records` с
      admin-ключом показывает её.
- [ ] В собранном билде нет ни `vmd_a_`, ни захардкоженного `api.vibemd.ru`:
      `grep -rE 'vmd_a_|api\.vibemd\.ru' ./dist` — пусто.
- [ ] Сущности с формами стоят на `append`, а не на `write`.
- [ ] `.vibemd.json` в `.gitignore`.

## Полный список эндпоинтов

`GET $API/v1/` возвращает машиночитаемый список всех методов, типов полей и
операторов. Если сомневаешься в сигнатуре — читай его, а не угадывай.