diff --git a/app/api/admin/plugins/route.ts b/app/api/admin/plugins/route.ts index dd81a9d0..c14003cb 100644 --- a/app/api/admin/plugins/route.ts +++ b/app/api/admin/plugins/route.ts @@ -187,6 +187,9 @@ export async function POST(request: NextRequest) { ...(manifest.configSchema && typeof manifest.configSchema === 'object' ? { configSchema: manifest.configSchema as ServerPlugin['configSchema'] } : {}), + ...(manifest.settingsSchema && typeof manifest.settingsSchema === 'object' + ? { settingsSchema: manifest.settingsSchema as ServerPlugin['settingsSchema'] } + : {}), ...(declaredFrameOrigins.length > 0 ? { frameOrigins: declaredFrameOrigins } : {}), diff --git a/app/api/plugins/route.ts b/app/api/plugins/route.ts index d3674534..19b7a7cf 100644 --- a/app/api/plugins/route.ts +++ b/app/api/plugins/route.ts @@ -43,7 +43,9 @@ export async function GET() { dev: p.dev, // Surface so clients can enforce api.http.fetch origin allowlists. httpOrigins: p.httpOrigins, - settingsSchema: undefined, // Will be read from the bundle's manifest + // 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 diff --git a/lib/admin/plugin-dev.ts b/lib/admin/plugin-dev.ts index 0632d8e6..7a5ea16d 100644 --- a/lib/admin/plugin-dev.ts +++ b/lib/admin/plugin-dev.ts @@ -185,6 +185,9 @@ async function loadDevPlugin(pluginDir: string): Promise ...(manifest.configSchema && typeof manifest.configSchema === 'object' ? { configSchema: manifest.configSchema as ServerPlugin['configSchema'] } : {}), + ...(manifest.settingsSchema && typeof manifest.settingsSchema === 'object' + ? { settingsSchema: manifest.settingsSchema as ServerPlugin['settingsSchema'] } + : {}), ...(frameOrigins.length > 0 ? { frameOrigins } : {}), ...(httpOrigins.length > 0 ? { httpOrigins } : {}), installedAt, diff --git a/lib/admin/plugin-registry.ts b/lib/admin/plugin-registry.ts index f3bc582e..e700eece 100644 --- a/lib/admin/plugin-registry.ts +++ b/lib/admin/plugin-registry.ts @@ -28,6 +28,21 @@ export interface PluginConfigField { options?: { label: string; value: string }[]; } +/** + * Per-user setting field, mirrors the manifest's `settingsSchema` shape + * (see lib/plugin-types.ts SettingFieldSchema). The server passes these + * through unchanged so the client can render the per-user settings UI. + */ +export interface PluginSettingsField { + type: 'boolean' | 'string' | 'number' | 'select'; + label: string; + description?: string; + default: unknown; + options?: string[]; + min?: number; + max?: number; +} + export interface ServerPlugin { id: string; name: string; @@ -40,6 +55,7 @@ export interface ServerPlugin { enabled: boolean; forceEnabled?: boolean; configSchema?: Record; + settingsSchema?: Record; installedAt: string; updatedAt: string; /** diff --git a/stores/plugin-store.ts b/stores/plugin-store.ts index 4ad281cf..2e433af9 100644 --- a/stores/plugin-store.ts +++ b/stores/plugin-store.ts @@ -313,6 +313,8 @@ interface ServerPluginInfo { dev?: boolean; /** Allowlist of origins this plugin may target via api.http.fetch(). */ httpOrigins?: string[]; + /** Per-user settings schema, captured from the manifest server-side. */ + settingsSchema?: InstalledPlugin['settingsSchema']; } const SERVER_MANAGED_KEY = 'server-managed-plugin-ids'; @@ -412,6 +414,7 @@ async function syncServerPlugins( forceEnabled: sp.forceEnabled, adminApproved: true, // Server-managed plugins are always approved settings: {}, + settingsSchema: sp.settingsSchema, bundleHash: sp.bundleHash, ...(sp.httpOrigins && sp.httpOrigins.length > 0 ? { httpOrigins: sp.httpOrigins } @@ -452,6 +455,7 @@ async function syncServerPlugins( forceEnabled: sp.forceEnabled, bundleHash: sp.bundleHash, httpOrigins: sp.httpOrigins, + settingsSchema: sp.settingsSchema, } : p ), @@ -464,10 +468,22 @@ async function syncServerPlugins( ...p, managed: true, forceEnabled: sp.forceEnabled, + settingsSchema: sp.settingsSchema, } : p ), })); + } else if ( + JSON.stringify(local.settingsSchema ?? null) !== JSON.stringify(sp.settingsSchema ?? null) + ) { + // Schema drift: the bundle is current but the persisted plugin record + // pre-dates the server passing settingsSchema through, so the per-user + // settings UI was rendering empty. Patch the schema in place. + set(state => ({ + plugins: state.plugins.map(p => + p.id === sp.id ? { ...p, settingsSchema: sp.settingsSchema } : p + ), + })); } else if (sp.forceEnabled && !local.enabled) { // Force-enable if the server says so but client has it disabled set(state => ({