fix: close marketplace client review findings
This commit is contained in:
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user