fix: resolve latest official plugin releases
This commit is contained in:
1 parent
5070578363
commit
0ec00c8887
7 files changed
+180
-16
No files matched your search
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"registryVersion": "1",
|
||||
"updatedAt": "2026-09-08T16:21:56.000Z",
|
||||
"updatedAt": "2026-09-08T16:40:19.000Z",
|
||||
"entries": [
|
||||
{
|
||||
"id": "org.edgeever.plugins.ai-rss",
|
||||
@@ -15,10 +15,10 @@
|
||||
"repositoryUrl": "https://github.com/tianma-if/edgeever-ai-rss"
|
||||
},
|
||||
"verification": {
|
||||
"version": "0.5.2",
|
||||
"version": "0.5.3",
|
||||
"checksums": {
|
||||
"manifestJson": "b8063f246730da33f9cee342670ca776c9319eb5a07585cb44f8641d49caa989",
|
||||
"mainJs": "46bc00002a647f1f087ac2444e02192ec5ae2d1f04719e027de9386e8bf04934",
|
||||
"manifestJson": "b164bf6ab199f0ef4282319dd87294a876ef3e7701962bae35a7b65c4e3c1156",
|
||||
"mainJs": "7828d256ad758f85bd736742a72cac62ba46db6a2108c3a974bf9f429fcc3be4",
|
||||
"stylesCss": "05cd135a1fe70c3f38d34850a2e9abf6b0c0530b09477960036f567375b2082e"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ import { useWorkspacePreferences } from "@/hooks/useWorkspacePreferences";
|
||||
import { useWorkspaceSelection } from "@/hooks/useWorkspaceSelection";
|
||||
import { useWorkspaceQueuedSync } from "@/hooks/useWorkspaceQueuedSync";
|
||||
import { EdgeEverPluginHost, type RegisteredPluginPanel } from "@/lib/plugins/plugin-host";
|
||||
import { loadPluginMarketplace } from "@/lib/plugins/plugin-marketplace";
|
||||
import { loadResolvedPluginMarketplace } from "@/lib/plugins/plugin-marketplace";
|
||||
import { updateOfficialMarketplacePlugins } from "@/lib/plugins/plugin-updates";
|
||||
import { createPublicNetworkAdapter } from "@/lib/plugins/public-network-adapter";
|
||||
import { clearRendererRecoveryRequired, isRendererRecoveryRequired } from "@/lib/renderer-recovery";
|
||||
@@ -803,8 +803,12 @@ export const WorkspaceApp = ({
|
||||
if (!active || running) return;
|
||||
running = true;
|
||||
try {
|
||||
const marketplace = await loadPluginMarketplace();
|
||||
const marketplace = await loadResolvedPluginMarketplace();
|
||||
const result = await updateOfficialMarketplacePlugins(pluginHost, marketplace.entries);
|
||||
const firstResolutionError = Object.entries(marketplace.resolutionErrors)[0];
|
||||
if (firstResolutionError) {
|
||||
console.error(`Official plugin ${firstResolutionError[0]} update resolution failed.`, firstResolutionError[1]);
|
||||
}
|
||||
if (active && result.updated.length > 0) {
|
||||
setAppNoticeDialog({
|
||||
title: t("plugins.noticeTitle"),
|
||||
|
||||
@@ -16,4 +16,9 @@ describe("plugin manager card layout", () => {
|
||||
expect(source).toContain("href={entry.repositoryUrl}");
|
||||
expect(source).toContain("href={extension.source.repositoryUrl}");
|
||||
});
|
||||
|
||||
test("shows update checks when marketplace plugins exist without installed extensions", () => {
|
||||
expect(source).toContain("snapshot.extensions.length > 0 || (marketplaceQuery.data?.entries.length ?? 0) > 0");
|
||||
expect(source).not.toContain("{snapshot.extensions.length > 0 ? (\n <Button");
|
||||
});
|
||||
});
|
||||
@@ -9,7 +9,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import type { EdgeEverPluginHost, InstalledExtension, RegisteredPluginCommand, RegisteredPluginPanel } from "@/lib/plugins/plugin-host";
|
||||
import { PluginPanelDialog } from "@/components/plugins/PluginPanelDialog";
|
||||
import { loadPluginMarketplace } from "@/lib/plugins/plugin-marketplace";
|
||||
import { loadResolvedPluginMarketplace } from "@/lib/plugins/plugin-marketplace";
|
||||
import { GitHubMark, GitHubRepositoryLink } from "@/components/GitHubRepositoryLink";
|
||||
import { applyPluginUpdate, checkPluginUpdates, type PluginUpdateInfo } from "@/lib/plugins/plugin-updates";
|
||||
import { PluginUpdateDialog } from "@/components/plugins/PluginUpdateDialog";
|
||||
@@ -310,7 +310,7 @@ export const PluginManagerCard = ({
|
||||
const [activePanel, setActivePanel] = useState<RegisteredPluginPanel | null>(null);
|
||||
const [pendingUpdate, setPendingUpdate] = useState<PluginUpdateInfo | null>(null);
|
||||
const [pendingTrustPluginId, setPendingTrustPluginId] = useState<string | null>(null);
|
||||
const marketplaceQuery = useQuery({ queryKey: ["plugin-marketplace", "v1"], queryFn: () => loadPluginMarketplace(), staleTime: 5 * 60_000 });
|
||||
const marketplaceQuery = useQuery({ queryKey: ["plugin-marketplace", "v1"], queryFn: () => loadResolvedPluginMarketplace(), staleTime: 5 * 60_000 });
|
||||
const extensionVersionKey = snapshot.extensions
|
||||
.map((extension) => `${extension.manifest.id}:${extension.manifest.version}:${extension.source.kind}`)
|
||||
.join("|");
|
||||
@@ -374,8 +374,11 @@ export const PluginManagerCard = ({
|
||||
["plugin-updates", extensionVersionKey, refreshedMarketplace.data?.updatedAt ?? "unavailable"],
|
||||
result,
|
||||
);
|
||||
setLastManualCheckCount(result.updates.length);
|
||||
const firstCheckError = Object.values(result.errors)[0];
|
||||
const firstCheckError = Object.values({
|
||||
...(refreshedMarketplace.data?.resolutionErrors ?? {}),
|
||||
...result.errors,
|
||||
})[0];
|
||||
setLastManualCheckCount(firstCheckError ? null : result.updates.length);
|
||||
if (firstCheckError) setError(t("plugins.updates.checkFailed", { message: firstCheckError }));
|
||||
} catch (checkError) {
|
||||
setError(checkError instanceof Error ? checkError.message : String(checkError));
|
||||
@@ -423,7 +426,7 @@ export const PluginManagerCard = ({
|
||||
</span>
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-1">
|
||||
{snapshot.extensions.length > 0 ? (
|
||||
{snapshot.extensions.length > 0 || (marketplaceQuery.data?.entries.length ?? 0) > 0 ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
||||
@@ -90,7 +90,7 @@ const requireAsset = (release: GithubReleaseResponse, name: string) => {
|
||||
return asset;
|
||||
};
|
||||
|
||||
type GithubAssetDownloader = (
|
||||
export type GithubAssetDownloader = (
|
||||
coordinates: GithubRepositoryCoordinates,
|
||||
releaseTag: string,
|
||||
asset: GithubReleaseAsset,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { parseMarketplaceRegistry } from "@edgeever/plugin-api";
|
||||
import { sha256Hex } from "./github-plugin-distribution.ts";
|
||||
import { resolveOfficialPluginMarketplace } from "./plugin-marketplace.ts";
|
||||
|
||||
describe("bundled plugin marketplace", () => {
|
||||
test("keeps verified checksums aligned with bundled extension files", async () => {
|
||||
@@ -29,13 +30,100 @@ describe("bundled plugin marketplace", () => {
|
||||
repositoryUrl: "https://github.com/tianma-if/edgeever-ai-rss",
|
||||
distribution: { type: "github", repositoryUrl: "https://github.com/tianma-if/edgeever-ai-rss" },
|
||||
verification: {
|
||||
version: "0.5.2",
|
||||
version: "0.5.3",
|
||||
checksums: {
|
||||
manifestJson: "b8063f246730da33f9cee342670ca776c9319eb5a07585cb44f8641d49caa989",
|
||||
mainJs: "46bc00002a647f1f087ac2444e02192ec5ae2d1f04719e027de9386e8bf04934",
|
||||
manifestJson: "b164bf6ab199f0ef4282319dd87294a876ef3e7701962bae35a7b65c4e3c1156",
|
||||
mainJs: "7828d256ad758f85bd736742a72cac62ba46db6a2108c3a974bf9f429fcc3be4",
|
||||
stylesCss: "05cd135a1fe70c3f38d34850a2e9abf6b0c0530b09477960036f567375b2082e",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("resolves a newer official GitHub release with verified asset checksums", async () => {
|
||||
const registry = parseMarketplaceRegistry({
|
||||
registryVersion: "1",
|
||||
updatedAt: "2026-09-08T00:00:00.000Z",
|
||||
entries: [{
|
||||
id: "org.edgeever.plugins.ai-rss",
|
||||
name: "EdgeEver AI RSS",
|
||||
description: "RSS",
|
||||
author: "EdgeEver",
|
||||
publisher: "edgeever",
|
||||
category: "News & AI",
|
||||
repositoryUrl: "https://github.com/tianma-if/edgeever-ai-rss",
|
||||
distribution: { type: "github", repositoryUrl: "https://github.com/tianma-if/edgeever-ai-rss" },
|
||||
verification: { version: "0.5.2", checksums: { manifestJson: "a".repeat(64), mainJs: "b".repeat(64) } },
|
||||
}],
|
||||
});
|
||||
const manifest = {
|
||||
type: "plugin",
|
||||
id: "org.edgeever.plugins.ai-rss",
|
||||
name: "EdgeEver AI RSS",
|
||||
version: "0.5.3",
|
||||
apiVersion: "2",
|
||||
settingsUi: "host",
|
||||
entry: "./main.js",
|
||||
permissions: ["ui:notices"],
|
||||
};
|
||||
const manifestText = JSON.stringify(manifest);
|
||||
const assets = {
|
||||
"manifest.json": new TextEncoder().encode(manifestText).buffer,
|
||||
"main.js": new TextEncoder().encode("export default {};").buffer,
|
||||
};
|
||||
const request = async (input) => {
|
||||
const url = String(input);
|
||||
if (url.includes("/contents/manifest.json")) return new Response(manifestText);
|
||||
if (url.includes("/releases/tags/")) return Response.json({
|
||||
tag_name: "v0.5.3",
|
||||
draft: false,
|
||||
assets: Object.entries(assets).map(([name, buffer], index) => ({
|
||||
id: index + 1,
|
||||
name,
|
||||
size: buffer.byteLength,
|
||||
url: `https://api.github.test/assets/${index + 1}`,
|
||||
browser_download_url: `https://github.test/v0.5.3/${name}`,
|
||||
})),
|
||||
});
|
||||
throw new Error(`Unexpected request: ${url}`);
|
||||
};
|
||||
const download = async (_coordinates, _releaseTag, asset) => assets[asset.name];
|
||||
|
||||
const resolved = await resolveOfficialPluginMarketplace(registry, request, download);
|
||||
|
||||
expect(resolved.resolutionErrors).toEqual({});
|
||||
expect(resolved.entries[0].verification).toEqual({
|
||||
version: "0.5.3",
|
||||
checksums: {
|
||||
manifestJson: await sha256Hex(assets["manifest.json"]),
|
||||
mainJs: await sha256Hex(assets["main.js"]),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("keeps the pinned release and reports an error when live resolution fails", async () => {
|
||||
const registry = parseMarketplaceRegistry({
|
||||
registryVersion: "1",
|
||||
updatedAt: "2026-09-08T00:00:00.000Z",
|
||||
entries: [{
|
||||
id: "org.edgeever.plugins.ai-rss",
|
||||
name: "EdgeEver AI RSS",
|
||||
description: "RSS",
|
||||
author: "EdgeEver",
|
||||
publisher: "edgeever",
|
||||
category: "News & AI",
|
||||
repositoryUrl: "https://github.com/tianma-if/edgeever-ai-rss",
|
||||
distribution: { type: "github", repositoryUrl: "https://github.com/tianma-if/edgeever-ai-rss" },
|
||||
verification: { version: "0.5.3", checksums: { manifestJson: "a".repeat(64), mainJs: "b".repeat(64) } },
|
||||
}],
|
||||
});
|
||||
|
||||
const resolved = await resolveOfficialPluginMarketplace(
|
||||
registry,
|
||||
async () => new Response(null, { status: 503 }),
|
||||
);
|
||||
|
||||
expect(resolved.entries[0]).toEqual(registry.entries[0]);
|
||||
expect(resolved.resolutionErrors[registry.entries[0].id]).toContain("503");
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,9 @@
|
||||
import { parseMarketplaceRegistry, type MarketplaceRegistry } from "@edgeever/plugin-api";
|
||||
import { parseMarketplaceRegistry, type MarketplaceEntry, type MarketplaceRegistry } from "@edgeever/plugin-api";
|
||||
import {
|
||||
downloadGithubExtension,
|
||||
type GithubAssetDownloader,
|
||||
} from "@/lib/plugins/github-plugin-distribution";
|
||||
import { isVersionOutdated } from "@/lib/version-check";
|
||||
|
||||
export const DEFAULT_PLUGIN_REGISTRY_URL = "/extensions/registry.json";
|
||||
|
||||
@@ -11,3 +16,62 @@ export const loadPluginMarketplace = async (
|
||||
if (!response.ok) throw new Error(`Plugin marketplace request failed with HTTP ${response.status}.`);
|
||||
return parseMarketplaceRegistry(await response.json());
|
||||
};
|
||||
|
||||
export interface ResolvedPluginMarketplace extends MarketplaceRegistry {
|
||||
resolutionErrors: Record<string, string>;
|
||||
}
|
||||
|
||||
const resolveOfficialGithubEntry = async (
|
||||
entry: MarketplaceEntry,
|
||||
request: typeof fetch,
|
||||
downloadAssetBytes?: GithubAssetDownloader,
|
||||
): Promise<MarketplaceEntry> => {
|
||||
if (entry.publisher !== "edgeever" || entry.distribution.type !== "github") return entry;
|
||||
const downloaded = await downloadGithubExtension(entry.distribution.repositoryUrl, request, downloadAssetBytes);
|
||||
if (downloaded.manifest.id !== entry.id) {
|
||||
throw new Error("Official repository manifest id does not match the marketplace entry.");
|
||||
}
|
||||
if (isVersionOutdated(downloaded.manifest.version, entry.verification.version)) {
|
||||
throw new Error("Official repository version is older than the marketplace verified version.");
|
||||
}
|
||||
if (!downloaded.checksums.manifestJson) throw new Error("Official extension release is missing a manifest checksum.");
|
||||
if (downloaded.manifest.type === "plugin" && !downloaded.checksums.mainJs) {
|
||||
throw new Error("Official plugin release is missing a main.js checksum.");
|
||||
}
|
||||
return {
|
||||
...entry,
|
||||
verification: {
|
||||
version: downloaded.manifest.version,
|
||||
checksums: downloaded.checksums,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const resolveOfficialPluginMarketplace = async (
|
||||
registry: MarketplaceRegistry,
|
||||
request: typeof fetch = window.fetch.bind(window),
|
||||
downloadAssetBytes?: GithubAssetDownloader,
|
||||
): Promise<ResolvedPluginMarketplace> => {
|
||||
const settled = await Promise.allSettled(
|
||||
registry.entries.map((entry) => resolveOfficialGithubEntry(entry, request, downloadAssetBytes)),
|
||||
);
|
||||
const resolutionErrors: Record<string, string> = {};
|
||||
const entries = settled.map((result, index) => {
|
||||
if (result.status === "fulfilled") return result.value;
|
||||
const entry = registry.entries[index];
|
||||
if (!entry) throw result.reason;
|
||||
resolutionErrors[entry.id] = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
||||
return entry;
|
||||
});
|
||||
return { ...registry, entries, resolutionErrors };
|
||||
};
|
||||
|
||||
export const loadResolvedPluginMarketplace = async (
|
||||
registryUrl = DEFAULT_PLUGIN_REGISTRY_URL,
|
||||
request: typeof fetch = window.fetch.bind(window),
|
||||
downloadAssetBytes?: GithubAssetDownloader,
|
||||
) => resolveOfficialPluginMarketplace(
|
||||
await loadPluginMarketplace(registryUrl, request),
|
||||
request,
|
||||
downloadAssetBytes,
|
||||
);
|
||||
Reference in new issue
Block a user