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

Протокол плагинов v1

Здесь описаны все маршруты, заголовки, тела запросов, коды исходов и правила повторов удалённого протокола плагинов. Плагин — это отдельно развёрнутый сервис: он общается с Dee Wan по HTTP и не выполняет никакого кода внутри него. См. также Написание плагина — первый плагин от начала до конца, Безопасность плагинов — граница доверия, и Тестирование плагина — как его тестировать.

Поддерживаемые версии протокола: 1. Версия согласуется один раз, при установке, и закрепляется за инсталляцией. Обновление, которое изменило бы её, отклоняется (protocol_changed): перевод инсталляции на v2 означает её удаление и установку нового релиза. Несовместимое изменение маршрута, смысла или кода ошибки требует протокола v2. Добавление полей в ответ допускается только потому, что клиенты обязаны игнорировать неизвестные им ключи ответа. Формы запросов закрыты — неизвестный ключ отклоняется и никогда не игнорируется.

Каждый маршрут плагина принимает Authorization: Bearer <token>, где токен — это dwp_, за которым следуют 43 URL-безопасных символа, кодирующих 256 случайных бит. Ядро хранит только его хеш SHA-256 и никогда не может показать его снова. Плагин получает его один раз, при активации.

Маршруты находятся под /api/plugin/v1. Им не нужны ни заголовок Origin, ни cookie, потому что сервер не отправляет ни того, ни другого. Перед чтением тела запроса ядро последовательно отклоняет запросы, в которых вызывающая сторона указала сайт или базу данных, сопоставляет токен с одной активной инсталляцией, учитывает запрос при ограничении частоты, определяет сайт и направляет запрос в базу данных арендатора.

Следствия, которые стоит назвать прямо:

  • Сайт определяется по токену. Плагин не может указать его самостоятельно, и сама отправка X-Site-Id — это invalid_request.
  • Пользовательский cookie не является аутентификацией плагина, а токен плагина не принимается ни на одном административном маршруте.
  • Инсталляции в состоянии pending, disabled и revoked отвечают plugin_unauthorized до какого-либо доступа к арендатору. Так же отвечает каждая инсталляция на сайте, который не активен.
Заголовок Где Значение
Authorization: Bearer <token> каждый запрос плагина Токен инсталляции. Обязателен.
Idempotency-Key переходы, записи, черновики, медиа и предложения; подготовка будущей версии использует сочетание инсталляции, релиза и записи Обязателен. От 1 до 128 символов в диапазоне \x21–\x7e (печатные ASCII, без пробела). Хранится 30 дней. Не используется /launch/exchange.
Content-Type: application/json запросы с телом Единственный формат тела.
X-Site-Id никогда Отклоняется. Сайт берётся из токена.
Retry-After в ответе, допускающем повтор запроса В ответе 429 ядро указывает задержку в секундах. При обработке входящих ответов, допускающих повтор, клиенты также принимают HTTP-даты. Соблюдайте указанную задержку с учётом лимитов числа попыток, общего времени повторов и ожидания.
Referrer-Policy: no-referrer ответ ядра на запуск и ваша страница управления Ядро устанавливает его только в собственном ответе на запуск. Затем браузер открывает ваш management_url с dee_wan_launch в query, поэтому ваша страница должна сама отправлять Referrer-Policy: no-referrer и убирать код из адресной строки: обменяйте его на стороне сервера, затем перенаправьте на тот же URL без dee_wan_launch.
Лимит Значение
Тело запроса 16384 байт; превышение лимита — limit_exceeded
Тело записи полей или предложения 81920 байт, 50 полей, 65536 байт значений
Тело черновика 1916384 байт; черновик 1900000 байт, 200 на человека
Тело медиа 33619968 байт; файл 33554432 байт
Тело манифеста 32768 байт
Ключ идемпотентности 128 символов
Страница контента 50 элементов, по умолчанию 20
Записи делегирования 100 id записей за одно чтение
Запросы на инсталляцию 120 за окно 60 с, по ключу инсталляции
Время жизни кода запуска 60 с, однократное использование, хранится в виде хеша
Одновременное действие старого и нового токена при ротации не более 10 минут, завершается первым запросом с новым токеном
Запись идемпотентности 30 дней. После этого тот же ключ — новая команда
Время жизни делегирования без ограничения по времени. См. «Делегирования»
Вызов 10 с на POST, обмен и ответ вместе; код истекает вместе с ним
Ответ на вызов 65536 байт
Вход вызова 32768 байт значений операции или фактов политики
Повтор операции 24 часа для одного Idempotency-Key одного вызывающего
Возможность Что разрешает
content:read Маршруты чтения контента в пределах выданных моделей.
workflow:read Блок workflow при чтении: состояние, ревизия и переходы через границу публикации. Команды должны закреплять workflow.revision, поэтому плагину, который их отправляет, это нужно.
workflow:publish Команда kind: publish.
workflow:unpublish Команда kind: unpublish.
workflow:locale Команда, которая называет один locale и меняет состояние только выбранного языка, и список workflow.locales при чтении. Без неё команда с locale получает capability_refused; команда без locale всегда меняет состояние всей группы переводов.
workflow:release Подготавливать и читать будущие версии этой инсталляции; отправлять ограниченную команду публикации и снятия с публикации с одним атомарным коммитом в базе сайта.
content:write Сохранение значений полей элемента от имени пользователя, выдавшего делегирование, с привязкой к текущей версии элемента.
content:create Создавать записи с текущими правами человека.
content:delete Перемещать записи в Корзину с текущими правами человека.
draft:write Чтение, запись и удаление собственных рабочих черновиков делегирующего человека.
media:write Загрузка медиа от имени делегирующего человека, у которого должно быть media:upload.
media:read Читать существующие файлы и метаданные.
media:update Изменять метаданные существующих медиа.
media:delete Удалять неиспользуемые медиа с текущим правом media:delete и проверкой ссылок в сохранённой истории.
content:restore Восстанавливать запись из корзины с текущими правами человека на удаление и языки.
comments:read Читать комментарии посетителей с текущим правом модерации и видимостью разрешённых моделей.
comments:moderate Модерировать комментарии с обычными проверками согласия и конфликтов состояния.
redirects:read Читать перенаправления с текущим правом site:settings.
redirects:write Проверять и изменять перенаправления с обычными проверками ревизии «Меню и SEO» и шаблонов.
mail:send Отправлять одобренные администратором шаблоны заданным получателям с текущим правом site:plugins и ограниченными объявленными подстановками.
renderer:metadata Сохранять объявленные метаданные для отдельных языков в новой версии черновика; значения SEO и проверки сайта из ядра имеют приоритет.
content:propose Предложение значений полей на проверку редактору и чтение предложений этой инсталляции.
media:source Ответ на media_sources манифеста: люди просматривают их в окне выбора медиа и копируют выбранный файл в собственную библиотеку сайта или, для источника, объявившего режим reference, привязывают его. Из ядра ничего не читает.

Предоставленный плагину доступ ограничен выбранными возможностями и списком id моделей контента конкретного сайта. Пустой список моделей означает отсутствие доступа к контенту, а не все модели. Возможности вне запроса сохранённого манифеста не могут быть выданы, поэтому обновление плагина никогда не может расширить предоставленный доступ.

pending, active, disabled, revoked. Вызывать API плагинов может только active. revoked — конечное состояние: его токен никогда не становится снова действительным, и удаление сначала выполняет отзыв.

При установке (kind: "activate") и при ротации токена (kind: "rotate") ядро отправляет POST $defs/activation_handoff в виде JSON на activation_url манифеста. Он несёт сырой токен и activation_code, который администратор вставил в Dee Wan. Плагин выпускает этот код, по одному на установку или ротацию, и должен его проверить. Ядро принимает только коды, соответствующие ^[A-Za-z0-9_-]{16,128}$. При любом ответе 2xx токен считается принятым. Любой другой статус, перенаправление или отсутствие ответа в течение 5 с означает отказ принять токен.

Пока ядро ждёт этого ответа, токен недействителен: инсталляция остаётся pending, пока ядро не получит ваш 2xx. Вызов /api/plugin/v1, сделанный изнутри обработчика, отвечает plugin_unauthorized. Сохраните токен, ответьте 2xx, затем вызовите /context. Ротация работает так же: новый токен действителен только после вашего 2xx.

Человек открывает плагин из Dee Wan. Плагин обменивает код запуска на /launch/exchange и получает delegation_id. Обмен операции над записью тоже возвращает его, привязанным к этой записи. Каждая команда должна указывать делегирование; запросы на чтение тоже могут его указывать. Оно указывает пользователя, от имени которого действует плагин, и не расширяет доступ, предоставленный установленному плагину.

  • Без ограничения по времени. Делегирование не истекает со временем, поэтому расписание может использовать его через дни или недели.
  • Оно перестаёт работать, когда указанный в нём пользователь деактивирован или больше не является участником сайта (requester_unauthorized), и навсегда — когда инсталляция удалена, что отзывает её токен (plugin_unauthorized на каждый запрос). Пока инсталляция отключена или сайт неактивен, каждый запрос тоже отвечает plugin_unauthorized.
  • Полномочия перечитываются при каждом запросе. Команда отклоняется с requester_unauthorized, когда человек больше не может выполнить этот переход. Чтение вместо этого отвечает transitions: [].
  • Делегирования действий над контентом (instance_id задан) действуют только в пределах группы переводов элемента, для которого они были созданы: самого элемента, его переводов и основного экземпляра группы. Запрос к любому другому элементу или к исходному элементу, который больше не существует, возвращает requester_unauthorized.
  • Делегирования manage (instance_id равен null) действуют в отношении записей моделей из своих model_ids: разрешённых плагину моделей, которые пользователь мог читать в момент запуска, все записи или только свои.
  • Модели перепроверяются при каждом запросе. Элемент, чья модель не входит в model_ids, или который человек больше не может читать, — это requester_unauthorized. /delegations/:delegationId перечисляет, какие модели сейчас доступны в рамках делегирования.
  • Свои записи проверяются по одной. При read:own или update:own модель возвращается с entries: "checked". /delegations/:delegationId/entries говорит, какие из ваших записей человек может читать сейчас и что он может с ними делать. Не показывайте то, что он пропустил.
  • Учётные данные дополнительно ограничивают доступ. Делегирование, открытое учётными данными управления, несёт только возможности инсталляции, которые разрешают scopes этих учётных данных. Команда вне их — capability_refused.
  • Доступ к моделям проверяется при каждом запросе. Запись недоступной плагину модели — это content_not_found, каким бы ни было делегирование.
  • Публичное значение action возвращается таким, каким было при запуске. При последующих делегированных запросах требуемые действия плагина проверяются по текущим объявлениям и правам пользователя: старый запуск не сохраняет отозванное право.

Независимые действия объявляются в role_actions: [{ id, label }]. Для id действует тот же формат стабильного локального идентификатора, что и для операции. Администратор назначает действия в разделе Настройки → Роли. В роли они хранятся как plugin_actions: ["<manifest-id>/<action-id>"]. Поэтому роли с одинаковым доступом к контенту могут иметь разные права на действия плагинов.

Укажите requires_actions: ["<action-id>"] у действия над записью, представления администратора или операции. Для именованного представления управления используется manage_view_action_requirements: { "<view-id>": ["<action-id>"] }. Все действия и представления должны быть объявлены в том же манифесте. Повторяющиеся, неизвестные и чужие идентификаторы отклоняются. Возможности инсталляции, доступ к моделям, ограничения полей и языков и правила рабочего процесса продолжают действовать.

Ядро проверяет права при показе, запуске и выполнении действий, включая делегированные запросы. После удаления права или объявления действия существующая сессия теряет доступ к нему. Первое назначение может сделать суперадминистратор или администратор с системным правом *; одного site:plugins недостаточно. Неизменённые назначения удалённого плагина можно оставить в роли: без объявления они ничего не разрешают.

Учётным данным управления дополнительно нужен явно выбранный scope plugin_action:<manifest-id>/<action-id>. Одного plugins:use недостаточно. При выдаче проверяются объявления установленных плагинов и права создателя; при выполнении выбранные scopes пересекаются с его текущими правами. Системное право * создателя не расширяет учётные данные за пределы выбранных действий.

Манифест может объявить operations (не больше 8) и policies (не больше 8). Ядро выполняет их на плагине. Каждую форму и каждый результат оно рисует своими компонентами; ничто из присланного плагином не является разметкой или кодом.

  • У операции закрытый input не больше чем из 8 полей — text (с max_length), number (min, max, integer), boolean, select (закрытые options) — и список result из компонентов CMS: text, rows (ключ/значение), links (только https) и json (предлагается к скачиванию). scope: "site" запускается из раздела Настройки → Плагины и требует site:plugins. scope: "entry" выполняется над одной записью выданной модели, которую человек читает; при destructive: true пользователь также должен иметь право изменять запись и подтвердить действие перед выполнением. audience обязателен. Операции interactive показываются и выполняются в админке, API управления, CLI и MCP. Операции automation там не появляются, и эти маршруты выполнения их отклоняют; вызывает такую операцию только именованный процесс ядра (передача из планировщика контента в Scheduling) — через тот же транспорт, с проверкой текущих прав и предоставленного доступа, идемпотентностью и аудитом.
  • Политика объявляет один kind: save (каждое сохранение контента, включая то, которое меняет только связи, медиа или настройки поиска), restore (восстановление старой версии или выход записи из корзины), delete (запись или язык перемещены в корзину или удалены окончательно), publish, unpublish и transition (каждый переход workflow, включая переходы по расписанию и от плагинов). Её связанный вход — $defs/policy_facts: вид, id и slug модели, id записи (null для новой записи), язык, changed_fields, values и, где применимо, links, media, seo, action и transition. Для save и restore они содержат изменённые части кандидата; для остальных видов — всю версию, о которой идёт речь. Связи, общие для всех языков, и медиа передаются только с основным языком. Никаких учётных данных и никакого другого контента. Она отвечает $defs/policy_reply: allow, warning с сообщением, refuse с кодом и сообщением или — только для save и restore — transform с заменяющими values, links, media или seo. fields называют ключи полей.
  • Операция может быть и столбцом списка контента, и действием над строками. Столбец — это scope: "page" с audience: "column", видом cell (text, badge или link), result: ["cells"], без input и не destructive. Для одной разрешённой плагину модели он получает $defs/column_request — модель и не более 50 идентификаторов строк, которые человек может читать, но не значения — и отвечает одним блоком cells: не более одной ячейки на названную строку, каждая { id, text, tone?, url? } (tone для метки, https-url для ссылки, текст до 80). Нужное он читает в пределах предоставленного ему доступа. Столбец не создаёт делегирования и никогда не перечисляется и не запускается как обычная операция. Действие над строками — интерактивная операция entry с list_action: true: ядро запускает её по одному разу для каждой из не более 25 строк, выбранных человеком, проверяя его текущие права для каждой строки.
  • requires перечисляет возможности инсталляции из requested_capabilities. Элемент доступен, только если плагину предоставлены все перечисленные возможности. Принятое обновление манифеста никогда не расширяет предоставленный доступ, поэтому новый элемент, которому нужно больше, недоступен, пока администратор его не выдаст.

Один вызов по порядку:

  1. Ядро записывает вызов — инсталляцию, сайт, вызывающего, объявленный id, необязательные модель и запись, хеш входа, срок — и SHA-256 одноразового кода dwi_.
  2. Ядро отправляет POST $defs/invocation_envelope на объявленный endpoint: версию протокола, id инсталляции, id вызова, код, срок. Он не несёт ни контента, ни учётных данных. Перенаправления отклоняются.
  3. Плагин отправляет код на POST /api/plugin/v1/invocations/exchange со своим bearer-токеном и получает связанный вход ($defs/invocation_binding). Ядро расходует код один раз.
  4. Плагин отвечает на POST ядра с HTTP 200 и $defs/operation_reply или $defs/policy_reply.

Ядро принимает ответ, только если именно эта инсталляция обменяла вызов и по-прежнему имеет необходимые для него права: активна, та же принятая версия и объявление, все нужные возможности и доступ к модели по-прежнему предоставлены. Всё остальное ядро классифицирует и хранит в журнале аудита хеши, а не тела:

Сбой Когда
plugin_timeout нет полного ответа за 10 с
plugin_unreachable нет соединения, перенаправление или 5xx
plugin_response_invalid любой другой статус, не схема ответа, необъявленный компонент результата, слишком большой ответ или ответ без обмена
plugin_refused операция ответила status: "refused" со своими кодом и сообщением
plugin_unavailable инсталляцию отключили, отозвали, сузили или изменили во время вызова

Других нет: plugin_timeout, plugin_unreachable, plugin_response_invalid, plugin_refused, plugin_unavailable. Никогда не отвечайте до обмена и никогда не кешируйте связку: у каждого вызова свой код.

  • Ядро спрашивает после своих проверок (поля, правила, права, процесс, публикация) и до записи. Политика не заменяет эти проверки и не меняет этап согласования.
  • Оно спрашивает активные инсталляции, которым разрешён доступ к модели записи и чья политика доступна, все сразу, не больше 8 одновременно. Сохранение или восстановление спрашивает один раз на каждый язык, значения, связи, медиа или настройки поиска которого меняются; публикация, снятие с публикации, удаление или переход — один раз на каждый затронутый язык. Восстановление спрашивает политики save и restore.
  • transform заменяет части записи и языка, о которых вас спросили, по ключу поля, и ничего больше: он не называет запись, поэтому не может дотянуться до другой. values и связи по языкам попадают в этот язык; связи, общие для всех языков, и media — только с основного языка; seo заменяет настройки поиска этого языка (в форме редактора, изображение как { media_id, alt }). Список заменяет весь список. Восстановление принимает только values и seo. Затем ядро повторно проверяет всю запись с вашими заменами по всем правилам, которые применяются к пользовательскому вводу (правила полей, форматированный текст, связи, медиа, уникальность, права). Если хотя бы одна замена не проходит проверку, запись отклоняется с plugin_policy_transform_invalid и указанием причины; если две политики по-разному меняют одну часть, запись отклоняется с plugin_policy_transform_conflict. Вашу политику не спрашивают снова на этом втором проходе. Результат перечисляет outcome: "transformed" с изменёнными вами полями и вашим необязательным message, а сохранённая версия записывает это в meta.policy_transforms. Transform, который ничего не меняет, — это разрешение. transform для любого другого вида — ответ, который ядро не принимает.
  • refuse всегда отклоняет запись. Редактор называет ваш плагин и политику и показывает ваше сообщение у полей, которые вы назвали. warning пропускает запись и показывает сообщение. Повторная отправка не требуется.
  • Реакцию на тайм-аут, сбой или ответ, который ядро не принимает, определяет режим, выбранный администратором для политики в разделе Настройки → Плагины: warn (по умолчанию) пропускает запись с предупреждением, required её отклоняет.
  • caller.via — person, management_credential, planner, когда контент-планировщик работает за владельца плана, или plugin, когда проверяется делегированная запись другого плагина. Импорт, таблицу и восстановление спрашивают от имени человека, который их запускает.
  • Запись выполняется, только если показанное вам всё ещё актуально. Изменение в промежутке — обычный конфликт версий, и ваш ответ к другому контенту не применяется.

Манифест может объявить hooks: не более одного каждого вида, upload_metadata и comment_moderation. У каждого есть id, label, HTTPS-endpoint того же origin и requires (возможности из requested_capabilities). Хук ВЫКЛЮЧЕН, пока администратор не включит его для инсталляции в разделе Настройки → Плагины. Ядро вызывает включённый хук ДО операции, а не после: upload_metadata — перед сохранением нового файла библиотеки, comment_moderation — перед сохранением комментария посетителя.

  • Хук вызывается через тот же транспорт вызовов, с kind: "hook" в привязке, caller: null и без делегирования. Ядро сообщает хуку, о чём его спрашивают, и никогда не сообщает, кто спросил: ни человека, ни почты, ни учётных данных, ни байтов файла, ни других записей.
  • upload_metadata: входные данные — $defs/hook_upload_facts (имя файла, MIME-тип, размер, папка и альтернативные тексты; ширина и высота равны null, пока не измерены). Отвечайте $defs/hook_upload_reply: accept, при желании с normalized (новое filename, сохраняющее расширение, и альтернативные тексты только на языках, которые загрузка уже назвала), либо refuse с кодом и сообщением, которое читает загружающий (422 upload_hook_refused).
  • comment_moderation: входные данные — $defs/hook_comment_facts (запись, id комментария, родитель, имя автора, сайт и текст; никогда не почта). Отвечайте $defs/hook_comment_reply: approve, hold (остаётся на модерации) или reject (сохраняется как спам или, с as: "refused", как отклонённый).
  • Хук, который не смог ответить, никогда не одобряет. Тайм-аут (5 секунд), сбой, тело не из закрытых ответов или инсталляция, отключённая или изменённая во время работы, отклоняют загрузку (503 upload_hook_unavailable, ничего не сохраняется) или оставляют комментарий на модерации.
  • Идентификатор повтора — хеш запроса: в течение 24 часов при повторной загрузке или повторной отправке комментария используется первый ПРИНЯТЫЙ ответ, а хук повторно не вызывается. Неудачный вызов не воспроизводится; при следующем таком же запросе хук вызывается снова.

Манифест может объявить media_sources (не больше 4) и запросить media:source. Каждый источник — библиотека, которую люди просматривают в окне выбора медиа рядом с собственной библиотекой сайта, а также через API управления. Каждый источник объявляет modes: copy (ядро копирует выбранный файл в сайт, поэтому опубликованный контент никогда не зависит от источника), reference (ядро хранит только URL доставки и идентичность файла, а байты хранит источник) или оба. Режим называет человек или вызывающий API при каждом добавлении; ядро никогда не выводит его из URL или имени.

  • Источник вызывается через тот же транспорт вызовов, с kind: "media_source" в связке и без делегирования. Его связанный вход — $defs/media_source_request: browse с query (пустым для первой страницы), cursor (предыдущий next_cursor или null) и limit (не больше 24) либо item с item_id, который выбрал человек.
  • Отвечайте на browse через $defs/media_source_page_reply, а на item — через $defs/media_source_item_reply, или status: "refused" с кодом и сообщением. Каждый элемент несёт заголовок, имя файла, MIME-тип, необязательные размер и габариты, thumbnail_url, download_url (обязателен для copy), delivery_url (обязателен для reference) и attribution: source_name, license и необязательные author, author_url, license_url и source_url. description изображения (не больше 125 символов) становится его альтернативным текстом на языке сайта по умолчанию.
  • thumbnail_url и download_url должны быть на единственном origin манифеста. delivery_url должен быть на одном из delivery_origins источника (не больше 4 чистых origin, обязательны с reference и отсутствуют без него; origin манифеста может быть одним из них). Ответ, называющий любой другой origin, — это plugin_response_invalid. Окно выбора загружает миниатюры прямо с вашего origin без referrer и без учётных данных Dee Wan. Ссылки авторства показываются, но никогда не запрашиваются.
  • Когда человек выбирает элемент, ядро снова спрашивает item, затем выполняет GET download_url без учётных данных и без перенаправлений, в пределах 20 с и лимита медиафайла. Байты проходят те же проверки, что и загрузка: виды, форматы и лимиты размера сайта и его проверку дубликатов. Новое медиа сохраняет авторство и id плагина, источника и элемента. Подписывайте download_url сами, если ваши файлы не публичны.
  • В режиме reference ядро ничего не загружает. Строка медиа хранит ваш delivery_url как адрес, ваши id плагина, источника и элемента как идентичность и объявленные вами метаданные; один и тот же элемент, добавленный дважды, подчиняется on_duplicate. Опубликованные страницы, копии сайта и генерируемые сайты используют delivery_url как есть, поэтому он должен отдаваться, пока им пользуются сайты. Ядро никогда не проксирует его, не заменяет его байты и не обращается к вам, когда файл удалён из библиотеки. Режим, которого источник не объявил, отклоняется с media_source_mode_unsupported.
  • Просмотр ничего не меняет, поэтому не аудируется. Копия — обычная загрузка медиа от имени человека, который её выбрал.
  • Вызывающему нужен media:upload. Отключённая инсталляция, отзыв возможности media:source или изменённое объявление сразу делают источник недоступным.

Что представляет собой эта инсталляция. Id инсталляции, сайт, к которому она привязана, принятая версия протокола и предоставленный доступ. Прочитайте его при запуске, чтобы узнать, что эта инсталляция может запрашивать. Он не называет ни пользователя, ни хеш токена, ни базу данных, ни инфраструктуру.

  • Вызывающий: плагин со своим токеном инсталляции
  • Требует: ничего
  • Нет query, нет тела.
  • Отвечает: { "data": <context> } — $defs/context.

Исходы:

  • plugin_unauthorized
  • rate_limited
  • temporary_failure

Что может делать эта инсталляция

Окно терминала
curl "https://cms.example/api/plugin/v1/context" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp"
{
"data": {
"installation_id": "plg_0000000000000000000000000000d1ee",
"site": {
"id": "cdocssite00000000000000001",
"name": "docs-site",
"default_lang": "en"
},
"protocol_version": 1,
"capabilities": [
"content:read",
"workflow:read",
"workflow:publish",
"workflow:unpublish"
],
"model_ids": [
"cdocsmodel0000000000000001"
]
}
}

Неизвестный, отключённый или отозванный токен

Окно терминала
curl "https://cms.example/api/plugin/v1/context" \
-H "Authorization: Bearer dwp_UnknownTokenNotRealUnknownTokenNotRealUnkno"
{
"outcome": {
"code": "plugin_unauthorized",
"message": "The installation token is missing, unknown, disabled or revoked.",
"retry": false
}
}

Один ответ на все четыре случая. Ничто не сообщает вызывающему, какой из них имеет место.

Плагин не может назвать сайт

Окно терминала
curl "https://cms.example/api/plugin/v1/context" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp" \
-H "X-Site-Id: cotherdocsite00000000001"
{
"outcome": {
"code": "invalid_request",
"message": "A plugin request cannot name a site.",
"retry": false
}
}

Сайт берётся из токена. Сама отправка заголовка отклоняется, даже с правильным значением.

Неизвестный маршрут плагина

Окно терминала
curl "https://cms.example/api/plugin/v1/schedules" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp"
{
"outcome": {
"code": "content_not_found",
"message": "No such plugin route.",
"retry": false
}
}

В протоколе v1 есть маршруты выше и ничего больше. 404 здесь — не временный сбой.

Список контента в одной выданной модели. Одна страница основных экземпляров одной модели. model_id обязателен: чтения «всех моделей» нет. Набор фильтров закрыт — любой другой параметр query даёт invalid_request, а не игнорируется, — а страница представляет собой краткую сводку, а не снимок контента. Появляются только основные экземпляры групп переводов. Группа — это единица, которую перемещает переход.

  • Вызывающий: плагин, content:read
  • Требует: content:read
  • model_id (обязательный) — id модели, доступной плагину.
  • published — true или false.
  • after — последний instance_id предыдущей страницы.
  • limit — 1..50, по умолчанию 20.
  • Отвечает: { "data": [<summary>], "next": <instance_id|null> } — $defs/content_page.

Исходы:

  • capability_refused — нет content:read или модель недоступна плагину
  • invalid_request — отсутствует model_id, неизвестный фильтр, limit вне диапазона
  • plugin_unauthorized
  • rate_limited
  • temporary_failure

Одна страница одной модели

Окно терминала
curl "https://cms.example/api/plugin/v1/content?model_id=cdocsmodel0000000000000001&published=false&limit=1" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp"
{
"data": [
{
"instance_id": "cdocsinstance00000000000001",
"primary_instance_id": "cdocsinstance00000000000001",
"translation_group_id": "cdocsgroup0000000000000001",
"model_id": "cdocsmodel0000000000000001",
"model_slug": "article",
"lang": "en",
"label": "Hello",
"slug": "hello",
"current_version_id": "cdocsversion00000000000001",
"published_version_id": null
}
],
"next": null
}

Фильтр, отличный от model_id, published, after, limit, — это invalid_request, а не игнорирование.

Набор фильтров закрыт

Окно терминала
curl "https://cms.example/api/plugin/v1/content?model_id=cdocsmodel0000000000000001&sort=title" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp"
{
"outcome": {
"code": "invalid_request",
"message": "model_id is required; filters are model_id, published, after, limit.",
"retry": false
}
}

Чтение одного элемента и доступных для него переходов. Ответ содержит id, метку (заголовок записи на её языке, а при его отсутствии — заметку текущей версии), slug и id текущей и опубликованной версий. При наличии workflow:read в ответ добавляются состояние workflow, его ревизия и переходы, пересекающие границу публикации. Список переходов ограничен возможностями этой инсталляции. Если передать delegation_id, список дополнительно ограничивается действиями, доступными указанному в делегировании пользователю СЕЙЧАС; его права проверяются при каждом запросе. Если пользователь деактивирован или больше не является участником сайта, запрос отклоняется с requester_unauthorized. Если он всё ещё участник, но больше не может изменять эту запись, API возвращает 200 с transitions: [] (команда всё равно была бы отклонена с requester_unauthorized). Делегирование действия над контентом принимается только для записей из группы переводов, для которой оно было создано. Делегирование manage принимается для любой записи моделей, указанных при его создании. Любое делегирование отклоняется, если модель записи не входит в его model_ids или указанный в нём пользователь больше не может читать запись. Без workflow:read блок workflow отсутствует. При допустимом делегировании ответ также содержит снимок saved, ограниченный текущими правами. Каждая команда должна закреплять workflow.revision, поэтому плагину, отправляющему команды, нужен workflow:read. Если запись ещё не переходила между этапами согласования, её ревизия равна 0. Если запись относится к другому сайту или её модель недоступна плагину, API возвращает content_not_found; тот же ответ возвращается для id с другого сайта.

  • Вызывающий: плагин, content:read
  • Требует: content:read, а также workflow:read для блока workflow
  • delegation_id — делегирование из /launch/exchange или из связки операции над записью. Никакие другие параметры query не принимаются.
  • Отвечает: { "data": <summary> } — $defs/content_summary.

Исходы:

  • capability_refused
  • content_not_found
  • requester_unauthorized — названное делегирование неизвестно, принадлежит другой инсталляции, привязано к другой группе переводов или к элементу, который больше не существует, либо указанный в нём пользователь деактивирован или больше не является участником сайта, либо модель элемента не входит в model_ids делегирования, либо указанный в делегировании пользователь больше не может читать элемент
  • invalid_request
  • plugin_unauthorized
  • rate_limited
  • temporary_failure

Один элемент с переходами, которые запрашивающий может выполнить сейчас

Окно терминала
curl "https://cms.example/api/plugin/v1/content/cdocsinstance00000000000001?delegation_id=pld_0000000000000000000000000000d1ee" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp"
{
"data": {
"instance_id": "cdocsinstance00000000000001",
"primary_instance_id": "cdocsinstance00000000000001",
"translation_group_id": "cdocsgroup0000000000000001",
"model_id": "cdocsmodel0000000000000001",
"model_slug": "article",
"lang": "en",
"label": "Hello",
"slug": "hello",
"current_version_id": "cdocsversion00000000000001",
"published_version_id": null,
"workflow": {
"state": "draft",
"revision": 0,
"transitions": [
{
"to": "published",
"kind": "publish"
}
]
}
}
}

Закрепите то, что возвращает этот ответ: current_version_id для публикации, published_version_id для снятия с публикации и workflow.revision для обоих.

Публикация или снятие с публикации при соблюдении условий. Маршрут меняет только состояние публикации. Четыре условия проверяются в одной атомарной операции жизненного цикла: закреплённая версия должна по-прежнему быть той, которую переместит переход, ревизия workflow должна по-прежнему быть той, относительно которой создано расписание, targetState должен быть переходом, доступным из текущего состояния, а смысл этого перехода для публикации должен совпадать с kind. Делегирование должно покрывать элемент (см. «Делегирования»), а полномочия делегирующего человека перечитываются перед записью. Более новый черновик никогда не публикуется молча. Изменение процесса согласования не игнорируется. Публикация применяется ко всей группе переводов, поэтому результат называет основной экземпляр. С workflow:locale тело, которое называет locale, меняет состояние только выбранного языка: targetState проверяется по собственному состоянию языка в workflow.locales, снятие с публикации закрепляет собственную опубликованную версию этого языка, а результат называет перемещённый locale. Переход, который публикует, также сообщает redirect: автоматическое перенаправление, которое тот же вызов жизненного цикла сделал для изменённого адреса, или причину, по которой его нет. Точный повтор возвращает весь первый результат, включая locale и redirect. Ядро записывает перенаправление после фиксации перехода. Пока его исход не записан, команда и любой её повтор отвечают temporary_failure с сообщением, что переход применён. Повторите с тем же ключом и телом: тогда повтор вернёт весь результат.

Обычная публикация закрепляет текущий обычный черновик. Для будущей версии укажите вместе future_draft_id и expected_future_revision и закрепите сохранённую версию этой ветки. Обычный черновик останется нетронутым. Повторная публикация уже опубликованного этапа разрешена только команде будущей версии с действующими правами.

  • Вызывающий: плагин, workflow:publish или workflow:unpublish
  • Требует: workflow:publish для kind: publish, workflow:unpublish для kind: unpublish
  • Idempotency-Key (обязательный) — от 1 до 128 символов в диапазоне \x21–\x7e (печатные ASCII, без пробела). Выводите его из расписания, а не из часов: точный повтор возвращает первый результат, а тот же ключ с другими входными данными — это idempotency_key_reused. Ядро хранит запись 30 дней. После этого тот же ключ считается новой командой.
  • Тело — $defs/publish_command или $defs/unpublish_command. Форма ЗАКРЫТА: неизвестный ключ или закрепление другого вида — это invalid_request.
  • Отвечает: { "outcome": {...}, "data": <transition_result> } — $defs/transition_result.

Исходы:

  • transition_applied / idempotent_replay
  • invalid_request
  • capability_refused
  • requester_unauthorized — делегирование неизвестно, не покрывает этот элемент, или у указанного в нём пользователя больше нет прав для перехода
  • content_not_found
  • scheduled_target_stale
  • workflow_conflict
  • dependent_content
  • idempotency_key_reused
  • transition_unavailable / transition_refused
  • rate_limited
  • temporary_failure

Публикация закреплённого черновика

Окно терминала
curl -X POST "https://cms.example/api/plugin/v1/content/cdocsinstance00000000000001/transitions/published" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: sched:plg_0000000000000000000000000000d1ee:sch_0001:r1" \
--data '{
"kind": "publish",
"expected_current_version_id": "cdocsversion00000000000001",
"expected_workflow_revision": 0,
"delegation_id": "pld_0000000000000000000000000000d1ee"
}'
{
"outcome": {
"code": "transition_applied",
"message": "The transition was applied.",
"retry": false
},
"data": {
"instance_id": "cdocsinstance00000000000001",
"primary_instance_id": "cdocsinstance00000000000001",
"from": "draft",
"to": "published",
"published": true,
"published_changed": true,
"published_version_id": "cdocsversion00000000000001",
"revision": 1,
"redirect": {
"status": "none",
"reason": "no_public_address"
},
"policy_results": []
}
}

Та же команда ещё раз

Окно терминала
curl -X POST "https://cms.example/api/plugin/v1/content/cdocsinstance00000000000001/transitions/published" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: sched:plg_0000000000000000000000000000d1ee:sch_0001:r1" \
--data '{
"kind": "publish",
"expected_current_version_id": "cdocsversion00000000000001",
"expected_workflow_revision": 0,
"delegation_id": "pld_0000000000000000000000000000d1ee"
}'
{
"outcome": {
"code": "idempotent_replay",
"message": "This command was already applied. The first result is returned.",
"retry": false
},
"data": {
"instance_id": "cdocsinstance00000000000001",
"primary_instance_id": "cdocsinstance00000000000001",
"from": "draft",
"to": "published",
"published": true,
"published_changed": true,
"published_version_id": "cdocsversion00000000000001",
"revision": 1,
"redirect": {
"status": "none",
"reason": "no_public_address"
},
"policy_results": []
}
}

Повтор после сетевого сбоя безопасен: возвращается весь первый результат, включая redirect, и ничего не выполняется дважды.

Тот же ключ, другая команда

Окно терминала
curl -X POST "https://cms.example/api/plugin/v1/content/cdocsinstance00000000000001/transitions/published" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: sched:plg_0000000000000000000000000000d1ee:sch_0001:r1" \
--data '{
"kind": "publish",
"expected_current_version_id": "cdocsversion00000000000001",
"expected_workflow_revision": 7,
"delegation_id": "pld_0000000000000000000000000000d1ee"
}'
{
"outcome": {
"code": "idempotency_key_reused",
"message": "This idempotency key was already used for a different command.",
"retry": false
}
}

Выводите ключ из расписания и его ревизии. Перепланирование — это новая ревизия, а значит, и новый ключ.

Снятие с публикации закреплённой опубликованной версии

Окно терминала
curl -X POST "https://cms.example/api/plugin/v1/content/cdocsinstance00000000000001/transitions/draft" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: sched:plg_0000000000000000000000000000d1ee:sch_0002:r1" \
--data '{
"kind": "unpublish",
"expected_published_version_id": "cdocsversion00000000000001",
"expected_workflow_revision": 1,
"delegation_id": "pld_0000000000000000000000000000d1ee"
}'
{
"outcome": {
"code": "transition_applied",
"message": "The transition was applied.",
"retry": false
},
"data": {
"instance_id": "cdocsinstance00000000000001",
"primary_instance_id": "cdocsinstance00000000000001",
"from": "published",
"to": "draft",
"published": false,
"published_changed": true,
"published_version_id": null,
"revision": 2,
"policy_results": []
}
}

Более новый черновик никогда не публикуется молча

Окно терминала
curl -X POST "https://cms.example/api/plugin/v1/content/cdocsinstance00000000000001/transitions/published" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: sched:plg_0000000000000000000000000000d1ee:sch_0003:r1" \
--data '{
"kind": "publish",
"expected_current_version_id": "cdocsversion00000000000001",
"expected_workflow_revision": 2,
"delegation_id": "pld_0000000000000000000000000000d1ee"
}'
{
"outcome": {
"code": "scheduled_target_stale",
"message": "The pinned version is no longer the one this transition would move.",
"retry": false
}
}

Кто-то сохранил более новый черновик после создания расписания. Автоматический повтор не допускается: дальнейшее решение принимает пользователь.

Изменение процесса согласования не игнорируется

Окно терминала
curl -X POST "https://cms.example/api/plugin/v1/content/cdocsinstance00000000000001/transitions/published" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: sched:plg_0000000000000000000000000000d1ee:sch_0004:r1" \
--data '{
"kind": "publish",
"expected_current_version_id": "cdocsversion00000000000002",
"expected_workflow_revision": 0,
"delegation_id": "pld_0000000000000000000000000000d1ee"
}'
{
"outcome": {
"code": "workflow_conflict",
"message": "The content moved through its workflow after this was requested.",
"retry": false
}
}

Форма запроса закрыта

Окно терминала
curl -X POST "https://cms.example/api/plugin/v1/content/cdocsinstance00000000000001/transitions/published" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: sched:plg_0000000000000000000000000000d1ee:sch_0005:r1" \
--data '{
"kind": "publish",
"expected_current_version_id": "cdocsversion00000000000002",
"expected_workflow_revision": 2,
"delegation_id": "pld_0000000000000000000000000000d1ee",
"override_dependents": true
}'
{
"outcome": {
"code": "invalid_request",
"message": "The request is not valid for protocol v1.",
"retry": false
}
}

В протоколе v1 нет переопределения. Неизвестный ключ отклоняется, а не отбрасывается.

Мутация без ключа идемпотентности

Окно терминала
curl -X POST "https://cms.example/api/plugin/v1/content/cdocsinstance00000000000001/transitions/published" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp" \
-H "Content-Type: application/json" \
--data '{
"kind": "publish",
"expected_current_version_id": "cdocsversion00000000000002",
"expected_workflow_revision": 2,
"delegation_id": "pld_0000000000000000000000000000d1ee"
}'
{
"outcome": {
"code": "invalid_request",
"message": "Idempotency-Key is required.",
"retry": false
}
}

Сохранение значений полей от имени пользователя. Указанные значения полей одной записи сохраняются через обычный механизм жизненного цикла ОТ ИМЕНИ пользователя, выдавшего делегирование. Его права на модель, владение записью и состояние учётной записи проверяются заново; в истории контента автором сохранения указывается этот пользователь. Сохранение привязано к версии: expected_version_id должен по-прежнему указывать на текущую версию, иначе API возвращает version_conflict и ничего не записывает. Значения проверяются по тем же правилам, что и в редакторе: типы полей, уникальность, иерархия, язык и размер версии. fields принимает значения не более 50 полей общим объёмом не более 65536 байт. Типизированные ссылки передаются отдельно в relations, locale_relations и media. При изменении ссылок fields может быть пустым. Без locale значения относятся к собственному языку указанной записи. Если поле slug переводимое, оно переименовывает адрес только этого языка. Если поле slug общее, оно задаёт адрес основного языка. slugs в ответе содержит адреса всех языков. В административной истории указываются пользователь и установленный плагин.

  • Вызывающий: плагин, content:write
  • Требует: content:write
  • Idempotency-Key (обязательный) — как для переходов. Повтор возвращает первый результат.
  • Тело — $defs/content_write_command. Закрытое.
  • Отвечает: { "outcome": {...}, "data": <save_result> } — $defs/content_save_result.

Исходы:

  • content_saved / idempotent_replay
  • invalid_request
  • capability_refused
  • requester_unauthorized — делегирование не покрывает этот элемент, или указанный в нём пользователь сейчас не может его изменить
  • content_not_found
  • version_conflict
  • validation_failed — data называет reason правила и, если они есть, fields
  • limit_exceeded
  • idempotency_key_reused
  • rate_limited
  • temporary_failure

Сохранение одного поля с привязкой к текущей версии

Окно терминала
curl -X PUT "https://cms.example/api/plugin/v1/content/cdocsinstance00000000000001" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: write:plg_0000000000000000000000000000d1ee:0001" \
--data '{
"delegation_id": "pld_0000000000000000000000000000d1ee",
"expected_version_id": "cdocsversion00000000000002",
"fields": {
"title": "Hello from the plugin"
}
}'
{
"outcome": {
"code": "content_saved",
"message": "The field values were saved as a new version.",
"retry": false
},
"data": {
"instance_id": "cdocsinstance00000000000001",
"primary_instance_id": "cdocsinstance00000000000001",
"translation_group_id": "cdocsgroup0000000000000001",
"version": {
"id": "cdocsversion00000000000003",
"version_number": 3
},
"current_version_id": "cdocsversion00000000000003",
"published_version_id": null,
"slugs": {
"en": "hello",
"fr": "hello"
},
"locales": {
"en": "cdocsinstance00000000000001",
"fr": "cdocsinstancefr000000000001"
},
"warnings": [],
"policy_results": []
}
}

Новая версия принадлежит делегирующему человеку. Закрепите current_version_id для следующей записи.

Запись, закреплённая за более старой версией

Окно терминала
curl -X PUT "https://cms.example/api/plugin/v1/content/cdocsinstance00000000000001" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: write:plg_0000000000000000000000000000d1ee:0002" \
--data '{
"delegation_id": "pld_0000000000000000000000000000d1ee",
"expected_version_id": "cdocsversion00000000000001",
"fields": {
"title": "Lost update"
}
}'
{
"outcome": {
"code": "version_conflict",
"message": "The entry changed after the pinned version. Read it again before writing.",
"retry": false
}
}

Ничего не записано. Прочитайте элемент заново и согласуйте дальнейшие действия с пользователем.

Предложения этой инсталляции для одного элемента. Предложения, которые эта инсталляция сделала для элемента, сначала новые, не более 50, в любом состоянии: open, applied или dismissed. Предложения другой инсталляции или учётных данных управления не перечисляются.

  • Вызывающий: плагин, content:propose
  • Требует: content:propose
  • Нет query, нет тела.
  • Отвечает: { "data": [<proposal>] } — $defs/proposal_list.

Исходы:

  • capability_refused
  • content_not_found
  • invalid_request
  • plugin_unauthorized
  • rate_limited
  • temporary_failure

Предложение значений полей на проверку редактору. Записывает предложенные значения указанных полей с привязкой к base_version_id для проверки редактором элемента. НИЧЕГО НЕ МЕНЯЕТСЯ, пока пользователь не применит предложение в Dee Wan: Apply — это обычное сохранение этого человека, с его правами, относительно закреплённой версии. Редактор показывает имя плагина и человека, от имени которого он действовал. Делегирующий человек должен по-прежнему иметь возможность читать элемент.

  • Вызывающий: плагин, content:propose
  • Требует: content:propose
  • Idempotency-Key (обязательный) — как для переходов.
  • Тело — $defs/proposal_command. Закрытое.
  • Отвечает: { "outcome": {...}, "data": <proposal> } — $defs/proposal.

Исходы:

  • proposal_created / idempotent_replay
  • invalid_request
  • capability_refused
  • requester_unauthorized
  • content_not_found
  • version_conflict — base_version_id больше не текущая версия
  • validation_failed
  • limit_exceeded
  • idempotency_key_reused
  • rate_limited
  • temporary_failure

Предложение значения на проверку редактору

Окно терминала
curl -X POST "https://cms.example/api/plugin/v1/content/cdocsinstance00000000000001/proposals" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: propose:plg_0000000000000000000000000000d1ee:0001" \
--data '{
"delegation_id": "pld_0000000000000000000000000000d1ee",
"base_version_id": "cdocsversion00000000000003",
"fields": {
"title": "A better title"
},
"note": "Shorter and clearer."
}'
{
"outcome": {
"code": "proposal_created",
"message": "The proposal was created. Nothing changes until a person applies it.",
"retry": false
},
"data": {
"id": "prop_00000000000000000000000000000d1e",
"revision": 0,
"instance_id": "cdocsinstance00000000000001",
"lang": "en",
"base_version_id": "cdocsversion00000000000003",
"fields": {
"title": "A better title"
},
"note": "Shorter and clearer.",
"source": {
"kind": "plugin",
"id": "plg_0000000000000000000000000000d1ee",
"label": "My Plugin",
"requested_by": "[email protected]"
},
"state": "open",
"applied_version_id": null,
"created_at": "2026-01-01T00:00:00.000Z",
"resolved_at": null
}
}

Ничего не изменилось. Редактор записи показывает его как предложение от My Plugin по запросу человека.

Одно предложение этой инсталляции. Прочитайте одно предложение, чтобы узнать, применил ли его человек или отклонил, и какую версию создало применение. Предложение другой инсталляции или для записи, недоступной плагину — это content_not_found.

  • Вызывающий: плагин, content:propose
  • Требует: content:propose
  • Нет query, нет тела.
  • Отвечает: { "data": <proposal> } — $defs/proposal.

Исходы:

  • capability_refused
  • content_not_found
  • invalid_request
  • plugin_unauthorized
  • rate_limited
  • temporary_failure

Чтение рабочего черновика человека. Рабочий черновик пользователя, выдавшего делегирование, содержит несохранённые изменения одного элемента на одном языке, которые редактор записи может восстановить. Черновик не является ни сохранённой версией, ни записью аудита. Он принадлежит человеку: плагин читает только черновик того человека, который его запустил. target — id элемента или new:<model id> для ещё не созданного элемента, для чего нужно делегирование manage. Человек должен по-прежнему иметь возможность изменить элемент (или создать элемент этой модели). Черновики истекают через 30 дней.

  • Вызывающий: плагин, draft:write
  • Требует: draft:write
  • delegation_id, target, locale — все обязательны, больше ничего. $defs/draft_query.
  • Отвечает: { "data": <draft|null>, "expired": <boolean> } — $defs/draft_read.

Исходы:

  • capability_refused
  • content_not_found
  • requester_unauthorized
  • invalid_request
  • plugin_unauthorized
  • rate_limited
  • temporary_failure

Запись рабочего черновика человека. Заменяет этот черновик только при совпадении его текущей ревизии с expected_revision (0, если черновика ещё нет); если другое устройство или вкладка записали раньше, ответ — draft_conflict с текущим черновиком в data.current. Черновик содержит не более 1900000 байт, а у человека на сайте не более 200 черновиков; превышение любого лимита — limit_exceeded. data — рабочее состояние редактора; Dee Wan проверяет его только при сохранении человеком.

  • Вызывающий: плагин, draft:write
  • Требует: draft:write
  • Idempotency-Key (обязательный).
  • Тело — $defs/draft_write_command. Закрытое.
  • Отвечает: { "outcome": {...}, "data": <draft> } — $defs/working_draft.

Исходы:

  • draft_saved / idempotent_replay
  • draft_conflict
  • limit_exceeded
  • invalid_request
  • capability_refused
  • requester_unauthorized
  • content_not_found
  • idempotency_key_reused
  • rate_limited
  • temporary_failure

Удаление рабочего черновика человека. Удаляет ровно ревизию revision этого черновика. Более новая ревизия сохраняется, а ответ — draft_conflict. Если черновика уже нет, ответ — draft_discarded.

  • Вызывающий: плагин, draft:write
  • Требует: draft:write
  • Idempotency-Key (обязательный).
  • Тело — $defs/draft_discard_command. Закрытое.
  • Отвечает: { "outcome": {...} }.

Исходы:

  • draft_discarded / idempotent_replay
  • draft_conflict
  • invalid_request
  • capability_refused
  • requester_unauthorized
  • content_not_found
  • idempotency_key_reused
  • rate_limited
  • temporary_failure

Загрузка медиа от имени человека. Обычная загрузка медиа, с той же проверкой и хранением, что и в медиатеке: тип файла, определённый по байтам, должен входить в media_types для вида файлов, разрешённого на сайте (по умолчанию изображения и документы; аудио и видео — когда сайт их включит), размером не более 33554432 байт. У делегирующего человека должно быть media:upload. Загружать может любое делегирование этой инсталляции. Отправьте multipart/form-data ровно с такими частями: file, delegation_id и, при необходимости, alt_texts (JSON-объект: код языка сайта → текст), width, height, on_duplicate. Любая другая часть — это invalid_request. Отклонённый файл — это validation_failed с reason правила медиа. Если сайт уже хранит те же байты, решает on_duplicate: ask (по умолчанию) отвечает media_duplicate с существующим файлом и ничего не загружает, use_existing отвечает media_reused с существующим файлом, copy загружает отдельную копию.

  • Вызывающий: плагин, media:write
  • Требует: media:write
  • Idempotency-Key (обязательный).
  • Тело — multipart, $defs/media_upload_form.
  • Отвечает: { "outcome": {...}, "data": <media> } — $defs/media; media_duplicate несёт $defs/media_duplicate.

Исходы:

  • media_uploaded / media_reused / idempotent_replay
  • media_duplicate — сайт уже хранит эти байты, а on_duplicate равно ask
  • invalid_request
  • capability_refused
  • requester_unauthorized
  • validation_failed
  • limit_exceeded
  • idempotency_key_reused
  • content_not_found — повтор, медиа которого с тех пор удалено
  • rate_limited
  • temporary_failure

Обмен кода запуска на делегирование. Браузер приходит на management_url с dee_wan_launch. БЭКЕНД плагина отправляет этот код сюда со своим bearer-токеном в течение 60 секунд. Код расходуется один раз. Возвращается привязка — инсталляция, сайт, действие, необязательный экземпляр контента и model_ids: точный список разрешённых плагину моделей, которые пользователь мог читать в момент запуска, все записи или только свои (для действия над контентом — модель элемента), — и безопасная для отображения идентичность для собственной сессии плагина. Показывайте и действуйте только внутри model_ids; /delegations/:delegationId говорит, что действует сейчас. Он не даёт никаких возможностей Dee Wan, которых ещё нет у токена инсталляции, а код, выпущенный для другой инсталляции, отклоняется. Idempotency-Key не нужен: сам код одноразовый, поэтому повтор после потерянного ответа отвечает invalid_request, и человеку приходится запустить заново.

  • Вызывающий: плагин со своим токеном инсталляции
  • Требует: ничего
  • Тело — $defs/launch_exchange_request. Без Idempotency-Key.
  • Отвечает: { "data": <delegation> } — $defs/launch_delegation.

Исходы:

  • invalid_request — неизвестен, уже использован, просрочен или выпущен для другой инсталляции
  • plugin_unauthorized
  • rate_limited
  • temporary_failure

Превращение кода запуска в делегирование

Окно терминала
curl -X POST "https://cms.example/api/plugin/v1/launch/exchange" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp" \
-H "Content-Type: application/json" \
--data '{
"code": "dwl_ExampleLaunchCodeExampleLaunchCodeExampleLa"
}'
{
"data": {
"delegation_id": "pld_0000000000000000000000000000d1ee",
"installation_id": "plg_0000000000000000000000000000d1ee",
"site_id": "cdocssite00000000000000001",
"action": "schedule",
"instance_id": "cdocsinstance00000000000001",
"model_ids": [
"cdocsmodel0000000000000001"
],
"user": {
"id": "cdocsuser00000000000000001",
"email": "[email protected]"
}
}
}

Сохраните delegation_id. Каждая последующая команда несёт его, и ядро перечитывает полномочия этого человека.

Код запуска одноразовый

Окно терминала
curl -X POST "https://cms.example/api/plugin/v1/launch/exchange" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp" \
-H "Content-Type: application/json" \
--data '{
"code": "dwl_ExampleLaunchCodeExampleLaunchCodeExampleLa"
}'
{
"outcome": {
"code": "invalid_request",
"message": "The launch code is unknown, used or expired.",
"retry": false
}
}

Обменять код вызова на связанный вход. Ядро отправляет POST $defs/invocation_envelope на объявленный endpoint операции или политики. Прежде чем ответить на этот POST, плагин отправляет сюда code из конверта со своим bearer-токеном. Код расходуется один раз. В ответ приходит связка, которую записало ядро: id операции или политики, необязательные модель и запись, отображаемая личность того, кто спросил, и связанный input — значения операции или для политики $defs/policy_facts. Связка операции над записью также несёт delegation_id: долговременное делегирование, привязанное к вызывающему, сайту, инсталляции, модели и записи. Храните его как делегирование запуска для последующих чтений и команд перехода этой записи; ядро перечитывает полномочия указанного в нём пользователя при каждом использовании. Связка операции сайта несёт делегирование, как запуск manage: без записи и с выданными моделями, которые читает вызывающий. Политика получает null. Ядро принимает ответ на свой POST только от инсталляции, которая обменяла этот вызов, и только пока она по-прежнему имеет необходимые для него права. Всё происходит в пределах 10 секунд.

  • Вызывающий: плагин со своим токеном инсталляции, пока отвечает на вызов
  • Требует: ничего
  • Тело — $defs/invocation_exchange_request. Без Idempotency-Key.
  • Отвечает: { "data": <binding> } — $defs/invocation_binding.

Исходы:

  • invalid_request — неизвестный, уже использованный, истёкший или выпущенный для другой инсталляции
  • capability_refused — инсталляцию сузили или её манифест изменился после запроса ядра
  • plugin_unauthorized
  • rate_limited
  • temporary_failure

Код вызова работает один раз и только для инсталляции, для которой выпущен

Окно терминала
curl -X POST "https://cms.example/api/plugin/v1/invocations/exchange" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp" \
-H "Content-Type: application/json" \
--data '{
"code": "dwi_ExampleInvocationCodeExampleInvocationCodeE"
}'
{
"outcome": {
"code": "invalid_request",
"message": "The invocation code is unknown, used or expired.",
"retry": false
}
}

Чтение того, какие модели сейчас доступны в рамках делегирования. model_ids делегирования, суженные до списка разрешённых плагину моделей и текущих прав пользователя, — это перечитывается при каждом запросе. Делегирование manage перечисляет каждую модель, которую пользователь всё ещё может читать: все записи или только свои при read:own. Делегирование действия над контентом перечисляет модель своего элемента, пока пользователь может читать эту запись. kinds называет виды переходов, которые человек может выполнить над элементами модели: у инсталляции есть workflow:publish / workflow:unpublish, человек может обновлять элементы, а в процессе модели есть переход этого вида, право на который у человека есть. entries говорит, верно ли это для каждого элемента. all: человек читает каждый элемент модели, и kinds действует для каждого. checked: человек читает или обновляет там только свои элементы, поэтому kinds — это максимум, который он может выполнить над элементом, который может обновлять; спросите /delegations/:delegationId/entries, какие элементы показывать и что разрешено для каждого. Используйте это, чтобы показывать только то, что человек может видеть, и предлагать перенос или отмену запланированного перехода, только когда его вид указан. Делегирование, открытое учётными данными управления, перечисляет только виды внутри своего потолка возможностей. Команды всё равно проверяются отдельно.

  • Вызывающий: плагин со своим токеном инсталляции
  • Требует: ничего
  • Ни параметров query, ни тела.
  • Отвечает: { "data": { "delegation_id", "models": [{ "id", "slug", "kinds", "entries" }] } } — $defs/delegation_models.

Исходы:

  • requester_unauthorized — делегирование неизвестно, принадлежит другой инсталляции, либо указанный в нём пользователь деактивирован или больше не является участником сайта
  • invalid_request
  • plugin_unauthorized
  • rate_limited
  • temporary_failure

Какие модели сейчас доступны в рамках делегирования

Окно терминала
curl "https://cms.example/api/plugin/v1/delegations/pld_0000000000000000000000000000d1ee" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp"
{
"data": {
"delegation_id": "pld_0000000000000000000000000000d1ee",
"models": [
{
"id": "cdocsmodel0000000000000001",
"slug": "article",
"kinds": [
"publish",
"unpublish"
],
"entries": "all"
}
]
}
}

Спрашивайте снова перед каждой страницей и действием. Человек, потерявший модель или право, теряет его и здесь.

Чтение того, какие записи сейчас доступны в рамках делегирования. Для моделей, которые /delegations/:delegationId помечает entries: "checked". Назовите до 100 id записей; ответ перечисляет только те, которые указанный в делегировании пользователь может читать сейчас, каждую с видами переходов, которые он может выполнить над ней: kinds модели, если он может обновлять эту запись, иначе ни одного. Запись другого сайта, запись в корзине, запись модели вне model_ids делегирования или текущего списка разрешённых моделей, запись вне группы переводов делегирования действия над контентом или запись, которую человек не может читать, пропускается без причины. Владение и права перечитываются при каждом запросе, поэтому спрашивайте снова перед каждой страницей и действием. Ответ предназначен только этому пользователю. Не показывайте записи, которые не вошли в ответ.

  • Вызывающий: плагин со своим токеном инсталляции
  • Требует: ничего
  • instance_id — от 1 до 100 раз, каждый — id записи. Повторы считаются один раз. Других параметров query и тела нет.
  • Отвечает: { "data": { "delegation_id", "entries": [{ "instance_id", "model_id", "kinds" }] } } — $defs/delegation_entries.

Исходы:

  • invalid_request — нет instance_id, их больше 100, один из них некорректен или передан другой параметр
  • requester_unauthorized — делегирование неизвестно, принадлежит другой инсталляции, либо указанный в нём пользователь деактивирован или больше не является участником сайта
  • plugin_unauthorized
  • rate_limited
  • temporary_failure

Какие записи сейчас доступны в рамках делегирования

Окно терминала
curl "https://cms.example/api/plugin/v1/delegations/pld_0000000000000000000000000000d1ee/entries?instance_id=cdocsinstance00000000000001&instance_id=cotherinstance0000000000001" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp"
{
"data": {
"delegation_id": "pld_0000000000000000000000000000d1ee",
"entries": [
{
"instance_id": "cdocsinstance00000000000001",
"model_id": "cdocsmodel0000000000000001",
"kinds": [
"publish",
"unpublish"
]
}
]
}
}

Запись другого сайта пропущена, причина не названа. Показывайте только перечисленные записи.

Создать запись. Создание сохранённой записи проходит обычную проверку через механизм жизненного цикла. Делегирование manage должно включать модель.

  • Вызывающий: плагин от имени пользователя, предоставившего делегирование
  • Требует: content:create и текущее право создания
  • Idempotency-Key обязателен.
  • Тело: $defs/content_create_command.
  • Ответ: content_saved с $defs/content_save_result.

Исходы:

  • content_saved / idempotent_replay
  • capability_refused / requester_unauthorized / content_not_found / validation_failed / idempotency_key_reused

Перенести запись в корзину. Обычный механизм жизненного цикла перемещает запись целиком в корзину. Сначала снять с публикации. Текущая версия проверяется атомарно.

  • Вызывающий: плагин от имени пользователя, предоставившего делегирование
  • Требует: content:delete и текущее право удаления
  • Idempotency-Key обязателен.
  • Тело: $defs/content_delete_command.
  • Ответ: content_deleted с обычным результатом переноса в корзину.

Исходы:

  • content_deleted / idempotent_replay
  • capability_refused / requester_unauthorized / content_not_found / version_conflict / validation_failed / idempotency_key_reused

Перечислить сохранённые версии. Ограниченная пагинация ID версий. Проверяется владение основной записью.

  • Вызывающий: плагин от имени пользователя, предоставившего делегирование
  • Требует: content:read и текущее право чтения истории
  • Query: delegation_id обязателен; limit, cursor необязательны.
  • Ответ: $defs/content_versions.

Исходы:

  • 200
  • capability_refused / requester_unauthorized / content_not_found / invalid_request

Прочитать сохранённую версию. Канонический снимок ограничен правами на поля, языки и владение. С include_dependencies=true ответ дополнительно содержит dependency_refs: [{ instance_id, locale }] и dependencies_incomplete. Учитываются обычные и языковые связи, связи внутри вложенных компонентов и встроенные общие секции. Действуют текущие права пользователя и разрешённые плагину модели. Идентификаторы недоступных записей не возвращаются; общий признак неполноты остаётся true. Этот список не даёт дополнительных прав на чтение или публикацию.

  • Вызывающий: плагин от имени пользователя, предоставившего делегирование
  • Требует: content:read и текущее право чтения истории
  • Query: delegation_id обязателен.
  • Необязательные параметры: include_dependencies=true и dependency_locale=<code>. Для второго нужен первый и доступный пользователю язык сайта. Без языка проверяются доступные языки версии.
  • Ответ: {data: <content_version>}; $defs/content_version.

Исходы:

  • 200
  • capability_refused / requester_unauthorized / content_not_found / invalid_request

Прочитать метаданные медиа. Общий сервис ресурсов читает медиа этого сайта.

  • Вызывающий: плагин от имени пользователя, предоставившего делегирование
  • Требует: media:read и текущее право чтения медиатеки
  • Query: delegation_id обязателен.
  • Ответ: {data: <media>} с метаданными представления.

Исходы:

  • 200
  • capability_refused / requester_unauthorized / content_not_found / invalid_request

Прочитать байты медиа. Общий обработчик чтения провайдера получает публичные и приватные файлы сайта. Для внешних ссылок локальные байты недоступны. Ответ с приватным файлом запрещает кеширование.

  • Вызывающий: плагин от имени пользователя, предоставившего делегирование
  • Требует: media:read и текущее право чтения медиатеки
  • Query: delegation_id обязателен.
  • Ответ: Файл с безопасными значениями MIME-типа и Content-Disposition.

Исходы:

  • 200
  • capability_refused / requester_unauthorized / content_not_found / limit_exceeded / temporary_failure

Изменить метаданные медиа. Общая проверка и атомарная идемпотентная запись.

  • Вызывающий: плагин от имени пользователя, предоставившего делегирование
  • Требует: media:update и текущее право media:update
  • Idempotency-Key обязателен.
  • Тело: $defs/media_update_command.
  • Ответ: media_updated, {id}.

Исходы:

  • media_updated / idempotent_replay
  • capability_refused / requester_unauthorized / content_not_found / validation_failed / idempotency_key_reused

Подготовка независимой будущей версии. Создаёт сохранённую ветку из текущего обычного черновика записи и связывает её с этой инсталляцией и релизом. Повтор для той же инсталляции, релиза и основной записи возвращает существующую ветку. Обычный черновик и значения EAV не меняются. У человека должны сохраняться права чтения и изменения записи; необязательный locale проверяет доступ к указанному языку. Имя обрезается по краям и ограничено 120 символами. Редактируйте ветку в Dee Wan по editor_url: у неё собственные ревизия и закреплённая версия. Зафиксировать ветку для выполнения может только команда публикации с действующими правами.

  • Вызывающий: плагин от имени человека с делегированными правами.
  • Требует: workflow:release и действующих делегированных прав редактирования.
  • Тело — $defs/future_create_command. Закрытое. Обязательны delegation_id, release_id, name; необязателен locale. Idempotency-Key не нужен.
  • Ответ: 201 { "data": <future with editor_url> } — $defs/future_created, в том числе при повторном обращении.

Исходы:

  • invalid_request
  • capability_refused / requester_unauthorized / content_not_found
  • validation_failed / version_conflict / transition_refused / temporary_failure

Чтение будущей версии этой инсталляции. Сайт, основная запись и инсталляция ветки должны совпадать с запросом. Ответ содержит состояние ветки, revision, current_version_id и сводку записи с учётом действующих прав; её current_version_id указывает на ветку. Непроверенные значения полей не возвращаются. Сводка workflow учитывает доступные переходы для будущей версии, включая разрешённую повторную публикацию. Для публикации укажите вместе future_draft_id и expected_future_revision, а в expected_current_version_id закрепите сохранённую версию ветки. Перед фиксацией и публикацией ядро проверяет обязательные поля, задачи согласования, workflow, связи и медиа. Обычный черновик не меняется. Если результат команды неизвестен, повторите исходную команду с теми же версией и ключом.

  • Вызывающий: плагин от имени человека с делегированными правами.
  • Требует: workflow:release и действующих делегированных прав чтения.
  • Параметр запроса: обязательный delegation_id — $defs/future_read_query.
  • Ответ: 200 { "data": { "future": <future>, "summary": <content_summary> } } — $defs/future_read.

Исходы:

  • invalid_request
  • capability_refused / requester_unauthorized / content_not_found

Применить согласованный релиз. Планирует конечное состояние не более 25 команд одного сайта. Проверяет права, доступ к моделям, workflow, версии, языки и зависимости. Данные публикации, события workflow и записи повтора применяются одним защищённым пакетом в базе сайта или не применяются вовсе. Разные языки одной группы допустимы; пересекающиеся области отклоняются. Доставка и автоматические перенаправления выполняются после коммита.

  • Вызывающий: плагин от имени делегирующего человека
  • Требуется: workflow:release
  • Требуется Idempotency-Key. Тело: $defs/release_command.
  • Ответ: release_applied или idempotent_replay с $defs/release_result.

Исходы:

  • operation_applied или документированный результат сохранения или релиза
  • capability_refused, requester_unauthorized, validation_failed, version_conflict, limit_exceeded, temporary_failure

Читать комментарии посетителей. Использует существующее чтение для модерации и текущую видимость моделей и записей.

  • Вызывающий: плагин от имени делегирующего человека
  • Требуется: comments:read
  • Query: delegation_id и существующие фильтры списка комментариев API управления.
  • Ответ: Ограниченный список комментариев без приватных адресов электронной почты.

Исходы:

  • operation_applied или документированный результат сохранения или релиза
  • capability_refused, requester_unauthorized, validation_failed, version_conflict, limit_exceeded, temporary_failure

Модерировать комментарии посетителей. Использует обычный сервис модерации. Не создаёт вымышленное согласие; устаревшее состояние отклоняется отдельно для каждого комментария.

  • Вызывающий: плагин от имени делегирующего человека
  • Требуется: comments:moderate
  • Требуется Idempotency-Key. Тело: delegation_id, to, comments: [{id, expected}].
  • Ответ: Обычный результат модерации.

Исходы:

  • operation_applied или документированный результат сохранения или релиза
  • capability_refused, requester_unauthorized, validation_failed, version_conflict, limit_exceeded, temporary_failure

Читать перенаправления. Требует текущего права site:settings.

  • Вызывающий: плагин от имени делегирующего человека
  • Требуется: redirects:read
  • Query: delegation_id.
  • Ответ: Только принятые revision и redirects.

Исходы:

  • operation_applied или документированный результат сохранения или релиза
  • capability_refused, requester_unauthorized, validation_failed, version_conflict, limit_exceeded, temporary_failure

Проверить шаблоны перенаправлений. Использует ту же проверку шаблонов по текущим адресам, что и админ-интерфейс.

  • Вызывающий: плагин от имени делегирующего человека
  • Требуется: redirects:write
  • Тело: delegation_id, redirects.
  • Ответ: Результат проверки шаблонов и digest, необходимый для сохранения.

Исходы:

  • operation_applied или документированный результат сохранения или релиза
  • capability_refused, requester_unauthorized, validation_failed, version_conflict, limit_exceeded, temporary_failure

Изменить перенаправления. Принимает только изменения перенаправлений. Действуют обычные проверки ревизии, конфликтов путей, циклов и подтверждения шаблонов.

  • Вызывающий: плагин от имени делегирующего человека
  • Требуется: redirects:write
  • Требуется Idempotency-Key. Тело: delegation_id, expected_revision, redirects, необязательный redirect_review_digest.
  • Ответ: Только принятые revision и redirects.

Исходы:

  • operation_applied или документированный результат сохранения или релиза
  • capability_refused, requester_unauthorized, validation_failed, version_conflict, limit_exceeded, temporary_failure

Восстановить запись из корзины. Восстанавливает черновик через обычные проверки жизненного цикла, адресов, прав на поля и языки, а также владения.

  • Вызывающий: плагин от имени делегирующего человека
  • Требуется: content:restore
  • Требуется Idempotency-Key. Тело: delegation_id.
  • Ответ: Обычный результат восстановления из корзины.

Исходы:

  • operation_applied или документированный результат сохранения или релиза
  • capability_refused, requester_unauthorized, validation_failed, version_conflict, limit_exceeded, temporary_failure

Удалить неиспользуемые медиа. Требует текущего права media:delete. Текущий контент, сохранённая история и конкурентно добавленные ссылки не дают удалить используемый файл. Очистка хранилища выполняется отдельно.

  • Вызывающий: плагин от имени делегирующего человека
  • Требуется: media:delete
  • Требуется Idempotency-Key. Тело: delegation_id.
  • Ответ: Обычный результат удаления медиа и состояние очистки.

Исходы:

  • operation_applied или документированный результат сохранения или релиза
  • capability_refused, requester_unauthorized, validation_failed, version_conflict, limit_exceeded, temporary_failure

Отправить одобренный шаблон письма. Требуются текущее право site:plugins и шаблон, проверенный администратором. Для заданных адресов нужно делегирование manage без instance_id. Для entry_author и entry_assignee передайте instance_id: запись должна входить в доступные инсталляции и делегированию модели и записи, а инициатор должен по-прежнему её видеть. Подходит делегирование manage или действия над соответствующей записью. entry_author выбирает аккаунт, создавший выбранную запись в CMS (content_instance.created_by_id), а не публичного автора блога, связанный профиль или последнего редактора. entry_assignee выбирает текущего назначенного пользователя из процесса согласования (content_workflow.assigned_to_id). Аккаунт получателя должен быть активным, иметь текущий доступ к сайту и право читать запись; активному суперадминистратору отдельная роль на сайте не нужна. Если подходящего аккаунта нет, возвращается validation_failed.

Плагин не задаёт адрес, идентификатор аккаунта, правило выбора получателя, тему, текст или учётные данные провайдера. Ответ не раскрывает адрес. Тело запроса ограничено 16384 байтами; допускается до 16 объявленных подстановок по 1000 символов. После подстановки тема должна содержать 1–200 символов без переноса строки, текст — до 12000 символов. Превышение отклоняется до сохранения записи повтора.

Принятая команда сохраняет письмо и аккаунт получателя. Повтор с тем же ключом использует этот снимок, даже после изменения автора, назначения или шаблона. Снова проверяются доступ инициатора и исходного получателя к записи, активность аккаунта и неизменность адреса. Другая запись или другие подстановки с тем же ключом отклоняются. Идентификатор сообщения при повторе сохраняется; неподтверждённый ответ провайдера допускает повтор.

  • Вызывающий: плагин от имени делегирующего человека
  • Требуется: mail:send
  • Требуется Idempotency-Key. Тело: $defs/mail_command.
  • Ответ: operation_applied или idempotent_replay, {sent:true}. Это подтверждает приём провайдером, а не доставку во входящие.

Исходы:

  • operation_applied или документированный результат сохранения или релиза
  • capability_refused, requester_unauthorized, validation_failed, version_conflict, limit_exceeded, temporary_failure

Сохранить метаданные рендерера. Записывает только объявленные дополнения этой инсталляции для одного языка как обычную новую сохранённую версию. Публикация выполняется отдельно. Канонические SEO-значения, проверка сайта и языковые ссылки ядра имеют приоритет; скрипт или вызов CMS во время работы сайта не добавляются.

  • Вызывающий: плагин от имени делегирующего человека
  • Требуется: renderer:metadata
  • Требуется Idempotency-Key. Тело: $defs/renderer_metadata_command.
  • Ответ: Обычный результат content_saved.

Исходы:

  • operation_applied или документированный результат сохранения или релиза
  • capability_refused, requester_unauthorized, validation_failed, version_conflict, limit_exceeded, temporary_failure

Ответы на команды и все отказы содержат объект outcome с машинным кодом и сообщением для пользователя. Успешные чтения, /launch/exchange и /invocations/exchange этого не делают: они отвечают { "data": … } (и "next" на странице списка) без outcome. Три отказа тоже несут data: validation_failed называет правило ($defs/validation_detail), draft_conflict несёт текущий черновик в data.current, а media_duplicate несёт существующий файл и число его копий ($defs/media_duplicate).

{
"outcome": {
"code": "transition_applied",
"message": "…",
"retry": false
},
"data": {}
}

Выбирайте действие по code только после проверки кода, HTTP-статуса, флага retry и строкового message. Текст сообщения предназначен для человека и не стабилен. Один флаг retry не делает ответ достоверным.

HTTP Код Повтор Значение
200 release_applied нет Релиз применён атомарно.
200 operation_applied нет Операция применена.
200 transition_applied нет Переход был применён.
200 idempotent_replay нет Эта команда уже была применена. Возвращается первый результат.
200 content_saved нет Значения полей сохранены как новая версия.
200 draft_saved нет Рабочий черновик сохранён.
200 draft_discarded нет Рабочий черновик удалён.
200 media_uploaded нет Медиа загружено.
200 media_reused нет Сайт уже хранит этот файл. Возвращено существующее медиа, ничего не загружено.
200 proposal_created нет Предложение создано. Ничего не меняется, пока человек его не применит.
400 invalid_request нет Запрос недействителен для протокола v1.
401 plugin_unauthorized нет Токен инсталляции отсутствует, неизвестен, отключён или отозван.
403 capability_refused нет Этой инсталляции не была выдана эта возможность или модель.
403 requester_unauthorized нет У человека, который это запросил, больше нет нужных для этого полномочий.
404 content_not_found нет Такого контента нет в области действия этой инсталляции.
409 scheduled_target_stale нет Закреплённая версия больше не та, которую переместит этот переход.
409 workflow_conflict нет Контент прошёл через свой workflow после того, как это было запрошено.
409 dependent_content нет От этого элемента зависит другой контент. Человек должен проверить снятие с публикации в Dee Wan.
409 idempotency_key_reused нет Этот ключ идемпотентности уже использовался для другой команды.
409 version_conflict нет Запись изменилась после закреплённой версии. Прочитайте её заново перед записью.
409 media_duplicate нет Сайт уже хранит этот файл. Ничего не загружено. Отправьте on_duplicate, чтобы использовать его или загрузить копию.
409 draft_conflict нет Рабочий черновик изменился после закреплённой ревизии. Прочитайте его заново перед записью.
413 limit_exceeded нет Запрос превышает лимит размера или количества.
422 transition_unavailable нет Этот переход недоступен из текущего состояния контента.
422 transition_refused нет Жизненный цикл отклонил этот переход. Ничего не изменено.
422 validation_failed нет Правила контента отклонили значения. Ничего не изменено.
429 rate_limited да Слишком много запросов для этой инсталляции.
503 temporary_failure да Dee Wan не смог выполнить запрос. Неизвестно, были ли внесены какие-либо изменения.
  • Сначала проверяйте структурированный outcome; только при его отсутствии определяйте результат по HTTP-статусу. Известный code, HTTP-статус и логический retry должны соответствовать таблице, а message должен быть строкой. При несовпадении или некорректном поле outcome возвращается untrusted_response без повтора, даже если HTTP-статус обычно допускает повтор.
  • Корректные rate_limited (429) и temporary_failure (503) допускают повтор. Ядро выдаёт temporary_failure только с 503, но не с 500, 502 или 504.
  • Входящие 429/500/502/503/504 без члена outcome допускают повтор: в том числе пустые тела, не-JSON и ошибки посредников. При любом другом статусе ответ на команду без достоверного outcome приводит к untrusted_response без повтора. Перенаправления не выполняются.
  • Сетевые ошибки, таймауты и ошибки чтения ответа повторяемы как no_response. Они не доказывают отсутствие эффекта. Если сохранённый токен нельзя прочитать, возвращается token_unreadable без повтора.
  • Соблюдайте Retry-After для повторяемых ответов: целые неотрицательные секунды или каноническая HTTP-дата (IMF-fixdate). Прошедшая дата означает нулевую задержку. Некорректное значение использует ограниченный экспоненциальный backoff с джиттером. Ожидание выше максимума, лимит попыток или возраста завершаются с retry_exhausted.
  • Достоверный успех — код 200: release_applied, operation_applied, transition_applied, idempotent_replay, content_deleted, media_updated, content_saved, draft_saved, draft_discarded, media_uploaded, media_reused, proposal_created. Любой другой корректный неповторяемый исход — терминальный отказ под своим кодом.
  • При повторе или возобновлении работы после восстановления блокировки используйте точно те же тело и Idempotency-Key. Запросы могут повторяться. Их эффект должен возникать не более одного раза в пределах хранения ключа. Команду запускает только текущий владелец блокировки; срок запроса должен истечь раньше срока блокировки. Не отправляйте команду параллельно намеренно. Таймаут не доказывает остановку удалённого запроса.
  • Держите лимит возраста намного ниже 30 дней: после срока хранения ядро забывает ключ. Исчерпание лимитов прекращает отправки и оставляет неподтверждённый эффект неизвестным. Перед созданием нового расписания с новым ключом проверьте контент.
  • Эти правила относятся к повторам команд. Успешное чтение всё равно требует документированной формы данных. /launch/exchange расходует одноразовый код и после потери ответа не допускает автоматического повтора.

Эти маршруты для браузера требуют пользовательской сессии и заголовка сайта. Установка, выдача прав, смена токена, удаление и настройка шаблонов писем требуют site:plugins. Именованные представления вместо этого проверяют объявленные текущие права на модели и системные действия. Токен плагина не даёт доступа к этим маршрутам; они описаны, чтобы автор понимал интерфейс хоста. Каждый из них, выполняющий мутацию, пишет запись аудита и отказывает (503 audit_unavailable), а не выполняет мутацию без аудита. Отказ отвечает { "error": <text>, "reason": <code> }. Ветвитесь по reason, а не по error. missing_permission также называет право в required.

Список действий установленных плагинов для назначения ролям. Идентификатор действия включает пространство имён плагина. Удалённые или необъявленные действия не дают права на выполнение.

  • Кто вызывает: пользователь с правом управления ролями.
  • Право: role:manage.
  • Ответ: { "data": [{ "plugin_id", "plugin_name", "action_id", "action", "label" }] }.
  • Коды ответа: 200, 403.

Список доступных рабочих областей плагинов. Навигация, размещение на панели управления и прямые маршруты используют одну текущую проверку прав. Фреймы изолированы от origin админ-интерфейса.

  • Вызывающий: вошедший пользователь
  • Требуется: текущие объявленные права представления и доступные для чтения разрешённые модели
  • Ответ: Доступные именованные представления и проверенные объявления фреймов.

Исходы:

  • 200

Открыть доступную рабочую область плагина. Недоступное, отключённое, неразрешённое или нечитаемое представление возвращает not_found.

  • Вызывающий: вошедший пользователь
  • Требуется: те же текущие права, что и для списка
  • Ответ: Одно проверенное представление.

Исходы:

  • 200
  • 404

Проверить шаблоны писем и получателей. Принимает только объявленные идентификаторы шаблонов и подстановки. Одобренная конфигурация сохраняется отдельно для инсталляции и записывается в аудит.

  • Вызывающий: администратор сайта
  • Требуется: site:plugins
  • Соответствие объявленного id шаблона и {recipient_source?, recipients, subject, text}: до 8 шаблонов. Отсутствующее значение recipient_source или fixed требует 1–8 одобренных адресов. Для entry_author и entry_assignee адресов быть не должно (recipients: []). Тема — 1–200 символов без переноса строки, текст — 1–12000 символов.
  • Ответ: Одобренная конфигурация шаблонов.

Исходы:

  • 200
  • 403
  • 409
  • 422

Список инсталляций. Каждая неотозванная инсталляция на сайте с исключёнными секретами: без токена и его хеша.

  • Вызывающий: пользователь с site:plugins
  • Требует: site:plugins
  • Нет тела.
  • Отвечает: { "data": [<installation>] } — $defs/installation.

Исходы:

  • 200
  • 403 missing_permission

Модели, которые можно выдать. Id, имя и slug каждой модели контента на сайте, для экрана настройки доступа. Настройки доступа хранят id моделей, поэтому переименование не меняет доступ, а после удаления модели соответствующее разрешение перестаёт действовать.

  • Вызывающий: пользователь с site:plugins
  • Требует: site:plugins
  • Нет тела.
  • Отвечает: { "data": [{ "id", "name", "slug" }] }

Исходы:

  • 200
  • 403 missing_permission

Прочитать каталоги плагинов. Загружает каждый URL каталога, который оператор задал в PLUGIN_CATALOGUE_URLS, по тем же правилам URL и с тем же отказом от редиректов, что и для манифеста. Ничего не сохраняется и ничему не доверяется: запись только даёт URL манифеста, а установка из неё — это обычные действия «Проверить» и «Установить», которые снова загружают и проверяют манифест. Страница каталога — { "catalogue_version": 1, "plugins": [<entry>], "next"?: "https://…" }. Запись — { "id", "name", "manifest_url", "summary"?, "categories"?, "homepage"?, "icon"? }, без других ключей. id следует правилу id манифеста. name — не длиннее 64 символов, summary — не длиннее 280. categories содержит не больше 5 slug в нижнем регистре. Каждый URL проходит правила URL плагинов. next должен оставаться на origin первой страницы. Пределы на каталог: 5 страниц, 128 KiB на страницу, 100 записей на страницу и 200 всего. Читается не больше 4 каталогов. Каталог, нарушивший правило, исключается целиком и называется в catalogues. Неверная запись пропускается и называется в problems. Если у двух записей один id, побеждает первая: по порядку каталогов в настройке, затем страниц, затем записей. Сбой каталога никогда не меняет установленные плагины и прямую установку.

  • Вызывающий: пользователь с site:plugins
  • Требует: site:plugins
  • Нет тела.
  • Отвечает: { "data": { "configured", "entries": [{ "id", "name", "summary", "categories", "manifest_url", "homepage", "icon", "catalogue" }], "catalogues": [{ "catalogue", "origin", "ok", "entries" | "reason" }], "problems": [{ "catalogue", "index", "plugin_id", "reason", "fields" }] } }

Исходы:

  • 200 — также когда каталог не настроен (configured: false) и когда все каталоги не прочитаны. reason каталога — url_invalid, url_not_https, url_credentials, url_fragment, url_private_destination, remote_redirect_refused, remote_body_too_large, remote_unreachable, catalogue_unavailable, catalogue_invalid, catalogue_next_refused, catalogue_too_many_pages, catalogue_too_many_entries или too_many_catalogues. reason проблемы — entry_invalid или entry_duplicate
  • 403 missing_permission

Действия над контентом для отображения. Ограниченные пары { id, label }, которые объявляют активные инсталляции, чтобы экран контента мог отрисовать кнопку, принадлежащую ядру. Никакая разметка, скрипт, CSS или URL иконки никогда не приходят от плагина. С instance_id перечисляются только инсталляции, которым разрешена модель этой записи, а для неизвестного элемента не перечисляется ни одна.

  • Вызывающий: любой участник сайта
  • Требует: ничего, кроме доступа к сайту
  • instance_id (необязательный параметр query) — элемент, для которого предназначены кнопки. Нет тела.
  • Отвечает: { "data": [{ "installation_id", "plugin_name", "actions": [{ "id", "label" }] }] }

Исходы:

  • 200

Карточки боковой панели для отображения. Карточки entry_sidebar или page_sidebar, которые активные инсталляции объявляют в ui_extensions. Ядро само рисует каждую карточку по её заголовку и описанию: от плагина не приходит ни разметка, ни скрипт, ни CSS, ни фрейм, ни URL. Карточка запускается через маршрут запуска ниже. Карточка действия над контентом показывается только для элемента, к модели которого плагину предоставлен доступ. Карточка страницы управления показывается для такого элемента или пользователю с site:plugins. Для неизвестного элемента не показывается ни одна.

  • Вызывающий: любой участник сайта
  • Требует: ничего, кроме доступа к сайту
  • placement (параметр query) — entry_sidebar или page_sidebar. instance_id (необязательный параметр query) — элемент на экране. Нет тела.
  • Отвечает: { "data": [<plugin_card>] } — $defs/plugin_card.

Исходы:

  • 200
  • 400 invalid_placement

Операции, которые человек может выполнить. operations, которые объявляют активные инсталляции и которые этот человек может выполнить сейчас. Без instance_id: операции сайта (им нужно site:plugins) и операции над записями в моделях, которые человек читает. С instance_id: операции этой записи. Операция, для которой плагину не предоставлены возможности из requires, не показывается, как и операция automation или столбец списка контента. Endpoint не показывается никогда.

  • Вызывающий: любой участник сайта
  • Требует: ничего, кроме доступа к сайту
  • instance_id (необязательный параметр query) — запись на экране. Нет тела.
  • Отвечает: { "data": [{ installation_id, plugin_id, plugin_name, operation }] }.

Исходы:

  • 200

Выполнить операцию. Ядро проверяет значения по объявленному входу, записывает вызов, отправляет конверт на объявленный endpoint и ждёт обмена и ответа. Завершённый ответ проверяется по $defs/operation_result и объявленным компонентам результата и пишется в аудит по хешу. Точный повтор с тем же Idempotency-Key и телом возвращает первый результат с replayed: true.

  • Вызывающий: человек, которому операция показана
  • Требует: собственное правило операции: site:plugins для операции сайта, чтение записи для операции над записью, изменение — для разрушающей
  • Тело — { "instance_id"?: string, "input"?: { … } }. instance_id обязателен для операции над записью и отклоняется для операции сайта.
  • Idempotency-Key — обязателен.
  • Отвечает: { "data": { invocation_id, installation_id, plugin_id, plugin_name, operation, instance_id, outcome: "completed", result } }.

Исходы:

  • 400 idempotency_key_required, malformed_body, invalid_request, input_invalid (с issues), input_too_large
  • 404 operation_unavailable — неизвестна, это операция automation, столбец списка контента или сейчас этому человеку не предлагается
  • 409 idempotency_key_reused, invocation_in_progress, plugin_unavailable
  • 422 plugin_refused — собственный refusal: { code, message } плагина
  • 502 plugin_unreachable, plugin_response_invalid
  • 503 audit_unavailable
  • 504 plugin_timeout

Столбцы списка контента для отрисовки. Операции page, которые активные инсталляции объявляют для одной модели: scope: "page", audience: "column" и вид cell. Ядро добавляет в список контента этой модели столбец с заголовком — названием операции. Столбец показывается, только пока инсталляция активна, ей разрешён доступ к модели, её requires выданы и человек читает модель. Область действия и список моделей учётных данных ограничивают доступ к столбцу так же, как к другим элементам интерфейса плагина.

  • Вызывающий: любой участник сайта, который может читать модель
  • Требует: ничего сверх чтения модели
  • model_id (query) — обязателен. Без тела.
  • Отвечает: { "data": [{ installation_id, plugin_name, operation, header, cell }] }. cell — text, badge или link.

Исходы:

  • 200
  • 400 model_id_required

Вычислить один столбец для страницы строк. Ядро оставляет строки, которые человек может читать сейчас (не более 50, в названной модели, не в корзине), и запускает операцию page через единый транспорт вызовов со входом $defs/column_request: модель и идентификаторы этих строк, без значений. Плагин отвечает одним блоком cells не более чем с одной ячейкой на каждую строку, о которой его спросили. Ячейка для другой строки, повторная строка, ссылка без https-url, url или tone у вида, которому они не положены, или текст длиннее 80 символов делают весь ответ plugin_response_invalid. Столбец — чтение: он не создаёт делегирования, не хранит повторяемый ответ и не пишет строку аудита.

  • Вызывающий: человек, который читает модель
  • Требует: ничего сверх чтения модели
  • Тело — { "model_id": string, "instance_ids": string[1..50] }. Закрытое.
  • Отвечает: { "data": { installation_id, plugin_name, operation, header, cell, cells: [{ id, text, tone?, url? }] } }. cells пуст, когда человек не читает ни одной строки или плагину нечего показать.

Исходы:

  • 400 malformed_body, invalid_request
  • 404 operation_unavailable — неизвестна, не столбец, сейчас этому человеку не предлагается
  • 409 plugin_unavailable
  • 422 plugin_refused
  • 502 plugin_unreachable, plugin_response_invalid
  • 504 plugin_timeout

Действия над выбранными строками. Интерактивные операции entry с list_action: true, которые активные инсталляции объявляют для одной модели. Ядро предлагает их для строк, выбранных человеком в списке контента. Разрушающая операция показывается только пользователям с правом изменять записи модели. Endpoint никогда не перечисляется.

  • Вызывающий: любой участник сайта, который может читать модель
  • Требует: ничего сверх чтения модели
  • model_id (query) — обязателен. Без тела.
  • Отвечает: { "data": [{ installation_id, plugin_id, plugin_name, operation }] }.

Исходы:

  • 200
  • 400 model_id_required

Выполнить действие над выбранными строками. Ядро запускает операцию по одному разу для каждой названной строки, тем же сервисом, что и Run an operation, поэтому каждая строка проверяется по текущему доступу человека, сверяется с объявленным входом, пишется в аудит и получает свой ответ. Не более 25 различных строк, по несколько за раз. Отказ по одной строке не останавливает остальные. Ключ идемпотентности каждой строки выводится из Idempotency-Key запроса и идентификатора строки, поэтому повтор всего запроса воспроизводит уже выполненные строки и выполняет остальные.

  • Вызывающий: человек, которому операция показана, по каждой строке
  • Требует: собственное правило операции для каждой строки: чтение записи, изменение — для разрушающей
  • Тело — { "instance_ids": string[1..25], "input"?: { … } }. Закрытое. Один и тот же input применяется ко всем строкам.
  • Idempotency-Key — обязателен.
  • Отвечает: { "data": { "results": [<item>], "summary": { completed, failed } } }. Элемент — { instance_id, ok: true, run }, где run — запуск операции из Run an operation, или { instance_id, ok: false, status, reason, error, refusal? } с причиной, которую этот маршрут вернул бы для этой строки.

Исходы:

  • 200 — что бы ни произошло с каждой строкой
  • 400 idempotency_key_required, malformed_body, invalid_request
  • 404 operation_unavailable — неизвестна или не действие над строками

Редакторы полей для отображения. Каждая активная или отключённая инсталляция с расширениями field_editor, которые объявляет её сохранённый манифест. Ядро рисует редактор плагина только для активной инсталляции и только если поле ему по-прежнему подходит. Иначе поле сохраняет значение, а ядро рисует свой встроенный редактор, называет плагин и объясняет, почему его редактор недоступен. Поле модели указывает редактор в ui.pluginEditor ({ "plugin": "<plugin id>", "extension": "<extension id>" }). Сохранение модели, которое добавляет или меняет эту ссылку, отклоняется, если редактор не объявлен активной инсталляцией или поле не несёт его сохраняемый тип, правила и варианты (400 plugin_editor_not_installed, plugin_editor_not_active, plugin_editor_not_found, plugin_editor_type_mismatch, plugin_editor_rules_mismatch, plugin_editor_options_mismatch или plugin_editor_invalid). Неизменённая ссылка сохраняется после отключения или удаления её плагина.

  • Вызывающий: любой участник сайта
  • Требует: ничего, кроме доступа к сайту
  • Нет тела.
  • Отвечает: { "data": [<field_editor_source>] } — $defs/field_editor_source.

Исходы:

  • 200

Блоки форматированного текста для предложения. Каждая активная или отключённая инсталляция, чей сохранённый манифест объявляет rich_blocks, с каждым определением в пространстве имён <plugin id>/<name>. Редактор форматированного текста предлагает блоки активной инсталляции в меню вставки, сам рисует их поля и показывает их превью по объявленному дереву render. Сохранённый блок сохраняет свой тип, версию и значения, когда его плагин отключён или удалён; редактор показывает его как сохранённый, но не зарегистрированный, а рендереры ничего не показывают для него на публичной странице. Сохранение контента, которое добавляет или меняет блок плагина, отклоняется, если активная инсталляция не объявляет его тип (422 rich_block_unregistered), не знает его версию (rich_block_version_unknown) или не принимает его значения после объявленных миграций (rich_block_values_invalid). Неизменённый блок сохраняется независимо от того, что зарегистрировано.

  • Вызывающий: любой участник сайта
  • Требует: ничего, кроме доступа к сайту
  • Нет тела.
  • Отвечает: { "data": [<rich_block_source>] } — $defs/rich_block_source.

Исходы:

  • 200

Редакционные представления, которые этот человек может открыть. Каждая инсталляция, которая не удалена и чей сохранённый манифест объявляет представление управления (calendar, releases), с объявленными ею представлениями и её состоянием для этого человека: available (активна, и человек читает хотя бы одну модель, доступную плагину, включая только свои записи), no_readable_models (человек не читает ни одной модели, доступной плагину), disabled или unavailable (ещё не активна). Пустой список означает, что такой плагин не установлен. Больше ничего о предоставленном доступе не показывается.

  • Вызывающий: любой участник сайта
  • Требует: ничего, кроме доступа к сайту
  • Ни параметров query, ни тела.
  • Отвечает: { "data": [{ "installation_id", "plugin_name", "views", "state" }] }

Исходы:

  • 200

Получение и валидация манифеста. Получает URL манифеста, валидирует его и возвращает то, что он запрашивает. Ничего не сохраняется, и после этого никакой инсталляции не существует. Это шаг проверки, который делает одобрение осознанным.

  • Вызывающий: пользователь с site:plugins
  • Требует: site:plugins
  • Тело — { "manifest_url": "https://…" }
  • Отвечает: { "data": { "manifest": <manifest>, "protocol_version": 1, "review_digest": "<sha256>" } }

Исходы:

  • 200
  • 400 malformed_body / причина манифеста — manifest_invalid, protocol_unsupported, manifest_origin_mismatch, manifest_unavailable, url_invalid, url_not_https, url_credentials, url_fragment, url_private_destination, remote_redirect_refused, remote_body_too_large, remote_unreachable
  • 403 missing_permission

Установка. Повторно получает и повторно валидирует манифест, записывает ожидающую инсталляцию с одобренными возможностями и id моделей, выпускает токен и передаёт сырой токен вместе с одноразовым кодом активации на activation_url манифеста. Инсталляция становится active только после 2xx от плагина. Ядро хранит хеш и никогда не хранит токен. Неудачная активация ничего не оставляет после себя, а вторая установка того же плагина на тот же сайт — это конфликт, а не вторая инсталляция.

  • Вызывающий: пользователь с site:plugins
  • Требует: site:plugins
  • Тело — { "manifest_url", "activation_code", "capabilities": […], "model_ids": […] }. Необязательный expected_review_digest закрепляет URL и манифест из результата проверки. Изменившийся манифест отклоняется до передачи токена. Возможности должны быть подмножеством запроса манифеста. Пустой model_ids означает отсутствие доступа к контенту, а не все модели.
  • Отвечает: { "data": <installation> }, 201.

Исходы:

  • 201
  • 400 malformed_body / invalid_activation_code / invalid_capabilities / invalid_model_ids / причина манифеста
  • 403 missing_permission
  • 409 installation_exists / backend_url_unset / activation_superseded / manifest_changed_since_review
  • 502 с reason — activation_refused (ответ не 2xx), url_invalid, url_not_https, url_credentials, url_fragment, url_private_destination, remote_redirect_refused, remote_body_too_large, remote_unreachable — плагин отказал или был недоступен. Ничего не установлено
  • 503 audit_unavailable

Изменение доступа. Заменяет возможности и id моделей. Выбирать можно только возможности, запрошенные сохранённым манифестом; возможность вне этого списка отклоняется. Изменение манифеста само по себе не расширяет предоставленный доступ. Сокращение вступает в силу при следующем запросе плагина.

  • Вызывающий: пользователь с site:plugins
  • Требует: site:plugins
  • Тело — { "capabilities": […], "model_ids": […] }
  • Отвечает: { "data": <installation> }

Исходы:

  • 200
  • 400 malformed_body / invalid_capabilities / invalid_model_ids
  • 403 missing_permission
  • 404 not_found
  • 409 installation_state_changed
  • 503 audit_unavailable

Настройка реакции на сбой политики контента. Ставит одной объявленной политике warn (по умолчанию: тайм-аут, сбой или неверный ответ пропускают запись с предупреждением) или required (те же сбои отклоняют запись). Настоящий refuse отклоняет всегда. expected_mode — режим, который видел администратор; другое изменение в промежутке или принятое обновление — конфликт.

  • Вызывающий: пользователь с site:plugins
  • Требует: site:plugins
  • Тело — { "mode": "required", "expected_mode": "warn" }
  • Отвечает: { "data": <installation> }

Исходы:

  • 200
  • 400 malformed_body
  • 403 missing_permission
  • 404 not_found / policy_not_found
  • 409 policy_mode_changed с текущим mode
  • 503 audit_unavailable

Включение и выключение хука перед операцией. Ставит одному объявленному хуку on или off. Объявленный хук off, пока администратор его не включит. Пока он on, хук upload_metadata спрашивается перед приёмом нового файла библиотеки, а comment_moderation — перед сохранением комментария посетителя; хук, который не смог ответить, отклоняет загрузку или оставляет комментарий на модерации и никогда не одобряет. expected_mode — состояние, которое видел администратор; другое изменение в промежутке или принятое обновление — конфликт.

  • Вызывающий: пользователь с site:plugins
  • Требует: site:plugins
  • Тело — { "mode": "on", "expected_mode": "off" }
  • Отвечает: { "data": <installation> }

Исходы:

  • 200
  • 400 malformed_body
  • 403 missing_permission
  • 404 not_found / hook_not_found
  • 409 hook_mode_changed с текущим mode
  • 503 audit_unavailable

Принятие новой версии манифеста. Повторно получает сохранённый URL манифеста и принимает релиз, который администратор только что проверил, — expected_version должен совпадать с тем, что отдаётся, иначе проверка устарела. Id плагина, origin и принятая версия протокола не должны были измениться. Возможности ПЕРЕСЕКАЮТСЯ с новым запросом. Обновление никогда не расширяет предоставленный доступ. Релиз, который меняет принятую версию протокола, не может быть принят: удалите его и установите заново. Политика, которую новый релиз больше не объявляет, теряет свой режим; новая политика начинает с warn.

  • Вызывающий: пользователь с site:plugins
  • Требует: site:plugins
  • Тело — { "expected_version": "0.2.0" }
  • Отвечает: { "data": <installation> }

Исходы:

  • 200
  • 400 malformed_body / причина манифеста
  • 403 missing_permission
  • 404 not_found
  • 409 manifest_identity_changed / protocol_changed / manifest_changed_since_review / installation_state_changed
  • 503 audit_unavailable

Ротация токена. Требует нового одноразового кода от плагина, потому что ротация — это передача, а не сброс. Ядро выпускает токен, отправляет его на закреплённый origin активации и только после этого заменяет хеш. Если плагин отказывает, старый токен продолжает работать. Предыдущий хеш остаётся действительным не более 10 минут, а первый запрос с НОВЫМ токеном немедленно завершает период совместного действия токенов: с этого момента старый токен отвечает 401 plugin_unauthorized. Плагин с несколькими воркерами должен переключить их все вместе или заставить воркер один раз перечитать сохранённый токен, когда он получает 401 сразу после ротации.

  • Вызывающий: пользователь с site:plugins
  • Требует: site:plugins
  • Тело — { "activation_code": "…" }
  • Отвечает: { "data": { "rotated": true } }

Исходы:

  • 200
  • 400 invalid_activation_code (также для тела, которое не является JSON-объектом)
  • 403 missing_permission
  • 404 not_found
  • 409 installation_not_active / backend_url_unset / installation_state_changed
  • 502 с reason — activation_refused (ответ не 2xx), url_invalid, url_not_https, url_credentials, url_fragment, url_private_destination, remote_redirect_refused, remote_body_too_large, remote_unreachable — и СТАРЫЙ токен продолжает работать
  • 503 audit_unavailable

Отключение. Сохраняет конфигурацию и журнал аудита и полностью останавливает API плагина: каждый запрос отвечает plugin_unauthorized до какого-либо доступа к арендатору. Карточки плагина исчезают, а поля, использующие его редакторы, сохраняют значения и показываются во встроенном редакторе.

  • Вызывающий: пользователь с site:plugins
  • Требует: site:plugins
  • Нет тела.
  • Отвечает: { "data": { "id", "state": "disabled" } }

Исходы:

  • 200
  • 403 missing_permission
  • 404 not_found
  • 409 installation_state_changed
  • 503 audit_unavailable

Включение. Возвращает отключённую инсталляцию в active, с прежним токеном и правами доступа.

  • Вызывающий: пользователь с site:plugins
  • Требует: site:plugins
  • Нет тела.
  • Отвечает: { "data": { "id", "state": "active" } }

Исходы:

  • 200
  • 403 missing_permission
  • 404 not_found
  • 409 installation_state_changed
  • 503 audit_unavailable

Удаление. Сначала выполняет отзыв: состояние становится revoked, оба хеша удаляются, и токен уже никогда не может стать действительным. Атрибуция в аудите сохраняется, потому что событие workflow хранит снимок инсталляции, а не внешний ключ. Dee Wan НЕ заявляет, что это удалило что-либо из того, что хранит плагин, — данные на своём сервере удаляет сам плагин. Плагин можно удалить, даже если его редакторы используются в полях моделей. Значения и правила этих полей сохраняются, а редактирование выполняется встроенным редактором.

  • Вызывающий: пользователь с site:plugins
  • Требует: site:plugins
  • Нет тела.
  • Отвечает: { "data": { "id", "state": "revoked" } }

Исходы:

  • 200
  • 403 missing_permission
  • 404 not_found
  • 409 installation_state_changed
  • 503 audit_unavailable

Открытие плагина с делегированием. Выпускает одноразовый код, действительный 60 секунд, хранящийся хешированным и привязанный к инсталляции, сайту, пользователю, действию и необязательному экземпляру, и возвращает management_url с этим кодом. Ответ содержит Referrer-Policy: no-referrer и Cache-Control: no-store. action должен быть manage без экземпляра или id из content_actions сохранённого манифеста с экземпляром. Для действия над контентом модель элемента должна быть доступна установленному плагину. Запуск manage может назвать view из manage_views сохранённого манифеста; тогда URL также содержит dee_wan_view — подсказку о целевой странице, которая ничего не разрешает. Запуск с представлением — редакционный: ему не нужно site:plugins, нужна лишь одна модель, доступная плагину, которую человек читает: все записи или только свои. Простой запуск manage — это администрирование плагина, ему нужно site:plugins. Делегирование записывает model_ids: модели, доступные плагину, которые человек читает (для действия над контентом — модель элемента), суженные до моделей учётных данных управления, когда действуют они. Запуск от учётных данных управления также записывает потолок возможностей: возможности инсталляции, которые разрешают scopes этих учётных данных. Каждая последующая команда через это делегирование вне его отклоняется с capability_refused. Cookie сессии Dee Wan никогда не покидают Dee Wan.

Запуск объявленного представления manage может передать entry_context: { instance_id, locale? }. Ядро проверяет текущий доступ на чтение, доступ к языку и ограничения инсталляции и модели, затем добавляет подсказки dee_wan_entry и, при наличии языка, dee_wan_locale. Они не дают дополнительных прав: делегирование представления не привязывается к одной записи, а плагин заново читает её с текущими правами.

  • Вызывающий: участник сайта для действия над контентом или представления manage. site:plugins для простого manage
  • Требует: доступа на чтение к названному элементу для действия над контентом; доступа на чтение хотя бы к одной разрешённой плагину модели для открытия представления
  • Тело — { "action": "manage" }, { "action": "manage", "view": "calendar" } (или "releases") или { "action": "<action id>", "instance_id": "…" }
  • Для объявленного представления можно добавить "entry_context": { "instance_id": "…", "locale": "en" }; locale необязателен.
  • Отвечает: { "data": { "url": "https://…?dee_wan_launch=…" } } — $defs/launch.

Исходы:

  • 200
  • 400 malformed_body / invalid_launch_action / invalid_entry_context
  • 403 missing_permission (для простого manage) / no_readable_models (запуск с представлением, а человек не читает ни одной модели, доступной плагину) / model_denied / not_owner (человек не может читать элемент) / model_not_granted (модель записи недоступна плагину)
  • 404 not_found — нет такой инсталляции или нет такого элемента
  • 409 installation_not_active

Расширение field_editor с control: "group" рисует одно JSON-поле как форму не более чем из 8 полей с названиями: text (max_length до 500), number (min, max, integer), boolean и select (до 12 вариантов). Ключи полей формы уникальны. Хранимое значение остаётся одним JSON-объектом не более 8192 байт. Выбор редактора в конструкторе моделей записывает в поле его закрытую схему (config.schema, с validationType: "object"), и обычное сохранение проверяет переданные значения и структуру: недопустимые значения, неизвестные ключи и значения, не являющиеся объектом, отклоняются. В режиме черновика обязательные поля формы можно пока не заполнять; строгое сохранение и любая публикация требуют их заполнения. Схема остаётся в поле при отключении или удалении плагина, поэтому сохранённые значения остаются допустимыми, а встроенный JSON-редактор их показывает.

Существующие запросы на чтение кратких сведений продолжают работать. delegation_id в GET /content/:instanceId добавляет saved: { id, version_number, data }. Снимок ограничен текущими правами на модель, поля, язык, владение. Без делегирования ответ содержит только краткие сведения. GET /content/:instanceId/versions?delegation_id=…&limit=50&cursor=… перечисляет сохранённые версии. GET /content/:instanceId/versions/:versionId?delegation_id=… читает снимок версии. История проверяет владельца основной записи. Удалённая политикой хранения версия недоступна.

POST /content: обычное создание через механизм жизненного цикла. Нужны content:create, делегирование manage, Idempotency-Key. Тело: delegation_id, model_id, locales; необязательные primary_lang, slugs, unset_fields, relations, locale_relations, media, seo, label. В unset_fields для каждого языка укажите необязательные переводимые поля с настроенным резервным значением, чьи переопределения нужно удалить. Одно поле нельзя одновременно задать и удалить. Формат управления записями общий. PUT /content/:instanceId дополнительно принимает relations, locale_relations, media. Версия закреплена expected_version_id. Пустой fields допустим при изменении ссылок. DELETE /content/:instanceId переносит запись в корзину: delegation_id, expected_version_id, Idempotency-Key. Нужны content:delete и право человека удалять запись. Опубликованную запись сначала снимите с публикации. Ответ content_deleted; повтор возвращает первый результат.

GET /media/:mediaId?delegation_id=…: метаданные. /bytes: сохранённые байты провайдера. Нужны media:read и право пользователя на чтение медиатеки. Приватные файлы изолированы в пределах сайта; кеш запрещён. У внешней ссылки локальных байтов нет. PUT /media/:mediaId: {delegation_id,metadata}, Idempotency-Key, право media:update. Поля: alt_texts, default_focal, loading, fetch_priority. Общая проверка медиа. Ответ media_updated, {id}.

Карточка или редактор поля может объявить frame: {url,origin,operations?}. Источник фрейма должен точно совпадать с источником его URL и источником плагина. Администратор проверяет его до установки/обновления. Iframe: credentialless, sandbox только allow-scripts, без referrer, cookies администратора, токена. При загрузке Dee Wan передаёт MessagePort сообщением dee-wan:connect, protocol 1, value, disabled. Дочерний интерфейс использует только порт. Сообщения: ready; change с value; resize с height; operation с id, operation, input. Dee Wan проверяет объявление и текущие права, вызывает существующую операцию, отвечает result с id/result. Повтор id возвращает тот же результат. Изменение поля проходит обычное сохранение. Новые разрешения после обновления выключены.

relation: models, необязательный multiple. group: fields, repeatable, max_items. Не более трёх вложенных групп и пятидесяти элементов. Обычные типизированные поля и выбор связей. Сохранение проверяет сайт, модель и права чтения цели. Render-узлы group/each задают область children. Для медиа верхнего уровня и внутри групп действуют обычные закрепления за версиями. Выравнивание абзацев/заголовков: left, center, right, justify; общий контракт редактора и рендера.

Виджет хранит html/css/javascript/title в типизированном snippet-блоке. В документе это инертный JSON. Редактор и сайт используют статический frame и общий протокол MessageChannel. Оба используют общий документ и принудительный sandbox: credentialless, без allow-same-origin; скрипты, формы и всплывающие окна работают только внутри. Данные формы обрабатывает провайдер. Не храните секреты провайдера в контенте.

Сохранённые данные содержат ID. Публичный rich-вывод заменяет доступную цель ограниченным объектом чтения: reference: true, instance_id, текущие title, url и properties с доступными скалярными полями, указанными узлом property. { field: "target", property: "address" } показывает поле цели; обычный { field: "target" } — её заголовок. Ссылки используют общий опубликованный адрес Page/Blog/ управляемого маршрута либо публичное поле url. Без адреса остаётся текст. Скрытые цели и приватные поля исключены. Сохранение принимает только ID, а не присланные подписи и URL.

  • schemas/manifest-v1.schema.json — манифест, сгенерированный из объекта, по которому ядро валидирует каждый полученный манифест.
  • schemas/protocol-v1.schema.json — запросы, ответы, тело передачи активации, исходы, лимиты и формы секретов, сгенерированные из тех же определений. Формы запросов закрыты. Формы ответов допускают дополнительные свойства, потому что клиенты должны игнорировать неизвестные им ключи ответа.

JSON Schema не заменяет семантическую валидацию. Включайте проверку форматов: URL используют uri, а адрес управления — uri-template. Адреса манифеста, базы, активации и управления после подстановки должны иметь один origin и проходить политику HTTPS/частных адресов. Исключения разрешены только для точно указанных origin разработки. Эта проверка не определяет IP-адрес домена через DNS. Возможности уникальны. Идентификаторы действий над контентом тоже должны быть уникальны. Идентификаторы ui_extensions уникальны и не входят в reserved_ids, а карточка запускает только идентификатор из content_actions или запись manage_views, объявленные тем же манифестом. Нужна хотя бы одна поддерживаемая версия протокола. Предоставленный доступ ограничивается возможностями, запрошенными манифестом, и моделями этого сайта. Вид перехода определяет, какую единственную версию обязательно закрепить в запросе. Соответствие HTTP-статуса, кода и retry проверяется после проверки структуры ответа.


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