fix: close marketplace client review findings

This commit is contained in:
2026-08-28 21:01:51 +08:00
parent 8dfa542860
commit 1614f7efc1
25 changed files with 1224 additions and 143 deletions

View File

@@ -101,6 +101,7 @@ export interface CodingPluginMarketplaceService {
acquire(pluginId: string): Promise<PublicPluginLibrary>;
remove(pluginId: string): Promise<PublicPluginLibrary>;
install(pluginId: string): Promise<PublicPluginInstallation>;
installBeta(pluginId: string): Promise<PublicPluginInstallation>;
update(pluginId: string): Promise<PublicPluginInstallation>;
uninstall(pluginId: string): Promise<PublicPluginInstallation>;
}
@@ -127,25 +128,33 @@ function publicInstallation(snapshot: InstallationSnapshot): PublicPluginInstall
async function publicLibrary(
library: MarketplaceLibrarySnapshot,
packageStore: Pick<PluginPackageStore, 'readInstalledIndex'>,
packageStore: Pick<PluginPackageStore, 'getInstalled'>,
): Promise<PublicPluginLibrary> {
const libraryPluginIds = new Set(library.items.map(({ pluginId }) => pluginId));
const latest = new Map<string, Awaited<ReturnType<PluginPackageStore['readInstalledIndex']>>[number]>();
for (const record of await packageStore.readInstalledIndex()) {
if (!libraryPluginIds.has(record.pluginId)) continue;
const current = latest.get(record.pluginId);
if (!current || record.installedAt >= current.installedAt) latest.set(record.pluginId, record);
}
return {
library,
installations: [...latest.values()]
.sort((left, right) => left.pluginId.localeCompare(right.pluginId))
.map((record) => ({
status: 'installed',
const installations = await Promise.all(library.items.map(async ({ pluginId }) => {
try {
const record = await packageStore.getInstalled(pluginId);
return record ? {
status: 'installed' as const,
pluginId: record.pluginId,
releaseId: record.releaseId,
version: record.version,
})),
} : null;
} catch (error) {
const candidate = error && typeof error === 'object' && 'code' in error
&& typeof error.code === 'string'
? error.code
: '';
const reason = /^[a-z][a-z0-9_]{0,63}$/u.test(candidate)
? candidate
: 'plugin_backend_unavailable';
return { status: 'unavailable' as const, pluginId, reason };
}
}));
return {
library,
installations: installations
.filter((installation): installation is NonNullable<typeof installation> => installation !== null)
.sort((left, right) => left.pluginId.localeCompare(right.pluginId)),
};
}
@@ -161,8 +170,18 @@ export function createCodingPluginMarketplaceService(
await options.onChanged?.({ pluginId, kind });
return publicInstallation(snapshot);
};
const installBeta = async (pluginId: string): Promise<PublicPluginInstallation> => {
const snapshot = await options.packageStore.resolveAndInstall({
pluginId,
makeloreVersion: options.clientVersion,
channel: 'beta',
explicitBeta: true,
});
await options.onChanged?.({ pluginId, kind: 'install' });
return publicInstallation(snapshot);
};
const uninstall = async (pluginId: string): Promise<PublicPluginInstallation> => {
const snapshot = await options.packageStore.removeUnused(pluginId);
const snapshot = await options.packageStore.uninstall(pluginId);
await options.onChanged?.({ pluginId, kind: 'uninstall' });
return publicInstallation(snapshot);
};
@@ -181,6 +200,7 @@ export function createCodingPluginMarketplaceService(
return publicLibrary(snapshot, options.packageStore);
},
install: (pluginId) => install(pluginId, 'install'),
installBeta,
update: (pluginId) => install(pluginId, 'update'),
uninstall,
};

View File

@@ -12,8 +12,21 @@ const ROOT = '/api/coding/plugin-marketplace';
const DETAIL = /^\/api\/coding\/plugin-marketplace\/plugins\/([^/]+)$/u;
const LIBRARY_MUTATION = /^\/api\/coding\/plugin-marketplace\/library\/([^/]+)$/u;
const INSTALLATION = /^\/api\/coding\/plugin-marketplace\/install\/([^/]+)$/u;
const BETA_INSTALLATION = /^\/api\/coding\/plugin-marketplace\/install\/([^/]+)\/beta$/u;
const UPDATE = /^\/api\/coding\/plugin-marketplace\/update\/([^/]+)$/u;
const PLUGIN_ID = /^[a-z][a-z0-9.-]{0,127}$/u;
const BOUNDED_MARKETPLACE_CODES = new Set([
'plugin_auth_required',
'plugin_account_changed',
'plugin_library_required',
'plugin_release_not_ready',
'plugin_release_yanked',
'plugin_incompatible_client',
'plugin_signature_invalid',
'plugin_artifact_invalid',
'plugin_runtime_suspended',
'plugin_backend_unavailable',
]);
class PluginMarketplaceRouteError extends Error {
constructor(
@@ -99,7 +112,8 @@ function sendError(res: ServerResponse, error: unknown): void {
: error.status === 409 ? 409
: error.status === 422 ? 422
: 503;
const code = status === 401 ? 'plugin_auth_required'
const code = BOUNDED_MARKETPLACE_CODES.has(error.code) ? error.code
: status === 401 ? 'plugin_auth_required'
: status === 403 ? 'plugin_library_required'
: status === 404 ? 'plugin_not_found'
: status === 409 ? (error.code === 'marketplace_account_changed'
@@ -123,12 +137,16 @@ function sendError(res: ServerResponse, error: unknown): void {
}
if (error instanceof PluginPackageStoreError) {
const status = error.code === 'plugin_release_unavailable' || error.code === 'plugin_beta_selection_required'
|| error.code === 'plugin_release_conflict' || error.code === 'plugin_incompatible_client'
|| error.code === 'plugin_release_conflict' || error.code === 'plugin_release_not_ready'
|| error.code === 'plugin_incompatible_client'
|| error.code === 'plugin_release_yanked' || error.code === 'plugin_runtime_suspended'
? 409
: error.code === 'plugin_library_required' ? 403
: error.code === 'plugin_artifact_invalid' || error.code === 'plugin_signature_invalid'
|| error.code === 'plugin_manifest_invalid' ? 422
: error.code === 'plugin_account_changed' ? 409 : 503;
const code = error.code === 'plugin_account_changed' ? 'plugin_account_changed'
const code = BOUNDED_MARKETPLACE_CODES.has(error.code) ? error.code
: error.code === 'plugin_account_changed' ? 'plugin_account_changed'
: status === 409 ? 'plugin_release_not_ready'
: status === 422 ? 'plugin_artifact_invalid'
: 'plugin_backend_unavailable';
@@ -165,6 +183,7 @@ function isKnownPath(pathname: string): boolean {
|| DETAIL.test(pathname)
|| LIBRARY_MUTATION.test(pathname)
|| INSTALLATION.test(pathname)
|| BETA_INSTALLATION.test(pathname)
|| UPDATE.test(pathname);
}
@@ -221,6 +240,13 @@ export async function handlePluginMarketplaceRoutes(
sendJson(res, 200, result);
return true;
}
const betaInstallationMatch = BETA_INSTALLATION.exec(url.pathname);
if (betaInstallationMatch && req.method === 'POST') {
exactNoQuery(url);
await exactEmptyBody(req);
sendJson(res, 200, await marketplace.installBeta(routePluginId(betaInstallationMatch[1])));
return true;
}
const updateMatch = UPDATE.exec(url.pathname);
if (updateMatch && req.method === 'POST') {
exactNoQuery(url);