// Host setter for the organization file's `plugins` switch (REQ-PLUG-1, // REQ-ROLE-3). It is the only writer of that key: no broker verb, mosaic // command, Settings form or chief-of-staff write reaches it (sage-0301). // Writes go through the organization writer, which logs old and new names // like a limit change. A write is a patch per kind (see set()): kinds it doesn't name are kept. A refused write throws a code and logs nothing. import { organizationWriter, OrganizationError } from "./organization.mjs"; import { verifiedPlugin } from "../../plugins/src/pinned.mjs"; import { hasModule } from "../../plugins/src/loader.mjs"; import { isPlainObject } from "./util.mjs"; import { KINDS, PluginError, checkPlugins, checkUsageSettings } from "../../plugins/src/kinds.mjs"; // `exists(kind, name)`, when given, refuses a name no module answers to (the host passes the // loader's hasModule). Without it the setter checks only the shape, as before. export function pluginSettings({ dir, rolesDir, exists = null, dataRoot = null }) { const writer = organizationWriter({ dir, rolesDir, dataRoot }); function read(id) { const { document, revision } = writer.read(id); return { business: id, revision, plugins: structuredClone(document.plugins ?? {}), ...(document.usage === undefined ? {} : { usage: structuredClone(document.usage) }) }; } function set(id, args, actor) { if (!isPlainObject(args) || Object.keys(args).some(key => key !== "revision" && key !== "plugins" && key !== "usage")) { throw new OrganizationError("invalid-request"); } if (typeof args.revision !== "string" || !/^[a-f0-9]{64}$/.test(args.revision)) { throw new OrganizationError("invalid-request"); } // A Date or a Map has no own keys and would pass checkPlugins, then serialize // as something else (Filbert F1, sage-0314). Only a JSON-shaped object passes. const shaped = args.plugins !== null && typeof args.plugins === "object"; const proto = shaped ? Object.getPrototypeOf(args.plugins) : undefined; if (proto !== Object.prototype && proto !== null) { throw new OrganizationError("invalid-plugins", "plugins must be a plain object with one key per kind"); } // The patch names the kinds it changes (vikunja:19/309): a kind with a value is switched to it, // a kind with null is removed, and every kind it doesn't name keeps its setting. An empty patch // changes nothing and is refused rather than written as a no-op. const patch = Object.entries(args.plugins); if (patch.length === 0) throw new OrganizationError("invalid-plugins", "the patch names no kind; to remove a kind give it null"); const removals = patch.filter(([, setting]) => setting === null).map(([kind]) => kind); const sets = Object.fromEntries(patch.filter(([, setting]) => setting !== null)); for (const kind of removals) { if (!Object.hasOwn(KINDS, kind)) throw new OrganizationError("invalid-plugins", `plugins names unknown kind ${JSON.stringify(kind)}; adding a kind is a core change`); } try { checkPlugins(sets, "plugins"); } catch (e) { if (e instanceof PluginError) throw new OrganizationError("invalid-plugins", e.message); throw e; } if (exists) { for (const [kind, setting] of Object.entries(sets)) { for (const name of Array.isArray(setting) ? setting : [setting]) { if (dataRoot !== null && !hasModule(kind,name)) { try { verifiedPlugin(dataRoot,name,kind); } catch(e) { throw new OrganizationError(e.code === "missing-lock" ? "unknown-plugin" : "invalid-plugins", "installed plugin verification refused"); } } if (!exists(kind, name)) throw new OrganizationError("unknown-plugin", `no ${kind} module named ${name} in this tree`); } } } const next = structuredClone(writer.read(id).document); if (Object.hasOwn(args, "usage")) { if (args.usage === null) delete next.usage; else next.usage = checkUsageSettings(args.usage); } next.plugins = { ...structuredClone(next.plugins ?? {}), ...structuredClone(sets) }; for (const kind of removals) delete next.plugins[kind]; const receipt = writer.write(id, next, { revision: args.revision, actor }); return { ...read(id), transaction: receipt.transaction }; } return { writer, read, set }; }