Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/tame-registry-config-errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"emdash": patch
"@emdash-cms/admin": patch
---

Fixes invalid plugin registry settings causing the admin manifest to fail with a generic server error. EmDash reports malformed `experimental.registry` fields while Astro loads the site configuration. If invalid registry settings reach the runtime, the admin remains available and shows which field to correct in `astro.config.mjs`.
35 changes: 35 additions & 0 deletions packages/admin/src/components/RegistryConfigurationBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Banner } from "@cloudflare/kumo";
import { useLingui } from "@lingui/react/macro";

import type { AdminManifest } from "../lib/api/client.js";

type RegistryConfigurationError = NonNullable<AdminManifest["registryConfigurationError"]>;

export function RegistryConfigurationBanner({ error }: { error: RegistryConfigurationError }) {
const { t } = useLingui();
let description: string;

switch (error.field) {
case "experimental.registry.aggregatorUrl":
description = t`Check experimental.registry.aggregatorUrl in astro.config.mjs, then restart EmDash.`;
break;
case "experimental.registry.policy.minimumReleaseAge":
description = t`Check experimental.registry.policy.minimumReleaseAge in astro.config.mjs, then restart EmDash.`;
break;
case "experimental.registry.policy.minimumReleaseAgeExclude":
description = t`Check experimental.registry.policy.minimumReleaseAgeExclude in astro.config.mjs, then restart EmDash.`;
break;
default:
description = t`Check experimental.registry in astro.config.mjs, then restart EmDash.`;
}

return (
<Banner
variant="error"
role="alert"
aria-label={t`Plugin registry configuration error`}
title={t`Plugin registry configuration error`}
description={description}
/>
);
}
8 changes: 8 additions & 0 deletions packages/admin/src/components/Shell.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { useMatches } from "@tanstack/react-router";
import * as React from "react";

import type { AdminManifest } from "../lib/api/client.js";
import { useCurrentUser } from "../lib/api/current-user";
import { getLocaleDir } from "../locales/config.js";
import { useLocale } from "../locales/useLocale.js";
import { AdminCommandPalette } from "./AdminCommandPalette";
import { Header } from "./Header";
import { RegistryConfigurationBanner } from "./RegistryConfigurationBanner.js";
import { Sidebar, SidebarNav } from "./Sidebar";
import { WelcomeModal } from "./WelcomeModal";

Expand Down Expand Up @@ -39,6 +41,7 @@ export interface ShellProps {
}>;
i18n?: { defaultLocale: string; locales: string[] };
version?: string;
registryConfigurationError?: AdminManifest["registryConfigurationError"];
};
}

Expand Down Expand Up @@ -104,6 +107,11 @@ export function Shell({ children, manifest }: ShellProps) {
{/* Main content area — scrolls independently so sidebar stays full height */}
<div className="flex flex-1 flex-col overflow-hidden">
<Header />
{manifest.registryConfigurationError && (
<div className="px-6 pt-6">
<RegistryConfigurationBanner error={manifest.registryConfigurationError} />
</div>
)}
<main
className={
fullBleed
Expand Down
13 changes: 13 additions & 0 deletions packages/admin/src/lib/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,19 @@ export interface AdminManifest {
minimumReleaseAgeExclude?: string[];
};
};
/** Field-level diagnostic returned when registry configuration is invalid. */
registryConfigurationError?: {
code:
| "REGISTRY_AGGREGATOR_URL_REQUIRED"
| "REGISTRY_AGGREGATOR_URL_INVALID"
| "REGISTRY_AGGREGATOR_URL_FORBIDDEN"
| "REGISTRY_MINIMUM_RELEASE_AGE_INVALID"
| "REGISTRY_MINIMUM_RELEASE_AGE_EXCLUDE_INVALID";
field:
| "experimental.registry.aggregatorUrl"
| "experimental.registry.policy.minimumReleaseAge"
| "experimental.registry.policy.minimumReleaseAgeExclude";
};
/**
* Admin branding overrides for white-labeling.
* Set via the `admin` config in `astro.config.mjs`.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, expect, it } from "vitest";

import { RegistryConfigurationBanner } from "../../src/components/RegistryConfigurationBanner";
import { render } from "../utils/render.tsx";

describe("RegistryConfigurationBanner", () => {
it("directs administrators to the invalid aggregator URL setting", async () => {
const screen = await render(
<RegistryConfigurationBanner
error={{
code: "REGISTRY_AGGREGATOR_URL_INVALID",
field: "experimental.registry.aggregatorUrl",
}}
/>,
);

await expect
.element(screen.getByRole("alert", { name: "Plugin registry configuration error" }))
.toBeInTheDocument();
await expect
.element(
screen.getByText(
"Check experimental.registry.aggregatorUrl in astro.config.mjs, then restart EmDash.",
),
)
.toBeInTheDocument();
});

it("directs administrators to the invalid release policy setting", async () => {
const screen = await render(
<RegistryConfigurationBanner
error={{
code: "REGISTRY_MINIMUM_RELEASE_AGE_INVALID",
field: "experimental.registry.policy.minimumReleaseAge",
}}
/>,
);

await expect
.element(
screen.getByText(
"Check experimental.registry.policy.minimumReleaseAge in astro.config.mjs, then restart EmDash.",
),
)
.toBeInTheDocument();
});

it("does not name the wrong setting for a diagnostic from a newer server", async () => {
const screen = await render(
<RegistryConfigurationBanner
error={
{
code: "REGISTRY_FUTURE_SETTING_INVALID",
field: "experimental.registry.futureSetting",
} as never
}
/>,
);

await expect
.element(
screen.getByText("Check experimental.registry in astro.config.mjs, then restart EmDash."),
)
.toBeInTheDocument();
expect(screen.container.textContent).not.toContain("aggregatorUrl");
});
});
9 changes: 9 additions & 0 deletions packages/core/src/astro/integration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
import { buildMigrationManifest } from "../../migrations/manifest-builder.js";
import { writeMigrationManifest } from "../../migrations/manifest-writer.js";
import type { ResolvedPlugin } from "../../plugins/types.js";
import { normalizeRegistryConfig } from "../../registry/config.js";
import { VERSION } from "../../version.js";
import { setDevTypegenRefresh } from "../dev-typegen.js";
import { local } from "../storage/adapters.js";
Expand Down Expand Up @@ -328,6 +329,11 @@ export function emdash(config: EmDashConfig = {}): AstroIntegration {
migrations: normalizeMigrationConfig(config.migrations),
};

// Validate environment-independent registry settings while Astro is still
// evaluating its config. The command-aware check in astro:config:setup
// applies the stricter production localhost policy.
normalizeRegistryConfig(resolvedConfig.experimental?.registry, { allowLocalhost: true });

// Validate marketplace URL
if (resolvedConfig.marketplace) {
const url = resolvedConfig.marketplace;
Expand Down Expand Up @@ -463,6 +469,9 @@ export function emdash(config: EmDashConfig = {}): AstroIntegration {
command,
}) => {
astroCommand = command;
normalizeRegistryConfig(resolvedConfig.experimental?.registry, {
allowLocalhost: command === "dev" || command === "sync",
});
printBanner(logger);
// Capture the host's Astro version so the runtime can expose it
// to the admin and the registry install gate for `env:astro`
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/astro/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { Kysely } from "kysely";

import type { ContentFieldFilters } from "../content-list-query.js";
import type { RouteCallerInput, RouteMeta } from "../plugins/routes.js";
import type { ManifestRegistryConfigurationError } from "../registry/config.js";

// Re-export core types
export type {
Expand Down Expand Up @@ -214,6 +215,8 @@ export interface EmDashManifest {
minimumReleaseAgeExclude?: string[];
};
};
/** Safe field-level diagnostic when the registry configuration cannot be normalized. */
registryConfigurationError?: ManifestRegistryConfigurationError;
/**
* Admin branding overrides for white-labeling.
* Set via the `admin` config in `astro.config.mjs`.
Expand Down
16 changes: 10 additions & 6 deletions packages/core/src/emdash-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ import { isContentSaveRejection } from "./plugins/save-rejection.js";
import type { CronScheduler } from "./plugins/scheduler/types.js";
import { PluginStateRepository } from "./plugins/state.js";
import { syncDeclaredStorageIndexes } from "./plugins/storage-indexes.js";
import { normalizeRegistryConfig } from "./registry/config.js";
import { resolveManifestRegistryConfig } from "./registry/config.js";
import { requestCached } from "./request-cache.js";
import { getRequestContext } from "./request-context.js";
import { publishDueContent, type PublishedRef } from "./scheduled-publish.js";
Expand Down Expand Up @@ -2606,11 +2606,14 @@ export class EmDashRuntime {
}
: undefined;

// Normalize the experimental registry config for browser consumption.
// Validation errors here surface as 500s from the manifest endpoint
// rather than being silently dropped -- a misconfigured registry
// should be loud, not invisible.
const registry = normalizeRegistryConfig(this.config.experimental?.registry) ?? undefined;
const { registry, error: registryConfigurationError } = resolveManifestRegistryConfig(
this.config.experimental?.registry,
);
if (registryConfigurationError) {
console.error(
`EmDash registry configuration error in ${registryConfigurationError.field} (${registryConfigurationError.code})`,
);
}

return {
version: VERSION,
Expand All @@ -2628,6 +2631,7 @@ export class EmDashRuntime {
},
marketplace: !!this.config.marketplace,
registry,
registryConfigurationError,
};
}

Expand Down
Loading
Loading