이 문서는 플러그인 레지스트리를 기반으로 직접 소프트웨어를 만들기 위한 고급 주제입니다. EmDash 사이트에 플러그인을 설치하기만 하면 된다면 이 내용은 전혀 필요하지 않습니다. 설정에서 레지스트리를 활성화하고 관리자 대시보드를 사용하세요.
레지스트리의 디스커버리 부분은 공개된 읽기 전용 API입니다. @emdash-cms/registry-client 패키지가 이를 감싸고 있어, EmDash 밖에서 플러그인 디렉터리, 검색 페이지 또는 릴리스 피드를 만들 수 있습니다. 이 클라이언트는 fetch를 사용할 수 있는 곳이라면 어디서든 실행됩니다. Node, Workers, 브라우저, Astro 사이트 모두 가능합니다. Astro 사이트는 @emdash-cms/registry-loader를 사용해 같은 데이터를 라이브 콘텐츠 컬렉션으로 노출할 수 있습니다.
설치
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>
현재 검증된 핸들은 애그리게이터가 해석한 경우에 존재합니다. DID 형식은 폴백으로 유지하고, 이후 응답에서 핸들을 제공하면 현재 핸들 URL로 리디렉션하세요. 퍼블리셔가 핸들을 바꾸더라도 DID와 패키지 슬러그는 패키지의 안정적인 식별자로 유지됩니다.
정확한 q 값은 핸들, DID, 또는 둘 중 하나 뒤에 /slug를 붙인 것일 수 있습니다. 예를 들어 @example.com/my-gallery는 하나의 패키지를 선택하고, example.com은 해당 퍼블리셔의 패키지를 반환합니다.
Astro 라이브 로더 사용
@emdash-cms/registry-loader를 설치한 다음 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 })를, 패키지 하나에는 getLiveEntry("plugins", { publisher, slug })를 사용하세요. 목록에 아이콘 같은 릴리스 데이터가 필요하면 컬렉션 필터에 includeLatestRelease: true를 추가하세요. 게시된 릴리스가 있는 패키지마다 레지스트리 요청이 한 번 더 발생하며, 릴리스를 3초 안에 불러오지 못한 항목은 latestRelease 없이 반환됩니다. 사이트에 Astro 캐시 제공자가 있다면 반환된 캐시 힌트를 Astro.cache.set()에 전달하세요. Astro 라이브 컬렉션은 페이지네이션 메타데이터를 반환하지 않으므로, 인터페이스에 다음 커서가 필요하면 DiscoveryClient.searchPackages()를 직접 사용하세요.
패키지 상세 페이지는 DID와 슬러그로 플러그인을 가져온 다음 최신 릴리스를 가져옵니다.
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 })— 핸들과 슬러그로 패키지를 확인합니다.getPackage({ did, slug })— DID와 슬러그로 패키지를 가져옵니다.listReleases({ did, package, limit?, cursor? })— 시맨틱 버전 내림차순의 릴리스이며, yank(철회)된 릴리스도 포함합니다.getLatestRelease({ did, package })— 애그리게이터가 선택한, yank되지 않은 가장 높은 릴리스입니다.
getPackage()와 resolvePackage()는 historicalReleaseCount와
releaseHistoryComplete를 반환할 수 있습니다. 이 필드는 애그리게이터가 보존한 운영 기록을 나타내며,
퍼블리셔가 서명한 메타데이터가 아닙니다. 개수가 1인 경우를 첫 릴리스로 취급하는 것은
releaseHistoryComplete가 true일 때만 하세요. 증거가 없거나 불완전하다고 해서 릴리스 경과 기간 정책을
우회해서는 안 됩니다.
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);
}
다음에 읽을 내용
- 플러그인 레지스트리 — EmDash 사이트에서 레지스트리를 활성화하고 사용하기
- 번들 및 게시 — 플러그인을 레지스트리에 게시하기