77 lines
4.3 KiB
JavaScript
77 lines
4.3 KiB
JavaScript
// 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 };
|
|
}
|