feat: disable plugins by default, require admin approval
This commit is contained in:
+6
-1
@@ -25,6 +25,7 @@ export interface SettingRestriction {
|
||||
export interface FeatureGates {
|
||||
pluginsEnabled: boolean;
|
||||
pluginsUploadEnabled: boolean;
|
||||
requirePluginApproval: boolean;
|
||||
themesEnabled: boolean;
|
||||
sidebarAppsEnabled: boolean;
|
||||
userThemesEnabled: boolean;
|
||||
@@ -41,8 +42,9 @@ export interface FeatureGates {
|
||||
}
|
||||
|
||||
export const DEFAULT_FEATURE_GATES: FeatureGates = {
|
||||
pluginsEnabled: true,
|
||||
pluginsEnabled: false,
|
||||
pluginsUploadEnabled: true,
|
||||
requirePluginApproval: true,
|
||||
themesEnabled: true,
|
||||
sidebarAppsEnabled: true,
|
||||
userThemesEnabled: true,
|
||||
@@ -80,6 +82,8 @@ export interface SettingsPolicy {
|
||||
themePolicy: ThemePolicy;
|
||||
/** Plugin IDs that are force-enabled (users cannot disable) */
|
||||
forceEnabledPlugins: string[];
|
||||
/** Plugin IDs that have been approved by admin (users can enable) */
|
||||
approvedPlugins: string[];
|
||||
/** Theme IDs that are force-enabled (users cannot deactivate) */
|
||||
forceEnabledThemes: string[];
|
||||
}
|
||||
@@ -90,6 +94,7 @@ export const DEFAULT_POLICY: SettingsPolicy = {
|
||||
defaults: {},
|
||||
themePolicy: { ...DEFAULT_THEME_POLICY },
|
||||
forceEnabledPlugins: [],
|
||||
approvedPlugins: [],
|
||||
forceEnabledThemes: [],
|
||||
};
|
||||
|
||||
|
||||
+7
-7
@@ -27,7 +27,7 @@ import {
|
||||
import { toast as appToast } from '@/stores/toast-store';
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
|
||||
// ─── Permission helpers ──────────────────────────────────────
|
||||
// --- Permission helpers --------------------------------------
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function getPluginExternals(): any {
|
||||
@@ -61,7 +61,7 @@ function guardedHook<T extends (...args: never[]) => unknown>(
|
||||
return bus.register(plugin.id, handler, order);
|
||||
}
|
||||
|
||||
// ─── Plugin-scoped storage ───────────────────────────────────
|
||||
// --- Plugin-scoped storage -----------------------------------
|
||||
|
||||
function createPluginStorage(pluginId: string) {
|
||||
const prefix = `plugin:${pluginId}:`;
|
||||
@@ -93,7 +93,7 @@ function createPluginStorage(pluginId: string) {
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Plugin-scoped logger ────────────────────────────────────
|
||||
// --- Plugin-scoped logger ------------------------------------
|
||||
|
||||
function createPluginLogger(pluginId: string) {
|
||||
const tag = `[plugin:${pluginId}]`;
|
||||
@@ -105,7 +105,7 @@ function createPluginLogger(pluginId: string) {
|
||||
};
|
||||
}
|
||||
|
||||
// ─── PluginAPI interface ─────────────────────────────────────
|
||||
// --- PluginAPI interface -------------------------------------
|
||||
|
||||
export interface PluginAPI {
|
||||
plugin: { id: string; version: string; settings: Record<string, unknown> };
|
||||
@@ -316,7 +316,7 @@ export interface PluginHooksAPI {
|
||||
onSidebarAppChange: (handler: (...args: unknown[]) => unknown) => Disposable;
|
||||
}
|
||||
|
||||
// ─── Permission mapping for hooks ────────────────────────────
|
||||
// --- Permission mapping for hooks ----------------------------
|
||||
|
||||
const HOOK_PERMISSIONS: Record<string, Permission> = {
|
||||
// Email
|
||||
@@ -465,7 +465,7 @@ const HOOK_BUSES: Record<string, { register: (pluginId: string, handler: (...arg
|
||||
...Object.fromEntries(Object.entries(sidebarAppHooks)),
|
||||
};
|
||||
|
||||
// ─── Slot registration bridge ────────────────────────────────
|
||||
// --- Slot registration bridge --------------------------------
|
||||
// Lazy import to avoid circular dependency — plugin-store imports plugin-api indirectly
|
||||
|
||||
let registerSlotFn: ((name: SlotName, reg: { pluginId: string; component: React.ComponentType<Record<string, unknown>>; order: number }) => Disposable) | null = null;
|
||||
@@ -487,7 +487,7 @@ function registerSlot(
|
||||
return registerSlotFn(slotName, { pluginId, component, order });
|
||||
}
|
||||
|
||||
// ─── Factory ─────────────────────────────────────────────────
|
||||
// --- Factory -------------------------------------------------
|
||||
|
||||
export function createPluginAPI(plugin: InstalledPlugin): PluginAPI {
|
||||
// Build hooks proxy — each hook method checks permission and registers on the right bus
|
||||
|
||||
@@ -8,7 +8,7 @@ import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as ReactJSX from 'react/jsx-runtime';
|
||||
|
||||
// ─── Shared React (window.__PLUGIN_EXTERNALS__) ─────────────
|
||||
// --- Shared React (window.__PLUGIN_EXTERNALS__) -------------
|
||||
|
||||
export function exposePluginExternals(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
@@ -20,7 +20,7 @@ export function exposePluginExternals(): void {
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Active plugin tracking ──────────────────────────────────
|
||||
// --- Active plugin tracking ----------------------------------
|
||||
|
||||
interface ActivePlugin {
|
||||
id: string;
|
||||
@@ -31,7 +31,7 @@ interface ActivePlugin {
|
||||
|
||||
const activePlugins = new Map<string, ActivePlugin>();
|
||||
|
||||
// ─── Load a single plugin ────────────────────────────────────
|
||||
// --- Load a single plugin ------------------------------------
|
||||
|
||||
type PluginStoreAccessor = {
|
||||
setPluginStatus: (id: string, status: InstalledPlugin['status'], error?: string) => void;
|
||||
@@ -101,7 +101,7 @@ export async function loadPlugin(plugin: InstalledPlugin): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Deactivate a single plugin ──────────────────────────────
|
||||
// --- Deactivate a single plugin ------------------------------
|
||||
|
||||
export function deactivatePlugin(pluginId: string): void {
|
||||
const active = activePlugins.get(pluginId);
|
||||
@@ -127,7 +127,7 @@ export function deactivatePlugin(pluginId: string): void {
|
||||
console.info(`[plugin-loader] Plugin "${pluginId}" deactivated`);
|
||||
}
|
||||
|
||||
// ─── Activate all enabled plugins ────────────────────────────
|
||||
// --- Activate all enabled plugins ----------------------------
|
||||
|
||||
export async function activateAllPlugins(plugins: InstalledPlugin[]): Promise<void> {
|
||||
// Ensure externals are exposed
|
||||
@@ -139,7 +139,7 @@ export async function activateAllPlugins(plugins: InstalledPlugin[]): Promise<vo
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Deactivate all plugins ─────────────────────────────────
|
||||
// --- Deactivate all plugins ---------------------------------
|
||||
|
||||
export function deactivateAllPlugins(): void {
|
||||
for (const pluginId of [...activePlugins.keys()]) {
|
||||
@@ -147,13 +147,13 @@ export function deactivateAllPlugins(): void {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Check if a plugin is active ─────────────────────────────
|
||||
// --- Check if a plugin is active -----------------------------
|
||||
|
||||
export function isPluginActive(pluginId: string): boolean {
|
||||
return activePlugins.has(pluginId);
|
||||
}
|
||||
|
||||
// ─── Setup auto-disable callback ─────────────────────────────
|
||||
// --- Setup auto-disable callback -----------------------------
|
||||
|
||||
export function setupAutoDisable(): void {
|
||||
pluginErrorTracker.setAutoDisableCallback((pluginId) => {
|
||||
|
||||
@@ -79,6 +79,8 @@ export interface InstalledPlugin {
|
||||
managed?: boolean;
|
||||
// True when plugin is admin-enforced and cannot be disabled locally.
|
||||
forceEnabled?: boolean;
|
||||
// True when plugin has been approved by an admin. Unapproved plugins cannot be enabled.
|
||||
adminApproved?: boolean;
|
||||
settingsSchema?: Record<string, SettingFieldSchema>;
|
||||
settings: Record<string, unknown>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user