このサイトは非公式の日本語訳です。Cloudflare・EmDashプロジェクトが運営するサイトではありません。

このページで分かること

  • @emdash-cms/registry-client の役割(読み取り専用の検索APIをEmDashの外から使う)と、APIが実験的な段階であること
  • パッケージの一覧の取得と特定、Astroのライブローダー(@emdash-cms/registry-loader)、検索用のメソッド
  • 信頼できないレコードの扱い(null の確認、URLのスキームの確認)と、ホストとの互換性による絞り込み
難易度
上級
読む時間
4分
このページの目次

このページは、プラグインレジストリを利用する独自のソフトウェアを作るための上級者向けのトピックです。EmDashのサイトにプラグインをインストールしたいだけなら、この内容は必要ありません。設定でレジストリを有効にし、管理画面のダッシュボードを使います。

レジストリの検索側(discovery)は、公開された読み取り専用のAPIです。@emdash-cms/registry-client パッケージがこのAPIをラップしているため、EmDashの外で、プラグインの一覧、検索ページ、リリースのフィードを作れます。クライアントは、fetch を使える場所ならどこでも動きます。Node、Workers、ブラウザー、Astroのサイトで使えます。Astroのサイトでは、@emdash-cms/registry-loader を使って、同じデータをライブコンテンツコレクション(Live Content Collections)として公開できます。

インストール

discovery サブパスは、認証やOAuthの依存関係を持ちません。クライアントをインストールし、正確なバージョンに固定します。

npm install @emdash-cms/registry-client@0.6.0

パッケージの一覧の取得と特定

次のAstroのページは、レジストリのすべてのプラグインを一覧にします。

src/pages/plugins/index.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>

現在の検証済みのハンドルは、アグリゲーターが解決済みの場合に含まれます。DIDの形式は代替として残しておき、後のレスポンスでハンドルが得られたら、現在のハンドルのURLにリダイレクトします。公開者がハンドルを変更しても、DIDとパッケージのスラッグは、パッケージを識別する変わらない値として残ります。

q に完全一致の値を指定する場合は、ハンドル、DID、またはそのどちらかのあとに /slug を続けた値を使えます。たとえば、@example.com/my-gallery は1つのパッケージを選び、example.com はその公開者のパッケージを返します。

Astroのライブローダーの利用

@emdash-cms/registry-loader をインストールし、src/live.config.ts でローダーを登録します。

src/live.config.ts
import { registryLoader } from "@emdash-cms/registry-loader";
import { defineLiveCollection } from "astro:content";

export const collections = {
	plugins: defineLiveCollection({ loader: registryLoader() }),
};

件数を限った結果には getLiveCollection("plugins", { q, limit }) を、1つのパッケージには getLiveEntry("plugins", { publisher, slug }) を使います。サイトにAstroのキャッシュプロバイダーがある場合は、返されたキャッシュのヒントを Astro.cache.set() に渡します。Astroのライブコレクションはページネーションのメタデータを返さないため、画面で次のカーソルが必要な場合は DiscoveryClient.searchPackages() を直接使います。

パッケージの詳細ページは、DIDとスラッグでプラグインを取得し、そのあと最新のリリースを取得します。

src/lib/plugin.ts
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 };
}

検索用のメソッド

クライアントは、アグリゲーターのクエリごとに1つのメソッドを提供します。

  • searchPackages({ q, capability?, limit?, cursor? }):フリーテキストで検索します。指定したアクセスのカテゴリーを宣言しているパッケージに絞り込むこともできます。{ packages, cursor? } を返します。
  • resolvePackage({ handle, slug }):ハンドルとスラッグからパッケージを特定します。
  • getPackage({ did, slug }):DIDとスラッグでパッケージを取得します。
  • listReleases({ did, package, limit?, cursor? }):取り下げ(yank)済みのリリースを含め、セマンティックバージョンの降順でリリースを返します。
  • getLatestRelease({ did, package }):アグリゲーターが選んだ、取り下げられていないリリースのうち最も高いバージョンを返します。

getPackage()resolvePackage() は、historicalReleaseCountreleaseHistoryComplete を返す場合があります。これらのフィールドは、アグリゲーターが保持している運用上の履歴を表すもので、公開者が署名したメタデータではありません。件数が1のときに初回のリリースとみなすのは、releaseHistoryCompletetrue の場合だけにします。根拠が欠けている、または不完全であることを理由に、リリースの経過期間に関するポリシーを迂回してはいけません。

getPackageStatus()resolvePackageStatus() は、それぞれ対応するパッケージのクエリをラップし、安全な ListingUnavailable のレスポンスを { status: "unavailable" } に対応付けます。成功した結果は { status: "passed", value } になります。インデックスには登録されているが利用できない掲載と、存在しないパッケージとを、公開者が制御するエラーの内容を描画せずに区別する必要があるユーザーインターフェースでは、これらのメソッドを使います。

リリースを表示したり選択したりする前に、取り下げを判定するヘルパーを使います。

src/lib/plugin.ts
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;
}

該当するラベルによってリリースが利用対象から外される場合、withdrawntrue になります。ラベルのデータが不正な場合は安全側に倒れ、withdrawnmalformed の両方が true になります。

信頼できないレコードの扱い

アグリゲーターは、自分が作成していないレコードを中継する、信頼できないインデックスです。そのため、クライアントは境界で1件ずつレコードを検証します。このことから、次の2つの決まりがあります。

  • profilerelease のフィールドは null になる場合があります。 中継されたレコードが検証に失敗すると、クライアントは呼び出し全体を失敗させずに、そのレコードを null として返します。こうすると、不正なレコードが1件あっても検索ページが空になりません。pkg.profile?.namelatest.release?.artifacts.package を読む前に、必ず null かどうかを確認します。
  • 描画する前に、URLのスキームを自分で検証します。 検証で確かめるのは構造で、URLの安全性ではありません。uri フィールドに javascript: のスキームが入っている場合もあります。レジストリから得たURLを hrefsrc に入れる前に、httphttps だけを許可する独自の許可リストを適用します。

2xx以外のレスポンスでは ClientResponseError(パッケージから再エクスポートされています)が投げられます。このエラーは .error.description.status.headers を持ちます。リファレンス実装のアグリゲーターは、必要なすべての肯定的なラベルの提供元が承認した、CIDが完全一致するリビジョンだけを返します。atproto-accept-labelers ヘッダーは、リクエストとキャッシュを識別するために、設定済みのDIDをそのまま宣言します。アグリゲーターはこの宣言を検証しますが、判断の基準になるのはアグリゲーターに設定されたポリシーです。

ホストとの互換性による絞り込み

リリースは、requires ブロックで環境の要件(EmDashやAstroのバージョンの範囲)を宣言できます。@emdash-cms/registry-client/env サブパスがこの要件を評価するため、プラグインの一覧で、特定のホストでは動かないリリースに印を付けられます。

src/lib/compat.ts
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);
}
本サイトの補足 やさしい解説

WordPressでいえば、公式プラグインディレクトリの情報を外部のサイトから読み出して、独自のプラグイン紹介ページを作るような使い方です。EmDashのサイトでプラグインを使うだけなら、このページの内容は必要ありません。レジストリから届くデータは誰でも公開できるものなので、クライアントは形を検証し、不正なレコードを null にして返します。URLが安全かどうかまでは検証しないため、リンクや画像に使う前に httphttps かどうかを自分で確かめます。