feat(admin): show canonical registry plugin names - #3080
Conversation
🦋 Changeset detectedLatest commit: 106e516 The changes in this PR will be included in the next version bump. This PR includes changesets to release 17 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
🚀 Deploying Preview to Cloudflare 🚀Preview URL: https://codex-plugin-public-names.try.emdashcms.com, https://codex-plugin-public-names-emdash-playground.emdash-cms.workers.dev (commit 106e516)This URL reflects your latest Preview deploymentPreview Deployments by commit
|
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
docs | e850c3b | Sep 12 2026, 08:47 AM |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/registry-moderation
@emdash-cms/registry-verification
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
There was a problem hiding this comment.
The approach is sound: this PR adds canonical, mutable-handle-based public names for registry plugins, dedicated deep links, exact public-name search, and a clear invalid-handle state, all while keeping install/update trust on the immutable DID. The implementation follows EmDash conventions well — all new UI strings are wrapped for Lingui, Tailwind classes are RTL-safe (ms/me, dir="auto"), a changeset is included, and there are focused component + unit tests.
I read the full diff, the changed admin components/lib, related registry API code, and the updated docs. No core/runtime code is touched, so SQL safety, authorization, locale-filtering, and logged-out query counts are not in scope here.
Headline issues:
-
Handle case is not normalized before it becomes part of the identity/URL.
formatRegistryPublicNamelowercases the handle for display, butregistryIdentitystores the resolver’s raw handle andregistryIdentityPublisherParamuses it verbatim for deep links. AT Protocol handles are case-insensitive, so a resolver may return mixed case; that would yield URLs like@Example.com/...which diverge from the canonical lowercase public name and may break case-sensitive routing or bookmarks. Normalize the handle when buildingRegistryIdentity. -
Two stale docstrings are now misleading because of this feature.
RegistryBrowse.tsxstill says cards link to/plugins/marketplace/$pluginId, andPublisherHandle.tsxclaims browse/detail surfaces only approved author names/shortened DIDs and that routing uses the full DID. Both are contradicted by the new public-name links/handle-based routing and should be updated. -
Minor CSS-override fragility in
PluginManager. The invalid-identity card addsborder-kumo-dangerto an element whose only border class is genericborder. The red override relies on generated CSS order; use an explicitborder-kumo-borderdefault likeRegistryPackageCarddoes.
Tests, localization, and the changeset itself look good: the changeset names the user-facing surface, the docs explain behavior accurately, and the new tests verify observable behavior rather than asserting implementation details back at themselves.
Findings
-
[needs fixing]
packages/admin/src/lib/registry-identity.ts:46registryIdentitystores the resolved handle verbatim andregistryIdentityPublisherParamuses it for deep-link URLs, butformatRegistryPublicNamelowercases the handle for display. AT Protocol handles are case-insensitive, so a resolver may return a mixed-case handle, producing URLs like/plugins/registry/@Example.com/slugthat diverge from the canonical lowercase public name and may break case-sensitive routing or bookmarks.Normalize the handle when constructing the identity so the stored value and the URL are always lowercase:
if (resolution.status === "ok") { return { status: "ok", did, slug, handle: resolution.handle.toLowerCase(), publicName: formatRegistryPublicName(resolution.handle, slug), }; }(Alternatively, lower-case inside
formatRegistryPublicNamecallers and/orregistryIdentityPublisherParamas well, but fixing it at the identity source keeps a single canonical value.) -
[suggestion]
packages/admin/src/components/PluginManager.tsx:378The invalid-identity branch adds
border-kumo-dangerto an element whose only border class is the genericborderutility. The red override is therefore dependent on generated CSS order; ifborderends up later in the stylesheet, the danger border is lost. The siblingRegistryPackageCardavoids this by explicitly usingborder-kumo-borderas the default andborder-kumo-dangeronly for the invalid case.Use an explicit default border color:
className={cn( "rounded-lg border border-kumo-border bg-kumo-base transition-colors", !plugin.enabled && "opacity-75", registryIdentity?.status === "invalid" && "border-kumo-danger", )} -
[suggestion]
packages/admin/src/components/RegistryBrowse.tsx:8-12This docstring is stale after the public-name changes. Cards now navigate to the dedicated
/plugins/registry/$publisher/$slugroute, and an exact public-name search is resolved directly viaresolveRegistryPackageStatusrather than being sent to the free-textsearchPackagesendpoint.Update the comment to describe the current routing and search behavior, e.g.:
* Cards navigate to `/plugins/registry/$publisher/$slug`. A search query * that matches the canonical public-name form (`@handle/slug`) is resolved * directly instead of being sent to the free-text search endpoint. -
[suggestion]
packages/admin/src/components/PublisherHandle.tsx:4-6This docstring is now misleading because of the public-name feature added in this PR. Registry browse cards render verified
@handle/slugpublic names (not just approved author names/shortened DIDs), and routing uses handle-based URLs with a DID fallback. The claim that browse/detail surfaces only approved names and that routing always uses the full DID is no longer accurate.Update the comment to reflect that
PublisherIdentityis now a fallback detail-line component, while public names and handle-based links live inRegistryPluginIdentityandregistry-identity.ts.
|
@emdashbot review The mixed-case handle regression, explicit border color, and stale comments from the previous review are fixed at ~ 🤖 Codex |
There was a problem hiding this comment.
The approach remains sound: canonical, mutable-handle-based public names for registry plugins, dedicated deep links, exact public-name search, and a clear invalid-handle state, all keeping install/update trust on the immutable DID. This re-read confirms all three of my prior findings are fixed.
What’s fixed:
- Handle case normalization —
registryIdentitynow lowercasesresolution.handleat the source and uses that normalized value forpublicNameandregistryIdentityPublisherParamURLs. Tests cover the case-insensitive resolver output path. - CSS override fragility —
PluginManagernow uses an explicitborder-kumo-borderdefault andborder-kumo-dangeronly for the invalid branch, matchingRegistryPackageCard. - Stale docstrings —
PublisherHandle.tsxandRegistryBrowse.tsxdocstrings now accurately describe public-name rendering, handle-based links, and direct public-name resolution.
What’s new:
- I read the updated
RegistryPluginDetail.tsx,RegistryPluginIdentity.tsx,registry-identity.ts, the new tests, the docs change, the changeset, and the router additions. Everything is localized, RTL-safe, and the tests verify observable behavior. - One remaining suggestion: the top docstring in
RegistryPluginDetail.tsxwas not updated and is now misleading about the route shape. It still claims the component is identified by${handle}/${slug}and wrapped on the same route as marketplace detail, ignoring the new dedicated/plugins/registry/$publisher/$slugroute and DID-based fallback URLs. I’ve left a line-anchored suggestion.
Overall this is close to clean; only that docstring needs a refresh.
Findings
-
[suggestion]
packages/admin/src/components/RegistryPluginDetail.tsx:10-13The top docstring is now stale. The component no longer only handles
${handle}/${slug}marketplace-route plugin IDs: the new dedicated route uses/plugins/registry/$publisher/$slug(where$publishermay be@handleor a DID), and old marketplace deep links can also pass DID-based IDs. Keeping the description accurate avoids confusing future readers about where this component is mounted and whatpluginIdcan contain.* Identified by a `pluginId` of `${publisher}/${slug}`, where `publisher` * is either a verified handle (with or without the leading `@`) or a * stable DID. The dedicated registry route renders this component at * `/plugins/registry/$publisher/$slug`; the marketplace detail route also * forwards matching legacy `${did}/{slug}` deep links here for backward * compatibility.
There was a problem hiding this comment.
This is a re-review of PR #3080. The approach remains sound: canonical, mutable-handle-based public names for registry plugins, a dedicated registry detail route, exact public-name search, and a clear invalid-handle state, all while keeping install/update trust on the immutable DID. I re-read the full diff and the relevant source files.
The one remaining suggestion from my previous review has been addressed: packages/admin/src/components/RegistryPluginDetail.tsx now has an accurate top docstring describing the ${publisher}/${slug} pluginId shape, the dedicated /plugins/registry/$publisher/$slug route, and the legacy marketplace-route fallback.
I checked the updated code for new issues:
registry-identity.tsnormalizes handles at the source, parses the@handle/slugform, and cleanly distinguishesok/invalid/missing/pendingstates.RegistryPluginIdentityanduseRegistryPluginIdentityare localized, RTL-safe, and avoid nested anchor elements when used inside card links.RegistryBrowseroutes exact public-name searches throughresolveRegistryPackageStatusand otherwise uses the existing free-text search; public-name card links now go to the new registry route.RegistryPluginDetailstrips the optional leading@, resolves both DID-based and handle-based IDs, renders the canonical public-name byline, and blocks install when the handle is conclusively invalid.PluginManagerdisplays canonical public names for installed registry plugins and highlights invalid handles without deactivating existing installations.- The new
/plugins/registry/$publisher/$slugroute is wired correctly, and the legacy marketplace detail route still forwards DID-shaped IDs for backward compatibility. - Tests cover case normalization, invalid/missing handle rendering, exact-name search, and public-name links.
- The changeset is user-facing and proportional; the docs prose matches the implemented behavior, including the distinction between conclusive invalid-handle failure and temporary lookup failure.
No remaining needs_fixing items and no new blockers. LGTM.
What does this PR do?
Adds canonical public names for registry plugins in the
@publisher.example/plugin-slugformat./plugins/registry/@publisher.example/plugin-slugdeep links.Related Discussion: #296
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runmessages.pochanges except in translation PRs — a workflow extracts catalogs on merge tomain.AI-generated code disclosure
Screenshots / test output
Registry listing — English
Registry listing — Arabic RTL
Validated on current
origin/main:pnpm buildpnpm typecheckpnpm lintpnpm lint:json | jq '.diagnostics | length'→0pnpm --filter @emdash-cms/admin test --run→ 169 files, 2,317 tests passedpnpm --dir docs build