import { createRouter } from '../../create-router'; import * as errors from '../../custom-errors'; import { isSuperAdmin } from '../../super-admin'; import { mountPrefix } from '../../plugins/manifest'; import { snapshotPlugins } from '../../plugins/mount'; import { installPlugin, pluginProcessStatus, setPluginRunning, uninstallPlugin } from '../../plugins/install'; // /api/plugins — what is on this machine, what is installed, and the four verbs that change it. // // Owner only, in its own right. Installing a plugin mounts routes and (later) starts a process, which is // an administrative act however many members share the server. The capability layer covers it too; this // is the belt to that braces, the same shape `/api/app-store` uses. // // ── This is not the app store ── // // The app store installs SIDECARS from a compiled-in catalogue, provisioning containers and asking the // user questions. This installs PLUGINS from the tree, and asks nothing: put the code there, push the // schema, mount the routes. The two coexist until the app store is rebuilt on this. export const pluginsRouter = createRouter(); pluginsRouter.use(async (ctx, next) => { if (!(await isSuperAdmin(ctx.get('user')))) throw errors.FORBIDDEN('Plugins are owner-only'); return next(); }); /** * GET /api/plugins — every plugin in the tree, with what the database knows about each. * * Reports `broken` alongside rather than failing: a directory with an unreadable manifest is something to * show the owner, and refusing the whole list because one plugin is malformed would hide the nine that * are fine. */ pluginsRouter.get('/', async (ctx) => { const { states, broken } = await snapshotPlugins(); // Asked per plugin rather than once, because `pm2 jlist` is a fork and most plugins have no sidecar to // ask about. A plugin that is installed and enabled but whose process is not online is the state worth // rendering differently — it is the difference between "off" and "broken". const statuses = await Promise.all(states.map(({ plugin }) => pluginProcessStatus(plugin))); return ctx.json({ plugins: states.map(({ plugin, install, outdated }, i) => ({ appName: plugin.appName, prefix: mountPrefix(plugin), label: plugin.manifest.label, summary: plugin.manifest.summary, icon: plugin.manifest.icon, color: plugin.manifest.color, publisher: plugin.manifest.publisher, version: plugin.manifest.version, platform: plugin.manifest.platform, permissions: plugin.manifest.permissions, // What the tree declared. The UI shows these so "installed but does nothing" is legible. has: { api: !!plugin.api, schema: !!plugin.schema, sidecar: !!plugin.sidecar, web: !!plugin.web, }, installed: !!install, enabled: install?.enabled ?? false, installedVersion: install?.version ?? null, outdated, processStatus: statuses[i] ?? null, })), broken, }); }); /** * The four verbs. Each returns the ordered list of what actually happened, rather than a bare `ok` — * "installed" and "installed but the sidecar would not start" are different outcomes and the second is * the one worth reading. See `plugins/install.ts` for why the order inside each is what it is. */ pluginsRouter.post('/:appName/install', async (ctx) => { const result = await installPlugin(ctx.req.param('appName')); return ctx.json(result, result.ok ? 200 : 400); }); pluginsRouter.post('/:appName/uninstall', async (ctx) => { const result = await uninstallPlugin(ctx.req.param('appName')); return ctx.json(result, result.ok ? 200 : 400); }); pluginsRouter.post('/:appName/enable', async (ctx) => { const result = await setPluginRunning(ctx.req.param('appName'), true); return ctx.json(result, result.ok ? 200 : 400); }); pluginsRouter.post('/:appName/disable', async (ctx) => { const result = await setPluginRunning(ctx.req.param('appName'), false); return ctx.json(result, result.ok ? 200 : 400); });