diff --git a/.github/config.workflow.toml b/.github/config.workflow.toml index 635c3efa..cc085119 100644 --- a/.github/config.workflow.toml +++ b/.github/config.workflow.toml @@ -287,6 +287,6 @@ max_coeff = 1.0 [plugins] -[plugins."@versia/openid".keys] +[plugins.config."@versia/openid".keys] private = "MC4CAQAwBQYDK2VwBCIEID+H5n9PY3zVKZQcq4jrnE1IiRd2EWWr8ApuHUXmuOzl" public = "MCowBQYDK2VwAyEAzenliNkgpXYsh3gXTnAoUWzlCPjIOppmAVx2DBlLsC8=" diff --git a/app.ts b/app.ts index 0dd38299..99548c57 100644 --- a/app.ts +++ b/app.ts @@ -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.", + "plugins.config.", )}`; throw new Error("Plugin configuration is invalid"); } diff --git a/classes/plugin/loader.test.ts b/classes/plugin/loader.test.ts index 136f1e54..fd965e7a 100644 --- a/classes/plugin/loader.test.ts +++ b/classes/plugin/loader.test.ts @@ -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, diff --git a/classes/plugin/loader.ts b/classes/plugin/loader.ts index 0456ba4d..201195e8 100644 --- a/classes/plugin/loader.ts +++ b/classes/plugin/loader.ts @@ -162,12 +162,42 @@ export class PluginLoader { */ public async loadPlugins( dir: string, + autoload: boolean, + enabled?: string[], + disabled?: string[], ): Promise<{ manifest: Manifest; plugin: Plugin }[]> { 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)); } } diff --git a/config/config.schema.json b/config/config.schema.json index 63c5ddc5..ac4b4375 100644 --- a/config/config.schema.json +++ b/config/config.schema.json @@ -4007,7 +4007,41 @@ }, "plugins": { "type": "object", - "additionalProperties": {} + "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": [ @@ -4019,7 +4053,8 @@ "http", "smtp", "filters", - "ratelimits" + "ratelimits", + "plugins" ], "additionalProperties": false, "$schema": "http://json-schema.org/draft-07/schema#" diff --git a/packages/config-manager/config.type.ts b/packages/config-manager/config.type.ts index 9b21a838..cc7c2344 100644 --- a/packages/config-manager/config.type.ts +++ b/packages/config-manager/config.type.ts @@ -21,37 +21,9 @@ const zUrl = z .refine((arg) => URL.canParse(arg), "Invalid url") .transform((arg) => arg.replace(/\/$/, "")); -export const configValidator = z.object({ - database: z.object({ - host: z.string().min(1).default("localhost"), - port: z - .number() - .int() - .min(1) - .max(2 ** 16 - 1) - .default(5432), - username: z.string().min(1), - password: z.string().default(""), - database: z.string().min(1).default("versia"), - replicas: z - .array( - z.object({ - host: z.string().min(1), - port: z - .number() - .int() - .min(1) - .max(2 ** 16 - 1) - .default(5432), - username: z.string().min(1), - password: z.string().default(""), - database: z.string().min(1).default("versia"), - }), - ) - .optional(), - }), - redis: z.object({ - queue: z +export const configValidator = z + .object({ + database: z .object({ host: z.string().min(1).default("localhost"), port: z @@ -59,19 +31,77 @@ export const configValidator = z.object({ .int() .min(1) .max(2 ** 16 - 1) - .default(6379), + .default(5432), + username: z.string().min(1), password: z.string().default(""), - database: z.number().int().default(0), - enabled: z.boolean().default(false), + database: z.string().min(1).default("versia"), + replicas: z + .array( + z + .object({ + host: z.string().min(1), + port: z + .number() + .int() + .min(1) + .max(2 ** 16 - 1) + .default(5432), + username: z.string().min(1), + password: z.string().default(""), + database: z.string().min(1).default("versia"), + }) + .strict(), + ) + .optional(), }) - .default({ - host: "localhost", - port: 6379, - password: "", - database: 0, - enabled: false, - }), - cache: z + .strict(), + redis: z + .object({ + queue: z + .object({ + host: z.string().min(1).default("localhost"), + port: z + .number() + .int() + .min(1) + .max(2 ** 16 - 1) + .default(6379), + password: z.string().default(""), + database: z.number().int().default(0), + enabled: z.boolean().default(false), + }) + .strict() + .default({ + host: "localhost", + port: 6379, + password: "", + database: 0, + enabled: false, + }), + cache: z + .object({ + host: z.string().min(1).default("localhost"), + port: z + .number() + .int() + .min(1) + .max(2 ** 16 - 1) + .default(6379), + password: z.string().default(""), + database: z.number().int().default(1), + enabled: z.boolean().default(false), + }) + .strict() + .default({ + host: "localhost", + port: 6379, + password: "", + database: 1, + enabled: false, + }), + }) + .strict(), + sonic: z .object({ host: z.string().min(1).default("localhost"), port: z @@ -79,228 +109,314 @@ export const configValidator = z.object({ .int() .min(1) .max(2 ** 16 - 1) - .default(6379), - password: z.string().default(""), - database: z.number().int().default(1), + .default(7700), + password: z.string(), enabled: z.boolean().default(false), }) - .default({ - host: "localhost", - port: 6379, - password: "", - database: 1, - enabled: false, - }), - }), - sonic: z.object({ - host: z.string().min(1).default("localhost"), - port: z - .number() - .int() - .min(1) - .max(2 ** 16 - 1) - .default(7700), - password: z.string(), - enabled: z.boolean().default(false), - }), - signups: z.object({ - registration: z.boolean().default(true), - rules: z.array(z.string()).default([]), - }), - oidc: z.object({ - forced: z.boolean().default(false), - allow_registration: z.boolean().default(true), - providers: z - .array( - 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(), - }), - ) - .default([]), - keys: z + .strict(), + signups: z .object({ - public: z.string().min(1).optional(), - private: z.string().min(1).optional(), + registration: z.boolean().default(true), + rules: z.array(z.string()).default([]), }) - .optional(), - }), - 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 - .number() - .int() - .min(1) - .max(2 ** 16 - 1) - .default(8080), - // Not using .ip() because we allow CIDR ranges and wildcards and such - banned_ips: z.array(z.string()).default([]), - banned_user_agents: z.array(z.string()).default([]), - proxy: z + .strict(), + oidc: z .object({ - enabled: z.boolean().default(false), - address: zUrl.or(z.literal("")), + forced: z.boolean().default(false), + allow_registration: z.boolean().default(true), + providers: z + .array( + 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 + .object({ + public: z.string().min(1).optional(), + private: z.string().min(1).optional(), + }) + .strict() + .optional(), }) - .default({ - enabled: false, - address: "", + .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 + .number() + .int() + .min(1) + .max(2 ** 16 - 1) + .default(8080), + // Not using .ip() because we allow CIDR ranges and wildcards and such + banned_ips: z.array(z.string()).default([]), + banned_user_agents: z.array(z.string()).default([]), + proxy: z + .object({ + enabled: z.boolean().default(false), + address: zUrl.or(z.literal("")), + }) + .strict() + .default({ + enabled: false, + address: "", + }) + .refine( + (arg) => !arg.enabled || !!arg.address, + "When proxy is enabled, address must be set", + ) + .transform((arg) => ({ + ...arg, + address: arg.enabled ? arg.address : undefined, + })), + tls: z + .object({ + enabled: z.boolean().default(false), + key: z.string(), + cert: z.string(), + passphrase: z.string().optional(), + ca: z.string().optional(), + }) + .strict() + .default({ + enabled: false, + key: "", + cert: "", + passphrase: "", + ca: "", + }), + bait: z + .object({ + enabled: z.boolean().default(false), + send_file: z.string().optional(), + 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: [], + }), }) - .refine( - (arg) => !arg.enabled || !!arg.address, - "When proxy is enabled, address must be set", - ) - .transform((arg) => ({ - ...arg, - address: arg.enabled ? arg.address : undefined, - })), - tls: z + .strict(), + frontend: z .object({ - enabled: z.boolean().default(false), - key: z.string(), - cert: z.string(), - passphrase: z.string().optional(), - ca: z.string().optional(), + enabled: z.boolean().default(true), + url: zUrl.default("http://localhost:3000"), + routes: z + .object({ + home: zUrlPath.default("/"), + login: zUrlPath.default("/oauth/authorize"), + consent: zUrlPath.default("/oauth/consent"), + register: zUrlPath.default("/register"), + password_reset: zUrlPath.default("/oauth/reset"), + }) + .strict() + .default({ + home: "/", + login: "/oauth/authorize", + consent: "/oauth/consent", + register: "/register", + password_reset: "/oauth/reset", + }), + settings: z.record(z.string(), z.any()).default({}), }) + .strict() .default({ - enabled: false, - key: "", - cert: "", - passphrase: "", - ca: "", + enabled: true, + url: "http://localhost:3000", + settings: {}, }), - bait: z + smtp: z .object({ + server: z.string().min(1), + port: z + .number() + .int() + .min(1) + .max(2 ** 16 - 1) + .default(465), + username: z.string().min(1), + password: z.string().min(1).optional(), + tls: z.boolean().default(true), enabled: z.boolean().default(false), - send_file: z.string().optional(), - bait_ips: z.array(z.string()).default([]), - bait_user_agents: z.array(z.string()).default([]), }) + .strict() .default({ + server: "", + port: 465, + username: "", + password: "", + tls: true, enabled: false, - send_file: "", - bait_ips: [], - bait_user_agents: [], }), - }), - frontend: z - .object({ - enabled: z.boolean().default(true), - url: zUrl.default("http://localhost:3000"), - routes: z - .object({ - home: zUrlPath.default("/"), - login: zUrlPath.default("/oauth/authorize"), - consent: zUrlPath.default("/oauth/consent"), - register: zUrlPath.default("/register"), - password_reset: zUrlPath.default("/oauth/reset"), - }) - .default({ - home: "/", - login: "/oauth/authorize", - consent: "/oauth/consent", - register: "/register", - password_reset: "/oauth/reset", - }), - settings: z.record(z.string(), z.any()).default({}), - }) - .default({ - enabled: true, - url: "http://localhost:3000", - settings: {}, - }), - smtp: z - .object({ - server: z.string().min(1), - port: z - .number() - .int() - .min(1) - .max(2 ** 16 - 1) - .default(465), - username: z.string().min(1), - password: z.string().min(1).optional(), - tls: z.boolean().default(true), - enabled: z.boolean().default(false), - }) - .default({ - server: "", - port: 465, - username: "", - password: "", - tls: true, - enabled: false, - }), - media: z - .object({ - backend: z - .nativeEnum(MediaBackendType) - .default(MediaBackendType.Local), - deduplicate_media: z.boolean().default(true), - local_uploads_folder: z.string().min(1).default("uploads"), - conversion: z - .object({ - convert_images: z.boolean().default(false), - convert_to: z.string().default("image/webp"), - convert_vector: z.boolean().default(false), - }) - .default({ + media: z + .object({ + backend: z + .nativeEnum(MediaBackendType) + .default(MediaBackendType.Local), + deduplicate_media: z.boolean().default(true), + local_uploads_folder: z.string().min(1).default("uploads"), + conversion: z + .object({ + convert_images: z.boolean().default(false), + 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, + local_uploads_folder: "uploads", + conversion: { convert_images: false, convert_to: "image/webp", - convert_vector: false, - }), - }) - .default({ - backend: MediaBackendType.Local, - deduplicate_media: true, - local_uploads_folder: "uploads", - conversion: { - convert_images: false, - convert_to: "image/webp", - }, - }), - s3: z - .object({ - endpoint: z.string(), - access_key: z.string(), - secret_access_key: z.string(), - region: z.string().optional(), - bucket_name: z.string().default("versia"), - public_url: zUrl, - }) - .default({ - endpoint: "", - access_key: "", - secret_access_key: "", - region: undefined, - bucket_name: "versia", - public_url: "https://cdn.example.com", - }), - validation: z - .object({ - max_displayname_size: z.number().int().default(50), - max_bio_size: z.number().int().default(5000), - max_note_size: z.number().int().default(5000), - max_avatar_size: z.number().int().default(5000000), - max_header_size: z.number().int().default(5000000), - max_media_size: z.number().int().default(40000000), - max_media_attachments: z.number().int().default(10), - max_media_description_size: z.number().int().default(1000), - max_poll_options: z.number().int().default(20), - max_poll_option_size: z.number().int().default(500), - min_poll_duration: z.number().int().default(60), - max_poll_duration: z.number().int().default(1893456000), - max_username_size: z.number().int().default(30), - max_field_count: z.number().int().default(10), - max_field_name_size: z.number().int().default(1000), - max_field_value_size: z.number().int().default(1000), - username_blacklist: z - .array(z.string()) - .default([ + }, + }), + s3: z + .object({ + endpoint: z.string(), + access_key: z.string(), + secret_access_key: z.string(), + region: z.string().optional(), + bucket_name: z.string().default("versia"), + public_url: zUrl, + }) + .strict() + .default({ + endpoint: "", + access_key: "", + secret_access_key: "", + region: undefined, + bucket_name: "versia", + public_url: "https://cdn.example.com", + }), + validation: z + .object({ + max_displayname_size: z.number().int().default(50), + max_bio_size: z.number().int().default(5000), + max_note_size: z.number().int().default(5000), + max_avatar_size: z.number().int().default(5000000), + max_header_size: z.number().int().default(5000000), + max_media_size: z.number().int().default(40000000), + max_media_attachments: z.number().int().default(10), + max_media_description_size: z.number().int().default(1000), + max_poll_options: z.number().int().default(20), + max_poll_option_size: z.number().int().default(500), + min_poll_duration: z.number().int().default(60), + max_poll_duration: z.number().int().default(1893456000), + max_username_size: z.number().int().default(30), + max_field_count: z.number().int().default(10), + max_field_name_size: z.number().int().default(1000), + max_field_value_size: z.number().int().default(1000), + username_blacklist: z + .array(z.string()) + .default([ + "well-known", + "about", + "activities", + "api", + "auth", + "dev", + "inbox", + "internal", + "main", + "media", + "nodeinfo", + "notice", + "oauth", + "objects", + "proxy", + "push", + "registration", + "relay", + "settings", + "status", + "tag", + "users", + "web", + "search", + "mfa", + ]), + blacklist_tempmail: z.boolean().default(false), + email_blacklist: z.array(z.string()).default([]), + url_scheme_whitelist: z + .array(z.string()) + .default([ + "http", + "https", + "ftp", + "dat", + "dweb", + "gopher", + "hyper", + "ipfs", + "ipns", + "irc", + "xmpp", + "ircs", + "magnet", + "mailto", + "mumble", + "ssb", + "gemini", + ]), + enforce_mime_types: z.boolean().default(false), + allowed_mime_types: z + .array(z.string()) + .default(Object.values(mimeTypes)), + challenges: z + .object({ + enabled: z.boolean().default(true), + difficulty: z.number().int().positive().default(50000), + expiration: z.number().int().positive().default(300), + key: z.string().default(""), + }) + .strict() + .default({ + enabled: true, + difficulty: 50000, + expiration: 300, + key: "", + }), + }) + .strict() + .default({ + max_displayname_size: 50, + max_bio_size: 5000, + max_note_size: 5000, + max_avatar_size: 5000000, + max_header_size: 5000000, + max_media_size: 40000000, + max_media_attachments: 10, + max_media_description_size: 1000, + max_poll_options: 20, + max_poll_option_size: 500, + min_poll_duration: 60, + max_poll_duration: 1893456000, + max_username_size: 30, + max_field_count: 10, + max_field_name_size: 1000, + max_field_value_size: 1000, + username_blacklist: [ "well-known", "about", "activities", @@ -326,12 +442,10 @@ export const configValidator = z.object({ "web", "search", "mfa", - ]), - blacklist_tempmail: z.boolean().default(false), - email_blacklist: z.array(z.string()).default([]), - url_scheme_whitelist: z - .array(z.string()) - .default([ + ], + blacklist_tempmail: false, + email_blacklist: [], + url_scheme_whitelist: [ "http", "https", "ftp", @@ -349,302 +463,272 @@ export const configValidator = z.object({ "mumble", "ssb", "gemini", - ]), - enforce_mime_types: z.boolean().default(false), - allowed_mime_types: z - .array(z.string()) - .default(Object.values(mimeTypes)), - challenges: z - .object({ - enabled: z.boolean().default(true), - difficulty: z.number().int().positive().default(50000), - expiration: z.number().int().positive().default(300), - key: z.string().default(""), - }) - .default({ + ], + enforce_mime_types: false, + allowed_mime_types: Object.values(mimeTypes), + challenges: { enabled: true, difficulty: 50000, expiration: 300, key: "", - }), - }) - .default({ - max_displayname_size: 50, - max_bio_size: 5000, - max_note_size: 5000, - max_avatar_size: 5000000, - max_header_size: 5000000, - max_media_size: 40000000, - max_media_attachments: 10, - max_media_description_size: 1000, - max_poll_options: 20, - max_poll_option_size: 500, - min_poll_duration: 60, - max_poll_duration: 1893456000, - max_username_size: 30, - max_field_count: 10, - max_field_name_size: 1000, - max_field_value_size: 1000, - username_blacklist: [ - "well-known", - "about", - "activities", - "api", - "auth", - "dev", - "inbox", - "internal", - "main", - "media", - "nodeinfo", - "notice", - "oauth", - "objects", - "proxy", - "push", - "registration", - "relay", - "settings", - "status", - "tag", - "users", - "web", - "search", - "mfa", - ], - blacklist_tempmail: false, - email_blacklist: [], - url_scheme_whitelist: [ - "http", - "https", - "ftp", - "dat", - "dweb", - "gopher", - "hyper", - "ipfs", - "ipns", - "irc", - "xmpp", - "ircs", - "magnet", - "mailto", - "mumble", - "ssb", - "gemini", - ], - enforce_mime_types: false, - allowed_mime_types: Object.values(mimeTypes), - challenges: { - enabled: true, - difficulty: 50000, - expiration: 300, - key: "", - }, - }), - defaults: z - .object({ - visibility: z.string().default("public"), - language: z.string().default("en"), - avatar: zUrl.optional(), - header: zUrl.optional(), - placeholder_style: z.string().default("thumbs"), - }) - .default({ - visibility: "public", - language: "en", - avatar: undefined, - header: undefined, - placeholder_style: "thumbs", - }), - federation: z - .object({ - blocked: z.array(zUrl).default([]), - followers_only: z.array(zUrl).default([]), - discard: z.object({ - reports: z.array(zUrl).default([]), - deletes: z.array(zUrl).default([]), - updates: z.array(zUrl).default([]), - media: z.array(zUrl).default([]), - follows: z.array(zUrl).default([]), - likes: z.array(zUrl).default([]), - reactions: z.array(zUrl).default([]), - banners: z.array(zUrl).default([]), - avatars: z.array(zUrl).default([]), + }, }), - bridge: z - .object({ - enabled: z.boolean().default(false), - software: z.enum(["versia-ap"]).or(z.string()), - allowed_ips: z.array(z.string().trim()).default([]), - token: z.string().default(""), - url: zUrl.optional(), - }) - .default({ + defaults: z + .object({ + visibility: z.string().default("public"), + language: z.string().default("en"), + avatar: zUrl.optional(), + header: zUrl.optional(), + placeholder_style: z.string().default("thumbs"), + }) + .strict() + .default({ + visibility: "public", + language: "en", + avatar: undefined, + header: undefined, + placeholder_style: "thumbs", + }), + federation: z + .object({ + blocked: z.array(zUrl).default([]), + followers_only: z.array(zUrl).default([]), + discard: z + .object({ + reports: z.array(zUrl).default([]), + deletes: z.array(zUrl).default([]), + updates: z.array(zUrl).default([]), + media: z.array(zUrl).default([]), + follows: z.array(zUrl).default([]), + likes: z.array(zUrl).default([]), + reactions: z.array(zUrl).default([]), + banners: z.array(zUrl).default([]), + avatars: z.array(zUrl).default([]), + }) + .strict(), + bridge: z + .object({ + enabled: z.boolean().default(false), + software: z.enum(["versia-ap"]).or(z.string()), + allowed_ips: z.array(z.string().trim()).default([]), + token: z.string().default(""), + url: zUrl.optional(), + }) + .strict() + .default({ + enabled: false, + software: "versia-ap", + allowed_ips: [], + token: "", + }) + .refine( + (arg) => (arg.enabled ? arg.url : true), + "When bridge is enabled, url must be set", + ), + }) + .strict() + .default({ + blocked: [], + followers_only: [], + discard: { + reports: [], + deletes: [], + updates: [], + media: [], + follows: [], + likes: [], + reactions: [], + banners: [], + avatars: [], + }, + bridge: { enabled: false, software: "versia-ap", allowed_ips: [], token: "", - }) - .refine( - (arg) => (arg.enabled ? arg.url : true), - "When bridge is enabled, url must be set", - ), - }) - .default({ - blocked: [], - followers_only: [], - discard: { - reports: [], - deletes: [], - updates: [], - media: [], - follows: [], - likes: [], - reactions: [], - banners: [], - avatars: [], - }, - bridge: { - enabled: false, - software: "versia-ap", - allowed_ips: [], - token: "", - }, - }), - instance: z - .object({ - name: z.string().min(1).default("Versia"), - description: z.string().min(1).default("A Versia instance"), - extended_description_path: z.string().optional(), - tos_path: z.string().optional(), - privacy_policy_path: z.string().optional(), - logo: zUrl.optional(), - banner: zUrl.optional(), - keys: z - .object({ - public: z.string().min(3).default("").or(z.literal("")), - private: z.string().min(3).default("").or(z.literal("")), - }) - .default({ + }, + }), + instance: z + .object({ + name: z.string().min(1).default("Versia"), + description: z.string().min(1).default("A Versia instance"), + extended_description_path: z.string().optional(), + tos_path: z.string().optional(), + privacy_policy_path: z.string().optional(), + logo: zUrl.optional(), + banner: zUrl.optional(), + keys: z + .object({ + public: 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", + extended_description_path: undefined, + tos_path: undefined, + privacy_policy_path: undefined, + logo: undefined, + banner: undefined, + keys: { public: "", private: "", - }), - }) - .default({ - name: "Versia", - description: "A Versia instance", - extended_description_path: undefined, - tos_path: undefined, - privacy_policy_path: undefined, - logo: undefined, - banner: undefined, - keys: { - public: "", - private: "", - }, - }), - permissions: z - .object({ - anonymous: z - .array(z.nativeEnum(RolePermissions)) - .default(DEFAULT_ROLES), - default: z - .array(z.nativeEnum(RolePermissions)) - .default(DEFAULT_ROLES), - admin: z.array(z.nativeEnum(RolePermissions)).default(ADMIN_ROLES), - }) - .default({ - anonymous: DEFAULT_ROLES, - default: DEFAULT_ROLES, - admin: ADMIN_ROLES, - }), - 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([]), - }), - logging: z - .object({ - log_requests: z.boolean().default(false), - log_responses: z.boolean().default(false), - log_requests_verbose: z.boolean().default(false), - log_level: z - .enum(["debug", "info", "warning", "error", "fatal"]) - .default("info"), - log_ip: z.boolean().default(false), - log_filters: z.boolean().default(true), - sentry: z - .object({ - enabled: z.boolean().default(false), - 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([]), - max_breadcrumbs: z.number().default(100), - environment: z.string().optional(), - }) - .default({ + }, + }), + permissions: z + .object({ + anonymous: z + .array(z.nativeEnum(RolePermissions)) + .default(DEFAULT_ROLES), + default: z + .array(z.nativeEnum(RolePermissions)) + .default(DEFAULT_ROLES), + admin: z + .array(z.nativeEnum(RolePermissions)) + .default(ADMIN_ROLES), + }) + .strict() + .default({ + anonymous: DEFAULT_ROLES, + default: DEFAULT_ROLES, + admin: ADMIN_ROLES, + }), + 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), + log_responses: z.boolean().default(false), + log_requests_verbose: z.boolean().default(false), + log_level: z + .enum(["debug", "info", "warning", "error", "fatal"]) + .default("info"), + log_ip: z.boolean().default(false), + log_filters: z.boolean().default(true), + sentry: z + .object({ + enabled: z.boolean().default(false), + 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([]), + max_breadcrumbs: z.number().default(100), + environment: z.string().optional(), + }) + .strict() + .default({ + enabled: false, + debug: false, + sample_rate: 1.0, + traces_sample_rate: 1.0, + max_breadcrumbs: 100, + }) + .refine( + (arg) => (arg.enabled ? !!arg.dsn : true), + "When sentry is enabled, DSN must be set", + ), + storage: z + .object({ + requests: z.string().default("logs/requests.log"), + }) + .strict() + .default({ + requests: "logs/requests.log", + }), + }) + .strict() + .default({ + log_requests: false, + log_responses: false, + log_requests_verbose: false, + log_level: "info", + log_ip: false, + log_filters: true, + sentry: { enabled: false, debug: false, sample_rate: 1.0, traces_sample_rate: 1.0, max_breadcrumbs: 100, - }) - .refine( - (arg) => (arg.enabled ? !!arg.dsn : true), - "When sentry is enabled, DSN must be set", - ), - storage: z - .object({ - requests: z.string().default("logs/requests.log"), - }) - .default({ + }, + storage: { requests: "logs/requests.log", - }), - }) - .default({ - log_requests: false, - log_responses: false, - log_requests_verbose: false, - log_level: "info", - log_ip: false, - log_filters: true, - sentry: { - enabled: false, - debug: false, - sample_rate: 1.0, - traces_sample_rate: 1.0, - max_breadcrumbs: 100, - }, - storage: { - requests: "logs/requests.log", - }, - }), - ratelimits: z.object({ - duration_coeff: z.number().default(1), - max_coeff: z.number().default(1), - custom: z - .record( - z.string(), - z.object({ - duration: z.number().default(30), - max: z.number().default(60), - }), - ) - .default({}), - }), - debug: z - .object({ - federation: z.boolean().default(false), - }) - .default({ - federation: false, - }), - plugins: z.record(z.string(), z.any()).optional(), -}); + }, + }), + ratelimits: z + .object({ + duration_coeff: z.number().default(1), + max_coeff: z.number().default(1), + custom: z + .record( + z.string(), + 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 + .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; diff --git a/plugins/openid/routes/authorize.test.ts b/plugins/openid/routes/authorize.test.ts index 1df837a1..fff32dbb 100644 --- a/plugins/openid/routes/authorize.test.ts +++ b/plugins/openid/routes/authorize.test.ts @@ -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"],