查询注册表

本页内容

这是一个高级主题,介绍如何针对插件注册表构建你自己的软件。如果你只是想在 EmDash 站点上安装插件,则不需要这些内容——在配置中启用注册表,并使用管理后台即可。

注册表的发现部分是一个公开的只读 API。@emdash-cms/registry-client 包对其进行了封装,因此你可以在 EmDash 之外构建插件目录、搜索页或发布动态。该客户端可以在任何支持 fetch 的地方运行——Node、Workers、浏览器或 Astro 站点。Astro 站点可以使用 @emdash-cms/registry-loader,通过 live 内容集合来公开同样的数据。

安装

discovery 子路径不携带任何身份验证或 OAuth 依赖。安装该客户端并将其固定到确切的版本:

npm install @emdash-cms/[email protected]

列出与解析包

以下 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 时将其重定向到当前的 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 }) 获取单个包。当列表需要图标等发布数据时,请在集合过滤器中添加 includeLatestRelease: true;每个已发布版本的包都会多产生一次注册表请求,而无法在 3 秒内加载其发布版本的条目会在没有 latestRelease 的情况下返回。当站点有 Astro 缓存提供方时,请将返回的缓存提示传给 Astro.cache.set()。Astro 的 live 集合不返回分页元数据,因此当界面需要下一个游标时,请直接使用 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? }) — 按语义版本降序排列的发布版本,包括已撤回(yanked)的版本。
  • 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 之前,请务必进行空值检查。
  • 渲染之前请自行验证 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);
}

接下来读什么