feat: marketplace update flow for installed plugins/themes
This commit is contained in:
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
import { useEffect, useState, useCallback } from 'react';
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Search, Download, Check, Loader2, Store, Puzzle, SwatchBook, Star, Eye, AlertTriangle } from 'lucide-react';
|
import { Search, Download, Check, Loader2, Store, Puzzle, SwatchBook, Star, Eye, AlertTriangle, ArrowUpCircle } from 'lucide-react';
|
||||||
import { apiFetch } from '@/lib/browser-navigation';
|
import { apiFetch } from '@/lib/browser-navigation';
|
||||||
import { isVersionSatisfied } from '@/lib/version-compare';
|
import { compareVersions, isVersionSatisfied } from '@/lib/version-compare';
|
||||||
|
|
||||||
const CURRENT_APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION || '0.0.0';
|
const CURRENT_APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION || '0.0.0';
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@ interface Extension {
|
|||||||
minAppVersion: string | null;
|
minAppVersion: string | null;
|
||||||
latestVersion: string | null;
|
latestVersion: string | null;
|
||||||
installed: boolean;
|
installed: boolean;
|
||||||
|
installedVersion: string | null;
|
||||||
iconUrl: string | null;
|
iconUrl: string | null;
|
||||||
bannerUrl: string | null;
|
bannerUrl: string | null;
|
||||||
author: {
|
author: {
|
||||||
@@ -104,6 +105,8 @@ export function MarketplaceTab() {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const isUpdate = ext.installed;
|
||||||
|
const targetVersion = ext.latestVersion || '1.0.0';
|
||||||
setInstalling(ext.slug);
|
setInstalling(ext.slug);
|
||||||
setMessage(null);
|
setMessage(null);
|
||||||
|
|
||||||
@@ -113,7 +116,7 @@ export function MarketplaceTab() {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
slug: ext.slug,
|
slug: ext.slug,
|
||||||
version: ext.latestVersion || '1.0.0',
|
version: targetVersion,
|
||||||
type: ext.type,
|
type: ext.type,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -122,13 +125,22 @@ export function MarketplaceTab() {
|
|||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const warnings = data.warnings?.length ? ` (${data.warnings.length} warning(s))` : '';
|
const warnings = data.warnings?.length ? ` (${data.warnings.length} warning(s))` : '';
|
||||||
setMessage({ type: 'success', text: `"${ext.name}" installed successfully${warnings}` });
|
setMessage({
|
||||||
setExtensions(prev => prev.map(e => e.slug === ext.slug ? { ...e, installed: true } : e));
|
type: 'success',
|
||||||
|
text: isUpdate
|
||||||
|
? `"${ext.name}" updated to v${targetVersion}${warnings}`
|
||||||
|
: `"${ext.name}" installed successfully${warnings}`,
|
||||||
|
});
|
||||||
|
setExtensions(prev => prev.map(e =>
|
||||||
|
e.slug === ext.slug
|
||||||
|
? { ...e, installed: true, installedVersion: targetVersion }
|
||||||
|
: e,
|
||||||
|
));
|
||||||
} else {
|
} else {
|
||||||
setMessage({ type: 'error', text: data.error || 'Installation failed' });
|
setMessage({ type: 'error', text: data.error || (isUpdate ? 'Update failed' : 'Installation failed') });
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setMessage({ type: 'error', text: 'Installation failed - network error' });
|
setMessage({ type: 'error', text: isUpdate ? 'Update failed - network error' : 'Installation failed - network error' });
|
||||||
} finally {
|
} finally {
|
||||||
setInstalling(null);
|
setInstalling(null);
|
||||||
}
|
}
|
||||||
@@ -270,6 +282,11 @@ function ExtensionCard({
|
|||||||
const previewHref = `/admin/marketplace/${encodeURIComponent(extension.slug)}`;
|
const previewHref = `/admin/marketplace/${encodeURIComponent(extension.slug)}`;
|
||||||
const versionMismatch = !!extension.minAppVersion
|
const versionMismatch = !!extension.minAppVersion
|
||||||
&& !isVersionSatisfied(CURRENT_APP_VERSION, extension.minAppVersion);
|
&& !isVersionSatisfied(CURRENT_APP_VERSION, extension.minAppVersion);
|
||||||
|
const updateAvailable = extension.installed
|
||||||
|
&& !!extension.installedVersion
|
||||||
|
&& !!extension.latestVersion
|
||||||
|
&& compareVersions(extension.latestVersion, extension.installedVersion) > 0
|
||||||
|
&& !versionMismatch;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group relative border border-border rounded-lg overflow-hidden hover:border-ring/30 transition-colors">
|
<div className="group relative border border-border rounded-lg overflow-hidden hover:border-ring/30 transition-colors">
|
||||||
@@ -359,8 +376,25 @@ function ExtensionCard({
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="px-4 pb-4 -mt-1 flex items-center gap-2 flex-wrap">
|
<div className="px-4 pb-4 -mt-1 flex items-center gap-2 flex-wrap">
|
||||||
{extension.installed ? (
|
{extension.installed && updateAvailable ? (
|
||||||
<span className="inline-flex items-center gap-1 h-7 px-2.5 rounded-md bg-emerald-100 text-emerald-700 dark:bg-emerald-950/30 dark:text-emerald-400 text-xs font-medium">
|
<button
|
||||||
|
onClick={(e) => { e.preventDefault(); e.stopPropagation(); onInstall(); }}
|
||||||
|
disabled={installing}
|
||||||
|
title={`Update from v${extension.installedVersion} to v${extension.latestVersion}`}
|
||||||
|
className="inline-flex items-center gap-1.5 h-7 px-3 rounded-md bg-blue-600 text-white text-xs font-medium hover:bg-blue-700 disabled:opacity-50 transition-colors"
|
||||||
|
>
|
||||||
|
{installing ? (
|
||||||
|
<Loader2 className="w-3 h-3 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<ArrowUpCircle className="w-3 h-3" />
|
||||||
|
)}
|
||||||
|
Update to v{extension.latestVersion}
|
||||||
|
</button>
|
||||||
|
) : extension.installed ? (
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center gap-1 h-7 px-2.5 rounded-md bg-emerald-100 text-emerald-700 dark:bg-emerald-950/30 dark:text-emerald-400 text-xs font-medium"
|
||||||
|
title={extension.installedVersion ? `Installed: v${extension.installedVersion}` : undefined}
|
||||||
|
>
|
||||||
<Check className="w-3 h-3" />
|
<Check className="w-3 h-3" />
|
||||||
Installed
|
Installed
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useParams } from 'next/navigation';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
|
ArrowUpCircle,
|
||||||
Download,
|
Download,
|
||||||
Loader2,
|
Loader2,
|
||||||
Puzzle,
|
Puzzle,
|
||||||
@@ -21,7 +22,7 @@ import {
|
|||||||
ChevronUp,
|
ChevronUp,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { apiFetch } from '@/lib/browser-navigation';
|
import { apiFetch } from '@/lib/browser-navigation';
|
||||||
import { isVersionSatisfied } from '@/lib/version-compare';
|
import { compareVersions, isVersionSatisfied } from '@/lib/version-compare';
|
||||||
|
|
||||||
const CURRENT_APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION || '0.0.0';
|
const CURRENT_APP_VERSION = process.env.NEXT_PUBLIC_APP_VERSION || '0.0.0';
|
||||||
|
|
||||||
@@ -73,6 +74,7 @@ interface PreviewData {
|
|||||||
error: string | null;
|
error: string | null;
|
||||||
};
|
};
|
||||||
installed: boolean;
|
installed: boolean;
|
||||||
|
installedVersion: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RISKY_PERMISSIONS = new Set([
|
const RISKY_PERMISSIONS = new Set([
|
||||||
@@ -118,6 +120,8 @@ export default function MarketplacePreviewPage() {
|
|||||||
|
|
||||||
async function handleInstall() {
|
async function handleInstall() {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
const isUpdate = data.installed;
|
||||||
|
const targetVersion = data.extension.latestVersion || '1.0.0';
|
||||||
setInstalling(true);
|
setInstalling(true);
|
||||||
setMessage(null);
|
setMessage(null);
|
||||||
try {
|
try {
|
||||||
@@ -126,20 +130,25 @@ export default function MarketplacePreviewPage() {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
slug: data.extension.slug,
|
slug: data.extension.slug,
|
||||||
version: data.extension.latestVersion || '1.0.0',
|
version: targetVersion,
|
||||||
type: data.extension.type,
|
type: data.extension.type,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const warnings = body.warnings?.length ? ` (${body.warnings.length} warning(s))` : '';
|
const warnings = body.warnings?.length ? ` (${body.warnings.length} warning(s))` : '';
|
||||||
setMessage({ type: 'success', text: `"${data.extension.name}" installed${warnings}` });
|
setMessage({
|
||||||
setData(prev => prev ? { ...prev, installed: true } : prev);
|
type: 'success',
|
||||||
|
text: isUpdate
|
||||||
|
? `"${data.extension.name}" updated to v${targetVersion}${warnings}`
|
||||||
|
: `"${data.extension.name}" installed${warnings}`,
|
||||||
|
});
|
||||||
|
setData(prev => prev ? { ...prev, installed: true, installedVersion: targetVersion } : prev);
|
||||||
} else {
|
} else {
|
||||||
setMessage({ type: 'error', text: body.error || 'Installation failed' });
|
setMessage({ type: 'error', text: body.error || (isUpdate ? 'Update failed' : 'Installation failed') });
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setMessage({ type: 'error', text: 'Installation failed - network error' });
|
setMessage({ type: 'error', text: isUpdate ? 'Update failed - network error' : 'Installation failed - network error' });
|
||||||
} finally {
|
} finally {
|
||||||
setInstalling(false);
|
setInstalling(false);
|
||||||
}
|
}
|
||||||
@@ -204,6 +213,11 @@ export default function MarketplacePreviewPage() {
|
|||||||
const frameOrigins = (bundle.manifest?.frameOrigins as string[] | undefined) || [];
|
const frameOrigins = (bundle.manifest?.frameOrigins as string[] | undefined) || [];
|
||||||
const settingsSchema = bundle.manifest?.settingsSchema as Record<string, { type: string; label: string; description?: string; default?: unknown }> | undefined;
|
const settingsSchema = bundle.manifest?.settingsSchema as Record<string, { type: string; label: string; description?: string; default?: unknown }> | undefined;
|
||||||
const versionMismatch = !!ext.minAppVersion && !isVersionSatisfied(CURRENT_APP_VERSION, ext.minAppVersion);
|
const versionMismatch = !!ext.minAppVersion && !isVersionSatisfied(CURRENT_APP_VERSION, ext.minAppVersion);
|
||||||
|
const updateAvailable = data.installed
|
||||||
|
&& !!data.installedVersion
|
||||||
|
&& !!ext.latestVersion
|
||||||
|
&& compareVersions(ext.latestVersion, data.installedVersion) > 0
|
||||||
|
&& !versionMismatch;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 max-w-4xl">
|
<div className="space-y-6 max-w-4xl">
|
||||||
@@ -248,11 +262,22 @@ export default function MarketplacePreviewPage() {
|
|||||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||||
<h1 className="text-2xl font-semibold text-foreground break-words min-w-0">{ext.name}</h1>
|
<h1 className="text-2xl font-semibold text-foreground break-words min-w-0">{ext.name}</h1>
|
||||||
{ext.featured && <Star className="w-4 h-4 text-warning fill-warning shrink-0" />}
|
{ext.featured && <Star className="w-4 h-4 text-warning fill-warning shrink-0" />}
|
||||||
{data.installed && (
|
{data.installed && !updateAvailable && (
|
||||||
<span className="inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-md bg-emerald-100 text-emerald-700 dark:bg-emerald-950/30 dark:text-emerald-400 font-medium">
|
<span
|
||||||
|
className="inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-md bg-emerald-100 text-emerald-700 dark:bg-emerald-950/30 dark:text-emerald-400 font-medium"
|
||||||
|
title={data.installedVersion ? `Installed: v${data.installedVersion}` : undefined}
|
||||||
|
>
|
||||||
<Check className="w-3 h-3" /> Installed
|
<Check className="w-3 h-3" /> Installed
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{data.installed && updateAvailable && (
|
||||||
|
<span
|
||||||
|
className="inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-md bg-blue-100 text-blue-700 dark:bg-blue-950/30 dark:text-blue-400 font-medium"
|
||||||
|
title={`Installed v${data.installedVersion} → v${ext.latestVersion} available`}
|
||||||
|
>
|
||||||
|
<ArrowUpCircle className="w-3 h-3" /> Update available
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 mt-1 text-sm text-muted-foreground flex-wrap">
|
<div className="flex items-center gap-2 mt-1 text-sm text-muted-foreground flex-wrap">
|
||||||
<span className={`text-[10px] px-1.5 py-0.5 rounded font-medium ${
|
<span className={`text-[10px] px-1.5 py-0.5 rounded font-medium ${
|
||||||
@@ -279,6 +304,17 @@ export default function MarketplacePreviewPage() {
|
|||||||
<div className="flex flex-wrap items-center gap-2 shrink-0">
|
<div className="flex flex-wrap items-center gap-2 shrink-0">
|
||||||
{data.installed ? (
|
{data.installed ? (
|
||||||
<>
|
<>
|
||||||
|
{updateAvailable && (
|
||||||
|
<button
|
||||||
|
onClick={handleInstall}
|
||||||
|
disabled={installing || !!bundle.error}
|
||||||
|
title={`Update from v${data.installedVersion} to v${ext.latestVersion}`}
|
||||||
|
className="inline-flex items-center gap-1.5 h-9 px-4 rounded-md bg-blue-600 text-white text-sm font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
{installing ? <Loader2 className="w-4 h-4 animate-spin" /> : <ArrowUpCircle className="w-4 h-4" />}
|
||||||
|
Update to v{ext.latestVersion}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<Link
|
<Link
|
||||||
href={isPlugin ? `/admin/plugins/${ext.slug}` : '/admin/themes'}
|
href={isPlugin ? `/admin/plugins/${ext.slug}` : '/admin/themes'}
|
||||||
className="inline-flex items-center gap-1.5 h-9 px-3 rounded-md border border-border text-sm font-medium text-foreground hover:bg-muted transition-colors"
|
className="inline-flex items-center gap-1.5 h-9 px-3 rounded-md border border-border text-sm font-medium text-foreground hover:bg-muted transition-colors"
|
||||||
|
|||||||
@@ -149,9 +149,11 @@ export async function GET(
|
|||||||
getPluginRegistry(),
|
getPluginRegistry(),
|
||||||
getThemeRegistry(),
|
getThemeRegistry(),
|
||||||
]);
|
]);
|
||||||
const installed = type === 'theme'
|
const installedEntry = type === 'theme'
|
||||||
? themeRegistry.themes.some((t) => t.id === slug)
|
? themeRegistry.themes.find((t) => t.id === slug)
|
||||||
: pluginRegistry.plugins.some((p) => p.id === slug);
|
: pluginRegistry.plugins.find((p) => p.id === slug);
|
||||||
|
const installed = installedEntry !== undefined;
|
||||||
|
const installedVersion = installedEntry?.version ?? null;
|
||||||
|
|
||||||
// 4. Build screenshot URLs (proxy through the directory's public files endpoint).
|
// 4. Build screenshot URLs (proxy through the directory's public files endpoint).
|
||||||
const screenshots = Array.isArray(extension.screenshots)
|
const screenshots = Array.isArray(extension.screenshots)
|
||||||
@@ -211,6 +213,7 @@ export async function GET(
|
|||||||
error: bundleError,
|
error: bundleError,
|
||||||
},
|
},
|
||||||
installed,
|
installed,
|
||||||
|
installedVersion,
|
||||||
},
|
},
|
||||||
{ headers: { 'Cache-Control': 'no-store' } },
|
{ headers: { 'Cache-Control': 'no-store' } },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { logger } from '@/lib/logger';
|
|||||||
import {
|
import {
|
||||||
savePlugin,
|
savePlugin,
|
||||||
saveTheme,
|
saveTheme,
|
||||||
|
getPlugin,
|
||||||
|
getTheme,
|
||||||
getPluginRegistry,
|
getPluginRegistry,
|
||||||
getThemeRegistry,
|
getThemeRegistry,
|
||||||
type ServerPlugin,
|
type ServerPlugin,
|
||||||
@@ -64,8 +66,12 @@ export async function GET(request: NextRequest) {
|
|||||||
getThemeRegistry(),
|
getThemeRegistry(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const installedPlugins = new Set(pluginRegistry.plugins.map(p => p.id));
|
const installedPluginVersions = new Map(
|
||||||
const installedThemes = new Set(themeRegistry.themes.map(t => t.id));
|
pluginRegistry.plugins.map(p => [p.id, p.version] as const),
|
||||||
|
);
|
||||||
|
const installedThemeVersions = new Map(
|
||||||
|
themeRegistry.themes.map(t => [t.id, t.version] as const),
|
||||||
|
);
|
||||||
|
|
||||||
const fileUrl = (path: unknown): string | null =>
|
const fileUrl = (path: unknown): string | null =>
|
||||||
typeof path === 'string' && path
|
typeof path === 'string' && path
|
||||||
@@ -73,14 +79,19 @@ export async function GET(request: NextRequest) {
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (data.data) {
|
if (data.data) {
|
||||||
data.data = data.data.map((ext: Record<string, unknown>) => ({
|
data.data = data.data.map((ext: Record<string, unknown>) => {
|
||||||
...ext,
|
const slug = ext.slug as string;
|
||||||
iconUrl: fileUrl(ext.iconPath),
|
const installedVersion = ext.type === 'theme'
|
||||||
bannerUrl: fileUrl(ext.bannerPath),
|
? installedThemeVersions.get(slug) ?? null
|
||||||
installed: ext.type === 'theme'
|
: installedPluginVersions.get(slug) ?? null;
|
||||||
? installedThemes.has(ext.slug as string)
|
return {
|
||||||
: installedPlugins.has(ext.slug as string),
|
...ext,
|
||||||
}));
|
iconUrl: fileUrl(ext.iconPath),
|
||||||
|
bannerUrl: fileUrl(ext.bannerPath),
|
||||||
|
installed: installedVersion !== null,
|
||||||
|
installedVersion,
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json(data, {
|
return NextResponse.json(data, {
|
||||||
@@ -200,6 +211,9 @@ export async function POST(request: NextRequest) {
|
|||||||
warnings.push(...sanitized.warnings);
|
warnings.push(...sanitized.warnings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const existingTheme = await getTheme(resolvedId);
|
||||||
|
const isUpdate = existingTheme !== null;
|
||||||
|
|
||||||
const theme: ServerTheme = {
|
const theme: ServerTheme = {
|
||||||
id: resolvedId,
|
id: resolvedId,
|
||||||
name: (manifest.name as string) || slug,
|
name: (manifest.name as string) || slug,
|
||||||
@@ -207,15 +221,28 @@ export async function POST(request: NextRequest) {
|
|||||||
author: (manifest.author as string) || 'Unknown',
|
author: (manifest.author as string) || 'Unknown',
|
||||||
description: (manifest.description as string) || '',
|
description: (manifest.description as string) || '',
|
||||||
variants: (manifest.variants as string[]) || ['light', 'dark'],
|
variants: (manifest.variants as string[]) || ['light', 'dark'],
|
||||||
enabled: true,
|
enabled: existingTheme?.enabled ?? true,
|
||||||
installedAt: now,
|
...(existingTheme?.forceEnabled !== undefined
|
||||||
|
? { forceEnabled: existingTheme.forceEnabled }
|
||||||
|
: {}),
|
||||||
|
installedAt: existingTheme?.installedAt ?? now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
};
|
};
|
||||||
|
|
||||||
await saveTheme(theme, css);
|
await saveTheme(theme, css);
|
||||||
await auditLog('marketplace.install_theme', { id: theme.id, name: theme.name, version: theme.version, slug }, ip);
|
await auditLog(
|
||||||
|
isUpdate ? 'marketplace.update_theme' : 'marketplace.install_theme',
|
||||||
|
{
|
||||||
|
id: theme.id,
|
||||||
|
name: theme.name,
|
||||||
|
version: theme.version,
|
||||||
|
slug,
|
||||||
|
...(isUpdate ? { previousVersion: existingTheme.version } : {}),
|
||||||
|
},
|
||||||
|
ip,
|
||||||
|
);
|
||||||
|
|
||||||
return NextResponse.json({ success: true, theme, warnings });
|
return NextResponse.json({ success: true, theme, warnings, updated: isUpdate });
|
||||||
} else {
|
} else {
|
||||||
// Plugin installation
|
// Plugin installation
|
||||||
// Read entrypoint JS
|
// Read entrypoint JS
|
||||||
@@ -297,6 +324,9 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const existingPlugin = await getPlugin(resolvedId);
|
||||||
|
const isUpdate = existingPlugin !== null;
|
||||||
|
|
||||||
const plugin: ServerPlugin = {
|
const plugin: ServerPlugin = {
|
||||||
id: resolvedId,
|
id: resolvedId,
|
||||||
name: (manifest.name as string) || slug,
|
name: (manifest.name as string) || slug,
|
||||||
@@ -306,8 +336,11 @@ export async function POST(request: NextRequest) {
|
|||||||
type: (manifest.type as string) || 'hook',
|
type: (manifest.type as string) || 'hook',
|
||||||
permissions,
|
permissions,
|
||||||
entrypoint,
|
entrypoint,
|
||||||
enabled: true,
|
enabled: existingPlugin?.enabled ?? true,
|
||||||
installedAt: now,
|
...(existingPlugin?.forceEnabled !== undefined
|
||||||
|
? { forceEnabled: existingPlugin.forceEnabled }
|
||||||
|
: {}),
|
||||||
|
installedAt: existingPlugin?.installedAt ?? now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
...(manifest.configSchema && typeof manifest.configSchema === 'object'
|
...(manifest.configSchema && typeof manifest.configSchema === 'object'
|
||||||
? { configSchema: manifest.configSchema as ServerPlugin['configSchema'] }
|
? { configSchema: manifest.configSchema as ServerPlugin['configSchema'] }
|
||||||
@@ -328,9 +361,22 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
await savePlugin(plugin, code);
|
await savePlugin(plugin, code);
|
||||||
invalidateFrameOriginsCache();
|
invalidateFrameOriginsCache();
|
||||||
await auditLog('marketplace.install_plugin', { id: plugin.id, name: plugin.name, version: plugin.version, slug, frameOrigins: declaredFrameOrigins, httpOrigins: declaredHttpOrigins, apiPostPaths: declaredApiPostPaths }, ip);
|
await auditLog(
|
||||||
|
isUpdate ? 'marketplace.update_plugin' : 'marketplace.install_plugin',
|
||||||
|
{
|
||||||
|
id: plugin.id,
|
||||||
|
name: plugin.name,
|
||||||
|
version: plugin.version,
|
||||||
|
slug,
|
||||||
|
frameOrigins: declaredFrameOrigins,
|
||||||
|
httpOrigins: declaredHttpOrigins,
|
||||||
|
apiPostPaths: declaredApiPostPaths,
|
||||||
|
...(isUpdate ? { previousVersion: existingPlugin.version } : {}),
|
||||||
|
},
|
||||||
|
ip,
|
||||||
|
);
|
||||||
|
|
||||||
return NextResponse.json({ success: true, plugin, warnings });
|
return NextResponse.json({ success: true, plugin, warnings, updated: isUpdate });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Marketplace install error', { error: error instanceof Error ? error.message : 'Unknown error' });
|
logger.error('Marketplace install error', { error: error instanceof Error ? error.message : 'Unknown error' });
|
||||||
|
|||||||
Reference in New Issue
Block a user