Перейти к содержимому
Версия 1.0 ещё не выпущена; проект находится на стадии pre-MVP. См. возможности и ограничения версии 1.0.

Автономный бэкенд: справочник API

Эта страница описывает API, аутентификацию, развёртывание и откат генерируемого автономного бэкенда. Она описывает генерируемые исходники, а не развёртывание в Cloudflare, прошедшее приёмку. Что такое автономный бэкенд и как его сгенерировать, см. в обзоре. Файлы, зависимости и соответствие контента таблицам описаны в деталях реализации.

Генерируемые ID, например Article.findMany, обозначают операции. HTTP-маршруты используют имя модели в нижнем регистре, например /api/blogpost. Настраиваемый каталог включает только операции с генерируемой защитой.

Метод/путь после /api/<model> Операция Результат
GET / findMany Массив строк
GET /paginated findManyPaginated { data, total, hasMore }
GET /unique findUnique Строка или null
POST / create Строка, статус 201
PUT / update Строка
DELETE / delete Строка

Связующие таблицы предоставляют только три операции чтения; список записывается через владельца (ниже). Таблицы переводов также принимают create, update и delete редактора. Массовые операции, aggregate/count/groupBy, upsert, updateEach и прочие ненастроенные операции отсутствуют. Отключение убирает операцию из выполнения и OpenAPI, но сохраняет её путь зарезервированным, прежде чем подключаются собственные прямые маршруты. У чтений нет POST-алиаса.

Аргументы чтения передаются query-параметрами, структурные значения — JSON. Запись принимает объекты аргументов Prisma в JSON-теле. Кодируйте структурные query-значения для URL.

GET /api/blogpost?take=25&skip=25
GET /api/blogpost/unique?where={"id":"item-id"}
POST /api/blogpost {"data":{"id":"new-post","featured":false}}
PUT /api/blogpost {"where":{"id":"new-post"},"data":{"featured":true}}
DELETE /api/blogpost {"where":{"id":"new-post"}}

Это не публичный /v1 API CMS: потребитель получает результаты Prisma, а не контракт CMS { data, page }.

Идентификатор связи к одному записывается в строке, как любой столбец. Если цель отсутствует, возвращается 422 relation_target_missing.

Иерархическая ссылка на родителя (связь к одному со своей моделью, отмеченная Иерархия (родитель/потомок)) никогда не образует цикл. Создание или изменение, которое делает строку собственным родителем или помещает её под одного из её потомков, возвращает 422 relation_hierarchy_cycle с field (столбец <field>_id) и targets. Сгенерированные маршруты создания и изменения и доверенные маршруты проверяют это до записи. SQLite-триггеры в migrations/ отклоняют ту же запись внутри базы, поэтому две параллельные записи не могут пройти обе; отклонённая получает тот же ответ. Каждая миграция удаляет эти триггеры до своих изменений и создаёт их заново после. Создание корневых записей, смена родителя и удаление ссылки на родителя (null) выполняются обычными операциями записи. Если указан идентификатор другой модели, другого сайта или несуществующей строки, возвращается relation_target_missing. Строки локалей не хранят родителя.

У связи-списка один маршрут на поле у владельца: PUT /api/<owner>/relations/<field>. В теле укажите строку владельца, а также весь упорядоченный список или изменения в нём.

PUT /api/article/relations/tags {"where":{"id":"post-1"},"set":["tag-2","tag-1"]}
PUT /api/article/relations/tags {"where":{"id":"post-1"},"connect":["tag-3"],"disconnect":["tag-1"]}
POST /api/articletranslation {"data":{"article_id":"post-1","lang":"fr","title":"Bonjour"}}
  • set заменяет список в указанном порядке. connect добавляет в конец; disconnect убирает, а несвязанный идентификатор считается уже убранным. set нельзя сочетать с двумя другими.
  • Итоговый список проверяется правилами CMS: без повторов (relation_duplicate_target), обязательность и minItems (relation_too_few), maxItems (relation_too_many), существующие цели (relation_target_missing). Количество элементов проверяется при каждом запросе. Удаление цели удаляет связь, поэтому элементов может стать меньше minItems; тогда маршрут отвечает relation_too_few, пока запись не вернёт список в допустимые пределы. Список, равный сохранённому, не перезаписывается и возвращается в сохранённом виде.
  • Целевая строка one-to-many, уже принадлежащая другому владельцу, отклоняется с 409 relation_target_taken.
  • Связи перезаписываются одним пакетом операций БД. Если список изменился после чтения, ничего не пишется, ответ 409 relation_changed_concurrently.
  • Маршрут наследует <Owner>.update: режим доступа, разрешение, хуки операции и групп. Отключение <Owner>.update отключает маршрут; путь остаётся зарезервированным.

Строка локали создаётся с ключом строки и языком. Язык должен быть одним из языков, с которыми сгенерирован сайт (422 unknown_locale): настроенные языки блога или язык, который есть в опубликованном контенте. Сайт типа Website не закрепляет список языков, поэтому язык без опубликованного контента требует регенерации. Основная строка должна существовать (422 translation_row_missing); для каждого языка у неё допускается одна строка перевода (409 locale_exists). Обновление меняет только переведённые значения. Записи переводов наследуют <Model>.update, удаления — <Model>.delete.

Поле-связь может допускать больше одной модели. Такое поле хранится в одной join-таблице на каждую допустимую модель, например ArticleRelatedTag и ArticleRelatedPerson; в каждой есть внешний ключ на свою модель, который удаляет связь вместе с целью. Строки всех этих таблиц делят один порядок списка через position, поэтому список может свободно смешивать модели. Смешанное поле «к одному» (manyToOne, oneToOne) хранится так же, а не в столбце. Ссылка на родителя (иерархия) допускает только одну модель.

  • Ссылка — это {"model":"<model slug>","id":"<row id>"}. В set, connect, disconnect и relations атомарного создания голый идентификатор отклоняется с 422 relation_target_model_required, если поле допускает несколько моделей. Если указана модель, которую поле не допускает, возвращается 422 relation_target_model_not_allowed, а для идентификатора, которому не соответствует строка указанной модели, возвращается 422 relation_target_missing. Поле с одной допустимой моделью по-прежнему принимает голые идентификаторы, а также { model, id } с собственной моделью.

  • Ответ перечисляет ссылки { "model", "id" } по порядку. В targets ошибки ссылка такого поля записывается как model:id.

  • Ко всему смешанному списку применяются те же правила, что и к любому списку: без повторов, minItems и maxItems, цель one-to-many у одного владельца (409 relation_target_taken), одна запись одним пакетом. Поле «к одному» хранит одну ссылку на все свои модели, в том числе при параллельной записи: запись, прочитавшая более старый список, отвечает 409 relation_changed_concurrently.

  • GET /api/<model>/files/<field>/<row id> отдаёт приватный файл, когда <field>_url строки содержит метку private-file:, из привязки R2 PRIVATE_FILES в Worker; Node-сервер (npm run serve:local) читает его из абсолютного существующего каталога, названного в PRIVATE_FILES_DIRECTORY, где тип содержимого файла хранится в <key>.meta.json рядом с ним. Он наследует <Model>.findUnique: режим доступа, разрешение, хуки и доступность, а также собственное защищённое чтение строки вызывающим. Отвечает 404 для строки без метки, для объекта, которого нет в бакете, и для строки, которую вызывающий не может читать. Без привязки и без каталога отвечает 503 private_files_unavailable и никогда не обращается к публичному хранилищу. Ответ — private, no-store; картинки и PDF показываются на месте, всё остальное скачивается.

  • Медиа из форматированного текста выдаются тем же защищённым маршрутом с ?media=<media id>. Он находит метку private-file: в текстовом поле самой записи, а не принимает ключ хранилища от вызывающего. &variant=thumb и &variant=preview выбирают приватные превью WebP этого файла; при отсутствии превью отдаётся оригинал. Экспортируемая функция privateRichText(html, endpoint, rowId) подставляет эти адреса в канонический HTML перед отображением. Сам маршрут файла при каждом запросе заново проверяет право чтения записи.

  • GET /api/<owner>/relations/<field>?id=<owner id> читает упорядоченный список с моделью каждой ссылки (добавьте &locale= для списка со своими связями для каждого языка). Он наследует <Owner>.findUnique, а приватное поле отдаётся только редакторам. Чтение одной join-таблицы, например GET /api/articlerelatedtag, возвращает только строки этой модели; position — место во всём списке.

  • Опубликованный элемент со ссылкой на модель, которую поле не допускает, блокирует генерацию (invalid_reference), как и модель, которой нет на сайте (relation_target_absent).

Создание строки со списками одной записью

Заголовок раздела «Создание строки со списками одной записью»

У владельца хотя бы с одним списком связей есть также POST /api/<owner>/with-relations. Он создаёт строку и её списки связей вместе. Обычный POST /api/<owner> по-прежнему создаёт строку без списков, а PUT /api/<owner>/relations/<field> по-прежнему позже меняет список.

POST /api/article/with-relations {"data":{"id":"post-1","title":"Hello","author_id":"person-1"},"relations":{"tags":["tag-2","tag-1"],"topics":["topic-1","topic-2"]}}
POST /api/note/with-relations {"data":{"id":"note-1"},"relations":{"related":{"en":["tag-1"],"fr":["tag-2","tag-3"]}}}
  • data — тело обычного создания; его проверяют та же форма guard и те же хуки создания, что и у POST /api/<owner>. Отсутствующий id генерируется. relations называет поля-списки: общий список — массив идентификаторов по порядку, список со своими связями для каждого языка — объект «язык → массив». Всё остальное в теле возвращает 400 invalid_request.
  • Проверяется каждое поле-список владельца, а пропущенное поле считается пустым. Обязательный список (minItems) нужно передать, иначе ответ 422 relation_too_few. Для списка со связями для каждого языка проверяются только названные языки.
  • До записи маршрут проверяет правила CMS: без повторов (relation_duplicate_target), minItems и maxItems (relation_too_few, relation_too_many), существующие цели (relation_target_missing), цель one-to-many у другого владельца (409 relation_target_taken), ссылку на родителя, называющую создаваемую строку (relation_hierarchy_cycle), идентификатор уже существующей строки (409 record_exists), и идентификаторы связей «к одному» в data (relation_target_missing).
  • Строка и все её связи записываются одним пакетом операций БД в D1 и одной транзакцией в SQLite. Если хотя бы один SQL-оператор завершается ошибкой, изменения не сохраняются: нет строки без связей и связи без строки. Два запроса на создание, одновременно претендующие на одну цель one-to-many, не могут оба завершиться успешно. Один получает ответ 201, остальные — 409 relation_target_taken; отклонённые запросы ничего не сохраняют. Если при записи возникает конфликт, который предварительные проверки не могли обнаружить, например другой запрос успел занять уникальное значение, возвращается 409 write_conflict.
  • При успехе возвращаются статус 201 и { "id", <columns>, <field>: [ids] }. Приватные поля опущены, как в любом ответе записи.
  • Маршрут наследует <Owner>.create: режим доступа, разрешение, хуки операции и групп. Пользователю без права создания он возвращает 403 forbidden. Отключение <Owner>.create убирает маршрут. Если у create владельца есть переопределение операции, маршрут отвечает 501 atomic_create_unavailable, потому что переопределение заменяет запись, которую выполняет этот маршрут. Без средства выполнения пакетов операций он отвечает 503 relation_writes_unavailable. OpenAPI помечает маршрут x-atomic-create.
  • Он не записывает строки переводов. Создавайте их через POST /api/<model>translation.

Развёртывание запускает scripts/check-relation-ids.mjs до применения миграций. База, мигрированная более ранним генератором, может хранить идентификаторы связей без строки, и миграция, добавляющая их внешние ключи, целиком завершилась бы ошибкой. Проверка перечисляет каждый (модель, столбец, id строки, отсутствующая цель) со способом исправления и завершается отказом с relation_ids_dangling до любой миграции. npm run db:init и npm run data:load -- --database <file> запускают ту же проверку с той же базой до локальных миграций. Локальная генерация автономного бэкенда в существующую целевую базу отклоняется по той же причине.

Проверка готовности отклоняет определения связей, требованиям которых не сможет удовлетворить ни одна сгенерированная операция записи: недостижимые лимиты (relation_bounds_unsatisfiable), целевую модель, которой нет на сайте (relation_target_absent), и обязательные связи к одному, ведущие обратно к своей модели (relation_required_cycle). Опубликованные элементы, чьи ссылки на родителя уже образуют цикл, отклоняются с relation_hierarchy_cycle с перечнем элементов. Конструктор контента показывает ту же заметку у поля связи.

Сгенерированные текстовые поля применяют заданные правила minLength, maxLength и pattern. Шаблоны поддерживают свойства Unicode, например \p{L} и \p{N}; намеренное ограничение символами ASCII тоже сохраняется. После изменения правил заново сгенерируйте и разверните бэкенд. Пользовательская проверка текста в виде исполняемого кода останавливает генерацию с unsupported_text_validation: код не выполняется, а правило не отбрасывается. Некорректные правила, в том числе некомпилируемый шаблон или минимум выше максимума, останавливают генерацию с invalid_field_validation.

Сгенерированные маршруты списков принимают filter[location][within_radius]=13.75,100.5,1000 (широта, долгота, метры) и filter[location][within_bbox]=13,100,14,101 (юг, запад, север, восток). Границы включены; запад больше востока означает переход через антимеридиан. Порядок по расстоянию: sort=location&origin=13.75,100.5; sort=-location меняет направление. Пустые координаты идут последними; равные расстояния упорядочены по ID строки. Радиус использует формулу гаверсинусов, расстояние — в метрах.

Параметры проходят через существующий маршрут, аутентификацию, ограничения строк и действующие разрешения полей. Отключённое поле возвращает 403 spatial_field_denied. Эти параметры не включают поле или операцию, удалённые пользовательской настройкой или сохранённым запросом. sort несовместим с orderBy; неверные координаты или синтаксис возвращают 400 spatial_query_invalid.

Локальный SQLite и D1 используют адаптер драйвера. Без адаптера пространственный запрос возвращает 501 spatial_adapter_required. Генерируется также SQL для PostgreSQL и MySQL; перед развёртыванием этих провайдеров нужна отдельная проверка их среды выполнения. Фильтрация и сортировка выполняются до SQL-пагинации; подсчёт использует те же условия.

Общие ссылки требуют действующих разрешений исходного сайта. Генерация с такими ссылками возвращает shared_references_require_live_delivery; удалите или замените их перед созданием автономного бэкенда. Обычные связи внутри сайта поддерживаются.

<field>_id сохраняет идентификатор медиа; <field>_url — перенесённый адрес оригинала по умолчанию. Поля с медиа также содержат JSON <field>_originals: язык выдачи → ID медиа → свойства и перенесённый URL точного сохранённого оригинала. Общие компоненты и форматированный текст хранят одно значение и оригиналы для каждого языка выдачи. Для приватных полей и компонентов с приватными вложенными полями эти метаданные также приватны.

Импортируйте mediaOriginal из src/media-original.ts. mediaOriginal(row, 'cover', 'fr') читает оригинал для cover_id; четвёртый аргумент задаёт ID медиа в компоненте или форматированном тексте. Если языка или файла нет, результат — null. Генерация использует сохранённый результат языкового fallback опубликованной версии, а не новый файл библиотеки. Exit переносит каждый сохранённый объект отдельно и меняет адрес, сохраняя идентификатор медиа.

Аутентификация, проверка запросов и ошибки

Заголовок раздела «Аутентификация, проверка запросов и ошибки»

Политика определяет анонимный, сессионный или сервисный доступ. Для модели без настройки действует service. Токены чтения хранятся в виде хешей с ограничением по моделям в ACCESS_CONFIG. Маршруты без модели и /api/openapi.json требуют wildcard scope. Сервисная операция записи без настроенной аутентификации для записи возвращает 503 writes_unavailable, отсутствие учётных данных — 401, отказ полномочий — 403. Необязательный Better Auth предоставляет сессии локальному runtime и Worker. Локальный runtime и Worker принимают один и тот же сервисный bearer API_BEARER_TOKEN.

Управляемое развёртывание при каждом деплое собирает ACCESS_CONFIG из двух источников: токенов, выданных CMS, в сгенерированном файле access/read-tokens.json и вашего собственного vars.ACCESS_CONFIG в wrangler.jsonc. Развёртывается их объединение. Если нет ни того ни другого — корректный пустой список (tokens: []). Оба источника проверяются одним модулем правил src/access-config-rules.js, который также используется при запуске Worker. Он требует версию 1, совпадение сайта в конфигурации и каждом токене, шестнадцатеричные хеши SHA-256 в нижнем регистре и ограничения доступа по моделям. Право записи, повторяющиеся ID токенов и повторяющиеся хеши недопустимы. Неверное собственное значение отклоняется с owned_access_config_invalid, неверный сгенерированный файл — с generated_access_tokens_invalid, одинаковый id в обоих — с access_token_id_conflict, одинаковый хеш в обоих — с access_config_union_invalid, и всё это до изменения базы данных или Worker. Ваши собственные записи никогда не удаляются. Значение, заданное только в панели Cloudflare, следующий деплой заменит, поэтому держите свои записи в wrangler.jsonc. Коммитьте только хеши длинных случайных секретов, никогда сами секреты. При запуске вне сгенерированного процесса развёртывания отсутствующий или неверный ACCESS_CONFIG останавливает запуск.

Настройки → Код и публикация → Токены чтения развёрнутого API, с разрешением site:infra.

  • Выдача. Укажите имя и одну или несколько моделей. Секрет — dw_art_ и 43 символа base64url — показывается один раз и нигде не хранится. CMS хранит его SHA-256-хеш. Вызывающие стороны отправляют Authorization: Bearer <secret>. CMS никогда не выпускает токен *. Wildcard-токен остаётся в вашем собственном vars.ACCESS_CONFIG.
  • Доставка. Ничего не попадает в развёрнутый бэкенд, пока вы не перегенерируете и не развернёте его. Выпуск или отзыв помечает сайт устаревшим. Перегенерация записывает активные токены, только хеши, в access/read-tokens.json, сопоставляя каждую область со сгенерированным именем модели (и её таблицы переводов).
  • Отзыв. Отзыв не запускает деплой. Развёрнутый бэкенд принимает отозванный токен до следующей перегенерации и деплоя. После отключения CMS больше не может обновлять эти данные в бэкенде.
  • Ротация. Выдайте замену, разверните, переведите вызывающую сторону на новый секрет, отзовите старый токен, разверните снова. Замены на месте нет.
  • Модели. Области — это id моделей, поэтому переименование модели ничего не меняет. Удаление модели или отмена импорта, который её создал, убирает её из каждого токена. Токен, у которого не осталось моделей, отзывается и никогда не расширяется.
  • API-токены сайта CMS (site_api_token) никогда не копируются в сгенерированный бэкенд.
  • Генерация отказывает для токена, чья область больше не соответствует сгенерированной модели (token_scope_not_emitted), а генерация одной модели отказывает, пока существуют активные токены (read_tokens_need_site_graduation).

Правила проверки запросов чтения разрешают сравнение на равенство по ID, а правила редактора — по сгенерированным столбцам. Приватные поля модели исключаются из выборок чтения (анонимное чтение и токены чтения) и из каждого ответа на запись; столбец компонента, в котором где-либо ниже есть приватное поле, исключается целиком, а связующая таблица приватной списковой связи отвечает только редакторам. Таблица модели с одной записью хранит одну строку: уникальный индекс отклоняет вторую, а маршрут создания отвечает 409 single_entry_exists. Обычные списки сортируются по id, по умолчанию 25 строк, максимум 100. Переводы сортируются по языку и id. Связующие таблицы — по позиции и id, по умолчанию 100, максимум 200. Неподдерживаемые селекторы/операторы отвергаются. D1 изолирована по сайту. Артефакт отказывает при несовпадении SITE_ID, не обещая изоляцию арендаторов на уровне строк.

Поле компонента переносится как одно встроенное JSON-значение с вложенностью до 3 уровней ниже корневого компонента, тот же предел действует в CMS при построении определения. Более глубокая цепочка отклоняется до генерации с ошибкой component_nesting_too_deep, где указаны компонент и путь компонентов к нему. Ссылки на медиа во вложенных компонентах находятся на любом уровне.

Настройка не может убрать аутентификацию, разрешения, выбор варианта, guards или защиту от раскрытия данных в ошибках. Сгенерированная цепочка обработчиков определяет правила проверки до вызова hooks, проверяет итоговую структуру запросов и запрещает обход через переменные окружения независимо от настроек в зоне owned. Остальные поддерживаемые настройки защиты установки остаются входами генерации. allowE2EGuardBypass всё ещё устарел upstream, но здесь не является обходом для override.

Ошибки маршрутов нормализуются по правилам исходного генератора. Тексты с SQL, вызовами Prisma или локальными путями заменяются на request_refused с сохранением статуса. Необработанные ошибки собственного кода возвращают request_failed. Настроенный коммит передаётся в x-artifact-commit.

Настройки → Код и публикация → Защита сгенерированного API остаётся входом политики генератора на уровне установки. При подключении собственных расширений следующие настройки защиты фиксируются. Объявления в зоне owned не могут их ослабить. Существующие варианты reader/editor остаются единственным механизмом выбора варианта.

Настройка Значение в сгенерированной цепочке расширений
requireGuardShape true
validateGuardShapes true
requireDefaultVariantOptIn Сохраняет выбранную политику установки
enableUpdateEach false
guardResolutionOrder before-hooks
allowE2EGuardBypass false
validateResolvedShapes true

Аутентифицированный GET /api/openapi.json объединяет upstream-схемы включённых генерируемых операций с декларативными маршрутами, проверяемыми по схемам. Override с тем же контрактом сохраняет схему запроса/результата. Пользовательские маршруты объявляют операцию доступа и необязательную модель. Поддерживаемое подмножество схем проверяется на входе и выходе. У raw Hono-маршрутов нет генерируемого контракта OpenAPI. Наличие документации не открывает анонимный доступ.

Необязательная аутентификация и доступ артефакта

Заголовок раздела «Необязательная аутентификация и доступ артефакта»

В разделе Настройки → Код и публикация → Аутентификация, кэш и доступ standalone включите Better Auth один раз для бэкенда и выберите Пароль, Ссылка для входа (Resend), Google или GitHub. По умолчанию он выключен: зависимость, runtime, маршруты, таблицы, миграция, настройки провайдеров и секреты Better Auth не создаются. Аккаунты CMS, сессии, приглашения, хеши паролей и ключи провайдеров не переносятся. Прикладная модель User отделена от таблиц идентификации артефакта.

Режим доступа Кто может отправить запрос
public Явный типизированный анонимный principal без учётных данных.
session Действующая сессия Better Auth этого артефакта.
service API_BEARER_TOKEN или scoped-токен, разрешающий чтение.
session_or_service Сессия артефакта либо сервисная аутентификация.

Режим доступа модели и overrides операций задаются на странице модели → Отдельный API. Для сохранения нужен site:infra. Политика хранится по id модели, поэтому переживает переименование slug и удаляется вместе с моделью. При сохранении указывается ревизия, которую нужно заменить. Устаревший экран получает 409 policy_changed_concurrently. Slug, дающие одно имя модели (blog-post и blog_post), отклоняются при создании или переименовании модели. Для модели без настройки явно действует service. Операция без override наследует режим модели. Используются точные ID: User.findMany, User.findManyPaginated, User.findUnique, User.create, User.update, User.delete. Удалённые операции вызывают отказ валидации. Сгенерированные типы тоже отклоняют устаревшие ID. Доступность операций остаётся в owned-реестре: политика доступа не включает выключенную операцию. Политики требуют сессию независимо от способа входа.

Аутентификация и проверка полномочий выполняются до hooks и overrides. Guards, существующие границы владения и защита от раскрытия данных в ошибках обязательны. Публичная операция создания отклоняет системные поля, владельца, арендатора, роли, администратора, пароль, верификацию и связи провайдера, включая вложенные значения. Она создаёт прикладные данные. Регистрация идёт только через маршруты Better Auth. Raw-маршруты используют service. Декларативные маршруты обязаны указать access.mode и access.operation. Модель необязательна. OpenAPI показывает режим каждой операции.

Необязательный корень установки Пакет Блок Точная версия
root, только при включении better-auth dependencies 1.6.25

При включении создаются src/better-auth.ts, проверка провайдеров, существующие правила ограничений/блокировки входа и отдельная миграция инфраструктурных таблиц _artifact_auth_*. У них нет CRUD-маршрутов. Node/SQLite и Worker/D1 используют одинаковые методы, ограничения пароля, проверенные учётные записи OAuth, хешированные токены, отзыв сессий через БД, семидневный срок сессии и проверку origin для запросов, изменяющих данные. Access/refresh/ID-токены провайдеров удаляются. Ответы не раскрывают внутренние ошибки провайдера.

npm run auth:init создаёт новый случайный секрет в игнорируемом .dev.vars, сохраняя уже существующий секрет артефакта. Node загружает файл через node --env-file=.dev.vars src/serve.local.mjs, Wrangler — автоматически. Сначала выполните db:init. ARTIFACT_ORIGIN — origin API, например локальный http://127.0.0.1:8787. Удалённый origin требует HTTPS. Развёртывание берёт его из проверенного целевого hostname, а не из CMS.

Выбранный метод Обязательные runtime-значения Целевой callback
Пароль BETTER_AUTH_SECRET Маршруты пароля под /api/auth
Magic link BETTER_AUTH_SECRET, RESEND_API_KEY, MAIL_FROM <ARTIFACT_ORIGIN>/api/auth/magic-link/verify
Google BETTER_AUTH_SECRET, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET <ARTIFACT_ORIGIN>/api/auth/callback/google
GitHub BETTER_AUTH_SECRET, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET <ARTIFACT_ORIGIN>/api/auth/callback/github

Зарегистрируйте callbacks в OAuth-приложениях владельца артефакта. Resend требует подтверждённый домен отправителя. Ключи нужны только выбранным методам. Их отсутствие останавливает preflight до развёртывания. Имена Actions secrets получают префикс ARTIFACT_, обходя зарезервированный GitHub префикс GITHUB_. При публикации система создаёт новый секрет артефакта и шифрует его целевым публичным ключом. Секреты, уже существующие под целевыми именами, сохраняются без ротации. npm run backend:deliver читает ключи провайдеров только из переменных окружения ARTIFACT_*. Значения секретов не попадают в исходники, манифесты, входные данные генерации, дайджесты, отслеживаемые переменные, логи и журнал развёртывания.

Регенерация сохраняет owned/shared-файлы. Для включения auth в существующем shared-пакете и смены методов нужна повторная генерация, обновляющая зависимости и bindings вызывающего workflow. Выключить аутентификацию в существующем целевом каталоге нельзя: используйте новый каталог, сохранив учётные записи и историю миграций прежнего. Изменения доступа учитываются в дайджестах исходных данных генерации и манифесте.

verify:deployment учитывает политику: публичное чтение проверяется анонимно, сервисное — bearer-токеном, session-only требует действующую целевую сессию в ARTIFACT_VERIFY_SESSION_COOKIE. Без неё проверка возвращает verify_session_required и не объявляет непроведённую проверку успешной. При первом развёртывании инициализируйте целевой бэкенд, зарегистрируйтесь через его auth-маршруты и повторите проверку с сессией. Не сохраняйте краткоживущую сессию в исходниках или материалах проверки. Проверка в браузере и приёмка рабочего развёртывания остаются отдельными этапами.

Администрирование учётных записей приложения

Заголовок раздела «Администрирование учётных записей приложения»

По умолчанию выключено. Когда Better Auth включён, артефакт может также открыть плагин admin из Better Auth для собственных учётных записей приложения. Учётные записи, роли и сессии остаются в базе приложения, а полномочия принадлежат администратору приложения. CMS Dee Wan не хранит копию учётных записей, не участвует в запросе и не обязана работать. При выключенной интеграции не создаётся ни плагин, ни маршрут, ни миграция, ни столбец, ни binding.

Включение. Добавьте "accountAdmin": true в настройку аутентификации сайта, затем перегенерируйте и разверните: PATCH /api/sites/<site-id>/standalone-auth с {"authentication":{"enabled":true,"methods":["password"],"accountAdmin":true}} и разрешением site:infra. Сохранение без accountAdmin оставляет текущее значение. В админке это переключатель «Управление аккаунтами в сгенерированном приложении» в разделе Настройки → Код и публикация → Аутентификация, кэш и доступ standalone; пока Better Auth выключен, он недоступен. Настройка добавляет миграцию migrations/<номер>_artifact_auth_admin.sql: она добавляет столбцы role, banned, banReason и banExpires в _artifact_auth_user и impersonatedBy в _artifact_auth_session. Номер — следующий свободный в цепочке миграций артефакта на момент первого включения (0004 для нового артефакта). Перегенерация сохраняет этот файл и номер, применённая миграция никогда не перенумеровывается. Если позже выключить настройку, маршруты и страница исчезнут, а столбцы и файл миграции останутся; повторное включение использует тот же файл.

Кто администратор. Пользователь с role, равной admin, или с идентификатором в необязательном runtime-binding ARTIFACT_ADMIN_USER_IDS (через запятую, не более 50 идентификаторов). Новая учётная запись получает настроенную роль по умолчанию, если администратор не выбрал другую. list-users и get-user добавляют к каждому аккаунту access (admin или user) и adminSource (role, configuration, both или null); значения вычисляются при каждом запросе из сохранённой роли и текущего binding. Binding никогда не меняет сохранённую роль; страница аккаунтов показывает и то, и другое. Чтобы создать первого администратора, зарегистрируйтесь через /api/auth/sign-up/email, прочитайте свой идентификатор в GET /api/auth/get-session, задайте binding как секрет Worker целевой среды (или в окружении Node-сервера) и перезапустите. Следующих администраторов назначают через set-role. База данных запрещает удаление или блокировку последнего активного администратора и снятие с него прав. Binding остаётся отдельным способом восстановления доступа. Сгенерированный workflow развёртывания этим binding не управляет.

Все вызовы используют cookie сессии самого администратора, а изменения должны приходить с ARTIFACT_ORIGIN, как любой другой вызов auth. Пути лежат под /api/auth/admin/.

Операция Метод и путь Тело или запрос Результат
Список учётных записей GET /api/auth/admin/list-users limit (не более 100, по умолчанию 100), offset, searchValue, searchField, sortBy { users, total }
Создать учётную запись POST /api/auth/admin/create-user { name, email, role?, password? } Better Auth
Удалить учётную запись POST /api/auth/admin/remove-user { userId } Better Auth
Одна учётная запись GET /api/auth/admin/get-user id Учётная запись
Изменить профиль POST /api/auth/admin/update-user { userId, data }, в data только name, image или email Обновлённая учётная запись
Задать доступ приложения POST /api/auth/admin/set-role { userId, role }, role — настроенная роль приложения (по умолчанию admin или user) Обновлённая учётная запись
Заблокировать POST /api/auth/admin/ban-user { userId, banReason?, banExpiresIn? } (секунды) Помечает учётную запись заблокированной и завершает все её сессии
Разблокировать POST /api/auth/admin/unban-user { userId } Снимает блокировку
Сессии учётной записи POST /api/auth/admin/list-user-sessions { userId } { sessions }
Отозвать одну сессию POST /api/auth/admin/revoke-user-session { sessionToken } { success: true }
Отозвать все сессии POST /api/auth/admin/revoke-user-sessions { userId } { success: true }

Заблокированная учётная запись не может войти, пока её не разблокируют или не истечёт banExpiresIn; вход отвечает 403 authentication_failed.

Отказы. Каждый отказ отвечает { "error": "account_admin_refused", "code": "<code>" } с указанным статусом.

Статус code Когда
401 нет кода или код Better Auth Нет действующей сессии
403 YOU_ARE_NOT_ALLOWED_TO_… Вошедший пользователь не администратор; либо чужой origin
404 USER_NOT_FOUND Нет такой учётной записи
404 ACCOUNT_ADMIN_OPERATION_UNSUPPORTED Любой другой путь или метод под /api/auth/admin/: установка пароля, имитация, остановка имитации, проверка разрешения
400 ACCOUNT_ADMIN_PROFILE_FIELD_UNSUPPORTED update-user с полем кроме name, image, email или без полей
400 YOU_CANNOT_BAN_YOURSELF Администратор блокирует собственную учётную запись
400 YOU_ARE_NOT_ALLOWED_TO_SET_NON_EXISTENT_VALUE set-role с ролью кроме admin или user

Роль администратора предоставляет только разрешения, которые нужны этим операциям, поэтому операция вне таблицы недоступна, даже если запрос дошёл до плагина. Данные аккаунтов изменяет только Better Auth. Защитные триггеры в базе проверяют удаление; общего CRUD над таблицами аккаунтов нет.

Страница аккаунтов. Артефакт отдаёт страницу для этих операций по адресу /admin/accounts на собственном origin. Она использует собственную сессию администратора приложения в браузере, с cookie самой страницы, и вызывает только операции из таблицы, а также get-session, sign-in/email и sign-out самого артефакта. Без сессии страница показывает форму входа, если выбран метод password; при другом методе сначала войдите через само приложение. Вошедший пользователь, не являющийся администратором, видит отказ. У аккаунта с правами администратора страница показывает сохранённую роль и отдельно «Administrator (role)», «Administrator (configured)» или «Administrator (role and configured)». Администратор может искать аккаунты по email, править профиль, задавать роль, блокировать и разблокировать аккаунт, смотреть и отзывать его сессии. Страница не хранит данных аккаунтов и не содержит адреса CMS, поэтому работает при выключенной CMS. При выключенной интеграции маршрута нет. В админке Dee Wan, когда переключатель сохранён во включённом положении и развёртывание записало hostname (Настройки → Код и публикация → Дополнительно — вебхук сборки и развёртывание → Standalone-бэкенд), ссылка «Открыть управление аккаунтами» под переключателем открывает https://<hostname>/admin/accounts в новой вкладке; пока hostname не записан, админка показывает «Разверните приложение, чтобы записать его адрес.» CMS никогда не проксирует cookie приложения: страница, отданная CMS, не может вызвать эти операции, потому что cookie сессии артефакта принадлежит origin артефакта, а его список доверенных origin содержит только этот origin. Подойдут также curl или собственный клиент на origin артефакта.

В Настройки → Код и публикация включите вход в приложение и администрирование его учётных записей. В разделе Роли приложения добавьте роли, например customer или staff, выберите роль при регистрации и разрешённые действия API для каждой модели. Сохраните настройки, затем пересоздайте и разверните приложение. На странице /admin/accounts администратор сможет назначать эти роли учётным записям.

Поддерживается до 32 ролей, включая admin и user. Имя начинается со строчной латинской буквы и содержит 1–64 строчные латинские буквы, цифры, дефисы или подчёркивания. Роль при регистрации должна существовать и не может быть admin. Роль администратора сохраняет все действия. Все действия — явное разрешение; иначе доступны только выбранные идентификаторы действий. Пустой список не разрешает ничего. Без этой необязательной настройки прежнее поведение admin/user сохраняется.

Конфигурация хранится в backend_policy.authentication.roles:

{"default":"customer","grants":{"admin":["*"],"user":[],"customer":["Article.findMany","Article.findUnique"]}}

Ограничения относятся к сеансам приложения на генерируемых маршрутах API. Публичные действия остаются публичными, а служебные учётные данные сохраняют свои разрешения. Доверенные запросы чтения и запись переводов наследуют указанное для них действие. Роли и учётные записи CMS независимы от ролей приложения. Новое назначение проверяется при следующих запросах, даже если CMS недоступна. Администратор, указанный в конфигурации для восстановления доступа, сохраняет доступ. Удалённая или неизвестная роль не разрешает действий приложения. Обычная перегенерация и публикация переносят политику без перезаписи вашего кода.

Создание и удаление. На странице /admin/accounts нажмите New account, укажите имя, адрес почты, начальную роль и пароль, если включён вход по паролю. Запись создаёт тот же сервис Better Auth: публичная регистрация и отправка писем не нужны. В карточке пользователя кнопка Delete account запрашивает подтверждение необратимого удаления и завершает все сессии. Ответ 409 ACCOUNT_ADMIN_LINKED_RECORDS перечисляет таблицы и поля со связанными записями: сначала переназначьте или удалите эти связи. Проверка в базе данных не допускает каскадного удаления данных приложения. Нельзя удалить себя (YOU_CANNOT_REMOVE_YOURSELF) или последнего активного администратора (ACCOUNT_ADMIN_LAST_ADMINISTRATOR).

data/published-snapshot.json генерируется и проверяется по хешу. db:init применяет локальные миграции перед загрузкой, data:load загружает данные в указанную базу SQLite или D1. Повторная загрузка того же снимка ничего не меняет. База со снимком другого сайта или артефакта, изменённые байты снимка и неполная загрузка вызывают отказ. Снимок с другим контентом заменяет каждую таблицу, которой владеет: загрузка удаляет строки таблицы и записывает строки снимка. Строка, которой нет в снимке, удаляется независимо от того, кто её создал; это не приводит к отказу при загрузке. Таблицы, которые генератор не создавал, не затрагиваются. Храните строки приложения в его собственных таблицах. Отказ observed_row_set_mismatch относится к локальной генерации в её собственную одноразовую базу --db, а не к data:load.

Окно терминала
npm run db:init -- --database ./standalone.db

Регенерация проверяет владение исходниками, создаёт схему/приложение/защиту/сайт, регенерирует Prisma, guards и маршруты, удаляет пути машины генерации и проверяет настройку. Миграции дополняют историю. Разрушительные или несовместимые изменения формы требуют отдельного явного рассмотрения. Итоговый манифест владения должен описывать завершённое дерево.

npm run regenerate запускает scripts/regenerate.mjs, затем prisma:generate, portable и customization:check. Для артефакта всего сайта нужны два входных значения:

  • definitions.json в корне репозитория: определения сайта в том виде, в каком их отдаёт CMS. Git игнорирует этот файл, а управляемая сборка удаляет его перед коммитом. Без него регенерация отказывает с definitions_missing.
  • GENERATION_SOURCE_VERSION: версия, которую описывают эти определения. Без неё регенерация отказывает с generation_source_absent. Значение, которое отличается от data.generationSourceVersion в файле, вызывает отказ generation_source_version_mismatch.

С подключением. CMS отдаёт определения только build-токену. Войдите в админку с правом site:deploy и вызовите POST /api/sites/<site-id>/build/token. Ответ содержит build_token и generation_source_version. Токен живёт недолго и читает определения только этой версии. DEE_WAN_URL — тот же адрес CMS, который использует управляемый workflow. Если сайт изменится после выпуска токена, запрос определений ответит 409 source_moved.

Окно терминала
curl -sS -o definitions.json -H "Authorization: Bearer <build_token>" "$DEE_WAN_URL/api/sites/<site-id>/definitions"
export GENERATION_SOURCE_VERSION=<generation_source_version>
node scripts/install-dependencies.mjs
npm run regenerate
npm run migrate:emit
npm run typecheck
npm run build
npm run files:manifest
rm definitions.json

После отключения. Без CMS регенерация работает только из definitions.json, который вы получили раньше, например шагами выше перед выводом CMS из эксплуатации. Храните эту копию вне репозитория: это вся модель контента сайта. Регенерация из неё пересобирает генерируемые файлы для той же версии. Более нового контента она не приносит.

Окно терминала
cp <kept-definitions.json> definitions.json
export GENERATION_SOURCE_VERSION="$(node -p "require('./definitions.json').data.generationSourceVersion")"
node scripts/install-dependencies.mjs
npm run regenerate
npm run migrate:emit
npm run typecheck
npm run build
npm run files:manifest
rm definitions.json

Регенерация отказывает, если генерируемый файл изменён. Если после отключения вы правите генерируемые файлы, больше не выполняйте регенерацию.

Артефакту --model вместо этого нужны MODEL_VERSION_ID и определения этой модели. Без переменной он отказывает с model_version_absent. Маршрут CMS /definitions отдаёт только определения сайта. Обработчик манифеста одной модели читает эти данные, сохраняет и повторно проверяет настройки защиты запросов, выбранные для данного артефакта.

deploy-from-repository.yml выбирает точный коммит, устанавливает проверенные зависимости, проверяет/собирает, определяет D1, применяет migrations/ через wrangler d1 migrations apply, развёртывает API, устанавливает секрет записи, загружает закоммиченный снимок и сбрасывает кеш ответов, проверяет контент/медиа и развёртывает сайт. verify:deployment сравнивает строки и коммит, verify:media проверяет, что ссылки на медиа в сохранённых проекциях больше не ведут на хранилище CMS. Более ранний git_sha выбирает старую ревизию кода. Это не автоматическое восстановление базы или медиа: до отката сохраните совместимую историю миграций, резервные копии и объекты владельца.

Кеш ответов и его инвалидация описаны в разделе Кеш ответов.

D1 не поддерживает интерактивные транзакции: строки и total пагинации читаются отдельно и могут различаться при конкурентной записи. Raw Hono-обработчики должны явно применять guard-формы к Prisma-запросам. Расширенный клиент сам по себе не ограничивает все запросы, raw SQL не защищён. Локальная приёмка Worker и приёмка развёртывания — разные проверки. Одни исходники и написанные проверки не доказывают прохождение ни одной из них.

Файлы, зависимости и соответствие контента таблицам, на которых построен этот API, описаны в деталях реализации.


Стабильная версия ещё не выпущена (pre-MVP)
Коммит исходного кода: fe23db481c422b40e317b92fc98116b4c0c26a87 (с локальными изменениями)
Дата сборки (UTC): 2026-10-18T09:22:34.630Z
Коммит для обновления: не определён: в рабочей копии есть изменения. Примечания к выпуску