การเขียนปลั๊กอิน
ปลั๊กอินคือเซอร์วิสที่ deploy แยกต่างหาก Dee Wan ไม่โหลดโค้ดของปลั๊กอิน ไม่รัน JavaScript ของปลั๊กอิน ใน admin UI และไม่มอบ database binding ใด ๆ ให้ปลั๊กอิน — ปลั๊กอินคุยกับ Dee Wan ผ่าน HTTPS และ ไม่มีช่องทางอื่น เนื้อหาต่อไปนี้สร้างปลั๊กอินหนึ่งตัวตั้งแต่ต้นจนจบ: manifest, การติดตั้ง, grant, launch, คำสั่ง เผยแพร่แบบมีเงื่อนไข และการ uninstall
คุณต้องมีที่สำหรับรันเซอร์วิส HTTPS และที่สำหรับเก็บ state ของมัน Cloudflare Worker ที่มี
ฐานข้อมูล D1 คือสิ่งที่ปลั๊กอินอ้างอิง (reference plugin) ใช้ (plugins/scheduling/) และเอกสารนี้สมมติ
รูปแบบนั้น แต่ไม่มีสิ่งใดใน protocol v1 ที่บังคับเช่นนั้น
ทุก request และ response ด้านล่างถูก execute กับ route จริงโดย
backend/test/plugin-docs.test.ts id และ secret ทั้งหมดเป็นข้อมูลสังเคราะห์และจงใจให้ใช้ไม่ได้
- Protocol v1 — ทุก route, header, body, outcome และกฎการ retry
- ความปลอดภัยของปลั๊กอิน — token, delegation, ขอบเขต และ threat model
- การทดสอบปลั๊กอิน — checklist ของ contract และชุดติดตั้งแบบสองเซอร์วิสบนเครื่อง local
- แบบที่เครื่องอ่านได้:
schemas/manifest-v1.schema.json,schemas/protocol-v1.schema.json
สิ่งที่คุณต้องรับผิดชอบ
หัวข้อที่มีชื่อว่า “สิ่งที่คุณต้องรับผิดชอบ”| คุณเป็นเจ้าของ | Dee Wan เป็นเจ้าของ |
|---|---|
| เซอร์วิสของคุณ ฐานข้อมูล นาฬิกา และการ retry ของมัน | เนื้อหา, เวอร์ชัน, กฎ workflow, สิทธิ์, audit |
| browser session ของคุณเองและ UI ของคุณเอง | transition นั้นถูกต้องตามกฎหรือไม่ และการนำไปใช้แบบ atomic |
| การตัดสินใจว่าจะร้องขอเมื่อใด | การตัดสินใจว่าคำตอบคือใช่หรือไม่ |
| การลบข้อมูลของคุณเอง | การเพิกถอนสิทธิ์เข้าถึงของคุณ |
Dee Wan ไม่มีวันเชื่อคำกล่าวอ้างของปลั๊กอินว่า transition ได้รับอนุญาต ว่าเวอร์ชันเป็นปัจจุบัน หรือ ว่าการยกเลิกการเผยแพร่ปลอดภัย สิ่งเหล่านั้นถูกตรวจซ้ำภายในการเปลี่ยนแปลง (mutation)
1. ให้บริการ manifest
หัวข้อที่มีชื่อว่า “1. ให้บริการ manifest”manifest คือเอกสาร HTTPS หนึ่งฉบับที่อธิบายปลั๊กอินของคุณและสิ่งที่มันต้องการ Dee Wan ดึงมัน ครั้งเดียวตอน review และเก็บ snapshot ที่ได้รับการยอมรับไว้ มันไม่ถูกดึงใหม่ใน request ทั่วไป
{ "manifest_version": 1, "id": "com.example.myplugin", "name": "My Plugin", "version": "0.1.0", "protocol_versions": [1], "base_url": "https://plugin.example", "activation_url": "https://plugin.example/dee-wan/activate", "management_url": "https://plugin.example/installations/{installation_id}", "requested_capabilities": [ "content:read", "workflow:read", "workflow:publish", "workflow:unpublish" ], "content_actions": [{ "id": "schedule", "label": "Schedule" }]}กฎที่ควรรู้ก่อนเขียน ซึ่งถูกบังคับใช้ทั้งหมด:
idเป็นตัวระบุแบบ reverse-domain ที่คงที่ และมีอย่างน้อยสาม label มันไม่เปลี่ยนข้าม release ส่วนversionคือ release ของคุณและเปลี่ยนได้อย่างอิสระmanifest_version,protocol_versionsและversionเป็นสามสิ่งที่ต่างกัน- ทุก URL เป็น HTTPS ไม่มี credential และไม่มี fragment และทั้งหมด — รวมถึง URL ของ manifest
เอง — ต้องใช้ origin เดียว (ONE) origin ที่สองคือ
manifest_origin_mismatch - key ระดับบนสุดที่ไม่รู้จักจะถูกปฏิเสธ manifest ไม่สามารถพึ่งพาฟิลด์ที่ core นี้เพิกเฉยได้ ดังนั้นคุณ จึงไม่สามารถส่ง key ไปวันนี้แล้วหวังว่ามันจะได้รับการเคารพในภายหลัง
- redirect ถูกปฏิเสธทั้งตอนดึง manifest และตอน activation ให้บริการทั้งสองโดยตรง
- ปลายทาง private, loopback, link-local และ reserved ถูกปฏิเสธ สำหรับการพัฒนาบนเครื่อง local ดู การทดสอบปลั๊กอิน
content_actionsมีได้สูงสุดสี่คู่{ id, label }ที่มีขอบเขตจำกัด — ไม่มี markup ไม่มี script ไม่มี CSS ไม่มี URL ของไอคอน core render ปุ่มของตัวเองด้วยสไตล์ของตัวเองmanagement_urlอาจมี{installation_id}และใช้สำหรับการนำทางเท่านั้น มันไม่มีวันได้รับ credential อายุยาวใน URL
schema ฉบับเต็มซึ่งสร้างจาก object ที่ core ใช้ validate คือ
schemas/manifest-v1.schema.json
2. รับการส่งมอบตอน activation
หัวข้อที่มีชื่อว่า “2. รับการส่งมอบตอน activation”การติดตั้งคือการส่งมอบระหว่างสองฝ่าย ดังนั้นเซอร์วิสของคุณต้องมีหนึ่ง route ก่อนที่ใครจะติดตั้งมันได้:
activation_url ออก activation code แบบใช้ครั้งเดียว มอบให้ผู้ดูแลผ่านช่องทางอื่น (out of band) แล้ว
รอ เมื่อผู้ดูแลติดตั้ง Dee Wan จะ POST สิ่งนี้ไปยัง activation_url:
{ "kind": "activate", "installation_id": "plg_0000000000000000000000000000d1ee", "site_id": "cdocssite00000000000000001", "dee_wan_base_url": "https://cms.example", "protocol_version": 1, "token": "dwp_ExampleTokenNotRealExampleTokenNotRealExamp", "activation_code": "activation-code-0123456789"}สิ่งที่ handler ของคุณต้องทำ:
- ใช้
activation_codeครั้งเดียวเท่านั้น และปฏิเสธโค้ดที่ไม่รู้จัก ใช้ไปแล้ว หรือหมดอายุ นี่คือ สิ่งเดียวที่พิสูจน์ว่าผู้เรียกคือผู้ดูแลที่คุณให้โค้ดไป - ตรวจว่า
protocol_versionเป็นเวอร์ชันที่คุณรองรับ และdee_wan_base_urlเป็น origin ที่คุณยอมรับ - เก็บ
tokenเป็น secret ของคุณสำหรับ installation (การติดตั้ง Dee Wan หนึ่งชุด) นี้ — เข้ารหัสขณะอยู่นิ่ง (at rest) ไม่มีวันบันทึกลง log ไม่มีวันอยู่ใน URL หรือ error body - ตอบ 2xx core จะทำเครื่องหมาย installation เป็น
activeเฉพาะเมื่อนั้น และเก็บเฉพาะ hash SHA-256 ของ token เท่านั้น มันไม่สามารถแสดง token ได้อีก
kind ที่เป็น rotate คือการส่งมอบแบบเดียวกันสำหรับ installation ที่มีอยู่แล้ว: แทนที่ token ที่เก็บไว้
ทั้งสองแบบมีโค้ดมาด้วย เพราะ rotation คือการส่งมอบ ไม่ใช่การ reset
หาก handler ของคุณล้มเหลว จะไม่มีสิ่งใดหลงเหลืออยู่ในทั้งสองฝั่ง: core ลบ installation ที่รออยู่ และผู้ดูแลจะเห็นเหตุผล การ retry ไม่สร้าง installation ที่สอง
3. Review แล้วจึงติดตั้ง
หัวข้อที่มีชื่อว่า “3. Review แล้วจึงติดตั้ง”ผู้ดูแลที่มี site:plugins จะ review manifest ก่อน การ review ไม่เก็บสิ่งใดไว้ — มัน
มีไว้เพื่อให้การอนุมัติทำโดยรู้ข้อมูล

Review manifest
curl -X POST "https://cms.example/api/plugins/review" \ -H "X-Site-Id: cdocssite00000000000000001" \ -H "Content-Type: application/json" \ -b "__Secure-better-auth.session_token=$SESSION" \ --data '{"manifest_url":"https://plugin.example/dee-wan/manifest.json"}'{ "data": { "manifest": { "manifest_version": 1, "id": "com.example.myplugin", "name": "My Plugin", "version": "0.1.0", "protocol_versions": [1], "base_url": "https://plugin.example", "activation_url": "https://plugin.example/dee-wan/activate", "management_url": "https://plugin.example/installations/{installation_id}", "requested_capabilities": [ "content:read", "workflow:read", "workflow:publish", "workflow:unpublish" ], "content_actions": [{ "id": "schedule", "label": "Schedule" }] }, "protocol_version": 1 }}จากนั้นผู้ดูแลจึงติดตั้ง: capability ที่อนุมัติ, id ของโมเดลที่อนุมัติ และโค้ดแบบใช้ครั้งเดียวของคุณ
ชุดที่อนุมัติต้องเป็นส่วนย่อยของสิ่งที่ manifest ร้องขอ model_ids ที่ว่างหมายถึงไม่มีสิทธิ์เข้าถึงเนื้อหา
เลย — ไม่ใช่ทุกโมเดล
ติดตั้ง ซึ่งจะ activate ด้วย
curl -X POST "https://cms.example/api/plugins" \ -H "X-Site-Id: cdocssite00000000000000001" \ -H "Content-Type: application/json" \ -b "__Secure-better-auth.session_token=$SESSION" \ --data '{"manifest_url":"https://plugin.example/dee-wan/manifest.json","activation_code":"activation-code-0123456789","capabilities":["content:read","workflow:read","workflow:publish","workflow:unpublish"],"model_ids":["cdocsmodel0000000000000001"]}'{ "data": { "id": "plg_0000000000000000000000000000d1ee", "plugin_id": "com.example.myplugin", "plugin_name": "My Plugin", "plugin_version": "0.1.0", "protocol_version": 1, "state": "active", "capabilities": [ "content:read", "workflow:read", "workflow:publish", "workflow:unpublish" ], "requested_capabilities": [ "content:read", "workflow:read", "workflow:publish", "workflow:unpublish" ], "model_ids": ["cdocsmodel0000000000000001"], "content_actions": [{ "id": "schedule", "label": "Schedule" }], "manifest_url": "https://plugin.example/dee-wan/manifest.json", "management_origin": "https://plugin.example", "created_by_email": "admin@example.com", "created_at": "2026-01-01T00:00:00.000Z", "updated_at": "2026-01-01T00:00:00.000Z" }}ไม่มี token และไม่มี hash ปรากฏใน response นั้น ในการอ่านครั้งต่อ ๆ ไป หรือใน audit entry ที่การติดตั้ง เขียนไว้ เซอร์วิสของคุณถือสำเนาเพียงชุดเดียว
4. จำกัด grant ให้แคบลง
หัวข้อที่มีชื่อว่า “4. จำกัด grant ให้แคบลง”grant เปลี่ยนได้ทุกเมื่อ และเปลี่ยนได้เฉพาะภายในสิ่งที่ manifest ที่เก็บไว้ร้องขอ การลบ capability หรือโมเดลมีผลกับ request ถัดไปของคุณ — ไม่มี cache ให้ต้องรอ
นำ capability ที่ปรากฏว่าปลั๊กอินนี้ไม่ต้องใช้ออก
curl -X PUT "https://cms.example/api/plugins/plg_0000000000000000000000000000d1ee/grant" \ -H "X-Site-Id: cdocssite00000000000000001" \ -H "Content-Type: application/json" \ -b "__Secure-better-auth.session_token=$SESSION" \ --data '{"capabilities":["content:read","workflow:read","workflow:publish"],"model_ids":["cdocsmodel0000000000000001"]}'{ "data": { "id": "plg_0000000000000000000000000000d1ee", "plugin_id": "com.example.myplugin", "plugin_name": "My Plugin", "plugin_version": "0.1.0", "protocol_version": 1, "state": "active", "capabilities": ["content:read", "workflow:read", "workflow:publish"], "requested_capabilities": [ "content:read", "workflow:read", "workflow:publish", "workflow:unpublish" ], "model_ids": ["cdocsmodel0000000000000001"], "content_actions": [{ "id": "schedule", "label": "Schedule" }], "manifest_url": "https://plugin.example/dee-wan/manifest.json", "management_origin": "https://plugin.example", "created_by_email": "admin@example.com", "created_at": "2026-01-01T00:00:00.000Z", "updated_at": "2026-01-01T00:00:00.000Z" }}การขอ capability ที่อยู่นอก manifest ที่เก็บไว้คือ 400 invalid_capabilities ซึ่งเป็นเหตุผลที่
การอัปเดตปลั๊กอินไม่สามารถขยาย grant ที่มีอยู่ได้: การยอมรับ manifest ใหม่เป็นการกระทำที่แยกต่างหากและชัดเจน
(POST /api/plugins/:id/upgrade) และมันใช้ส่วนร่วม (intersection) แทนที่จะขยาย
5. เปิด UI ของคุณผ่าน delegated launch
หัวข้อที่มีชื่อว่า “5. เปิด UI ของคุณผ่าน delegated launch”UI สำหรับจัดการของคุณคือหน้าของคุณเอง ที่เปิดในแท็บระดับบนสุดแท็บใหม่ Dee Wan ไม่ฝังมัน และ session cookie ของ Dee Wan ไม่มีวันออกนอก Dee Wan launch จะออกโค้ดแบบใช้ครั้งเดียว ใช้ได้ 60 วินาที เก็บแบบ hash ผูกกับ installation, ไซต์, ผู้ใช้, action และ — สำหรับ action บนเนื้อหา — instance หนึ่งรายการ
ขอ launch URL
curl -X POST "https://cms.example/api/plugins/plg_0000000000000000000000000000d1ee/launch" \ -H "X-Site-Id: cdocssite00000000000000001" \ -H "Content-Type: application/json" \ -b "__Secure-better-auth.session_token=$SESSION" \ --data '{"action":"schedule","instance_id":"cdocsinstance00000000000001"}'{ "data": { "url": "https://plugin.example/installations/plg_0000000000000000000000000000d1ee?dee_wan_launch=dwl_ExampleLaunchCodeExampleLaunchCodeExampleLa" }}browser จะนำทางไปที่นั่น หน้าของคุณต้องไม่โหลด resource จากบุคคลที่สามใด ๆ ก่อนการแลกเปลี่ยน และ
launch response มี Referrer-Policy: no-referrer เพื่อไม่ให้โค้ดรั่วไหลผ่าน
referrer จากนั้นแบ็กเอนด์ (BACKEND) ของคุณจะแลกเปลี่ยนโค้ดด้วย installation token ของคุณ:
แลกเปลี่ยนโค้ดเป็น 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" } }}ตอนนี้ให้สร้าง session cookie ของคุณเอง (OWN) สำหรับ browser นั้น และเก็บ delegation_id ผูกไว้กับมัน
delegation ไม่ใช่ credential และไม่มอบสิ่งใดด้วยตัวเอง: มันระบุบุคคลที่ร้องขอ และ
ทุกคำสั่งที่คุณส่งในภายหลังจะแนบมันไปด้วย เพื่อให้ core อ่านสิทธิ์อำนาจของบุคคลนั้นใหม่ตอน execute
ดู ความปลอดภัยของปลั๊กอิน
6. อ่านเนื้อหา และ pin สิ่งที่คุณตั้งใจจะเปลี่ยนให้ตรงตัว
หัวข้อที่มีชื่อว่า “6. อ่านเนื้อหา และ pin สิ่งที่คุณตั้งใจจะเปลี่ยนให้ตรงตัว”ก่อนบันทึกความตั้งใจใด ๆ ให้อ่านรายการด้วย delegation บล็อก workflow บอกคุณ
ว่าบุคคลนั้นทำอะไรได้จริงในตอนนี้ และให้ค่าสามค่าสำหรับ pin
อ่านรายการหนึ่งรายการในฐานะบุคคลที่ร้องขอ
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" }] } }}เก็บสิ่งต่อไปนี้ไว้กับ record ของคุณเอง:
primary_instance_id— instance ที่ transition อ้างถึง การเผยแพร่มีผลกับ translation group ทั้งกลุ่ม และ primary คือตัวแทน (handle) ของกลุ่มcurrent_version_idสำหรับการเผยแพร่ หรือpublished_version_idสำหรับการยกเลิกการเผยแพร่ นั่นคือ pinworkflow.revisiontoและkindที่คุณเลือก ซึ่งต้องเป็นหนึ่งในtransitionsที่เสนอให้
7. ส่งคำสั่ง
หัวข้อที่มีชื่อว่า “7. ส่งคำสั่ง”การเปลี่ยนแปลงหนึ่งครั้ง โดยมีเงื่อนไขตามทุกสิ่งที่คุณ pin ไว้ พร้อม idempotency key ที่คุณสามารถสร้างซ้ำได้
เผยแพร่เวอร์ชันที่ถูก 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 }}สร้าง key จาก record ของคุณเองและ revision ของมัน — <installation>:<record>:r<revision> คือสิ่งที่
ปลั๊กอินอ้างอิงทำ — ไม่มีวันสร้างจากนาฬิกา จากนั้น:
- replay ที่ตรงกันทุกประการจะคืนผลลัพธ์แรกเป็น
idempotent_replayการ retry หลัง timeout จึงปลอดภัย - key เดียวกันกับ input ที่ต่างออกไปคือ
idempotency_key_reusedการตั้งเวลาใหม่คือ revision ใหม่ ดังนั้น จึงเป็น key ใหม่ - ทุกการปฏิเสธมีชื่อและเป็นแบบ terminal ยกเว้น
rate_limitedและtemporary_failureอ่านตาราง ใน Protocol v1 และจัดการแต่ละตัว อย่าเปลี่ยนความล้มเหลวที่ไม่รู้จัก ให้เป็นความสำเร็จ และอย่า retry ไปตลอดกาล
audit trail บันทึกสิ่งนี้ว่าเป็นการกระทำของปลั๊กอิน ไม่ใช่ของผู้ใช้: workflow event ระบุ installation, plugin id ชื่อและเวอร์ชันที่ได้รับการยอมรับของคุณ, เวอร์ชันของโปรโตคอล, digest ของ idempotency key และบุคคลที่ delegation ของเขาอนุญาตการกระทำนั้น admin UI แสดง ชื่อปลั๊กอินของคุณเป็นผู้กระทำ
8. Uninstall
หัวข้อที่มีชื่อว่า “8. Uninstall”การ uninstall จะ revoke ก่อน สถานะกลายเป็น revoked hash ของ token ทั้งสองถูกทิ้ง และ token นั้น
ไม่มีวันใช้ได้อีก
Uninstall
curl -X DELETE "https://cms.example/api/plugins/plg_0000000000000000000000000000d1ee" \ -H "X-Site-Id: cdocssite00000000000000001" \ -b "__Secure-better-auth.session_token=$SESSION"{ "data": { "id": "plg_0000000000000000000000000000d1ee", "state": "revoked" } }ทุก request หลังจากนั้น รวมถึง request ที่อยู่ระหว่างทางในคิวของคุณแล้ว
curl "https://cms.example/api/plugin/v1/context" \ -H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp"{ "outcome": { "code": "plugin_unauthorized", "message": "The installation token is missing, unknown, disabled or revoked.", "retry": false }}มีสองสิ่งตามมา และทั้งสองควรอยู่ในเอกสารของคุณเอง:
- การระบุผู้กระทำใน audit ยังคงอยู่ workflow event เก็บ snapshot ของ installation ของคุณแทนที่จะเป็น foreign key ดังนั้นการ uninstall จึงไม่สามารถลบว่าใครเผยแพร่อะไรได้
- Dee Wan ไม่อ้างว่าได้ลบสิ่งใดที่คุณถืออยู่ การลบฝั่งระยะไกลเป็นการกระทำของคุณ และ เอกสารของคุณต้องบอกว่าคุณเก็บอะไร เก็บนานเท่าใด และผู้ดูแลระบบ (operator) ลบมันได้อย่างไร คำตอบของ ปลั๊กอินอ้างอิงอยู่ใน Scheduling แบบ end to end
สิ่งที่ควรสร้างต่อไป
หัวข้อที่มีชื่อว่า “สิ่งที่ควรสร้างต่อไป”Protocol v1 จงใจไม่ให้ scheduler, คิว หรือ event stream แก่คุณ หากปลั๊กอินของคุณทำงาน ในภายหลัง คุณต้องรับผิดชอบ:
- lease เพื่อไม่ให้ worker สองตัวของคุณ execute record เดียวกันพร้อมกัน —
Idempotency-Keyคือ ตัวป้องกันชั้นที่สองของ core ไม่ใช่ชั้นแรกของคุณ - retry ที่มีขอบเขตพร้อม jitter เพดานจำนวนครั้ง และเพดานอายุ และเคารพ
Retry-Afterภายในค่าสูงสุด ที่คุณเลือก - ตารางความล้มเหลวแบบ terminal ที่ผู้ใช้ของคุณอ่านได้ เพราะทุกการปฏิเสธแบบมีเงื่อนไขหมายความว่ามีคน ต้องตัดสินใจบางอย่าง
- การอนุญาต (authorisation) ของคุณเองสำหรับ UI ของคุณเอง core ตรวจ launch หลังจากนั้น session เป็นของคุณ
plugins/scheduling/ คือตัวอย่างที่ทำจริงครบถ้วนของทั้งสี่ข้อ