Автономный бэкенд: справочник 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=25GET /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 }.
Запросы к API развёрнутого приложения
Заголовок раздела «Запросы к API развёрнутого приложения»Какой API использовать
Заголовок раздела «Какой API использовать»Для автономных бэкендов Dee Wan использует prisma-generator-express. Возможности генератора шире, чем набор маршрутов, включённых в проекте Dee Wan. Здесь три разных контракта:
| API | Язык запросов | Ответ |
|---|---|---|
Действующий API CMS /v1/<model> |
filter[...], tag, relation, fields, expand, sort, непрозрачные курсоры |
Опубликованный контент CMS в { contracts, data, page } |
Автономный /api/<model> |
Аргументы Prisma, сложные значения — JSON; действуют сгенерированные правила reader/editor | Строки Prisma; /paginated возвращает { data, total, hasMore } |
| Маршрут сохранённого запроса | Проверенные where, orderBy, take, skip; фиксированные поля ответа, метод и путь |
Результат привязанной операции Prisma |
В руководстве по публичному API есть полный пример запроса к CMS. Параметры Prisma where, include, distinct и groupBy не работают в /v1. Токен сайта CMS не служит учётными данными автономного API.
Стандартные сгенерированные маршруты
Заголовок раздела «Стандартные сгенерированные маршруты»Обычный читатель может отбирать основные строки по id.equals. Редактор — по равенству значений сгенерированных столбцов. Основные списки сортируются по id: по умолчанию 25 строк, максимум 100. Переводы сортируются по lang, затем id. Строки связующих таблиц — по position, затем id, по умолчанию 100, максимум 200. Для перехода вперёд используйте положительный take и неотрицательный skip.
Можно выбирать сгенерированные столбцы и, у переводимой модели, вложенные поля translations.select. Приватные столбцы недоступны читателям. Вложенные переводы возвращаются по 25, максимум 50; для большего числа используйте отдельный постраничный маршрут таблицы переводов. Поддержка связей в Prisma сама по себе не разрешает произвольное раскрытие остальных связей.
В примере предполагается модель Article с переводимыми title и body. Фактические имена смотрите в сгенерированной схеме Prisma и OpenAPI: ключ поля CMS и имя столбца могут различаться. Токен должен разрешать чтение Article согласно политике развёрнутого приложения.
const args = { where: { id: { equals: 'article-1' } }, select: { id: true, translations: { select: { id: true, lang: true, title: true, body: true }, take: 10, }, }, orderBy: { id: 'asc' }, take: 25, skip: 0,};const url = new URL('/api/article/paginated', apiOrigin);for (const [key, value] of Object.entries(args)) { url.searchParams.set(key, typeof value === 'object' ? JSON.stringify(value) : String(value));}const response = await fetch(url, { headers: { Authorization: `Bearer ${serviceToken}` },});if (!response.ok) throw new Error(`API refused the request: ${response.status}`);const { data, total, hasMore } = await response.json();curl --get --data-urlencode кодирует скобки, пробелы и кавычки:
curl --get 'https://api.example.com/api/article/paginated' \ -H "Authorization: Bearer $ARTIFACT_API_TOKEN" \ --data-urlencode 'where={"id":{"equals":"article-1"}}' \ --data-urlencode 'orderBy={"id":"asc"}' \ --data-urlencode 'take=25' \ --data-urlencode 'skip=0'Пока hasMore равен true, увеличивайте skip на запрошенный размер страницы. Фильтры и сортировку сохраняйте. /api/article возвращает массив без оболочки CMS; /unique — строку или null. total в постраничном ответе считает все подходящие строки, а не только текущую страницу. В D1 строки и общее количество читаются отдельно и могут разойтись при одновременной записи. Страницы со смещением не образуют неизменяемый снимок.
Правила reader/editor выбираются по проверенной личности вызывающего. Не передавайте собственный заголовок выбора варианта. Анонимный доступ, сессия приложения и сервисный токен всё равно требуют включённой операции и допустимой формы запроса. Подробнее — в разделе Аутентификация, проверка запросов и ошибки.
Более сложный сохранённый запрос
Заголовок раздела «Более сложный сохранённый запрос»В рабочей области запросов модели сохраните нужный приложению запрос, привяжите доверенные метод и путь, затем перегенерируйте и разверните проект. Сохранённый findMany может описывать поддерживаемые скалярные операторы, логические группы и фильтры по связям, вложенный выбор полей, сортировку и ограниченную пагинацию. Глубина связей — до 2, положительный take — до 100. Принятое значение take становится значением по умолчанию и максимумом.
Операторы зависят от типа поля: для текста — равенство, принадлежность списку, сравнения, contains, startsWith, endsWith; для чисел и дат — сравнения; для Boolean — равенство; для enum — принадлежность списку. Для связей доступны some/every/none и is/isNot, для логических групп — AND/OR/NOT. Фильтры скалярных списков требуют поддержки схемой и коннектором базы. JSON и бинарные поля не служат универсальными фильтрами. Неподдерживаемые или неоднозначные формы отклоняются: например, элементы массива с разной структурой не объединяются в более широкое разрешение. SQLite/D1 не принимают PostgreSQL-параметр Prisma mode: "insensitive".
Маршрут привязан к одному сохранённому запросу. Клиент меняет только разрешённую структуру фильтров, сортировку и пагинацию. Он не может запросить другой набор полей через select/include, добавить отсутствующий оператор, выбрать другой запрос по имени или превысить лимиты. findUnique получает объявленный селектор из пути. Значения в шаблоне — примеры, а не ограничения арендатора или владельца; последние задаются доверенным серверным контекстом. См. декларативные варианты запросов.
Например, сохраните Post.findMany с where: { title: { contains: "Fir" } }, orderBy: { title: "asc" }, select: { id: true, title: true }, take: 2 и привяжите GET /api/posts/recent. Клиент может заменить разрешённое значение фильтра и направление сортировки, уменьшить страницу и получить массив только с id и title:
curl --get 'https://api.example.com/api/posts/recent' \ -H "Authorization: Bearer $ARTIFACT_API_TOKEN" \ --data-urlencode 'where={"title":{"contains":"Sec"}}' \ --data-urlencode 'orderBy={"title":"desc"}' \ --data-urlencode 'take=1'take=3, клиентский select или новый непроверенный предикат не соответствуют контракту этого маршрута. Требование «только опубликованные» или «только свой арендатор» задавайте доверенным ограничением, а не значением в примере фильтра.
Полные возможности генератора для собственного приложения
Заголовок раздела «Полные возможности генератора для собственного приложения»Ниже описаны возможности библиотеки prisma-generator-express, а не стандартный каталог маршрутов Dee Wan. Они доступны, когда приложение явно настроило операцию, аутентификацию, разрешения и совместимую форму проверки. В связанном с CMS проекте используйте предусмотренные точки расширения; не редактируйте сгенерированные файлы и не отключайте обязательные проверки. Возможности базы и адаптера также ограничивают запрос. Сверяйтесь с руководством библиотеки и OpenAPI своего проекта.
| Аргумент или операция Prisma | Назначение и ограничения |
|---|---|
where |
AND, OR, NOT, скалярные предикаты, some, every, none для списков связей, is, isNot для одиночной связи. Каждый оператор и поле должны быть разрешены. |
select |
Только выбранные скалярные поля и вложенные связи. Включайте ID и внешние ключи, нужные клиенту для ссылок. |
include |
Добавляет связи к результату модели. select и include несовместимы на одном уровне; внутри связи можно выбрать свой набор полей. |
omit |
Исключает поля, если это поддерживают версия Prisma, коннектор и правила маршрута. Не обходит серверный набор разрешённых полей. |
_count связей |
Выбранные количества связанных записей через _count.select, если операция и правила разрешают. Это не отдельный корневой маршрут count. |
| Аргументы вложенной связи | Независимые фильтр, сортировка и пагинация списка. Вложенный where ограничивает дочерние записи; чтобы исключить родителей, нужен фильтр связи в корневом where. |
orderBy |
Объект сортировки или массив для нескольких ключей. Уникальный последний ключ обеспечивает воспроизводимый порядок страниц. Нужна поддержка поля или связи схемой. |
take, skip |
Размер результата и смещение с учётом лимитов маршрута и проверки. Отрицательный take означает обратную пагинацию только там, где она явно поддержана. |
cursor |
Уникальный селектор, часто со skip: 1, чтобы не повторять строку курсора. Это не непрозрачный курсор CMS /v1. |
distinct |
Уникальные значения указанных скалярных полей. Не превращает все остальные поля результата в SQL GROUP BY. |
count |
Количество подходящих строк либо настроенные счётчики полей. Не возвращает сами строки или группы. |
aggregate |
_count, _sum, _avg, _min, _max согласно типам полей модели. |
groupBy |
Поля by, агрегаты, необязательные having, сортировка и пагинация. where отбирает строки до группировки, having — группы после неё. Действуют правила Prisma. |
findFirst, findUnique, строгие варианты |
Первая подходящая строка или уникальный селектор. Строгий вариант завершает запрос ошибкой при отсутствии строки; обычный может вернуть null. |
mode: "insensitive", операторы скалярных списков и часть агрегатов зависят от коннектора. Нельзя перенести PostgreSQL-запрос в SQLite/D1 только потому, что оба используют Prisma. Стандартный проект Dee Wan не включает count, aggregate, groupBy, cursor, distinct и POST-варианты чтения.
Сложный вложенный запрос
Заголовок раздела «Сложный вложенный запрос»Это учебный пример отдельного PostgreSQL-приложения с моделями Article, Author, Category, Comment. У статьи есть одиночный author и списки categories, comments; у комментария — author. Проверенная форма маршрута должна явно разрешать этот запрос. Имена моделей иллюстративны и не описывают работающий сайт Dee Wan.
const query = { where: { AND: [ { published: { equals: true } }, { publishedAt: { gte: '2026-01-01T00:00:00.000Z' } }, { categories: { some: { slug: { equals: 'architecture' } } } }, { author: { is: { active: { equals: true } } } }, ], OR: [ { title: { contains: 'design', mode: 'insensitive' } }, { title: { contains: 'city', mode: 'insensitive' } }, ], }, select: { id: true, title: true, publishedAt: true, author: { select: { id: true, name: true } }, categories: { select: { id: true, slug: true }, orderBy: { slug: 'asc' }, take: 5 }, comments: { where: { approved: { equals: true } }, orderBy: [{ createdAt: 'desc' }, { id: 'desc' }], take: 3, select: { id: true, body: true, author: { select: { id: true, name: true } }, }, }, }, orderBy: [{ publishedAt: 'desc' }, { id: 'desc' }], take: 10, skip: 0,};Корневой фильтр требует категорию architecture и активного автора. Комментарии отдельно ограничены тремя последними одобренными; статья без таких комментариев всё равно может попасть в ответ. У каждой статьи будут вложенные автор, категории и комментарии с выбранными полями. Доверенные серверные правила публикации и изоляции арендаторов должны сохраняться, даже когда клиент меняет разрешённые значения фильтров.
Для настроенного маршрута библиотеки со стандартным префиксом модели кодируйте каждый сложный аргумент верхнего уровня в JSON:
const url = new URL('/article/', apiOrigin);for (const [key, value] of Object.entries(query)) { url.searchParams.set(key, JSON.stringify(value));}const response = await fetch(url, { headers: authorizationHeaders });Библиотека также поддерживает чтение с JSON-телом, если оно включено: POST /article/read для findMany, POST /article/paginated для постраничного чтения. Это позволяет обойти ограничение длины URL, но не меняет права, проверки или стоимость запроса. В стандартном проекте Dee Wan эти варианты намеренно отключены. Секрет токена нельзя помещать в URL.
Количество, агрегаты и группы
Заголовок раздела «Количество, агрегаты и группы»Это отдельные маршруты библиотеки, которые нужно явно настроить. Ниже — тела запросов для включённых POST-вариантов чтения и той же учебной схемы. В GET-вариантах передавайте аргументы как JSON в параметрах URL.
POST /article/count{"where":{"published":{"equals":true}}}
POST /article/aggregate{"where":{"published":{"equals":true}},"_count":{"_all":true},"_max":{"publishedAt":true}}
POST /article/groupby{"by":["authorId"],"where":{"published":{"equals":true}},"_count":{"_all":true},"orderBy":{"authorId":"asc"},"take":20}count возвращает результат подсчёта Prisma, aggregate — объект агрегатов, groupBy — массив групп. Оболочка CMS contracts/page не добавляется. Только findManyPaginated возвращает { data, total, hasMore }. Его hasMore предназначен для перехода вперёд со смещением и положительным take, а не для произвольной курсорной или обратной пагинации.
Для постраничного distinct библиотека может выполнить ограниченный поиск уникальных значений или перейти к обычному подсчёту строк. При чтении distinct через правила проверки также используется проверяемый подсчёт без distinct. Поэтому total может быть больше количества уникальных результатов. Проверьте distinctCountLimit и источник подсчёта маршрута, прежде чем считать любой итог точным. Настройка генератора findManyPaginatedMode выбирает параллельные отдельные чтения или интерактивную транзакцию при генерации; D1 не поддерживает второй путь.
Поэтапная выдача через SSE доступна только для Express в самой библиотеке. Это не режим потокового чтения автономного Hono/D1 API Dee Wan. Наличие возможности в README генератора не доказывает, что маршрут включён, развёрнут или прошёл проверку в вашем проекте.
Перед использованием в рабочем приложении
Заголовок раздела «Перед использованием в рабочем приложении»- Проверьте OpenAPI развёрнутого проекта, схему, включённые операции и действующие правила запросов.
- Запрашивайте только нужные поля; задайте отдельный предел для каждого вложенного списка связей.
- Используйте детерминированную сортировку; учитывайте весь запрос в продолжении страницы и ключах кеша.
- Сначала проверяйте HTTP-статус, затем разбирайте успешный ответ. Сохраняйте нормализованные ошибки сервера.
- Сервисные токены храните на сервере; в браузере используйте настроенную сессию приложения.
- После изменения запросов, схемы или правил доступа перегенерируйте и разверните проект. До замены развёрнутая версия сохраняет прежний контракт.
Запись связей и переводов
Заголовок раздела «Запись связей и переводов»Идентификатор связи к одному записывается в строке, как любой столбец. Если цель отсутствует, возвращается 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:, из привязки R2PRIVATE_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 строки. Радиус использует формулу гаверсинусов, расстояние — в метрах.
Для filter[location][within_polygon] передайте GeoJSON Polygon с URL-кодированием, например {"type":"Polygon","coordinates":[[[100,13],[101,13],[101,14],[100,14],[100,13]]]}. Координаты полигона идут в порядке долгота, широта. Внешняя граница включается, внутренние области-отверстия и их границы исключаются. Поддерживаются вогнутые полигоны. Ограничения: 16 замкнутых колец, суммарно 256 координатных пар с учётом замыкающих точек и 16 КиБ входных данных. Вырожденные и самопересекающиеся кольца, а также пересекающиеся или не лежащие строго внутри внешнего кольца отверстия отклоняются. Рёбра — прямые отрезки на плоскости долготы и широты, без автоматического перехода через 180-й меридиан. MultiPolygon не принимается. См. Пространственные запросы.
Параметры проходят через существующий маршрут, аутентификацию, ограничения строк и действующие разрешения полей. Отключённое поле возвращает 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 останавливает запуск.
Токены чтения, выданные в CMS
Заголовок раздела «Токены чтения, выданные в CMS»Настройки → Код и публикация → Параметры разработчика → Токены чтения развёрнутого 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.
Настройки проверки запросов (guard)
Заголовок раздела «Настройки проверки запросов (guard)»Настройки → Код и публикация → Параметры разработчика → Защита сгенерированного API остаётся входом политики генератора на уровне установки. При подключении собственных расширений следующие настройки защиты фиксируются. Объявления в зоне owned не могут их ослабить. Существующие варианты reader/editor остаются единственным механизмом выбора варианта.

| Настройка | Значение в сгенерированной цепочке расширений |
|---|---|
requireGuardShape |
true |
validateGuardShapes |
true |
requireDefaultVariantOptIn |
Сохраняет выбранную политику установки |
enableUpdateEach |
false |
guardResolutionOrder |
before-hooks |
allowE2EGuardBypass |
false |
validateResolvedShapes |
true |
OpenAPI
Заголовок раздела «OpenAPI»Аутентифицированный GET /api/openapi.json объединяет upstream-схемы включённых генерируемых операций с декларативными маршрутами, проверяемыми по схемам. Override с тем же контрактом сохраняет схему запроса/результата. Пользовательские маршруты объявляют операцию доступа и необязательную модель. Поддерживаемое подмножество схем проверяется на входе и выходе. У raw Hono-маршрутов нет генерируемого контракта OpenAPI. Наличие документации не открывает анонимный доступ.
Необязательная аутентификация и доступ артефакта
Заголовок раздела «Необязательная аутентификация и доступ артефакта»В разделе Настройки → Код и публикация → Параметры разработчика → Сгенерированный бэкенд включите Better Auth один раз для бэкенда и выберите Пароль, Ссылка для входа (Resend), Google или GitHub. По умолчанию он выключен: зависимость, runtime, маршруты, таблицы, миграция, настройки провайдеров и секреты Better Auth не создаются. Аккаунты CMS, сессии, приглашения, хеши паролей и ключи провайдеров не переносятся. Прикладная модель User отделена от таблиц идентификации артефакта.
| Режим доступа | Кто может отправить запрос |
|---|---|
public |
Явный типизированный анонимный principal без учётных данных. |
session |
Действующая сессия Better Auth этого артефакта. |
service |
API_BEARER_TOKEN, именной ключ интеграции с нужными разрешениями или токен чтения. |
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 |
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 оставляет текущее значение. В админке это переключатель «Управление аккаунтами в сгенерированном приложении» в разделе Настройки → Код и публикация → Параметры разработчика → Сгенерированный бэкенд; пока 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?, data? } |
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 с необъявленным полем или без полей |
| 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 (Настройки → Код и публикация → Параметры разработчика → Сгенерированный бэкенд), ссылка «Открыть управление аккаунтами» под переключателем открывает https://<hostname>/admin/accounts в новой вкладке; пока hostname не записан, админка показывает «Разверните приложение, чтобы записать его адрес.» CMS никогда не проксирует cookie приложения: страница, отданная CMS, не может вызвать эти операции, потому что cookie сессии артефакта принадлежит origin артефакта, а его список доверенных origin содержит только этот origin. Подойдут также curl или собственный клиент на origin артефакта.
Ключи интеграций
Заголовок раздела «Ключи интеграций»Включите управление учётными записями приложения и откройте /admin/accounts →
Machine credentials. Нажмите New credential, задайте имя интеграции, выберите конкретные
операции моделей и при необходимости срок действия. Случайный секрет с префиксом dwk_ показывается
один раз: сохраните его перед выходом. Передавайте Authorization: Bearer <секрет>.
Rotate сразу заменяет секрет выбранного ключа; Revoke сразу отзывает его. Остальные ключи
продолжают работать. Истёкший или отозванный ключ заменить через Rotate нельзя: выпустите новый.
Ключи принадлежат созданному приложению. Его база хранит только хеши секретов. При каждом запросе
приложение проверяет отзыв и срок действия, в том числе после отделения от CMS и без связи с ним.
Разрешения задаются точными идентификаторами операций, например GradArticle.create и
GradArticle.update, без подстановочного *. Режим доступа, доступность операций, ограничения строк
и полей и ваш код приложения продолжают действовать. Сервисный ключ не открывает маршрут, который
требует пользовательскую сессию. Изменения переводов наследуют разрешения update/delete основной
модели. Прежние API_BEARER_TOKEN и токены чтения ACCESS_CONFIG по-прежнему поддерживаются.
Для управления ключами нужна сессия администратора приложения; изменения требуют его origin. Сам ключ интеграции не даёт права создавать или отзывать ключи.
| Метод и путь | Запрос | Ответ |
|---|---|---|
GET /api/auth/machine/operations |
— | { operations: [{ id, model, operation }] } — доступные операции |
GET /api/auth/machine/list?offset=0 |
Неотрицательное смещение | { credentials, total, offset, limit: 50 }, без секретов и хешей |
POST /api/auth/machine/create |
{ name, permissions, expiresAt? }; срок — будущая Unix-метка в миллисекундах или null |
201 { credential, token } |
POST /api/auth/machine/rotate |
{ id } |
{ credential, token }, секрет показывается один раз |
POST /api/auth/machine/revoke |
{ id } |
{ id, revoked: true } |
Неизвестные операции, недопустимые разрешения и прошедший срок отклоняются. Одновременная замена
одного секрета или попытка заменить неактивный ключ возвращает 409: обновите список перед повтором.
Используются сессии и проверка администратора закреплённой версии Better Auth; отдельный плагин API-ключей не нужен.
Для интеграции добавляется migrations/<number>_artifact_machine.sql после существующей истории миграций. Ранее применённые миграции аутентификации и управления учётными записями остаются без изменений. Регенерация сохраняет имя уже записанной миграции ключей.
Дополнительные поля профиля
Заголовок раздела «Дополнительные поля профиля»Поля учётной записи приложения задаются в принадлежащем вам файле
src/owned/profile-fields.json созданного приложения. Изначально он пуст; регенерация его сохраняет:
{ "customerId": { "type": "string", "editable": "admin", "required": true, "default": "unassigned", "maxLength": 80 }, "locale": { "type": "string", "editable": "user", "default": "en", "options": ["en", "th"] }, "subscribed": { "type": "boolean", "editable": "user", "default": false }}Типы: string, number, boolean и date (календарная дата YYYY-MM-DD). Для каждого поля задайте
editable: "user" или "admin". Поля admin нельзя передать при регистрации или самостоятельном
изменении профиля, даже с пустым или ложным значением. Администратор приложения может изменять оба
вида полей. Поддерживаются label, required, default, числовые min/max, строковые maxLength
и options. Максимум — 32 дополнительных поля. Системные поля личности, ролей и сессий переопределять нельзя. Имена должны быть уникальны без учёта регистра, как в SQLite. Изменение только регистра имени существующего или сохранённого столбца отклоняется.
После изменения запустите npm run auth:profile. Команда проверит настройки, добавит миграцию
приложения для новых столбцов и создаст src/generated/profile-types.ts. Тип ApplicationProfile
также экспортируется из src/profile-fields.ts. Новому обязательному полю нужно допустимое значение
по умолчанию: оно заполнит существующие записи. npm run auth:profile также записывает состояние
схемы в src/generated/profile-schema.json. Добавьте состояние, типы и миграции в один коммит.
npm run typecheck и npm run db:init проверяют подготовку, не создавая миграций. Примените
сохранённую в коммите миграцию к целевой базе до запуска пересобранного приложения. Схема базы CMS
не меняется.
Элементы управления появляются на странице в New account и Profile. Администратор передаёт
дополнительные значения в data операции create-user; update-user принимает их вместе с name,
image и email. Ответы чтения включают объявленные поля. Пользователи могут передавать поля user
в /api/auth/sign-up/email и /api/auth/update-user. Необъявленные поля, неверные значения и
запрещённые изменения отклоняются.
Повторный запуск не дублирует миграции. Удаление объявления скрывает поле, сохраняя его данные;
возврат того же объявления использует прежние значения. Для изменения типа или обязательности
нужны ваша миграция приложения и обновлённая запись решения src/generated/profile-schema.json.
Автоматическая подготовка отклоняет такую смену до явного изменения. Новый default действует на
создаваемые учётные записи и не меняет существующие значения.
Если подготовка прервалась, снова запустите npm run auth:profile. Журнал незавершённой подготовки
сначала восстановит ту же миграцию, затем обработает новые изменения объявления. Режим --check
сообщает о незавершённой работе, не меняя файлы. Повторная генерация сохраняет журнал; не удаляйте
его, чтобы обойти подготовку.
Роли приложения
Заголовок раздела «Роли приложения»В Настройки → Код и публикация включите вход в приложение и администрирование его
учётных записей. В разделе Роли приложения добавьте роли, например 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 --complete. Команда удерживает исключительное право записи на всех этапах: создаёт исходный код, запускает prisma:generate, нормализует клиент и проверяет пользовательские расширения. Журнал восстановления охватывает все эти шаги. Для артефакта всего сайта нужны два входных значения:
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.mjsnpm run regeneratenpm run migrate:emitnpm run typechecknpm run buildnpm run files:manifestrm definitions.jsonПосле отключения. Без CMS регенерация работает только из definitions.json, который вы получили раньше, например шагами выше перед выводом CMS из эксплуатации. Храните эту копию вне репозитория: это вся модель контента сайта. Регенерация из неё пересобирает генерируемые файлы для той же версии. Более нового контента она не приносит.
cp <kept-definitions.json> definitions.jsonexport GENERATION_SOURCE_VERSION="$(node -p "require('./definitions.json').data.generationSourceVersion")"node scripts/install-dependencies.mjsnpm run regeneratenpm run migrate:emitnpm run typechecknpm run buildnpm run files:manifestrm 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)
Коммит исходного кода: e207e63239a627fbd2ae9c414ef7c6eb19fcfe5a (с локальными изменениями)
Дата сборки (UTC): 2026-10-21T09:28:24.214Z
Коммит для обновления: не определён: в рабочей копии есть изменения. Примечания к выпуску