ข้ามไปยังเนื้อหา
Pre-MVP โค้ดเบส 1.0 ยังไม่ใช่ผลิตภัณฑ์ที่เผยแพร่ — ดู ขอบเขตผลิตภัณฑ์ 1.0

การเขียนปลั๊กอิน

ปลั๊กอินคือเซอร์วิสที่ 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 ทั้งหมดเป็นข้อมูลสังเคราะห์และจงใจให้ใช้ไม่ได้

คุณเป็นเจ้าของ Dee Wan เป็นเจ้าของ
เซอร์วิสของคุณ ฐานข้อมูล นาฬิกา และการ retry ของมัน เนื้อหา, เวอร์ชัน, กฎ workflow, สิทธิ์, audit
browser session ของคุณเองและ UI ของคุณเอง transition นั้นถูกต้องตามกฎหรือไม่ และการนำไปใช้แบบ atomic
การตัดสินใจว่าจะร้องขอเมื่อใด การตัดสินใจว่าคำตอบคือใช่หรือไม่
การลบข้อมูลของคุณเอง การเพิกถอนสิทธิ์เข้าถึงของคุณ

Dee Wan ไม่มีวันเชื่อคำกล่าวอ้างของปลั๊กอินว่า transition ได้รับอนุญาต ว่าเวอร์ชันเป็นปัจจุบัน หรือ ว่าการยกเลิกการเผยแพร่ปลอดภัย สิ่งเหล่านั้นถูกตรวจซ้ำภายในการเปลี่ยนแปลง (mutation)

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

การติดตั้งคือการส่งมอบระหว่างสองฝ่าย ดังนั้นเซอร์วิสของคุณต้องมีหนึ่ง 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 ของคุณต้องทำ:

  1. ใช้ activation_code ครั้งเดียวเท่านั้น และปฏิเสธโค้ดที่ไม่รู้จัก ใช้ไปแล้ว หรือหมดอายุ นี่คือ สิ่งเดียวที่พิสูจน์ว่าผู้เรียกคือผู้ดูแลที่คุณให้โค้ดไป
  2. ตรวจว่า protocol_version เป็นเวอร์ชันที่คุณรองรับ และ dee_wan_base_url เป็น origin ที่คุณยอมรับ
  3. เก็บ token เป็น secret ของคุณสำหรับ installation (การติดตั้ง Dee Wan หนึ่งชุด) นี้ — เข้ารหัสขณะอยู่นิ่ง (at rest) ไม่มีวันบันทึกลง log ไม่มีวันอยู่ใน URL หรือ error body
  4. ตอบ 2xx core จะทำเครื่องหมาย installation เป็น active เฉพาะเมื่อนั้น และเก็บเฉพาะ hash SHA-256 ของ token เท่านั้น มันไม่สามารถแสดง token ได้อีก

kind ที่เป็น rotate คือการส่งมอบแบบเดียวกันสำหรับ installation ที่มีอยู่แล้ว: แทนที่ token ที่เก็บไว้ ทั้งสองแบบมีโค้ดมาด้วย เพราะ rotation คือการส่งมอบ ไม่ใช่การ reset

หาก handler ของคุณล้มเหลว จะไม่มีสิ่งใดหลงเหลืออยู่ในทั้งสองฝั่ง: core ลบ installation ที่รออยู่ และผู้ดูแลจะเห็นเหตุผล การ retry ไม่สร้าง installation ที่สอง

ผู้ดูแลที่มี site:plugins จะ review manifest ก่อน การ review ไม่เก็บสิ่งใดไว้ — มัน มีไว้เพื่อให้การอนุมัติทำโดยรู้ข้อมูล

Settings → Plugins หลัง Review: capability ที่ร้องขอและโมเดลของไซต์ แต่ละรายการเป็น checkbox อยู่เหนือ activation code

Review manifest

Terminal window
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 ด้วย

Terminal window
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 ที่การติดตั้ง เขียนไว้ เซอร์วิสของคุณถือสำเนาเพียงชุดเดียว

grant เปลี่ยนได้ทุกเมื่อ และเปลี่ยนได้เฉพาะภายในสิ่งที่ manifest ที่เก็บไว้ร้องขอ การลบ capability หรือโมเดลมีผลกับ request ถัดไปของคุณ — ไม่มี cache ให้ต้องรอ

นำ capability ที่ปรากฏว่าปลั๊กอินนี้ไม่ต้องใช้ออก

Terminal window
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) แทนที่จะขยาย

UI สำหรับจัดการของคุณคือหน้าของคุณเอง ที่เปิดในแท็บระดับบนสุดแท็บใหม่ Dee Wan ไม่ฝังมัน และ session cookie ของ Dee Wan ไม่มีวันออกนอก Dee Wan launch จะออกโค้ดแบบใช้ครั้งเดียว ใช้ได้ 60 วินาที เก็บแบบ hash ผูกกับ installation, ไซต์, ผู้ใช้, action และ — สำหรับ action บนเนื้อหา — instance หนึ่งรายการ

ขอ launch URL

Terminal window
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

Terminal window
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

อ่านรายการหนึ่งรายการในฐานะบุคคลที่ร้องขอ

Terminal window
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 สำหรับการยกเลิกการเผยแพร่ นั่นคือ pin
  • workflow.revision
  • to และ kind ที่คุณเลือก ซึ่งต้องเป็นหนึ่งใน transitions ที่เสนอให้

การเปลี่ยนแปลงหนึ่งครั้ง โดยมีเงื่อนไขตามทุกสิ่งที่คุณ pin ไว้ พร้อม idempotency key ที่คุณสามารถสร้างซ้ำได้

เผยแพร่เวอร์ชันที่ถูก pin

Terminal window
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 แสดง ชื่อปลั๊กอินของคุณเป็นผู้กระทำ

การ uninstall จะ revoke ก่อน สถานะกลายเป็น revoked hash ของ token ทั้งสองถูกทิ้ง และ token นั้น ไม่มีวันใช้ได้อีก

Uninstall

Terminal window
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 ที่อยู่ระหว่างทางในคิวของคุณแล้ว

Terminal window
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/ คือตัวอย่างที่ทำจริงครบถ้วนของทั้งสี่ข้อ