Перейти к содержимому
Pre-MVP. Кодовая база 1.0 не является выпущенным продуктом — см. границы продукта 1.0.

Протокол 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 — конечное состояние: его токен никогда не становится снова действительным, и удаление сначала выполняет отзыв.

Что представляет собой эта инсталляция. 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 модели из гранта.
  • publishedtrue или 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": "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
}
}

Прочитать один элемент и то, что он может сделать дальше. 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_refused
  • content_not_found
  • requester_unauthorized — названное делегирование неизвестно, или его человек больше не соответствует требованиям
  • 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": "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 для обоих.

Опубликовать или снять с публикации, условно. Единственная мутация в протоколе 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_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
}
}

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

Окно терминала
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
}
}

Обменять код запуска на делегирование. Браузер приходит на management_url с dee_wan_launch. БЭКЕНД плагина отправляет этот код сюда со своим bearer-токеном в течение 60 секунд. Код используется один раз. В ответ приходит привязка — инсталляция, сайт, действие, необязательный экземпляр контента — и безопасная для отображения идентичность для собственной сессии плагина. Она не даёт никаких возможностей Dee Wan, которых ещё нет у токена инсталляции, а код, выпущенный для другой инсталляции, отклоняется.

  • Вызывающий: плагин со своим токеном инсталляции
  • Требует: ничего
  • Тело — $defs/launch_exchange_request.
  • Отвечает: { "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",
"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), а не выполняет мутацию без аудита.

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

  • Вызывающий: пользователь с 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

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

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

Исходы:

  • 200

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

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

Исходы:

  • 200
  • 400 с reasonmanifest_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

Установка. Повторно загружает и повторно проверяет манифест, записывает ожидающую инсталляцию с одобренными возможностями и 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

Изменить грант. Заменяет возможности и id моделей. Он может только сужаться в пределах того, что запросил СОХРАНЁННЫЙ манифест, — возможность вне его отклоняется, поэтому изменённый манифест не может расширить грант. Сокращение вступает в силу со следующего запроса плагина.

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

Исходы:

  • 200
  • 400 invalid_capabilities / invalid_model_ids
  • 404
  • 409 installation_state_changed

Принять новую версию манифеста. Повторно загружает сохранённый 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

Ротация токена. Требует свежего одноразового кода от плагина, потому что ротация — это передача, а не сброс. Ядро выпускает токен, отправляет его на закреплённый 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 удалённая причина — и СТАРЫЙ токен продолжает работать

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

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

Исходы:

  • 200
  • 404
  • 409 installation_state_changed

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

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

Исходы:

  • 200
  • 404
  • 409 installation_state_changed

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

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

Исходы:

  • 200
  • 404
  • 409 installation_state_changed

Открыть плагин с делегированием. Выпускает одноразовый код, действительный 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 и хранятся в репозитории. Тест сравнивает сохранённые копии с тем, что выдаёт код, поэтому они не могут описывать протокол, на котором это ядро не говорит.