Topics

整理 EmDash 1.0 的外掛程式規範~沙箱、功能權限、分散式登錄表~

  • column

2026年09月28日,Cloudflare 製造的 Astro 原生 CMS「EmDash」的 1.0 正式發布。其後 10 月 2 日發布 1.1,10 月 7 日發布 1.2,短短不到兩週內連續推出更新。

內部部落格「ゆるろぐ」從 0.22 升級到 1.2 的內容寫在這篇文章裡。這次要整理的是 1.0 中變化最大的「外掛程式的機制」。

关于 EmDash 是什麼樣的 CMS,可以看最初的文章。
簡單來說就是「用 Astro + Cloudflare 重新製作的 WordPress」。

1.0 中外掛程式如何改變

0.x 時代的外掛程式簡單來說就是「寫在 Astro 設定檔中的 npm 套件」,由於與網站在同一個行程中運行,外掛程式擁有與網站程式碼相同的權限,就像 WordPress 一樣可以存取資料庫和環境變數。

1.0 版本導入了沙盒和分散式登錄機制。
這是一個非常重大的改進。

  • 外掛程式預設在隔離的環境中執行,無法存取網站的內容、媒體、使用者、機密或網路。
  • 所需的權限(Capability)由外掛程式端宣告,由網站管理員在安裝時核准。
  • 核准的權限不只是「寫在紙上」,執行時環境會實際強制執行。
  • 外掛程式可以從管理介面一鍵安裝。分發源是 AT Protocol 上的分散式登錄檔

Cloudflare 的部落格用 WordPress「所有外掛程式共享同一個行程」的模式來對比說明這一點。
越是了解 WordPress 外掛程式引發之事件的人,越會感受到這個論述的震撼。

外掛程式有兩種「執行方式」

首先要掌握的是,同一個外掛程式程式碼有信任(Trusted)和沙箱(Sandboxed)兩種執行模式。

Trusted

Sandboxed

登錄位置

astro.config.mjs 的 plugins: []

sandboxed: [] 或從管理介面安裝

執行環境

與網站相同的行程

隔離的 V8 isolate

Capability

僅限宣告(不強制執行)

執行時強制

網路

無限制

已封鎖。僅允許寫入 allowedHosts 中的主機透過 ctx.http 存取

資料存取

完整資料庫存取

自己的儲存空間/KV,以及授權範圍內的內容

Node.js API

可使用

無法使用(僅限 Web API)

資源限制

無

CPU 50ms、子請求 10、30 秒、約 128MB

適合的用途

自有程式碼、經過審查的 npm 套件

第三方、登錄表分發

「隔離」的含義是,為每個外掛程式準備一個專用的小型執行環境,並將其與網站主體分開運行。
在 Cloudflare Workers 上,Dynamic Workers 的機制在請求時為外掛程式啟動 Worker。
從 1.0 版本開始,在 Node.js 上運行時,也會在單獨的程序中啟動與 Workers 相同的運行時(workerd),然後加載外掛程式,因此「沙箱不再限於 Cloudflare」。

關鍵是外掛程式的程式碼在兩種模式下都相同,因此通常可以在本地環境中以信任模式開發,在生產環境中放入沙箱。

外掛程式結構:在構建時和執行時分離檔案

EmDash 外掛程式分為兩個部分,執行時機不同。

  1. descriptor(描述子):id、version、所需的 Capability、storage 定義等中繼資料,由 astro.config.mjs 匯入,因此 在構建時在 Vite 中執行。不能具有副作用。
  2. definition(定義):hooks 或 routes 的内容。在请求时在服务器上运行
my-plugin/
├── src/
│   ├── index.ts          # descriptor。astro.config.mjs から import される
│   └── sandbox-entry.ts  # definePlugin({ hooks, routes })。実行時にロードされる
└── package.json          # "." と "./sandbox" を exports で分ける
// src/index.ts
import type { PluginDescriptor } from "emdash";

export function myPlugin(): PluginDescriptor {
	return {
		id: "my-plugin",
		version: "1.0.0",
		format: "standard",
		entrypoint: "@my-org/my-plugin/sandbox",
		capabilities: ["content:read", "network:request"],
		allowedHosts: ["api.example.com"],
		options: {},
	};
}
// src/sandbox-entry.ts
import { definePlugin, type PluginContext } from "emdash";

export default definePlugin({
	hooks: {
		"content:afterSave": async (event, ctx: PluginContext) => {
			ctx.log.info(`saved ${event.collection}/${event.content.id}`);
		},
	},
});

在 descriptor 端用 entrypoint 写入 definition 的模块路径,EmDash 在运行时加载它。虽然想把它放在同一个文件中,但 Vite 的构建会将运行时代码卷入,所以分开才是正确做法。

Standard 和 Native

作为另一个维度,插件有 Standard 和 Native 两种格式。

  • Standard:上述结构的格式。管理界面 UI 由下文所述的 Block Kit(JSON)描述。既能在 Trusted 中运行,也能在 Sandboxed 中运行,可以发布到注册表。
  • Native:可以同时包含用 React 编写的管理界面、Portable Text(正文编辑器)的自定义块和用于网站端渲染的 Astro 组件。作为代价,只能在 Trusted 中运行,无法发布到注册表。

基本上用 Standard 编写,仅在「想向编辑器添加自定义块」或「想在网站 HTML 中插入组件」时才使用 Native。

在 Yuru Log 中,我们使用 Native 插件自制了 Amazon 产品卡和视频嵌入。这两者都是通过向编辑器添加专用块,然后用 Astro 组件渲染的,原理上无法在沙箱中实现(因为在运行时安装的插件无法引入构建时所需的 Astro 组件)。

Capability:权限词汇表

Capability 是沙箱的核心。插件以字符串形式声明「想访问什么」,管理员在安装时的对话框中确认和授予权限。

1.x 中,詞彙已**按照 <對象>:<操作> 的形式整理**。以下是一些常用的例子。

Capability

可以做的事

content:read / content:write

內容的讀取 / 建立、編輯、刪除

content:publish

發布、不發布、排程

media:read / media:write

媒體的讀取 / 上傳和刪除

network:request

對外部發送 HTTP 請求。僅限 allowedHosts 中指定的主機

admin.editor-draft:read / patch

讀取編輯中的未保存內容 / 提出修正建議

hooks.content-policy:register

審查內容並阻止發布

此外還有分類、評論、重新導向、使用者、郵件發送等,共 27 種類。

storage 和 KV 無需宣告,隨時可用。各外掛程式有獨立的作用域,看不到其他外掛程式的資料。

0.x 的名稱(read:content、network:fetch 等)已被棄用,但仍可作為別名使用。執行期會將其正規化為現行名稱,因此現有外掛程式可直接執行,在組建時會發出警告。

Hooks:可以對哪些事件進行反應

1.2.0 的執行時中定義了 30 種類的 Hook。包括內容的保存、發布、排程、刪除,媒體上傳,郵件發送,評論投稿與審核,外掛本身的安裝與啟用,以及透過 cron 進行的定期執行。相比 0.x 版本增加了不少,特別是發布前後(content:beforePublish / afterPublish)現在是分開的 Hook,這是一項重大變化。

在 content:beforeSave 中執行 throw 可以取消保存,所以驗證類的外掛應寫在這裡。若想要停止發布本身,需要宣告 hooks.content-policy:register,然後使用發布前的 Hook。

1.2 版本的變更中有項容易被忽視但很重要的改動:升級後首次啟動時,現有外掛的 plugin:install 和 plugin:activate 會重新執行。在 install Hook 中寫入初始資料的外掛,必須寫成即使重複投入也不會損壞的形式。

Block Kit:不執行 JavaScript 的管理介面

在沙箱中,外掛的 JavaScript 無法在瀏覽器中執行。因此管理介面用 Block Kit 以 JSON 描述。外掛返回「標題、表格、表單、切換」等區塊的陣列,EmDash 側負責渲染。

流程如下。使用者打開外掛頁面時,EmDash 向外掛發送「頁面已打開」的請求,外掛返回區塊陣列。使用者提交表單時,這次是帶著輸入值的請求,外掛保存該資料後返回新的區塊陣列。僅透過請求與回應就能成立 UI,這種設計與沙箱的限制相容性很好。

除了管理介面頁面和儀表板小工具之外,1.x 版本還能用 Block Kit 製作編輯器旁的面板(editorPanels)。這是用來顯示各文章的狀態和操作於編輯畫面上。例如翻譯狀態顯示、SEO 或文案檢查、連結失效檢查、從本文生成摘要等,針對「正在編寫的文章」的功能都可在此整合。

去中心化登錄:沒有中央市集設計

外掛的發布機制也在 1.0 版本改變了。從使用者的角度看只是「在管理介面中搜尋,一鍵安裝」,但底層是建構在 Bluesky 的基礎(AT Protocol)上,具有相當獨特的架構。

  • プラグイン的資訊存放在作者的帳戶中。作者使用 Bluesky 相關帳戶登入,對套件和發佈資訊進行簽名,並將其放在自己帳戶的資料區域中。不會存放在註冊表營運者的伺服器上
  • Cloudflare 的註冊表只是「目錄之一」。plugins.emdashcms.com只是彙整各作者公開的資訊以便搜尋,任何人都可以蒐集相同資訊來建立另一個目錄
  • 安裝時驗證內容。EmDash 會檢查下載的外掛「是否與作者簽名公開的內容相同」。檢驗檔案雜湊值、套件名稱與版本、要求的 Capability 是否與簽名資訊相符,若是由 GitHub Actions 建立的,也會驗證該建置紀錄。由目錄方替換或遭竄改的內容會在此被過濾掉
  • 審核只會「隱藏」內容。Cloudflare 的目錄使用 AI 模型(Clef)審查說明文字、連結和圖片,如有問題就不在目錄中顯示。但無法改變外掛內容或從作者那裡奪取所有權

也就是說「即使註冊表營運者消失,只要作者帳戶中保存了資訊,配布就會繼續」「營運者可以從目錄中隱藏,但無法竄改」的模式。
不同於 npm 和 WordPress.org 的中央市場,這是在沒有中央市集的前提下設計的特色。

目前只提供免費外掛,付費外掛則在 AT Protocol 有進展時,將採用同樣的分散式模型對應

翻譯機制應該可以做成外掛

ゆるろぐ 上我們運行著從日文文章用 LLM 生成英文草稿的機制(用 EmDash 建立多語言部落格的故事)。目前是用 Node 腳本,從 .env 讀取 EmDash 的 API 令牌和翻譯 API 金鑰,取得日文文章進行翻譯,然後建立帶有 translationOf 的英文草稿

將此套用到 1.x 的外掛規格,幾乎就直接變成 Standard 外掛了

  • 觸發器:content:afterPublish 鉤子。日文文章發佈時就會啟動
  • 文章讀寫:content:read和content:write。翻譯結果建立為草稿
  • 翻譯 API 呼叫:network:request,以及在allowedHosts中新增一個 API 主機位址
  • API 金鑰:在settingsSchema的secret欄位中。從管理畫面輸入並加密儲存。不再需要.env或發行 API 令牌
  • 翻譯狀態:在 storage 中儲存「翻譯了原文的哪個版本」,在editorPanels的文章編輯畫面中顯示「有英文版本/原文已更新,需要重新翻譯」

原本需要手動執行的指令碼,現在只需要管理者批准 Capability 就能執行。而且執行時會保證翻譯 API 以外的主機無法存取。「處理 API 金鑰的程式碼要放在哪裡」這個煩惱從根本上消失了,這就是這個機制的優點所在。

設計上需要注意一點。沙盒中的外掛程式每次向主機提出請求(讀取設定、取得文章、寫入 storage)時,都會計為一次子請求,預設上限是 10 次。翻譯單篇文章應該還在範圍內,但如果要「批量翻譯未翻譯的文章」這樣的處理,在迴圈中執行ctx.content.get()很快就會達到上限。可能需要透過 cron 鉤子分次處理的設計。

另外,像切換語言這樣的網站端 UI 無法從沙盒產生,那部分需要在主題中實作。

總結

  • EmDash 1.0 的外掛程式以沙盒為前提。宣告 Capability、管理者批准、執行時強制執行
  • 同一份程式碼可在 Trusted 和 Sandboxed 兩種模式下執行。開發階段用 Trusted、正式環境用沙盒,這是基本做法
  • 如果需要編輯器的自訂區塊或 Astro 元件,使用 Native(僅限 Trusted)
  • descriptor(建置時)和 definition(執行時)必須分開在不同檔案中
  • Capability 的名稱在 1.x 版本中改為 content:read 格式。舊名稱仍以別名運作,但已不推薦使用
  • 登錄表是建立在 AT Protocol 上的分散型系統。目錄可以隱藏,但無法被竄改
  • 在沙箱中「一次主機呼叫 = 一次子請求」,上限為 10 次。需要意識到次數限制進行設計

在 WordPress 中,每安裝一個外掛程式,您就等於接受了「該外掛程式擁有網站的全部權限」。
只要存在一個有漏洞的外掛程式,整個網站都會面臨風險。

EmDash 1.0 針對沙箱型外掛程式進行了改進,在安裝時會顯示外掛程式可用的權限,執行時只允許已批准範圍內的操作。
對於尋求從 WordPress 遷移方案的使用者來說,外掛程式的安全性會成為選擇 EmDash 的重要原因之一。

目前外掛程式的數量仍然不多,還遠未達到 WordPress 那樣「隨處可得」的狀態,但我們期待外掛程式數量能逐漸增加。
既然公司內部網站已累積了實際運營經驗,我們打算將其納入公司提供的服務中。

本文作者

從 DTP 跨足 Web 世界,轉眼間便掌握了標記語言、frontend 開發、專案指導,以及 accessibility 等各項技能——是名真正的「技術高人」。自 Liberogic 創立初期便展現多才多藝的能力,如今儼然成為公司內的活字典。最近正沉迷於利用 AI 提示詞探索「accessibility 對應能否更多依靠 AI?」的效率化研究。技術與思維都在不斷進化中。

二俣 歩

IAAP 認證網頁無障礙專家(WAS)/ 標記語言工程師 / Frontend 工程師 / 網頁總監

查看此員工的文章

信心十足的團隊體制與迅速的應對能力是我們的優勢

Liberogic 擁有經驗豐富的人員積極推進專案,因而獲得客戶的高度評價。
我們恰當地安排專案經理和總監,致力於順利推進整個專案。 我們避免不必要的全面投入而導致成本增加,而是採用適材適所配置資源的方式,因此在業務把握到估價制作與提交的速度上也備受好評。

※ 請注意,我們不積極進行 SES 形式的駐場業務。

Slack、Teams、Redmine、Backlog、Asana、Jira、Notion、Google Workspace、Zoom、Webex 等幾乎所有主要的專案管理工具和聊天工具都可供您使用。

請諮詢我們解決您的網站問題。

案例分析