โปรโตคอล v1
ทุก route, header, body, รหัส outcome และกฎการ retry ของ remote plugin protocol ปลั๊กอินคือ service ที่ deploy แยกต่างหาก: มันสื่อสารกับ Dee Wan ผ่าน HTTP และไม่รันโค้ดใดภายใน Dee Wan ดูเพิ่มเติมที่ การเขียนปลั๊กอิน สำหรับปลั๊กอินแรกตั้งแต่ต้นจนจบ, ความปลอดภัยของปลั๊กอิน สำหรับขอบเขตความเชื่อถือ และ การทดสอบปลั๊กอิน สำหรับวิธีทดสอบปลั๊กอิน
เวอร์ชันของ protocol ที่รองรับ: 1 เวอร์ชันจะถูกตกลงกันครั้งเดียวตอนติดตั้ง และถูก pin ไว้กับ
installation (การติดตั้ง Dee Wan หนึ่งชุด) การเปลี่ยนแปลงที่ breaking ต่อ route, ความหมาย หรือรหัส error
ต้องใช้ protocol v2; ฟิลด์ใน response ที่เพิ่มเข้ามาได้รับอนุญาตเพียงเพราะ client ถูกกำหนดให้ต้องเพิกเฉยต่อ
key ใน response ที่ไม่รู้จัก shape ของ request เป็นแบบปิด — key ที่ไม่รู้จักจะถูกปฏิเสธ ไม่ใช่ถูกเพิกเฉย
การยืนยันตัวตน
หัวข้อที่มีชื่อว่า “การยืนยันตัวตน”ทุก route ของปลั๊กอินรับ Authorization: Bearer <token> โดย token คือ dwp_ ตามด้วยอักขระ
URL-safe 43 ตัวจากความสุ่ม 256 บิต core เก็บเพียง hash SHA-256 ของมันและไม่สามารถแสดงมันได้อีก
ปลั๊กอินได้รับมันเพียงครั้งเดียวตอน activation
route เหล่านี้อยู่ใต้ /api/plugin/v1 และถูก mount ก่อนการตรวจ origin และ session ฝั่งเบราว์เซอร์
เพราะ server ไม่ได้ส่งทั้ง header Origin และ cookie middleware requirePlugin ของพวกมันเอง
resolve installation และไซต์ ก่อน ที่จะอ่าน body ของ request ใด ๆ ตามลำดับนี้: ปฏิเสธไซต์หรือ
ฐานข้อมูลที่ผู้เรียกระบุชื่อมา, resolve token ไปยัง installation ที่ active หนึ่งตัว, ใช้ rate limit,
resolve ไซต์, route ไปยังฐานข้อมูลของ tenant
ผลที่ควรระบุให้ชัด:
- ไซต์ได้มาจาก token ปลั๊กอินระบุไซต์เองไม่ได้ และการส่ง
X-Site-Idมาเลยคือinvalid_request - cookie ของผู้ใช้ไม่ใช่การยืนยันตัวตนของปลั๊กอิน และ token ของปลั๊กอินจะไม่ถูกยอมรับบน route ด้านการดูแลระบบใดเลย
- installation ที่เป็น
pending,disabledและrevokedจะตอบplugin_unauthorizedก่อนที่จะมี การเข้าถึง tenant ใด ๆ
| Header | ที่ไหน | ความหมาย |
|---|---|---|
Authorization: Bearer <token> |
ทุก request ของปลั๊กอิน | token ของ installation บังคับ |
Idempotency-Key |
ทุก mutation | บังคับ อักขระ ASCII ที่พิมพ์ได้ไม่เกิน 128 ตัว bearer token คือการยืนยันตัวตน ไม่ใช่การควบคุม replay |
Content-Type: application/json |
request ที่มี body | รูปแบบ body เดียวที่รองรับ |
X-Site-Id |
ไม่เคย | ถูกปฏิเสธ ไซต์มาจาก token |
Retry-After |
บน response 429 | วินาที ให้ปฏิบัติตาม ภายในค่าสูงสุดของคุณเอง |
Referrer-Policy: no-referrer |
บน response ของ launch | core ตั้งค่านี้เพื่อไม่ให้ launch code รั่วผ่าน referrer |
ขีดจำกัด
หัวข้อที่มีชื่อว่า “ขีดจำกัด”| ขีดจำกัด | ค่า |
|---|---|
| Request body | 16384 ไบต์ |
| Manifest body | 32768 ไบต์ |
| Idempotency key | 128 อักขระ |
| หน้าของเนื้อหา | 50 รายการ, ค่าเริ่มต้น 20 |
| Request ต่อ installation | 120 ต่อหน้าต่าง 60s, ใช้ installation เป็น key |
| อายุของ launch code | 60s, ใช้ครั้งเดียว, เก็บแบบ hash |
| ช่วงซ้อนทับตอน rotate token | ไม่เกิน 10 นาที |
Capability
หัวข้อที่มีชื่อว่า “Capability”| Capability | สิ่งที่อนุญาต |
|---|---|
content:read |
route สำหรับอ่านเนื้อหา ภายในโมเดลที่ได้รับ grant |
workflow:read |
บล็อก workflow ในการอ่าน: state, revision และ transition ที่ข้ามขอบเขตการเผยแพร่ |
workflow:publish |
คำสั่ง kind: publish |
workflow:unpublish |
คำสั่ง kind: unpublish |
grant (สิทธิ์ที่มอบให้) คือส่วนตัดกันของ capability เหล่านี้กับ list ของ id ของโมเดลเนื้อหาต่อไซต์ list โมเดลที่ว่างหมายถึงไม่มีสิทธิ์เข้าถึงเนื้อหา — ไม่ใช่ทุกโมเดล capability ที่อยู่นอกคำขอใน manifest ที่เก็บไว้ ไม่สามารถ grant ได้ ดังนั้นการอัปเดตปลั๊กอินไม่มีทางขยาย grant ที่มีอยู่ได้
สถานะของ installation
หัวข้อที่มีชื่อว่า “สถานะของ installation”pending, active, disabled, revoked มีเพียง active ที่เรียก plugin API ได้ revoked เป็นสถานะสุดท้าย:
token ของมันจะไม่กลับมาใช้ได้อีก และการถอนการติดตั้งจะ revoke ก่อนสิ่งอื่นใด
Route ของปลั๊กอิน
หัวข้อที่มีชื่อว่า “Route ของปลั๊กอิน”GET /api/plugin/v1/context
หัวข้อที่มีชื่อว่า “GET /api/plugin/v1/context”installation นี้คืออะไร id ของ installation, ไซต์ที่มันผูกอยู่, เวอร์ชัน protocol ที่ยอมรับ และ grant ให้อ่านตอนเริ่มต้นเพื่อรู้ว่า installation นี้ขออะไรได้บ้าง; มันไม่ระบุผู้ใช้, ไม่มี hash ของ token, ไม่มีฐานข้อมูล และไม่มีโครงสร้างพื้นฐานใด
- ผู้เรียก: ปลั๊กอิน พร้อม token ของ installation
- ต้องการ: ไม่มี
- ไม่มี query ไม่มี body
- คำตอบ:
{ "data": <context> }—$defs/context
Outcome:
plugin_unauthorizedrate_limitedtemporary_failure
installation นี้ทำอะไรได้บ้าง
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" ] }}token ที่ไม่รู้จัก ถูก disable หรือถูก revoke
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 }}ไซต์มาจาก token การส่ง header นี้มาเลยจะถูกปฏิเสธ แม้จะเป็นค่าที่ถูกต้องก็ตาม
route ของปลั๊กอินที่ไม่รู้จัก
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 }}Protocol v1 มี route ข้างต้นและไม่มีอะไรอื่น 404 ตรงนี้ไม่ใช่ความล้มเหลวชั่วคราว
GET /api/plugin/v1/content
หัวข้อที่มีชื่อว่า “GET /api/plugin/v1/content”แสดงรายการเนื้อหาในโมเดลที่ได้รับ grant หนึ่งโมเดล instance หลักหนึ่งหน้าในโมเดลเดียว model_id เป็นค่าบังคับ: ไม่มีการอ่านแบบ “ทุกโมเดล” ชุด filter เป็นแบบปิด — query parameter อื่นใดคือ invalid_request ไม่ใช่ถูกเพิกเฉย — และหน้านี้เป็นสรุปแบบแคบ ไม่ใช่ snapshot ของเนื้อหา มีเพียง primary ของ translation group ที่ปรากฏ; group คือหน่วยที่ transition เคลื่อนย้าย
- ผู้เรียก: ปลั๊กอิน,
content:read - ต้องการ:
content:read model_id(บังคับ) — id ของโมเดลจาก grantpublished—trueหรือfalseafter—instance_idตัวสุดท้ายของหน้าก่อนหน้าlimit— 1..50, ค่าเริ่มต้น 20- คำตอบ:
{ "data": [<summary>], "next": <instance_id|null> }—$defs/content_page
Outcome:
capability_refused— ไม่มีcontent:readหรือโมเดลอยู่นอก grantinvalid_request— ไม่มีmodel_id, filter ที่ไม่รู้จัก,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}filter ที่อยู่นอก model_id, published, after, limit คือ invalid_request ไม่ใช่ถูกเพิกเฉย
ชุด filter เป็นแบบปิด
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, label, slug, id ของ version ปัจจุบันและ version ที่เผยแพร่ และ — เมื่อมี workflow:read — state ของ workflow, revision ของมัน และ transition ที่ข้ามขอบเขตการเผยแพร่ transition ถูกจำกัดให้แคบลงตาม capability ที่ installation นี้ถืออยู่ ส่ง delegation_id มา แล้วมันจะถูกจำกัดให้แคบลงอีกตามสิ่งที่บุคคลนั้นทำได้ ในตอนนี้ โดยอ่านซ้ำจาก control; delegation ที่บุคคลของมันสูญเสียอำนาจไปแล้วจะตอบ requester_unauthorized แทนที่จะเป็น list ที่สั้นลงอย่างเงียบ ๆ รายการที่อยู่นอกไซต์หรือนอก grant ของโมเดลคือ content_not_found ซึ่งเป็นคำตอบเดียวกับ id ข้ามไซต์ด้วย
- ผู้เรียก: ปลั๊กอิน,
content:read - ต้องการ:
content:readและworkflow:readสำหรับบล็อกworkflow delegation_id— launch delegation ที่ถูกใช้แล้ว ไม่รับ query parameter อื่น- คำตอบ:
{ "data": <summary> }—$defs/content_summary
Outcome:
capability_refusedcontent_not_foundrequester_unauthorized— delegation ที่ระบุไม่เป็นที่รู้จัก หรือบุคคลของมันไม่มีคุณสมบัติอีกต่อไปinvalid_requestplugin_unauthorizedrate_limitedtemporary_failure
หนึ่งรายการ พร้อม transition ที่ผู้ร้องขอทำได้ในตอนนี้
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" } ] } }}ให้ pin สิ่งที่คำตอบนี้ให้มา: current_version_id สำหรับการเผยแพร่, published_version_id สำหรับการยกเลิกการเผยแพร่ และ workflow.revision สำหรับทั้งสองแบบ
POST /api/plugin/v1/content/:instanceId/transitions/:targetState
หัวข้อที่มีชื่อว่า “POST /api/plugin/v1/content/:instanceId/transitions/:targetState”เผยแพร่หรือยกเลิกการเผยแพร่แบบมีเงื่อนไข mutation เดียวที่ protocol v1 มี มันมีเงื่อนไขสี่ทางพร้อมกัน ซึ่งทั้งหมดถูกตรวจภายใน lifecycle mutation แบบ atomic เดียวกัน: version ที่ถูก pin ต้องยังเป็นตัวที่ transition จะเคลื่อนย้าย, revision ของ workflow ต้องยังเป็นตัวที่ใช้ตอนสร้างรายการตั้งเวลา, targetState ต้องเป็น transition ที่ใช้ได้จาก state ปัจจุบัน และความหมายด้านการเผยแพร่ของ transition นั้นต้องตรงกับ kind อำนาจของบุคคลที่ delegate จะถูกอ่านซ้ำจาก control ก่อนการเขียน draft ที่ใหม่กว่าจะไม่ถูกเผยแพร่อย่างเงียบ ๆ เลย; workflow ที่เปลี่ยนไปจะไม่ถูกเดินผ่านอย่างเงียบ ๆ เลย การเผยแพร่มีผลกับทั้ง translation group ซึ่งเป็นเหตุผลที่ผลลัพธ์ระบุ instance หลัก
- ผู้เรียก: ปลั๊กอิน,
workflow:publishหรือworkflow:unpublish - ต้องการ:
workflow:publishสำหรับkind: publish,workflow:unpublishสำหรับkind: unpublish Idempotency-Key(บังคับ) — อักขระ ASCII ที่พิมพ์ได้ไม่เกิน 128 ตัว ให้สร้างจากรายการตั้งเวลา ไม่ใช่จากนาฬิกา: replay ที่ตรงกันทุกประการจะคืนผลลัพธ์แรก และ key เดิมกับอินพุตที่ต่างกันคือidempotency_key_reused- Body —
$defs/publish_commandหรือ$defs/unpublish_commandshape เป็นแบบ ปิด: key ที่ไม่รู้จัก หรือ pin ของอีก kind หนึ่ง คือinvalid_request - คำตอบ:
{ "outcome": {...}, "data": <transition_result> }—$defs/transition_result
Outcome:
transition_applied/idempotent_replayinvalid_requestcapability_refusedrequester_unauthorizedcontent_not_foundscheduled_target_staleworkflow_conflictdependent_contentidempotency_key_reusedtransition_unavailable/transition_refusedrate_limitedtemporary_failure
เผยแพร่ draft ที่ถูก pin
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 }}การ retry หลัง network ล้มเหลวปลอดภัย: ผลลัพธ์แรกจะกลับมา และไม่มีอะไรรันซ้ำสองครั้ง
key เดิม คำสั่งต่างกัน
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 }}ให้สร้าง key จากรายการตั้งเวลาและ revision ของมัน การตั้งเวลาใหม่คือ revision ใหม่ จึงเป็น key ใหม่
ยกเลิกการเผยแพร่ version ที่ live และถูก pin
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 }}draft ที่ใหม่กว่าจะไม่ถูกเผยแพร่อย่างเงียบ ๆ เลย
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 }}มีคนบันทึก draft ที่ใหม่กว่าหลังจากสร้างรายการตั้งเวลาแล้ว เป็นสถานะสุดท้าย: บุคคลเป็นผู้ตัดสินใจว่าจะทำอย่างไร
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 }}shape ของ request เป็นแบบปิด
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 }}ไม่มีการ override ใน protocol v1 key ที่ไม่รู้จักจะถูกปฏิเสธ ไม่ใช่ถูกทิ้ง
mutation ที่ไม่มี idempotency key
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”แลก launch code เป็น delegation เบราว์เซอร์มาถึง management_url พร้อม dee_wan_launch BACKEND ของปลั๊กอินส่งรหัสนั้นมาที่นี่ พร้อม bearer token ของตัวเอง ภายใน 60 วินาที รหัสถูกใช้ได้ครั้งเดียว สิ่งที่ได้กลับมาคือการผูก — installation, ไซต์, action, content instance (ไม่บังคับ) — และตัวตนที่ปลอดภัยต่อการแสดงผลสำหรับ session ของปลั๊กอินเอง มันไม่ได้มอบ capability ใดของ Dee Wan ที่ token ของ installation ยังไม่มีอยู่แล้ว และรหัสที่ออกให้ installation อื่นจะถูกปฏิเสธ
- ผู้เรียก: ปลั๊กอิน พร้อม token ของ installation
- ต้องการ: ไม่มี
- Body —
$defs/launch_exchange_request - คำตอบ:
{ "data": <delegation> }—$defs/launch_delegation
Outcome:
invalid_request— ไม่รู้จัก, ถูกใช้แล้ว, หมดอายุ หรือออกให้ installation อื่นplugin_unauthorizedrate_limitedtemporary_failure
เปลี่ยน launch code เป็น delegation
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 ไว้ ทุกคำสั่งหลังจากนี้จะแนบมันไป และ core จะอ่านอำนาจของบุคคลนั้นซ้ำ
launch code ใช้ได้ครั้งเดียว
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
หัวข้อที่มีชื่อว่า “Outcome”ทุก route ของปลั๊กอินตอบด้วยรหัสสำหรับเครื่องและข้อความสำหรับมนุษย์:
{ "outcome": { "code": "transition_applied", "message": "…", "retry": false }, "data": {}}ให้แตก branch ตาม code ข้อความมีไว้สำหรับคนและไม่เสถียร retry คือคำแถลงของ core เองว่าการลองอีกครั้ง
จะช่วยได้หรือไม่ และเป็นฟิลด์เดียวที่ runner ต้องใช้ในการตัดสินใจ
| HTTP | รหัส | Retry | ความหมาย |
|---|---|---|---|
| 200 | transition_applied |
ไม่ | transition ถูกนำไปใช้แล้ว |
| 200 | idempotent_replay |
ไม่ | คำสั่งนี้ถูกนำไปใช้แล้ว ผลลัพธ์แรกถูกส่งกลับ |
| 400 | invalid_request |
ไม่ | request ไม่ถูกต้องสำหรับ protocol v1 |
| 401 | plugin_unauthorized |
ไม่ | token ของ installation หายไป, ไม่รู้จัก, ถูก disable หรือถูก revoke |
| 403 | capability_refused |
ไม่ | installation นี้ไม่ได้รับ grant capability หรือโมเดลนั้น |
| 403 | requester_unauthorized |
ไม่ | บุคคลที่ร้องขอสิ่งนี้ไม่มีอำนาจที่ต้องใช้อีกต่อไป |
| 404 | content_not_found |
ไม่ | ไม่มีเนื้อหานี้ในขอบเขตของ installation นี้ |
| 409 | scheduled_target_stale |
ไม่ | version ที่ถูก pin ไม่ใช่ตัวที่ transition นี้จะเคลื่อนย้ายอีกต่อไป |
| 409 | workflow_conflict |
ไม่ | เนื้อหาเคลื่อนผ่าน workflow ของมันไปแล้วหลังจากที่มีการร้องขอนี้ |
| 409 | dependent_content |
ไม่ | มีเนื้อหาอื่นที่พึ่งพารายการนี้ บุคคลต้องตรวจทานการยกเลิกการเผยแพร่ใน Dee Wan |
| 409 | idempotency_key_reused |
ไม่ | idempotency key นี้ถูกใช้ไปแล้วกับคำสั่งอื่น |
| 422 | transition_unavailable |
ไม่ | transition นั้นใช้ไม่ได้จาก state ปัจจุบันของเนื้อหา |
| 422 | transition_refused |
ไม่ | lifecycle ปฏิเสธ transition นี้ ไม่มีอะไรถูกเปลี่ยน |
| 429 | rate_limited |
ใช่ | request มากเกินไปสำหรับ installation นี้ |
| 503 | temporary_failure |
ใช่ | Dee Wan ดำเนินการ request ไม่สำเร็จ ไม่มีสิ่งใดที่ทราบว่าถูกเปลี่ยน |
กฎการ retry
หัวข้อที่มีชื่อว่า “กฎการ retry”rate_limited— retry และปฏิบัติตามRetry-Afterให้ปฏิเสธรายการตั้งเวลาแทนการรอ หากเวลารอ เกินค่าสูงสุดของคุณเองtemporary_failureและความล้มเหลวของ transport (timeout, connection reset, ไม่มี response) — retry ด้วย exponential backoff ที่มีขอบเขตพร้อม jitter จนถึงเพดานจำนวนครั้งและอายุที่ระบุไว้ในเอกสาร- ทุกอย่างที่เหลือเป็นสถานะสุดท้าย (TERMINAL) อย่า retry และอย่าเปลี่ยนมันให้เป็นความสำเร็จ
- คำตอบที่ HTTP status และ
outcome.codeไม่ตรงกัน หรือ body ไม่ใช่ envelope ถือว่าไม่น่าเชื่อถือ: ให้ถือเป็นการปฏิเสธขั้นสุดท้ายแทนการเดา - การ retry หลัง outcome ที่ไม่ทราบผลปลอดภัยเพราะ
Idempotency-Key: replay ที่ตรงกันทุกประการจะคืน ผลลัพธ์แรกและไม่นำสิ่งใดไปใช้ซ้ำสองครั้ง มันไม่ใช่สิ่งทดแทน lease ของคุณเอง — worker สองตัวของคุณ ยังคงต้องไม่ดำเนินการรายการตั้งเวลาเดียวกันพร้อมกัน - ไม่มีสิ่งใดในนี้ที่เป็นการ retry ไม่รู้จบ และไม่มีความล้มเหลวที่ไม่ทราบผลใดถูกรายงานว่านำไปใช้แล้ว
Route ด้านการดูแลระบบ
หัวข้อที่มีชื่อว่า “Route ด้านการดูแลระบบ”นี่คือ route ฝั่งเบราว์เซอร์ที่อยู่หลัง session ของผู้ใช้, header ของไซต์ และสิทธิ์
site:plugins — surface ที่ติดตั้ง, grant, rotate และลบ พวกมันไม่ใช่ส่วนที่ปลั๊กอินเรียก; มีไว้ที่นี่
เพื่อให้ผู้เขียนปลั๊กอินรู้ว่าผู้ดูแลทำอะไร route ที่ทำ mutation แต่ละตัวจะเขียน audit entry และจะปฏิเสธ
(503 audit_unavailable) แทนที่จะ mutate โดยไม่มี audit
GET /api/plugins
หัวข้อที่มีชื่อว่า “GET /api/plugins”แสดงรายการ installation ทุก installation ที่ยังไม่ถูก revoke บนไซต์ แบบปกปิดข้อมูล: ไม่มี token ไม่มี hash
- ผู้เรียก: ผู้ใช้ที่มี
site:plugins - ต้องการ:
site:plugins - ไม่มี body
- คำตอบ:
{ "data": [<installation>] }—$defs/installation
Outcome:
- 200
- 403
missing_permission
GET /api/plugins/models
หัวข้อที่มีชื่อว่า “GET /api/plugins/models”โมเดลที่ grant ได้ id, ชื่อ และ slug ของทุกโมเดลเนื้อหาบนไซต์ สำหรับหน้าจอ grant GRANT เก็บ id ดังนั้นการเปลี่ยนชื่อยังคงรักษามันไว้ และโมเดลที่ถูกลบจะทำให้ส่วนนั้นของมันไม่มีผล
- ผู้เรียก: ผู้ใช้ที่มี
site:plugins - ต้องการ:
site:plugins - ไม่มี body
- คำตอบ:
{ "data": [{ "id", "name", "slug" }] }
Outcome:
- 200
- 403
missing_permission
GET /api/plugins/actions
หัวข้อที่มีชื่อว่า “GET /api/plugins/actions”action ของเนื้อหาที่จะ render คู่ { id, label } ที่มีขอบเขตซึ่ง installation ที่ active ประกาศไว้ เพื่อให้หน้าจอเนื้อหา render ปุ่มที่ core เป็นเจ้าของได้ ไม่มี markup, ไม่มี script, ไม่มี CSS, ไม่มี URL ของไอคอนใดมาจากปลั๊กอินเลย
- ผู้เรียก: สมาชิกใดก็ได้ของไซต์
- ต้องการ: ไม่มีอะไรนอกจากสิทธิ์เข้าถึงไซต์
- ไม่มี body
- คำตอบ:
{ "data": [{ "installation_id", "plugin_name", "actions": [{ "id", "label" }] }] }
Outcome:
- 200
POST /api/plugins/review
หัวข้อที่มีชื่อว่า “POST /api/plugins/review”ดึงและตรวจสอบ manifest ดึง URL ของ manifest ตรวจสอบมัน และคืนสิ่งที่มันขอ ไม่มีอะไรถูกเก็บและไม่มี installation เกิดขึ้นหลังจากนั้น; นี่คือขั้นตอนตรวจทานที่ทำให้การอนุมัติเป็นไปอย่างมีข้อมูล
- ผู้เรียก: ผู้ใช้ที่มี
site:plugins - ต้องการ:
site:plugins - Body —
{ "manifest_url": "https://…" } - คำตอบ:
{ "data": { "manifest": <manifest>, "protocol_version": 1 } }
Outcome:
- 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”ติดตั้ง ดึงและตรวจสอบ manifest อีกครั้ง บันทึก installation สถานะ pending พร้อม capability และ id ของโมเดลที่อนุมัติ ออก token และส่ง token ดิบพร้อม activation code แบบใช้ครั้งเดียวไปยัง activation_url ของ manifest installation จะกลายเป็น active ก็ต่อเมื่อได้ 2xx จากปลั๊กอินเท่านั้น; core เก็บ hash และไม่เคยเก็บ token activation ที่ล้มเหลวจะไม่ทิ้งอะไรไว้ และการติดตั้งปลั๊กอินเดียวกันเป็นครั้งที่สองบนไซต์เดียวกันคือ conflict ไม่ใช่ installation ที่สอง
- ผู้เรียก: ผู้ใช้ที่มี
site:plugins - ต้องการ:
site:plugins - Body —
{ "manifest_url", "activation_code", "capabilities": […], "model_ids": […] }capability ต้องเป็น subset ของคำขอใน manifest;model_idsที่ว่างหมายถึงไม่มีสิทธิ์เข้าถึงเนื้อหา ไม่ใช่ทุกโมเดล - คำตอบ:
{ "data": <installation> }, 201
Outcome:
- 201
- 400
invalid_activation_code/invalid_capabilities/invalid_model_ids/ reason ของ manifest - 409
installation_exists/backend_url_unset/activation_superseded - 502 พร้อม reason จากฝั่ง remote — ปลั๊กอินปฏิเสธหรือไม่สามารถติดต่อได้
- 503
audit_unavailable
PUT /api/plugins/:id/grant
หัวข้อที่มีชื่อว่า “PUT /api/plugins/:id/grant”เปลี่ยน grant แทนที่ capability และ id ของโมเดล มันทำได้เพียงจำกัดให้แคบลงภายในสิ่งที่ manifest ที่ เก็บไว้ ขอ — capability ที่อยู่นอกนั้นจะถูกปฏิเสธ ดังนั้น manifest ที่เปลี่ยนไปไม่สามารถขยาย grant ได้ การลดสิทธิ์มีผลใน request ถัดไปของปลั๊กอิน
- ผู้เรียก: ผู้ใช้ที่มี
site:plugins - ต้องการ:
site:plugins - Body —
{ "capabilities": […], "model_ids": […] } - คำตอบ:
{ "data": <installation> }
Outcome:
- 200
- 400
invalid_capabilities/invalid_model_ids - 404
- 409
installation_state_changed
POST /api/plugins/:id/upgrade
หัวข้อที่มีชื่อว่า “POST /api/plugins/:id/upgrade”ยอมรับ manifest เวอร์ชันใหม่ ดึง URL ของ manifest ที่เก็บไว้อีกครั้งและยอมรับ release ที่ผู้ดูแลเพิ่งตรวจทาน — expected_version ต้องตรงกับสิ่งที่ถูก serve มิฉะนั้นการตรวจทานถือว่าเก่าแล้ว id ของปลั๊กอิน, origin และเวอร์ชัน protocol ที่ยอมรับต้องไม่เปลี่ยน capability จะถูก ตัดกัน (INTERSECTED) กับคำขอใหม่; การอัปเกรดไม่เคยขยาย grant
- ผู้เรียก: ผู้ใช้ที่มี
site:plugins - ต้องการ:
site:plugins - Body —
{ "expected_version": "0.2.0" } - คำตอบ:
{ "data": <installation> }
Outcome:
- 200
- 400 reason ของ manifest
- 409
manifest_identity_changed/protocol_changed/manifest_changed_since_review
POST /api/plugins/:id/rotate
หัวข้อที่มีชื่อว่า “POST /api/plugins/:id/rotate”Rotate token ต้องใช้รหัสใช้ครั้งเดียวใหม่จากปลั๊กอิน เพราะการ rotate คือการส่งมอบ ไม่ใช่การรีเซ็ต core ออก token ส่งไปยัง activation origin ที่ถูก pin และหลังจากนั้นจึงสลับ hash หากปลั๊กอินปฏิเสธ token เก่ายังใช้งานได้ hash ก่อนหน้ายังใช้ได้ไม่เกิน 10 นาที และ request แรกที่แนบ token ใหม่ จะยุติช่วงซ้อนทับนั้นทันที
- ผู้เรียก: ผู้ใช้ที่มี
site:plugins - ต้องการ:
site:plugins - Body —
{ "activation_code": "…" } - คำตอบ:
{ "data": { "rotated": true } }
Outcome:
- 200
- 400
invalid_activation_code - 409
installation_not_active/backend_url_unset/installation_state_changed - 502 reason จากฝั่ง remote — และ token เก่า ยังใช้งานได้
POST /api/plugins/:id/disable
หัวข้อที่มีชื่อว่า “POST /api/plugins/:id/disable”Disable เก็บการตั้งค่าและ audit trail ไว้ และหยุด plugin API ทันที: ทุก request ตอบ plugin_unauthorized ก่อนการเข้าถึง tenant ใด ๆ
- ผู้เรียก: ผู้ใช้ที่มี
site:plugins - ต้องการ:
site:plugins - ไม่มี body
- คำตอบ:
{ "data": { "id", "state": "disabled" } }
Outcome:
- 200
- 404
- 409
installation_state_changed
POST /api/plugins/:id/enable
หัวข้อที่มีชื่อว่า “POST /api/plugins/:id/enable”Enable คืน installation ที่ถูก disable กลับเป็น active ด้วย token เดิมและ grant เดิม
- ผู้เรียก: ผู้ใช้ที่มี
site:plugins - ต้องการ:
site:plugins - ไม่มี body
- คำตอบ:
{ "data": { "id", "state": "active" } }
Outcome:
- 200
- 404
- 409
installation_state_changed
DELETE /api/plugins/:id
หัวข้อที่มีชื่อว่า “DELETE /api/plugins/:id”ถอนการติดตั้ง revoke ก่อน: state กลายเป็น revoked, hash ทั้งสองถูกทิ้ง และ token จะไม่กลับมาใช้ได้อีก การระบุที่มาใน audit ยังคงอยู่ เพราะ workflow event เก็บ snapshot ของ installation แทนที่จะเป็น foreign key Dee Wan ไม่ อ้างว่าการนี้ลบสิ่งใดที่ปลั๊กอินถืออยู่ — การลบฝั่ง remote เป็น action ของปลั๊กอินเอง
- ผู้เรียก: ผู้ใช้ที่มี
site:plugins - ต้องการ:
site:plugins - ไม่มี body
- คำตอบ:
{ "data": { "id", "state": "revoked" } }
Outcome:
- 200
- 404
- 409
installation_state_changed
POST /api/plugins/:id/launch
หัวข้อที่มีชื่อว่า “POST /api/plugins/:id/launch”เปิดปลั๊กอินแบบ delegate ออกรหัสใช้ครั้งเดียว มีอายุ 60 วินาที เก็บแบบ hash ผูกกับ installation, ไซต์, ผู้ใช้, action และ instance (ไม่บังคับ) และคืน management_url พร้อมรหัสนั้น response มี Referrer-Policy: no-referrer และ Cache-Control: no-store action ต้องเป็น manage โดยไม่มี instance หรือเป็น id ของ content_actions จาก manifest ที่เก็บไว้พร้อม instance หนึ่งตัว cookie ของ session Dee Wan ไม่เคยออกจาก Dee Wan
- ผู้เรียก: สมาชิกของไซต์สำหรับ content action;
site:pluginsสำหรับmanage - ต้องการ: สิทธิ์อ่านรายการที่ระบุ สำหรับ content action
- Body —
{ "action": "manage" }หรือ{ "action": "<action id>", "instance_id": "…" } - คำตอบ:
{ "data": { "url": "https://…?dee_wan_launch=…" } }—$defs/launch
Outcome:
- 200
- 400
invalid_launch_action - 403
missing_permission(สำหรับmanage) /model_denied - 404
not_found - 409
installation_not_active
Schema ที่เครื่องอ่านได้
หัวข้อที่มีชื่อว่า “Schema ที่เครื่องอ่านได้”schemas/manifest-v1.schema.json— manifest ที่สร้างจากออบเจกต์ ที่ core ใช้ตรวจสอบทุก manifest ที่ดึงมาschemas/protocol-v1.schema.json— request, response, outcome, ขีดจำกัด และรูปแบบของ secret ที่สร้างจากนิยามชุดเดียวกัน
ทั้งสองถูกเขียนโดย npm run docs:plugins และถูก commit ไว้ มีการทดสอบที่เทียบสำเนาที่ commit ไว้กับ
สิ่งที่โค้ด render ออกมา ดังนั้นพวกมันจึงไม่สามารถอธิบาย protocol ที่ core นี้ไม่ได้ใช้