ข้ามไปยังเนื้อหา
Dee Wan 1.0 ยังอยู่ระหว่างพัฒนาและยังไม่เปิดตัว (pre-MVP) — ดู ขอบเขตผลิตภัณฑ์ 1.0

ความปลอดภัยของปลั๊กอิน

installation token ของ Dee Wan คืออะไร ถูกเก็บและ rotate อย่างไร สิทธิ์อำนาจของบุคคลหนึ่งไปถึง คำสั่งของปลั๊กอินได้อย่างไร grant จำกัดขอบเขตอะไรบ้างจริง ๆ และผู้ที่ยึดปลั๊กอินได้สามารถทำอะไรได้ และทำอะไรไม่ได้

ทุกตัวอย่างในหน้านี้ถูกเล่นซ้ำกับ route จริงโดย test suite ของ Dee Wan เอง id และ secret ทั้งหมดเป็นข้อมูลสังเคราะห์

ดูเพิ่มเติมที่ Protocol v1 สำหรับรายละเอียดของแต่ละ route และ การเขียนปลั๊กอิน สำหรับขั้นตอนการติดตั้ง

browser ── user session ──► Dee Wan admin Worker
│ control database: installations, token hashes, grants, launch codes
│ tenant database: content, versions, workflow, audit
│
│ HTTPS + installation bearer token
plugin service ◄──────────────┘
│ its own database, its own clock, its own sessions
└── HTTPS + installation bearer token ──► /api/plugin/v1 ──► the same lifecycle service a person uses

คุณสมบัติสามข้อค้ำขอบเขตนั้นไว้:

  1. Plugin API ไม่ใช่ admin API มันอยู่ภายใต้ /api/plugin/v1 มี middleware ของตัวเอง และใช้ร่วมกันเพียง lifecycle service — ไม่มี implementation ที่สองของ transition ที่ อาจคลาดเคลื่อนไปจากตัวที่บุคคลใช้งาน
  2. UI ของปลั๊กอินทำงานภายในขอบเขตที่ตรวจทานแล้ว การ์ดแบบ declarative ใช้ตัวควบคุมของ core เฟรมที่เป็นตัวเลือกใช้ origin ของปลั๊กอินที่ตรวจทานแล้ว sandbox แยก และ MessageChannel ที่ผูกไว้ เฟรมไม่รับ admin session หรือ installation token การแก้ฟิลด์ใช้การ บันทึก ตามปกติ ส่วน operation ผ่าน การตรวจสิทธิ์ปัจจุบันจาก host ส่วน HTML/CSS/JS ภายนอกทำงานในเฟรม origin แบบ opaque แยกต่างหาก ทั้งใน preview และ renderer ที่รองรับ โดยเข้าถึงเอกสารแม่ของ admin หรือเว็บไซต์ไม่ได้
  3. ปลั๊กอินไม่มีสิทธิ์เข้าถึงฐานข้อมูล ปลั๊กอินไม่ได้รับ Prisma client ไม่ได้รับ D1 binding ไม่ได้รับ credential ของ R2 และไม่ได้รับสิทธิ์เข้าถึงบัญชี Cloudflare ทุกสิ่งที่มันอ่านหรือเปลี่ยนได้ต้องผ่าน route ภายใต้ /api/plugin/v1 และผ่านคำตอบที่ core ขอจากมัน — ผลของการดำเนินการ การตัดสินของนโยบาย หรือรายการจากแหล่งสื่อ — ซึ่ง core ตรวจสอบก่อนนำไปใช้
รูปแบบ dwp_ ตามด้วยอักขระแบบ URL-safe 43 ตัว — ความสุ่ม 256 bit จาก crypto.getRandomValues
ให้แก่ปลั๊กอิน ครั้งเดียว ในการส่งมอบตอน activation ผ่าน HTTPS ไปยัง origin ของ manifest เอง
core เก็บ เฉพาะ hash แบบ SHA-256 ไม่มีเส้นทางโค้ดใดที่พิมพ์ token ออกมาได้อีก
ปลั๊กอินส่ง Authorization: Bearer <token> ในทุก request
ไม่มีวันอยู่ใน URL, query parameter, error body, บรรทัด log, audit entry หรือ fixture
ขอบเขต การติดตั้งปลั๊กอินหนึ่งรายการ คือปลั๊กอินหนึ่งตัวบนเว็บไซต์หนึ่งแห่ง

prefix มีไว้เพื่อให้สตริงที่รั่วไหลถูกจดจำได้ว่าเป็น credential ของปลั๊กอิน Dee Wan ส่วน entropy คือสิ่งที่ทำให้มันเป็น credential ค่า bearer ที่ผิดรูปแบบจะถูกปฏิเสธจากรูปแบบก่อนการอ่านฐานข้อมูลใด ๆ ความพยายามเดาค่าที่ผิดรูปแบบจึงไม่ทำให้เกิดการค้นหาในฐานข้อมูล

rotation คือการส่งมอบ ไม่ใช่การ reset และต้องใช้โค้ดแบบใช้ครั้งเดียวใหม่จากปลั๊กอิน — มิฉะนั้น ใครก็ตามที่เข้าถึง admin route ได้ก็สามารถแทนที่ credential ได้อย่างเงียบ ๆ

  1. ผู้ดูแลได้รับ activation code ใหม่จากปลั๊กอิน
  2. core ออก token ใหม่และส่งมันพร้อมโค้ดนั้นไปยัง activation_url ที่ถูก pin ไว้จาก manifest ที่เก็บไว้ ไม่ใช่ไปยัง URL ที่ส่งมาตอน rotation
  3. core จะสลับ hash เฉพาะเมื่อได้ 2xx เท่านั้น หากปลั๊กอินปฏิเสธหรือติดต่อไม่ได้ token เก่า ยังคงใช้งานได้ — rotation ที่ล้มเหลวต้องไม่มีวันล็อก installation ที่ใช้งานได้อยู่ออกไป
  4. hash ก่อนหน้ายังใช้ได้นานสูงสุด 10 นาที เพื่อไม่ให้ request ที่กำลังส่งอยู่ ถูกตัดกลางคันระหว่าง rotation
  5. request แรกที่ถือ token ใหม่ (NEW) จะยุติช่วงซ้อนทับนั้นทันที ตั้งแต่วินาทีนั้น token เก่า จะได้คำตอบ plugin_unauthorized ซึ่งเป็นการปฏิเสธแบบ terminal

ขั้นตอนที่ 5 สำคัญหากปลั๊กอินของคุณรัน worker มากกว่าหนึ่งตัว หาก worker ตัวหนึ่งเริ่มใช้ token ใหม่ ขณะที่อีกตัวยังถือ token เก่าไว้ในหน่วยความจำ request ถัดไปของ worker ตัวที่สองจะถูกปฏิเสธด้วย 401 plugin_unauthorized แบบ terminal และหากมันสรุปผลงานของตนตามคำตอบนั้น งานนั้นจะสูญหาย ให้สลับ worker ทุกตัวไปใช้ token ใหม่พร้อมกัน หรือเมื่อได้ plugin_unauthorized ให้อ่าน token ที่เก็บไว้ใหม่หนึ่งครั้ง และ retry ด้วย token นั้นเฉพาะเมื่อมันเปลี่ยนไปแล้วนับตั้งแต่ส่ง request เท่านั้น

core ไม่มีวันเก็บและไม่มีวันบันทึก activation code ลง log ปลั๊กอินที่ออกโค้ดต้องการเพียงสิ่งที่ใช้ตรวจโค้ดได้: ปลั๊กอิน Scheduling เก็บ hash แบบ SHA-256 ของแต่ละโค้ดพร้อมเวลาหมดอายุ และไม่เคยเก็บตัวโค้ดเอง

ทั้ง disabled และ revoked หยุด plugin API ตั้งแต่หน้าประตู — ก่อนการเข้าถึง tenant ก่อนที่ body จะถูกอ่าน revoked เป็นสถานะสุดท้าย: hash ถูกทิ้ง และ token นั้นไม่มีวันใช้ได้อีก การ uninstall จะ revoke ก่อน และ Dee Wan ไม่รับรองสิ่งใดเกี่ยวกับข้อมูลที่ระบบมองไม่เห็น

UI สำหรับจัดการของปลั๊กอินเป็นหน้าแยกต่างหาก และ session cookie ของ Dee Wan ไม่มีวันออกนอก Dee Wan ตัวเชื่อมระหว่างทั้งสองคือ launch code:

  • สุ่ม ใช้ครั้งเดียว เก็บแบบ hash และใช้ได้ 60 วินาที
  • ผูกกับ installation, ไซต์, ผู้ใช้, action และ — สำหรับ action บนเนื้อหา — instance ของเนื้อหาหนึ่งรายการ
  • ถูกแลกเปลี่ยนโดยแบ็กเอนด์ (BACKEND) ของปลั๊กอิน ด้วย bearer token ของตัวเอง เพื่อรับ delegation record
  • โค้ดที่ออกให้ installation หนึ่งไม่สามารถถูกแลกเปลี่ยนโดยอีก installation หนึ่งได้
  • launch response มี Referrer-Policy: no-referrer หน้าจัดการของปลั๊กอินเองต้องส่ง header เดียวกัน ต้องไม่โหลด resource จากบุคคลที่สามใด ๆ ก่อนการแลกเปลี่ยน และต้อง redirect ไปยัง URL ที่ไม่มี dee_wan_launch เมื่อการแลกเปลี่ยนเสร็จแล้ว

core ตรวจสอบก่อนออกโค้ด: ผู้ร้องขอมี session และสิทธิ์เข้าถึงไซต์ manage แบบธรรมดาต้องการ site:plugins launch แบบ manage ที่ระบุ view ที่ประกาศไว้ (ปฏิทินบรรณาธิการหรือ release) ต้องการโมเดลหนึ่งใน grant ที่ผู้ร้องขออ่านได้ — ทุกรายการหรือเฉพาะรายการของตัวเอง — และ action บนเนื้อหาต้องการสิทธิ์อ่านรายการนั้นโดยเฉพาะ และจะถูกปฏิเสธ (model_not_granted) เมื่อโมเดลของรายการนั้นไม่อยู่ใน grant ของ installation admin UI เสนอ action บนเนื้อหาของปลั๊กอินเฉพาะกับรายการของโมเดลที่ได้รับ grant เท่านั้น delegation บันทึก model_ids คือโมเดลใน grant ที่ผู้ร้องขออ่านได้ด้วย read หรือ read:own (สำหรับ action บนเนื้อหา คือโมเดลของรายการนั้น) การอ่านและคำสั่งทุกครั้งหลังจากนั้นจะถูกปฏิเสธสำหรับรายการที่อยู่นอกโมเดลเหล่านั้น หรือรายการที่บุคคลนั้นอ่านไม่ได้แล้ว

delegation ไม่ใช่ credential มันไม่มี capability ใด ๆ สิ่งที่มันมีคือตัวตน — และ นั่นคือสิ่งที่ทำให้การอ่านสิทธิ์อำนาจใหม่เป็นไปได้:

คำสั่งของปลั๊กอินทุกคำสั่งระบุ delegation_id และ core จะอ่านสิทธิ์อำนาจปัจจุบัน (CURRENT) ของบุคคลนั้นใหม่ จาก control database ก่อนจะเขียนอะไรก็ตาม สิทธิ์อำนาจที่บันทึกไว้ตอนสร้างตารางเวลา จะไม่มีวันถูกส่งต่อไปใช้

delegation เข้าถึงอะไรได้บ้าง:

  • delegation แบบ content-action ผูกกับ instance หนึ่งรายการ และกระทำได้เฉพาะกับรายการใน translation group ของ instance นั้น — กลุ่มนี้เคลื่อนไปพร้อมกันเมื่อเผยแพร่ ดังนั้นนั่นคือหน่วย การระบุ มันกับรายการอื่นใดคือ requester_unauthorized
  • delegation แบบ manage ไม่มี instance มันเข้าถึงรายการได้ก็ต่อเมื่อโมเดลของรายการยังอยู่ใน grant อยู่ใน model_ids ของ delegation และบุคคลนั้นอ่านรายการนั้นได้ในตอนนี้ บุคคลที่อ่านได้เฉพาะรายการ ของตัวเองจะเข้าถึงได้เฉพาะรายการของตัวเอง กรณีอื่นคือ requester_unauthorized หรือ content_not_found
  • core ไม่ตรวจ action ของ delegation ในคำสั่ง action บอกปลั๊กอินว่าจะเปิดหน้าจอใด มันไม่ใช่สิทธิ์

delegation ไม่หมดอายุตามเวลา นั่นเป็นความตั้งใจ: ตารางเวลาที่สร้างวันนี้อาจรันในเดือนหน้า และยังต้องระบุตัวบุคคลที่ร้องขอได้ สิ่งที่จำกัดมันแทนคือ:

  • สิทธิ์อำนาจปัจจุบัน (CURRENT) ของบุคคลนั้น ซึ่งถูกอ่านใหม่ทุกคำสั่ง — เขาต้องยังเป็นผู้ใช้ที่ active ยังมีสิทธิ์เข้าถึงไซต์ และยังถือสิทธิ์ที่ transition นั้นต้องการ
  • installation — installation ที่ถูกปิดใช้งาน ถูก uninstall หรือ token ที่ไม่ถูกต้องอีกต่อไป จะหยุดทุก request ก่อนที่ delegation ใด ๆ จะถูกอ่าน
  • grant — โมเดลของรายการต้องยังได้รับ grant อยู่ และ capability ต้องยังถูกถืออยู่

ดังนั้นกรณีเหล่านี้ทั้งหมดจะถูกปฏิเสธตอน execute แม้ว่า delegation จะใช้ได้ตอนที่ถูกสร้าง: บุคคลนั้น เสียบทบาท (role) บนไซต์ บัญชีของเขาถูกปิดใช้งาน บทบาทของเขาไม่มีสิทธิ์ เผยแพร่อีกต่อไป รายการอยู่นอก translation group ของ delegation หรือ delegation เป็นของอีก installation หนึ่ง การอ่านที่มี delegation_id ตรวจ delegation ในลักษณะเดียวกัน: delegation ที่ใช้ไม่ได้จะถูกปฏิเสธด้วย requester_unauthorized แทนที่จะตอบด้วยรายการที่สั้นลงอย่างเงียบ ๆ และ delegation ที่ใช้ได้จะจำกัด transition ที่เสนอให้แคบลงเหลือเฉพาะสิ่งที่บุคคลนั้นทำได้ในตอนนี้

บุคคลที่ร้องขอไม่มีสิทธิ์อำนาจที่จำเป็นอีกต่อไป

หน้าต่างเทอร์มินัล
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_0009:r1" \
--data '{"kind":"publish","expected_current_version_id":"cdocsversion00000000000001","expected_workflow_revision":0,"delegation_id":"pld_0000000000000000000000000000d1ee"}'
{
"outcome": {
"code": "requester_unauthorized",
"message": "The person who requested this no longer holds the authority it needs.",
"retry": false
}
}

ไม่มีอะไรถูกเขียน นี่เป็นผลแบบ terminal: ปลั๊กอินต้องไม่ retry และต้องแสดงให้ผู้ใช้เห็นว่า ตารางเวลานี้ต้องการบุคคลที่มีสิทธิ์อำนาจที่ถูกต้อง

ข้อยกเว้นเดียวที่ตั้งใจไว้คือ replay Idempotency-Key ที่ถูกนำไปใช้แล้วจะคืน ผลลัพธ์แรกแม้ผู้ร้องขอจะเสียสิทธิ์อำนาจไปแล้วก็ตาม — งานเสร็จไปแล้ว และ การตอบซ้ำไม่ใช่การมอบสิทธิ์ใหม่

ไซต์มาจาก token และไม่มาจากสิ่งใดที่ผู้เรียกส่งมา ไม่มี X-Site-Id ใน request ของปลั๊กอิน ไม่มีไซต์ใน body และไม่มีชื่อฐานข้อมูลที่ใดเลย: ทั้งสองอย่างถูกปฏิเสธทันที แทนที่จะ ถูกเพิกเฉย ดังนั้น bug ในปลั๊กอินจึงไม่สามารถกลายเป็นการอ่านข้าม tenant ได้

การระบุชื่อฐานข้อมูล

หน้าต่างเทอร์มินัล
curl "https://cms.example/api/plugin/v1/context" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp" \
-H "X-D1-Binding: SOME_OTHER_TENANT"
{
"outcome": {
"code": "invalid_request",
"message": "A plugin request cannot name a database.",
"retry": false
}
}

สิทธิ์เข้าถึงโมเดลคือส่วนร่วม (intersection) ของ capability และรายการ id ของโมเดลเนื้อหาที่เก็บไว้ สิ่งที่อยู่นอก ขอบเขตจะตอบ content_not_found — คำตอบเดียวกับ “ไม่มีเนื้อหานี้” ดังนั้นปลั๊กอินจึงไม่สามารถทำแผนที่ โครงสร้างของไซต์ด้วยการสุ่มตรวจได้

id จากไซต์อื่น

หน้าต่างเทอร์มินัล
curl "https://cms.example/api/plugin/v1/content/cotherinstance0000000000001" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp"
{
"outcome": {
"code": "content_not_found",
"message": "No such content in this installation’s scope.",
"retry": false
}
}

grant ที่ถูกจำกัดให้แคบลงมีผลกับ request ถัดไปทันที:

หลังจากผู้ดูแลลบโมเดลออกจาก grant

หน้าต่างเทอร์มินัล
curl "https://cms.example/api/plugin/v1/content/cdocsinstance00000000000001" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp"
{
"outcome": {
"code": "content_not_found",
"message": "No such content in this installation’s scope.",
"retry": false
}
}

grant เก็บ id ของโมเดล ดังนั้นการเปลี่ยนชื่อโมเดลจะคง grant ไว้ และการลบโมเดลจะทำให้ส่วนนั้น ของ grant ไม่มีผล UI ด้านการดูแลระบบแสดง slug ปัจจุบันไว้ข้าง id

cookie ของผู้ใช้ไม่ใช่การยืนยันตัวตนของปลั๊กอิน route ของปลั๊กอินอ่าน Authorization และไม่อ่านอย่างอื่น ดังนั้น browser session ที่ถูกขโมยไปจึงไม่สามารถนำมา replay กับ plugin API ได้:

session cookie บน route ของปลั๊กอิน

หน้าต่างเทอร์มินัล
curl "https://cms.example/api/plugin/v1/context" \
-b "__Secure-better-auth.session_token=$SESSION"
{
"outcome": {
"code": "plugin_unauthorized",
"message": "The installation token is missing, unknown, disabled or revoked.",
"retry": false
}
}

ทิศทางกลับกันก็เป็นจริงเช่นกัน: token ของปลั๊กอิน ที่ถูกนำไปใช้กับ route ด้านการดูแลระบบจะถูกปฏิเสธในฐานะที่ไม่ได้ยืนยันตัวตน route เหล่านั้นต้องการ session ของผู้ใช้ header ของไซต์ และ site:plugins ปลั๊กอินไม่มีทั้งสามอย่าง

installation ที่ถูกปิดใช้งาน ก่อนการเข้าถึง tenant ใด ๆ

หน้าต่างเทอร์มินัล
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
}
}

คำตอบเดียวครอบคลุมทั้งกรณีไม่มี ไม่รู้จัก ถูกปิดใช้งาน และถูก revoke ไม่มีสิ่งใดบอกผู้เรียกว่าเป็นกรณีใด

request ถูกจำกัดต่อ installation — 120 ครั้งต่อช่วง 60 วินาที — โดยใช้กลไกบังคับใช้ พื้นฐานเดียวกับส่วนอื่นของผลิตภัณฑ์ และตอบกลับเป็น envelope ของโปรโตคอลเองพร้อม Retry-After สิ่งเหล่านี้คือการควบคุมการใช้งานในทางที่ผิด ไม่มีสิ่งใดในนี้ถูกวัด รายงาน หรือเก็บไว้เป็น telemetry

เกินขีดจำกัด

หน้าต่างเทอร์มินัล
curl "https://cms.example/api/plugin/v1/context" \
-H "Authorization: Bearer dwp_ExampleTokenNotRealExampleTokenNotRealExamp"
{
"outcome": {
"code": "rate_limited",
"message": "Too many requests for this installation.",
"retry": true
}
}

เคารพ Retry-After และปฏิเสธ record ของคุณเองแทนการรอ หากระยะหน่วงเกินค่าสูงสุด ที่คุณเลือกไว้ล่วงหน้า

core เรียกปลั๊กอินสี่เรื่อง: การดึง manifest การส่งมอบตอน activation หรือ rotation การเรียกใช้ การดำเนินการ นโยบาย หรือ media source ที่ประกาศไว้ และการดาวน์โหลดไฟล์จาก media source ที่บุคคลเลือก ทั้งหมดมีขอบเขตจำกัดและถูก pin ไว้

  • HTTPS เท่านั้น credential ใน URL และ fragment ถูกปฏิเสธ
  • ปลายทางถูกตัดสินจาก host name ใน URL:
    • IPv4 literal ถูกปฏิเสธหากเป็น private, loopback, link-local, shared (CGNAT), multicast, reserved, benchmarking หรืออยู่ในช่วงสำหรับเอกสาร (192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24) IPv4 literal สาธารณะถูกยอมรับ
    • การเขียนแบบฐานสิบและฐานสิบหก เช่น https://2130706433/ หรือ https://0x7f000001/ จะถูก URL parser แปลงเป็นรูปแบบมีจุดก่อน จากนั้นจึงถูกตัดสินแบบเดียวกัน
    • IPv6 literal ทุกตัวถูกปฏิเสธ
    • ชื่อที่ไม่มีจุดถูกปฏิเสธ รวมถึง localhost, localdomain, local, internal และ home.arpa และชื่อใดก็ตามที่ลงท้ายด้วยหนึ่งในนั้น (printer.local, db.internal)
  • redirect ถูกปฏิเสธ ไม่ถูกติดตาม 3xx คือ remote_redirect_refused
  • response body ถูกจำกัดที่ 32 KB และการเรียกที่ 5 วินาที การเรียกใช้การดำเนินการหรือนโยบายจำกัดที่ 64 KB และ 10 วินาทีสำหรับคำขอ การแลกรหัส และคำตอบรวมกัน
  • ทุก URL — manifest, base, activation, management, endpoint ของการดำเนินการและนโยบาย — ต้องใช้ origin เดียวกัน ดังนั้นไม่มีการเรียกใดถูกชี้ไปยังที่ที่ manifest ที่ผ่านการตรวจทานไม่ได้ระบุไว้
  • การเรียกใช้ไม่ส่งเนื้อหาและไม่ส่งข้อมูลรับรอง: มีเพียง id การติดตั้ง id การเรียกใช้ รหัสใช้ครั้งเดียว และเวลาหมดอายุ ปลั๊กอินแลกรหัสด้วยโทเค็นของตัวเอง core รับคำตอบจากการติดตั้งนั้นเท่านั้น หลังการแลก รหัสเท่านั้น และเฉพาะตอนที่การติดตั้งยังใช้งานอยู่และสิทธิ์ยังครอบคลุมการดำเนินการหรือนโยบายนั้น
  • การแลกรหัสของการดำเนินการจะคืน delegation_id ด้วย core บันทึกไว้พร้อมการเรียกใช้ โดยผูกกับผู้เรียก ไซต์ และการติดตั้ง และผูกกับโมเดลและรายการสำหรับการดำเนินการกับรายการ delegation ของ การดำเนินการระดับไซต์ไม่มีรายการ และเข้าถึงโมเดลที่ได้รับสิทธิ์ซึ่งผู้เรียกอ่านได้ เหมือน launch แบบ manage การดำเนินการระดับไซต์ต้องมี site:plugins อยู่แล้ว มันไม่ใช่รหัสและแลกที่ /launch/exchange ไม่ได้ การใช้ทุกครั้งถูกตรวจเหมือน launch delegation
  • response จากระยะไกลถูก parse เป็น input ที่ไม่น่าเชื่อถือเทียบกับ manifest schema key ที่ไม่รู้จักคือ การปฏิเสธ ไม่ใช่สิ่งที่จะเพิกเฉย
  • คำตอบของ media source ถูกรับเฉพาะเมื่อ thumbnail_url และ download_url ทุกตัวอยู่บน origin ของ manifest และ delivery_url อยู่บนหนึ่งใน delivery_origins ที่ source ประกาศ core ส่ง GET ไปยัง download_url ที่เลือกโดยไม่มี credential ไม่มี cookie และไม่มี redirect ภายใน 20 วินาทีและขีดจำกัดไฟล์สื่อ แล้วตรวจไบต์เหมือนการอัปโหลดทั่วไป ตัวเลือกโหลดพรีวิวจากปลั๊กอินด้วย referrerpolicy="no-referrer" ทั้งที่อยู่ของแอดมินและ credential ใด ๆ ของ Dee Wan จึงไม่ไปถึงปลั๊กอิน สำเนาเป็นไฟล์ของไซต์เอง ไม่มีสิ่งสาธารณะใดโหลดจากปลั๊กอิน ไฟล์ที่เพิ่มในโหมด reference เป็นข้อยกเว้นเดียว: core ไม่เคยดึง delivery_url ของมัน แต่หน้าที่เผยแพร่ สำเนาไซต์ และไซต์ที่สร้างโหลดจาก origin การส่งมอบที่ประกาศ origin นั้นจึงเป็นส่วนหนึ่งของ สิ่งที่เบราว์เซอร์ของผู้เยี่ยมชมไซต์ติดต่อ
  • Workers ไม่สามารถ resolve DNS ก่อน fetch ได้ ดังนั้นสิ่งที่ตรวจคือสิ่งที่ URL ระบุเอง ชื่อสาธารณะ ที่ resolve ไปเป็นที่อยู่ private อยู่นอกสิ่งที่การตรวจมองเห็น
  • HTTP บนเครื่อง local เป็นไปได้ เฉพาะ เมื่อ ENVIRONMENT=development และ origin ที่ตรงตัวอยู่ในรายการ PLUGIN_DEV_ORIGINS ไม่มี wildcard ไม่มีทางลัดสำหรับช่วง private ไม่มี fallback ใน production

ผู้ดูแลระบบใส่ URL ของแคตตาล็อกใน PLUGIN_CATALOGUE_URLS คั่นด้วยจุลภาคได้ ส่วน «ค้นหาปลั๊กอิน» ใน การตั้งค่า → ปลั๊กอิน อ่านค่านี้ ค่าเริ่มต้นไม่มี และ Dee Wan ไม่มี registry ในตัว

  • URL ของแคตตาล็อก หน้า next ทุกหน้า และ URL ทุกตัวในรายการ ต้องผ่านกฎเดียวกับ URL ของ manifest การเปลี่ยนเส้นทางถูกปฏิเสธ next ต้องอยู่บน origin เดียวกับหน้าแรก
  • สูงสุด 4 แคตตาล็อก แคตตาล็อกละ 5 หน้า หน้าละ 128 KiB หน้าละ 100 รายการ และแคตตาล็อกละ 200 รายการ แคตตาล็อกที่เกินขีดจำกัดถูกตัดออกทั้งหมดและแสดงเหตุผล
  • รายการหนึ่งมี id name และ manifest_url และมี summary categories homepage และ icon ได้ตามต้องการ คีย์ที่ไม่รู้จักถูกปฏิเสธ รายการที่ไม่ถูกต้องถูกข้ามและระบุตามตำแหน่ง
  • สองรายการที่มี id เดียวกัน: รายการแรกชนะ ตามลำดับที่ตั้งค่า แล้วตามลำดับหน้า แล้วตามลำดับรายการ
  • ไอคอนแสดงเป็นรูปภาพเท่านั้น โดยไม่ส่ง referrer
  • การอยู่ในแคตตาล็อกไม่ได้ให้สิทธิ์อะไร ตรวจ ส่ง URL ของ manifest จากรายการไปยังการตรวจตามปกติ และการติดตั้งจะดึงและตรวจ manifest อีกครั้ง รูปแบบของหน้าอยู่ใน Protocol v1 หัวข้อ GET /api/plugins/catalogue
  • แคตตาล็อกที่ล้มเหลวไม่กระทบปลั๊กอินที่ติดตั้งแล้ว และไม่กระทบการติดตั้งด้วย URL ของ manifest

ปลั๊กอินเป็นผู้กระทำ (actor) ไม่ใช่ผู้ใช้ ไม่มีการสร้างผู้ใช้ปลอม และไม่มีการเขียน installation id ลงใน foreign key ของผู้ใช้

transition ของปลั๊กอินบันทึก installation id, plugin id ที่ได้รับการยอมรับ ชื่อและเวอร์ชัน, เวอร์ชันของ โปรโตคอล, digest ของ idempotency key — ไม่ใช่ตัว key เอง — สถานะผลลัพธ์และ publication pointer และอีเมลของบุคคลที่ delegation ของเขาอนุญาตการกระทำนั้น ตัวตนของ installation อยู่ใน control database และ workflow audit อยู่ใน tenant database โดยไม่มี foreign key ข้ามฐานข้อมูล: event ของ tenant เก็บ snapshot ที่เปลี่ยนแปลงไม่ได้ นั่นคือเหตุผลเช่นกันว่าทำไม การ uninstall จึงไม่สามารถลบการระบุผู้กระทำได้

การกระทำด้านการดูแลระบบ — install, การเปลี่ยน grant, การยอมรับ manifest, rotation, disable, enable, uninstall — ถูกเขียนลงใน admin audit ledger โดยผู้ใช้ที่กระทำ และจะปฏิเสธ (503 audit_unavailable) แทนที่จะเปลี่ยนแปลงโดยไม่มี audit

การเขียนที่ปลั๊กอินทำแทนบุคคล — การบันทึกฟิลด์ การอัปโหลด media ข้อเสนอ — ทำงานในนามบุคคลนั้น: เวอร์ชันหรือแถว media เป็นของเขา และ admin audit ledger บันทึกเขาเป็นผู้กระทำ พร้อม id ของ installation และ id ของปลั๊กอินในการเปลี่ยนแปลง (plugin.content.save, plugin.media.upload, plugin.proposal.create) มันจะปฏิเสธ (temporary_failure) แทนที่จะเขียนโดยไม่มี audit ข้อเสนอ ยังเก็บชื่อปลั๊กอินและอีเมลของบุคคล ซึ่ง editor ของรายการแสดง working draft คือ งานที่ยังไม่บันทึกของบุคคลนั้นเอง: ไม่ใช่เวอร์ชันและไม่ใช่ audit และปลั๊กอินอ่านได้เฉพาะ draft ของบุคคลที่ delegation ของมันระบุ

การเรียกใช้การดำเนินการหรือการตรวจของนโยบายถูกบันทึกเป็น plugin.operation.run หรือ plugin.policy.check โดยผู้กระทำคือผู้ที่ขอ และใน change มีการติดตั้ง id ที่ประกาศ ผลที่จัดประเภทแล้ว และ digest ของข้อมูลเข้าและคำตอบ ตัวค่าและคำตอบเองไม่ถูกเก็บไว้ที่นั่น การเลือกของผู้ดูแลว่านโยบายที่ไม่ตอบหมายถึงอะไร ถูกบันทึกเป็น plugin.policy.mode

นโยบายเห็นข้อเท็จจริงของการเขียนครั้งเดียว — โมเดล รายการ ภาษา คีย์ของฟิลด์ที่เปลี่ยนและค่า — และไม่มีอะไรอื่น: ไม่มีข้อมูลรับรองและไม่มีเนื้อหาอื่น นโยบายตอบ allow, warning หรือ refuse นโยบาย save หรือ restore อาจตอบ transform แทน: ค่า ลิงก์ สื่อ หรือ SEO override ที่ใช้แทน สำหรับรายการและภาษาเดียวกับที่ถูกถาม core ตรวจ candidate ที่ถูกแปลงแล้วเหมือนข้อมูลที่บุคคลนั้นป้อนเอง และเก็บมันเป็นส่วนหนึ่งของการเขียนของบุคคลนั้น ไม่มีคำตอบของนโยบายใดเข้าถึงรายการอื่นหรือภาษาอื่น ย้ายเวิร์กโฟลว์ หรือแทนที่การตรวจของ Dee Wan เองซึ่งทำงานก่อน การเขียนจะบันทึกก็ต่อเมื่อเวอร์ชันที่นโยบายเห็นยังเป็นปัจจุบัน การเปลี่ยนแปลงระหว่างนั้นคือข้อขัดแย้งตามปกติ

หากปลั๊กอินถูกยึดครองทั้งหมด ผู้โจมตีจะถือ installation token หนึ่งตัวสำหรับไซต์หนึ่งไซต์ มันใช้ได้เฉพาะ capability ที่ผู้ดูแลให้ไว้ เฉพาะกับโมเดลที่ได้รับ grant และการเขียนทุกครั้งต้องระบุ delegation_id ซึ่งบุคคลนั้นยังมีสิทธิ์อำนาจสำหรับการเขียนนั้น ภายในขอบเขตที่ delegation นั้นเข้าถึงได้ (translation group เดียวสำหรับ action บนเนื้อหา; สำหรับ manage คือโมเดลที่ได้รับ grant ซึ่งบุคคลนั้นยังอ่านได้) ภายในขอบเขตนี้มันสามารถ:

  • ด้วย content:read: อ่านสรุป และเมื่อมี delegation ที่ยังใช้ได้ อ่านค่าที่บันทึกกับ revision ที่ยังเก็บไว้ ภายในขอบเขตโมเดล ฟิลด์ ภาษา และความเป็นเจ้าของของบุคคลนั้น
  • ด้วย workflow:publish, workflow:unpublish หรือ workflow:locale: เผยแพร่หรือยกเลิกการเผยแพร่เนื้อหา โดย pin เวอร์ชันที่ตรงตัว และตรงกับ workflow revision ปัจจุบัน
  • ด้วย content:write: บันทึกค่า ความสัมพันธ์ และสื่อของรายการที่มีอยู่; ด้วย content:create: สร้างรายการ; ด้วย content:delete: ย้ายรายการไปถังขยะ โดยผ่านการตรวจ lifecycle ตามสิทธิ์ของบุคคลนั้น
  • ด้วย draft:write: อ่าน เขียน และทิ้ง working draft ของบุคคลที่มอบ delegation เอง
  • ด้วย media:write: อัปโหลดไฟล์; ด้วย media:read: อ่านไฟล์และ metadata ที่มีอยู่; ด้วย media:update: แก้ metadata ภายในสิทธิ์สื่อปัจจุบันของบุคคลนั้น
  • ด้วย content:propose: สร้างข้อเสนอที่คนตรวจทาน และอ่านข้อเสนอของตัวเอง
  • ผ่านนโยบาย save หรือ restore ที่ได้รับ grant: แทนค่า ลิงก์ สื่อ หรือ SEO override ในการเขียนที่มันถูกถาม; ผ่านนโยบายใดก็ตามที่ได้รับ grant: เห็นข้อเท็จจริงของการเขียนนั้น และเตือนหรือปฏิเสธมัน
  • ผ่านการดำเนินการหรือแหล่งสื่อที่ได้รับ grant: ตอบสิ่งที่ core ถาม ซึ่ง core ตรวจสอบคำตอบ
  • เปิด route แลกเปลี่ยนด้วยโค้ดที่มันถืออยู่แล้ว

มันไม่สามารถ:

  • เข้าถึงไซต์อื่น โมเดลอื่น หรือ delegation ของ installation อื่น
  • ใช้ capability การดำเนินการ หรือนโยบายที่ไม่อยู่ใน grant
  • เขียนโดยไม่มี delegation หรือเกินกว่าที่บุคคลที่มอบ delegation ทำได้ในตอนนี้
  • อ่านผู้ใช้ บทบาท session การตั้งค่า นิยามโมเดล audit หรือรายการคลังสื่อทั้งหมด
  • อ่านค่าที่บันทึกหรือ revision นอกขอบเขตที่บุคคลผู้มอบ delegation อ่านได้
  • อ่านสื่อหรือแก้ metadata โดยไม่มี capability ของ installation และสิทธิ์สื่อของบุคคลนั้น ไฟล์ที่ป้องกันยังผ่านการตรวจไซต์และ visibility ตามเดิม
  • รันโค้ดในเอกสารแม่ของ admin หรือเข้าถึงฐานข้อมูล R2 หรือ credential ใด ๆ ของ Cloudflare
  • เผยแพร่ draft ที่ใหม่กว่าตัวที่ถูก pin ข้าม workflow ที่เปลี่ยนไปแล้ว override เนื้อหาที่ ขึ้นต่อกัน หรือใช้ override route ใด ๆ — ไม่มีเลยใน protocol v1
  • กระทำแทนบุคคลที่เสียสิทธิ์อำนาจไปแล้ว หรือปลอมแปลงบุคคล: delegation_id เป็นตัวระบุที่ใช้ค้นระเบียน ไม่ใช่การอ้างสิทธิ์
  • replay การเปลี่ยนแปลงให้เกิดผลครั้งที่สอง หรือบันทึกเวอร์ชัน อัปโหลดสื่อ หรือสร้างข้อเสนอโดยไม่มีบันทึก audit working draft ไม่ถูกบันทึกใน audit (ดู Audit)

delegation มีอายุยาวโดยการออกแบบ delegation ที่ได้มาวันนี้ยังใช้ได้อีกหลายเดือนข้างหน้า เพราะงานที่ตั้งเวลาไว้ต้องการเช่นนั้น ช่วงเวลานี้ถูกจำกัดด้วยบุคคลและ grant ไม่ใช่ด้วยนาฬิกา: มันปิดลงเมื่อบุคคลนั้นเสียสิทธิ์อำนาจหรือสิทธิ์เข้าถึงไซต์ เมื่อ grant เสียโมเดลหรือ capability หรือเมื่อ installation ถูกปิดใช้งาน ถูก uninstall หรือ token ถูก revoke ผู้ดูแล ที่ต้องการให้ delegation ที่ค้างอยู่ทั้งหมดของปลั๊กอินหยุดพร้อมกันจะ uninstall มัน

หาก launch code รั่วไหล — ผ่าน referrer, log หรือ URL ที่แชร์ — มันใช้ได้ครั้งเดียว อายุไม่เกิน 60 วินาที เก็บแบบ hash และไร้ประโยชน์หากไม่มี installation token ที่ใช้แลกเปลี่ยนมัน

เอเจนต์สร้างลิงก์เปิดได้ ข้อมูลรับรองการจัดการหรือแอปที่เชื่อมต่อซึ่งมี plugins:use ได้ลิงก์ เดียวกันผ่าน API การจัดการหรือ MCP สร้างโดยโค้ดเดียวกันด้วยการตรวจเดียวกัน ในนามผู้สร้างข้อมูลรับรอง คำตอบมีเพียงลิงก์และเวลาหมดอายุ ไม่เคยเป็นผลลัพธ์ของปลั๊กอิน delegation ที่ตามมาเป็นของผู้สร้าง จำกัดด้วยโมเดลของข้อมูลรับรองและถูกตัดด้วย scope: มีเฉพาะ capability ของ installation ที่ scope เหล่านั้นอนุญาต (content:read สำหรับ content:read และ workflow:read; lifecycle:publish สำหรับ workflow:publish, workflow:unpublish และ workflow:locale; content:write สำหรับ content:write, content:create และ draft:write; content:trash สำหรับ content:delete; media:write สำหรับ media:write, media:read, media:update และ media:source; content:propose) คำสั่งที่อยู่นอกเพดานนี้คือ capability_refused scope มีแต่จะตัดออก

agent เรียกใช้ operation ที่ประกาศไว้ได้ เมื่อมี plugins:use Management API และ MCP ยังเรียกใช้ operation แบบ interactive ที่ manifest ประกาศไว้และคืนผลลัพธ์ นี่คือผลลัพธ์เดียวจากปลั๊กอินที่ agent ได้รับ core ส่ง POST ไปยัง endpoint ของคุณพร้อมรหัสใช้ครั้งเดียว ไม่เคยส่งข้อมูลรับรองการจัดการหรือค่าของผู้เรียก คุณแลกรหัสด้วย token ของ installation เพื่อรับ input ที่ผูกไว้ ก่อนเรียกใช้ทุกครั้งและก่อนเล่นซ้ำผลลัพธ์ตามคีย์ core ตรวจสถานะปลั๊กอิน grant และสิทธิ์ปัจจุบันของผู้เรียกใหม่ audit เก็บ digest ของค่าและผลลัพธ์ ไม่ใช่ตัวค่า operation แบบ automation ไม่เคยถูกเสนอให้ agent

หากบัญชีผู้ดูแลถูกยึด ผู้โจมตีสามารถติดตั้งปลั๊กอินและมอบสิทธิ์ที่บัญชีนั้นมีอยู่ให้ปลั๊กอินได้ นั่น คือความเสี่ยงที่แท้จริงในการออกแบบนี้ และเป็นเหตุผลที่ site:plugins เป็นสิทธิ์แยกต่างหาก — ในบรรดาบทบาทที่ไซต์ใหม่มีตั้งแต่เริ่มต้น มีเพียง admin ที่ถือสิทธิ์นี้ — ไม่มีวันถูกรวมโดยนัยในสิทธิ์เผยแพร่ และเป็นเหตุผลที่ทุกขั้นตอนของ การติดตั้งถูก audit และทุก grant ถูกเลือกด้วยมือจากคำขอที่ระบุชัดเจนใน manifest

สิ่งที่ Dee Wan ไม่สามารถป้องกันได้ คือการดำเนินงานของปลั๊กอินเอง: uptime นาฬิกา นโยบาย retry สิ่งที่มันเก็บในฐานข้อมูล และมันลบข้อมูลของคุณหรือไม่เมื่อคุณขอ เอกสารของปลั๊กอินเองต้องตอบคำถามเหล่านั้น และผู้ดูแลต้องอ่านมันก่อนจะมอบ grant ใด ๆ ดู ปลั๊กอิน Scheduling สำหรับตัวอย่างว่า สิ่งนั้นมีหน้าตาอย่างไร

การสร้าง การลบ การอ่านสื่อที่มีอยู่ และการแก้ metadata ต้องได้รับ capability แยกกัน สิทธิ์ content:write และ media:write เดิมไม่เพิ่มอำนาจเหล่านี้ เป้าหมายความสัมพันธ์ที่เพิ่มหรือเปลี่ยน รวมถึงลิงก์ใน component ต้องอยู่ในทั้งรายการโมเดลของ installation และรายการโมเดลของ delegation ลิงก์ที่ไม่เปลี่ยนยังคงอยู่ได้เมื่อแก้ไขส่วนอื่น


ยังไม่เปิดตัว (pre-MVP) และยังไม่มีเวอร์ชันเสถียร
คอมมิตซอร์สโค้ด: e207e63239a627fbd2ae9c414ef7c6eb19fcfe5a (มีการแก้ไขที่ยังไม่คอมมิต)
วันที่สร้างเอกสาร (UTC): 2026-10-21T09:28:24.214Z
คอมมิตสำหรับอัปเกรด: ระบุไม่ได้: มีการแก้ไขที่ยังไม่คอมมิต. บันทึกการเปลี่ยนแปลง