import { NextResponse } from 'next/server'; import { getPluginRegistry, getThemeRegistry } from '@/lib/admin/plugin-registry'; import { listDevPlugins } from '@/lib/admin/plugin-dev'; import { configManager } from '@/lib/admin/config-manager'; import { logger } from '@/lib/logger'; /** * GET /api/plugins - Public endpoint for clients to discover server-managed plugins & themes * * Returns all enabled plugins and themes so the client can sync them to IndexedDB. * No admin auth required - this is how regular users receive plugins/themes. */ export async function GET() { try { await configManager.ensureLoaded(); const policy = configManager.getPolicy(); const policyForceEnabledIds = new Set(policy.forceEnabledPlugins || []); const [pluginRegistry, themeRegistry, devEntries] = await Promise.all([ getPluginRegistry(), getThemeRegistry(), listDevPlugins(), ]); // Dev plugins win on id collision so a developer can shadow an installed // plugin without uninstalling it first. const devIds = new Set(devEntries.map(e => e.plugin.id)); const installedEnabled = pluginRegistry.plugins.filter(p => p.enabled && !devIds.has(p.id)); const plugins = [ ...devEntries.map(e => ({ ...e.plugin, dev: true })), ...installedEnabled.map(p => ({ ...p, dev: false })), ].map(p => ({ id: p.id, name: p.name, version: p.version, author: p.author, description: p.description, type: p.type, permissions: p.permissions, entrypoint: p.entrypoint, // Policy is the canonical source for force-enable. The per-plugin field // can drift for dev plugins (manifest always loads forceEnabled:false) // and during pending policy saves; OR'ing here unifies the signal so // the client's auto-enable path triggers consistently. forceEnabled: p.forceEnabled || policyForceEnabledIds.has(p.id), // Content hash + updatedAt let clients detect re-uploads even when // the manifest version is unchanged. bundleHash: p.bundleHash, updatedAt: p.updatedAt, // Marks plugins loaded from PLUGIN_DEV_DIR. Surface in UI as a badge. dev: p.dev, // Surface so clients can enforce api.http.fetch origin allowlists. httpOrigins: p.httpOrigins, // Surface so clients can enforce api.http.post path allowlists. apiPostPaths: p.apiPostPaths, // Per-user settings schema, captured from the manifest at upload/load // time so the client can render the settings UI without re-parsing. settingsSchema: p.settingsSchema, })); // Only serve enabled themes const themes = themeRegistry.themes .filter(t => t.enabled) .map(t => ({ id: t.id, name: t.name, version: t.version, author: t.author, description: t.description, variants: t.variants, forceEnabled: t.forceEnabled || false, })); return NextResponse.json( { plugins, themes }, { headers: { 'Cache-Control': 'no-store' } }, ); } catch (error) { logger.error('Plugin list error', { error: error instanceof Error ? error.message : 'Unknown error' }); return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); } }