feat(plugin): Add override settings to plugin loading

This commit is contained in:
Jesse Wierzbinski 2024-10-06 15:55:15 +02:00
parent c0805ff125
commit f26ab0f0e6
No known key found for this signature in database
7 changed files with 705 additions and 548 deletions

View file

@ -287,6 +287,6 @@ max_coeff = 1.0
[plugins]
[plugins."@versia/openid".keys]
[plugins.config."@versia/openid".keys]
private = "MC4CAQAwBQYDK2VwBCIEID+H5n9PY3zVKZQcq4jrnE1IiRd2EWWr8ApuHUXmuOzl"
public = "MCowBQYDK2VwAyEAzenliNkgpXYsh3gXTnAoUWzlCPjIOppmAVx2DBlLsC8="

11
app.ts
View file

@ -119,19 +119,24 @@ export const appFactory = async () => {
const loader = new PluginLoader();
const plugins = await loader.loadPlugins(join(process.cwd(), "plugins"));
const plugins = await loader.loadPlugins(
join(process.cwd(), "plugins"),
config.plugins?.autoload,
config.plugins?.overrides.enabled,
config.plugins?.overrides.disabled,
);
for (const data of plugins) {
serverLogger.info`Loading plugin ${chalk.blueBright(data.manifest.name)} ${chalk.blueBright(data.manifest.version)} ${chalk.gray(`[${plugins.indexOf(data) + 1}/${plugins.length}]`)}`;
try {
// biome-ignore lint/complexity/useLiteralKeys: loadConfig is a private method
await data.plugin["_loadConfig"](
config.plugins?.[data.manifest.name],
config.plugins?.config?.[data.manifest.name],
);
} catch (e) {
serverLogger.fatal`Plugin configuration is invalid: ${chalk.redBright(e as ValidationError)}`;
serverLogger.fatal`Put your configuration at ${chalk.blueBright(
"plugins.<plugin-name>",
"plugins.config.<plugin-name>",
)}`;
throw new Error("Plugin configuration is invalid");
}

View file

@ -201,7 +201,7 @@ describe("PluginLoader", () => {
default: mockPlugin,
}));
const plugins = await pluginLoader.loadPlugins("/some/path");
const plugins = await pluginLoader.loadPlugins("/some/path", true);
expect(plugins).toEqual([
{
manifest: manifestContent,

View file

@ -162,12 +162,42 @@ export class PluginLoader {
*/
public async loadPlugins(
dir: string,
autoload: boolean,
enabled?: string[],
disabled?: string[],
): Promise<{ manifest: Manifest; plugin: Plugin<ZodTypeAny> }[]> {
const plugins = await PluginLoader.findPlugins(dir);
const enabledOn = (enabled?.length ?? 0) > 0;
const disabledOn = (disabled?.length ?? 0) > 0;
if (enabledOn && disabledOn) {
this.logger
.fatal`Both enabled and disabled lists are specified. Only one of them can be used.`;
throw new Error("Invalid configuration");
}
return Promise.all(
plugins.map(async (plugin) => {
const manifest = await this.parseManifest(dir, plugin);
// If autoload is disabled, only load plugins explicitly enabled
if (
!(autoload || enabledOn || enabled?.includes(manifest.name))
) {
return null;
}
// If enabled is specified, only load plugins in the enabled list
// If disabled is specified, only load plugins not in the disabled list
if (enabledOn && !enabled?.includes(manifest.name)) {
return null;
}
if (disabled?.includes(manifest.name)) {
return null;
}
const pluginInstance = await this.loadPlugin(
dir,
`${plugin}/index`,
@ -175,6 +205,6 @@ export class PluginLoader {
return { manifest, plugin: pluginInstance };
}),
);
).then((data) => data.filter((d) => d !== null));
}
}

View file

@ -4006,10 +4006,44 @@
}
},
"plugins": {
"type": "object",
"properties": {
"autoload": {
"type": "boolean",
"default": true
},
"overrides": {
"type": "object",
"properties": {
"enabled": {
"type": "array",
"items": {
"type": "string"
},
"default": []
},
"disabled": {
"type": "array",
"items": {
"type": "string"
},
"default": []
}
},
"additionalProperties": false,
"default": {
"enabled": [],
"disabled": []
}
},
"config": {
"type": "object",
"additionalProperties": {}
}
},
"additionalProperties": false
}
},
"required": [
"database",
"redis",
@ -4019,7 +4053,8 @@
"http",
"smtp",
"filters",
"ratelimits"
"ratelimits",
"plugins"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"

View file

@ -21,8 +21,10 @@ const zUrl = z
.refine((arg) => URL.canParse(arg), "Invalid url")
.transform((arg) => arg.replace(/\/$/, ""));
export const configValidator = z.object({
database: z.object({
export const configValidator = z
.object({
database: z
.object({
host: z.string().min(1).default("localhost"),
port: z
.number()
@ -35,7 +37,8 @@ export const configValidator = z.object({
database: z.string().min(1).default("versia"),
replicas: z
.array(
z.object({
z
.object({
host: z.string().min(1),
port: z
.number()
@ -46,11 +49,14 @@ export const configValidator = z.object({
username: z.string().min(1),
password: z.string().default(""),
database: z.string().min(1).default("versia"),
}),
})
.strict(),
)
.optional(),
}),
redis: z.object({
})
.strict(),
redis: z
.object({
queue: z
.object({
host: z.string().min(1).default("localhost"),
@ -64,6 +70,7 @@ export const configValidator = z.object({
database: z.number().int().default(0),
enabled: z.boolean().default(false),
})
.strict()
.default({
host: "localhost",
port: 6379,
@ -84,6 +91,7 @@ export const configValidator = z.object({
database: z.number().int().default(1),
enabled: z.boolean().default(false),
})
.strict()
.default({
host: "localhost",
port: 6379,
@ -91,8 +99,10 @@ export const configValidator = z.object({
database: 1,
enabled: false,
}),
}),
sonic: z.object({
})
.strict(),
sonic: z
.object({
host: z.string().min(1).default("localhost"),
port: z
.number()
@ -102,24 +112,30 @@ export const configValidator = z.object({
.default(7700),
password: z.string(),
enabled: z.boolean().default(false),
}),
signups: z.object({
})
.strict(),
signups: z
.object({
registration: z.boolean().default(true),
rules: z.array(z.string()).default([]),
}),
oidc: z.object({
})
.strict(),
oidc: z
.object({
forced: z.boolean().default(false),
allow_registration: z.boolean().default(true),
providers: z
.array(
z.object({
z
.object({
name: z.string().min(1),
id: z.string().min(1),
url: z.string().min(1),
client_id: z.string().min(1),
client_secret: z.string().min(1),
icon: z.string().min(1).optional(),
}),
})
.strict(),
)
.default([]),
keys: z
@ -127,9 +143,12 @@ export const configValidator = z.object({
public: z.string().min(1).optional(),
private: z.string().min(1).optional(),
})
.strict()
.optional(),
}),
http: z.object({
})
.strict(),
http: z
.object({
base_url: z.string().min(1).default("http://versia.social"),
bind: z.string().min(1).default("0.0.0.0"),
bind_port: z
@ -146,6 +165,7 @@ export const configValidator = z.object({
enabled: z.boolean().default(false),
address: zUrl.or(z.literal("")),
})
.strict()
.default({
enabled: false,
address: "",
@ -166,6 +186,7 @@ export const configValidator = z.object({
passphrase: z.string().optional(),
ca: z.string().optional(),
})
.strict()
.default({
enabled: false,
key: "",
@ -180,13 +201,15 @@ export const configValidator = z.object({
bait_ips: z.array(z.string()).default([]),
bait_user_agents: z.array(z.string()).default([]),
})
.strict()
.default({
enabled: false,
send_file: "",
bait_ips: [],
bait_user_agents: [],
}),
}),
})
.strict(),
frontend: z
.object({
enabled: z.boolean().default(true),
@ -199,6 +222,7 @@ export const configValidator = z.object({
register: zUrlPath.default("/register"),
password_reset: zUrlPath.default("/oauth/reset"),
})
.strict()
.default({
home: "/",
login: "/oauth/authorize",
@ -208,6 +232,7 @@ export const configValidator = z.object({
}),
settings: z.record(z.string(), z.any()).default({}),
})
.strict()
.default({
enabled: true,
url: "http://localhost:3000",
@ -227,6 +252,7 @@ export const configValidator = z.object({
tls: z.boolean().default(true),
enabled: z.boolean().default(false),
})
.strict()
.default({
server: "",
port: 465,
@ -248,12 +274,14 @@ export const configValidator = z.object({
convert_to: z.string().default("image/webp"),
convert_vector: z.boolean().default(false),
})
.strict()
.default({
convert_images: false,
convert_to: "image/webp",
convert_vector: false,
}),
})
.strict()
.default({
backend: MediaBackendType.Local,
deduplicate_media: true,
@ -272,6 +300,7 @@ export const configValidator = z.object({
bucket_name: z.string().default("versia"),
public_url: zUrl,
})
.strict()
.default({
endpoint: "",
access_key: "",
@ -361,6 +390,7 @@ export const configValidator = z.object({
expiration: z.number().int().positive().default(300),
key: z.string().default(""),
})
.strict()
.default({
enabled: true,
difficulty: 50000,
@ -368,6 +398,7 @@ export const configValidator = z.object({
key: "",
}),
})
.strict()
.default({
max_displayname_size: 50,
max_bio_size: 5000,
@ -450,6 +481,7 @@ export const configValidator = z.object({
header: zUrl.optional(),
placeholder_style: z.string().default("thumbs"),
})
.strict()
.default({
visibility: "public",
language: "en",
@ -461,7 +493,8 @@ export const configValidator = z.object({
.object({
blocked: z.array(zUrl).default([]),
followers_only: z.array(zUrl).default([]),
discard: z.object({
discard: z
.object({
reports: z.array(zUrl).default([]),
deletes: z.array(zUrl).default([]),
updates: z.array(zUrl).default([]),
@ -471,7 +504,8 @@ export const configValidator = z.object({
reactions: z.array(zUrl).default([]),
banners: z.array(zUrl).default([]),
avatars: z.array(zUrl).default([]),
}),
})
.strict(),
bridge: z
.object({
enabled: z.boolean().default(false),
@ -480,6 +514,7 @@ export const configValidator = z.object({
token: z.string().default(""),
url: zUrl.optional(),
})
.strict()
.default({
enabled: false,
software: "versia-ap",
@ -491,6 +526,7 @@ export const configValidator = z.object({
"When bridge is enabled, url must be set",
),
})
.strict()
.default({
blocked: [],
followers_only: [],
@ -524,13 +560,19 @@ export const configValidator = z.object({
keys: z
.object({
public: z.string().min(3).default("").or(z.literal("")),
private: z.string().min(3).default("").or(z.literal("")),
private: z
.string()
.min(3)
.default("")
.or(z.literal("")),
})
.strict()
.default({
public: "",
private: "",
}),
})
.strict()
.default({
name: "Versia",
description: "A Versia instance",
@ -552,20 +594,25 @@ export const configValidator = z.object({
default: z
.array(z.nativeEnum(RolePermissions))
.default(DEFAULT_ROLES),
admin: z.array(z.nativeEnum(RolePermissions)).default(ADMIN_ROLES),
admin: z
.array(z.nativeEnum(RolePermissions))
.default(ADMIN_ROLES),
})
.strict()
.default({
anonymous: DEFAULT_ROLES,
default: DEFAULT_ROLES,
admin: ADMIN_ROLES,
}),
filters: z.object({
filters: z
.object({
note_content: z.array(z.string()).default([]),
emoji: z.array(z.string()).default([]),
username: z.array(z.string()).default([]),
displayname: z.array(z.string()).default([]),
bio: z.array(z.string()).default([]),
}),
})
.strict(),
logging: z
.object({
log_requests: z.boolean().default(false),
@ -582,11 +629,18 @@ export const configValidator = z.object({
dsn: z.string().url().or(z.literal("")).optional(),
debug: z.boolean().default(false),
sample_rate: z.number().min(0).max(1.0).default(1.0),
traces_sample_rate: z.number().min(0).max(1.0).default(1.0),
trace_propagation_targets: z.array(z.string()).default([]),
traces_sample_rate: z
.number()
.min(0)
.max(1.0)
.default(1.0),
trace_propagation_targets: z
.array(z.string())
.default([]),
max_breadcrumbs: z.number().default(100),
environment: z.string().optional(),
})
.strict()
.default({
enabled: false,
debug: false,
@ -602,10 +656,12 @@ export const configValidator = z.object({
.object({
requests: z.string().default("logs/requests.log"),
})
.strict()
.default({
requests: "logs/requests.log",
}),
})
.strict()
.default({
log_requests: false,
log_responses: false,
@ -624,27 +680,55 @@ export const configValidator = z.object({
requests: "logs/requests.log",
},
}),
ratelimits: z.object({
ratelimits: z
.object({
duration_coeff: z.number().default(1),
max_coeff: z.number().default(1),
custom: z
.record(
z.string(),
z.object({
z
.object({
duration: z.number().default(30),
max: z.number().default(60),
}),
})
.strict(),
)
.default({}),
}),
})
.strict(),
debug: z
.object({
federation: z.boolean().default(false),
})
.strict()
.default({
federation: false,
}),
plugins: z.record(z.string(), z.any()).optional(),
});
plugins: z
.object({
autoload: z.boolean().default(true),
overrides: z
.object({
enabled: z.array(z.string()).default([]),
disabled: z.array(z.string()).default([]),
})
.strict()
.default({
enabled: [],
disabled: [],
})
.refine(
// Only one of enabled or disabled can be set
(arg) =>
arg.enabled.length === 0 ||
arg.disabled.length === 0,
"Only one of enabled or disabled can be set",
),
config: z.record(z.string(), z.any()).optional(),
})
.strict(),
})
.strict();
export type Config = z.infer<typeof configValidator>;

View file

@ -14,7 +14,10 @@ const scope = "openid profile email";
const secret = "test-secret";
const privateKey = await crypto.subtle.importKey(
"pkcs8",
Buffer.from(config.plugins?.["@versia/openid"].keys.private, "base64"),
Buffer.from(
config.plugins?.config?.["@versia/openid"].keys.private,
"base64",
),
"Ed25519",
false,
["sign"],