这是针对插件注册表构建自有软件的高级主题。若只想在 EmDash 站点上安装插件,则不需要这些——在配置中启用注册表并使用管理仪表板即可。
注册表的发现侧是公开的只读 API。@emdash-cms/registry-client 包对其进行封装,因此你可以在 EmDash 之外构建插件目录、搜索页或发布动态。客户端可在任何提供 fetch 的环境运行——Node、Workers、浏览器或 Astro 站点。Astro 站点可使用 @emdash-cms/registry-loader,通过 live content collection 暴露相同数据。
安装
discovery 子路径不带认证或 OAuth 依赖。安装客户端并将其固定到确切版本:
npm install @emdash-cms/registry-client@0.6.0
列出与解析包
以下 Astro 页面列出注册表中的每个插件:
---
import { DiscoveryClient } from "@emdash-cms/registry-client/discovery";
const discovery = new DiscoveryClient({
aggregatorUrl: "https://registry.emdashcms.com",
});
const { packages } = await discovery.searchPackages({ q: "", limit: 50 });
---
<ul>
{
packages.map((pkg) => (
<li>
<a href={`/plugins/${pkg.handle ? `@${pkg.handle}` : pkg.did}/${pkg.slug}`}>
{pkg.profile?.name ?? pkg.slug}
</a>
{pkg.latestVersion && <span>v{pkg.latestVersion}</span>}
<p>{pkg.profile?.description}</p>
</li>
))
}
</ul>
当聚合器已解析时,会存在当前已验证的 handle。将 DID 形式作为后备保留,并在后续响应提供当前 handle URL 时重定向到它。发布者更改 handle 时,DID 与包 slug 仍是稳定的包身份。
精确的 q 值可以是 handle、DID,或任一身份后跟 /slug。例如,@example.com/my-gallery 选择一个包,example.com 返回该发布者的包。
使用 Astro live loader
安装 @emdash-cms/registry-loader,然后在 src/live.config.ts 中注册 loader:
import { registryLoader } from "@emdash-cms/registry-loader";
import { defineLiveCollection } from "astro:content";
export const collections = {
plugins: defineLiveCollection({ loader: registryLoader() }),
};
对有界结果使用 getLiveCollection("plugins", { q, limit }),对单个包使用 getLiveEntry("plugins", { publisher, slug })。当站点有 Astro 缓存提供方时,将返回的缓存提示传给 Astro.cache.set()。Astro live collections 不返回分页元数据,因此当界面需要下一个游标时,请直接使用 DiscoveryClient.searchPackages()。
包详情页按 DID 与 slug 获取插件,然后获取最新发布:
import { DiscoveryClient } from "@emdash-cms/registry-client/discovery";
const discovery = new DiscoveryClient({
aggregatorUrl: "https://registry.emdashcms.com",
});
export async function getPlugin(did: string, slug: string) {
const pkg = await discovery.getPackage({ did, slug });
const latest = await discovery.getLatestRelease({
did: pkg.did,
package: pkg.slug,
});
return { pkg, latest };
}
发现方法
客户端为每个聚合器查询公开一个方法:
searchPackages({ q, capability?, limit?, cursor? })— 自由文本搜索,可选过滤为声明给定访问类别的包。返回{ packages, cursor? }。resolvePackage({ handle, slug })— 从 handle 与 slug 解析包。getPackage({ did, slug })— 按 DID 与 slug 获取包。listReleases({ did, package, limit?, cursor? })— 按语义版本降序的发布,包括已撤回的发布。getLatestRelease({ did, package })— 聚合器选择的最高未撤回发布。
getPackage() 与 resolvePackage() 可返回 historicalReleaseCount 和
releaseHistoryComplete。这些字段描述聚合器保留的运营历史,
而非发布者签名的元数据。仅当 releaseHistoryComplete 为 true 时,
才将计数 1 视为首次发布。缺失或不完整的证据不得绕过发布年龄
策略。
getPackageStatus() 与 resolvePackageStatus() 封装其对应的包查询,并将
安全的 ListingUnavailable 响应映射为 { status: "unavailable" }。成功结果为
{ status: "passed", value }。在需要区分已索引但不可用的列表与缺失包、且不渲染
发布者控制错误内容的用户界面中使用这些方法。
在显示或选择发布前使用撤回辅助函数:
import {
DiscoveryClient,
type ValidatedReleaseView,
} from "@emdash-cms/registry-client/discovery";
import { evaluateRegistryReleaseWithdrawal } from "@emdash-cms/registry-client/withdrawal";
const discovery = new DiscoveryClient({
aggregatorUrl: "https://registry.emdashcms.com",
});
export function canShowRelease(release: ValidatedReleaseView) {
const result = evaluateRegistryReleaseWithdrawal(release, discovery.labelerPolicy);
return release.release !== null && !result.withdrawn;
}
当适用标签将发布从使用中移除时,withdrawn 为 true。无效的标签数据
会 fail closed:withdrawn 与 malformed 均为 true。
处理不可信记录
聚合器是不可信的索引,中继其未撰写的记录,因此客户端在边界验证每一条。由此得出两条规则:
profile与release字段可为null。 当中继记录验证失败时,客户端将其表面为null而不是使整个调用失败,因此一条格式错误的记录不会清空搜索页。读取pkg.profile?.name或latest.release?.artifacts.package前务必检查 null。- 渲染前自行验证 URL 方案。 验证检查结构,而非 URL 安全性——
uri字段可携带javascript:方案。在将任何注册表提供的 URL 放入href或src之前,应用你自己的http/https允许列表。
非 2xx 响应会抛出 ClientResponseError(从包中重新导出),带有 .error、.description、.status 与 .headers。参考聚合器仅返回经每个所需正标签源批准的精确 CID 修订。atproto-accept-labelers 标头为请求与缓存身份声明已配置的裸 DID。聚合器验证该声明,但其配置的策略仍具权威性。
按主机兼容性筛选
发布可在其 requires 块中声明环境要求(EmDash 或 Astro 版本范围)。@emdash-cms/registry-client/env 子路径会评估它们,因此目录可标记在给定主机上无法运行的发布:
import { checkEnvCompatibility, hostEnvFromVersions } from "@emdash-cms/registry-client/env";
import type { ValidatedReleaseView } from "@emdash-cms/registry-client/discovery";
const host = hostEnvFromVersions("0.37.0", "7.0.0");
// Pass a getLatestRelease() result. The returned array is empty when the
// release runs on this host.
export function envMismatches(latest: ValidatedReleaseView) {
return checkEnvCompatibility(latest.release?.requires, host);
}