Протокол v1
Каждый маршрут, заголовок, тело, код исхода и правило повтора удалённого протокола плагинов. Плагин — это отдельно развёрнутый сервис: он общается с Dee Wan по HTTP и не выполняет никакого кода внутри него. См. также Написание плагина — первый плагин от начала до конца, Безопасность плагинов — граница доверия, и Тестирование плагина — как его тестировать.
Поддерживаемые версии протокола: 1. Версия
согласуется один раз, при установке, и закрепляется за инсталляцией. Несовместимое изменение маршрута,
смысла или кода ошибки требует протокола v2; добавление полей в ответ допускается только потому,
что клиенты обязаны игнорировать неизвестные им ключи ответа. Формы запросов
закрыты — неизвестный ключ отклоняется и никогда не игнорируется.
Аутентификация
Заголовок раздела «Аутентификация»Каждый маршрут плагина принимает Authorization: Bearer <token>, где токен — это dwp_, за которым
следуют 43 URL-безопасных символа 256-битной случайности. Ядро хранит только его хеш SHA-256 и никогда
не может показать его снова; плагин получает его один раз, при активации.
Маршруты находятся под /api/plugin/v1 и подключаются до проверок origin и сессии, предназначенных
для браузера, потому что сервер не отправляет ни заголовок Origin, ни cookie. Их собственное
middleware requirePlugin определяет инсталляцию и сайт ДО чтения какого-либо тела запроса,
в таком порядке: отклонить сайт или базу данных, названные вызывающим, сопоставить токен с одной
активной инсталляцией, израсходовать лимит запросов, определить сайт, направить запрос в базу данных арендатора.
Следствия, которые стоит назвать прямо:
- Сайт выводится из токена. Плагин не может его назвать, и сама отправка
X-Site-Id— этоinvalid_request. - Пользовательский cookie не является аутентификацией плагина, а токен плагина не принимается ни на одном административном маршруте.
- Инсталляции в состоянии
pending,disabledиrevokedотвечаютplugin_unauthorizedдо какого-либо доступа к арендатору.
Заголовки
Заголовок раздела «Заголовки»| Заголовок | Где | Значение |
|---|---|---|
Authorization: Bearer <token> |
каждый запрос плагина | Токен инсталляции. Обязателен. |
Idempotency-Key |
каждая мутация | Обязателен. До 128 печатаемых символов ASCII. Bearer-токен — это аутентификация, а не контроль повторов. |
Content-Type: application/json |
запросы с телом | Единственный формат тела. |
X-Site-Id |
никогда | Отклоняется. Сайт берётся из токена. |
Retry-After |
в ответе 429 | Секунды. Соблюдайте его в пределах собственного максимума. |
Referrer-Policy: no-referrer |
в ответе на запуск | Устанавливается ядром, чтобы код запуска не мог утечь через referrer. |
| Лимит | Значение |
|---|---|
| Тело запроса | 16384 байт |
| Тело манифеста | 32768 байт |
| Ключ идемпотентности | 128 символов |
| Страница контента | 50 элементов, по умолчанию 20 |
| Запросы на инсталляцию | 120 за окно 60 с, по ключу инсталляции |
| Время жизни кода запуска | 60 с, однократное использование, хранится в виде хеша |
| Перекрытие при ротации токена | не более 10 минут |
Возможности
Заголовок раздела «Возможности»| Возможность | Что разрешает |
|---|---|
content:read |
Маршруты чтения контента в пределах выданных моделей. |
workflow:read |
Блок workflow при чтении: состояние, ревизия и переходы через границу публикации. |
workflow:publish |
Команда kind: publish. |
workflow:unpublish |
Команда kind: unpublish. |
Грант — это пересечение этих возможностей со списком id моделей контента для конкретного сайта. Пустой список моделей означает отсутствие доступа к контенту, а не все модели. Возможности вне запроса сохранённого манифеста не могут быть выданы, поэтому обновление плагина никогда не может расширить существующий грант.
Состояния инсталляции
Заголовок раздела «Состояния инсталляции»pending, active, disabled, revoked. Вызывать API плагинов может только active. revoked — конечное состояние:
его токен никогда не становится снова действительным, и удаление сначала выполняет отзыв.
Маршруты плагина
Заголовок раздела «Маршруты плагина»GET /api/plugin/v1/context
Заголовок раздела «GET /api/plugin/v1/context»Что представляет собой эта инсталляция. Id инсталляции, сайт, к которому она привязана, принятая версия протокола и грант. Прочитайте его при запуске, чтобы узнать, что эта инсталляция может запрашивать; он не называет ни пользователя, ни хеш токена, ни базу данных, ни инфраструктуру.
- Вызывающий: плагин со своим токеном инсталляции
- Требует: ничего
- Нет query, нет тела.
- Отвечает:
{ "data": <context> }—$defs/context.
Исходы:
plugin_unauthorizedrate_limitedtemporary_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 здесь — не временный сбой.
GET /api/plugin/v1/content
Заголовок раздела «GET /api/plugin/v1/content»Список контента в одной выданной модели. Одна страница основных экземпляров одной модели. 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_unauthorizedrate_limitedtemporary_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": "First draft", "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 }}GET /api/plugin/v1/content/:instanceId
Заголовок раздела «GET /api/plugin/v1/content/:instanceId»Прочитать один элемент и то, что он может сделать дальше. Id, метка, slug, id текущей и опубликованной версий и — с workflow:read — состояние workflow, его ревизия и переходы, пересекающие границу публикации. Переходы сужаются до возможностей, которыми обладает эта инсталляция. Передайте delegation_id, и они будут сужены ещё раз до того, что этот человек может сделать СЕЙЧАС, — с повторным чтением из control; делегирование, чей человек утратил полномочия, отвечает requester_unauthorized, а не молча укороченным списком. Элемент вне сайта или гранта моделей — это content_not_found; то же отвечает и id с другого сайта.
- Вызывающий: плагин,
content:read - Требует:
content:read, плюсworkflow:readдля блокаworkflow delegation_id— использованное делегирование запуска. Другие параметры query не принимаются.- Отвечает:
{ "data": <summary> }—$defs/content_summary.
Исходы:
capability_refusedcontent_not_foundrequester_unauthorized— названное делегирование неизвестно, или его человек больше не соответствует требованиямinvalid_requestplugin_unauthorizedrate_limitedtemporary_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": "First draft", "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 для обоих.
POST /api/plugin/v1/content/:instanceId/transitions/:targetState
Заголовок раздела «POST /api/plugin/v1/content/:instanceId/transitions/:targetState»Опубликовать или снять с публикации, условно. Единственная мутация в протоколе v1. Она условна в четырёх отношениях сразу, и все они проверяются внутри одной атомарной мутации жизненного цикла: закреплённая версия должна по-прежнему быть той, которую переместит переход, ревизия workflow должна по-прежнему быть той, относительно которой было создано расписание, targetState должен быть переходом, доступным из текущего состояния, и смысл этого перехода с точки зрения публикации должен совпадать с kind. Полномочия делегирующего человека перечитываются из control перед записью. Более новый черновик никогда не публикуется молча; изменённый workflow никогда не проходится молча. Публикация применяется ко всей группе переводов, поэтому результат называет основной экземпляр.
- Вызывающий: плагин,
workflow:publishилиworkflow:unpublish - Требует:
workflow:publishдляkind: publish,workflow:unpublishдляkind: unpublish Idempotency-Key(обязательный) — до 128 печатаемых символов ASCII. Выводите его из расписания, а не из часов: точный повтор возвращает первый результат, а тот же ключ с другими входными данными — этоidempotency_key_reused.- Тело —
$defs/publish_commandили$defs/unpublish_command. Форма ЗАКРЫТА: неизвестный ключ или закрепление другого вида — этоinvalid_request. - Отвечает:
{ "outcome": {...}, "data": <transition_result> }—$defs/transition_result.
Исходы:
transition_applied/idempotent_replayinvalid_requestcapability_refusedrequester_unauthorizedcontent_not_foundscheduled_target_staleworkflow_conflictdependent_contentidempotency_key_reusedtransition_unavailable/transition_refusedrate_limitedtemporary_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 }}Та же команда ещё раз
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 }}Повтор после сетевого сбоя безопасен: возвращается первый результат, и ничего не выполняется дважды.
Тот же ключ, другая команда
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 }}Более новый черновик никогда не публикуется молча
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 }}Кто-то сохранил более новый черновик после создания расписания. Конечный исход: решение принимает человек.
Изменённый workflow никогда не проходится молча
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 }}POST /api/plugin/v1/launch/exchange
Заголовок раздела «POST /api/plugin/v1/launch/exchange»Обменять код запуска на делегирование. Браузер приходит на management_url с dee_wan_launch. БЭКЕНД плагина отправляет этот код сюда со своим bearer-токеном в течение 60 секунд. Код используется один раз. В ответ приходит привязка — инсталляция, сайт, действие, необязательный экземпляр контента — и безопасная для отображения идентичность для собственной сессии плагина. Она не даёт никаких возможностей Dee Wan, которых ещё нет у токена инсталляции, а код, выпущенный для другой инсталляции, отклоняется.
- Вызывающий: плагин со своим токеном инсталляции
- Требует: ничего
- Тело —
$defs/launch_exchange_request. - Отвечает:
{ "data": <delegation> }—$defs/launch_delegation.
Исходы:
invalid_request— неизвестен, уже использован, истёк или выпущен для другой инсталляцииplugin_unauthorizedrate_limitedtemporary_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", "user": { "id": "cdocsuser00000000000000001", "email": "admin@example.com" } }}Сохраните 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 }}Каждый маршрут плагина отвечает машинным кодом и сообщением для человека:
{ "outcome": { "code": "transition_applied", "message": "…", "retry": false }, "data": {}}Ветвитесь по code. Сообщение предназначено для человека и не стабильно. retry — собственное утверждение ядра
о том, может ли помочь повторная попытка, и это единственное поле, которое нужно исполнителю для принятия решения.
| HTTP | Код | Повтор | Значение |
|---|---|---|---|
| 200 | transition_applied |
нет | Переход был применён. |
| 200 | idempotent_replay |
нет | Эта команда уже была применена. Возвращается первый результат. |
| 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 |
нет | Этот ключ идемпотентности уже использовался для другой команды. |
| 422 | transition_unavailable |
нет | Этот переход недоступен из текущего состояния контента. |
| 422 | transition_refused |
нет | Жизненный цикл отклонил этот переход. Ничего не изменено. |
| 429 | rate_limited |
да | Слишком много запросов для этой инсталляции. |
| 503 | temporary_failure |
да | Dee Wan не смог выполнить запрос. Неизвестно, чтобы что-либо изменилось. |
Правила повтора
Заголовок раздела «Правила повтора»rate_limited— повторяйте и соблюдайтеRetry-After. Отклоните расписание вместо ожидания, если ожидание превышает ваш собственный максимум.temporary_failureи транспортный сбой (тайм-аут, сброс соединения, отсутствие ответа) — повторяйте с ограниченной экспоненциальной задержкой и джиттером, до задокументированного предела числа попыток и возраста.- Всё остальное КОНЕЧНО. Не повторяйте это и не превращайте в успех.
- Ответ, у которого HTTP-статус и
outcome.codeне согласуются или тело которого не является конвертом, — недоверенный: считайте его конечным отказом, а не пытайтесь угадать. - Повтор после неизвестного исхода безопасен благодаря
Idempotency-Key: точный повтор возвращает первый результат и ничего не применяет дважды. Это не замена вашей собственной аренде (lease) — два ваших исполнителя по-прежнему не должны выполнять одно расписание одновременно. - Ничто здесь никогда не является бесконечным повтором, и ни один неизвестный сбой никогда не сообщается как применённый.
Административные маршруты
Заголовок раздела «Административные маршруты»Это маршруты для браузера за пользовательской сессией, заголовком сайта и
правом доступа site:plugins — интерфейс, который устанавливает, выдаёт гранты, выполняет ротацию и удаляет. Они не
входят в то, что вызывает плагин; они приведены здесь, чтобы автор плагина знал, что делает администратор.
Каждый из них, выполняющий мутацию, пишет запись аудита и отказывает (503 audit_unavailable), а не
выполняет мутацию без аудита.
GET /api/plugins
Заголовок раздела «GET /api/plugins»Список инсталляций. Каждая неотозванная инсталляция на сайте, в отредактированном виде: без токена, без хеша.
- Вызывающий: пользователь с
site:plugins - Требует:
site:plugins - Нет тела.
- Отвечает:
{ "data": [<installation>] }—$defs/installation.
Исходы:
- 200
- 403
missing_permission
GET /api/plugins/models
Заголовок раздела «GET /api/plugins/models»Модели, которые можно выдать. Id, имя и slug каждой модели контента на сайте для экрана гранта. ГРАНТ хранит id, поэтому переименование его сохраняет, а удалённая модель делает соответствующую его часть недействующей.
- Вызывающий: пользователь с
site:plugins - Требует:
site:plugins - Нет тела.
- Отвечает:
{ "data": [{ "id", "name", "slug" }] }
Исходы:
- 200
- 403
missing_permission
GET /api/plugins/actions
Заголовок раздела «GET /api/plugins/actions»Действия с контентом для отрисовки. Ограниченные пары { id, label }, которые объявляют активные инсталляции, чтобы экран контента мог отрисовать кнопку, принадлежащую ядру. Никакая разметка, скрипт, CSS или URL иконки никогда не приходят от плагина.
- Вызывающий: любой участник сайта
- Требует: ничего сверх доступа к сайту
- Нет тела.
- Отвечает:
{ "data": [{ "installation_id", "plugin_name", "actions": [{ "id", "label" }] }] }
Исходы:
- 200
POST /api/plugins/review
Заголовок раздела «POST /api/plugins/review»Получить и проверить манифест. Загружает URL манифеста, проверяет его и возвращает то, что он запрашивает. Ничего не сохраняется, и после этого никакой инсталляции не существует; это шаг проверки, который делает одобрение осознанным.
- Вызывающий: пользователь с
site:plugins - Требует:
site:plugins - Тело —
{ "manifest_url": "https://…" } - Отвечает:
{ "data": { "manifest": <manifest>, "protocol_version": 1 } }
Исходы:
- 200
- 400 с
reason—manifest_invalid,protocol_unsupported,manifest_origin_mismatch,url_not_https,url_credentials,url_fragment,url_private_destination,remote_redirect_refused,remote_body_too_large,remote_unreachable,manifest_unavailable
POST /api/plugins
Заголовок раздела «POST /api/plugins»Установка. Повторно загружает и повторно проверяет манифест, записывает ожидающую инсталляцию с одобренными возможностями и id моделей, выпускает токен и передаёт исходный токен вместе с одноразовым кодом активации на activation_url манифеста. Инсталляция становится active только после ответа 2xx от плагина; ядро хранит хеш и никогда не хранит токен. Неудачная активация ничего после себя не оставляет, а повторная установка того же плагина на тот же сайт — это конфликт, а не вторая инсталляция.
- Вызывающий: пользователь с
site:plugins - Требует:
site:plugins - Тело —
{ "manifest_url", "activation_code", "capabilities": […], "model_ids": […] }. Возможности должны быть подмножеством запроса манифеста; пустойmodel_idsозначает отсутствие доступа к контенту, а не все модели. - Отвечает:
{ "data": <installation> }, 201.
Исходы:
- 201
- 400
invalid_activation_code/invalid_capabilities/invalid_model_ids/ причина, связанная с манифестом - 409
installation_exists/backend_url_unset/activation_superseded - 502 с удалённой причиной — плагин отказал или был недоступен
- 503
audit_unavailable
PUT /api/plugins/:id/grant
Заголовок раздела «PUT /api/plugins/:id/grant»Изменить грант. Заменяет возможности и id моделей. Он может только сужаться в пределах того, что запросил СОХРАНЁННЫЙ манифест, — возможность вне его отклоняется, поэтому изменённый манифест не может расширить грант. Сокращение вступает в силу со следующего запроса плагина.
- Вызывающий: пользователь с
site:plugins - Требует:
site:plugins - Тело —
{ "capabilities": […], "model_ids": […] } - Отвечает:
{ "data": <installation> }
Исходы:
- 200
- 400
invalid_capabilities/invalid_model_ids - 404
- 409
installation_state_changed
POST /api/plugins/:id/upgrade
Заголовок раздела «POST /api/plugins/:id/upgrade»Принять новую версию манифеста. Повторно загружает сохранённый URL манифеста и принимает релиз, который администратор только что проверил, — expected_version должен совпадать с тем, что отдаётся, иначе проверка устарела. Id плагина, origin и принятая версия протокола не должны были измениться. Возможности ПЕРЕСЕКАЮТСЯ с новым запросом; обновление никогда не расширяет грант.
- Вызывающий: пользователь с
site:plugins - Требует:
site:plugins - Тело —
{ "expected_version": "0.2.0" } - Отвечает:
{ "data": <installation> }
Исходы:
- 200
- 400 причина, связанная с манифестом
- 409
manifest_identity_changed/protocol_changed/manifest_changed_since_review
POST /api/plugins/:id/rotate
Заголовок раздела «POST /api/plugins/:id/rotate»Ротация токена. Требует свежего одноразового кода от плагина, потому что ротация — это передача, а не сброс. Ядро выпускает токен, отправляет его на закреплённый origin активации и только после этого заменяет хеш. Если плагин отказывает, старый токен продолжает работать. Предыдущий хеш остаётся действительным не более 10 минут, а первый запрос с НОВЫМ токеном немедленно завершает это перекрытие.
- Вызывающий: пользователь с
site:plugins - Требует:
site:plugins - Тело —
{ "activation_code": "…" } - Отвечает:
{ "data": { "rotated": true } }
Исходы:
- 200
- 400
invalid_activation_code - 409
installation_not_active/backend_url_unset/installation_state_changed - 502 удалённая причина — и СТАРЫЙ токен продолжает работать
POST /api/plugins/:id/disable
Заголовок раздела «POST /api/plugins/:id/disable»Отключить. Сохраняет конфигурацию и журнал аудита и полностью останавливает API плагина: каждый запрос отвечает plugin_unauthorized до какого-либо доступа к арендатору.
- Вызывающий: пользователь с
site:plugins - Требует:
site:plugins - Нет тела.
- Отвечает:
{ "data": { "id", "state": "disabled" } }
Исходы:
- 200
- 404
- 409
installation_state_changed
POST /api/plugins/:id/enable
Заголовок раздела «POST /api/plugins/:id/enable»Включить. Возвращает отключённую инсталляцию в active с тем же токеном и тем же грантом.
- Вызывающий: пользователь с
site:plugins - Требует:
site:plugins - Нет тела.
- Отвечает:
{ "data": { "id", "state": "active" } }
Исходы:
- 200
- 404
- 409
installation_state_changed
DELETE /api/plugins/:id
Заголовок раздела «DELETE /api/plugins/:id»Удалить. Сначала выполняет отзыв: состояние становится revoked, оба хеша удаляются, и токен никогда не может снова стать действительным. Атрибуция аудита сохраняется, потому что событие workflow сохранило снимок инсталляции, а не внешний ключ. Dee Wan НЕ утверждает, что это удалило что-либо, хранящееся у плагина, — удалённое удаление является собственным действием плагина.
- Вызывающий: пользователь с
site:plugins - Требует:
site:plugins - Нет тела.
- Отвечает:
{ "data": { "id", "state": "revoked" } }
Исходы:
- 200
- 404
- 409
installation_state_changed
POST /api/plugins/:id/launch
Заголовок раздела «POST /api/plugins/:id/launch»Открыть плагин с делегированием. Выпускает одноразовый код, действительный 60 секунд, хранящийся в виде хеша и привязанный к инсталляции, сайту, пользователю, действию и необязательному экземпляру, и возвращает management_url с этим кодом. Ответ содержит Referrer-Policy: no-referrer и Cache-Control: no-store. action должно быть manage без экземпляра или id из content_actions сохранённого манифеста с экземпляром. Cookie сессии Dee Wan никогда не покидают Dee Wan.
- Вызывающий: участник сайта для действия с контентом;
site:pluginsдляmanage - Требует: доступ на чтение к названному элементу для действия с контентом
- Тело —
{ "action": "manage" }или{ "action": "<action id>", "instance_id": "…" } - Отвечает:
{ "data": { "url": "https://…?dee_wan_launch=…" } }—$defs/launch.
Исходы:
- 200
- 400
invalid_launch_action - 403
missing_permission(дляmanage) /model_denied - 404
not_found - 409
installation_not_active
Машиночитаемые схемы
Заголовок раздела «Машиночитаемые схемы»schemas/manifest-v1.schema.json— манифест, сгенерированный из объекта, которым ядро проверяет каждый загруженный манифест.schemas/protocol-v1.schema.json— запросы, ответы, исходы, лимиты и формы секретов, сгенерированные из тех же определений.
Оба файла записываются командой npm run docs:plugins и хранятся в репозитории. Тест сравнивает сохранённые копии
с тем, что выдаёт код, поэтому они не могут описывать протокол, на котором это ядро не говорит.