diff --git a/.github/config.workflow.toml b/.github/config.workflow.toml index 7d048662..882e2e8e 100644 --- a/.github/config.workflow.toml +++ b/.github/config.workflow.toml @@ -1,99 +1,168 @@ -[database] +# You can change the URL to the commit/tag you are using +#:schema https://raw.githubusercontent.com/versia-pub/server/main/config/config.schema.json + +# All values marked as "sensitive" can be set to "PATH:/path/to/file" to read the value from a file (e.g. a secret manager) + + +[postgres] +# PostgreSQL database configuration host = "localhost" port = 5432 username = "versia" +# Sensitive value password = "versia" database = "versia" +# Additional read-only replicas +# [[postgres.replicas]] +# host = "other-host" +# port = 5432 +# username = "versia" +# password = "mycoolpassword2" +# database = "replica1" + [redis.queue] +# A Redis database used for managing queues. +# Required for federation host = "localhost" port = 6379 -password = "" +# Sensitive value +# password = "test" database = 0 -[redis.cache] -host = "localhost" -port = 6379 -password = "" -database = 1 +# A Redis database used for caching SQL queries. +# Optional, can be the same as the queue instance +# [redis.cache] +# host = "localhost" +# port = 6380 +# database = 1 +# password = "" + +# Search and indexing configuration +[search] +# Enable indexing and searching? enabled = false -[sonic] +# Optional if search is disabled +[search.sonic] host = "localhost" port = 40007 +# Sensitive value password = "" -enabled = false -[signups] -# Whether to enable registrations or not -registration = true -rules = [ - "Do not harass others", - "Be nice to people", - "Don't spam", - "Don't post illegal content", -] +[registration] +# Can users sign up freely? +allow = true +# NOT IMPLEMENTED +require_approval = false +# Message to show to users when registration is disabled +# message = "ran out of spoons to moderate registrations, sorry" [http] +# URL that the instance will be accessible at base_url = "http://0.0.0.0:8080" +# Address to bind to (0.0.0.0 is suggested for proxies) bind = "0.0.0.0" bind_port = 8080 # Bans IPv4 or IPv6 IPs (wildcards, networks and ranges are supported) banned_ips = [] +# Banned user agents, regex format +banned_user_agents = [ + # "curl\/7.68.0", + # "wget\/1.20.3", +] + +# URL to an eventual HTTP proxy +# Will be used for all outgoing requests +# proxy_address = "http://localhost:8118" + +# TLS configuration. You should probably be using a reverse proxy instead of this +# [http.tls] +# key = "/path/to/key.pem" +# cert = "/path/to/cert.pem" +# Sensitive value +# passphrase = "awawa" +# ca = "/path/to/ca.pem" + +[frontend] +# Enable custom frontends (warning: not enabling this will make Versia Server only accessible via the Mastodon API) +# Frontends also control the OpenID flow, so if you disable this, you will need to use the Mastodon frontend +enabled = true +# The URL to reach the frontend at (should be on a local network) +url = "http://localhost:3000" + +[frontend.routes] +# Special routes for your frontend, below are the defaults for Versia-FE +# Can be set to a route already used by Versia Server, as long as it is on a different HTTP method +# e.g. /oauth/authorize is a POST-only route, so you can serve a GET route at /oauth/authorize +# home = "/" +# login = "/oauth/authorize" +# consent = "/oauth/consent" +# register = "/register" +# password_reset = "/oauth/reset" -[smtp] +[frontend.settings] +# Arbitrary key/value pairs to be passed to the frontend +# This can be used to set up custom themes, etc on supported frontends. +# theme = "dark" + +# NOT IMPLEMENTED +[email] +# Enable email sending +send_emails = false + +# If send_emails is true, the following settings are required +# [email.smtp] # SMTP server to use for sending emails -server = "smtp.example.com" -port = 465 -username = "test@example.com" -password = "password123" -tls = true +# server = "smtp.example.com" +# port = 465 +# username = "test@example.com" +# Sensitive value +# password = "password123" +# tls = true [media] # Can be "s3" or "local", where "local" uploads the file to the local filesystem -# If you need to change this value after setting up your instance, you must move all the files -# from one backend to the other manually +# Changing this value will not retroactively apply to existing data +# Don't forget to fill in the s3 config :3 backend = "local" -# Whether to check the hash of media when uploading to avoid duplication -deduplicate_media = true # If media backend is "local", this is the folder where the files will be stored -local_uploads_folder = "uploads" +# Can be any path +uploads_path = "uploads" [media.conversion] +# Whether to automatically convert images to another format on upload convert_images = false -# Can be: "jxl", "webp", "avif", "png", "jpg", "heif" +# Can be: "image/jxl", "image/webp", "image/avif", "image/png", "image/jpeg", "image/heif", "image/gif" # JXL support will likely not work -convert_to = "webp" +convert_to = "image/webp" +# Also convert SVG images? +convert_vectors = false # [s3] -# Can be left blank if you don't use the S3 media backend -# endpoint = "https://s3-us-west-2.amazonaws.com" -# access_key = "" -# secret_access_key = "" -# region = "us-west-2" +# Can be left commented if you don't use the S3 media backend +# endpoint = "https://s3.example.com" +# Sensitive value +# access_key = "XXXXX" +# Sensitive value +# secret_access_key = "XXX" +# region = "us-east-1" # bucket_name = "versia" # public_url = "https://cdn.example.com" [validation] -# Self explanatory -max_displayname_size = 50 -max_bio_size = 160 -max_note_size = 5000 -max_avatar_size = 5_000_000 -max_header_size = 5_000_000 -max_media_size = 40_000_000 -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 -# An array of strings, defaults are from Akkoma -username_blacklist = [ - ".well-known", - "~", +# Checks user data +# Does not retroactively apply to previously entered data +[validation.accounts] +max_displayname_characters = 50 +max_username_characters = 30 +max_bio_characters = 5000 +max_avatar_bytes = 5_000_000 +max_header_bytes = 5_000_000 +# Regex is allowed here +disallowed_usernames = [ + "well-known", "about", "activities", "api", @@ -119,12 +188,14 @@ username_blacklist = [ "search", "mfa", ] -# Whether to blacklist known temporary email providers -blacklist_tempmail = false -# Additional email providers to blacklist -email_blacklist = [] -# Valid URL schemes, otherwise the URL is parsed as text -url_scheme_whitelist = [ +max_field_count = 10 +max_field_name_characters = 1000 +max_field_value_characters = 1000 +max_pinned_notes = 20 + +[validation.notes] +max_characters = 5000 +allowed_url_schemes = [ "http", "https", "ftp", @@ -142,76 +213,122 @@ url_scheme_whitelist = [ "mumble", "ssb", "gemini", -] # NOT IMPLEMENTED - -enforce_mime_types = false -allowed_mime_types = [ - "image/jpeg", - "image/png", - "image/gif", - "image/heic", - "image/heif", - "image/webp", - "image/avif", - "video/webm", - "video/mp4", - "video/quicktime", - "video/ogg", - "audio/wave", - "audio/wav", - "audio/x-wav", - "audio/x-pn-wave", - "audio/vnd.wave", - "audio/ogg", - "audio/vorbis", - "audio/mpeg", - "audio/mp3", - "audio/webm", - "audio/flac", - "audio/aac", - "audio/m4a", - "audio/x-m4a", - "audio/mp4", - "audio/3gpp", - "video/x-ms-asf", ] +max_attachments = 16 + +[validation.media] +max_bytes = 40_000_000 +max_description_characters = 1000 +# An empty array allows all MIME types +allowed_mime_types = [] + +[validation.emojis] +max_bytes = 1_000_000 +max_shortcode_characters = 100 +max_description_characters = 1000 + +[validation.polls] +max_options = 20 +max_option_characters = 500 +min_duration_seconds = 60 +# 100 days +max_duration_seconds = 8_640_000 + +[validation.emails] +# Blocks over 10,000 common tempmail domains +disallow_tempmail = false +# Regex is allowed here +disallowed_domains = [] [validation.challenges] # "Challenges" (aka captchas) are a way to verify that a user is human -# Versia Server's challenges use no external services, and are Proof of Work based +# Versia Server's challenges use no external services, and are proof-of-work based # This means that they do not require any user interaction, instead # they require the user's computer to do a small amount of work -enabled = true -# The difficulty of the challenge, higher is harder +# The difficulty of the challenge, higher is will take more time to solve difficulty = 50000 # Challenge expiration time in seconds expiration = 300 # 5 minutes # Leave this empty to generate a new key +# Sensitive value key = "YBpAV0KZOeM/MZ4kOb2E9moH9gCUr00Co9V7ncGRJ3wbd/a9tLDKKFdI0BtOcnlpfx0ZBh0+w3WSvsl0TsesTg==" +# Block content that matches these regular expressions +[validation.filters] +note_content = [ + # "(https?://)?(www\\.)?youtube\\.com/watch\\?v=[a-zA-Z0-9_-]+", + # "(https?://)?(www\\.)?youtu\\.be/[a-zA-Z0-9_-]+", +] +emoji_shortcode = [] +username = [] +displayname = [] +bio = [] + [notifications] +# Web Push Notifications configuration. +# Leave out to disable. [notifications.push] -# Whether to enable push notifications -enabled = true - -[notifications.push.vapid] +# Subject field embedded in the push notification +# subject = "mailto:joe@example.com" +# +[notifications.push.vapid_keys] # VAPID keys for push notifications # Run Versia Server with those values missing to generate new keys +# Sensitive value public = "BBanhyj2_xWwbTsWld3T49VcAoKZHrVJTzF1f6Av2JwQY_wUi3CF9vZ0WeEcACRj6EEqQ7N35CkUh5epF7n4P_s" +# Sensitive value private = "Eujaz7NsF0rKZOVrAFL7mMpFdl96f591ERsRn81unq0" -# Optional -# subject = "mailto:joe@example.com" [defaults] # Default visibility for new notes +# Can be public, unlisted, private or direct +# Private only sends to followers, unlisted doesn't show up in timelines visibility = "public" -# Default language for new notes +# Default language for new notes (ISO code) language = "en" -# Default avatar, must be a valid URL or "" +# Default avatar, must be a valid URL or left out for a placeholder avatar # avatar = "" -# Default header, must be a valid URL or "" +# Default header, must be a valid URL or left out for none # header = "" +# A style name from https://www.dicebear.com/styles +placeholder_style = "thumbs" + +[queues] +# Controls the delivery queue (for outbound federation) +[queues.delivery] +# Time in seconds to remove completed jobs +remove_after_complete_seconds = 31536000 +# Time in seconds to remove failed jobs +remove_after_failure_seconds = 31536000 + +# Controls the inbox processing queue (for inbound federation) +[queues.inbox] +# Time in seconds to remove completed jobs +remove_after_complete_seconds = 31536000 +# Time in seconds to remove failed jobs +remove_after_failure_seconds = 31536000 + +# Controls the fetch queue (for remote data refreshes) +[queues.fetch] +# Time in seconds to remove completed jobs +remove_after_complete_seconds = 31536000 +# Time in seconds to remove failed jobs +remove_after_failure_seconds = 31536000 + +# Controls the push queue (for push notification delivery) +[queues.push] +# Time in seconds to remove completed jobs +remove_after_complete_seconds = 31536000 +# Time in seconds to remove failed jobs +remove_after_failure_seconds = 31536000 + +# Controls the media queue (for media processing) +[queues.media] +# Time in seconds to remove completed jobs +remove_after_complete_seconds = 31536000 +# Time in seconds to remove failed jobs +remove_after_failure_seconds = 31536000 [federation] # This is a list of domain names, such as "mastodon.social" or "pleroma.site" @@ -236,57 +353,140 @@ reactions = [] banners = [] avatars = [] +# For bridge software, such as versia-pub/activitypub +# Bridges must be hosted separately from the main Versia Server process +# [federation.bridge] +# Only versia-ap exists for now +# software = "versia-ap" +# If this is empty, any bridge with the correct token +# will be able to send data to your instance +# v4, v6, ranges and wildcards are supported +# allowed_ips = ["192.168.1.0/24"] +# Token for the bridge software +# Bridge must have the same token! +# Sensitive value +# token = "mycooltoken" +# url = "https://ap.versia.social" + [instance] name = "Versia" -description = "A test instance of Versia Server" -# URL to your instance logo (jpg files should be renamed to jpeg) -# logo = "" -# URL to your instance banner (jpg files should be renamed to jpeg) -# banner = "" +description = "A Versia Server instance" +# Paths to instance long description, terms of service, and privacy policy +# These will be parsed as Markdown +# +# extended_description_path = "config/extended_description.md" +# tos_path = "config/tos.md" +# privacy_policy_path = "config/privacy_policy.md" -[filters] -# Regex filters for federated and local data -# Drops data matching the filters -# Does not apply retroactively to existing data +# Primary instance languages. ISO 639-1 codes. +languages = ["en"] -# Note contents -note_content = [ - # "(https?://)?(www\\.)?youtube\\.com/watch\\?v=[a-zA-Z0-9_-]+", - # "(https?://)?(www\\.)?youtu\\.be/[a-zA-Z0-9_-]+", -] -emoji = [] -# These will drop users matching the filters -username = [] -displayname = [] -bio = [] +[instance.contact] +email = "staff@yourinstance.com" + +[instance.branding] +# logo = "https://cdn.example.com/logo.png" +# banner = "https://cdn.example.com/banner.png" + +# Used for federation. If left empty or missing, the server will generate one for you. +[instance.keys] +# Sensitive value +public = "MCowBQYDK2VwAyEASN0V5OWRbhRCnuhxfRLqpUOfszHozvrLLVhlIYLNTZM=" +# Sensitive value +private = "MC4CAQAwBQYDK2VwBCIEIKaxDGMaW71OcCGMY+GKTZPtLPNlTvMFe3G5qXVHPhQM" + +[[instance.rules]] +# Short description of the rule +text = "No hate speech" +# Longer version of the rule with additional information +hint = "Hate speech includes slurs, threats, and harassment." + +[[instance.rules]] +text = "No spam" + +# [[instance.rules]] +# ...etc + +[permissions] +# Control default permissions for users +# Note that an anonymous user having a permission will not allow them +# to do things that require authentication (e.g. 'owner:notes' -> posting a note will need +# auth, but viewing a note will not) +# See https://server.versia.pub/api/roles#list-of-permissions for a list of all permissions + +# Defaults to being able to login and manage their own content +# anonymous = [] + +# Defaults to identical to anonymous +# default = [] + +# Defaults to being able to manage all instance data, content, and users +# admin = [] [logging] -# Log all requests (warning: this is a lot of data) -log_requests = true -# Log request and their contents (warning: this is a lot of data) -log_requests_verbose = false -# For GDPR compliance, you can disable logging of IPs -log_ip = false - -# Log all filtered objects -log_filters = true - -[ratelimits] -# These settings apply to every route at once -# Amount to multiply every route's duration by -duration_coeff = 1.0 -# Amount to multiply every route's max requests per [duration] by -max_coeff = 1.0 - -[ratelimits.custom] -# Add in any API route in this style here -# Applies before the global ratelimit changes -# "/api/v1/accounts/:id/block" = { duration = 30, max = 60 } -# "/api/v1/timelines/public" = { duration = 60, max = 200 } + +# Available levels: debug, info, warning, error, fatal +log_level = "debug" + +log_file_path = "logs/versia.log" + +[logging.types] +# Either pass a boolean +# requests = true +# Or a table with the following keys: +# requests_content = { level = "debug", log_file_path = "logs/requests.log" } +# Available types are: requests, responses, requests_content, filters + +# https://sentry.io support +# Uncomment to enable +# [logging.sentry] +# Sentry DSN for error logging +# dsn = "https://example.com" +# debug = false + +# sample_rate = 1.0 +# traces_sample_rate = 1.0 +# Can also be regex +# trace_propagation_targets = [] +# max_breadcrumbs = 100 +# environment = "production" [plugins] +# Whether to automatically load all plugins in the plugins directory +autoload = true + +# Override for autoload +[plugins.overrides] +enabled = [] +disabled = [] + +[plugins.config."@versia/openid"] +# If enabled, Versia will require users to log in with an OpenID provider +forced = false + +# Allow registration with OpenID providers +# If signups.registration is false, it will only be possible to register with OpenID +allow_registration = true [plugins.config."@versia/openid".keys] -private = "MC4CAQAwBQYDK2VwBCIEID+H5n9PY3zVKZQcq4jrnE1IiRd2EWWr8ApuHUXmuOzl" -public = "MCowBQYDK2VwAyEAzenliNkgpXYsh3gXTnAoUWzlCPjIOppmAVx2DBlLsC8=" +# Run Versia Server with those values missing to generate a new key +public = "MCowBQYDK2VwAyEAfyZx8r98gVHtdH5EF1NYrBeChOXkt50mqiwKO2TX0f8=" +private = "MC4CAQAwBQYDK2VwBCIEILDi1g7+bwNjBBvL4CRWHZpCFBR2m2OPCot62Wr+TCbq" + +# The provider MUST support OpenID Connect with .well-known discovery +# Most notably, GitHub does not support this +# Redirect URLs in your OpenID provider can be set to this: +# /oauth/sso//callback* +# The asterisk is important, as it allows for any query parameters to be passed +# Authentik for example uses regex so it can be set to (regex): +# /oauth/sso//callback.* +# [[plugins.config."@versia/openid".providers]] +# name = "CPlusPatch ID" +# id = "cpluspatch-id" +# This MUST match the provider's issuer URI, including the trailing slash (or lack thereof) +# url = "https://id.cpluspatch.com/application/o/versia-testing/" +# client_id = "XXXX" +# Sensitive value +# client_secret = "XXXXX" +# icon = "https://cpluspatch.com/images/icons/logo.svg" diff --git a/api/api/auth/login/index.test.ts b/api/api/auth/login/index.test.ts index 29bf7885..b29028fe 100644 --- a/api/api/auth/login/index.test.ts +++ b/api/api/auth/login/index.test.ts @@ -1,7 +1,7 @@ import { afterAll, describe, expect, test } from "bun:test"; import { randomString } from "@/math"; import { Application } from "@versia/kit/db"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; import { fakeRequest, getTestUsers } from "~/tests/utils"; const { users, deleteUsers, passwords } = await getTestUsers(1); diff --git a/api/api/auth/login/index.ts b/api/api/auth/login/index.ts index c3c46202..a9352ecf 100644 --- a/api/api/auth/login/index.ts +++ b/api/api/auth/login/index.ts @@ -7,7 +7,7 @@ import type { Context } from "hono"; import { setCookie } from "hono/cookie"; import { SignJWT } from "jose"; import { ApiError } from "~/classes/errors/api-error"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; const schemas = { form: z.object({ diff --git a/api/api/auth/redirect/index.ts b/api/api/auth/redirect/index.ts index 07310ff4..fb6df120 100644 --- a/api/api/auth/redirect/index.ts +++ b/api/api/auth/redirect/index.ts @@ -3,7 +3,7 @@ import { createRoute, z } from "@hono/zod-openapi"; import { db } from "@versia/kit/db"; import { Applications, Tokens } from "@versia/kit/tables"; import { and, eq } from "drizzle-orm"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; const schemas = { query: z.object({ diff --git a/api/api/auth/reset/index.test.ts b/api/api/auth/reset/index.test.ts index 3488f267..cd3e829e 100644 --- a/api/api/auth/reset/index.test.ts +++ b/api/api/auth/reset/index.test.ts @@ -1,7 +1,7 @@ import { afterAll, describe, expect, test } from "bun:test"; import { randomString } from "@/math"; import { Application } from "@versia/kit/db"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; import { fakeRequest, getTestUsers } from "~/tests/utils"; const { users, deleteUsers, passwords } = await getTestUsers(1); diff --git a/api/api/auth/reset/index.ts b/api/api/auth/reset/index.ts index dbc9a487..96cb5a43 100644 --- a/api/api/auth/reset/index.ts +++ b/api/api/auth/reset/index.ts @@ -4,7 +4,7 @@ import { User } from "@versia/kit/db"; import { Users } from "@versia/kit/tables"; import { eq } from "drizzle-orm"; import type { Context } from "hono"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; const schemas = { form: z.object({ diff --git a/api/api/v1/accounts/:id/statuses.ts b/api/api/v1/accounts/:id/statuses.ts index d64726fe..8f7ff7e8 100644 --- a/api/api/v1/accounts/:id/statuses.ts +++ b/api/api/v1/accounts/:id/statuses.ts @@ -10,8 +10,8 @@ import { Timeline } from "@versia/kit/db"; import { Notes, RolePermissions } from "@versia/kit/tables"; import { and, eq, gt, gte, inArray, isNull, lt, or, sql } from "drizzle-orm"; import { Account as AccountSchema } from "~/classes/schemas/account"; +import { zBoolean } from "~/classes/schemas/common.ts"; import { Status as StatusSchema } from "~/classes/schemas/status"; -import { zBoolean } from "~/packages/config-manager/config.type"; const route = createRoute({ method: "get", diff --git a/api/api/v1/accounts/index.ts b/api/api/v1/accounts/index.ts index 86585dc5..b504e091 100644 --- a/api/api/v1/accounts/index.ts +++ b/api/api/v1/accounts/index.ts @@ -6,8 +6,8 @@ import { Users } from "@versia/kit/tables"; import { and, eq, isNull } from "drizzle-orm"; import ISO6391 from "iso-639-1"; import { ApiError } from "~/classes/errors/api-error"; -import { config } from "~/packages/config-manager"; -import { zBoolean } from "~/packages/config-manager/config.type"; +import { zBoolean } from "~/classes/schemas/common"; +import { config } from "~/config.ts"; const schema = z.object({ username: z.string().openapi({ @@ -157,7 +157,7 @@ export default apiRoute((app) => const { username, email, password, agreement, locale } = context.req.valid("json"); - if (!config.signups.registration) { + if (!config.registration.allow) { throw new ApiError(422, "Registration is disabled"); } @@ -217,7 +217,11 @@ export default apiRoute((app) => } // Check if username doesnt match filters - if (config.filters.username.some((filter) => username?.match(filter))) { + if ( + config.validation.filters.username.some((filter) => + filter.test(username), + ) + ) { errors.details.username.push({ error: "ERR_INVALID", description: "contains blocked words", @@ -225,10 +229,13 @@ export default apiRoute((app) => } // Check if username is too long - if ((username?.length ?? 0) > config.validation.max_username_size) { + if ( + (username?.length ?? 0) > + config.validation.accounts.max_username_characters + ) { errors.details.username.push({ error: "ERR_TOO_LONG", - description: `is too long (maximum is ${config.validation.max_username_size} characters)`, + description: `is too long (maximum is ${config.validation.accounts.max_username_characters} characters)`, }); } @@ -241,7 +248,11 @@ export default apiRoute((app) => } // Check if username is reserved - if (config.validation.username_blacklist.includes(username ?? "")) { + if ( + config.validation.accounts.disallowed_usernames.some((filter) => + filter.test(username), + ) + ) { errors.details.username.push({ error: "ERR_RESERVED", description: "is reserved", @@ -274,9 +285,11 @@ export default apiRoute((app) => // Check if email is blocked if ( - config.validation.email_blacklist.includes(email) || - (config.validation.blacklist_tempmail && - tempmailDomains.domains.includes((email ?? "").split("@")[1])) + config.validation.emails.disallowed_domains.some((f) => + f.test(email.split("@")[1]), + ) || + (config.validation.emails.disallow_tempmail && + tempmailDomains.domains.includes(email.split("@")[1])) ) { errors.details.email.push({ error: "ERR_BLOCKED", diff --git a/api/api/v1/accounts/lookup/index.ts b/api/api/v1/accounts/lookup/index.ts index 70be012a..8ce23d1f 100644 --- a/api/api/v1/accounts/lookup/index.ts +++ b/api/api/v1/accounts/lookup/index.ts @@ -12,7 +12,7 @@ import { and, eq, isNull } from "drizzle-orm"; import { ApiError } from "~/classes/errors/api-error"; import { Account } from "~/classes/schemas/account"; import { Account as AccountSchema } from "~/classes/schemas/account"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; const route = createRoute({ method: "get", diff --git a/api/api/v1/accounts/relationships/index.ts b/api/api/v1/accounts/relationships/index.ts index b5e93e13..c5e20de8 100644 --- a/api/api/v1/accounts/relationships/index.ts +++ b/api/api/v1/accounts/relationships/index.ts @@ -3,8 +3,8 @@ import { createRoute, z } from "@hono/zod-openapi"; import { Relationship } from "@versia/kit/db"; import { RolePermissions } from "@versia/kit/tables"; import { Account as AccountSchema } from "~/classes/schemas/account"; +import { zBoolean } from "~/classes/schemas/common.ts"; import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship"; -import { zBoolean } from "~/packages/config-manager/config.type"; const route = createRoute({ method: "get", diff --git a/api/api/v1/accounts/search/index.ts b/api/api/v1/accounts/search/index.ts index 4d47064f..6aa21726 100644 --- a/api/api/v1/accounts/search/index.ts +++ b/api/api/v1/accounts/search/index.ts @@ -6,7 +6,7 @@ import { eq, ilike, not, or, sql } from "drizzle-orm"; import stringComparison from "string-comparison"; import { ApiError } from "~/classes/errors/api-error"; import { Account as AccountSchema } from "~/classes/schemas/account"; -import { zBoolean } from "~/packages/config-manager/config.type"; +import { zBoolean } from "~/classes/schemas/common.ts"; export const route = createRoute({ method: "get", diff --git a/api/api/v1/accounts/update_credentials/index.test.ts b/api/api/v1/accounts/update_credentials/index.test.ts index 079565bb..f1c0ebb4 100644 --- a/api/api/v1/accounts/update_credentials/index.test.ts +++ b/api/api/v1/accounts/update_credentials/index.test.ts @@ -1,6 +1,6 @@ import { afterAll, describe, expect, test } from "bun:test"; import type { Account as APIAccount } from "@versia/client/types"; -import { config } from "~/packages/config-manager/index.ts"; +import { config } from "~/config.ts"; import { fakeRequest, getTestUsers } from "~/tests/utils"; const { tokens, deleteUsers } = await getTestUsers(1); diff --git a/api/api/v1/accounts/update_credentials/index.ts b/api/api/v1/accounts/update_credentials/index.ts index 65eae4a5..4075f680 100644 --- a/api/api/v1/accounts/update_credentials/index.ts +++ b/api/api/v1/accounts/update_credentials/index.ts @@ -8,8 +8,8 @@ import { and, eq, isNull } from "drizzle-orm"; import { ApiError } from "~/classes/errors/api-error"; import { contentToHtml } from "~/classes/functions/status"; import { Account as AccountSchema } from "~/classes/schemas/account"; -import { zBoolean } from "~/packages/config-manager/config.type"; -import { config } from "~/packages/config-manager/index.ts"; +import { zBoolean } from "~/classes/schemas/common.ts"; +import { config } from "~/config.ts"; const route = createRoute({ method: "patch", @@ -62,9 +62,9 @@ const route = createRoute({ .refine( (v) => v.size <= - config.validation - .max_avatar_size, - `Avatar must be less than ${config.validation.max_avatar_size} bytes`, + config.validation.accounts + .max_avatar_bytes, + `Avatar must be less than ${config.validation.accounts.max_avatar_bytes} bytes`, ) .openapi({ description: @@ -84,9 +84,9 @@ const route = createRoute({ .refine( (v) => v.size <= - config.validation - .max_header_size, - `Header must be less than ${config.validation.max_header_size} bytes`, + config.validation.accounts + .max_header_bytes, + `Header must be less than ${config.validation.accounts.max_header_bytes} bytes`, ) .openapi({ description: @@ -144,7 +144,9 @@ const route = createRoute({ .element.shape.value, }), ) - .max(config.validation.max_field_count), + .max( + config.validation.accounts.max_field_count, + ), }) .partial(), }, diff --git a/api/api/v1/challenges/index.ts b/api/api/v1/challenges/index.ts index b25ae8d7..7a6ea9a3 100644 --- a/api/api/v1/challenges/index.ts +++ b/api/api/v1/challenges/index.ts @@ -2,7 +2,7 @@ import { apiRoute, auth } from "@/api"; import { generateChallenge } from "@/challenges"; import { createRoute, z } from "@hono/zod-openapi"; import { ApiError } from "~/classes/errors/api-error"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; import { ErrorSchema } from "~/types/api"; const route = createRoute({ @@ -45,7 +45,7 @@ const route = createRoute({ export default apiRoute((app) => app.openapi(route, async (context) => { - if (!config.validation.challenges.enabled) { + if (!config.validation.challenges) { throw new ApiError(400, "Challenges are disabled in config"); } diff --git a/api/api/v1/emojis/:id/index.ts b/api/api/v1/emojis/:id/index.ts index 61618685..7445b228 100644 --- a/api/api/v1/emojis/:id/index.ts +++ b/api/api/v1/emojis/:id/index.ts @@ -10,7 +10,7 @@ import { createRoute, z } from "@hono/zod-openapi"; import { RolePermissions } from "@versia/kit/tables"; import { ApiError } from "~/classes/errors/api-error"; import { CustomEmoji as CustomEmojiSchema } from "~/classes/schemas/emoji"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; import { ErrorSchema } from "~/types/api"; const schema = z @@ -31,8 +31,8 @@ const schema = z "Emoji image encoded using multipart/form-data", }) .refine( - (v) => v.size <= config.validation.max_emoji_size, - `Emoji must be less than ${config.validation.max_emoji_size} bytes`, + (v) => v.size <= config.validation.emojis.max_bytes, + `Emoji must be less than ${config.validation.emojis.max_bytes} bytes`, ), ), category: CustomEmojiSchema.shape.category.optional(), diff --git a/api/api/v1/emojis/index.ts b/api/api/v1/emojis/index.ts index 1acfd351..363317bb 100644 --- a/api/api/v1/emojis/index.ts +++ b/api/api/v1/emojis/index.ts @@ -6,7 +6,7 @@ import { Emojis, RolePermissions } from "@versia/kit/tables"; import { and, eq, isNull, or } from "drizzle-orm"; import { ApiError } from "~/classes/errors/api-error"; import { CustomEmoji as CustomEmojiSchema } from "~/classes/schemas/emoji"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; const schema = z.object({ shortcode: CustomEmojiSchema.shape.shortcode, @@ -25,8 +25,8 @@ const schema = z.object({ "Emoji image encoded using multipart/form-data", }) .refine( - (v) => v.size <= config.validation.max_emoji_size, - `Emoji must be less than ${config.validation.max_emoji_size} bytes`, + (v) => v.size <= config.validation.emojis.max_bytes, + `Emoji must be less than ${config.validation.emojis.max_bytes} bytes`, ), ), category: CustomEmojiSchema.shape.category.optional(), diff --git a/api/api/v1/frontend/config/index.ts b/api/api/v1/frontend/config/index.ts index 2b58c31e..34a33fa9 100644 --- a/api/api/v1/frontend/config/index.ts +++ b/api/api/v1/frontend/config/index.ts @@ -1,6 +1,6 @@ import { apiRoute } from "@/api"; import { createRoute, z } from "@hono/zod-openapi"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; const route = createRoute({ method: "get", diff --git a/api/api/v1/instance/extended_description.test.ts b/api/api/v1/instance/extended_description.test.ts index ccaad17e..67db3eb2 100644 --- a/api/api/v1/instance/extended_description.test.ts +++ b/api/api/v1/instance/extended_description.test.ts @@ -12,7 +12,7 @@ describe("/api/v1/instance/extended_description", () => { const json = await response.json(); expect(json).toEqual({ - updated_at: new Date(1970, 0, 0).toISOString(), + updated_at: new Date(0).toISOString(), content: '

This is a Versia server with the default extended description.

\n', }); diff --git a/api/api/v1/instance/extended_description.ts b/api/api/v1/instance/extended_description.ts index 810eaa3d..905c96ba 100644 --- a/api/api/v1/instance/extended_description.ts +++ b/api/api/v1/instance/extended_description.ts @@ -1,8 +1,8 @@ import { apiRoute } from "@/api"; -import { renderMarkdownInPath } from "@/markdown"; import { createRoute } from "@hono/zod-openapi"; +import { markdownParse } from "~/classes/functions/status"; import { ExtendedDescription as ExtendedDescriptionSchema } from "~/classes/schemas/extended-description"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; const route = createRoute({ method: "get", @@ -27,14 +27,17 @@ const route = createRoute({ export default apiRoute((app) => app.openapi(route, async (context) => { - const { content, lastModified } = await renderMarkdownInPath( - config.instance.extended_description_path ?? "", - "This is a [Versia](https://versia.pub) server with the default extended description.", + const content = await markdownParse( + config.instance.extended_description_path?.content ?? + "This is a [Versia](https://versia.pub) server with the default extended description.", ); return context.json( { - updated_at: lastModified.toISOString(), + updated_at: new Date( + config.instance.extended_description_path?.file + .lastModified ?? 0, + ).toISOString(), content, }, 200, diff --git a/api/api/v1/instance/index.ts b/api/api/v1/instance/index.ts index 09fd6710..e626cab3 100644 --- a/api/api/v1/instance/index.ts +++ b/api/api/v1/instance/index.ts @@ -1,13 +1,13 @@ import { apiRoute, auth } from "@/api"; -import { renderMarkdownInPath } from "@/markdown"; import { proxyUrl } from "@/response"; import { createRoute, type z } from "@hono/zod-openapi"; import { Instance, Note, User } from "@versia/kit/db"; import { Users } from "@versia/kit/tables"; import { and, eq, isNull } from "drizzle-orm"; +import { markdownParse } from "~/classes/functions/status"; import { InstanceV1 as InstanceV1Schema } from "~/classes/schemas/instance-v1"; +import { config } from "~/config.ts"; import manifest from "~/package.json"; -import { config } from "~/packages/config-manager"; const route = createRoute({ method: "get", @@ -65,35 +65,38 @@ export default apiRoute((app) => } | undefined; - const { content } = await renderMarkdownInPath( - config.instance.extended_description_path ?? "", - "This is a [Versia](https://versia.pub) server with the default extended description.", + const content = await markdownParse( + config.instance.extended_description_path?.content ?? + "This is a [Versia](https://versia.pub) server with the default extended description.", ); - // TODO: fill in more values return context.json({ - approval_required: false, + approval_required: config.registration.require_approval, configuration: { polls: { max_characters_per_option: - config.validation.max_poll_option_size, - max_expiration: config.validation.max_poll_duration, - max_options: config.validation.max_poll_options, - min_expiration: config.validation.min_poll_duration, + config.validation.polls.max_option_characters, + max_expiration: + config.validation.polls.max_duration_seconds, + max_options: config.validation.polls.max_options, + min_expiration: + config.validation.polls.min_duration_seconds, }, statuses: { characters_reserved_per_url: 0, - max_characters: config.validation.max_note_size, + max_characters: config.validation.notes.max_characters, max_media_attachments: - config.validation.max_media_attachments, + config.validation.notes.max_attachments, }, media_attachments: { - supported_mime_types: config.validation.allowed_mime_types, - image_size_limit: config.validation.max_media_size, - image_matrix_limit: config.validation.max_media_size, - video_size_limit: config.validation.max_media_size, - video_frame_rate_limit: config.validation.max_media_size, - video_matrix_limit: config.validation.max_media_size, + supported_mime_types: + config.validation.media.allowed_mime_types, + image_size_limit: config.validation.media.max_bytes, + // TODO: Implement + image_matrix_limit: 1 ** 10, + video_size_limit: 1 ** 10, + video_frame_rate_limit: 60, + video_matrix_limit: 1 ** 10, }, accounts: { max_featured_tags: 100, @@ -101,23 +104,22 @@ export default apiRoute((app) => }, short_description: config.instance.description, description: content, - // TODO: Add contact email - email: "", + email: config.instance.contact.email, invites_enabled: false, - registrations: config.signups.registration, - // TODO: Implement - languages: ["en"], - rules: config.signups.rules.map((r, index) => ({ + registrations: config.registration.allow, + languages: config.instance.languages, + rules: config.instance.rules.map((r, index) => ({ id: String(index), - text: r, + text: r.text, + hint: r.hint, })), stats: { domain_count: knownDomainsCount, status_count: statusCount, user_count: userCount, }, - thumbnail: config.instance.logo - ? proxyUrl(config.instance.logo).toString() + thumbnail: config.instance.branding.logo + ? proxyUrl(config.instance.branding.logo).toString() : null, title: config.instance.name, uri: config.http.base_url.host, diff --git a/api/api/v1/instance/privacy_policy.test.ts b/api/api/v1/instance/privacy_policy.test.ts index 66195bb5..6cfff924 100644 --- a/api/api/v1/instance/privacy_policy.test.ts +++ b/api/api/v1/instance/privacy_policy.test.ts @@ -10,7 +10,7 @@ describe("/api/v1/instance/privacy_policy", () => { const json = await response.json(); expect(json).toEqual({ - updated_at: new Date(1970, 0, 0).toISOString(), + updated_at: new Date(0).toISOString(), // This instance has not provided any privacy policy. content: "

This instance has not provided any privacy policy.

\n", diff --git a/api/api/v1/instance/privacy_policy.ts b/api/api/v1/instance/privacy_policy.ts index f8cb35a4..6c3d9dd4 100644 --- a/api/api/v1/instance/privacy_policy.ts +++ b/api/api/v1/instance/privacy_policy.ts @@ -1,8 +1,8 @@ import { apiRoute, auth } from "@/api"; -import { renderMarkdownInPath } from "@/markdown"; import { createRoute } from "@hono/zod-openapi"; +import { markdownParse } from "~/classes/functions/status"; import { PrivacyPolicy as PrivacyPolicySchema } from "~/classes/schemas/privacy-policy"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; const route = createRoute({ method: "get", @@ -32,13 +32,15 @@ const route = createRoute({ export default apiRoute((app) => app.openapi(route, async (context) => { - const { content, lastModified } = await renderMarkdownInPath( - config.instance.privacy_policy_path ?? "", - "This instance has not provided any privacy policy.", + const content = await markdownParse( + config.instance.privacy_policy_path?.content ?? + "This instance has not provided any privacy policy.", ); return context.json({ - updated_at: lastModified.toISOString(), + updated_at: new Date( + config.instance.privacy_policy_path?.file.lastModified ?? 0, + ).toISOString(), content, }); }), diff --git a/api/api/v1/instance/rules.test.ts b/api/api/v1/instance/rules.test.ts index 69a4b95a..07c5ab23 100644 --- a/api/api/v1/instance/rules.test.ts +++ b/api/api/v1/instance/rules.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { config } from "~/packages/config-manager/index.ts"; +import { config } from "~/config.ts"; import { fakeRequest } from "~/tests/utils"; // /api/v1/instance/rules @@ -11,10 +11,10 @@ describe("/api/v1/instance/rules", () => { const json = await response.json(); expect(json).toEqual( - config.signups.rules.map((rule, index) => ({ + config.instance.rules.map((r, index) => ({ id: String(index), - text: rule, - hint: "", + text: r.text, + hint: r.hint, })), ); }); diff --git a/api/api/v1/instance/rules.ts b/api/api/v1/instance/rules.ts index 7826a888..1b1a7eee 100644 --- a/api/api/v1/instance/rules.ts +++ b/api/api/v1/instance/rules.ts @@ -1,7 +1,7 @@ import { apiRoute, auth } from "@/api"; import { createRoute, z } from "@hono/zod-openapi"; import { Rule as RuleSchema } from "~/classes/schemas/rule"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; const route = createRoute({ method: "get", @@ -32,10 +32,10 @@ const route = createRoute({ export default apiRoute((app) => app.openapi(route, (context) => { return context.json( - config.signups.rules.map((rule, index) => ({ + config.instance.rules.map((r, index) => ({ id: String(index), - text: rule, - hint: "", + text: r.text, + hint: r.hint, })), ); }), diff --git a/api/api/v1/instance/terms_of_service.test.ts b/api/api/v1/instance/terms_of_service.test.ts index be42a689..fb3f5baf 100644 --- a/api/api/v1/instance/terms_of_service.test.ts +++ b/api/api/v1/instance/terms_of_service.test.ts @@ -10,7 +10,7 @@ describe("/api/v1/instance/terms_of_service", () => { const json = await response.json(); expect(json).toEqual({ - updated_at: new Date(1970, 0, 0).toISOString(), + updated_at: new Date(0).toISOString(), // This instance has not provided any terms of service. content: "

This instance has not provided any terms of service.

\n", diff --git a/api/api/v1/instance/terms_of_service.ts b/api/api/v1/instance/terms_of_service.ts index bc5b4d8a..eb6e1587 100644 --- a/api/api/v1/instance/terms_of_service.ts +++ b/api/api/v1/instance/terms_of_service.ts @@ -1,8 +1,8 @@ import { apiRoute, auth } from "@/api"; -import { renderMarkdownInPath } from "@/markdown"; import { createRoute } from "@hono/zod-openapi"; +import { markdownParse } from "~/classes/functions/status"; import { TermsOfService as TermsOfServiceSchema } from "~/classes/schemas/tos"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; const route = createRoute({ method: "get", @@ -33,13 +33,15 @@ const route = createRoute({ export default apiRoute((app) => app.openapi(route, async (context) => { - const { content, lastModified } = await renderMarkdownInPath( - config.instance.tos_path ?? "", - "This instance has not provided any terms of service.", + const content = await markdownParse( + config.instance.tos_path?.content ?? + "This instance has not provided any terms of service.", ); return context.json({ - updated_at: lastModified.toISOString(), + updated_at: new Date( + config.instance.tos_path?.file.lastModified ?? 0, + ).toISOString(), content, }); }), diff --git a/api/api/v1/notifications/index.ts b/api/api/v1/notifications/index.ts index e38a4f18..de4cc25c 100644 --- a/api/api/v1/notifications/index.ts +++ b/api/api/v1/notifications/index.ts @@ -4,8 +4,8 @@ import { Timeline } from "@versia/kit/db"; import { Notifications, RolePermissions } from "@versia/kit/tables"; import { and, eq, gt, gte, inArray, lt, not, sql } from "drizzle-orm"; import { Account as AccountSchema } from "~/classes/schemas/account"; +import { zBoolean } from "~/classes/schemas/common.ts"; import { Notification as NotificationSchema } from "~/classes/schemas/notification.ts"; -import { zBoolean } from "~/packages/config-manager/config.type"; const route = createRoute({ method: "get", diff --git a/api/api/v1/roles/index.test.ts b/api/api/v1/roles/index.test.ts index faecb91b..12997104 100644 --- a/api/api/v1/roles/index.test.ts +++ b/api/api/v1/roles/index.test.ts @@ -1,7 +1,7 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { Role } from "@versia/kit/db"; import { RolePermissions } from "@versia/kit/tables"; -import { config } from "~/packages/config-manager/index.ts"; +import { config } from "~/config.ts"; import { fakeRequest, getTestUsers } from "~/tests/utils"; const { users, deleteUsers, tokens } = await getTestUsers(1); diff --git a/api/api/v1/statuses/:id/index.ts b/api/api/v1/statuses/:id/index.ts index a29bd3ac..5e66e6cc 100644 --- a/api/api/v1/statuses/:id/index.ts +++ b/api/api/v1/statuses/:id/index.ts @@ -11,13 +11,13 @@ import { Media } from "@versia/kit/db"; import { RolePermissions } from "@versia/kit/tables"; import { ApiError } from "~/classes/errors/api-error"; import { Attachment as AttachmentSchema } from "~/classes/schemas/attachment"; +import { zBoolean } from "~/classes/schemas/common.ts"; import { PollOption } from "~/classes/schemas/poll"; import { Status as StatusSchema, StatusSource as StatusSourceSchema, } from "~/classes/schemas/status"; -import { zBoolean } from "~/packages/config-manager/config.type"; -import { config } from "~/packages/config-manager/index.ts"; +import { config } from "~/config.ts"; const schema = z .object({ @@ -35,7 +35,7 @@ const schema = z }), media_ids: z .array(AttachmentSchema.shape.id) - .max(config.validation.max_media_attachments) + .max(config.validation.notes.max_attachments) .default([]) .openapi({ description: @@ -51,7 +51,7 @@ const schema = z language: StatusSchema.shape.language.optional(), "poll[options]": z .array(PollOption.shape.title) - .max(config.validation.max_poll_options) + .max(config.validation.polls.max_options) .optional() .openapi({ description: @@ -60,8 +60,8 @@ const schema = z "poll[expires_in]": z.coerce .number() .int() - .min(config.validation.min_poll_duration) - .max(config.validation.max_poll_duration) + .min(config.validation.polls.min_duration_seconds) + .max(config.validation.polls.max_duration_seconds) .optional() .openapi({ description: diff --git a/api/api/v1/statuses/index.test.ts b/api/api/v1/statuses/index.test.ts index f04f90d5..f3aafe8e 100644 --- a/api/api/v1/statuses/index.test.ts +++ b/api/api/v1/statuses/index.test.ts @@ -3,7 +3,7 @@ import type { Status as ApiStatus } from "@versia/client/types"; import { Media, db } from "@versia/kit/db"; import { Emojis } from "@versia/kit/tables"; import { eq } from "drizzle-orm"; -import { config } from "~/packages/config-manager/index.ts"; +import { config } from "~/config.ts"; import { fakeRequest, getTestUsers } from "~/tests/utils"; const { users, tokens, deleteUsers } = await getTestUsers(5); @@ -61,7 +61,7 @@ describe("/api/v1/statuses", () => { Authorization: `Bearer ${tokens[0].data.accessToken}`, }, body: new URLSearchParams({ - status: "a".repeat(config.validation.max_note_size + 1), + status: "a".repeat(config.validation.notes.max_characters + 1), local_only: "true", }), }); diff --git a/api/api/v1/statuses/index.ts b/api/api/v1/statuses/index.ts index 8f56c010..6780e975 100644 --- a/api/api/v1/statuses/index.ts +++ b/api/api/v1/statuses/index.ts @@ -4,13 +4,13 @@ import { Media, Note } from "@versia/kit/db"; import { RolePermissions } from "@versia/kit/tables"; import { ApiError } from "~/classes/errors/api-error"; import { Attachment as AttachmentSchema } from "~/classes/schemas/attachment"; +import { zBoolean } from "~/classes/schemas/common.ts"; import { PollOption } from "~/classes/schemas/poll"; import { Status as StatusSchema, StatusSource as StatusSourceSchema, } from "~/classes/schemas/status"; -import { zBoolean } from "~/packages/config-manager/config.type"; -import { config } from "~/packages/config-manager/index.ts"; +import { config } from "~/config.ts"; const schema = z .object({ @@ -28,7 +28,7 @@ const schema = z }), media_ids: z .array(AttachmentSchema.shape.id) - .max(config.validation.max_media_attachments) + .max(config.validation.notes.max_attachments) .default([]) .openapi({ description: @@ -44,7 +44,7 @@ const schema = z language: StatusSchema.shape.language.optional(), "poll[options]": z .array(PollOption.shape.title) - .max(config.validation.max_poll_options) + .max(config.validation.polls.max_options) .optional() .openapi({ description: @@ -53,8 +53,8 @@ const schema = z "poll[expires_in]": z.coerce .number() .int() - .min(config.validation.min_poll_duration) - .max(config.validation.max_poll_duration) + .min(config.validation.polls.min_duration_seconds) + .max(config.validation.polls.max_duration_seconds) .optional() .openapi({ description: diff --git a/api/api/v1/timelines/home.test.ts b/api/api/v1/timelines/home.test.ts index c0a81097..ee727177 100644 --- a/api/api/v1/timelines/home.test.ts +++ b/api/api/v1/timelines/home.test.ts @@ -1,6 +1,6 @@ import { afterAll, describe, expect, test } from "bun:test"; import type { Status as ApiStatus } from "@versia/client/types"; -import { config } from "~/packages/config-manager/index.ts"; +import { config } from "~/config.ts"; import { fakeRequest, getTestStatuses, getTestUsers } from "~/tests/utils"; const { users, tokens, deleteUsers } = await getTestUsers(5); diff --git a/api/api/v1/timelines/public.test.ts b/api/api/v1/timelines/public.test.ts index 1f7ec651..84a61693 100644 --- a/api/api/v1/timelines/public.test.ts +++ b/api/api/v1/timelines/public.test.ts @@ -1,6 +1,6 @@ import { afterAll, describe, expect, test } from "bun:test"; import type { Status as ApiStatus } from "@versia/client/types"; -import { config } from "~/packages/config-manager/index.ts"; +import { config } from "~/config.ts"; import { fakeRequest, getTestStatuses, getTestUsers } from "~/tests/utils"; const { users, tokens, deleteUsers } = await getTestUsers(5); diff --git a/api/api/v1/timelines/public.ts b/api/api/v1/timelines/public.ts index 7a9306b9..aaebd997 100644 --- a/api/api/v1/timelines/public.ts +++ b/api/api/v1/timelines/public.ts @@ -3,8 +3,8 @@ import { createRoute, z } from "@hono/zod-openapi"; import { Timeline } from "@versia/kit/db"; import { Notes, RolePermissions } from "@versia/kit/tables"; import { and, eq, gt, gte, inArray, lt, or, sql } from "drizzle-orm"; +import { zBoolean } from "~/classes/schemas/common.ts"; import { Status as StatusSchema } from "~/classes/schemas/status"; -import { zBoolean } from "~/packages/config-manager/config.type"; const route = createRoute({ method: "get", diff --git a/api/api/v2/filters/:id/index.ts b/api/api/v2/filters/:id/index.ts index 17094669..7ac2457b 100644 --- a/api/api/v2/filters/:id/index.ts +++ b/api/api/v2/filters/:id/index.ts @@ -4,11 +4,11 @@ import { db } from "@versia/kit/db"; import { FilterKeywords, Filters, RolePermissions } from "@versia/kit/tables"; import { type SQL, and, eq, inArray } from "drizzle-orm"; import { ApiError } from "~/classes/errors/api-error"; +import { zBoolean } from "~/classes/schemas/common.ts"; import { FilterKeyword as FilterKeywordSchema, Filter as FilterSchema, } from "~/classes/schemas/filters"; -import { zBoolean } from "~/packages/config-manager/config.type"; import { ErrorSchema } from "~/types/api"; const routeGet = createRoute({ diff --git a/api/api/v2/instance/index.ts b/api/api/v2/instance/index.ts index b02cd3e3..efb6e7e4 100644 --- a/api/api/v2/instance/index.ts +++ b/api/api/v2/instance/index.ts @@ -5,8 +5,8 @@ import { User } from "@versia/kit/db"; import { Users } from "@versia/kit/tables"; import { and, eq, isNull } from "drizzle-orm"; import { Instance as InstanceSchema } from "~/classes/schemas/instance"; +import { config } from "~/config.ts"; import pkg from "~/package.json"; -import { config } from "~/packages/config-manager"; const route = createRoute({ method: "get", @@ -69,92 +69,99 @@ export default apiRoute((app) => mastodon: 1, }, thumbnail: { - url: config.instance.logo - ? proxyUrl(config.instance.logo).toString() + url: config.instance.branding.logo + ? proxyUrl(config.instance.branding.logo).toString() : pkg.icon, }, banner: { - url: config.instance.banner - ? proxyUrl(config.instance.banner).toString() + url: config.instance.branding.banner + ? proxyUrl(config.instance.branding.banner).toString() : null, }, icon: [], - languages: ["en"], + languages: config.instance.languages, configuration: { urls: { // TODO: Implement Streaming API streaming: "", }, vapid: { - // TODO: Fill in vapid values - public_key: "", + public_key: + config.notifications.push?.vapid_keys.public ?? "", }, accounts: { max_featured_tags: 100, max_displayname_characters: - config.validation.max_displayname_size, - avatar_limit: config.validation.max_avatar_size, - header_limit: config.validation.max_header_size, + config.validation.accounts.max_displayname_characters, + avatar_limit: config.validation.accounts.max_avatar_bytes, + header_limit: config.validation.accounts.max_header_bytes, max_username_characters: - config.validation.max_username_size, - max_note_characters: config.validation.max_bio_size, - max_pinned_statuses: 100, + config.validation.accounts.max_username_characters, + max_note_characters: + config.validation.accounts.max_bio_characters, + max_pinned_statuses: + config.validation.accounts.max_pinned_notes, fields: { - max_fields: config.validation.max_field_count, + max_fields: config.validation.accounts.max_field_count, max_name_characters: - config.validation.max_field_name_size, + config.validation.accounts + .max_field_name_characters, max_value_characters: - config.validation.max_field_value_size, + config.validation.accounts + .max_field_value_characters, }, }, statuses: { - max_characters: config.validation.max_note_size, + max_characters: config.validation.notes.max_characters, max_media_attachments: - config.validation.max_media_attachments, - characters_reserved_per_url: 0, + config.validation.notes.max_attachments, + // TODO: Implement + characters_reserved_per_url: 13, }, media_attachments: { - supported_mime_types: config.validation.allowed_mime_types, - image_size_limit: config.validation.max_media_size, - image_matrix_limit: config.validation.max_media_size, - video_size_limit: config.validation.max_media_size, - video_frame_rate_limit: config.validation.max_media_size, - video_matrix_limit: config.validation.max_media_size, + supported_mime_types: + config.validation.media.allowed_mime_types, + image_size_limit: config.validation.media.max_bytes, + image_matrix_limit: 1 ** 10, + video_size_limit: 1 ** 10, + video_frame_rate_limit: 60, + video_matrix_limit: 1 ** 10, description_limit: - config.validation.max_media_description_size, + config.validation.media.max_description_characters, }, emojis: { - emoji_size_limit: config.validation.max_emoji_size, + emoji_size_limit: config.validation.emojis.max_bytes, max_shortcode_characters: - config.validation.max_emoji_shortcode_size, + config.validation.emojis.max_shortcode_characters, max_description_characters: - config.validation.max_emoji_description_size, + config.validation.emojis.max_description_characters, }, polls: { max_characters_per_option: - config.validation.max_poll_option_size, - max_expiration: config.validation.max_poll_duration, - max_options: config.validation.max_poll_options, - min_expiration: config.validation.min_poll_duration, + config.validation.polls.max_option_characters, + max_expiration: + config.validation.polls.max_duration_seconds, + max_options: config.validation.polls.max_options, + min_expiration: + config.validation.polls.min_duration_seconds, }, translation: { enabled: false, }, }, registrations: { - enabled: config.signups.registration, - approval_required: false, - message: null, + enabled: config.registration.allow, + approval_required: config.registration.require_approval, + message: config.registration.message ?? null, }, contact: { - // TODO: Add contact email - email: "", + email: config.instance.contact.email, account: (contactAccount as User)?.toApi(), }, - rules: config.signups.rules.map((rule, index) => ({ + rules: config.instance.rules.map((r, index) => ({ id: String(index), - text: rule, - hint: "", + text: r.text, + hint: r.hint, })), sso: { forced: oidcConfig?.forced ?? false, diff --git a/api/api/v2/search/index.ts b/api/api/v2/search/index.ts index 6cca58f8..101aa5a0 100644 --- a/api/api/v2/search/index.ts +++ b/api/api/v2/search/index.ts @@ -12,10 +12,10 @@ import { and, eq, inArray, isNull, sql } from "drizzle-orm"; import { ApiError } from "~/classes/errors/api-error"; import { Account as AccountSchema } from "~/classes/schemas/account"; import { Id } from "~/classes/schemas/common"; +import { zBoolean } from "~/classes/schemas/common.ts"; import { Search as SearchSchema } from "~/classes/schemas/search"; import { searchManager } from "~/classes/search/search-manager"; -import { config } from "~/packages/config-manager"; -import { zBoolean } from "~/packages/config-manager/config.type"; +import { config } from "~/config.ts"; import { ErrorSchema } from "~/types/api"; const route = createRoute({ @@ -133,7 +133,7 @@ export default apiRoute((app) => ); } - if (!config.sonic.enabled) { + if (!config.search.enabled) { throw new ApiError(501, "Search is not enabled on this server"); } diff --git a/api/media/proxy/:id.ts b/api/media/proxy/:id.ts index 736390c3..19ec3f31 100644 --- a/api/media/proxy/:id.ts +++ b/api/media/proxy/:id.ts @@ -3,7 +3,7 @@ import { createRoute, z } from "@hono/zod-openapi"; import { proxy } from "hono/proxy"; import type { ContentfulStatusCode, StatusCode } from "hono/utils/http-status"; import { ApiError } from "~/classes/errors/api-error"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; import { ErrorSchema } from "~/types/api"; const schemas = { @@ -56,7 +56,7 @@ export default apiRoute((app) => const media = await proxy(id, { // @ts-expect-error Proxy is a Bun-specific feature - proxy: config.http.proxy.address, + proxy: config.http.proxy_address, }); // Check if file extension ends in svg or svg diff --git a/api/objects/:id/index.ts b/api/objects/:id/index.ts index 136b6c60..4c503d6f 100644 --- a/api/objects/:id/index.ts +++ b/api/objects/:id/index.ts @@ -8,7 +8,7 @@ import { Like, Note, User } from "@versia/kit/db"; import { Likes, Notes } from "@versia/kit/tables"; import { and, eq, inArray, sql } from "drizzle-orm"; import { ApiError } from "~/classes/errors/api-error"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; import { ErrorSchema, type KnownEntity } from "~/types/api"; const route = createRoute({ diff --git a/api/users/:uuid/outbox/index.ts b/api/users/:uuid/outbox/index.ts index 0183e7a0..dbaf626b 100644 --- a/api/users/:uuid/outbox/index.ts +++ b/api/users/:uuid/outbox/index.ts @@ -8,7 +8,7 @@ import { Note, User, db } from "@versia/kit/db"; import { Notes } from "@versia/kit/tables"; import { and, eq, inArray } from "drizzle-orm"; import { ApiError } from "~/classes/errors/api-error"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; import { ErrorSchema } from "~/types/api"; const schemas = { diff --git a/api/well-known/host-meta/index.ts b/api/well-known/host-meta/index.ts index 03731a35..9cea4aad 100644 --- a/api/well-known/host-meta/index.ts +++ b/api/well-known/host-meta/index.ts @@ -1,6 +1,6 @@ import { apiRoute } from "@/api"; import { createRoute, z } from "@hono/zod-openapi"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; const route = createRoute({ method: "get", diff --git a/api/well-known/nodeinfo/2.0/index.ts b/api/well-known/nodeinfo/2.0/index.ts index 688f9091..18e8ef59 100644 --- a/api/well-known/nodeinfo/2.0/index.ts +++ b/api/well-known/nodeinfo/2.0/index.ts @@ -1,8 +1,8 @@ import { apiRoute } from "@/api"; import { createRoute, z } from "@hono/zod-openapi"; import { Note, User } from "@versia/kit/db"; +import { config } from "~/config.ts"; import manifest from "~/package.json"; -import { config } from "~/packages/config-manager"; const route = createRoute({ method: "get", @@ -65,7 +65,7 @@ export default apiRoute((app) => }, localPosts: noteCount, }, - openRegistrations: config.signups.registration, + openRegistrations: config.registration.allow, metadata: { nodeName: config.instance.name, nodeDescription: config.instance.description, diff --git a/api/well-known/nodeinfo/index.ts b/api/well-known/nodeinfo/index.ts index 7ef86316..e7fa841f 100644 --- a/api/well-known/nodeinfo/index.ts +++ b/api/well-known/nodeinfo/index.ts @@ -1,6 +1,6 @@ import { apiRoute } from "@/api"; import { createRoute, z } from "@hono/zod-openapi"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; const route = createRoute({ method: "get", diff --git a/api/well-known/openid-configuration/index.ts b/api/well-known/openid-configuration/index.ts index 32d07668..a35e9094 100644 --- a/api/well-known/openid-configuration/index.ts +++ b/api/well-known/openid-configuration/index.ts @@ -1,6 +1,6 @@ import { apiRoute } from "@/api"; import { createRoute, z } from "@hono/zod-openapi"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; const route = createRoute({ method: "get", diff --git a/api/well-known/versia.ts b/api/well-known/versia.ts index 41b13688..502a7fd7 100644 --- a/api/well-known/versia.ts +++ b/api/well-known/versia.ts @@ -5,8 +5,8 @@ import { InstanceMetadata as InstanceMetadataSchema } from "@versia/federation/s import { User } from "@versia/kit/db"; import { Users } from "@versia/kit/tables"; import { asc } from "drizzle-orm"; +import { config } from "~/config.ts"; import pkg from "~/package.json"; -import { config } from "~/packages/config-manager"; const route = createRoute({ method: "get", @@ -29,6 +29,10 @@ export default apiRoute((app) => // Get date of first user creation const firstUser = await User.fromSql(undefined, asc(Users.createdAt)); + const publicKey = Buffer.from( + await crypto.subtle.exportKey("spki", config.instance.keys.public), + ).toString("base64"); + return context.json( { type: "InstanceMetadata" as const, @@ -43,18 +47,18 @@ export default apiRoute((app) => name: config.instance.name, description: config.instance.description, public_key: { - key: config.instance.keys.public, + key: publicKey, algorithm: "ed25519" as const, }, software: { name: "Versia Server", version: pkg.version, }, - banner: config.instance.banner - ? urlToContentFormat(config.instance.banner) + banner: config.instance.branding.banner + ? urlToContentFormat(config.instance.branding.banner) : undefined, - logo: config.instance.logo - ? urlToContentFormat(config.instance.logo) + logo: config.instance.branding.logo + ? urlToContentFormat(config.instance.branding.logo) : undefined, shared_inbox: new URL( "/inbox", diff --git a/api/well-known/webfinger/index.ts b/api/well-known/webfinger/index.ts index 4c8e9ac6..8a2ce87f 100644 --- a/api/well-known/webfinger/index.ts +++ b/api/well-known/webfinger/index.ts @@ -12,7 +12,7 @@ import { User } from "@versia/kit/db"; import { Users } from "@versia/kit/tables"; import { and, eq, isNull } from "drizzle-orm"; import { ApiError } from "~/classes/errors/api-error"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; import { ErrorSchema } from "~/types/api"; const schemas = { @@ -90,7 +90,7 @@ export default apiRoute((app) => let activityPubUrl = ""; - if (config.federation.bridge.enabled) { + if (config.federation.bridge) { const manager = await User.getFederationRequester(); try { @@ -98,7 +98,7 @@ export default apiRoute((app) => user.data.username, config.http.base_url.host, "application/activity+json", - config.federation.bridge.url?.toString(), + config.federation.bridge.url.origin, ); } catch (e) { const error = e as ResponseError; @@ -136,7 +136,7 @@ export default apiRoute((app) => type: user.avatar?.getPreferredMimeType() ?? "image/svg+xml", - href: user.getAvatarUrl(config), + href: user.getAvatarUrl(), }, ].filter(Boolean) as { rel: string; diff --git a/app.ts b/app.ts index 94076e40..5245ce95 100644 --- a/app.ts +++ b/app.ts @@ -12,12 +12,11 @@ import { cors } from "hono/cors"; import { createMiddleware } from "hono/factory"; import { prettyJSON } from "hono/pretty-json"; import { secureHeaders } from "hono/secure-headers"; +import { config } from "~/config.ts"; import pkg from "~/package.json" with { type: "application/json" }; -import { config } from "~/packages/config-manager/index.ts"; import { ApiError } from "./classes/errors/api-error.ts"; import { PluginLoader } from "./classes/plugin/loader.ts"; import { agentBans } from "./middlewares/agent-bans.ts"; -import { bait } from "./middlewares/bait.ts"; import { boundaryCheck } from "./middlewares/boundary-check.ts"; import { ipBans } from "./middlewares/ip-bans.ts"; import { logger } from "./middlewares/logger.ts"; @@ -33,21 +32,8 @@ export const appFactory = async (): Promise> => { defaultHook: handleZodError, }); - /* const { printMetrics, registerMetrics } = prometheus({ - collectDefaultMetrics: true, - metricOptions: { - requestsTotal: { - customLabels: { - content_type: (c) => - c.res.headers.get("content-type") ?? "unknown", - }, - }, - }, - }); */ - app.use(ipBans); app.use(agentBans); - app.use(bait); app.use(logger); app.use(boundaryCheck); app.use( diff --git a/classes/config/schema.ts b/classes/config/schema.ts new file mode 100644 index 00000000..e4822bcd --- /dev/null +++ b/classes/config/schema.ts @@ -0,0 +1,740 @@ +import { z } from "@hono/zod-openapi"; +import { + ADMIN_ROLES, + DEFAULT_ROLES, + RolePermissions, +} from "@versia/kit/tables"; +import { type BunFile, file } from "bun"; +import { types as mimeTypes } from "mime-types"; +import { generateVAPIDKeys } from "web-push"; +import { ZodError } from "zod"; +import { fromZodError } from "zod-validation-error"; +import { iso631 } from "../schemas/common.ts"; + +export enum MediaBackendType { + Local = "local", + S3 = "s3", +} + +const urlPath = z + .string() + .trim() + .min(1) + // Remove trailing slashes, but keep the root slash + .transform((arg) => (arg === "/" ? arg : arg.replace(/\/$/, ""))); + +const url = z + .string() + .trim() + .min(1) + .refine((arg) => URL.canParse(arg), "Invalid url") + .transform((arg) => new URL(arg)); + +const unixPort = z + .number() + .int() + .min(1) + .max(2 ** 16 - 1); + +const fileFromPathString = (text: string): BunFile => file(text.slice(5)); + +// Not using .ip() because we allow CIDR ranges and wildcards and such +const ip = z + .string() + .describe("An IPv6/v4 address or CIDR range. Wildcards are also allowed"); + +const regex = z + .string() + .transform((arg) => new RegExp(arg)) + .describe("JavaScript regular expression"); + +export const sensitiveString = z + .string() + .refine( + (text) => + text.startsWith("PATH:") ? fileFromPathString(text).exists() : true, + (text) => ({ + message: `Path ${fileFromPathString(text).name} does not exist, is a directory or is not accessible`, + }), + ) + .transform((text) => + text.startsWith("PATH:") ? fileFromPathString(text).text() : text, + ) + .describe("You can use PATH:/path/to/file to load this value from a file"); + +export const filePathString = z + .string() + .transform((s) => file(s)) + .refine( + (file) => file.exists(), + (file) => ({ + message: `Path ${file.name} does not exist, is a directory or is not accessible`, + }), + ) + .transform(async (file) => ({ + content: await file.text(), + file, + })) + .describe("This value must be a file path"); + +export const keyPair = z + .strictObject({ + public: sensitiveString, + private: sensitiveString, + }) + .optional() + .transform(async (k, ctx) => { + if (!k) { + const keys = await crypto.subtle.generateKey("Ed25519", true, [ + "sign", + "verify", + ]); + + const privateKey = Buffer.from( + await crypto.subtle.exportKey("pkcs8", keys.privateKey), + ).toString("base64"); + + const publicKey = Buffer.from( + await crypto.subtle.exportKey("spki", keys.publicKey), + ).toString("base64"); + + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Public and private keys are not set. Here are generated keys for you to copy.\n\nPublic: ${publicKey}\nPrivate: ${privateKey}`, + }); + + return z.NEVER; + } + + let publicKey: CryptoKey; + let privateKey: CryptoKey; + + try { + publicKey = await crypto.subtle.importKey( + "spki", + Buffer.from(k.public, "base64"), + "Ed25519", + true, + ["verify"], + ); + } catch { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Public key is invalid", + }); + + return z.NEVER; + } + + try { + privateKey = await crypto.subtle.importKey( + "pkcs8", + Buffer.from(k.private, "base64"), + "Ed25519", + true, + ["sign"], + ); + } catch { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Private key is invalid", + }); + + return z.NEVER; + } + + return { + public: publicKey, + private: privateKey, + }; + }); + +export const vapidKeyPair = z + .strictObject({ + public: sensitiveString, + private: sensitiveString, + }) + .optional() + .transform((k, ctx) => { + if (!k) { + const keys = generateVAPIDKeys(); + + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `VAPID keys are not set. Here are generated keys for you to copy.\n\nPublic: ${keys.publicKey}\nPrivate: ${keys.privateKey}`, + }); + + return z.NEVER; + } + + return k; + }); + +export const hmacKey = sensitiveString.transform(async (text, ctx) => { + if (!text) { + const key = await crypto.subtle.generateKey( + { + name: "HMAC", + hash: "SHA-256", + }, + true, + ["sign"], + ); + + const exported = await crypto.subtle.exportKey("raw", key); + + const base64 = Buffer.from(exported).toString("base64"); + + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `HMAC key is not set. Here is a generated key for you to copy: ${base64}`, + }); + + return z.NEVER; + } + + try { + await crypto.subtle.importKey( + "raw", + Buffer.from(text, "base64"), + { + name: "HMAC", + hash: "SHA-256", + }, + true, + ["sign"], + ); + } catch { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "HMAC key is invalid", + }); + + return z.NEVER; + } + + return text; +}); + +try { + console.info(); +} catch (e) { + if (e instanceof ZodError) { + throw fromZodError(e); + } + + throw e; +} + +export const ConfigSchema = z + .strictObject({ + postgres: z + .strictObject({ + host: z.string().min(1).default("localhost"), + port: unixPort.default(5432), + username: z.string().min(1), + password: sensitiveString.default(""), + database: z.string().min(1).default("versia"), + replicas: z + .array( + z.strictObject({ + host: z.string().min(1), + port: unixPort.default(5432), + username: z.string().min(1), + password: sensitiveString.default(""), + database: z.string().min(1).default("versia"), + }), + ) + .describe("Additional read-only replicas") + .default([]), + }) + .describe("PostgreSQL database configuration"), + redis: z + .strictObject({ + queue: z + .strictObject({ + host: z.string().min(1).default("localhost"), + port: unixPort.default(6379), + password: sensitiveString.default(""), + database: z.number().int().default(0), + }) + .describe("A Redis database used for managing queues."), + cache: z + .strictObject({ + host: z.string().min(1).default("localhost"), + port: unixPort.default(6379), + password: sensitiveString.default(""), + database: z.number().int().default(1), + }) + .optional() + .describe( + "A Redis database used for caching SQL queries. Optional.", + ), + }) + .describe("Redis configuration. Used for queues and caching."), + search: z + .strictObject({ + enabled: z + .boolean() + .default(false) + .describe("Enable indexing and searching?"), + sonic: z + .strictObject({ + host: z.string().min(1).default("localhost"), + port: unixPort.default(7700), + password: sensitiveString, + }) + .describe("Sonic database configuration") + .optional(), + }) + .refine( + (o) => !o.enabled || o.sonic, + "When search is enabled, Sonic configuration must be set", + ) + .describe("Search and indexing configuration"), + registration: z.strictObject({ + allow: z + .boolean() + .default(true) + .describe("Can users sign up freely?"), + require_approval: z.boolean().default(false), + message: z + .string() + .optional() + .describe( + "Message to show to users when registration is disabled", + ), + }), + http: z.strictObject({ + base_url: url.describe( + "URL that the instance will be accessible at", + ), + bind: z.string().min(1).default("0.0.0.0"), + bind_port: unixPort.default(8080), + banned_ips: z.array(ip).default([]), + banned_user_agents: z.array(regex).default([]), + proxy_address: url + .optional() + .describe("URL to an eventual HTTP proxy") + .refine(async (url) => { + if (!url) { + return true; + } + + // Test the proxy + const response = await fetch( + "https://api.ipify.org?format=json", + { + // @ts-expect-error Proxy is a Bun-specific feature + proxy: url.origin, + }, + ); + + return response.ok; + }, "The HTTP proxy address is not reachable"), + tls: z + .strictObject({ + key: filePathString, + cert: filePathString, + passphrase: sensitiveString.optional(), + ca: filePathString.optional(), + }) + .describe( + "TLS configuration. You should probably be using a reverse proxy instead of this", + ) + .optional(), + }), + frontend: z.strictObject({ + enabled: z.boolean().default(true), + url: url.default("http://localhost:3000"), + routes: z.strictObject({ + home: urlPath.default("/"), + login: urlPath.default("/oauth/authorize"), + consent: urlPath.default("/oauth/consent"), + register: urlPath.default("/register"), + password_reset: urlPath.default("/oauth/reset"), + }), + settings: z.record(z.string(), z.any()).default({}), + }), + email: z + .strictObject({ + send_emails: z.boolean().default(false), + smtp: z + .strictObject({ + server: z.string().min(1), + port: unixPort.default(465), + username: z.string().min(1), + password: sensitiveString.optional(), + tls: z.boolean().default(true), + }) + .optional(), + }) + .refine( + (o) => o.send_emails || !o.smtp, + "When send_emails is enabled, SMTP configuration must be set", + ), + media: z.strictObject({ + backend: z + .nativeEnum(MediaBackendType) + .default(MediaBackendType.Local), + uploads_path: z.string().min(1).default("uploads"), + conversion: z.strictObject({ + convert_images: z.boolean().default(false), + convert_to: z.string().default("image/webp"), + convert_vectors: z.boolean().default(false), + }), + }), + s3: z + .strictObject({ + endpoint: url, + access_key: sensitiveString, + secret_access_key: sensitiveString, + region: z.string().optional(), + bucket_name: z.string().optional(), + public_url: url.describe( + "Public URL that uploaded media will be accessible at", + ), + }) + .optional(), + validation: z.strictObject({ + accounts: z.strictObject({ + max_displayname_characters: z + .number() + .int() + .nonnegative() + .default(50), + max_username_characters: z + .number() + .int() + .nonnegative() + .default(30), + max_bio_characters: z + .number() + .int() + .nonnegative() + .default(5000), + max_avatar_bytes: z + .number() + .int() + .nonnegative() + .default(5_000_000), + max_header_bytes: z + .number() + .int() + .nonnegative() + .default(5_000_000), + disallowed_usernames: z + .array(regex) + .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", + ]), + max_field_count: z.number().int().default(10), + max_field_name_characters: z.number().int().default(1000), + max_field_value_characters: z.number().int().default(1000), + max_pinned_notes: z.number().int().default(20), + }), + notes: z.strictObject({ + max_characters: z.number().int().nonnegative().default(5000), + allowed_url_schemes: z + .array(z.string()) + .default([ + "http", + "https", + "ftp", + "dat", + "dweb", + "gopher", + "hyper", + "ipfs", + "ipns", + "irc", + "xmpp", + "ircs", + "magnet", + "mailto", + "mumble", + "ssb", + "gemini", + ]), + max_attachments: z.number().int().default(16), + }), + media: z.strictObject({ + max_bytes: z.number().int().nonnegative().default(40_000_000), + max_description_characters: z + .number() + .int() + .nonnegative() + .default(1000), + allowed_mime_types: z + .array(z.string()) + .default(Object.values(mimeTypes)), + }), + emojis: z.strictObject({ + max_bytes: z.number().int().nonnegative().default(1_000_000), + max_shortcode_characters: z + .number() + .int() + .nonnegative() + .default(100), + max_description_characters: z + .number() + .int() + .nonnegative() + .default(1_000), + }), + polls: z.strictObject({ + max_options: z.number().int().nonnegative().default(20), + max_option_characters: z + .number() + .int() + .nonnegative() + .default(500), + min_duration_seconds: z + .number() + .int() + .nonnegative() + .default(60), + max_duration_seconds: z + .number() + .int() + .nonnegative() + .default(100 * 24 * 60 * 60), + }), + emails: z.strictObject({ + disallow_tempmail: z + .boolean() + .default(false) + .describe("Blocks over 10,000 common tempmail domains"), + disallowed_domains: z.array(regex).default([]), + }), + challenges: z + .strictObject({ + difficulty: z.number().int().positive().default(50000), + expiration: z.number().int().positive().default(300), + key: hmacKey, + }) + .optional() + .describe( + "CAPTCHA challenge configuration. Challenges are disabled if not provided.", + ), + filters: z + .strictObject({ + note_content: z.array(regex).default([]), + emoji_shortcode: z.array(regex).default([]), + username: z.array(regex).default([]), + displayname: z.array(regex).default([]), + bio: z.array(regex).default([]), + }) + .describe( + "Block content that matches these regular expressions", + ), + }), + notifications: z.strictObject({ + push: z + .strictObject({ + vapid_keys: vapidKeyPair, + subject: z + .string() + .optional() + .describe( + "Subject field embedded in the push notification. Example: 'mailto:contact@example.com'", + ), + }) + .describe( + "Web Push Notifications configuration. Leave out to disable.", + ) + .optional(), + }), + defaults: z.strictObject({ + visibility: z + .enum(["public", "unlisted", "private", "direct"]) + .default("public"), + language: z.string().default("en"), + avatar: url.optional(), + header: url.optional(), + placeholder_style: z + .string() + .default("thumbs") + .describe("A style name from https://www.dicebear.com/styles"), + }), + federation: z.strictObject({ + blocked: z.array(z.string()).default([]), + followers_only: z.array(z.string()).default([]), + discard: z.strictObject({ + reports: z.array(z.string()).default([]), + deletes: z.array(z.string()).default([]), + updates: z.array(z.string()).default([]), + media: z.array(z.string()).default([]), + follows: z.array(z.string()).default([]), + likes: z.array(z.string()).default([]), + reactions: z.array(z.string()).default([]), + banners: z.array(z.string()).default([]), + avatars: z.array(z.string()).default([]), + }), + bridge: z + .strictObject({ + software: z.enum(["versia-ap"]).or(z.string()), + allowed_ips: z.array(ip).default([]), + token: sensitiveString, + url, + }) + .optional(), + }), + queues: z.record( + z.enum(["delivery", "inbox", "fetch", "push", "media"]), + z.strictObject({ + remove_after_complete_seconds: z + .number() + .int() + .nonnegative() + // 1 year + .default(60 * 60 * 24 * 365), + remove_after_failure_seconds: z + .number() + .int() + .nonnegative() + // 1 year + .default(60 * 60 * 24 * 365), + }), + ), + instance: z.strictObject({ + name: z.string().min(1).default("Versia Server"), + description: z.string().min(1).default("A Versia instance"), + extended_description_path: filePathString.optional(), + tos_path: filePathString.optional(), + privacy_policy_path: filePathString.optional(), + branding: z.strictObject({ + logo: url.optional(), + banner: url.optional(), + }), + languages: z + .array(iso631) + .describe("Primary instance languages. ISO 639-1 codes."), + contact: z.strictObject({ + email: z + .string() + .email() + .describe("Email to contact the instance administration"), + }), + rules: z + .array( + z.strictObject({ + text: z + .string() + .min(1) + .max(255) + .describe("Short description of the rule"), + hint: z + .string() + .min(1) + .max(4096) + .optional() + .describe( + "Longer version of the rule with additional information", + ), + }), + ) + .default([]), + keys: keyPair, + }), + permissions: z.strictObject({ + 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), + }), + logging: z.strictObject({ + types: z.record( + z.enum([ + "requests", + "responses", + "requests_content", + "filters", + ]), + z + .boolean() + .default(false) + .or( + z.strictObject({ + level: z + .enum([ + "debug", + "info", + "warning", + "error", + "fatal", + ]) + .default("info"), + log_file_path: z.string().optional(), + }), + ), + ), + log_level: z + .enum(["debug", "info", "warning", "error", "fatal"]) + .default("info"), + sentry: z + .strictObject({ + dsn: url, + 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(), + }) + .optional(), + log_file_path: z.string().default("logs/versia.log"), + }), + debug: z + .strictObject({ + federation: z.boolean().default(false), + }) + .optional(), + plugins: z.strictObject({ + autoload: z.boolean().default(true), + overrides: z + .strictObject({ + enabled: z.array(z.string()).default([]), + disabled: z.array(z.string()).default([]), + }) + .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(), + }), + }) + .refine( + // If media backend is S3, s3 config must be set + (arg) => arg.media.backend === MediaBackendType.Local || !!arg.s3, + "When media backend is S3, S3 configuration must be set", + ); diff --git a/packages/config-manager/json-schema.ts b/classes/config/to-json-schema.ts similarity index 52% rename from packages/config-manager/json-schema.ts rename to classes/config/to-json-schema.ts index cd4d48b8..6af0d8ff 100644 --- a/packages/config-manager/json-schema.ts +++ b/classes/config/to-json-schema.ts @@ -1,6 +1,6 @@ import { zodToJsonSchema } from "zod-to-json-schema"; -import { configValidator } from "./config.type"; +import { ConfigSchema } from "./schema.ts"; -const jsonSchema = zodToJsonSchema(configValidator); +const jsonSchema = zodToJsonSchema(ConfigSchema, {}); console.write(`${JSON.stringify(jsonSchema, null, 4)}\n`); diff --git a/classes/database/instance.ts b/classes/database/instance.ts index 4d3dcfaf..26433542 100644 --- a/classes/database/instance.ts +++ b/classes/database/instance.ts @@ -12,7 +12,7 @@ import { eq, inArray, } from "drizzle-orm"; -import { config } from "~/packages/config-manager/index.ts"; +import { config } from "~/config.ts"; import { ApiError } from "../errors/api-error.ts"; import { BaseInterface } from "./base.ts"; import { User } from "./user.ts"; @@ -147,7 +147,7 @@ export class Instance extends BaseInterface { const { ok, raw, data } = await requester .get(wellKnownUrl, { // @ts-expect-error Bun extension - proxy: config.http.proxy.address, + proxy: config.http.proxy_address, }) .catch((e) => ({ ...(e as ResponseError).response, @@ -204,7 +204,7 @@ export class Instance extends BaseInterface { links: { rel: string; href: string }[]; }>(wellKnownUrl, { // @ts-expect-error Bun extension - proxy: config.http.proxy.address, + proxy: config.http.proxy_address, }) .catch((e) => ({ ...( @@ -256,7 +256,7 @@ export class Instance extends BaseInterface { software: { version: string }; }>(metadataUrl.href, { // @ts-expect-error Bun extension - proxy: config.http.proxy.address, + proxy: config.http.proxy_address, }) .catch((e) => ({ ...( diff --git a/classes/database/like.ts b/classes/database/like.ts index 629f26f5..0d7ebe9d 100644 --- a/classes/database/like.ts +++ b/classes/database/like.ts @@ -17,7 +17,7 @@ import { eq, inArray, } from "drizzle-orm"; -import { config } from "~/packages/config-manager/index.ts"; +import { config } from "~/config.ts"; import { BaseInterface } from "./base.ts"; import { Note } from "./note.ts"; import { User } from "./user.ts"; diff --git a/classes/database/media.ts b/classes/database/media.ts index 715e51c8..d36e91db 100644 --- a/classes/database/media.ts +++ b/classes/database/media.ts @@ -15,9 +15,9 @@ import { inArray, } from "drizzle-orm"; import sharp from "sharp"; +import { MediaBackendType } from "~/classes/config/schema.ts"; import type { Attachment as AttachmentSchema } from "~/classes/schemas/attachment.ts"; -import { MediaBackendType } from "~/packages/config-manager/config.type"; -import { config } from "~/packages/config-manager/index.ts"; +import { config } from "~/config.ts"; import { ApiError } from "../errors/api-error.ts"; import { getMediaHash } from "../media/media-hasher.ts"; import { MediaJobType, mediaQueue } from "../queues/media.ts"; @@ -135,11 +135,7 @@ export class Media extends BaseInterface { switch (config.media.backend) { case MediaBackendType.Local: { - const path = join( - config.media.local_uploads_folder, - hash, - fileName, - ); + const path = join(config.media.uploads_path, hash, fileName); await write(path, file); @@ -154,7 +150,7 @@ export class Media extends BaseInterface { } const client = new S3Client({ - endpoint: config.s3.endpoint, + endpoint: config.s3.endpoint.origin, region: config.s3.region, bucket: config.s3.bucket_name, accessKeyId: config.s3.access_key, @@ -260,21 +256,21 @@ export class Media extends BaseInterface { } private static checkFile(file: File): void { - if (file.size > config.validation.max_media_size) { + if (file.size > config.validation.media.max_bytes) { throw new ApiError( 413, - `File too large, max size is ${config.validation.max_media_size} bytes`, + `File too large, max size is ${config.validation.media.max_bytes} bytes`, ); } if ( - config.validation.enforce_mime_types && - !config.validation.allowed_mime_types.includes(file.type) + config.validation.media.allowed_mime_types.length > 0 && + !config.validation.media.allowed_mime_types.includes(file.type) ) { throw new ApiError( 415, `File type ${file.type} is not allowed`, - `Allowed types: ${config.validation.allowed_mime_types.join(", ")}`, + `Allowed types: ${config.validation.media.allowed_mime_types.join(", ")}`, ); } } diff --git a/classes/database/note.ts b/classes/database/note.ts index 3fb01f44..06dbcb09 100644 --- a/classes/database/note.ts +++ b/classes/database/note.ts @@ -38,7 +38,7 @@ import { parseTextMentions, } from "~/classes/functions/status"; import type { Status as StatusSchema } from "~/classes/schemas/status.ts"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; import { DeliveryJobType, deliveryQueue } from "../queues/delivery.ts"; import type { Status } from "../schemas/status.ts"; import { Application } from "./application.ts"; @@ -594,7 +594,7 @@ export class Note extends BaseInterface { const { data } = await requester.get(uri, { // @ts-expect-error Bun extension - proxy: config.http.proxy.address, + proxy: config.http.proxy_address, }); const note = await new EntityValidator().Note(data); diff --git a/classes/database/reaction.ts b/classes/database/reaction.ts index 41e07acf..60412ef8 100644 --- a/classes/database/reaction.ts +++ b/classes/database/reaction.ts @@ -9,7 +9,7 @@ import { eq, inArray, } from "drizzle-orm"; -import { config } from "~/packages/config-manager/index.ts"; +import { config } from "~/config.ts"; import { BaseInterface } from "./base.ts"; type ReactionType = InferSelectModel & { diff --git a/classes/database/role.ts b/classes/database/role.ts index f29d6cea..0ebefb9d 100644 --- a/classes/database/role.ts +++ b/classes/database/role.ts @@ -14,7 +14,7 @@ import { eq, inArray, } from "drizzle-orm"; -import { config } from "~/packages/config-manager/index.ts"; +import { config } from "~/config.ts"; import { BaseInterface } from "./base.ts"; type RoleType = InferSelectModel; diff --git a/classes/database/timeline.ts b/classes/database/timeline.ts index 74c1adcd..d382289c 100644 --- a/classes/database/timeline.ts +++ b/classes/database/timeline.ts @@ -1,6 +1,6 @@ import { Notes, Notifications, Users } from "@versia/kit/tables"; import { type SQL, gt } from "drizzle-orm"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; import { Note } from "./note.ts"; import { Notification } from "./notification.ts"; import { User } from "./user.ts"; diff --git a/classes/database/user.ts b/classes/database/user.ts index 6d403179..a201f307 100644 --- a/classes/database/user.ts +++ b/classes/database/user.ts @@ -47,7 +47,7 @@ import { import { htmlToText } from "html-to-text"; import { findManyUsers } from "~/classes/functions/user"; import { searchManager } from "~/classes/search/search-manager"; -import { type Config, config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; import type { KnownEntity } from "~/types/api.ts"; import { DeliveryJobType, deliveryQueue } from "../queues/delivery.ts"; import { PushJobType, pushQueue } from "../queues/push.ts"; @@ -522,7 +522,7 @@ export class User extends BaseInterface { }); // Also do push notifications - if (config.notifications.push.enabled) { + if (config.notifications.push) { await this.notifyPush(notification.id, type, relatedUser, note); } } @@ -603,7 +603,7 @@ export class User extends BaseInterface { } if (instance.data.protocol === "activitypub") { - if (!config.federation.bridge.enabled) { + if (!config.federation.bridge) { throw new Error("ActivityPub bridge is not enabled"); } @@ -627,7 +627,7 @@ export class User extends BaseInterface { const requester = await User.getFederationRequester(); const output = await requester.get>(uri, { // @ts-expect-error Bun extension - proxy: config.http.proxy.address, + proxy: config.http.proxy_address, }); const { data: json } = output; @@ -815,10 +815,9 @@ export class User extends BaseInterface { /** * Get the user's avatar in raw URL format - * @param config The config to use * @returns The raw URL for the user's avatar */ - public getAvatarUrl(config: Config): URL { + public getAvatarUrl(): URL { if (!this.avatar) { return ( config.defaults.avatar || @@ -912,10 +911,9 @@ export class User extends BaseInterface { /** * Get the user's header in raw URL format - * @param config The config to use * @returns The raw URL for the user's header */ - public getHeaderUrl(config: Config): URL | null { + public getHeaderUrl(): URL | null { if (!this.header) { return config.defaults.header ?? null; } @@ -996,7 +994,7 @@ export class User extends BaseInterface { JSON.stringify(entity), ); - if (config.debug.federation) { + if (config.debug?.federation) { const logger = getLogger("federation"); // Log public key @@ -1014,8 +1012,8 @@ export class User extends BaseInterface { * * @returns The requester */ - public static async getFederationRequester(): Promise { - const signatureConstructor = await SignatureConstructor.fromStringKey( + public static getFederationRequester(): FederationRequester { + const signatureConstructor = new SignatureConstructor( config.instance.keys.private, config.http.base_url, ); @@ -1087,7 +1085,7 @@ export class User extends BaseInterface { try { await new FederationRequester().post(inbox, entity, { // @ts-expect-error Bun extension - proxy: config.http.proxy.address, + proxy: config.http.proxy_address, headers: { ...headers.toJSON(), "Content-Type": "application/json; charset=utf-8", @@ -1117,9 +1115,9 @@ export class User extends BaseInterface { url: user.uri || new URL(`/@${user.username}`, config.http.base_url).toString(), - avatar: proxyUrl(this.getAvatarUrl(config)).toString(), - header: this.getHeaderUrl(config) - ? proxyUrl(this.getHeaderUrl(config) as URL).toString() + avatar: proxyUrl(this.getAvatarUrl()).toString(), + header: this.getHeaderUrl() + ? proxyUrl(this.getHeaderUrl() as URL).toString() : "", locked: user.isLocked, created_at: new Date(user.createdAt).toISOString(), @@ -1135,9 +1133,9 @@ export class User extends BaseInterface { bot: user.isBot, source: isOwnAccount ? user.source : undefined, // TODO: Add static avatar and header - avatar_static: proxyUrl(this.getAvatarUrl(config)).toString(), - header_static: this.getHeaderUrl(config) - ? proxyUrl(this.getHeaderUrl(config) as URL).toString() + avatar_static: proxyUrl(this.getAvatarUrl()).toString(), + header_static: this.getHeaderUrl() + ? proxyUrl(this.getHeaderUrl() as URL).toString() : "", acct: this.getAcct(), // TODO: Add these fields diff --git a/classes/functions/status.ts b/classes/functions/status.ts index 42874851..8ed9380a 100644 --- a/classes/functions/status.ts +++ b/classes/functions/status.ts @@ -18,7 +18,7 @@ import { import MarkdownIt from "markdown-it"; import markdownItContainer from "markdown-it-container"; import markdownItTocDoneRight from "markdown-it-toc-done-right"; -import { config } from "~/packages/config-manager/index.ts"; +import { config } from "~/config.ts"; import { transformOutputToUserWithRelations, userExtrasTemplate, diff --git a/classes/inbox/processor.test.ts b/classes/inbox/processor.test.ts index 4e0076bb..4838b3ae 100644 --- a/classes/inbox/processor.test.ts +++ b/classes/inbox/processor.test.ts @@ -9,8 +9,10 @@ import { User, } from "@versia/kit/db"; import type { SocketAddress } from "bun"; +import type { z } from "zod"; import { ValidationError } from "zod-validation-error"; -import { config } from "~/packages/config-manager/index.ts"; +import { config } from "~/config.ts"; +import type { ConfigSchema } from "../config/schema.ts"; import { InboxProcessor } from "./processor.ts"; // Mock dependencies @@ -58,7 +60,7 @@ mock.module("@versia/federation", () => ({ RequestParserHandler: jest.fn(), })); -mock.module("~/packages/config-manager/index.ts", () => ({ +mock.module("~/config.ts", () => ({ config: { debug: { federation: false, @@ -172,9 +174,13 @@ describe("InboxProcessor", () => { }); test("returns false for valid bridge request", () => { - config.federation.bridge.enabled = true; - config.federation.bridge.token = "valid-token"; - config.federation.bridge.allowed_ips = ["127.0.0.1"]; + config.federation.bridge = { + token: "valid-token", + allowed_ips: ["127.0.0.1"], + url: new URL("https://test.com"), + software: "versia-ap", + }; + mockHeaders.authorization = "Bearer valid-token"; // biome-ignore lint/complexity/useLiteralKeys: Private method @@ -183,7 +189,9 @@ describe("InboxProcessor", () => { }); test("returns error response for invalid token", () => { - config.federation.bridge.enabled = true; + config.federation.bridge = {} as z.infer< + typeof ConfigSchema + >["federation"]["bridge"]; mockHeaders.authorization = "Bearer invalid-token"; // biome-ignore lint/complexity/useLiteralKeys: Private method diff --git a/classes/inbox/processor.ts b/classes/inbox/processor.ts index 6f4a4ca0..bab8412f 100644 --- a/classes/inbox/processor.ts +++ b/classes/inbox/processor.ts @@ -23,7 +23,7 @@ import { eq } from "drizzle-orm"; import type { StatusCode } from "hono/utils/http-status"; import { matches } from "ip-matching"; import { type ValidationError, isValidationError } from "zod-validation-error"; -import { config } from "~/packages/config-manager/index.ts"; +import { config } from "~/config.ts"; type ResponseBody = { message?: string; @@ -98,7 +98,7 @@ export class InboxProcessor { throw new Error("Sender is not defined"); } - if (config.debug.federation) { + if (config.debug?.federation) { this.logger.debug`Sender public key: ${chalk.gray( this.sender.key, )}`; @@ -134,7 +134,7 @@ export class InboxProcessor { * @returns {boolean | ResponseBody} - Whether to skip signature checks. May include a response body if there are errors. */ private shouldCheckSignature(): boolean | ResponseBody { - if (config.federation.bridge.enabled) { + if (config.federation.bridge) { const token = this.headers.authorization?.split("Bearer ")[1]; if (token) { @@ -158,6 +158,14 @@ export class InboxProcessor { * @returns */ private isRequestFromBridge(token: string): boolean | ResponseBody { + if (!config.federation.bridge) { + return { + message: + "Bridge is not configured. Please remove the Authorization header.", + code: 500, + }; + } + if (token !== config.federation.bridge.token) { return { message: diff --git a/classes/media/preprocessors/image-conversion.test.ts b/classes/media/preprocessors/image-conversion.test.ts index b83061d1..a6e598df 100644 --- a/classes/media/preprocessors/image-conversion.test.ts +++ b/classes/media/preprocessors/image-conversion.test.ts @@ -1,10 +1,10 @@ import { beforeEach, describe, expect, it, mock } from "bun:test"; import sharp from "sharp"; -import type { Config } from "~/packages/config-manager/config.type"; +import type { config } from "~/config.ts"; import { convertImage } from "./image-conversion.ts"; describe("ImageConversionPreprocessor", () => { - let mockConfig: Config; + let mockConfig: typeof config; beforeEach(() => { mockConfig = { @@ -15,9 +15,9 @@ describe("ImageConversionPreprocessor", () => { convert_vector: false, }, }, - } as Config; + } as unknown as typeof config; - mock.module("~/packages/config-manager/index.ts", () => ({ + mock.module("~/config.ts", () => ({ config: mockConfig, })); }); @@ -59,7 +59,7 @@ describe("ImageConversionPreprocessor", () => { }); it("should convert SVG when convert_vector is true", async () => { - mockConfig.media.conversion.convert_vector = true; + mockConfig.media.conversion.convert_vectors = true; const svgContent = ''; diff --git a/classes/media/preprocessors/image-conversion.ts b/classes/media/preprocessors/image-conversion.ts index a87c8fc2..55693658 100644 --- a/classes/media/preprocessors/image-conversion.ts +++ b/classes/media/preprocessors/image-conversion.ts @@ -4,7 +4,7 @@ */ import sharp from "sharp"; -import { config } from "~/packages/config-manager/index.ts"; +import { config } from "~/config.ts"; /** * Supported input media formats. @@ -39,7 +39,7 @@ const supportedOutputFormats = [ const isConvertible = (file: File): boolean => { if ( file.type === "image/svg+xml" && - !config.media.conversion.convert_vector + !config.media.conversion.convert_vectors ) { return false; } diff --git a/classes/plugin/loader.ts b/classes/plugin/loader.ts index ced3f0a4..0541e973 100644 --- a/classes/plugin/loader.ts +++ b/classes/plugin/loader.ts @@ -5,7 +5,7 @@ import chalk from "chalk"; import { parseJSON5, parseJSONC } from "confbox"; import type { ZodTypeAny } from "zod"; import { type ValidationError, fromZodError } from "zod-validation-error"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; import { Plugin } from "~/packages/plugin-kit/plugin"; import { type Manifest, manifestSchema } from "~/packages/plugin-kit/schema"; import type { HonoEnv } from "~/types/api"; @@ -230,10 +230,13 @@ export class PluginLoader { config.plugins?.config?.[data.manifest.name], ); } catch (e) { - logger.fatal`Plugin configuration is invalid: ${chalk.redBright(e as ValidationError)}`; + logger.fatal`Error encountered while loading plugin ${chalk.blueBright(data.manifest.name)} ${chalk.blueBright(data.manifest.version)} configuration.`; + logger.fatal`This is due to invalid, missing or incomplete configuration.`; logger.fatal`Put your configuration at ${chalk.blueBright( "plugins.config.", )}`; + logger.fatal`Here is the error message, please fix the configuration file accordingly:`; + logger.fatal`${(e as ValidationError).message}`; await Bun.sleep(Number.POSITIVE_INFINITY); } diff --git a/classes/schemas/account.ts b/classes/schemas/account.ts index 32cb9bc2..4ea23242 100644 --- a/classes/schemas/account.ts +++ b/classes/schemas/account.ts @@ -1,8 +1,8 @@ import { userAddressValidator } from "@/api.ts"; import { z } from "@hono/zod-openapi"; import type { Account as ApiAccount } from "@versia/client/types"; -import { config } from "~/packages/config-manager"; -import { zBoolean } from "~/packages/config-manager/config.type"; +import { zBoolean } from "~/classes/schemas/common.ts"; +import { config } from "~/config.ts"; import { iso631 } from "./common.ts"; import { CustomEmoji } from "./emoji.ts"; import { Role } from "./versia.ts"; @@ -12,7 +12,7 @@ export const Field = z.object({ .string() .trim() .min(1) - .max(config.validation.max_field_name_size) + .max(config.validation.accounts.max_field_name_characters) .openapi({ description: "The key of a given field’s key-value pair.", example: "Freak level", @@ -24,7 +24,7 @@ export const Field = z.object({ .string() .trim() .min(1) - .max(config.validation.max_field_value_size) + .max(config.validation.accounts.max_field_value_characters) .openapi({ description: "The value associated with the name key.", example: "

High

", @@ -87,9 +87,12 @@ export const Source = z .string() .trim() .min(0) - .max(config.validation.max_bio_size) + .max(config.validation.accounts.max_bio_characters) .refine( - (s) => !config.filters.bio.some((filter) => s.match(filter)), + (s) => + !config.validation.filters.bio.some((filter) => + filter.test(s), + ), "Bio contains blocked words", ) .openapi({ @@ -99,9 +102,12 @@ export const Source = z url: "https://docs.joinmastodon.org/entities/Account/#source-note", }, }), - fields: z.array(Field).max(config.validation.max_field_count).openapi({ - description: "Metadata about the account.", - }), + fields: z + .array(Field) + .max(config.validation.accounts.max_field_count) + .openapi({ + description: "Metadata about the account.", + }), }) .openapi({ description: @@ -126,15 +132,25 @@ export const Account = z.object({ .string() .min(3) .trim() - .max(config.validation.max_username_size) + .max(config.validation.accounts.max_username_characters) .regex( /^[a-z0-9_-]+$/, "Username can only contain letters, numbers, underscores and hyphens", ) .refine( - (s) => !config.filters.username.some((filter) => s.match(filter)), + (s) => + !config.validation.filters.username.some((filter) => + filter.test(s), + ), "Username contains blocked words", ) + .refine( + (s) => + !config.validation.accounts.disallowed_usernames.some((u) => + u.test(s), + ), + "Username is disallowed", + ) .openapi({ description: "The username of the account, not including domain.", example: "lexi", @@ -169,10 +185,12 @@ export const Account = z.object({ .string() .min(3) .trim() - .max(config.validation.max_displayname_size) + .max(config.validation.accounts.max_displayname_characters) .refine( (s) => - !config.filters.displayname.some((filter) => s.match(filter)), + !config.validation.filters.displayname.some((filter) => + filter.test(s), + ), "Display name contains blocked words", ) .openapi({ @@ -185,10 +203,11 @@ export const Account = z.object({ note: z .string() .min(0) - .max(config.validation.max_bio_size) + .max(config.validation.accounts.max_bio_characters) .trim() .refine( - (s) => !config.filters.bio.some((filter) => s.match(filter)), + (s) => + !config.validation.filters.bio.some((filter) => filter.test(s)), "Bio contains blocked words", ) .openapi({ @@ -255,7 +274,7 @@ export const Account = z.object({ }), fields: z .array(Field) - .max(config.validation.max_field_count) + .max(config.validation.accounts.max_field_count) .openapi({ description: "Additional metadata attached to a profile as name-value pairs.", diff --git a/classes/schemas/attachment.ts b/classes/schemas/attachment.ts index 215d6d20..cbea78f3 100644 --- a/classes/schemas/attachment.ts +++ b/classes/schemas/attachment.ts @@ -1,5 +1,5 @@ import { z } from "@hono/zod-openapi"; -import { config } from "~/packages/config-manager/index.ts"; +import { config } from "~/config.ts"; import { Id } from "./common.ts"; export const Attachment = z @@ -54,7 +54,7 @@ export const Attachment = z description: z .string() .trim() - .max(config.validation.max_media_description_size) + .max(config.validation.media.max_description_characters) .nullable() .openapi({ description: diff --git a/classes/schemas/common.ts b/classes/schemas/common.ts index d3a40c18..41bc0b7b 100644 --- a/classes/schemas/common.ts +++ b/classes/schemas/common.ts @@ -4,3 +4,8 @@ import ISO6391 from "iso-639-1"; export const Id = z.string().uuid(); export const iso631 = z.enum(ISO6391.getAllCodes() as [string, ...string[]]); + +export const zBoolean = z + .string() + .transform((v) => ["true", "1", "on"].includes(v.toLowerCase())) + .or(z.boolean()); diff --git a/classes/schemas/emoji.ts b/classes/schemas/emoji.ts index 6fcc0f5d..5b38aaa0 100644 --- a/classes/schemas/emoji.ts +++ b/classes/schemas/emoji.ts @@ -1,7 +1,7 @@ import { emojiValidator } from "@/api.ts"; import { z } from "@hono/zod-openapi"; -import { zBoolean } from "~/packages/config-manager/config.type"; -import { config } from "~/packages/config-manager/index.ts"; +import { zBoolean } from "~/classes/schemas/common.ts"; +import { config } from "~/config.ts"; import { Id } from "./common.ts"; export const CustomEmoji = z @@ -15,7 +15,7 @@ export const CustomEmoji = z .string() .trim() .min(1) - .max(config.validation.max_emoji_shortcode_size) + .max(config.validation.emojis.max_shortcode_characters) .regex( emojiValidator, "Shortcode must only contain letters (any case), numbers, dashes or underscores.", @@ -77,7 +77,7 @@ export const CustomEmoji = z /* Versia Server API extension */ description: z .string() - .max(config.validation.max_emoji_description_size) + .max(config.validation.emojis.max_description_characters) .nullable() .openapi({ description: diff --git a/classes/schemas/filters.ts b/classes/schemas/filters.ts index e1e57de5..edc75200 100644 --- a/classes/schemas/filters.ts +++ b/classes/schemas/filters.ts @@ -1,6 +1,5 @@ import { z } from "@hono/zod-openapi"; -import { zBoolean } from "~/packages/config-manager/config.type.ts"; -import { Id } from "./common.ts"; +import { Id, zBoolean } from "./common.ts"; export const FilterStatus = z .object({ diff --git a/classes/schemas/poll.ts b/classes/schemas/poll.ts index 37064b67..92a0c35b 100644 --- a/classes/schemas/poll.ts +++ b/classes/schemas/poll.ts @@ -1,5 +1,5 @@ import { z } from "@hono/zod-openapi"; -import { config } from "~/packages/config-manager/index.ts"; +import { config } from "~/config.ts"; import { Id } from "./common.ts"; import { CustomEmoji } from "./emoji.ts"; @@ -9,7 +9,7 @@ export const PollOption = z .string() .trim() .min(1) - .max(config.validation.max_poll_option_size) + .max(config.validation.polls.max_option_characters) .openapi({ description: "The text value of the poll option.", example: "yes", diff --git a/classes/schemas/status.ts b/classes/schemas/status.ts index dbbb1c0e..ffb40710 100644 --- a/classes/schemas/status.ts +++ b/classes/schemas/status.ts @@ -1,11 +1,10 @@ import { z } from "@hono/zod-openapi"; import type { Status as ApiNote } from "@versia/client/types"; -import { zBoolean } from "~/packages/config-manager/config.type.ts"; -import { config } from "~/packages/config-manager/index.ts"; +import { config } from "~/config.ts"; import { Account } from "./account.ts"; import { Attachment } from "./attachment.ts"; import { PreviewCard } from "./card.ts"; -import { Id, iso631 } from "./common.ts"; +import { Id, iso631, zBoolean } from "./common.ts"; import { CustomEmoji } from "./emoji.ts"; import { FilterResult } from "./filters.ts"; import { Poll } from "./poll.ts"; @@ -58,12 +57,12 @@ export const StatusSource = z }), text: z .string() - .max(config.validation.max_note_size) + .max(config.validation.notes.max_characters) .trim() .refine( (s) => - !config.filters.note_content.some((filter) => - s.match(filter), + !config.validation.filters.note_content.some((filter) => + filter.test(s), ), "Status contains blocked words", ) diff --git a/classes/schemas/versia.ts b/classes/schemas/versia.ts index ef8d5cd1..a293d580 100644 --- a/classes/schemas/versia.ts +++ b/classes/schemas/versia.ts @@ -1,6 +1,6 @@ import { z } from "@hono/zod-openapi"; import { RolePermission } from "@versia/client/types"; -import { config } from "~/packages/config-manager/index.ts"; +import { config } from "~/config.ts"; import { Id } from "./common.ts"; /* Versia Server API extension */ @@ -56,7 +56,7 @@ export const NoteReaction = z name: z .string() .min(1) - .max(config.validation.max_emoji_shortcode_size) + .max(config.validation.emojis.max_shortcode_characters) .trim() .openapi({ description: "Custom Emoji shortcode or Unicode emoji.", diff --git a/classes/search/search-manager.ts b/classes/search/search-manager.ts index fe262f51..25689381 100644 --- a/classes/search/search-manager.ts +++ b/classes/search/search-manager.ts @@ -10,7 +10,7 @@ import { Ingest as SonicChannelIngest, Search as SonicChannelSearch, } from "sonic-channel"; -import { type Config, config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; /** * Enum for Sonic index types @@ -32,17 +32,21 @@ export class SonicSearchManager { /** * @param config Configuration for Sonic */ - public constructor(private config: Config) { + public constructor() { + if (!config.search.sonic) { + throw new Error("Sonic configuration is missing"); + } + this.searchChannel = new SonicChannelSearch({ - host: config.sonic.host, - port: config.sonic.port, - auth: config.sonic.password, + host: config.search.sonic.host, + port: config.search.sonic.port, + auth: config.search.sonic.password, }); this.ingestChannel = new SonicChannelIngest({ - host: config.sonic.host, - port: config.sonic.port, - auth: config.sonic.password, + host: config.search.sonic.host, + port: config.search.sonic.port, + auth: config.search.sonic.password, }); } @@ -50,7 +54,7 @@ export class SonicSearchManager { * Connect to Sonic */ public async connect(silent = false): Promise { - if (!this.config.sonic.enabled) { + if (!config.search.enabled) { !silent && this.logger.info`Sonic search is disabled`; return; } @@ -127,7 +131,7 @@ export class SonicSearchManager { * @param user User to add */ public async addUser(user: User): Promise { - if (!this.config.sonic.enabled) { + if (!config.search.enabled) { return; } @@ -310,4 +314,4 @@ export class SonicSearchManager { } } -export const searchManager = new SonicSearchManager(config); +export const searchManager = new SonicSearchManager(); diff --git a/classes/workers/delivery.ts b/classes/workers/delivery.ts index fde56634..2764e655 100644 --- a/classes/workers/delivery.ts +++ b/classes/workers/delivery.ts @@ -1,7 +1,7 @@ import { User } from "@versia/kit/db"; import { Worker } from "bullmq"; import chalk from "chalk"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; import { connection } from "~/utils/redis.ts"; import { type DeliveryJobData, @@ -52,10 +52,10 @@ export const getDeliveryWorker = (): Worker< { connection, removeOnComplete: { - age: config.queues.delivery.remove_on_complete, + age: config.queues.delivery?.remove_after_complete_seconds, }, removeOnFail: { - age: config.queues.delivery.remove_on_failure, + age: config.queues.delivery?.remove_after_failure_seconds, }, }, ); diff --git a/classes/workers/fetch.ts b/classes/workers/fetch.ts index f29cb366..da834227 100644 --- a/classes/workers/fetch.ts +++ b/classes/workers/fetch.ts @@ -2,7 +2,7 @@ import { Instance } from "@versia/kit/db"; import { Instances } from "@versia/kit/tables"; import { Worker } from "bullmq"; import { eq } from "drizzle-orm"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; import { connection } from "~/utils/redis.ts"; import { type FetchJobData, @@ -52,10 +52,10 @@ export const getFetchWorker = (): Worker => { connection, removeOnComplete: { - age: config.queues.fetch.remove_on_complete, + age: config.queues.fetch?.remove_after_complete_seconds, }, removeOnFail: { - age: config.queues.fetch.remove_on_failure, + age: config.queues.fetch?.remove_after_failure_seconds, }, }, ); diff --git a/classes/workers/inbox.ts b/classes/workers/inbox.ts index 37521ad7..ea8c9936 100644 --- a/classes/workers/inbox.ts +++ b/classes/workers/inbox.ts @@ -1,7 +1,7 @@ import { getLogger } from "@logtape/logtape"; import { Instance, User } from "@versia/kit/db"; import { Worker } from "bullmq"; -import { config } from "~/packages/config-manager/index.ts"; +import { config } from "~/config.ts"; import { connection } from "~/utils/redis.ts"; import { InboxProcessor } from "../inbox/processor.ts"; import { @@ -168,10 +168,10 @@ export const getInboxWorker = (): Worker => { connection, removeOnComplete: { - age: config.queues.inbox.remove_on_complete, + age: config.queues.inbox?.remove_after_complete_seconds, }, removeOnFail: { - age: config.queues.inbox.remove_on_failure, + age: config.queues.inbox?.remove_after_failure_seconds, }, }, ); diff --git a/classes/workers/media.ts b/classes/workers/media.ts index cb628616..63277366 100644 --- a/classes/workers/media.ts +++ b/classes/workers/media.ts @@ -1,6 +1,6 @@ import { Media } from "@versia/kit/db"; import { Worker } from "bullmq"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; import { connection } from "~/utils/redis.ts"; import { calculateBlurhash } from "../media/preprocessors/blurhash.ts"; import { convertImage } from "../media/preprocessors/image-conversion.ts"; @@ -100,10 +100,10 @@ export const getMediaWorker = (): Worker => { connection, removeOnComplete: { - age: config.queues.media.remove_on_complete, + age: config.queues.media?.remove_after_complete_seconds, }, removeOnFail: { - age: config.queues.media.remove_on_failure, + age: config.queues.media?.remove_after_failure_seconds, }, }, ); diff --git a/classes/workers/push.ts b/classes/workers/push.ts index 82c10cd6..09b05f57 100644 --- a/classes/workers/push.ts +++ b/classes/workers/push.ts @@ -2,7 +2,7 @@ import { htmlToText } from "@/content_types.ts"; import { Note, PushSubscription, Token, User } from "@versia/kit/db"; import { Worker } from "bullmq"; import { sendNotification } from "web-push"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; import { connection } from "~/utils/redis.ts"; import { type PushJobData, @@ -18,6 +18,11 @@ export const getPushWorker = (): Worker => data: { psId, relatedUserId, type, noteId, notificationId }, } = job; + if (!config.notifications.push) { + await job.log("Push notifications are disabled"); + return; + } + await job.log( `Sending push notification for note [${notificationId}]`, ); @@ -105,17 +110,18 @@ export const getPushWorker = (): Worker => preferred_locale: "en-US", notification_id: notificationId, notification_type: type, - icon: relatedUser.getAvatarUrl(config), + icon: relatedUser.getAvatarUrl(), title, body: truncate(body, 140), }), { vapidDetails: { subject: - config.notifications.push.vapid.subject || + config.notifications.push.subject || config.http.base_url.origin, - privateKey: config.notifications.push.vapid.private, - publicKey: config.notifications.push.vapid.public, + privateKey: + config.notifications.push.vapid_keys.private, + publicKey: config.notifications.push.vapid_keys.public, }, contentEncoding: "aesgcm", }, @@ -128,10 +134,10 @@ export const getPushWorker = (): Worker => { connection, removeOnComplete: { - age: config.queues.push.remove_on_complete, + age: config.queues.push?.remove_after_complete_seconds, }, removeOnFail: { - age: config.queues.push.remove_on_failure, + age: config.queues.push?.remove_after_failure_seconds, }, }, ); diff --git a/cli/commands/emoji/add.ts b/cli/commands/emoji/add.ts index 4a0dae16..ee00a209 100644 --- a/cli/commands/emoji/add.ts +++ b/cli/commands/emoji/add.ts @@ -5,7 +5,7 @@ import chalk from "chalk"; import { and, eq, isNull } from "drizzle-orm"; import ora from "ora"; import { BaseCommand } from "~/cli/base"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; export default class EmojiAdd extends BaseCommand { public static override args = { @@ -62,7 +62,7 @@ export default class EmojiAdd extends BaseCommand { "Accept-Encoding": "identity", }, // @ts-expect-error Proxy is a Bun-specific feature - proxy: config.http.proxy.address, + proxy: config.http.proxy_address, }); if (!response.ok) { diff --git a/cli/commands/emoji/import.ts b/cli/commands/emoji/import.ts index f4c6c201..c0140551 100644 --- a/cli/commands/emoji/import.ts +++ b/cli/commands/emoji/import.ts @@ -7,7 +7,7 @@ import { lookup } from "mime-types"; import ora from "ora"; import { unzip } from "unzipit"; import { BaseCommand } from "~/cli/base"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; type MetaType = { emojis: { @@ -69,7 +69,7 @@ export default class EmojiImport extends BaseCommand { "Accept-Encoding": "identity", }, // @ts-expect-error Proxy is a Bun-specific feature - proxy: config.http.proxy.address, + proxy: config.http.proxy_address, }); if (!response.ok) { diff --git a/cli/commands/index/rebuild.ts b/cli/commands/index/rebuild.ts index bf79a4e5..bedb68f1 100644 --- a/cli/commands/index/rebuild.ts +++ b/cli/commands/index/rebuild.ts @@ -2,7 +2,7 @@ import { Args, Flags } from "@oclif/core"; import ora from "ora"; import { SonicIndexType, searchManager } from "~/classes/search/search-manager"; import { BaseCommand } from "~/cli/base"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; export default class IndexRebuild extends BaseCommand { public static override args = { @@ -28,8 +28,8 @@ export default class IndexRebuild extends BaseCommand { public async run(): Promise { const { flags, args } = await this.parse(IndexRebuild); - if (!config.sonic.enabled) { - this.error("Sonic search is disabled"); + if (!config.search.enabled) { + this.error("Search is disabled"); this.exit(1); } diff --git a/cli/commands/user/reset.ts b/cli/commands/user/reset.ts index f90049b3..01117b65 100644 --- a/cli/commands/user/reset.ts +++ b/cli/commands/user/reset.ts @@ -4,7 +4,7 @@ import chalk from "chalk"; import { renderUnicodeCompact } from "uqr"; import { UserFinderCommand } from "~/cli/classes"; import { formatArray } from "~/cli/utils/format"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; export default class UserReset extends UserFinderCommand { public static override description = "Resets users' passwords"; diff --git a/config.ts b/config.ts new file mode 100644 index 00000000..75a265ec --- /dev/null +++ b/config.ts @@ -0,0 +1,50 @@ +/** + * @file config.ts + * @summary Config system to retrieve and modify system configuration + * @description Can read from a hand-written file, config.toml, or from a machine-saved file, config.internal.toml + * Fuses both and provides a way to retrieve individual values + */ + +import { file } from "bun"; +import { loadConfig, watchConfig } from "c12"; +import chalk from "chalk"; +import type { z } from "zod"; +import { fromZodError } from "zod-validation-error"; +import { ConfigSchema } from "./classes/config/schema.ts"; + +if (!(await file("config/config.toml").exists())) { + throw new Error("config.toml does not or is not accessible."); +} + +const { config } = await watchConfig>({ + configFile: "./config/config.toml", + overrides: + ( + await loadConfig>({ + configFile: "./config/config.internal.toml", + }) + ).config ?? undefined, +}); + +const parsed = await ConfigSchema.safeParseAsync(config); + +if (!parsed.success) { + console.error( + `âš  Error encountered while loading ${chalk.gray("config.toml")}.`, + ); + console.error( + "âš  This is due to invalid, missing or incorrect values in the configuration file.", + ); + console.error( + "âš  Here is the error message, please fix the configuration file accordingly:", + ); + const errorMessage = fromZodError(parsed.error).message; + + console.info(errorMessage); + + throw new Error("Configuration file is invalid."); +} + +const exportedConfig = parsed.data; + +export { exportedConfig as config }; diff --git a/config/config.example.toml b/config/config.example.toml index f4b52460..4fcc9289 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -1,16 +1,20 @@ # You can change the URL to the commit/tag you are using #:schema https://raw.githubusercontent.com/versia-pub/server/main/config/config.schema.json -[database] -# Main PostgreSQL database connection +# All values marked as "sensitive" can be set to "PATH:/path/to/file" to read the value from a file (e.g. a secret manager) + + +[postgres] +# PostgreSQL database configuration host = "localhost" port = 5432 username = "versia" +# Sensitive value password = "mycoolpassword" database = "versia" -# Add any eventual read-only database replicas here -# [[database.replicas]] +# Additional read-only replicas +# [[postgres.replicas]] # host = "other-host" # port = 5432 # username = "versia" @@ -18,45 +22,48 @@ database = "versia" # database = "replica1" [redis.queue] -# Redis instance for storing the federation queue +# A Redis database used for managing queues. # Required for federation host = "localhost" port = 6379 -password = "" +# Sensitive value +# password = "test" database = 0 -[redis.cache] -# Redis instance to be used as a timeline cache +# A Redis database used for caching SQL queries. # Optional, can be the same as the queue instance -host = "localhost" -port = 6380 -password = "" -database = 1 +# [redis.cache] +# host = "localhost" +# port = 6380 +# database = 1 +# password = "" + +# Search and indexing configuration +[search] +# Enable indexing and searching? enabled = false -[sonic] -# If Sonic is not configured, search will not be enabled -host = "localhost" -port = 7700 -password = "" -enabled = true - -[signups] -# Whether to enable registrations or not -registration = true -rules = [ - "Do not harass others", - "Be nice to people", - "Don't spam", - "Don't post illegal content", -] +# Optional if search is disabled +# [search.sonic] +# host = "localhost" +# port = 7700 +# Sensitive value +# password = "test" + +[registration] +# Can users sign up freely? +allow = true +# NOT IMPLEMENTED +require_approval = false +# Message to show to users when registration is disabled +# message = "ran out of spoons to moderate registrations, sorry" [http] -# The full URL Versia Server will be reachable by (paths are not supported) -base_url = "https://versia.localhost:9900" +# URL that the instance will be accessible at +base_url = "https://example.com" # Address to bind to (0.0.0.0 is suggested for proxies) -bind = "versia.localhost" -bind_port = 9900 +bind = "0.0.0.0" +bind_port = 8080 # Bans IPv4 or IPv6 IPs (wildcards, networks and ranges are supported) banned_ips = [] @@ -66,29 +73,17 @@ banned_user_agents = [ # "wget\/1.20.3", ] -[http.proxy] -# For HTTP proxies (e.g. Tor proxies) +# URL to an eventual HTTP proxy # Will be used for all outgoing requests -enabled = false -address = "http://localhost:8118" +# proxy_address = "http://localhost:8118" -[http.tls] -# If these values are set, Versia Server will use these files for TLS -enabled = false -key = "" -cert = "" -passphrase = "" -ca = "" - -[http.bait] -# Enable the bait feature (sends fake data to those who are flagged) -enabled = false -# Path to file of bait data (if not provided, Versia Server will send the entire Bee Movie script) -send_file = "" -# IPs to send bait data to (wildcards, networks and ranges are supported) -bait_ips = ["127.0.0.1", "::1"] -# User agents to send bait data to (regex format) -bait_user_agents = ["curl", "wget"] +# TLS configuration. You should probably be using a reverse proxy instead of this +# [http.tls] +# key = "/path/to/key.pem" +# cert = "/path/to/cert.pem" +# Sensitive value +# passphrase = "awawa" +# ca = "/path/to/ca.pem" [frontend] # Enable custom frontends (warning: not enabling this will make Versia Server only accessible via the Mastodon API) @@ -112,27 +107,29 @@ url = "http://localhost:3000" # This can be used to set up custom themes, etc on supported frontends. # theme = "dark" -[smtp] +# NOT IMPLEMENTED +[email] +# Enable email sending +send_emails = false + +# If send_emails is true, the following settings are required +# [email.smtp] # SMTP server to use for sending emails -server = "smtp.example.com" -port = 465 -username = "test@example.com" -password = "password123" -tls = true -# Disable all email functions (this will allow people to sign up without verifying -# their email) -enabled = false +# server = "smtp.example.com" +# port = 465 +# username = "test@example.com" +# Sensitive value +# password = "password123" +# tls = true [media] # Can be "s3" or "local", where "local" uploads the file to the local filesystem # Changing this value will not retroactively apply to existing data # Don't forget to fill in the s3 config :3 backend = "s3" -# Whether to check the hash of media when uploading to avoid duplication -deduplicate_media = true # If media backend is "local", this is the folder where the files will be stored # Can be any path -local_uploads_folder = "uploads" +uploads_path = "uploads" [media.conversion] # Whether to automatically convert images to another format on upload @@ -141,41 +138,30 @@ convert_images = true # JXL support will likely not work convert_to = "image/webp" # Also convert SVG images? -convert_vector = false +convert_vectors = false # [s3] # Can be left commented if you don't use the S3 media backend -# endpoint = "" +# endpoint = "https://s3.example.com" +# Sensitive value # access_key = "XXXXX" +# Sensitive value # secret_access_key = "XXX" -# region = "" +# region = "us-east-1" # bucket_name = "versia" # public_url = "https://cdn.example.com" [validation] # Checks user data # Does not retroactively apply to previously entered data -max_displayname_size = 50 # Character length -max_bio_size = 5000 -max_note_size = 5000 -max_avatar_size = 5_000_000 # Bytes -max_header_size = 5_000_000 -max_media_size = 40_000_000 -max_media_attachments = 10 -max_media_description_size = 1000 -max_emoji_size = 1000000 -max_emoji_shortcode_size = 100 -max_emoji_description_size = 1000 -max_poll_options = 20 -max_poll_option_size = 500 -min_poll_duration = 60 # Seconds -max_poll_duration = 1893456000 -max_username_size = 30 -max_field_count = 10 -max_field_name_size = 1000 -max_field_value_size = 1000 -# Forbidden usernames, defaults are from Akkoma -username_blacklist = [ +[validation.accounts] +max_displayname_characters = 50 +max_username_characters = 30 +max_bio_characters = 5000 +max_avatar_bytes = 5_000_000 +max_header_bytes = 5_000_000 +# Regex is allowed here +disallowed_usernames = [ "well-known", "about", "activities", @@ -202,12 +188,14 @@ username_blacklist = [ "search", "mfa", ] -# Whether to blacklist known temporary email providers -blacklist_tempmail = false -# Additional email providers to blacklist (list of domains) -email_blacklist = [] -# Valid URL schemes, otherwise the URL is parsed as text -url_scheme_whitelist = [ +max_field_count = 10 +max_field_name_characters = 1000 +max_field_value_characters = 1000 +max_pinned_notes = 20 + +[validation.notes] +max_characters = 5000 +allowed_url_schemes = [ "http", "https", "ftp", @@ -226,39 +214,71 @@ url_scheme_whitelist = [ "ssb", "gemini", ] -# Only allow those MIME types of data to be uploaded -# This can easily be spoofed, but if it is spoofed it will appear broken -# to normal clients until despoofed -enforce_mime_types = false -# Defaults to all valid MIME types -# allowed_mime_types = [] - -[validation.challenges] +max_attachments = 16 + +[validation.media] +max_bytes = 40_000_000 +max_description_characters = 1000 +# An empty array allows all MIME types +allowed_mime_types = [] + +[validation.emojis] +max_bytes = 1_000_000 +max_shortcode_characters = 100 +max_description_characters = 1000 + +[validation.polls] +max_options = 20 +max_option_characters = 500 +min_duration_seconds = 60 +# 100 days +max_duration_seconds = 8_640_000 + +[validation.emails] +# Blocks over 10,000 common tempmail domains +disallow_tempmail = false +# Regex is allowed here +disallowed_domains = [] + +# [validation.challenges] # "Challenges" (aka captchas) are a way to verify that a user is human -# Versia Server's challenges use no external services, and are Proof of Work based +# Versia Server's challenges use no external services, and are proof-of-work based # This means that they do not require any user interaction, instead # they require the user's computer to do a small amount of work -enabled = false # The difficulty of the challenge, higher is will take more time to solve -difficulty = 50000 +# difficulty = 50000 # Challenge expiration time in seconds -expiration = 300 # 5 minutes +# expiration = 300 # 5 minutes # Leave this empty to generate a new key -key = "" +# Sensitive value +# key = "" -[notifications] +# Block content that matches these regular expressions +[validation.filters] +note_content = [ + # "(https?://)?(www\\.)?youtube\\.com/watch\\?v=[a-zA-Z0-9_-]+", + # "(https?://)?(www\\.)?youtu\\.be/[a-zA-Z0-9_-]+", +] +emoji_shortcode = [] +username = [] +displayname = [] +bio = [] -[notifications.push] -# Whether to enable push notifications -enabled = true +[notifications] -[notifications.push.vapid] +# Web Push Notifications configuration. +# Leave out to disable. +# [notifications.push] +# Subject field embedded in the push notification +# subject = "mailto:joe@example.com" +# +# [notifications.push.vapid_keys] # VAPID keys for push notifications # Run Versia Server with those values missing to generate new keys -public = "" -private = "" -# Optional -# subject = "mailto:joe@example.com" +# Sensitive value +# public = "" +# Sensitive value +# private = "" [defaults] # Default visibility for new notes @@ -278,37 +298,37 @@ placeholder_style = "thumbs" # Controls the delivery queue (for outbound federation) [queues.delivery] # Time in seconds to remove completed jobs -remove_on_complete = 31536000 +remove_after_complete_seconds = 31536000 # Time in seconds to remove failed jobs -remove_on_failure = 31536000 +remove_after_failure_seconds = 31536000 # Controls the inbox processing queue (for inbound federation) [queues.inbox] # Time in seconds to remove completed jobs -remove_on_complete = 31536000 +remove_after_complete_seconds = 31536000 # Time in seconds to remove failed jobs -remove_on_failure = 31536000 +remove_after_failure_seconds = 31536000 # Controls the fetch queue (for remote data refreshes) [queues.fetch] # Time in seconds to remove completed jobs -remove_on_complete = 31536000 +remove_after_complete_seconds = 31536000 # Time in seconds to remove failed jobs -remove_on_failure = 31536000 +remove_after_failure_seconds = 31536000 # Controls the push queue (for push notification delivery) [queues.push] # Time in seconds to remove completed jobs -remove_on_complete = 31536000 +remove_after_complete_seconds = 31536000 # Time in seconds to remove failed jobs -remove_on_failure = 31536000 +remove_after_failure_seconds = 31536000 # Controls the media queue (for media processing) [queues.media] # Time in seconds to remove completed jobs -remove_on_complete = 31536000 +remove_after_complete_seconds = 31536000 # Time in seconds to remove failed jobs -remove_on_failure = 31536000 +remove_after_failure_seconds = 31536000 [federation] # This is a list of domain names, such as "mastodon.social" or "pleroma.site" @@ -335,39 +355,58 @@ avatars = [] # For bridge software, such as versia-pub/activitypub # Bridges must be hosted separately from the main Versia Server process -[federation.bridge] -enabled = false +# [federation.bridge] # Only versia-ap exists for now -software = "versia-ap" +# software = "versia-ap" # If this is empty, any bridge with the correct token # will be able to send data to your instance -allowed_ips = ["192.168.1.0/24"] +# v4, v6, ranges and wildcards are supported +# allowed_ips = ["192.168.1.0/24"] # Token for the bridge software # Bridge must have the same token! -token = "mycooltoken" -url = "https://ap.versia.social" +# Sensitive value +# token = "mycooltoken" +# url = "https://ap.versia.social" [instance] name = "Versia" description = "A Versia Server instance" -# Path to a file containing a longer description of your instance -# This will be parsed as Markdown + +# Paths to instance long description, terms of service, and privacy policy +# These will be parsed as Markdown +# # extended_description_path = "config/extended_description.md" -# Path to a file containing the terms of service of your instance -# This will be parsed as Markdown # tos_path = "config/tos.md" -# Path to a file containing the privacy policy of your instance -# This will be parsed as Markdown # privacy_policy_path = "config/privacy_policy.md" -# URL to your instance logo -# logo = "" -# URL to your instance banner -# banner = "" + +# Primary instance languages. ISO 639-1 codes. +languages = ["en"] + +[instance.contact] +# email = "staff@yourinstance.com" + +[instance.branding] +# logo = "https://cdn.example.com/logo.png" +# banner = "https://cdn.example.com/banner.png" # Used for federation. If left empty or missing, the server will generate one for you. -[instance.keys] -public = "" -private = "" +# [instance.keys] +# Sensitive value +# public = "" +# Sensitive value +# private = "" + +[[instance.rules]] +# Short description of the rule +text = "No hate speech" +# Longer version of the rule with additional information +hint = "Hate speech includes slurs, threats, and harassment." + +[[instance.rules]] +text = "No spam" + +# [[instance.rules]] +# ...etc [permissions] # Control default permissions for users @@ -385,67 +424,34 @@ private = "" # Defaults to being able to manage all instance data, content, and users # admin = [] - -[filters] -# Regex filters for federated and local data -# Drops data matching the filters -# Does not apply retroactively to existing data - -# Note contents -note_content = [ - # "(https?://)?(www\\.)?youtube\\.com/watch\\?v=[a-zA-Z0-9_-]+", - # "(https?://)?(www\\.)?youtu\\.be/[a-zA-Z0-9_-]+", -] -emoji = [] -# These will drop users matching the filters -username = [] -displayname = [] -bio = [] - [logging] -# Log all requests (warning: this is a lot of data) -log_requests = false -# Log request and their contents (warning: this is a lot of data) -log_requests_verbose = false + # Available levels: debug, info, warning, error, fatal log_level = "debug" -# For GDPR compliance, you can disable logging of IPs -log_ip = false -# Log all filtered objects -log_filters = true +log_file_path = "logs/versia.log" -[logging.sentry] -# Whether to enable https://sentry.io error logging -enabled = false +[logging.types] +# Either pass a boolean +# requests = true +# Or a table with the following keys: +# requests_content = { level = "debug", log_file_path = "logs/requests.log" } +# Available types are: requests, responses, requests_content, filters + +# https://sentry.io support +# Uncomment to enable +# [logging.sentry] # Sentry DSN for error logging -dsn = "" -debug = false +# dsn = "https://example.com" +# debug = false -sample_rate = 1.0 -traces_sample_rate = 1.0 +# sample_rate = 1.0 +# traces_sample_rate = 1.0 # Can also be regex -trace_propagation_targets = [] -max_breadcrumbs = 100 +# trace_propagation_targets = [] +# max_breadcrumbs = 100 # environment = "production" -[logging.storage] -# Path to logfile for requests -requests = "logs/requests.log" - -[ratelimits] -# These settings apply to every route at once -# Amount to multiply every route's duration by -duration_coeff = 1.0 -# Amount to multiply every route's max requests per [duration] by -max_coeff = 1.0 - -[ratelimits.custom] -# Add in any API route in this style here -# Applies before the global ratelimit changes -# "/api/v1/accounts/:id/block" = { duration = 30, max = 60 } -# "/api/v1/timelines/public" = { duration = 60, max = 200 } - [plugins] # Whether to automatically load all plugins in the plugins directory autoload = true @@ -481,5 +487,6 @@ allow_registration = true # This MUST match the provider's issuer URI, including the trailing slash (or lack thereof) # url = "https://id.cpluspatch.com/application/o/versia-testing/" # client_id = "XXXX" +# Sensitive value # client_secret = "XXXXX" # icon = "https://cpluspatch.com/images/icons/logo.svg" diff --git a/config/config.schema.json b/config/config.schema.json index cde891c5..988b1cd2 100644 --- a/config/config.schema.json +++ b/config/config.schema.json @@ -1,7 +1,7 @@ { "type": "object", "properties": { - "database": { + "postgres": { "type": "object", "properties": { "host": { @@ -21,6 +21,7 @@ }, "password": { "type": "string", + "description": "You can use PATH:/path/to/file to load this value from a file", "default": "" }, "database": { @@ -38,9 +39,7 @@ "minLength": 1 }, "port": { - "type": "integer", - "minimum": 1, - "maximum": 65535, + "$ref": "#/properties/postgres/properties/port", "default": 5432 }, "username": { @@ -48,8 +47,9 @@ "minLength": 1 }, "password": { - "type": "string", - "default": "" + "$ref": "#/properties/postgres/properties/password", + "default": "", + "description": "You can use PATH:/path/to/file to load this value from a file" }, "database": { "type": "string", @@ -57,13 +57,29 @@ "default": "versia" } }, - "required": ["host", "username"], + "required": [ + "host", + "port", + "username", + "password", + "database" + ], "additionalProperties": false - } + }, + "description": "Additional read-only replicas", + "default": [] } }, - "required": ["username"], - "additionalProperties": false + "required": [ + "host", + "port", + "username", + "password", + "database", + "replicas" + ], + "additionalProperties": false, + "description": "PostgreSQL database configuration" }, "redis": { "type": "object", @@ -77,27 +93,22 @@ "default": "localhost" }, "port": { - "type": "integer", - "minimum": 1, - "maximum": 65535, + "$ref": "#/properties/postgres/properties/port", "default": 6379 }, "password": { - "type": "string", - "default": "" + "$ref": "#/properties/postgres/properties/password", + "default": "", + "description": "You can use PATH:/path/to/file to load this value from a file" }, "database": { "type": "integer", "default": 0 } }, + "required": ["host", "port", "password", "database"], "additionalProperties": false, - "default": { - "host": "localhost", - "port": 6379, - "password": "", - "database": 0 - } + "description": "A Redis database used for managing queues." }, "cache": { "type": "object", @@ -108,76 +119,79 @@ "default": "localhost" }, "port": { - "type": "integer", - "minimum": 1, - "maximum": 65535, + "$ref": "#/properties/postgres/properties/port", "default": 6379 }, "password": { - "type": "string", - "default": "" + "$ref": "#/properties/postgres/properties/password", + "default": "", + "description": "You can use PATH:/path/to/file to load this value from a file" }, "database": { "type": "integer", "default": 1 - }, - "enabled": { - "type": "boolean", - "default": false } }, + "required": ["host", "port", "password", "database"], "additionalProperties": false, - "default": { - "host": "localhost", - "port": 6379, - "password": "", - "database": 1, - "enabled": false - } + "description": "A Redis database used for caching SQL queries. Optional." } }, - "additionalProperties": false + "required": ["queue", "cache"], + "additionalProperties": false, + "description": "Redis configuration. Used for queues and caching." }, - "sonic": { + "search": { "type": "object", "properties": { - "host": { - "type": "string", - "minLength": 1, - "default": "localhost" - }, - "port": { - "type": "integer", - "minimum": 1, - "maximum": 65535, - "default": 7700 - }, - "password": { - "type": "string" - }, "enabled": { "type": "boolean", - "default": false + "default": false, + "description": "Enable indexing and searching?" + }, + "sonic": { + "type": "object", + "properties": { + "host": { + "type": "string", + "minLength": 1, + "default": "localhost" + }, + "port": { + "$ref": "#/properties/postgres/properties/port", + "default": 7700 + }, + "password": { + "$ref": "#/properties/postgres/properties/password" + } + }, + "required": ["host", "port", "password"], + "additionalProperties": false, + "description": "Sonic database configuration" } }, - "required": ["password"], - "additionalProperties": false + "required": ["enabled", "sonic"], + "additionalProperties": false, + "description": "Search and indexing configuration" }, - "signups": { + "registration": { "type": "object", "properties": { - "registration": { + "allow": { "type": "boolean", - "default": true + "default": true, + "description": "Can users sign up freely?" }, - "rules": { - "type": "array", - "items": { - "type": "string" - }, - "default": [] + "require_approval": { + "type": "boolean", + "default": false + }, + "message": { + "type": "string", + "description": "Message to show to users when registration is disabled" } }, + "required": ["allow", "require_approval", "message"], "additionalProperties": false }, "http": { @@ -186,7 +200,7 @@ "base_url": { "type": "string", "minLength": 1, - "default": "http://versia.social" + "description": "URL that the instance will be accessible at" }, "bind": { "type": "string", @@ -194,116 +208,62 @@ "default": "0.0.0.0" }, "bind_port": { - "type": "integer", - "minimum": 1, - "maximum": 65535, + "$ref": "#/properties/postgres/properties/port", "default": 8080 }, "banned_ips": { "type": "array", "items": { - "type": "string" + "type": "string", + "description": "An IPv6/v4 address or CIDR range. Wildcards are also allowed" }, "default": [] }, "banned_user_agents": { "type": "array", "items": { - "type": "string" + "type": "string", + "description": "JavaScript regular expression" }, "default": [] }, - "proxy": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": false - }, - "address": { - "anyOf": [ - { - "type": "string", - "minLength": 1 - }, - { - "type": "string", - "const": "" - } - ] - } - }, - "required": ["address"], - "additionalProperties": false, - "default": { - "enabled": false, - "address": "" - } + "proxy_address": { + "$ref": "#/properties/http/properties/base_url", + "description": "URL to an eventual HTTP proxy" }, "tls": { "type": "object", "properties": { - "enabled": { - "type": "boolean", - "default": false - }, "key": { - "type": "string" + "type": "string", + "description": "This value must be a file path" }, "cert": { - "type": "string" + "$ref": "#/properties/http/properties/tls/properties/key" }, "passphrase": { - "type": "string" + "$ref": "#/properties/postgres/properties/password", + "description": "You can use PATH:/path/to/file to load this value from a file" }, "ca": { - "type": "string" - } - }, - "required": ["key", "cert"], - "additionalProperties": false, - "default": { - "enabled": false, - "key": "", - "cert": "", - "passphrase": "", - "ca": "" - } - }, - "bait": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "default": false - }, - "send_file": { - "type": "string" - }, - "bait_ips": { - "type": "array", - "items": { - "type": "string" - }, - "default": [] - }, - "bait_user_agents": { - "type": "array", - "items": { - "type": "string" - }, - "default": [] + "$ref": "#/properties/http/properties/tls/properties/key", + "description": "This value must be a file path" } }, + "required": ["key", "cert", "passphrase", "ca"], "additionalProperties": false, - "default": { - "enabled": false, - "send_file": "", - "bait_ips": [], - "bait_user_agents": [] - } + "description": "TLS configuration. You should probably be using a reverse proxy instead of this" } }, + "required": [ + "base_url", + "bind", + "bind_port", + "banned_ips", + "banned_user_agents", + "proxy_address", + "tls" + ], "additionalProperties": false }, "frontend": { @@ -314,7 +274,7 @@ "default": true }, "url": { - "$ref": "#/properties/http/properties/proxy/properties/address/anyOf/0", + "$ref": "#/properties/http/properties/proxy_address", "default": "http://localhost:3000" }, "routes": { @@ -342,14 +302,14 @@ "default": "/oauth/reset" } }, - "additionalProperties": false, - "default": { - "home": "/", - "login": "/oauth/authorize", - "consent": "/oauth/consent", - "register": "/register", - "password_reset": "/oauth/reset" - } + "required": [ + "home", + "login", + "consent", + "register", + "password_reset" + ], + "additionalProperties": false }, "settings": { "type": "object", @@ -357,53 +317,52 @@ "default": {} } }, - "additionalProperties": false, - "default": { - "enabled": true, - "url": "http://localhost:3000", - "settings": {} - } + "required": ["enabled", "url", "routes", "settings"], + "additionalProperties": false }, - "smtp": { + "email": { "type": "object", "properties": { - "server": { - "type": "string", - "minLength": 1 - }, - "port": { - "type": "integer", - "minimum": 1, - "maximum": 65535, - "default": 465 - }, - "username": { - "type": "string", - "minLength": 1 - }, - "password": { - "type": "string", - "minLength": 1 - }, - "tls": { - "type": "boolean", - "default": true - }, - "enabled": { + "send_emails": { "type": "boolean", "default": false + }, + "smtp": { + "type": "object", + "properties": { + "server": { + "type": "string", + "minLength": 1 + }, + "port": { + "$ref": "#/properties/postgres/properties/port", + "default": 465 + }, + "username": { + "type": "string", + "minLength": 1 + }, + "password": { + "$ref": "#/properties/postgres/properties/password", + "description": "You can use PATH:/path/to/file to load this value from a file" + }, + "tls": { + "type": "boolean", + "default": true + } + }, + "required": [ + "server", + "port", + "username", + "password", + "tls" + ], + "additionalProperties": false } }, - "required": ["server", "username"], - "additionalProperties": false, - "default": { - "server": "", - "port": 465, - "username": "", - "password": "", - "tls": true, - "enabled": false - } + "required": ["send_emails", "smtp"], + "additionalProperties": false }, "media": { "type": "object", @@ -413,11 +372,7 @@ "enum": ["local", "s3"], "default": "local" }, - "deduplicate_media": { - "type": "boolean", - "default": true - }, - "local_uploads_folder": { + "uploads_path": { "type": "string", "minLength": 1, "default": "uploads" @@ -433,57 +388,51 @@ "type": "string", "default": "image/webp" }, - "convert_vector": { + "convert_vectors": { "type": "boolean", "default": false } }, - "additionalProperties": false, - "default": { - "convert_images": false, - "convert_to": "image/webp", - "convert_vector": false - } + "required": [ + "convert_images", + "convert_to", + "convert_vectors" + ], + "additionalProperties": false } }, - "additionalProperties": false, - "default": { - "backend": "local", - "deduplicate_media": true, - "local_uploads_folder": "uploads", - "conversion": { - "convert_images": false, - "convert_to": "image/webp" - } - } + "required": ["backend", "uploads_path", "conversion"], + "additionalProperties": false }, "s3": { "type": "object", "properties": { "endpoint": { - "type": "string" + "$ref": "#/properties/http/properties/proxy_address" }, "access_key": { - "type": "string" + "$ref": "#/properties/postgres/properties/password" }, "secret_access_key": { - "type": "string" + "$ref": "#/properties/postgres/properties/password" }, "region": { "type": "string" }, "bucket_name": { - "type": "string", - "default": "versia" + "type": "string" }, "public_url": { - "$ref": "#/properties/http/properties/proxy/properties/address/anyOf/0" + "$ref": "#/properties/http/properties/base_url", + "description": "Public URL that uploaded media will be accessible at" } }, "required": [ "endpoint", "access_key", "secret_access_key", + "region", + "bucket_name", "public_url" ], "additionalProperties": false @@ -491,1350 +440,1432 @@ "validation": { "type": "object", "properties": { - "max_displayname_size": { - "type": "integer", - "default": 50 - }, - "max_bio_size": { - "type": "integer", - "default": 5000 - }, - "max_note_size": { - "type": "integer", - "default": 5000 - }, - "max_avatar_size": { - "type": "integer", - "default": 5000000 - }, - "max_header_size": { - "type": "integer", - "default": 5000000 - }, - "max_media_size": { - "type": "integer", - "default": 40000000 - }, - "max_media_attachments": { - "type": "integer", - "default": 10 - }, - "max_media_description_size": { - "type": "integer", - "default": 1000 - }, - "max_emoji_size": { - "type": "integer", - "default": 1000000 - }, - "max_emoji_shortcode_size": { - "type": "integer", - "default": 100 - }, - "max_emoji_description_size": { - "type": "integer", - "default": 1000 - }, - "max_poll_options": { - "type": "integer", - "default": 20 - }, - "max_poll_option_size": { - "type": "integer", - "default": 500 - }, - "min_poll_duration": { - "type": "integer", - "default": 60 - }, - "max_poll_duration": { - "type": "integer", - "default": 1893456000 - }, - "max_username_size": { - "type": "integer", - "default": 30 - }, - "max_field_count": { - "type": "integer", - "default": 10 - }, - "max_field_name_size": { - "type": "integer", - "default": 1000 - }, - "max_field_value_size": { - "type": "integer", - "default": 1000 - }, - "username_blacklist": { - "type": "array", - "items": { - "type": "string" + "accounts": { + "type": "object", + "properties": { + "max_displayname_characters": { + "type": "integer", + "minimum": 0, + "default": 50 + }, + "max_username_characters": { + "type": "integer", + "minimum": 0, + "default": 30 + }, + "max_bio_characters": { + "type": "integer", + "minimum": 0, + "default": 5000 + }, + "max_avatar_bytes": { + "type": "integer", + "minimum": 0, + "default": 5000000 + }, + "max_header_bytes": { + "type": "integer", + "minimum": 0, + "default": 5000000 + }, + "disallowed_usernames": { + "type": "array", + "items": { + "$ref": "#/properties/http/properties/banned_user_agents/items" + }, + "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" + ] + }, + "max_field_count": { + "type": "integer", + "default": 10 + }, + "max_field_name_characters": { + "type": "integer", + "default": 1000 + }, + "max_field_value_characters": { + "type": "integer", + "default": 1000 + }, + "max_pinned_notes": { + "type": "integer", + "default": 20 + } }, - "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": { - "type": "boolean", - "default": false + "required": [ + "max_displayname_characters", + "max_username_characters", + "max_bio_characters", + "max_avatar_bytes", + "max_header_bytes", + "disallowed_usernames", + "max_field_count", + "max_field_name_characters", + "max_field_value_characters", + "max_pinned_notes" + ], + "additionalProperties": false }, - "email_blacklist": { - "type": "array", - "items": { - "type": "string" + "notes": { + "type": "object", + "properties": { + "max_characters": { + "type": "integer", + "minimum": 0, + "default": 5000 + }, + "allowed_url_schemes": { + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "http", + "https", + "ftp", + "dat", + "dweb", + "gopher", + "hyper", + "ipfs", + "ipns", + "irc", + "xmpp", + "ircs", + "magnet", + "mailto", + "mumble", + "ssb", + "gemini" + ] + }, + "max_attachments": { + "type": "integer", + "default": 16 + } }, - "default": [] + "required": [ + "max_characters", + "allowed_url_schemes", + "max_attachments" + ], + "additionalProperties": false }, - "url_scheme_whitelist": { - "type": "array", - "items": { - "type": "string" + "media": { + "type": "object", + "properties": { + "max_bytes": { + "type": "integer", + "minimum": 0, + "default": 40000000 + }, + "max_description_characters": { + "type": "integer", + "minimum": 0, + "default": 1000 + }, + "allowed_mime_types": { + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "application/vnd.lotus-1-2-3", + "application/andrew-inset", + "application/applixware", + "application/atom+xml", + "application/atomcat+xml", + "application/atomdeleted+xml", + "application/atomsvc+xml", + "application/atsc-dwd+xml", + "application/atsc-held+xml", + "application/atsc-rsat+xml", + "application/bdoc", + "application/calendar+xml", + "application/ccxml+xml", + "application/cdfx+xml", + "application/cdmi-capability", + "application/cdmi-container", + "application/cdmi-domain", + "application/cdmi-object", + "application/cdmi-queue", + "application/cpl+xml", + "application/cu-seeme", + "application/dash+xml", + "application/dash-patch+xml", + "application/davmount+xml", + "application/docbook+xml", + "application/dssc+der", + "application/dssc+xml", + "application/ecmascript", + "application/ecmascript", + "application/emma+xml", + "application/emotionml+xml", + "application/epub+zip", + "application/exi", + "application/express", + "application/fdt+xml", + "application/font-tdpfr", + "application/geo+json", + "application/gml+xml", + "application/gpx+xml", + "application/gxf", + "application/gzip", + "application/hjson", + "application/hyperstudio", + "application/inkml+xml", + "application/inkml+xml", + "application/ipfix", + "application/its+xml", + "application/java-archive", + "application/java-archive", + "application/java-archive", + "application/java-serialized-object", + "application/java-vm", + "application/javascript", + "application/javascript", + "application/json", + "application/json", + "application/json5", + "application/jsonml+json", + "application/ld+json", + "application/lgr+xml", + "application/lost+xml", + "application/mac-binhex40", + "application/mac-compactpro", + "application/mads+xml", + "application/manifest+json", + "application/marc", + "application/marcxml+xml", + "application/mathematica", + "application/mathematica", + "application/mathematica", + "application/mathml+xml", + "application/mbox", + "application/media-policy-dataset+xml", + "application/mediaservercontrol+xml", + "application/metalink+xml", + "application/metalink4+xml", + "application/mets+xml", + "application/mmt-aei+xml", + "application/mmt-usd+xml", + "application/mods+xml", + "application/mp21", + "application/mp21", + "application/mp4", + "application/mp4", + "application/msword", + "application/msword", + "application/mxf", + "application/n-quads", + "application/n-triples", + "application/node", + "application/octet-stream", + "application/octet-stream", + "application/octet-stream", + "application/octet-stream", + "application/octet-stream", + "application/octet-stream", + "application/octet-stream", + "application/octet-stream", + "application/octet-stream", + "application/octet-stream", + "application/octet-stream", + "application/octet-stream", + "application/x-msdos-program", + "application/x-msdownload", + "application/x-debian-package", + "application/x-apple-diskimage", + "application/x-iso9660-image", + "application/octet-stream", + "application/x-msdownload", + "application/octet-stream", + "application/octet-stream", + "application/octet-stream", + "application/oda", + "application/oebps-package+xml", + "application/ogg", + "application/omdoc+xml", + "application/onenote", + "application/onenote", + "application/onenote", + "application/onenote", + "application/oxps", + "application/p2p-overlay+xml", + "application/patch-ops-error+xml", + "application/pdf", + "application/pgp-encrypted", + "application/pgp-keys", + "application/pgp-signature", + "application/pics-rules", + "application/pkcs10", + "application/pkcs7-mime", + "application/pkcs7-mime", + "application/pkcs7-signature", + "application/pkcs8", + "application/pkix-attr-cert", + "application/pkix-cert", + "application/pkix-crl", + "application/pkix-pkipath", + "application/pkixcmp", + "application/pls+xml", + "application/postscript", + "application/postscript", + "application/postscript", + "application/provenance+xml", + "application/prs.cww", + "application/pskc+xml", + "application/raml+yaml", + "application/rdf+xml", + "application/rdf+xml", + "application/reginfo+xml", + "application/relax-ng-compact-syntax", + "application/resource-lists+xml", + "application/resource-lists-diff+xml", + "application/rls-services+xml", + "application/route-apd+xml", + "application/route-s-tsid+xml", + "application/route-usd+xml", + "application/rpki-ghostbusters", + "application/rpki-manifest", + "application/rpki-roa", + "application/rsd+xml", + "application/rss+xml", + "application/rtf", + "application/sbml+xml", + "application/scvp-cv-request", + "application/scvp-cv-response", + "application/scvp-vp-request", + "application/scvp-vp-response", + "application/sdp", + "application/senml+xml", + "application/sensml+xml", + "application/set-payment-initiation", + "application/set-registration-initiation", + "application/shf+xml", + "application/sieve", + "application/sieve", + "application/smil+xml", + "application/smil+xml", + "application/sparql-query", + "application/sparql-results+xml", + "application/srgs", + "application/srgs+xml", + "application/sru+xml", + "application/ssdl+xml", + "application/ssml+xml", + "application/swid+xml", + "application/tei+xml", + "application/tei+xml", + "application/thraud+xml", + "application/timestamped-data", + "application/toml", + "application/trig", + "application/ttml+xml", + "application/ubjson", + "application/urc-ressheet+xml", + "application/urc-targetdesc+xml", + "application/vnd.1000minds.decision-model+xml", + "application/vnd.3gpp.pic-bw-large", + "application/vnd.3gpp.pic-bw-small", + "application/vnd.3gpp.pic-bw-var", + "application/vnd.3gpp2.tcap", + "application/vnd.3m.post-it-notes", + "application/vnd.accpac.simply.aso", + "application/vnd.accpac.simply.imp", + "application/vnd.acucobol", + "application/vnd.acucorp", + "application/vnd.acucorp", + "application/vnd.adobe.air-application-installer-package+zip", + "application/vnd.adobe.formscentral.fcdt", + "application/vnd.adobe.fxp", + "application/vnd.adobe.fxp", + "application/vnd.adobe.xdp+xml", + "application/vnd.adobe.xfdf", + "application/vnd.age", + "application/vnd.ahead.space", + "application/vnd.airzip.filesecure.azf", + "application/vnd.airzip.filesecure.azs", + "application/vnd.amazon.ebook", + "application/vnd.americandynamics.acc", + "application/vnd.amiga.ami", + "application/vnd.android.package-archive", + "application/vnd.anser-web-certificate-issue-initiation", + "application/vnd.anser-web-funds-transfer-initiation", + "application/vnd.antix.game-component", + "application/vnd.apple.installer+xml", + "application/vnd.apple.keynote", + "application/vnd.apple.mpegurl", + "application/vnd.apple.numbers", + "application/vnd.apple.pages", + "application/vnd.apple.pkpass", + "application/vnd.aristanetworks.swi", + "application/vnd.astraea-software.iota", + "application/vnd.audiograph", + "application/vnd.balsamiq.bmml+xml", + "application/vnd.blueice.multipass", + "application/vnd.bmi", + "application/vnd.businessobjects", + "application/vnd.chemdraw+xml", + "application/vnd.chipnuts.karaoke-mmd", + "application/vnd.cinderella", + "application/vnd.citationstyles.style+xml", + "application/vnd.claymore", + "application/vnd.cloanto.rp9", + "application/vnd.clonk.c4group", + "application/vnd.clonk.c4group", + "application/vnd.clonk.c4group", + "application/vnd.clonk.c4group", + "application/vnd.clonk.c4group", + "application/vnd.cluetrust.cartomobile-config", + "application/vnd.cluetrust.cartomobile-config-pkg", + "application/vnd.commonspace", + "application/vnd.contact.cmsg", + "application/vnd.cosmocaller", + "application/vnd.crick.clicker", + "application/vnd.crick.clicker.keyboard", + "application/vnd.crick.clicker.palette", + "application/vnd.crick.clicker.template", + "application/vnd.crick.clicker.wordbank", + "application/vnd.criticaltools.wbs+xml", + "application/vnd.ctc-posml", + "application/vnd.cups-ppd", + "application/vnd.curl.car", + "application/vnd.curl.pcurl", + "application/vnd.dart", + "application/vnd.data-vision.rdz", + "application/vnd.dbf", + "application/vnd.dece.data", + "application/vnd.dece.data", + "application/vnd.dece.data", + "application/vnd.dece.data", + "application/vnd.dece.ttml+xml", + "application/vnd.dece.ttml+xml", + "application/vnd.dece.unspecified", + "application/vnd.dece.unspecified", + "application/vnd.dece.zip", + "application/vnd.dece.zip", + "application/vnd.denovo.fcselayout-link", + "application/vnd.dna", + "application/vnd.dolby.mlp", + "application/vnd.dpgraph", + "application/vnd.dreamfactory", + "application/vnd.ds-keypoint", + "application/vnd.dvb.ait", + "application/vnd.dvb.service", + "application/vnd.dynageo", + "application/vnd.ecowin.chart", + "application/vnd.enliven", + "application/vnd.epson.esf", + "application/vnd.epson.msf", + "application/vnd.epson.quickanime", + "application/vnd.epson.salt", + "application/vnd.epson.ssf", + "application/vnd.eszigno3+xml", + "application/vnd.eszigno3+xml", + "application/vnd.ezpix-album", + "application/vnd.ezpix-package", + "application/vnd.fdf", + "application/vnd.fdsn.mseed", + "application/vnd.fdsn.seed", + "application/vnd.fdsn.seed", + "application/vnd.flographit", + "application/vnd.fluxtime.clip", + "application/vnd.framemaker", + "application/vnd.framemaker", + "application/vnd.framemaker", + "application/vnd.framemaker", + "application/vnd.frogans.fnc", + "application/vnd.frogans.ltf", + "application/vnd.fsc.weblaunch", + "application/vnd.fujitsu.oasys", + "application/vnd.fujitsu.oasys2", + "application/vnd.fujitsu.oasys3", + "application/vnd.fujitsu.oasysgp", + "application/vnd.fujitsu.oasysprs", + "application/vnd.fujixerox.ddd", + "application/vnd.fujixerox.docuworks", + "application/vnd.fujixerox.docuworks.binder", + "application/vnd.fuzzysheet", + "application/vnd.genomatix.tuxedo", + "application/vnd.geogebra.file", + "application/vnd.geogebra.tool", + "application/vnd.geometry-explorer", + "application/vnd.geometry-explorer", + "application/vnd.geonext", + "application/vnd.geoplan", + "application/vnd.geospace", + "application/vnd.gmx", + "application/vnd.google-apps.document", + "application/vnd.google-apps.presentation", + "application/vnd.google-apps.spreadsheet", + "application/vnd.google-earth.kml+xml", + "application/vnd.google-earth.kmz", + "application/vnd.grafeq", + "application/vnd.grafeq", + "application/vnd.groove-account", + "application/vnd.groove-help", + "application/vnd.groove-identity-message", + "application/vnd.groove-injector", + "application/vnd.groove-tool-message", + "application/vnd.groove-tool-template", + "application/vnd.groove-vcard", + "application/vnd.hal+xml", + "application/vnd.handheld-entertainment+xml", + "application/vnd.hbci", + "application/vnd.hhe.lesson-player", + "application/vnd.hp-hpgl", + "application/vnd.hp-hpid", + "application/vnd.hp-hps", + "application/vnd.hp-jlyt", + "application/vnd.hp-pcl", + "application/vnd.hp-pclxl", + "application/vnd.hydrostatix.sof-data", + "application/vnd.ibm.minipay", + "application/vnd.ibm.modcap", + "application/vnd.ibm.modcap", + "application/vnd.ibm.modcap", + "application/vnd.ibm.rights-management", + "application/vnd.ibm.secure-container", + "application/vnd.iccprofile", + "application/vnd.iccprofile", + "application/vnd.igloader", + "application/vnd.immervision-ivp", + "application/vnd.immervision-ivu", + "application/vnd.insors.igm", + "application/vnd.intercon.formnet", + "application/vnd.intercon.formnet", + "application/vnd.intergeo", + "application/vnd.intu.qbo", + "application/vnd.intu.qfx", + "application/vnd.ipunplugged.rcprofile", + "application/vnd.irepository.package+xml", + "application/vnd.is-xpr", + "application/vnd.isac.fcs", + "application/vnd.jam", + "application/vnd.jcp.javame.midlet-rms", + "application/vnd.jisp", + "application/vnd.joost.joda-archive", + "application/vnd.kahootz", + "application/vnd.kahootz", + "application/vnd.kde.karbon", + "application/vnd.kde.kchart", + "application/vnd.kde.kformula", + "application/vnd.kde.kivio", + "application/vnd.kde.kontour", + "application/vnd.kde.kpresenter", + "application/vnd.kde.kpresenter", + "application/vnd.kde.kspread", + "application/vnd.kde.kword", + "application/vnd.kde.kword", + "application/vnd.kenameaapp", + "application/vnd.kidspiration", + "application/vnd.kinar", + "application/vnd.kinar", + "application/vnd.koan", + "application/vnd.koan", + "application/vnd.koan", + "application/vnd.koan", + "application/vnd.kodak-descriptor", + "application/vnd.las.las+xml", + "application/vnd.llamagraphics.life-balance.desktop", + "application/vnd.llamagraphics.life-balance.exchange+xml", + "application/vnd.lotus-approach", + "application/vnd.lotus-freelance", + "application/vnd.lotus-notes", + "application/vnd.lotus-organizer", + "application/vnd.lotus-screencam", + "application/vnd.lotus-wordpro", + "application/vnd.macports.portpkg", + "application/vnd.mapbox-vector-tile", + "application/vnd.mcd", + "application/vnd.medcalcdata", + "application/vnd.mediastation.cdkey", + "application/vnd.mfer", + "application/vnd.mfmp", + "application/vnd.micrografx.flo", + "application/vnd.micrografx.igx", + "application/vnd.mif", + "application/vnd.mobius.daf", + "application/vnd.mobius.dis", + "application/vnd.mobius.mbk", + "application/vnd.mobius.mqy", + "application/vnd.mobius.msl", + "application/vnd.mobius.plc", + "application/vnd.mobius.txf", + "application/vnd.mophun.application", + "application/vnd.mophun.certificate", + "application/vnd.mozilla.xul+xml", + "application/vnd.ms-artgalry", + "application/vnd.ms-cab-compressed", + "application/vnd.ms-excel", + "application/vnd.ms-excel", + "application/vnd.ms-excel", + "application/vnd.ms-excel", + "application/vnd.ms-excel", + "application/vnd.ms-excel", + "application/vnd.ms-excel.addin.macroenabled.12", + "application/vnd.ms-excel.sheet.binary.macroenabled.12", + "application/vnd.ms-excel.sheet.macroenabled.12", + "application/vnd.ms-excel.template.macroenabled.12", + "application/vnd.ms-fontobject", + "application/vnd.ms-htmlhelp", + "application/vnd.ms-ims", + "application/vnd.ms-lrm", + "application/vnd.ms-officetheme", + "application/vnd.ms-outlook", + "application/vnd.ms-pki.seccat", + "model/stl", + "application/vnd.ms-powerpoint", + "application/vnd.ms-powerpoint", + "application/vnd.ms-powerpoint", + "application/vnd.ms-powerpoint.addin.macroenabled.12", + "application/vnd.ms-powerpoint.presentation.macroenabled.12", + "application/vnd.ms-powerpoint.slide.macroenabled.12", + "application/vnd.ms-powerpoint.slideshow.macroenabled.12", + "application/vnd.ms-powerpoint.template.macroenabled.12", + "application/vnd.ms-project", + "application/vnd.ms-word.document.macroenabled.12", + "application/vnd.ms-word.template.macroenabled.12", + "application/vnd.ms-works", + "application/vnd.ms-works", + "application/vnd.ms-works", + "application/vnd.ms-works", + "application/vnd.ms-wpl", + "application/vnd.ms-xpsdocument", + "application/vnd.mseq", + "application/vnd.musician", + "application/vnd.muvee.style", + "application/vnd.mynfc", + "application/vnd.neurolanguage.nlu", + "application/vnd.nitf", + "application/vnd.nitf", + "application/vnd.noblenet-directory", + "application/vnd.noblenet-sealer", + "application/vnd.noblenet-web", + "application/vnd.nokia.n-gage.data", + "application/vnd.nokia.n-gage.symbian.install", + "application/vnd.nokia.radio-preset", + "application/vnd.nokia.radio-presets", + "application/vnd.novadigm.edm", + "application/vnd.novadigm.edx", + "application/vnd.novadigm.ext", + "application/vnd.oasis.opendocument.chart", + "application/vnd.oasis.opendocument.chart-template", + "application/vnd.oasis.opendocument.database", + "application/vnd.oasis.opendocument.formula", + "application/vnd.oasis.opendocument.formula-template", + "application/vnd.oasis.opendocument.graphics", + "application/vnd.oasis.opendocument.graphics-template", + "application/vnd.oasis.opendocument.image", + "application/vnd.oasis.opendocument.image-template", + "application/vnd.oasis.opendocument.presentation", + "application/vnd.oasis.opendocument.presentation-template", + "application/vnd.oasis.opendocument.spreadsheet", + "application/vnd.oasis.opendocument.spreadsheet-template", + "application/vnd.oasis.opendocument.text", + "application/vnd.oasis.opendocument.text-master", + "application/vnd.oasis.opendocument.text-template", + "application/vnd.oasis.opendocument.text-web", + "application/vnd.olpc-sugar", + "application/vnd.oma.dd2+xml", + "application/vnd.openblox.game+xml", + "application/vnd.openofficeorg.extension", + "application/vnd.openstreetmap.data+xml", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "application/vnd.openxmlformats-officedocument.presentationml.slide", + "application/vnd.openxmlformats-officedocument.presentationml.slideshow", + "application/vnd.openxmlformats-officedocument.presentationml.template", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.openxmlformats-officedocument.spreadsheetml.template", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.openxmlformats-officedocument.wordprocessingml.template", + "application/vnd.osgeo.mapguide.package", + "application/vnd.osgi.dp", + "application/vnd.osgi.subsystem", + "application/vnd.palm", + "application/vnd.palm", + "application/vnd.palm", + "application/vnd.pawaafile", + "application/vnd.pg.format", + "application/vnd.pg.osasli", + "application/vnd.picsel", + "application/vnd.pmi.widget", + "application/vnd.pocketlearn", + "application/vnd.powerbuilder6", + "application/vnd.previewsystems.box", + "application/vnd.proteus.magazine", + "application/vnd.publishare-delta-tree", + "application/vnd.pvi.ptid1", + "application/vnd.quark.quarkxpress", + "application/vnd.quark.quarkxpress", + "application/vnd.quark.quarkxpress", + "application/vnd.quark.quarkxpress", + "application/vnd.quark.quarkxpress", + "application/vnd.quark.quarkxpress", + "application/vnd.rar", + "application/vnd.realvnc.bed", + "application/vnd.recordare.musicxml", + "application/vnd.recordare.musicxml+xml", + "application/vnd.rig.cryptonote", + "application/vnd.rim.cod", + "application/vnd.rn-realmedia", + "application/vnd.rn-realmedia-vbr", + "application/vnd.route66.link66+xml", + "application/vnd.sailingtracker.track", + "application/vnd.seemail", + "application/vnd.sema", + "application/vnd.semd", + "application/vnd.semf", + "application/vnd.shana.informed.formdata", + "application/vnd.shana.informed.formtemplate", + "application/vnd.shana.informed.interchange", + "application/vnd.shana.informed.package", + "application/vnd.simtech-mindmapper", + "application/vnd.simtech-mindmapper", + "application/vnd.smaf", + "application/vnd.smart.teacher", + "application/vnd.software602.filler.form+xml", + "application/vnd.solent.sdkm+xml", + "application/vnd.solent.sdkm+xml", + "application/vnd.spotfire.dxp", + "application/vnd.spotfire.sfs", + "application/vnd.stardivision.calc", + "application/vnd.stardivision.draw", + "application/vnd.stardivision.impress", + "application/vnd.stardivision.math", + "application/vnd.stardivision.writer", + "application/vnd.stardivision.writer", + "application/vnd.stardivision.writer-global", + "application/vnd.stepmania.package", + "application/vnd.stepmania.stepchart", + "application/vnd.sun.wadl+xml", + "application/vnd.sun.xml.calc", + "application/vnd.sun.xml.calc.template", + "application/vnd.sun.xml.draw", + "application/vnd.sun.xml.draw.template", + "application/vnd.sun.xml.impress", + "application/vnd.sun.xml.impress.template", + "application/vnd.sun.xml.math", + "application/vnd.sun.xml.writer", + "application/vnd.sun.xml.writer.global", + "application/vnd.sun.xml.writer.template", + "application/vnd.sus-calendar", + "application/vnd.sus-calendar", + "application/vnd.svd", + "application/vnd.symbian.install", + "application/vnd.symbian.install", + "application/vnd.syncml+xml", + "application/vnd.syncml.dm+wbxml", + "application/vnd.syncml.dm+xml", + "application/vnd.syncml.dmddf+xml", + "application/vnd.tao.intent-module-archive", + "application/vnd.tcpdump.pcap", + "application/vnd.tcpdump.pcap", + "application/vnd.tcpdump.pcap", + "application/vnd.tmobile-livetv", + "application/vnd.trid.tpt", + "application/vnd.triscape.mxs", + "application/vnd.trueapp", + "application/vnd.ufdl", + "application/vnd.ufdl", + "application/vnd.uiq.theme", + "application/vnd.umajin", + "application/vnd.unity", + "application/vnd.uoml+xml", + "application/vnd.vcx", + "application/vnd.visio", + "application/vnd.visio", + "application/vnd.visio", + "application/vnd.visio", + "application/vnd.visionary", + "application/vnd.vsf", + "application/vnd.wap.wbxml", + "application/vnd.wap.wmlc", + "application/vnd.wap.wmlscriptc", + "application/vnd.webturbo", + "application/vnd.wolfram.player", + "application/vnd.wordperfect", + "application/vnd.wqd", + "application/vnd.wt.stf", + "application/vnd.xara", + "application/vnd.xfdl", + "application/vnd.yamaha.hv-dic", + "application/vnd.yamaha.hv-script", + "application/vnd.yamaha.hv-voice", + "application/vnd.yamaha.openscoreformat", + "application/vnd.yamaha.openscoreformat.osfpvg+xml", + "application/vnd.yamaha.smaf-audio", + "application/vnd.yamaha.smaf-phrase", + "application/vnd.yellowriver-custom-menu", + "application/vnd.zul", + "application/vnd.zul", + "application/vnd.zzazz.deck+xml", + "application/voicexml+xml", + "application/wasm", + "application/watcherinfo+xml", + "application/widget", + "application/winhlp", + "application/wsdl+xml", + "application/wspolicy+xml", + "application/x-7z-compressed", + "application/x-abiword", + "application/x-ace-compressed", + "application/x-arj", + "application/x-authorware-bin", + "application/x-authorware-bin", + "application/x-authorware-bin", + "application/x-authorware-bin", + "application/x-authorware-map", + "application/x-authorware-seg", + "application/x-bcpio", + "application/x-bittorrent", + "application/x-blorb", + "application/x-blorb", + "application/x-bzip", + "application/x-bzip2", + "application/x-bzip2", + "application/x-cbr", + "application/x-cbr", + "application/x-cbr", + "application/x-cbr", + "application/x-cbr", + "application/x-cdlink", + "application/x-cfs-compressed", + "application/x-chat", + "application/x-chess-pgn", + "application/x-chrome-extension", + "application/x-cocoa", + "application/x-conference", + "application/x-cpio", + "application/x-csh", + "application/x-debian-package", + "application/x-dgc-compressed", + "application/x-director", + "application/x-director", + "application/x-director", + "application/x-director", + "application/x-director", + "application/x-director", + "application/x-director", + "application/x-director", + "application/x-director", + "application/x-doom", + "application/x-dtbncx+xml", + "application/x-dtbook+xml", + "application/x-dtbresource+xml", + "application/x-dvi", + "application/x-envoy", + "application/x-eva", + "application/x-font-bdf", + "application/x-font-ghostscript", + "application/x-font-linux-psf", + "application/x-font-pcf", + "application/x-font-snf", + "application/x-font-type1", + "application/x-font-type1", + "application/x-font-type1", + "application/x-font-type1", + "application/x-freearc", + "application/x-futuresplash", + "application/x-gca-compressed", + "application/x-glulx", + "application/x-gnumeric", + "application/x-gramps-xml", + "application/x-gtar", + "application/x-hdf", + "application/x-httpd-php", + "application/x-install-instructions", + "application/x-java-archive-diff", + "application/x-java-jnlp-file", + "application/x-keepass2", + "application/x-latex", + "application/x-lua-bytecode", + "application/x-lzh-compressed", + "application/x-lzh-compressed", + "application/x-makeself", + "application/x-mie", + "application/x-mobipocket-ebook", + "application/x-mobipocket-ebook", + "application/x-ms-application", + "application/x-ms-shortcut", + "application/x-ms-wmd", + "application/x-ms-wmz", + "application/x-ms-xbap", + "application/x-msaccess", + "application/x-msbinder", + "application/x-mscardfile", + "application/x-msclip", + "application/x-msdownload", + "application/x-msdownload", + "application/x-msmediaview", + "application/x-msmediaview", + "application/x-msmediaview", + "image/wmf", + "image/emf", + "application/x-msmetafile", + "application/x-msmoney", + "application/x-mspublisher", + "application/x-msschedule", + "application/x-msterminal", + "application/x-mswrite", + "application/x-netcdf", + "application/x-netcdf", + "application/x-ns-proxy-autoconfig", + "application/x-nzb", + "application/x-perl", + "application/x-perl", + "application/x-pkcs12", + "application/x-pkcs12", + "application/x-pkcs7-certificates", + "application/x-pkcs7-certificates", + "application/x-pkcs7-certreqresp", + "application/x-redhat-package-manager", + "application/x-research-info-systems", + "application/x-sea", + "application/x-sh", + "application/x-shar", + "application/x-shockwave-flash", + "application/x-silverlight-app", + "application/x-sql", + "application/x-stuffit", + "application/x-stuffitx", + "application/x-subrip", + "application/x-sv4cpio", + "application/x-sv4crc", + "application/x-t3vm-image", + "application/x-tads", + "application/x-tar", + "application/x-tcl", + "application/x-tcl", + "application/x-tex", + "application/x-tex-tfm", + "application/x-texinfo", + "application/x-texinfo", + "model/obj", + "application/x-ustar", + "application/x-virtualbox-hdd", + "application/x-virtualbox-ova", + "application/x-virtualbox-ovf", + "application/x-virtualbox-vbox", + "application/x-virtualbox-vbox-extpack", + "application/x-virtualbox-vdi", + "application/x-virtualbox-vhd", + "application/x-virtualbox-vmdk", + "application/x-wais-source", + "application/x-web-app-manifest+json", + "application/x-x509-ca-cert", + "application/x-x509-ca-cert", + "application/x-x509-ca-cert", + "application/x-xfig", + "application/xliff+xml", + "application/x-xpinstall", + "application/x-xz", + "application/x-zmachine", + "application/x-zmachine", + "application/x-zmachine", + "application/x-zmachine", + "application/x-zmachine", + "application/x-zmachine", + "application/x-zmachine", + "application/x-zmachine", + "application/xaml+xml", + "application/xcap-att+xml", + "application/xcap-caps+xml", + "application/xcap-diff+xml", + "application/xcap-el+xml", + "application/xcap-ns+xml", + "application/xenc+xml", + "application/xhtml+xml", + "application/xhtml+xml", + "application/xml", + "application/xml", + "application/xml", + "application/xml", + "application/xml-dtd", + "application/xop+xml", + "application/xproc+xml", + "application/xslt+xml", + "application/xspf+xml", + "application/xv+xml", + "application/xv+xml", + "application/xv+xml", + "application/xv+xml", + "application/yang", + "application/yin+xml", + "application/zip", + "video/3gpp", + "audio/adpcm", + "audio/amr", + "audio/basic", + "audio/basic", + "audio/midi", + "audio/midi", + "audio/midi", + "audio/midi", + "audio/mobile-xmf", + "audio/mpeg", + "audio/mp4", + "audio/mp4", + "audio/mpeg", + "audio/mpeg", + "audio/mpeg", + "audio/mpeg", + "audio/mpeg", + "audio/ogg", + "audio/ogg", + "audio/ogg", + "audio/ogg", + "audio/s3m", + "audio/silk", + "audio/vnd.dece.audio", + "audio/vnd.dece.audio", + "audio/vnd.digital-winds", + "audio/vnd.dra", + "audio/vnd.dts", + "audio/vnd.dts.hd", + "audio/vnd.lucent.voice", + "audio/vnd.ms-playready.media.pya", + "audio/vnd.nuera.ecelp4800", + "audio/vnd.nuera.ecelp7470", + "audio/vnd.nuera.ecelp9600", + "audio/vnd.rip", + "audio/wave", + "audio/webm", + "audio/x-aac", + "audio/x-aiff", + "audio/x-aiff", + "audio/x-aiff", + "audio/x-caf", + "audio/x-flac", + "audio/x-matroska", + "audio/x-mpegurl", + "audio/x-ms-wax", + "audio/x-ms-wma", + "audio/x-pn-realaudio", + "audio/x-pn-realaudio", + "audio/x-pn-realaudio-plugin", + "audio/xm", + "chemical/x-cdx", + "chemical/x-cif", + "chemical/x-cmdf", + "chemical/x-cml", + "chemical/x-csml", + "chemical/x-xyz", + "font/collection", + "font/otf", + "font/ttf", + "font/woff", + "font/woff2", + "image/aces", + "image/apng", + "image/avci", + "image/avcs", + "image/avif", + "image/bmp", + "image/cgm", + "image/dicom-rle", + "image/fits", + "image/g3fax", + "image/gif", + "image/heic", + "image/heic-sequence", + "image/heif", + "image/heif-sequence", + "image/hej2k", + "image/hsj2", + "image/ief", + "image/jls", + "image/jp2", + "image/jp2", + "image/jpeg", + "image/jpeg", + "image/jpeg", + "image/jph", + "image/jphc", + "image/jpm", + "image/jpx", + "image/jpx", + "image/jxr", + "image/jxra", + "image/jxrs", + "image/jxs", + "image/jxsc", + "image/jxsi", + "image/jxss", + "image/ktx", + "image/ktx2", + "image/png", + "image/prs.btif", + "image/prs.pti", + "image/sgi", + "image/svg+xml", + "image/svg+xml", + "image/t38", + "image/tiff", + "image/tiff", + "image/tiff-fx", + "image/vnd.adobe.photoshop", + "image/vnd.airzip.accelerator.azv", + "image/vnd.dece.graphic", + "image/vnd.dece.graphic", + "image/vnd.dece.graphic", + "image/vnd.dece.graphic", + "image/vnd.djvu", + "image/vnd.djvu", + "text/vnd.dvb.subtitle", + "image/vnd.dwg", + "image/vnd.dxf", + "image/vnd.fastbidsheet", + "image/vnd.fpx", + "image/vnd.fst", + "image/vnd.fujixerox.edmics-mmr", + "image/vnd.fujixerox.edmics-rlc", + "image/vnd.microsoft.icon", + "image/vnd.ms-dds", + "image/vnd.ms-modi", + "image/vnd.ms-photo", + "image/vnd.net-fpx", + "image/vnd.pco.b16", + "image/vnd.tencent.tap", + "image/vnd.valve.source.texture", + "image/vnd.wap.wbmp", + "image/vnd.xiff", + "image/vnd.zbrush.pcx", + "image/webp", + "image/x-3ds", + "image/x-cmu-raster", + "image/x-cmx", + "image/x-freehand", + "image/x-freehand", + "image/x-freehand", + "image/x-freehand", + "image/x-freehand", + "image/x-jng", + "image/x-mrsid-image", + "image/x-pict", + "image/x-pict", + "image/x-portable-anymap", + "image/x-portable-bitmap", + "image/x-portable-graymap", + "image/x-portable-pixmap", + "image/x-rgb", + "image/x-tga", + "image/x-xbitmap", + "image/x-xpixmap", + "image/x-xwindowdump", + "message/disposition-notification", + "message/global", + "message/global-delivery-status", + "message/global-disposition-notification", + "message/global-headers", + "message/rfc822", + "message/rfc822", + "message/vnd.wfa.wsc", + "model/3mf", + "model/gltf+json", + "model/gltf-binary", + "model/iges", + "model/iges", + "model/mesh", + "model/mesh", + "model/mesh", + "model/mtl", + "model/step+xml", + "model/step+zip", + "model/step-xml+zip", + "model/vnd.collada+xml", + "model/vnd.dwf", + "model/vnd.gdl", + "model/vnd.gtw", + "model/vnd.mts", + "model/vnd.opengex", + "model/vnd.parasolid.transmit.binary", + "model/vnd.parasolid.transmit.text", + "model/vnd.sap.vds", + "model/vnd.usdz+zip", + "model/vnd.valve.source.compiled-map", + "model/vnd.vtu", + "model/vrml", + "model/vrml", + "model/x3d+fastinfoset", + "model/x3d+binary", + "model/x3d-vrml", + "model/x3d+vrml", + "model/x3d+xml", + "model/x3d+xml", + "text/cache-manifest", + "text/cache-manifest", + "text/calendar", + "text/calendar", + "text/coffeescript", + "text/coffeescript", + "text/css", + "text/csv", + "text/html", + "text/html", + "text/html", + "text/jade", + "text/jsx", + "text/less", + "text/markdown", + "text/markdown", + "text/mathml", + "text/mdx", + "text/n3", + "text/plain", + "text/plain", + "text/plain", + "text/plain", + "text/plain", + "text/plain", + "text/plain", + "text/plain", + "text/prs.lines.tag", + "text/richtext", + "text/sgml", + "text/sgml", + "text/shex", + "text/slim", + "text/slim", + "text/spdx", + "text/stylus", + "text/stylus", + "text/tab-separated-values", + "text/troff", + "text/troff", + "text/troff", + "text/troff", + "text/troff", + "text/troff", + "text/turtle", + "text/uri-list", + "text/uri-list", + "text/uri-list", + "text/vcard", + "text/vnd.curl", + "text/vnd.curl.dcurl", + "text/vnd.curl.mcurl", + "text/vnd.curl.scurl", + "text/vnd.familysearch.gedcom", + "text/vnd.fly", + "text/vnd.fmi.flexstor", + "text/vnd.graphviz", + "text/vnd.in3d.3dml", + "text/vnd.in3d.spot", + "text/vnd.sun.j2me.app-descriptor", + "text/vnd.wap.wml", + "text/vnd.wap.wmlscript", + "text/vtt", + "text/x-asm", + "text/x-asm", + "text/x-c", + "text/x-c", + "text/x-c", + "text/x-c", + "text/x-c", + "text/x-c", + "text/x-c", + "text/x-component", + "text/x-fortran", + "text/x-fortran", + "text/x-fortran", + "text/x-fortran", + "text/x-handlebars-template", + "text/x-java-source", + "text/x-lua", + "text/x-markdown", + "text/x-nfo", + "text/x-opml", + "text/x-pascal", + "text/x-pascal", + "text/x-processing", + "text/x-sass", + "text/x-scss", + "text/x-setext", + "text/x-sfv", + "text/x-suse-ymp", + "text/x-uuencode", + "text/x-vcalendar", + "text/x-vcard", + "text/yaml", + "text/yaml", + "video/3gpp", + "video/3gpp2", + "video/h261", + "video/h263", + "video/h264", + "video/iso.segment", + "video/jpeg", + "video/jpm", + "video/mj2", + "video/mj2", + "video/mp2t", + "video/mp4", + "video/mp4", + "video/mp4", + "video/mpeg", + "video/mpeg", + "video/mpeg", + "video/mpeg", + "video/mpeg", + "video/ogg", + "video/quicktime", + "video/quicktime", + "video/vnd.dece.hd", + "video/vnd.dece.hd", + "video/vnd.dece.mobile", + "video/vnd.dece.mobile", + "video/vnd.dece.pd", + "video/vnd.dece.pd", + "video/vnd.dece.sd", + "video/vnd.dece.sd", + "video/vnd.dece.video", + "video/vnd.dece.video", + "video/vnd.dvb.file", + "video/vnd.fvt", + "video/vnd.mpegurl", + "video/vnd.mpegurl", + "video/vnd.ms-playready.media.pyv", + "video/vnd.uvvu.mp4", + "video/vnd.uvvu.mp4", + "video/vnd.vivo", + "video/webm", + "video/x-f4v", + "video/x-fli", + "video/x-flv", + "video/x-m4v", + "video/x-matroska", + "video/x-matroska", + "video/x-matroska", + "video/x-mng", + "video/x-ms-asf", + "video/x-ms-asf", + "video/x-ms-vob", + "video/x-ms-wm", + "video/x-ms-wmv", + "video/x-ms-wmx", + "video/x-ms-wvx", + "video/x-msvideo", + "video/x-sgi-movie", + "video/x-smv", + "x-conference/x-cooltalk" + ] + } }, - "default": [ - "http", - "https", - "ftp", - "dat", - "dweb", - "gopher", - "hyper", - "ipfs", - "ipns", - "irc", - "xmpp", - "ircs", - "magnet", - "mailto", - "mumble", - "ssb", - "gemini" - ] + "required": [ + "max_bytes", + "max_description_characters", + "allowed_mime_types" + ], + "additionalProperties": false }, - "enforce_mime_types": { - "type": "boolean", - "default": false + "emojis": { + "type": "object", + "properties": { + "max_bytes": { + "type": "integer", + "minimum": 0, + "default": 1000000 + }, + "max_shortcode_characters": { + "type": "integer", + "minimum": 0, + "default": 100 + }, + "max_description_characters": { + "type": "integer", + "minimum": 0, + "default": 1000 + } + }, + "required": [ + "max_bytes", + "max_shortcode_characters", + "max_description_characters" + ], + "additionalProperties": false }, - "allowed_mime_types": { - "type": "array", - "items": { - "type": "string" + "polls": { + "type": "object", + "properties": { + "max_options": { + "type": "integer", + "minimum": 0, + "default": 20 + }, + "max_option_characters": { + "type": "integer", + "minimum": 0, + "default": 500 + }, + "min_duration_seconds": { + "type": "integer", + "minimum": 0, + "default": 60 + }, + "max_duration_seconds": { + "type": "integer", + "minimum": 0, + "default": 8640000 + } }, - "default": [ - "application/vnd.lotus-1-2-3", - "application/andrew-inset", - "application/applixware", - "application/atom+xml", - "application/atomcat+xml", - "application/atomdeleted+xml", - "application/atomsvc+xml", - "application/atsc-dwd+xml", - "application/atsc-held+xml", - "application/atsc-rsat+xml", - "application/bdoc", - "application/calendar+xml", - "application/ccxml+xml", - "application/cdfx+xml", - "application/cdmi-capability", - "application/cdmi-container", - "application/cdmi-domain", - "application/cdmi-object", - "application/cdmi-queue", - "application/cpl+xml", - "application/cu-seeme", - "application/dash+xml", - "application/dash-patch+xml", - "application/davmount+xml", - "application/docbook+xml", - "application/dssc+der", - "application/dssc+xml", - "application/ecmascript", - "application/ecmascript", - "application/emma+xml", - "application/emotionml+xml", - "application/epub+zip", - "application/exi", - "application/express", - "application/fdt+xml", - "application/font-tdpfr", - "application/geo+json", - "application/gml+xml", - "application/gpx+xml", - "application/gxf", - "application/gzip", - "application/hjson", - "application/hyperstudio", - "application/inkml+xml", - "application/inkml+xml", - "application/ipfix", - "application/its+xml", - "application/java-archive", - "application/java-archive", - "application/java-archive", - "application/java-serialized-object", - "application/java-vm", - "application/javascript", - "application/javascript", - "application/json", - "application/json", - "application/json5", - "application/jsonml+json", - "application/ld+json", - "application/lgr+xml", - "application/lost+xml", - "application/mac-binhex40", - "application/mac-compactpro", - "application/mads+xml", - "application/manifest+json", - "application/marc", - "application/marcxml+xml", - "application/mathematica", - "application/mathematica", - "application/mathematica", - "application/mathml+xml", - "application/mbox", - "application/media-policy-dataset+xml", - "application/mediaservercontrol+xml", - "application/metalink+xml", - "application/metalink4+xml", - "application/mets+xml", - "application/mmt-aei+xml", - "application/mmt-usd+xml", - "application/mods+xml", - "application/mp21", - "application/mp21", - "application/mp4", - "application/mp4", - "application/msword", - "application/msword", - "application/mxf", - "application/n-quads", - "application/n-triples", - "application/node", - "application/octet-stream", - "application/octet-stream", - "application/octet-stream", - "application/octet-stream", - "application/octet-stream", - "application/octet-stream", - "application/octet-stream", - "application/octet-stream", - "application/octet-stream", - "application/octet-stream", - "application/octet-stream", - "application/octet-stream", - "application/x-msdos-program", - "application/x-msdownload", - "application/x-debian-package", - "application/x-apple-diskimage", - "application/x-iso9660-image", - "application/octet-stream", - "application/x-msdownload", - "application/octet-stream", - "application/octet-stream", - "application/octet-stream", - "application/oda", - "application/oebps-package+xml", - "application/ogg", - "application/omdoc+xml", - "application/onenote", - "application/onenote", - "application/onenote", - "application/onenote", - "application/oxps", - "application/p2p-overlay+xml", - "application/patch-ops-error+xml", - "application/pdf", - "application/pgp-encrypted", - "application/pgp-keys", - "application/pgp-signature", - "application/pics-rules", - "application/pkcs10", - "application/pkcs7-mime", - "application/pkcs7-mime", - "application/pkcs7-signature", - "application/pkcs8", - "application/pkix-attr-cert", - "application/pkix-cert", - "application/pkix-crl", - "application/pkix-pkipath", - "application/pkixcmp", - "application/pls+xml", - "application/postscript", - "application/postscript", - "application/postscript", - "application/provenance+xml", - "application/prs.cww", - "application/pskc+xml", - "application/raml+yaml", - "application/rdf+xml", - "application/rdf+xml", - "application/reginfo+xml", - "application/relax-ng-compact-syntax", - "application/resource-lists+xml", - "application/resource-lists-diff+xml", - "application/rls-services+xml", - "application/route-apd+xml", - "application/route-s-tsid+xml", - "application/route-usd+xml", - "application/rpki-ghostbusters", - "application/rpki-manifest", - "application/rpki-roa", - "application/rsd+xml", - "application/rss+xml", - "application/rtf", - "application/sbml+xml", - "application/scvp-cv-request", - "application/scvp-cv-response", - "application/scvp-vp-request", - "application/scvp-vp-response", - "application/sdp", - "application/senml+xml", - "application/sensml+xml", - "application/set-payment-initiation", - "application/set-registration-initiation", - "application/shf+xml", - "application/sieve", - "application/sieve", - "application/smil+xml", - "application/smil+xml", - "application/sparql-query", - "application/sparql-results+xml", - "application/srgs", - "application/srgs+xml", - "application/sru+xml", - "application/ssdl+xml", - "application/ssml+xml", - "application/swid+xml", - "application/tei+xml", - "application/tei+xml", - "application/thraud+xml", - "application/timestamped-data", - "application/toml", - "application/trig", - "application/ttml+xml", - "application/ubjson", - "application/urc-ressheet+xml", - "application/urc-targetdesc+xml", - "application/vnd.1000minds.decision-model+xml", - "application/vnd.3gpp.pic-bw-large", - "application/vnd.3gpp.pic-bw-small", - "application/vnd.3gpp.pic-bw-var", - "application/vnd.3gpp2.tcap", - "application/vnd.3m.post-it-notes", - "application/vnd.accpac.simply.aso", - "application/vnd.accpac.simply.imp", - "application/vnd.acucobol", - "application/vnd.acucorp", - "application/vnd.acucorp", - "application/vnd.adobe.air-application-installer-package+zip", - "application/vnd.adobe.formscentral.fcdt", - "application/vnd.adobe.fxp", - "application/vnd.adobe.fxp", - "application/vnd.adobe.xdp+xml", - "application/vnd.adobe.xfdf", - "application/vnd.age", - "application/vnd.ahead.space", - "application/vnd.airzip.filesecure.azf", - "application/vnd.airzip.filesecure.azs", - "application/vnd.amazon.ebook", - "application/vnd.americandynamics.acc", - "application/vnd.amiga.ami", - "application/vnd.android.package-archive", - "application/vnd.anser-web-certificate-issue-initiation", - "application/vnd.anser-web-funds-transfer-initiation", - "application/vnd.antix.game-component", - "application/vnd.apple.installer+xml", - "application/vnd.apple.keynote", - "application/vnd.apple.mpegurl", - "application/vnd.apple.numbers", - "application/vnd.apple.pages", - "application/vnd.apple.pkpass", - "application/vnd.aristanetworks.swi", - "application/vnd.astraea-software.iota", - "application/vnd.audiograph", - "application/vnd.balsamiq.bmml+xml", - "application/vnd.blueice.multipass", - "application/vnd.bmi", - "application/vnd.businessobjects", - "application/vnd.chemdraw+xml", - "application/vnd.chipnuts.karaoke-mmd", - "application/vnd.cinderella", - "application/vnd.citationstyles.style+xml", - "application/vnd.claymore", - "application/vnd.cloanto.rp9", - "application/vnd.clonk.c4group", - "application/vnd.clonk.c4group", - "application/vnd.clonk.c4group", - "application/vnd.clonk.c4group", - "application/vnd.clonk.c4group", - "application/vnd.cluetrust.cartomobile-config", - "application/vnd.cluetrust.cartomobile-config-pkg", - "application/vnd.commonspace", - "application/vnd.contact.cmsg", - "application/vnd.cosmocaller", - "application/vnd.crick.clicker", - "application/vnd.crick.clicker.keyboard", - "application/vnd.crick.clicker.palette", - "application/vnd.crick.clicker.template", - "application/vnd.crick.clicker.wordbank", - "application/vnd.criticaltools.wbs+xml", - "application/vnd.ctc-posml", - "application/vnd.cups-ppd", - "application/vnd.curl.car", - "application/vnd.curl.pcurl", - "application/vnd.dart", - "application/vnd.data-vision.rdz", - "application/vnd.dbf", - "application/vnd.dece.data", - "application/vnd.dece.data", - "application/vnd.dece.data", - "application/vnd.dece.data", - "application/vnd.dece.ttml+xml", - "application/vnd.dece.ttml+xml", - "application/vnd.dece.unspecified", - "application/vnd.dece.unspecified", - "application/vnd.dece.zip", - "application/vnd.dece.zip", - "application/vnd.denovo.fcselayout-link", - "application/vnd.dna", - "application/vnd.dolby.mlp", - "application/vnd.dpgraph", - "application/vnd.dreamfactory", - "application/vnd.ds-keypoint", - "application/vnd.dvb.ait", - "application/vnd.dvb.service", - "application/vnd.dynageo", - "application/vnd.ecowin.chart", - "application/vnd.enliven", - "application/vnd.epson.esf", - "application/vnd.epson.msf", - "application/vnd.epson.quickanime", - "application/vnd.epson.salt", - "application/vnd.epson.ssf", - "application/vnd.eszigno3+xml", - "application/vnd.eszigno3+xml", - "application/vnd.ezpix-album", - "application/vnd.ezpix-package", - "application/vnd.fdf", - "application/vnd.fdsn.mseed", - "application/vnd.fdsn.seed", - "application/vnd.fdsn.seed", - "application/vnd.flographit", - "application/vnd.fluxtime.clip", - "application/vnd.framemaker", - "application/vnd.framemaker", - "application/vnd.framemaker", - "application/vnd.framemaker", - "application/vnd.frogans.fnc", - "application/vnd.frogans.ltf", - "application/vnd.fsc.weblaunch", - "application/vnd.fujitsu.oasys", - "application/vnd.fujitsu.oasys2", - "application/vnd.fujitsu.oasys3", - "application/vnd.fujitsu.oasysgp", - "application/vnd.fujitsu.oasysprs", - "application/vnd.fujixerox.ddd", - "application/vnd.fujixerox.docuworks", - "application/vnd.fujixerox.docuworks.binder", - "application/vnd.fuzzysheet", - "application/vnd.genomatix.tuxedo", - "application/vnd.geogebra.file", - "application/vnd.geogebra.tool", - "application/vnd.geometry-explorer", - "application/vnd.geometry-explorer", - "application/vnd.geonext", - "application/vnd.geoplan", - "application/vnd.geospace", - "application/vnd.gmx", - "application/vnd.google-apps.document", - "application/vnd.google-apps.presentation", - "application/vnd.google-apps.spreadsheet", - "application/vnd.google-earth.kml+xml", - "application/vnd.google-earth.kmz", - "application/vnd.grafeq", - "application/vnd.grafeq", - "application/vnd.groove-account", - "application/vnd.groove-help", - "application/vnd.groove-identity-message", - "application/vnd.groove-injector", - "application/vnd.groove-tool-message", - "application/vnd.groove-tool-template", - "application/vnd.groove-vcard", - "application/vnd.hal+xml", - "application/vnd.handheld-entertainment+xml", - "application/vnd.hbci", - "application/vnd.hhe.lesson-player", - "application/vnd.hp-hpgl", - "application/vnd.hp-hpid", - "application/vnd.hp-hps", - "application/vnd.hp-jlyt", - "application/vnd.hp-pcl", - "application/vnd.hp-pclxl", - "application/vnd.hydrostatix.sof-data", - "application/vnd.ibm.minipay", - "application/vnd.ibm.modcap", - "application/vnd.ibm.modcap", - "application/vnd.ibm.modcap", - "application/vnd.ibm.rights-management", - "application/vnd.ibm.secure-container", - "application/vnd.iccprofile", - "application/vnd.iccprofile", - "application/vnd.igloader", - "application/vnd.immervision-ivp", - "application/vnd.immervision-ivu", - "application/vnd.insors.igm", - "application/vnd.intercon.formnet", - "application/vnd.intercon.formnet", - "application/vnd.intergeo", - "application/vnd.intu.qbo", - "application/vnd.intu.qfx", - "application/vnd.ipunplugged.rcprofile", - "application/vnd.irepository.package+xml", - "application/vnd.is-xpr", - "application/vnd.isac.fcs", - "application/vnd.jam", - "application/vnd.jcp.javame.midlet-rms", - "application/vnd.jisp", - "application/vnd.joost.joda-archive", - "application/vnd.kahootz", - "application/vnd.kahootz", - "application/vnd.kde.karbon", - "application/vnd.kde.kchart", - "application/vnd.kde.kformula", - "application/vnd.kde.kivio", - "application/vnd.kde.kontour", - "application/vnd.kde.kpresenter", - "application/vnd.kde.kpresenter", - "application/vnd.kde.kspread", - "application/vnd.kde.kword", - "application/vnd.kde.kword", - "application/vnd.kenameaapp", - "application/vnd.kidspiration", - "application/vnd.kinar", - "application/vnd.kinar", - "application/vnd.koan", - "application/vnd.koan", - "application/vnd.koan", - "application/vnd.koan", - "application/vnd.kodak-descriptor", - "application/vnd.las.las+xml", - "application/vnd.llamagraphics.life-balance.desktop", - "application/vnd.llamagraphics.life-balance.exchange+xml", - "application/vnd.lotus-approach", - "application/vnd.lotus-freelance", - "application/vnd.lotus-notes", - "application/vnd.lotus-organizer", - "application/vnd.lotus-screencam", - "application/vnd.lotus-wordpro", - "application/vnd.macports.portpkg", - "application/vnd.mapbox-vector-tile", - "application/vnd.mcd", - "application/vnd.medcalcdata", - "application/vnd.mediastation.cdkey", - "application/vnd.mfer", - "application/vnd.mfmp", - "application/vnd.micrografx.flo", - "application/vnd.micrografx.igx", - "application/vnd.mif", - "application/vnd.mobius.daf", - "application/vnd.mobius.dis", - "application/vnd.mobius.mbk", - "application/vnd.mobius.mqy", - "application/vnd.mobius.msl", - "application/vnd.mobius.plc", - "application/vnd.mobius.txf", - "application/vnd.mophun.application", - "application/vnd.mophun.certificate", - "application/vnd.mozilla.xul+xml", - "application/vnd.ms-artgalry", - "application/vnd.ms-cab-compressed", - "application/vnd.ms-excel", - "application/vnd.ms-excel", - "application/vnd.ms-excel", - "application/vnd.ms-excel", - "application/vnd.ms-excel", - "application/vnd.ms-excel", - "application/vnd.ms-excel.addin.macroenabled.12", - "application/vnd.ms-excel.sheet.binary.macroenabled.12", - "application/vnd.ms-excel.sheet.macroenabled.12", - "application/vnd.ms-excel.template.macroenabled.12", - "application/vnd.ms-fontobject", - "application/vnd.ms-htmlhelp", - "application/vnd.ms-ims", - "application/vnd.ms-lrm", - "application/vnd.ms-officetheme", - "application/vnd.ms-outlook", - "application/vnd.ms-pki.seccat", - "model/stl", - "application/vnd.ms-powerpoint", - "application/vnd.ms-powerpoint", - "application/vnd.ms-powerpoint", - "application/vnd.ms-powerpoint.addin.macroenabled.12", - "application/vnd.ms-powerpoint.presentation.macroenabled.12", - "application/vnd.ms-powerpoint.slide.macroenabled.12", - "application/vnd.ms-powerpoint.slideshow.macroenabled.12", - "application/vnd.ms-powerpoint.template.macroenabled.12", - "application/vnd.ms-project", - "application/vnd.ms-word.document.macroenabled.12", - "application/vnd.ms-word.template.macroenabled.12", - "application/vnd.ms-works", - "application/vnd.ms-works", - "application/vnd.ms-works", - "application/vnd.ms-works", - "application/vnd.ms-wpl", - "application/vnd.ms-xpsdocument", - "application/vnd.mseq", - "application/vnd.musician", - "application/vnd.muvee.style", - "application/vnd.mynfc", - "application/vnd.neurolanguage.nlu", - "application/vnd.nitf", - "application/vnd.nitf", - "application/vnd.noblenet-directory", - "application/vnd.noblenet-sealer", - "application/vnd.noblenet-web", - "application/vnd.nokia.n-gage.data", - "application/vnd.nokia.n-gage.symbian.install", - "application/vnd.nokia.radio-preset", - "application/vnd.nokia.radio-presets", - "application/vnd.novadigm.edm", - "application/vnd.novadigm.edx", - "application/vnd.novadigm.ext", - "application/vnd.oasis.opendocument.chart", - "application/vnd.oasis.opendocument.chart-template", - "application/vnd.oasis.opendocument.database", - "application/vnd.oasis.opendocument.formula", - "application/vnd.oasis.opendocument.formula-template", - "application/vnd.oasis.opendocument.graphics", - "application/vnd.oasis.opendocument.graphics-template", - "application/vnd.oasis.opendocument.image", - "application/vnd.oasis.opendocument.image-template", - "application/vnd.oasis.opendocument.presentation", - "application/vnd.oasis.opendocument.presentation-template", - "application/vnd.oasis.opendocument.spreadsheet", - "application/vnd.oasis.opendocument.spreadsheet-template", - "application/vnd.oasis.opendocument.text", - "application/vnd.oasis.opendocument.text-master", - "application/vnd.oasis.opendocument.text-template", - "application/vnd.oasis.opendocument.text-web", - "application/vnd.olpc-sugar", - "application/vnd.oma.dd2+xml", - "application/vnd.openblox.game+xml", - "application/vnd.openofficeorg.extension", - "application/vnd.openstreetmap.data+xml", - "application/vnd.openxmlformats-officedocument.presentationml.presentation", - "application/vnd.openxmlformats-officedocument.presentationml.slide", - "application/vnd.openxmlformats-officedocument.presentationml.slideshow", - "application/vnd.openxmlformats-officedocument.presentationml.template", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "application/vnd.openxmlformats-officedocument.spreadsheetml.template", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "application/vnd.openxmlformats-officedocument.wordprocessingml.template", - "application/vnd.osgeo.mapguide.package", - "application/vnd.osgi.dp", - "application/vnd.osgi.subsystem", - "application/vnd.palm", - "application/vnd.palm", - "application/vnd.palm", - "application/vnd.pawaafile", - "application/vnd.pg.format", - "application/vnd.pg.osasli", - "application/vnd.picsel", - "application/vnd.pmi.widget", - "application/vnd.pocketlearn", - "application/vnd.powerbuilder6", - "application/vnd.previewsystems.box", - "application/vnd.proteus.magazine", - "application/vnd.publishare-delta-tree", - "application/vnd.pvi.ptid1", - "application/vnd.quark.quarkxpress", - "application/vnd.quark.quarkxpress", - "application/vnd.quark.quarkxpress", - "application/vnd.quark.quarkxpress", - "application/vnd.quark.quarkxpress", - "application/vnd.quark.quarkxpress", - "application/vnd.rar", - "application/vnd.realvnc.bed", - "application/vnd.recordare.musicxml", - "application/vnd.recordare.musicxml+xml", - "application/vnd.rig.cryptonote", - "application/vnd.rim.cod", - "application/vnd.rn-realmedia", - "application/vnd.rn-realmedia-vbr", - "application/vnd.route66.link66+xml", - "application/vnd.sailingtracker.track", - "application/vnd.seemail", - "application/vnd.sema", - "application/vnd.semd", - "application/vnd.semf", - "application/vnd.shana.informed.formdata", - "application/vnd.shana.informed.formtemplate", - "application/vnd.shana.informed.interchange", - "application/vnd.shana.informed.package", - "application/vnd.simtech-mindmapper", - "application/vnd.simtech-mindmapper", - "application/vnd.smaf", - "application/vnd.smart.teacher", - "application/vnd.software602.filler.form+xml", - "application/vnd.solent.sdkm+xml", - "application/vnd.solent.sdkm+xml", - "application/vnd.spotfire.dxp", - "application/vnd.spotfire.sfs", - "application/vnd.stardivision.calc", - "application/vnd.stardivision.draw", - "application/vnd.stardivision.impress", - "application/vnd.stardivision.math", - "application/vnd.stardivision.writer", - "application/vnd.stardivision.writer", - "application/vnd.stardivision.writer-global", - "application/vnd.stepmania.package", - "application/vnd.stepmania.stepchart", - "application/vnd.sun.wadl+xml", - "application/vnd.sun.xml.calc", - "application/vnd.sun.xml.calc.template", - "application/vnd.sun.xml.draw", - "application/vnd.sun.xml.draw.template", - "application/vnd.sun.xml.impress", - "application/vnd.sun.xml.impress.template", - "application/vnd.sun.xml.math", - "application/vnd.sun.xml.writer", - "application/vnd.sun.xml.writer.global", - "application/vnd.sun.xml.writer.template", - "application/vnd.sus-calendar", - "application/vnd.sus-calendar", - "application/vnd.svd", - "application/vnd.symbian.install", - "application/vnd.symbian.install", - "application/vnd.syncml+xml", - "application/vnd.syncml.dm+wbxml", - "application/vnd.syncml.dm+xml", - "application/vnd.syncml.dmddf+xml", - "application/vnd.tao.intent-module-archive", - "application/vnd.tcpdump.pcap", - "application/vnd.tcpdump.pcap", - "application/vnd.tcpdump.pcap", - "application/vnd.tmobile-livetv", - "application/vnd.trid.tpt", - "application/vnd.triscape.mxs", - "application/vnd.trueapp", - "application/vnd.ufdl", - "application/vnd.ufdl", - "application/vnd.uiq.theme", - "application/vnd.umajin", - "application/vnd.unity", - "application/vnd.uoml+xml", - "application/vnd.vcx", - "application/vnd.visio", - "application/vnd.visio", - "application/vnd.visio", - "application/vnd.visio", - "application/vnd.visionary", - "application/vnd.vsf", - "application/vnd.wap.wbxml", - "application/vnd.wap.wmlc", - "application/vnd.wap.wmlscriptc", - "application/vnd.webturbo", - "application/vnd.wolfram.player", - "application/vnd.wordperfect", - "application/vnd.wqd", - "application/vnd.wt.stf", - "application/vnd.xara", - "application/vnd.xfdl", - "application/vnd.yamaha.hv-dic", - "application/vnd.yamaha.hv-script", - "application/vnd.yamaha.hv-voice", - "application/vnd.yamaha.openscoreformat", - "application/vnd.yamaha.openscoreformat.osfpvg+xml", - "application/vnd.yamaha.smaf-audio", - "application/vnd.yamaha.smaf-phrase", - "application/vnd.yellowriver-custom-menu", - "application/vnd.zul", - "application/vnd.zul", - "application/vnd.zzazz.deck+xml", - "application/voicexml+xml", - "application/wasm", - "application/watcherinfo+xml", - "application/widget", - "application/winhlp", - "application/wsdl+xml", - "application/wspolicy+xml", - "application/x-7z-compressed", - "application/x-abiword", - "application/x-ace-compressed", - "application/x-arj", - "application/x-authorware-bin", - "application/x-authorware-bin", - "application/x-authorware-bin", - "application/x-authorware-bin", - "application/x-authorware-map", - "application/x-authorware-seg", - "application/x-bcpio", - "application/x-bittorrent", - "application/x-blorb", - "application/x-blorb", - "application/x-bzip", - "application/x-bzip2", - "application/x-bzip2", - "application/x-cbr", - "application/x-cbr", - "application/x-cbr", - "application/x-cbr", - "application/x-cbr", - "application/x-cdlink", - "application/x-cfs-compressed", - "application/x-chat", - "application/x-chess-pgn", - "application/x-chrome-extension", - "application/x-cocoa", - "application/x-conference", - "application/x-cpio", - "application/x-csh", - "application/x-debian-package", - "application/x-dgc-compressed", - "application/x-director", - "application/x-director", - "application/x-director", - "application/x-director", - "application/x-director", - "application/x-director", - "application/x-director", - "application/x-director", - "application/x-director", - "application/x-doom", - "application/x-dtbncx+xml", - "application/x-dtbook+xml", - "application/x-dtbresource+xml", - "application/x-dvi", - "application/x-envoy", - "application/x-eva", - "application/x-font-bdf", - "application/x-font-ghostscript", - "application/x-font-linux-psf", - "application/x-font-pcf", - "application/x-font-snf", - "application/x-font-type1", - "application/x-font-type1", - "application/x-font-type1", - "application/x-font-type1", - "application/x-freearc", - "application/x-futuresplash", - "application/x-gca-compressed", - "application/x-glulx", - "application/x-gnumeric", - "application/x-gramps-xml", - "application/x-gtar", - "application/x-hdf", - "application/x-httpd-php", - "application/x-install-instructions", - "application/x-java-archive-diff", - "application/x-java-jnlp-file", - "application/x-keepass2", - "application/x-latex", - "application/x-lua-bytecode", - "application/x-lzh-compressed", - "application/x-lzh-compressed", - "application/x-makeself", - "application/x-mie", - "application/x-mobipocket-ebook", - "application/x-mobipocket-ebook", - "application/x-ms-application", - "application/x-ms-shortcut", - "application/x-ms-wmd", - "application/x-ms-wmz", - "application/x-ms-xbap", - "application/x-msaccess", - "application/x-msbinder", - "application/x-mscardfile", - "application/x-msclip", - "application/x-msdownload", - "application/x-msdownload", - "application/x-msmediaview", - "application/x-msmediaview", - "application/x-msmediaview", - "image/wmf", - "image/emf", - "application/x-msmetafile", - "application/x-msmoney", - "application/x-mspublisher", - "application/x-msschedule", - "application/x-msterminal", - "application/x-mswrite", - "application/x-netcdf", - "application/x-netcdf", - "application/x-ns-proxy-autoconfig", - "application/x-nzb", - "application/x-perl", - "application/x-perl", - "application/x-pkcs12", - "application/x-pkcs12", - "application/x-pkcs7-certificates", - "application/x-pkcs7-certificates", - "application/x-pkcs7-certreqresp", - "application/x-redhat-package-manager", - "application/x-research-info-systems", - "application/x-sea", - "application/x-sh", - "application/x-shar", - "application/x-shockwave-flash", - "application/x-silverlight-app", - "application/x-sql", - "application/x-stuffit", - "application/x-stuffitx", - "application/x-subrip", - "application/x-sv4cpio", - "application/x-sv4crc", - "application/x-t3vm-image", - "application/x-tads", - "application/x-tar", - "application/x-tcl", - "application/x-tcl", - "application/x-tex", - "application/x-tex-tfm", - "application/x-texinfo", - "application/x-texinfo", - "model/obj", - "application/x-ustar", - "application/x-virtualbox-hdd", - "application/x-virtualbox-ova", - "application/x-virtualbox-ovf", - "application/x-virtualbox-vbox", - "application/x-virtualbox-vbox-extpack", - "application/x-virtualbox-vdi", - "application/x-virtualbox-vhd", - "application/x-virtualbox-vmdk", - "application/x-wais-source", - "application/x-web-app-manifest+json", - "application/x-x509-ca-cert", - "application/x-x509-ca-cert", - "application/x-x509-ca-cert", - "application/x-xfig", - "application/xliff+xml", - "application/x-xpinstall", - "application/x-xz", - "application/x-zmachine", - "application/x-zmachine", - "application/x-zmachine", - "application/x-zmachine", - "application/x-zmachine", - "application/x-zmachine", - "application/x-zmachine", - "application/x-zmachine", - "application/xaml+xml", - "application/xcap-att+xml", - "application/xcap-caps+xml", - "application/xcap-diff+xml", - "application/xcap-el+xml", - "application/xcap-ns+xml", - "application/xenc+xml", - "application/xhtml+xml", - "application/xhtml+xml", - "application/xml", - "application/xml", - "application/xml", - "application/xml", - "application/xml-dtd", - "application/xop+xml", - "application/xproc+xml", - "application/xslt+xml", - "application/xspf+xml", - "application/xv+xml", - "application/xv+xml", - "application/xv+xml", - "application/xv+xml", - "application/yang", - "application/yin+xml", - "application/zip", - "video/3gpp", - "audio/adpcm", - "audio/amr", - "audio/basic", - "audio/basic", - "audio/midi", - "audio/midi", - "audio/midi", - "audio/midi", - "audio/mobile-xmf", - "audio/mpeg", - "audio/mp4", - "audio/mp4", - "audio/mpeg", - "audio/mpeg", - "audio/mpeg", - "audio/mpeg", - "audio/mpeg", - "audio/ogg", - "audio/ogg", - "audio/ogg", - "audio/ogg", - "audio/s3m", - "audio/silk", - "audio/vnd.dece.audio", - "audio/vnd.dece.audio", - "audio/vnd.digital-winds", - "audio/vnd.dra", - "audio/vnd.dts", - "audio/vnd.dts.hd", - "audio/vnd.lucent.voice", - "audio/vnd.ms-playready.media.pya", - "audio/vnd.nuera.ecelp4800", - "audio/vnd.nuera.ecelp7470", - "audio/vnd.nuera.ecelp9600", - "audio/vnd.rip", - "audio/wave", - "audio/webm", - "audio/x-aac", - "audio/x-aiff", - "audio/x-aiff", - "audio/x-aiff", - "audio/x-caf", - "audio/x-flac", - "audio/x-matroska", - "audio/x-mpegurl", - "audio/x-ms-wax", - "audio/x-ms-wma", - "audio/x-pn-realaudio", - "audio/x-pn-realaudio", - "audio/x-pn-realaudio-plugin", - "audio/xm", - "chemical/x-cdx", - "chemical/x-cif", - "chemical/x-cmdf", - "chemical/x-cml", - "chemical/x-csml", - "chemical/x-xyz", - "font/collection", - "font/otf", - "font/ttf", - "font/woff", - "font/woff2", - "image/aces", - "image/apng", - "image/avci", - "image/avcs", - "image/avif", - "image/bmp", - "image/cgm", - "image/dicom-rle", - "image/fits", - "image/g3fax", - "image/gif", - "image/heic", - "image/heic-sequence", - "image/heif", - "image/heif-sequence", - "image/hej2k", - "image/hsj2", - "image/ief", - "image/jls", - "image/jp2", - "image/jp2", - "image/jpeg", - "image/jpeg", - "image/jpeg", - "image/jph", - "image/jphc", - "image/jpm", - "image/jpx", - "image/jpx", - "image/jxr", - "image/jxra", - "image/jxrs", - "image/jxs", - "image/jxsc", - "image/jxsi", - "image/jxss", - "image/ktx", - "image/ktx2", - "image/png", - "image/prs.btif", - "image/prs.pti", - "image/sgi", - "image/svg+xml", - "image/svg+xml", - "image/t38", - "image/tiff", - "image/tiff", - "image/tiff-fx", - "image/vnd.adobe.photoshop", - "image/vnd.airzip.accelerator.azv", - "image/vnd.dece.graphic", - "image/vnd.dece.graphic", - "image/vnd.dece.graphic", - "image/vnd.dece.graphic", - "image/vnd.djvu", - "image/vnd.djvu", - "text/vnd.dvb.subtitle", - "image/vnd.dwg", - "image/vnd.dxf", - "image/vnd.fastbidsheet", - "image/vnd.fpx", - "image/vnd.fst", - "image/vnd.fujixerox.edmics-mmr", - "image/vnd.fujixerox.edmics-rlc", - "image/vnd.microsoft.icon", - "image/vnd.ms-dds", - "image/vnd.ms-modi", - "image/vnd.ms-photo", - "image/vnd.net-fpx", - "image/vnd.pco.b16", - "image/vnd.tencent.tap", - "image/vnd.valve.source.texture", - "image/vnd.wap.wbmp", - "image/vnd.xiff", - "image/vnd.zbrush.pcx", - "image/webp", - "image/x-3ds", - "image/x-cmu-raster", - "image/x-cmx", - "image/x-freehand", - "image/x-freehand", - "image/x-freehand", - "image/x-freehand", - "image/x-freehand", - "image/x-jng", - "image/x-mrsid-image", - "image/x-pict", - "image/x-pict", - "image/x-portable-anymap", - "image/x-portable-bitmap", - "image/x-portable-graymap", - "image/x-portable-pixmap", - "image/x-rgb", - "image/x-tga", - "image/x-xbitmap", - "image/x-xpixmap", - "image/x-xwindowdump", - "message/disposition-notification", - "message/global", - "message/global-delivery-status", - "message/global-disposition-notification", - "message/global-headers", - "message/rfc822", - "message/rfc822", - "message/vnd.wfa.wsc", - "model/3mf", - "model/gltf+json", - "model/gltf-binary", - "model/iges", - "model/iges", - "model/mesh", - "model/mesh", - "model/mesh", - "model/mtl", - "model/step+xml", - "model/step+zip", - "model/step-xml+zip", - "model/vnd.collada+xml", - "model/vnd.dwf", - "model/vnd.gdl", - "model/vnd.gtw", - "model/vnd.mts", - "model/vnd.opengex", - "model/vnd.parasolid.transmit.binary", - "model/vnd.parasolid.transmit.text", - "model/vnd.sap.vds", - "model/vnd.usdz+zip", - "model/vnd.valve.source.compiled-map", - "model/vnd.vtu", - "model/vrml", - "model/vrml", - "model/x3d+fastinfoset", - "model/x3d+binary", - "model/x3d-vrml", - "model/x3d+vrml", - "model/x3d+xml", - "model/x3d+xml", - "text/cache-manifest", - "text/cache-manifest", - "text/calendar", - "text/calendar", - "text/coffeescript", - "text/coffeescript", - "text/css", - "text/csv", - "text/html", - "text/html", - "text/html", - "text/jade", - "text/jsx", - "text/less", - "text/markdown", - "text/markdown", - "text/mathml", - "text/mdx", - "text/n3", - "text/plain", - "text/plain", - "text/plain", - "text/plain", - "text/plain", - "text/plain", - "text/plain", - "text/plain", - "text/prs.lines.tag", - "text/richtext", - "text/sgml", - "text/sgml", - "text/shex", - "text/slim", - "text/slim", - "text/spdx", - "text/stylus", - "text/stylus", - "text/tab-separated-values", - "text/troff", - "text/troff", - "text/troff", - "text/troff", - "text/troff", - "text/troff", - "text/turtle", - "text/uri-list", - "text/uri-list", - "text/uri-list", - "text/vcard", - "text/vnd.curl", - "text/vnd.curl.dcurl", - "text/vnd.curl.mcurl", - "text/vnd.curl.scurl", - "text/vnd.familysearch.gedcom", - "text/vnd.fly", - "text/vnd.fmi.flexstor", - "text/vnd.graphviz", - "text/vnd.in3d.3dml", - "text/vnd.in3d.spot", - "text/vnd.sun.j2me.app-descriptor", - "text/vnd.wap.wml", - "text/vnd.wap.wmlscript", - "text/vtt", - "text/x-asm", - "text/x-asm", - "text/x-c", - "text/x-c", - "text/x-c", - "text/x-c", - "text/x-c", - "text/x-c", - "text/x-c", - "text/x-component", - "text/x-fortran", - "text/x-fortran", - "text/x-fortran", - "text/x-fortran", - "text/x-handlebars-template", - "text/x-java-source", - "text/x-lua", - "text/x-markdown", - "text/x-nfo", - "text/x-opml", - "text/x-pascal", - "text/x-pascal", - "text/x-processing", - "text/x-sass", - "text/x-scss", - "text/x-setext", - "text/x-sfv", - "text/x-suse-ymp", - "text/x-uuencode", - "text/x-vcalendar", - "text/x-vcard", - "text/yaml", - "text/yaml", - "video/3gpp", - "video/3gpp2", - "video/h261", - "video/h263", - "video/h264", - "video/iso.segment", - "video/jpeg", - "video/jpm", - "video/mj2", - "video/mj2", - "video/mp2t", - "video/mp4", - "video/mp4", - "video/mp4", - "video/mpeg", - "video/mpeg", - "video/mpeg", - "video/mpeg", - "video/mpeg", - "video/ogg", - "video/quicktime", - "video/quicktime", - "video/vnd.dece.hd", - "video/vnd.dece.hd", - "video/vnd.dece.mobile", - "video/vnd.dece.mobile", - "video/vnd.dece.pd", - "video/vnd.dece.pd", - "video/vnd.dece.sd", - "video/vnd.dece.sd", - "video/vnd.dece.video", - "video/vnd.dece.video", - "video/vnd.dvb.file", - "video/vnd.fvt", - "video/vnd.mpegurl", - "video/vnd.mpegurl", - "video/vnd.ms-playready.media.pyv", - "video/vnd.uvvu.mp4", - "video/vnd.uvvu.mp4", - "video/vnd.vivo", - "video/webm", - "video/x-f4v", - "video/x-fli", - "video/x-flv", - "video/x-m4v", - "video/x-matroska", - "video/x-matroska", - "video/x-matroska", - "video/x-mng", - "video/x-ms-asf", - "video/x-ms-asf", - "video/x-ms-vob", - "video/x-ms-wm", - "video/x-ms-wmv", - "video/x-ms-wmx", - "video/x-ms-wvx", - "video/x-msvideo", - "video/x-sgi-movie", - "video/x-smv", - "x-conference/x-cooltalk" - ] + "required": [ + "max_options", + "max_option_characters", + "min_duration_seconds", + "max_duration_seconds" + ], + "additionalProperties": false }, - "challenges": { + "emails": { "type": "object", "properties": { - "enabled": { + "disallow_tempmail": { "type": "boolean", - "default": true + "default": false, + "description": "Blocks over 10,000 common tempmail domains" }, + "disallowed_domains": { + "type": "array", + "items": { + "$ref": "#/properties/http/properties/banned_user_agents/items" + }, + "default": [] + } + }, + "required": ["disallow_tempmail", "disallowed_domains"], + "additionalProperties": false + }, + "challenges": { + "type": "object", + "properties": { "difficulty": { "type": "integer", "exclusiveMinimum": 0, @@ -1846,1278 +1877,75 @@ "default": 300 }, "key": { - "type": "string", - "default": "" + "$ref": "#/properties/postgres/properties/password", + "description": "You can use PATH:/path/to/file to load this value from a file" } }, + "required": ["difficulty", "expiration", "key"], "additionalProperties": false, - "default": { - "enabled": true, - "difficulty": 50000, - "expiration": 300, - "key": "" - } + "description": "CAPTCHA challenge configuration. Challenges are disabled if not provided." + }, + "filters": { + "type": "object", + "properties": { + "note_content": { + "type": "array", + "items": { + "$ref": "#/properties/http/properties/banned_user_agents/items" + }, + "default": [] + }, + "emoji_shortcode": { + "type": "array", + "items": { + "$ref": "#/properties/http/properties/banned_user_agents/items" + }, + "default": [] + }, + "username": { + "type": "array", + "items": { + "$ref": "#/properties/http/properties/banned_user_agents/items" + }, + "default": [] + }, + "displayname": { + "type": "array", + "items": { + "$ref": "#/properties/http/properties/banned_user_agents/items" + }, + "default": [] + }, + "bio": { + "type": "array", + "items": { + "$ref": "#/properties/http/properties/banned_user_agents/items" + }, + "default": [] + } + }, + "required": [ + "note_content", + "emoji_shortcode", + "username", + "displayname", + "bio" + ], + "additionalProperties": false, + "description": "Block content that matches these regular expressions" } }, - "additionalProperties": false, - "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_emoji_size": 1000000, - "max_emoji_shortcode_size": 100, - "max_emoji_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": [ - "application/vnd.lotus-1-2-3", - "application/andrew-inset", - "application/applixware", - "application/atom+xml", - "application/atomcat+xml", - "application/atomdeleted+xml", - "application/atomsvc+xml", - "application/atsc-dwd+xml", - "application/atsc-held+xml", - "application/atsc-rsat+xml", - "application/bdoc", - "application/calendar+xml", - "application/ccxml+xml", - "application/cdfx+xml", - "application/cdmi-capability", - "application/cdmi-container", - "application/cdmi-domain", - "application/cdmi-object", - "application/cdmi-queue", - "application/cpl+xml", - "application/cu-seeme", - "application/dash+xml", - "application/dash-patch+xml", - "application/davmount+xml", - "application/docbook+xml", - "application/dssc+der", - "application/dssc+xml", - "application/ecmascript", - "application/ecmascript", - "application/emma+xml", - "application/emotionml+xml", - "application/epub+zip", - "application/exi", - "application/express", - "application/fdt+xml", - "application/font-tdpfr", - "application/geo+json", - "application/gml+xml", - "application/gpx+xml", - "application/gxf", - "application/gzip", - "application/hjson", - "application/hyperstudio", - "application/inkml+xml", - "application/inkml+xml", - "application/ipfix", - "application/its+xml", - "application/java-archive", - "application/java-archive", - "application/java-archive", - "application/java-serialized-object", - "application/java-vm", - "application/javascript", - "application/javascript", - "application/json", - "application/json", - "application/json5", - "application/jsonml+json", - "application/ld+json", - "application/lgr+xml", - "application/lost+xml", - "application/mac-binhex40", - "application/mac-compactpro", - "application/mads+xml", - "application/manifest+json", - "application/marc", - "application/marcxml+xml", - "application/mathematica", - "application/mathematica", - "application/mathematica", - "application/mathml+xml", - "application/mbox", - "application/media-policy-dataset+xml", - "application/mediaservercontrol+xml", - "application/metalink+xml", - "application/metalink4+xml", - "application/mets+xml", - "application/mmt-aei+xml", - "application/mmt-usd+xml", - "application/mods+xml", - "application/mp21", - "application/mp21", - "application/mp4", - "application/mp4", - "application/msword", - "application/msword", - "application/mxf", - "application/n-quads", - "application/n-triples", - "application/node", - "application/octet-stream", - "application/octet-stream", - "application/octet-stream", - "application/octet-stream", - "application/octet-stream", - "application/octet-stream", - "application/octet-stream", - "application/octet-stream", - "application/octet-stream", - "application/octet-stream", - "application/octet-stream", - "application/octet-stream", - "application/x-msdos-program", - "application/x-msdownload", - "application/x-debian-package", - "application/x-apple-diskimage", - "application/x-iso9660-image", - "application/octet-stream", - "application/x-msdownload", - "application/octet-stream", - "application/octet-stream", - "application/octet-stream", - "application/oda", - "application/oebps-package+xml", - "application/ogg", - "application/omdoc+xml", - "application/onenote", - "application/onenote", - "application/onenote", - "application/onenote", - "application/oxps", - "application/p2p-overlay+xml", - "application/patch-ops-error+xml", - "application/pdf", - "application/pgp-encrypted", - "application/pgp-keys", - "application/pgp-signature", - "application/pics-rules", - "application/pkcs10", - "application/pkcs7-mime", - "application/pkcs7-mime", - "application/pkcs7-signature", - "application/pkcs8", - "application/pkix-attr-cert", - "application/pkix-cert", - "application/pkix-crl", - "application/pkix-pkipath", - "application/pkixcmp", - "application/pls+xml", - "application/postscript", - "application/postscript", - "application/postscript", - "application/provenance+xml", - "application/prs.cww", - "application/pskc+xml", - "application/raml+yaml", - "application/rdf+xml", - "application/rdf+xml", - "application/reginfo+xml", - "application/relax-ng-compact-syntax", - "application/resource-lists+xml", - "application/resource-lists-diff+xml", - "application/rls-services+xml", - "application/route-apd+xml", - "application/route-s-tsid+xml", - "application/route-usd+xml", - "application/rpki-ghostbusters", - "application/rpki-manifest", - "application/rpki-roa", - "application/rsd+xml", - "application/rss+xml", - "application/rtf", - "application/sbml+xml", - "application/scvp-cv-request", - "application/scvp-cv-response", - "application/scvp-vp-request", - "application/scvp-vp-response", - "application/sdp", - "application/senml+xml", - "application/sensml+xml", - "application/set-payment-initiation", - "application/set-registration-initiation", - "application/shf+xml", - "application/sieve", - "application/sieve", - "application/smil+xml", - "application/smil+xml", - "application/sparql-query", - "application/sparql-results+xml", - "application/srgs", - "application/srgs+xml", - "application/sru+xml", - "application/ssdl+xml", - "application/ssml+xml", - "application/swid+xml", - "application/tei+xml", - "application/tei+xml", - "application/thraud+xml", - "application/timestamped-data", - "application/toml", - "application/trig", - "application/ttml+xml", - "application/ubjson", - "application/urc-ressheet+xml", - "application/urc-targetdesc+xml", - "application/vnd.1000minds.decision-model+xml", - "application/vnd.3gpp.pic-bw-large", - "application/vnd.3gpp.pic-bw-small", - "application/vnd.3gpp.pic-bw-var", - "application/vnd.3gpp2.tcap", - "application/vnd.3m.post-it-notes", - "application/vnd.accpac.simply.aso", - "application/vnd.accpac.simply.imp", - "application/vnd.acucobol", - "application/vnd.acucorp", - "application/vnd.acucorp", - "application/vnd.adobe.air-application-installer-package+zip", - "application/vnd.adobe.formscentral.fcdt", - "application/vnd.adobe.fxp", - "application/vnd.adobe.fxp", - "application/vnd.adobe.xdp+xml", - "application/vnd.adobe.xfdf", - "application/vnd.age", - "application/vnd.ahead.space", - "application/vnd.airzip.filesecure.azf", - "application/vnd.airzip.filesecure.azs", - "application/vnd.amazon.ebook", - "application/vnd.americandynamics.acc", - "application/vnd.amiga.ami", - "application/vnd.android.package-archive", - "application/vnd.anser-web-certificate-issue-initiation", - "application/vnd.anser-web-funds-transfer-initiation", - "application/vnd.antix.game-component", - "application/vnd.apple.installer+xml", - "application/vnd.apple.keynote", - "application/vnd.apple.mpegurl", - "application/vnd.apple.numbers", - "application/vnd.apple.pages", - "application/vnd.apple.pkpass", - "application/vnd.aristanetworks.swi", - "application/vnd.astraea-software.iota", - "application/vnd.audiograph", - "application/vnd.balsamiq.bmml+xml", - "application/vnd.blueice.multipass", - "application/vnd.bmi", - "application/vnd.businessobjects", - "application/vnd.chemdraw+xml", - "application/vnd.chipnuts.karaoke-mmd", - "application/vnd.cinderella", - "application/vnd.citationstyles.style+xml", - "application/vnd.claymore", - "application/vnd.cloanto.rp9", - "application/vnd.clonk.c4group", - "application/vnd.clonk.c4group", - "application/vnd.clonk.c4group", - "application/vnd.clonk.c4group", - "application/vnd.clonk.c4group", - "application/vnd.cluetrust.cartomobile-config", - "application/vnd.cluetrust.cartomobile-config-pkg", - "application/vnd.commonspace", - "application/vnd.contact.cmsg", - "application/vnd.cosmocaller", - "application/vnd.crick.clicker", - "application/vnd.crick.clicker.keyboard", - "application/vnd.crick.clicker.palette", - "application/vnd.crick.clicker.template", - "application/vnd.crick.clicker.wordbank", - "application/vnd.criticaltools.wbs+xml", - "application/vnd.ctc-posml", - "application/vnd.cups-ppd", - "application/vnd.curl.car", - "application/vnd.curl.pcurl", - "application/vnd.dart", - "application/vnd.data-vision.rdz", - "application/vnd.dbf", - "application/vnd.dece.data", - "application/vnd.dece.data", - "application/vnd.dece.data", - "application/vnd.dece.data", - "application/vnd.dece.ttml+xml", - "application/vnd.dece.ttml+xml", - "application/vnd.dece.unspecified", - "application/vnd.dece.unspecified", - "application/vnd.dece.zip", - "application/vnd.dece.zip", - "application/vnd.denovo.fcselayout-link", - "application/vnd.dna", - "application/vnd.dolby.mlp", - "application/vnd.dpgraph", - "application/vnd.dreamfactory", - "application/vnd.ds-keypoint", - "application/vnd.dvb.ait", - "application/vnd.dvb.service", - "application/vnd.dynageo", - "application/vnd.ecowin.chart", - "application/vnd.enliven", - "application/vnd.epson.esf", - "application/vnd.epson.msf", - "application/vnd.epson.quickanime", - "application/vnd.epson.salt", - "application/vnd.epson.ssf", - "application/vnd.eszigno3+xml", - "application/vnd.eszigno3+xml", - "application/vnd.ezpix-album", - "application/vnd.ezpix-package", - "application/vnd.fdf", - "application/vnd.fdsn.mseed", - "application/vnd.fdsn.seed", - "application/vnd.fdsn.seed", - "application/vnd.flographit", - "application/vnd.fluxtime.clip", - "application/vnd.framemaker", - "application/vnd.framemaker", - "application/vnd.framemaker", - "application/vnd.framemaker", - "application/vnd.frogans.fnc", - "application/vnd.frogans.ltf", - "application/vnd.fsc.weblaunch", - "application/vnd.fujitsu.oasys", - "application/vnd.fujitsu.oasys2", - "application/vnd.fujitsu.oasys3", - "application/vnd.fujitsu.oasysgp", - "application/vnd.fujitsu.oasysprs", - "application/vnd.fujixerox.ddd", - "application/vnd.fujixerox.docuworks", - "application/vnd.fujixerox.docuworks.binder", - "application/vnd.fuzzysheet", - "application/vnd.genomatix.tuxedo", - "application/vnd.geogebra.file", - "application/vnd.geogebra.tool", - "application/vnd.geometry-explorer", - "application/vnd.geometry-explorer", - "application/vnd.geonext", - "application/vnd.geoplan", - "application/vnd.geospace", - "application/vnd.gmx", - "application/vnd.google-apps.document", - "application/vnd.google-apps.presentation", - "application/vnd.google-apps.spreadsheet", - "application/vnd.google-earth.kml+xml", - "application/vnd.google-earth.kmz", - "application/vnd.grafeq", - "application/vnd.grafeq", - "application/vnd.groove-account", - "application/vnd.groove-help", - "application/vnd.groove-identity-message", - "application/vnd.groove-injector", - "application/vnd.groove-tool-message", - "application/vnd.groove-tool-template", - "application/vnd.groove-vcard", - "application/vnd.hal+xml", - "application/vnd.handheld-entertainment+xml", - "application/vnd.hbci", - "application/vnd.hhe.lesson-player", - "application/vnd.hp-hpgl", - "application/vnd.hp-hpid", - "application/vnd.hp-hps", - "application/vnd.hp-jlyt", - "application/vnd.hp-pcl", - "application/vnd.hp-pclxl", - "application/vnd.hydrostatix.sof-data", - "application/vnd.ibm.minipay", - "application/vnd.ibm.modcap", - "application/vnd.ibm.modcap", - "application/vnd.ibm.modcap", - "application/vnd.ibm.rights-management", - "application/vnd.ibm.secure-container", - "application/vnd.iccprofile", - "application/vnd.iccprofile", - "application/vnd.igloader", - "application/vnd.immervision-ivp", - "application/vnd.immervision-ivu", - "application/vnd.insors.igm", - "application/vnd.intercon.formnet", - "application/vnd.intercon.formnet", - "application/vnd.intergeo", - "application/vnd.intu.qbo", - "application/vnd.intu.qfx", - "application/vnd.ipunplugged.rcprofile", - "application/vnd.irepository.package+xml", - "application/vnd.is-xpr", - "application/vnd.isac.fcs", - "application/vnd.jam", - "application/vnd.jcp.javame.midlet-rms", - "application/vnd.jisp", - "application/vnd.joost.joda-archive", - "application/vnd.kahootz", - "application/vnd.kahootz", - "application/vnd.kde.karbon", - "application/vnd.kde.kchart", - "application/vnd.kde.kformula", - "application/vnd.kde.kivio", - "application/vnd.kde.kontour", - "application/vnd.kde.kpresenter", - "application/vnd.kde.kpresenter", - "application/vnd.kde.kspread", - "application/vnd.kde.kword", - "application/vnd.kde.kword", - "application/vnd.kenameaapp", - "application/vnd.kidspiration", - "application/vnd.kinar", - "application/vnd.kinar", - "application/vnd.koan", - "application/vnd.koan", - "application/vnd.koan", - "application/vnd.koan", - "application/vnd.kodak-descriptor", - "application/vnd.las.las+xml", - "application/vnd.llamagraphics.life-balance.desktop", - "application/vnd.llamagraphics.life-balance.exchange+xml", - "application/vnd.lotus-approach", - "application/vnd.lotus-freelance", - "application/vnd.lotus-notes", - "application/vnd.lotus-organizer", - "application/vnd.lotus-screencam", - "application/vnd.lotus-wordpro", - "application/vnd.macports.portpkg", - "application/vnd.mapbox-vector-tile", - "application/vnd.mcd", - "application/vnd.medcalcdata", - "application/vnd.mediastation.cdkey", - "application/vnd.mfer", - "application/vnd.mfmp", - "application/vnd.micrografx.flo", - "application/vnd.micrografx.igx", - "application/vnd.mif", - "application/vnd.mobius.daf", - "application/vnd.mobius.dis", - "application/vnd.mobius.mbk", - "application/vnd.mobius.mqy", - "application/vnd.mobius.msl", - "application/vnd.mobius.plc", - "application/vnd.mobius.txf", - "application/vnd.mophun.application", - "application/vnd.mophun.certificate", - "application/vnd.mozilla.xul+xml", - "application/vnd.ms-artgalry", - "application/vnd.ms-cab-compressed", - "application/vnd.ms-excel", - "application/vnd.ms-excel", - "application/vnd.ms-excel", - "application/vnd.ms-excel", - "application/vnd.ms-excel", - "application/vnd.ms-excel", - "application/vnd.ms-excel.addin.macroenabled.12", - "application/vnd.ms-excel.sheet.binary.macroenabled.12", - "application/vnd.ms-excel.sheet.macroenabled.12", - "application/vnd.ms-excel.template.macroenabled.12", - "application/vnd.ms-fontobject", - "application/vnd.ms-htmlhelp", - "application/vnd.ms-ims", - "application/vnd.ms-lrm", - "application/vnd.ms-officetheme", - "application/vnd.ms-outlook", - "application/vnd.ms-pki.seccat", - "model/stl", - "application/vnd.ms-powerpoint", - "application/vnd.ms-powerpoint", - "application/vnd.ms-powerpoint", - "application/vnd.ms-powerpoint.addin.macroenabled.12", - "application/vnd.ms-powerpoint.presentation.macroenabled.12", - "application/vnd.ms-powerpoint.slide.macroenabled.12", - "application/vnd.ms-powerpoint.slideshow.macroenabled.12", - "application/vnd.ms-powerpoint.template.macroenabled.12", - "application/vnd.ms-project", - "application/vnd.ms-word.document.macroenabled.12", - "application/vnd.ms-word.template.macroenabled.12", - "application/vnd.ms-works", - "application/vnd.ms-works", - "application/vnd.ms-works", - "application/vnd.ms-works", - "application/vnd.ms-wpl", - "application/vnd.ms-xpsdocument", - "application/vnd.mseq", - "application/vnd.musician", - "application/vnd.muvee.style", - "application/vnd.mynfc", - "application/vnd.neurolanguage.nlu", - "application/vnd.nitf", - "application/vnd.nitf", - "application/vnd.noblenet-directory", - "application/vnd.noblenet-sealer", - "application/vnd.noblenet-web", - "application/vnd.nokia.n-gage.data", - "application/vnd.nokia.n-gage.symbian.install", - "application/vnd.nokia.radio-preset", - "application/vnd.nokia.radio-presets", - "application/vnd.novadigm.edm", - "application/vnd.novadigm.edx", - "application/vnd.novadigm.ext", - "application/vnd.oasis.opendocument.chart", - "application/vnd.oasis.opendocument.chart-template", - "application/vnd.oasis.opendocument.database", - "application/vnd.oasis.opendocument.formula", - "application/vnd.oasis.opendocument.formula-template", - "application/vnd.oasis.opendocument.graphics", - "application/vnd.oasis.opendocument.graphics-template", - "application/vnd.oasis.opendocument.image", - "application/vnd.oasis.opendocument.image-template", - "application/vnd.oasis.opendocument.presentation", - "application/vnd.oasis.opendocument.presentation-template", - "application/vnd.oasis.opendocument.spreadsheet", - "application/vnd.oasis.opendocument.spreadsheet-template", - "application/vnd.oasis.opendocument.text", - "application/vnd.oasis.opendocument.text-master", - "application/vnd.oasis.opendocument.text-template", - "application/vnd.oasis.opendocument.text-web", - "application/vnd.olpc-sugar", - "application/vnd.oma.dd2+xml", - "application/vnd.openblox.game+xml", - "application/vnd.openofficeorg.extension", - "application/vnd.openstreetmap.data+xml", - "application/vnd.openxmlformats-officedocument.presentationml.presentation", - "application/vnd.openxmlformats-officedocument.presentationml.slide", - "application/vnd.openxmlformats-officedocument.presentationml.slideshow", - "application/vnd.openxmlformats-officedocument.presentationml.template", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - "application/vnd.openxmlformats-officedocument.spreadsheetml.template", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "application/vnd.openxmlformats-officedocument.wordprocessingml.template", - "application/vnd.osgeo.mapguide.package", - "application/vnd.osgi.dp", - "application/vnd.osgi.subsystem", - "application/vnd.palm", - "application/vnd.palm", - "application/vnd.palm", - "application/vnd.pawaafile", - "application/vnd.pg.format", - "application/vnd.pg.osasli", - "application/vnd.picsel", - "application/vnd.pmi.widget", - "application/vnd.pocketlearn", - "application/vnd.powerbuilder6", - "application/vnd.previewsystems.box", - "application/vnd.proteus.magazine", - "application/vnd.publishare-delta-tree", - "application/vnd.pvi.ptid1", - "application/vnd.quark.quarkxpress", - "application/vnd.quark.quarkxpress", - "application/vnd.quark.quarkxpress", - "application/vnd.quark.quarkxpress", - "application/vnd.quark.quarkxpress", - "application/vnd.quark.quarkxpress", - "application/vnd.rar", - "application/vnd.realvnc.bed", - "application/vnd.recordare.musicxml", - "application/vnd.recordare.musicxml+xml", - "application/vnd.rig.cryptonote", - "application/vnd.rim.cod", - "application/vnd.rn-realmedia", - "application/vnd.rn-realmedia-vbr", - "application/vnd.route66.link66+xml", - "application/vnd.sailingtracker.track", - "application/vnd.seemail", - "application/vnd.sema", - "application/vnd.semd", - "application/vnd.semf", - "application/vnd.shana.informed.formdata", - "application/vnd.shana.informed.formtemplate", - "application/vnd.shana.informed.interchange", - "application/vnd.shana.informed.package", - "application/vnd.simtech-mindmapper", - "application/vnd.simtech-mindmapper", - "application/vnd.smaf", - "application/vnd.smart.teacher", - "application/vnd.software602.filler.form+xml", - "application/vnd.solent.sdkm+xml", - "application/vnd.solent.sdkm+xml", - "application/vnd.spotfire.dxp", - "application/vnd.spotfire.sfs", - "application/vnd.stardivision.calc", - "application/vnd.stardivision.draw", - "application/vnd.stardivision.impress", - "application/vnd.stardivision.math", - "application/vnd.stardivision.writer", - "application/vnd.stardivision.writer", - "application/vnd.stardivision.writer-global", - "application/vnd.stepmania.package", - "application/vnd.stepmania.stepchart", - "application/vnd.sun.wadl+xml", - "application/vnd.sun.xml.calc", - "application/vnd.sun.xml.calc.template", - "application/vnd.sun.xml.draw", - "application/vnd.sun.xml.draw.template", - "application/vnd.sun.xml.impress", - "application/vnd.sun.xml.impress.template", - "application/vnd.sun.xml.math", - "application/vnd.sun.xml.writer", - "application/vnd.sun.xml.writer.global", - "application/vnd.sun.xml.writer.template", - "application/vnd.sus-calendar", - "application/vnd.sus-calendar", - "application/vnd.svd", - "application/vnd.symbian.install", - "application/vnd.symbian.install", - "application/vnd.syncml+xml", - "application/vnd.syncml.dm+wbxml", - "application/vnd.syncml.dm+xml", - "application/vnd.syncml.dmddf+xml", - "application/vnd.tao.intent-module-archive", - "application/vnd.tcpdump.pcap", - "application/vnd.tcpdump.pcap", - "application/vnd.tcpdump.pcap", - "application/vnd.tmobile-livetv", - "application/vnd.trid.tpt", - "application/vnd.triscape.mxs", - "application/vnd.trueapp", - "application/vnd.ufdl", - "application/vnd.ufdl", - "application/vnd.uiq.theme", - "application/vnd.umajin", - "application/vnd.unity", - "application/vnd.uoml+xml", - "application/vnd.vcx", - "application/vnd.visio", - "application/vnd.visio", - "application/vnd.visio", - "application/vnd.visio", - "application/vnd.visionary", - "application/vnd.vsf", - "application/vnd.wap.wbxml", - "application/vnd.wap.wmlc", - "application/vnd.wap.wmlscriptc", - "application/vnd.webturbo", - "application/vnd.wolfram.player", - "application/vnd.wordperfect", - "application/vnd.wqd", - "application/vnd.wt.stf", - "application/vnd.xara", - "application/vnd.xfdl", - "application/vnd.yamaha.hv-dic", - "application/vnd.yamaha.hv-script", - "application/vnd.yamaha.hv-voice", - "application/vnd.yamaha.openscoreformat", - "application/vnd.yamaha.openscoreformat.osfpvg+xml", - "application/vnd.yamaha.smaf-audio", - "application/vnd.yamaha.smaf-phrase", - "application/vnd.yellowriver-custom-menu", - "application/vnd.zul", - "application/vnd.zul", - "application/vnd.zzazz.deck+xml", - "application/voicexml+xml", - "application/wasm", - "application/watcherinfo+xml", - "application/widget", - "application/winhlp", - "application/wsdl+xml", - "application/wspolicy+xml", - "application/x-7z-compressed", - "application/x-abiword", - "application/x-ace-compressed", - "application/x-arj", - "application/x-authorware-bin", - "application/x-authorware-bin", - "application/x-authorware-bin", - "application/x-authorware-bin", - "application/x-authorware-map", - "application/x-authorware-seg", - "application/x-bcpio", - "application/x-bittorrent", - "application/x-blorb", - "application/x-blorb", - "application/x-bzip", - "application/x-bzip2", - "application/x-bzip2", - "application/x-cbr", - "application/x-cbr", - "application/x-cbr", - "application/x-cbr", - "application/x-cbr", - "application/x-cdlink", - "application/x-cfs-compressed", - "application/x-chat", - "application/x-chess-pgn", - "application/x-chrome-extension", - "application/x-cocoa", - "application/x-conference", - "application/x-cpio", - "application/x-csh", - "application/x-debian-package", - "application/x-dgc-compressed", - "application/x-director", - "application/x-director", - "application/x-director", - "application/x-director", - "application/x-director", - "application/x-director", - "application/x-director", - "application/x-director", - "application/x-director", - "application/x-doom", - "application/x-dtbncx+xml", - "application/x-dtbook+xml", - "application/x-dtbresource+xml", - "application/x-dvi", - "application/x-envoy", - "application/x-eva", - "application/x-font-bdf", - "application/x-font-ghostscript", - "application/x-font-linux-psf", - "application/x-font-pcf", - "application/x-font-snf", - "application/x-font-type1", - "application/x-font-type1", - "application/x-font-type1", - "application/x-font-type1", - "application/x-freearc", - "application/x-futuresplash", - "application/x-gca-compressed", - "application/x-glulx", - "application/x-gnumeric", - "application/x-gramps-xml", - "application/x-gtar", - "application/x-hdf", - "application/x-httpd-php", - "application/x-install-instructions", - "application/x-java-archive-diff", - "application/x-java-jnlp-file", - "application/x-keepass2", - "application/x-latex", - "application/x-lua-bytecode", - "application/x-lzh-compressed", - "application/x-lzh-compressed", - "application/x-makeself", - "application/x-mie", - "application/x-mobipocket-ebook", - "application/x-mobipocket-ebook", - "application/x-ms-application", - "application/x-ms-shortcut", - "application/x-ms-wmd", - "application/x-ms-wmz", - "application/x-ms-xbap", - "application/x-msaccess", - "application/x-msbinder", - "application/x-mscardfile", - "application/x-msclip", - "application/x-msdownload", - "application/x-msdownload", - "application/x-msmediaview", - "application/x-msmediaview", - "application/x-msmediaview", - "image/wmf", - "image/emf", - "application/x-msmetafile", - "application/x-msmoney", - "application/x-mspublisher", - "application/x-msschedule", - "application/x-msterminal", - "application/x-mswrite", - "application/x-netcdf", - "application/x-netcdf", - "application/x-ns-proxy-autoconfig", - "application/x-nzb", - "application/x-perl", - "application/x-perl", - "application/x-pkcs12", - "application/x-pkcs12", - "application/x-pkcs7-certificates", - "application/x-pkcs7-certificates", - "application/x-pkcs7-certreqresp", - "application/x-redhat-package-manager", - "application/x-research-info-systems", - "application/x-sea", - "application/x-sh", - "application/x-shar", - "application/x-shockwave-flash", - "application/x-silverlight-app", - "application/x-sql", - "application/x-stuffit", - "application/x-stuffitx", - "application/x-subrip", - "application/x-sv4cpio", - "application/x-sv4crc", - "application/x-t3vm-image", - "application/x-tads", - "application/x-tar", - "application/x-tcl", - "application/x-tcl", - "application/x-tex", - "application/x-tex-tfm", - "application/x-texinfo", - "application/x-texinfo", - "model/obj", - "application/x-ustar", - "application/x-virtualbox-hdd", - "application/x-virtualbox-ova", - "application/x-virtualbox-ovf", - "application/x-virtualbox-vbox", - "application/x-virtualbox-vbox-extpack", - "application/x-virtualbox-vdi", - "application/x-virtualbox-vhd", - "application/x-virtualbox-vmdk", - "application/x-wais-source", - "application/x-web-app-manifest+json", - "application/x-x509-ca-cert", - "application/x-x509-ca-cert", - "application/x-x509-ca-cert", - "application/x-xfig", - "application/xliff+xml", - "application/x-xpinstall", - "application/x-xz", - "application/x-zmachine", - "application/x-zmachine", - "application/x-zmachine", - "application/x-zmachine", - "application/x-zmachine", - "application/x-zmachine", - "application/x-zmachine", - "application/x-zmachine", - "application/xaml+xml", - "application/xcap-att+xml", - "application/xcap-caps+xml", - "application/xcap-diff+xml", - "application/xcap-el+xml", - "application/xcap-ns+xml", - "application/xenc+xml", - "application/xhtml+xml", - "application/xhtml+xml", - "application/xml", - "application/xml", - "application/xml", - "application/xml", - "application/xml-dtd", - "application/xop+xml", - "application/xproc+xml", - "application/xslt+xml", - "application/xspf+xml", - "application/xv+xml", - "application/xv+xml", - "application/xv+xml", - "application/xv+xml", - "application/yang", - "application/yin+xml", - "application/zip", - "video/3gpp", - "audio/adpcm", - "audio/amr", - "audio/basic", - "audio/basic", - "audio/midi", - "audio/midi", - "audio/midi", - "audio/midi", - "audio/mobile-xmf", - "audio/mpeg", - "audio/mp4", - "audio/mp4", - "audio/mpeg", - "audio/mpeg", - "audio/mpeg", - "audio/mpeg", - "audio/mpeg", - "audio/ogg", - "audio/ogg", - "audio/ogg", - "audio/ogg", - "audio/s3m", - "audio/silk", - "audio/vnd.dece.audio", - "audio/vnd.dece.audio", - "audio/vnd.digital-winds", - "audio/vnd.dra", - "audio/vnd.dts", - "audio/vnd.dts.hd", - "audio/vnd.lucent.voice", - "audio/vnd.ms-playready.media.pya", - "audio/vnd.nuera.ecelp4800", - "audio/vnd.nuera.ecelp7470", - "audio/vnd.nuera.ecelp9600", - "audio/vnd.rip", - "audio/wave", - "audio/webm", - "audio/x-aac", - "audio/x-aiff", - "audio/x-aiff", - "audio/x-aiff", - "audio/x-caf", - "audio/x-flac", - "audio/x-matroska", - "audio/x-mpegurl", - "audio/x-ms-wax", - "audio/x-ms-wma", - "audio/x-pn-realaudio", - "audio/x-pn-realaudio", - "audio/x-pn-realaudio-plugin", - "audio/xm", - "chemical/x-cdx", - "chemical/x-cif", - "chemical/x-cmdf", - "chemical/x-cml", - "chemical/x-csml", - "chemical/x-xyz", - "font/collection", - "font/otf", - "font/ttf", - "font/woff", - "font/woff2", - "image/aces", - "image/apng", - "image/avci", - "image/avcs", - "image/avif", - "image/bmp", - "image/cgm", - "image/dicom-rle", - "image/fits", - "image/g3fax", - "image/gif", - "image/heic", - "image/heic-sequence", - "image/heif", - "image/heif-sequence", - "image/hej2k", - "image/hsj2", - "image/ief", - "image/jls", - "image/jp2", - "image/jp2", - "image/jpeg", - "image/jpeg", - "image/jpeg", - "image/jph", - "image/jphc", - "image/jpm", - "image/jpx", - "image/jpx", - "image/jxr", - "image/jxra", - "image/jxrs", - "image/jxs", - "image/jxsc", - "image/jxsi", - "image/jxss", - "image/ktx", - "image/ktx2", - "image/png", - "image/prs.btif", - "image/prs.pti", - "image/sgi", - "image/svg+xml", - "image/svg+xml", - "image/t38", - "image/tiff", - "image/tiff", - "image/tiff-fx", - "image/vnd.adobe.photoshop", - "image/vnd.airzip.accelerator.azv", - "image/vnd.dece.graphic", - "image/vnd.dece.graphic", - "image/vnd.dece.graphic", - "image/vnd.dece.graphic", - "image/vnd.djvu", - "image/vnd.djvu", - "text/vnd.dvb.subtitle", - "image/vnd.dwg", - "image/vnd.dxf", - "image/vnd.fastbidsheet", - "image/vnd.fpx", - "image/vnd.fst", - "image/vnd.fujixerox.edmics-mmr", - "image/vnd.fujixerox.edmics-rlc", - "image/vnd.microsoft.icon", - "image/vnd.ms-dds", - "image/vnd.ms-modi", - "image/vnd.ms-photo", - "image/vnd.net-fpx", - "image/vnd.pco.b16", - "image/vnd.tencent.tap", - "image/vnd.valve.source.texture", - "image/vnd.wap.wbmp", - "image/vnd.xiff", - "image/vnd.zbrush.pcx", - "image/webp", - "image/x-3ds", - "image/x-cmu-raster", - "image/x-cmx", - "image/x-freehand", - "image/x-freehand", - "image/x-freehand", - "image/x-freehand", - "image/x-freehand", - "image/x-jng", - "image/x-mrsid-image", - "image/x-pict", - "image/x-pict", - "image/x-portable-anymap", - "image/x-portable-bitmap", - "image/x-portable-graymap", - "image/x-portable-pixmap", - "image/x-rgb", - "image/x-tga", - "image/x-xbitmap", - "image/x-xpixmap", - "image/x-xwindowdump", - "message/disposition-notification", - "message/global", - "message/global-delivery-status", - "message/global-disposition-notification", - "message/global-headers", - "message/rfc822", - "message/rfc822", - "message/vnd.wfa.wsc", - "model/3mf", - "model/gltf+json", - "model/gltf-binary", - "model/iges", - "model/iges", - "model/mesh", - "model/mesh", - "model/mesh", - "model/mtl", - "model/step+xml", - "model/step+zip", - "model/step-xml+zip", - "model/vnd.collada+xml", - "model/vnd.dwf", - "model/vnd.gdl", - "model/vnd.gtw", - "model/vnd.mts", - "model/vnd.opengex", - "model/vnd.parasolid.transmit.binary", - "model/vnd.parasolid.transmit.text", - "model/vnd.sap.vds", - "model/vnd.usdz+zip", - "model/vnd.valve.source.compiled-map", - "model/vnd.vtu", - "model/vrml", - "model/vrml", - "model/x3d+fastinfoset", - "model/x3d+binary", - "model/x3d-vrml", - "model/x3d+vrml", - "model/x3d+xml", - "model/x3d+xml", - "text/cache-manifest", - "text/cache-manifest", - "text/calendar", - "text/calendar", - "text/coffeescript", - "text/coffeescript", - "text/css", - "text/csv", - "text/html", - "text/html", - "text/html", - "text/jade", - "text/jsx", - "text/less", - "text/markdown", - "text/markdown", - "text/mathml", - "text/mdx", - "text/n3", - "text/plain", - "text/plain", - "text/plain", - "text/plain", - "text/plain", - "text/plain", - "text/plain", - "text/plain", - "text/prs.lines.tag", - "text/richtext", - "text/sgml", - "text/sgml", - "text/shex", - "text/slim", - "text/slim", - "text/spdx", - "text/stylus", - "text/stylus", - "text/tab-separated-values", - "text/troff", - "text/troff", - "text/troff", - "text/troff", - "text/troff", - "text/troff", - "text/turtle", - "text/uri-list", - "text/uri-list", - "text/uri-list", - "text/vcard", - "text/vnd.curl", - "text/vnd.curl.dcurl", - "text/vnd.curl.mcurl", - "text/vnd.curl.scurl", - "text/vnd.familysearch.gedcom", - "text/vnd.fly", - "text/vnd.fmi.flexstor", - "text/vnd.graphviz", - "text/vnd.in3d.3dml", - "text/vnd.in3d.spot", - "text/vnd.sun.j2me.app-descriptor", - "text/vnd.wap.wml", - "text/vnd.wap.wmlscript", - "text/vtt", - "text/x-asm", - "text/x-asm", - "text/x-c", - "text/x-c", - "text/x-c", - "text/x-c", - "text/x-c", - "text/x-c", - "text/x-c", - "text/x-component", - "text/x-fortran", - "text/x-fortran", - "text/x-fortran", - "text/x-fortran", - "text/x-handlebars-template", - "text/x-java-source", - "text/x-lua", - "text/x-markdown", - "text/x-nfo", - "text/x-opml", - "text/x-pascal", - "text/x-pascal", - "text/x-processing", - "text/x-sass", - "text/x-scss", - "text/x-setext", - "text/x-sfv", - "text/x-suse-ymp", - "text/x-uuencode", - "text/x-vcalendar", - "text/x-vcard", - "text/yaml", - "text/yaml", - "video/3gpp", - "video/3gpp2", - "video/h261", - "video/h263", - "video/h264", - "video/iso.segment", - "video/jpeg", - "video/jpm", - "video/mj2", - "video/mj2", - "video/mp2t", - "video/mp4", - "video/mp4", - "video/mp4", - "video/mpeg", - "video/mpeg", - "video/mpeg", - "video/mpeg", - "video/mpeg", - "video/ogg", - "video/quicktime", - "video/quicktime", - "video/vnd.dece.hd", - "video/vnd.dece.hd", - "video/vnd.dece.mobile", - "video/vnd.dece.mobile", - "video/vnd.dece.pd", - "video/vnd.dece.pd", - "video/vnd.dece.sd", - "video/vnd.dece.sd", - "video/vnd.dece.video", - "video/vnd.dece.video", - "video/vnd.dvb.file", - "video/vnd.fvt", - "video/vnd.mpegurl", - "video/vnd.mpegurl", - "video/vnd.ms-playready.media.pyv", - "video/vnd.uvvu.mp4", - "video/vnd.uvvu.mp4", - "video/vnd.vivo", - "video/webm", - "video/x-f4v", - "video/x-fli", - "video/x-flv", - "video/x-m4v", - "video/x-matroska", - "video/x-matroska", - "video/x-matroska", - "video/x-mng", - "video/x-ms-asf", - "video/x-ms-asf", - "video/x-ms-vob", - "video/x-ms-wm", - "video/x-ms-wmv", - "video/x-ms-wmx", - "video/x-ms-wvx", - "video/x-msvideo", - "video/x-sgi-movie", - "video/x-smv", - "x-conference/x-cooltalk" - ], - "challenges": { - "enabled": true, - "difficulty": 50000, - "expiration": 300, - "key": "" - } - } + "required": [ + "accounts", + "notes", + "media", + "emojis", + "polls", + "emails", + "challenges", + "filters" + ], + "additionalProperties": false }, "notifications": { "type": "object", @@ -3125,41 +1953,30 @@ "push": { "type": "object", "properties": { - "enabled": { - "type": "boolean", - "default": true - }, - "vapid": { + "vapid_keys": { "type": "object", "properties": { "public": { - "type": "string" + "$ref": "#/properties/postgres/properties/password" }, "private": { - "type": "string" - }, - "subject": { - "type": "string" + "$ref": "#/properties/postgres/properties/password" } }, "required": ["public", "private"], - "additionalProperties": false, - "default": { - "public": "", - "private": "" - } + "additionalProperties": false + }, + "subject": { + "type": "string", + "description": "Subject field embedded in the push notification. Example: 'mailto:contact@example.com'" } }, + "required": ["vapid_keys", "subject"], "additionalProperties": false, - "default": { - "enabled": true, - "vapid": { - "public": "", - "private": "" - } - } + "description": "Web Push Notifications configuration. Leave out to disable." } }, + "required": ["push"], "additionalProperties": false }, "defaults": { @@ -3167,6 +1984,7 @@ "properties": { "visibility": { "type": "string", + "enum": ["public", "unlisted", "private", "direct"], "default": "public" }, "language": { @@ -3174,22 +1992,25 @@ "default": "en" }, "avatar": { - "$ref": "#/properties/http/properties/proxy/properties/address/anyOf/0" + "$ref": "#/properties/http/properties/proxy_address" }, "header": { - "$ref": "#/properties/http/properties/proxy/properties/address/anyOf/0" + "$ref": "#/properties/http/properties/proxy_address" }, "placeholder_style": { "type": "string", - "default": "thumbs" + "default": "thumbs", + "description": "A style name from https://www.dicebear.com/styles" } }, - "additionalProperties": false, - "default": { - "visibility": "public", - "language": "en", - "placeholder_style": "thumbs" - } + "required": [ + "visibility", + "language", + "avatar", + "header", + "placeholder_style" + ], + "additionalProperties": false }, "federation": { "type": "object", @@ -3197,14 +2018,14 @@ "blocked": { "type": "array", "items": { - "$ref": "#/properties/http/properties/proxy/properties/address/anyOf/0" + "type": "string" }, "default": [] }, "followers_only": { "type": "array", "items": { - "$ref": "#/properties/http/properties/proxy/properties/address/anyOf/0" + "type": "string" }, "default": [] }, @@ -3214,76 +2035,83 @@ "reports": { "type": "array", "items": { - "$ref": "#/properties/http/properties/proxy/properties/address/anyOf/0" + "type": "string" }, "default": [] }, "deletes": { "type": "array", "items": { - "$ref": "#/properties/http/properties/proxy/properties/address/anyOf/0" + "type": "string" }, "default": [] }, "updates": { "type": "array", "items": { - "$ref": "#/properties/http/properties/proxy/properties/address/anyOf/0" + "type": "string" }, "default": [] }, "media": { "type": "array", "items": { - "$ref": "#/properties/http/properties/proxy/properties/address/anyOf/0" + "type": "string" }, "default": [] }, "follows": { "type": "array", "items": { - "$ref": "#/properties/http/properties/proxy/properties/address/anyOf/0" + "type": "string" }, "default": [] }, "likes": { "type": "array", "items": { - "$ref": "#/properties/http/properties/proxy/properties/address/anyOf/0" + "type": "string" }, "default": [] }, "reactions": { "type": "array", "items": { - "$ref": "#/properties/http/properties/proxy/properties/address/anyOf/0" + "type": "string" }, "default": [] }, "banners": { "type": "array", "items": { - "$ref": "#/properties/http/properties/proxy/properties/address/anyOf/0" + "type": "string" }, "default": [] }, "avatars": { "type": "array", "items": { - "$ref": "#/properties/http/properties/proxy/properties/address/anyOf/0" + "type": "string" }, "default": [] } }, + "required": [ + "reports", + "deletes", + "updates", + "media", + "follows", + "likes", + "reactions", + "banners", + "avatars" + ], "additionalProperties": false }, "bridge": { "type": "object", "properties": { - "enabled": { - "type": "boolean", - "default": false - }, "software": { "anyOf": [ { @@ -3298,168 +2126,48 @@ "allowed_ips": { "type": "array", "items": { - "type": "string" + "$ref": "#/properties/http/properties/banned_ips/items" }, "default": [] }, "token": { - "type": "string", - "default": "" + "$ref": "#/properties/postgres/properties/password" }, "url": { - "$ref": "#/properties/http/properties/proxy/properties/address/anyOf/0" + "$ref": "#/properties/http/properties/proxy_address" } }, - "required": ["software"], - "additionalProperties": false, - "default": { - "enabled": false, - "software": "versia-ap", - "allowed_ips": [], - "token": "" - } + "required": ["software", "allowed_ips", "token", "url"], + "additionalProperties": false } }, - "required": ["discard"], - "additionalProperties": false, - "default": { - "blocked": [], - "followers_only": [], - "discard": { - "reports": [], - "deletes": [], - "updates": [], - "media": [], - "follows": [], - "likes": [], - "reactions": [], - "banners": [], - "avatars": [] - }, - "bridge": { - "enabled": false, - "software": "versia-ap", - "allowed_ips": [], - "token": "" - } - } + "required": ["blocked", "followers_only", "discard", "bridge"], + "additionalProperties": false }, "queues": { "type": "object", - "properties": { - "delivery": { - "type": "object", - "properties": { - "remove_on_complete": { - "type": "integer", - "default": 31536000 - }, - "remove_on_failure": { - "type": "integer", - "default": 31536000 - } - }, - "additionalProperties": false, - "default": { - "remove_on_complete": 31536000, - "remove_on_failure": 31536000 - } - }, - "inbox": { - "type": "object", - "properties": { - "remove_on_complete": { - "type": "integer", - "default": 31536000 - }, - "remove_on_failure": { - "type": "integer", - "default": 31536000 - } - }, - "additionalProperties": false, - "default": { - "remove_on_complete": 31536000, - "remove_on_failure": 31536000 - } - }, - "fetch": { - "type": "object", - "properties": { - "remove_on_complete": { - "type": "integer", - "default": 31536000 - }, - "remove_on_failure": { - "type": "integer", - "default": 31536000 - } - }, - "additionalProperties": false, - "default": { - "remove_on_complete": 31536000, - "remove_on_failure": 31536000 - } - }, - "push": { - "type": "object", - "properties": { - "remove_on_complete": { - "type": "integer", - "default": 31536000 - }, - "remove_on_failure": { - "type": "integer", - "default": 31536000 - } + "additionalProperties": { + "type": "object", + "properties": { + "remove_after_complete_seconds": { + "type": "integer", + "minimum": 0, + "default": 31536000 }, - "additionalProperties": false, - "default": { - "remove_on_complete": 31536000, - "remove_on_failure": 31536000 + "remove_after_failure_seconds": { + "type": "integer", + "minimum": 0, + "default": 31536000 } }, - "media": { - "type": "object", - "properties": { - "remove_on_complete": { - "type": "integer", - "default": 31536000 - }, - "remove_on_failure": { - "type": "integer", - "default": 31536000 - } - }, - "additionalProperties": false, - "default": { - "remove_on_complete": 31536000, - "remove_on_failure": 31536000 - } - } + "required": [ + "remove_after_complete_seconds", + "remove_after_failure_seconds" + ], + "additionalProperties": false }, - "additionalProperties": false, - "default": { - "delivery": { - "remove_on_complete": 31536000, - "remove_on_failure": 31536000 - }, - "inbox": { - "remove_on_complete": 31536000, - "remove_on_failure": 31536000 - }, - "fetch": { - "remove_on_complete": 31536000, - "remove_on_failure": 31536000 - }, - "push": { - "remove_on_complete": 31536000, - "remove_on_failure": 31536000 - }, - "media": { - "remove_on_complete": 31536000, - "remove_on_failure": 31536000 - } + "propertyNames": { + "enum": ["delivery", "inbox", "fetch", "push", "media"] } }, "instance": { @@ -3468,7 +2176,7 @@ "name": { "type": "string", "minLength": 1, - "default": "Versia" + "default": "Versia Server" }, "description": { "type": "string", @@ -3476,67 +2184,284 @@ "default": "A Versia instance" }, "extended_description_path": { - "type": "string" + "$ref": "#/properties/http/properties/tls/properties/key", + "description": "This value must be a file path" }, "tos_path": { - "type": "string" + "$ref": "#/properties/http/properties/tls/properties/key", + "description": "This value must be a file path" }, "privacy_policy_path": { - "type": "string" + "$ref": "#/properties/http/properties/tls/properties/key", + "description": "This value must be a file path" + }, + "branding": { + "type": "object", + "properties": { + "logo": { + "$ref": "#/properties/http/properties/proxy_address" + }, + "banner": { + "$ref": "#/properties/http/properties/proxy_address" + } + }, + "required": ["logo", "banner"], + "additionalProperties": false }, - "logo": { - "$ref": "#/properties/http/properties/proxy/properties/address/anyOf/0" + "languages": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "aa", + "ab", + "ae", + "af", + "ak", + "am", + "an", + "ar", + "as", + "av", + "ay", + "az", + "ba", + "be", + "bg", + "bi", + "bm", + "bn", + "bo", + "br", + "bs", + "ca", + "ce", + "ch", + "co", + "cr", + "cs", + "cu", + "cv", + "cy", + "da", + "de", + "dv", + "dz", + "ee", + "el", + "en", + "eo", + "es", + "et", + "eu", + "fa", + "ff", + "fi", + "fj", + "fo", + "fr", + "fy", + "ga", + "gd", + "gl", + "gn", + "gu", + "gv", + "ha", + "he", + "hi", + "ho", + "hr", + "ht", + "hu", + "hy", + "hz", + "ia", + "id", + "ie", + "ig", + "ii", + "ik", + "io", + "is", + "it", + "iu", + "ja", + "jv", + "ka", + "kg", + "ki", + "kj", + "kk", + "kl", + "km", + "kn", + "ko", + "kr", + "ks", + "ku", + "kv", + "kw", + "ky", + "la", + "lb", + "lg", + "li", + "ln", + "lo", + "lt", + "lu", + "lv", + "mg", + "mh", + "mi", + "mk", + "ml", + "mn", + "mr", + "ms", + "mt", + "my", + "na", + "nb", + "nd", + "ne", + "ng", + "nl", + "nn", + "no", + "nr", + "nv", + "ny", + "oc", + "oj", + "om", + "or", + "os", + "pa", + "pi", + "pl", + "ps", + "pt", + "qu", + "rm", + "rn", + "ro", + "ru", + "rw", + "sa", + "sc", + "sd", + "se", + "sg", + "si", + "sk", + "sl", + "sm", + "sn", + "so", + "sq", + "sr", + "ss", + "st", + "su", + "sv", + "sw", + "ta", + "te", + "tg", + "th", + "ti", + "tk", + "tl", + "tn", + "to", + "tr", + "ts", + "tt", + "tw", + "ty", + "ug", + "uk", + "ur", + "uz", + "ve", + "vi", + "vo", + "wa", + "wo", + "xh", + "yi", + "yo", + "za", + "zh", + "zu" + ] + }, + "description": "Primary instance languages. ISO 639-1 codes." + }, + "contact": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "description": "Email to contact the instance administration" + } + }, + "required": ["email"], + "additionalProperties": false }, - "banner": { - "$ref": "#/properties/http/properties/proxy/properties/address/anyOf/0" + "rules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "text": { + "type": "string", + "minLength": 1, + "maxLength": 255, + "description": "Short description of the rule" + }, + "hint": { + "type": "string", + "minLength": 1, + "maxLength": 4096, + "description": "Longer version of the rule with additional information" + } + }, + "required": ["text", "hint"], + "additionalProperties": false + }, + "default": [] }, "keys": { "type": "object", "properties": { "public": { - "anyOf": [ - { - "type": "string", - "minLength": 3, - "default": "" - }, - { - "type": "string", - "const": "" - } - ] + "$ref": "#/properties/postgres/properties/password" }, "private": { - "anyOf": [ - { - "type": "string", - "minLength": 3, - "default": "" - }, - { - "type": "string", - "const": "" - } - ] + "$ref": "#/properties/postgres/properties/password" } }, "required": ["public", "private"], - "additionalProperties": false, - "default": { - "public": "", - "private": "" - } + "additionalProperties": false } }, - "additionalProperties": false, - "default": { - "name": "Versia", - "description": "A Versia instance", - "keys": { - "public": "", - "private": "" - } - } + "required": [ + "name", + "description", + "extended_description_path", + "tos_path", + "privacy_policy_path", + "branding", + "languages", + "contact", + "rules", + "keys" + ], + "additionalProperties": false }, "permissions": { "type": "object", @@ -3814,204 +2739,62 @@ ] } }, - "additionalProperties": false, - "default": { - "anonymous": [ - "owner:note", - "read:note", - "read:note_likes", - "read:note_boosts", - "owner:account", - "read:account_follows", - "owner:like", - "owner:boost", - "read:account", - "owner:emoji", - "read:reaction", - "owner:reaction", - "read:emoji", - "owner:media", - "owner:block", - "owner:filter", - "owner:mute", - "owner:report", - "owner:settings", - "owner:notification", - "owner:follow", - "owner:app", - "search", - "push_notifications", - "public_timelines", - "private_timelines", - "oauth" - ], - "default": [ - "owner:note", - "read:note", - "read:note_likes", - "read:note_boosts", - "owner:account", - "read:account_follows", - "owner:like", - "owner:boost", - "read:account", - "owner:emoji", - "read:reaction", - "owner:reaction", - "read:emoji", - "owner:media", - "owner:block", - "owner:filter", - "owner:mute", - "owner:report", - "owner:settings", - "owner:notification", - "owner:follow", - "owner:app", - "search", - "push_notifications", - "public_timelines", - "private_timelines", - "oauth" - ], - "admin": [ - "owner:note", - "read:note", - "read:note_likes", - "read:note_boosts", - "owner:account", - "read:account_follows", - "owner:like", - "owner:boost", - "read:account", - "owner:emoji", - "read:reaction", - "owner:reaction", - "read:emoji", - "owner:media", - "owner:block", - "owner:filter", - "owner:mute", - "owner:report", - "owner:settings", - "owner:notification", - "owner:follow", - "owner:app", - "search", - "push_notifications", - "public_timelines", - "private_timelines", - "oauth", - "notes", - "accounts", - "likes", - "boosts", - "emojis", - "reactions", - "media", - "blocks", - "filters", - "mutes", - "reports", - "settings", - "roles", - "notifications", - "follows", - "impersonate", - "ignore_rate_limits", - "instance", - "instance:federation", - "instance:settings" - ] - } - }, - "filters": { - "type": "object", - "properties": { - "note_content": { - "type": "array", - "items": { - "type": "string" - }, - "default": [] - }, - "emoji": { - "type": "array", - "items": { - "type": "string" - }, - "default": [] - }, - "username": { - "type": "array", - "items": { - "type": "string" - }, - "default": [] - }, - "displayname": { - "type": "array", - "items": { - "type": "string" - }, - "default": [] - }, - "bio": { - "type": "array", - "items": { - "type": "string" - }, - "default": [] - } - }, + "required": ["anonymous", "default", "admin"], "additionalProperties": false }, "logging": { "type": "object", "properties": { - "log_requests": { - "type": "boolean", - "default": false - }, - "log_responses": { - "type": "boolean", - "default": false - }, - "log_requests_verbose": { - "type": "boolean", - "default": false + "types": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "type": "boolean", + "default": false + }, + { + "type": "object", + "properties": { + "level": { + "type": "string", + "enum": [ + "debug", + "info", + "warning", + "error", + "fatal" + ], + "default": "info" + }, + "log_file_path": { + "type": "string" + } + }, + "required": ["level", "log_file_path"], + "additionalProperties": false + } + ] + }, + "propertyNames": { + "enum": [ + "requests", + "responses", + "requests_content", + "filters" + ] + } }, "log_level": { "type": "string", "enum": ["debug", "info", "warning", "error", "fatal"], "default": "info" }, - "log_ip": { - "type": "boolean", - "default": false - }, - "log_filters": { - "type": "boolean", - "default": true - }, "sentry": { "type": "object", "properties": { - "enabled": { - "type": "boolean", - "default": false - }, "dsn": { - "anyOf": [ - { - "type": "string", - "format": "uri" - }, - { - "type": "string", - "const": "" - } - ] + "$ref": "#/properties/http/properties/proxy_address" }, "debug": { "type": "boolean", @@ -4044,79 +2827,23 @@ "type": "string" } }, - "additionalProperties": false, - "default": { - "enabled": false, - "debug": false, - "sample_rate": 1, - "traces_sample_rate": 1, - "max_breadcrumbs": 100 - } - }, - "storage": { - "type": "object", - "properties": { - "requests": { - "type": "string", - "default": "logs/requests.log" - } - }, - "additionalProperties": false, - "default": { - "requests": "logs/requests.log" - } - } - }, - "additionalProperties": false, - "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, - "traces_sample_rate": 1, - "max_breadcrumbs": 100 - }, - "storage": { - "requests": "logs/requests.log" - } - } - }, - "ratelimits": { - "type": "object", - "properties": { - "duration_coeff": { - "type": "number", - "default": 1 - }, - "max_coeff": { - "type": "number", - "default": 1 + "required": [ + "dsn", + "debug", + "sample_rate", + "traces_sample_rate", + "trace_propagation_targets", + "max_breadcrumbs", + "environment" + ], + "additionalProperties": false }, - "custom": { - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "duration": { - "type": "number", - "default": 30 - }, - "max": { - "type": "number", - "default": 60 - } - }, - "additionalProperties": false - }, - "default": {} + "log_file_path": { + "type": "string", + "default": "logs/versia.log" } }, + "required": ["types", "log_level", "sentry", "log_file_path"], "additionalProperties": false }, "debug": { @@ -4127,10 +2854,8 @@ "default": false } }, - "additionalProperties": false, - "default": { - "federation": false - } + "required": ["federation"], + "additionalProperties": false }, "plugins": { "type": "object", @@ -4157,30 +2882,38 @@ "default": [] } }, - "additionalProperties": false, - "default": { - "enabled": [], - "disabled": [] - } + "required": ["enabled", "disabled"], + "additionalProperties": false }, "config": { "type": "object", "additionalProperties": {} } }, + "required": ["autoload", "overrides", "config"], "additionalProperties": false } }, "required": [ - "database", + "postgres", "redis", - "sonic", - "signups", + "search", + "registration", "http", - "smtp", + "frontend", + "email", + "media", + "s3", + "validation", "notifications", - "filters", - "ratelimits" + "defaults", + "federation", + "queues", + "instance", + "permissions", + "logging", + "debug", + "plugins" ], "additionalProperties": false, "$schema": "http://json-schema.org/draft-07/schema#" diff --git a/drizzle.config.ts b/drizzle.config.ts index 1aa34449..f1b48f49 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,5 +1,5 @@ import type { Config } from "drizzle-kit"; -import { config } from "./packages/config-manager/index.ts"; +import { config } from "~/config.ts"; /** * Drizzle can't properly resolve imports with top-level await, so uncomment @@ -15,11 +15,11 @@ export default { user: "lysand", password: "lysand", database: "lysand", */ - host: config.database.host, - port: Number(config.database.port), - user: config.database.username, - password: config.database.password, - database: config.database.database, + host: config.postgres.host, + port: config.postgres.port, + user: config.postgres.username, + password: config.postgres.password, + database: config.postgres.database, }, // Print all statements verbose: true, diff --git a/drizzle/db.ts b/drizzle/db.ts index 03a51d41..35671eb0 100644 --- a/drizzle/db.ts +++ b/drizzle/db.ts @@ -4,28 +4,27 @@ import { type NodePgDatabase, drizzle } from "drizzle-orm/node-postgres"; import { withReplicas } from "drizzle-orm/pg-core"; import { migrate } from "drizzle-orm/postgres-js/migrator"; import { Pool } from "pg"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; import * as schema from "./schema.ts"; const primaryDb = new Pool({ - host: config.database.host, - port: Number(config.database.port), - user: config.database.username, - password: config.database.password, - database: config.database.database, + host: config.postgres.host, + port: config.postgres.port, + user: config.postgres.username, + password: config.postgres.password, + database: config.postgres.database, }); -const replicas = - config.database.replicas?.map( - (replica) => - new Pool({ - host: replica.host, - port: Number(replica.port), - user: replica.username, - password: replica.password, - database: replica.database, - }), - ) ?? []; +const replicas = config.postgres.replicas.map( + (replica) => + new Pool({ + host: replica.host, + port: replica.port, + user: replica.username, + password: replica.password, + database: replica.database, + }), +); export const db = (replicas.length ?? 0) > 0 diff --git a/entrypoints/api/index.ts b/entrypoints/api/index.ts index 2020642e..3b2ad825 100644 --- a/entrypoints/api/index.ts +++ b/entrypoints/api/index.ts @@ -2,7 +2,7 @@ import cluster from "node:cluster"; import { sentry } from "@/sentry"; import { createServer } from "@/server"; import { appFactory } from "~/app"; -import { config } from "~/packages/config-manager/index.ts"; +import { config } from "~/config.ts"; process.on("SIGINT", () => { process.exit(); diff --git a/entrypoints/api/setup.ts b/entrypoints/api/setup.ts index 7e584849..68c9d573 100644 --- a/entrypoints/api/setup.ts +++ b/entrypoints/api/setup.ts @@ -1,10 +1,9 @@ -import { checkConfig } from "@/init"; import { configureLoggers } from "@/loggers"; import { getLogger } from "@logtape/logtape"; import { Note } from "@versia/kit/db"; import IORedis from "ioredis"; +import { config } from "~/config.ts"; import { setupDatabase } from "~/drizzle/db"; -import { config } from "~/packages/config-manager/index.ts"; import { searchManager } from "../../classes/search/search-manager.ts"; const timeAtStart = performance.now(); @@ -26,15 +25,13 @@ serverLogger.info`Starting Versia Server...`; await setupDatabase(); -if (config.sonic.enabled) { +if (config.search.enabled) { await searchManager.connect(); } // Check if database is reachable const postCount = await Note.getCount(); -await checkConfig(config); - serverLogger.info`Versia Server started at ${config.http.bind}:${config.http.bind_port} in ${(performance.now() - timeAtStart).toFixed(0)}ms`; serverLogger.info`Database is online, now serving ${postCount} posts`; diff --git a/entrypoints/worker/setup.ts b/entrypoints/worker/setup.ts index 0e4f1e18..2996d697 100644 --- a/entrypoints/worker/setup.ts +++ b/entrypoints/worker/setup.ts @@ -1,11 +1,10 @@ -import { checkConfig } from "@/init"; import { configureLoggers } from "@/loggers"; import { getLogger } from "@logtape/logtape"; import { Note } from "@versia/kit/db"; import chalk from "chalk"; import IORedis from "ioredis"; +import { config } from "~/config.ts"; import { setupDatabase } from "~/drizzle/db"; -import { config } from "~/packages/config-manager/index.ts"; import { searchManager } from "../../classes/search/search-manager.ts"; const timeAtStart = performance.now(); @@ -28,15 +27,13 @@ serverLogger.info`Starting Versia Server Worker...`; await setupDatabase(); -if (config.sonic.enabled) { +if (config.search.enabled) { await searchManager.connect(); } // Check if database is reachable const postCount = await Note.getCount(); -await checkConfig(config); - serverLogger.info`Versia Server Worker started at ${config.http.bind}:${config.http.bind_port} in ${(performance.now() - timeAtStart).toFixed(0)}ms`; serverLogger.info`Database is online, containing ${postCount} posts`; diff --git a/middlewares/agent-bans.ts b/middlewares/agent-bans.ts index 356200d5..7de10db0 100644 --- a/middlewares/agent-bans.ts +++ b/middlewares/agent-bans.ts @@ -1,6 +1,6 @@ import { createMiddleware } from "hono/factory"; import { ApiError } from "~/classes/errors/api-error"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; export const agentBans = createMiddleware(async (context, next) => { // Check for banned user agents (regex) diff --git a/middlewares/bait.ts b/middlewares/bait.ts deleted file mode 100644 index fa2bc03f..00000000 --- a/middlewares/bait.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { getLogger } from "@logtape/logtape"; -import type { BunFile, SocketAddress } from "bun"; -import { createMiddleware } from "hono/factory"; -import { matches } from "ip-matching"; -import { config } from "~/packages/config-manager"; - -const baitFile = async (): Promise => { - const file = Bun.file(config.http.bait.send_file || "./beemovie.txt"); - - if (await file.exists()) { - return file; - } - - const logger = getLogger("server"); - - logger.error`Bait file not found: ${config.http.bait.send_file}`; -}; - -export const bait = createMiddleware(async (context, next) => { - const requestIp = context.env?.ip as SocketAddress | undefined | null; - - if (!config.http.bait.enabled) { - return await next(); - } - - const file = await baitFile(); - - if (!file) { - return await next(); - } - - // Check for bait IPs - if (requestIp?.address) { - for (const ip of config.http.bait.bait_ips) { - if (matches(ip, requestIp.address)) { - return context.body(file.stream()); - } - } - } - - // Check for bait user agents (regex) - const ua = context.req.header("user-agent") ?? ""; - - for (const agent of config.http.bait.bait_user_agents) { - if (new RegExp(agent).test(ua)) { - return context.body(file.stream()); - } - } - - await next(); -}); diff --git a/middlewares/ip-bans.ts b/middlewares/ip-bans.ts index c969a02d..2aafa233 100644 --- a/middlewares/ip-bans.ts +++ b/middlewares/ip-bans.ts @@ -4,7 +4,7 @@ import type { SocketAddress } from "bun"; import { createMiddleware } from "hono/factory"; import { matches } from "ip-matching"; import { ApiError } from "~/classes/errors/api-error"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; export const ipBans = createMiddleware(async (context, next) => { // Check for banned IPs diff --git a/middlewares/logger.ts b/middlewares/logger.ts index 43937afd..4ef3e04a 100644 --- a/middlewares/logger.ts +++ b/middlewares/logger.ts @@ -1,10 +1,10 @@ import { getLogger } from "@logtape/logtape"; import chalk from "chalk"; import { createMiddleware } from "hono/factory"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; export const logger = createMiddleware(async (context, next) => { - if (config.logging.log_requests) { + if (config.logging.types.requests) { const serverLogger = getLogger("server"); const body = await context.req.raw.clone().text(); @@ -25,7 +25,7 @@ export const logger = createMiddleware(async (context, next) => { const bodyLog = `${chalk.bold("Body")}: ${chalk.gray(body)}`; - if (config.logging.log_requests_verbose) { + if (config.logging.types.requests_content) { serverLogger.debug`${urlAndMethod}\n${hash}\n${headers}\n${bodyLog}`; } else { serverLogger.debug`${urlAndMethod}`; diff --git a/middlewares/url-check.ts b/middlewares/url-check.ts index 7b09cc20..62dab706 100644 --- a/middlewares/url-check.ts +++ b/middlewares/url-check.ts @@ -1,5 +1,5 @@ import { createMiddleware } from "hono/factory"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; export const urlCheck = createMiddleware(async (context, next) => { // Check that request URL matches base_url diff --git a/package.json b/package.json index 8df8bc50..4551c3fb 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "wc": "find server database *.ts docs packages types utils drizzle tests -type f -print0 | wc -m --files0-from=-", "cli": "bun run cli/index.ts", "prune": "ts-prune | grep -v server/ | grep -v dist/ | grep -v '(used in module)'", - "schema:generate": "bun run packages/config-manager/json-schema.ts > config/config.schema.json && bun run packages/plugin-kit/json-schema.ts > packages/plugin-kit/manifest.schema.json", + "schema:generate": "bun run classes/config/to-json-schema.ts > config/config.schema.json && bun run packages/plugin-kit/json-schema.ts > packages/plugin-kit/manifest.schema.json", "check": "bunx tsc -p .", "test": "find . -name \"*.test.ts\" -not -path \"./node_modules/*\" | xargs -I {} sh -c 'bun test {} || exit 255'", "docs:dev": "vitepress dev docs", diff --git a/packages/config-manager/config.type.ts b/packages/config-manager/config.type.ts deleted file mode 100644 index 63b84db4..00000000 --- a/packages/config-manager/config.type.ts +++ /dev/null @@ -1,863 +0,0 @@ -import { z } from "@hono/zod-openapi"; -import { - ADMIN_ROLES, - DEFAULT_ROLES, - RolePermissions, -} from "@versia/kit/tables"; -import { types as mimeTypes } from "mime-types"; - -export enum MediaBackendType { - Local = "local", - S3 = "s3", -} - -const zUrlPath = z - .string() - .trim() - .min(1) - // Remove trailing slashes, but keep the root slash - .transform((arg) => (arg === "/" ? arg : arg.replace(/\/$/, ""))); - -const zUrl = z - .string() - .trim() - .min(1) - .refine((arg) => URL.canParse(arg), "Invalid url") - .transform((arg) => arg.replace(/\/$/, "")) - .transform((arg) => new URL(arg)); - -export const zBoolean = z - .string() - .transform((v) => ["true", "1", "on"].includes(v.toLowerCase())) - .or(z.boolean()); - -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"), - }) - .strict(), - ) - .optional(), - }) - .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), - }) - .strict() - .default({ - host: "localhost", - port: 6379, - password: "", - database: 0, - }), - 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 - .number() - .int() - .min(1) - .max(2 ** 16 - 1) - .default(7700), - password: z.string(), - enabled: z.boolean().default(false), - }) - .strict(), - signups: z - .object({ - registration: z.boolean().default(true), - rules: z.array(z.string()).default([]), - }) - .strict(), - http: z - .object({ - base_url: zUrl.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: [], - }), - }) - .strict(), - 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"), - }) - .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: 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), - }) - .strict() - .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), - }) - .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", - }, - }), - 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() - .optional(), - 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_emoji_size: z.number().int().default(1000000), - max_emoji_shortcode_size: z.number().int().default(100), - max_emoji_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_emoji_size: 1000000, - max_emoji_shortcode_size: 100, - max_emoji_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: "", - }, - }), - notifications: z - .object({ - push: z - .object({ - enabled: z.boolean().default(true), - vapid: z - .object({ - public: z.string(), - private: z.string(), - subject: z.string().optional(), - }) - .strict() - .default({ - public: "", - private: "", - }), - }) - .strict() - .default({ - enabled: true, - vapid: { - public: "", - private: "", - }, - }), - }) - .strict(), - 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: "", - }, - }), - queues: z - .object({ - delivery: z - .object({ - remove_on_complete: z - .number() - .int() - // 1 year - .default(60 * 60 * 24 * 365), - remove_on_failure: z - .number() - .int() - // 1 year - .default(60 * 60 * 24 * 365), - }) - .strict() - .default({ - remove_on_complete: 60 * 60 * 24 * 365, - remove_on_failure: 60 * 60 * 24 * 365, - }), - inbox: z - .object({ - remove_on_complete: z - .number() - .int() - // 1 year - .default(60 * 60 * 24 * 365), - remove_on_failure: z - .number() - .int() - // 1 year - .default(60 * 60 * 24 * 365), - }) - .strict() - .default({ - remove_on_complete: 60 * 60 * 24 * 365, - remove_on_failure: 60 * 60 * 24 * 365, - }), - fetch: z - .object({ - remove_on_complete: z - .number() - .int() - // 1 year - .default(60 * 60 * 24 * 365), - remove_on_failure: z - .number() - .int() - // 1 year - .default(60 * 60 * 24 * 365), - }) - .strict() - .default({ - remove_on_complete: 60 * 60 * 24 * 365, - remove_on_failure: 60 * 60 * 24 * 365, - }), - push: z - .object({ - remove_on_complete: z - .number() - .int() - // 1 year - .default(60 * 60 * 24 * 365), - remove_on_failure: z - .number() - .int() - // 1 year - .default(60 * 60 * 24 * 365), - }) - .strict() - .default({ - remove_on_complete: 60 * 60 * 24 * 365, - remove_on_failure: 60 * 60 * 24 * 365, - }), - media: z - .object({ - remove_on_complete: z - .number() - .int() - // 1 year - .default(60 * 60 * 24 * 365), - remove_on_failure: z - .number() - .int() - // 1 year - .default(60 * 60 * 24 * 365), - }) - .strict() - .default({ - remove_on_complete: 60 * 60 * 24 * 365, - remove_on_failure: 60 * 60 * 24 * 365, - }), - }) - .strict() - .default({ - delivery: { - remove_on_complete: 60 * 60 * 24 * 365, - remove_on_failure: 60 * 60 * 24 * 365, - }, - inbox: { - remove_on_complete: 60 * 60 * 24 * 365, - remove_on_failure: 60 * 60 * 24 * 365, - }, - fetch: { - remove_on_complete: 60 * 60 * 24 * 365, - remove_on_failure: 60 * 60 * 24 * 365, - }, - push: { - remove_on_complete: 60 * 60 * 24 * 365, - remove_on_failure: 60 * 60 * 24 * 365, - }, - media: { - remove_on_complete: 60 * 60 * 24 * 365, - remove_on_failure: 60 * 60 * 24 * 365, - }, - }), - 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: "", - }, - }), - 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, - }, - 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), - }) - .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() - .optional(), - }) - .strict() - .refine( - // If media backend is S3, s3 config must be set - (arg) => arg.media.backend === MediaBackendType.Local || !!arg.s3, - "S3 config must be set when using S3 media backend", - ); - -export type Config = z.infer; diff --git a/packages/config-manager/index.ts b/packages/config-manager/index.ts deleted file mode 100644 index b653f697..00000000 --- a/packages/config-manager/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @file index.ts - * @summary ConfigManager system to retrieve and modify system configuration - * @description Can read from a hand-written file, config.toml, or from a machine-saved file, config.internal.toml - * Fuses both and provides a way to retrieve individual values - */ - -import { loadConfig, watchConfig } from "c12"; -import { fromZodError } from "zod-validation-error"; -import { type Config, configValidator } from "./config.type"; -export type { Config } from "./config.type"; - -const { config } = await watchConfig({ - configFile: "./config/config.toml", - overrides: - ( - await loadConfig({ - configFile: "./config/config.internal.toml", - }) - ).config ?? undefined, -}); - -const parsed = await configValidator.safeParseAsync(config); - -if (!parsed.success) { - console.error("Invalid config file:"); - throw fromZodError(parsed.error).message; -} - -const exportedConfig = parsed.data; - -export { exportedConfig as config }; diff --git a/packages/plugin-kit/manifest.schema.json b/packages/plugin-kit/manifest.schema.json index b2a82d74..83d3585f 100644 --- a/packages/plugin-kit/manifest.schema.json +++ b/packages/plugin-kit/manifest.schema.json @@ -37,7 +37,7 @@ "format": "uri" } }, - "required": ["name"], + "required": ["name", "email", "url"], "additionalProperties": false } }, @@ -75,10 +75,18 @@ "format": "uri" } }, + "required": ["type", "url"], "additionalProperties": false } }, - "required": ["name", "version", "description"], + "required": [ + "$schema", + "name", + "version", + "description", + "authors", + "repository" + ], "additionalProperties": false, "$schema": "http://json-schema.org/draft-07/schema#" } diff --git a/packages/plugin-kit/plugin.ts b/packages/plugin-kit/plugin.ts index 46435a4d..f685600c 100644 --- a/packages/plugin-kit/plugin.ts +++ b/packages/plugin-kit/plugin.ts @@ -51,7 +51,7 @@ export class Plugin { try { this.store = await this.configSchema.parseAsync(config); } catch (error) { - throw fromZodError(error as ZodError).message; + throw fromZodError(error as ZodError); } } diff --git a/plugins/openid/index.ts b/plugins/openid/index.ts index 7edcdaf1..51a6f061 100644 --- a/plugins/openid/index.ts +++ b/plugins/openid/index.ts @@ -1,10 +1,10 @@ import { z } from "@hono/zod-openapi"; import { Hooks, Plugin } from "@versia/kit"; import { User } from "@versia/kit/db"; -import chalk from "chalk"; import { getCookie } from "hono/cookie"; import { jwtVerify } from "jose"; import { JOSEError, JWTExpired } from "jose/errors"; +import { keyPair, sensitiveString } from "~/classes/config/schema.ts"; import { ApiError } from "~/classes/errors/api-error.ts"; import { RolePermissions } from "~/drizzle/schema.ts"; import authorizeRoute from "./routes/authorize.ts"; @@ -26,64 +26,12 @@ const configSchema = z.object({ id: z.string().min(1), url: z.string().min(1), client_id: z.string().min(1), - client_secret: z.string().min(1), + client_secret: sensitiveString, icon: z.string().min(1).optional(), }), ) .default([]), - keys: z - .object({ - public: z - .string() - .min(1) - .transform(async (v) => { - try { - return await crypto.subtle.importKey( - "spki", - Buffer.from(v, "base64"), - "Ed25519", - true, - ["verify"], - ); - } catch { - throw new Error( - "Public key at oidc.keys.public is invalid", - ); - } - }), - private: z - .string() - .min(1) - .transform(async (v) => { - try { - return await crypto.subtle.importKey( - "pkcs8", - Buffer.from(v, "base64"), - "Ed25519", - true, - ["sign"], - ); - } catch { - throw new Error( - "Private key at oidc.keys.private is invalid", - ); - } - }), - }) - .optional() - .transform(async (v, ctx) => { - if (!(v?.private && v?.public)) { - const { public_key, private_key } = await User.generateKeys(); - - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Keys are missing, please add the following to your config:\n\nkeys.public: ${chalk.gray(public_key)}\nkeys.private: ${chalk.gray(private_key)} - `, - }); - } - - return v as Exclude; - }), + keys: keyPair, }); const plugin = new Plugin(configSchema); diff --git a/plugins/openid/routes/authorize.test.ts b/plugins/openid/routes/authorize.test.ts index 776eedf7..2ca6b6b4 100644 --- a/plugins/openid/routes/authorize.test.ts +++ b/plugins/openid/routes/authorize.test.ts @@ -3,7 +3,7 @@ import { randomString } from "@/math"; import { Application } from "@versia/kit/db"; import { RolePermissions } from "@versia/kit/tables"; import { SignJWT } from "jose"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; import { fakeRequest, getTestUsers } from "~/tests/utils"; const { deleteUsers, tokens, users } = await getTestUsers(1); diff --git a/plugins/openid/routes/authorize.ts b/plugins/openid/routes/authorize.ts index b7bf489b..03e8a4ea 100644 --- a/plugins/openid/routes/authorize.ts +++ b/plugins/openid/routes/authorize.ts @@ -223,7 +223,7 @@ export default (plugin: PluginType): void => ...payload, name: user.data.displayName, preferred_username: user.data.username, - picture: user.getAvatarUrl(context.get("config")), + picture: user.getAvatarUrl(), updated_at: new Date( user.data.updatedAt, ).toISOString(), diff --git a/plugins/openid/routes/oauth/callback.ts b/plugins/openid/routes/oauth/callback.ts index c9a1c159..40687c64 100644 --- a/plugins/openid/routes/oauth/callback.ts +++ b/plugins/openid/routes/oauth/callback.ts @@ -6,6 +6,7 @@ import { OpenIdAccounts, RolePermissions, Users } from "@versia/kit/tables"; import { setCookie } from "hono/cookie"; import { SignJWT } from "jose"; import { ApiError } from "~/classes/errors/api-error.ts"; +import { Account as AccountSchema } from "~/classes/schemas/account.ts"; import type { PluginType } from "../../index.ts"; import { automaticOidcFlow } from "../../utils.ts"; @@ -199,30 +200,8 @@ export default (plugin: PluginType): void => { email?.split("@")[0] ?? randomString(8, "hex"); - const usernameValidator = z - .string() - .regex(/^[a-z0-9_]+$/) - .min(3) - .max( - context.get("config").validation - .max_username_size, - ) - .refine( - (value) => - !context - .get("config") - .validation.username_blacklist.includes( - value, - ), - ) - .refine((value) => - context - .get("config") - .filters.username.some((filter) => - value.match(filter), - ), - ) - .refine( + const usernameValidator = + AccountSchema.shape.username.refine( async (value) => !(await User.fromSql( and( diff --git a/tests/api/accounts.test.ts b/tests/api/accounts.test.ts index b4b46642..c374202d 100644 --- a/tests/api/accounts.test.ts +++ b/tests/api/accounts.test.ts @@ -6,7 +6,7 @@ import type { Account as ApiAccount, Relationship as ApiRelationship, } from "@versia/client/types"; -import { config } from "~/packages/config-manager/index.ts"; +import { config } from "~/config.ts"; import { fakeRequest, getTestUsers } from "~/tests/utils"; const { users, tokens, deleteUsers } = await getTestUsers(2); diff --git a/tests/utils.ts b/tests/utils.ts index 0c632b5c..3b191028 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -6,11 +6,12 @@ import { solveChallenge } from "altcha-lib"; import { type InferSelectModel, asc, inArray, like } from "drizzle-orm"; import { appFactory } from "~/app"; import { searchManager } from "~/classes/search/search-manager"; +import { config } from "~/config.ts"; import { setupDatabase } from "~/drizzle/db"; -import { config } from "~/packages/config-manager"; await setupDatabase(); -if (config.sonic.enabled) { + +if (config.search.enabled) { await searchManager.connect(); } diff --git a/types/api.ts b/types/api.ts index e79cddc6..4e0bbee6 100644 --- a/types/api.ts +++ b/types/api.ts @@ -13,8 +13,8 @@ import type { } from "@versia/federation/types"; import type { SocketAddress } from "bun"; import type { RouterRoute } from "hono/types"; +import type { ConfigSchema } from "~/classes/config/schema"; import type { AuthData } from "~/classes/functions/user"; -import type { Config } from "~/packages/config-manager"; export type HttpVerb = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS"; @@ -24,7 +24,7 @@ export const ErrorSchema = z.object({ export type HonoEnv = { Variables: { - config: Config; + config: z.infer; auth: AuthData; }; Bindings: { diff --git a/utils/api.ts b/utils/api.ts index 9c6d6ac3..d9672844 100644 --- a/utils/api.ts +++ b/utils/api.ts @@ -28,7 +28,7 @@ import { type ParsedQs, parse } from "qs"; import { fromZodError } from "zod-validation-error"; import { ApiError } from "~/classes/errors/api-error"; import type { AuthData } from "~/classes/functions/user"; -import { config } from "~/packages/config-manager/index.ts"; +import { config } from "~/config.ts"; import { ErrorSchema, type HonoEnv } from "~/types/api"; export const reusedResponses = { @@ -229,7 +229,7 @@ export const checkRouteNeedsChallenge = async ( required: boolean, context: Context, ): Promise => { - if (!required) { + if (!(required && config.validation.challenges)) { return; } @@ -325,7 +325,7 @@ export const auth = (options: { } // Challenge check - if (options.challenge && config.validation.challenges.enabled) { + if (options.challenge && config.validation.challenges) { await checkRouteNeedsChallenge(options.challenge, context); } @@ -573,7 +573,7 @@ export const debugRequest = async (req: Request): Promise => { const bodyLog = `${chalk.bold("Body")}: ${chalk.gray(body)}`; - if (config.logging.log_requests_verbose) { + if (config.logging.types.requests_content) { logger.debug`${urlAndMethod}\n${hash}\n${headers}\n${bodyLog}`; } else { logger.debug`${urlAndMethod}`; @@ -594,7 +594,7 @@ export const debugResponse = async (res: Response): Promise => { const bodyLog = `${chalk.bold("Body")}: ${chalk.gray(body)}`; - if (config.logging.log_requests_verbose) { + if (config.logging.types.requests_content) { logger.debug`${status}\n${headers}\n${bodyLog}`; } else { logger.debug`${status}`; diff --git a/utils/bull-board.ts b/utils/bull-board.ts index 306c176a..f860d2c3 100644 --- a/utils/bull-board.ts +++ b/utils/bull-board.ts @@ -8,7 +8,8 @@ import { fetchQueue } from "~/classes/queues/fetch"; import { inboxQueue } from "~/classes/queues/inbox"; import { mediaQueue } from "~/classes/queues/media"; import { pushQueue } from "~/classes/queues/push"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; +import pkg from "~/package.json"; import type { HonoEnv } from "~/types/api"; export const applyToHono = (app: OpenAPIHono): void => { @@ -31,9 +32,7 @@ export const applyToHono = (app: OpenAPIHono): void => { alternative: "/favicon.ico", }, boardLogo: { - path: - config.instance.logo?.toString() ?? - "https://cdn.versia.pub/branding/icon.svg", + path: config.instance.branding.logo?.origin ?? pkg.icon, height: 40, }, }, diff --git a/utils/challenges.ts b/utils/challenges.ts index 77836596..6fab2f8f 100644 --- a/utils/challenges.ts +++ b/utils/challenges.ts @@ -3,16 +3,20 @@ import { Challenges } from "@versia/kit/tables"; import { createChallenge } from "altcha-lib"; import type { Challenge } from "altcha-lib/types"; import { sql } from "drizzle-orm"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; export const generateChallenge = async ( - maxNumber = config.validation.challenges.difficulty, + maxNumber?: number, ): Promise<{ id: string; challenge: Challenge; expiresAt: string; createdAt: string; }> => { + if (!config.validation.challenges) { + throw new Error("Challenges are not enabled"); + } + const expirationDate = new Date( Date.now() + config.validation.challenges.expiration * 1000, ); @@ -23,7 +27,7 @@ export const generateChallenge = async ( const challenge = await createChallenge({ hmacKey: config.validation.challenges.key, expires: expirationDate, - maxNumber, + maxNumber: maxNumber ?? config.validation.challenges.difficulty, algorithm: "SHA-256", params: { challenge_id: uuid, diff --git a/utils/constants.ts b/utils/constants.ts index 1431d36b..50f95b80 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -1,4 +1,4 @@ -import { config } from "~/packages/config-manager/index.ts"; +import { config } from "~/config.ts"; export const localObjectUri = (id: string): URL => new URL(`/objects/${id}`, config.http.base_url); diff --git a/utils/content_types.ts b/utils/content_types.ts index 4769e42e..fb95caa3 100644 --- a/utils/content_types.ts +++ b/utils/content_types.ts @@ -1,7 +1,7 @@ import type { ContentFormat } from "@versia/federation/types"; import { htmlToText as htmlToTextLib } from "html-to-text"; import { lookup } from "mime-types"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; export const getBestContentType = ( content?: ContentFormat | null, @@ -67,7 +67,7 @@ export const mimeLookup = (url: URL): Promise => { const fetchLookup = fetch(url, { method: "HEAD", // @ts-expect-error Proxy is a Bun-specific feature - proxy: config.http.proxy.address, + proxy: config.http.proxy_address, }) .then( (response) => diff --git a/utils/init.ts b/utils/init.ts deleted file mode 100644 index a3cc2198..00000000 --- a/utils/init.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { getLogger } from "@logtape/logtape"; -import { User } from "@versia/kit/db"; -import chalk from "chalk"; -import { generateVAPIDKeys } from "web-push"; -import type { Config } from "~/packages/config-manager"; - -export const checkConfig = async (config: Config): Promise => { - await checkFederationConfig(config); - - await checkHttpProxyConfig(config); - - await checkChallengeConfig(config); - - await checkVapidConfig(config); -}; - -const checkHttpProxyConfig = async (config: Config): Promise => { - const logger = getLogger("server"); - - if (config.http.proxy.enabled) { - logger.info`HTTP proxy enabled at ${chalk.gray(config.http.proxy.address)}, testing...`; - - // Test the proxy - const response = await fetch("https://api.ipify.org?format=json", { - // @ts-expect-error Proxy is a Bun-specific feature - proxy: config.http.proxy.address, - }); - - const ip = (await response.json()).ip; - - logger.info`Your IPv4 address is ${chalk.gray(ip)}`; - - if (!response.ok) { - throw new Error( - "The HTTP proxy is enabled, but the proxy address is not reachable", - ); - } - } -}; - -const checkChallengeConfig = async (config: Config): Promise => { - const logger = getLogger("server"); - - if ( - config.validation.challenges.enabled && - !config.validation.challenges.key - ) { - logger.fatal`Challenges are enabled, but the challenge key is not set in the config`; - logger.fatal`Below is a generated key for you to copy in the config at validation.challenges.key`; - - const key = await crypto.subtle.generateKey( - { - name: "HMAC", - hash: "SHA-256", - }, - true, - ["sign"], - ); - - const exported = await crypto.subtle.exportKey("raw", key); - - const base64 = Buffer.from(exported).toString("base64"); - - logger.fatal`Generated key: ${chalk.gray(base64)}`; - - // Hang until Ctrl+C is pressed - await Bun.sleep(Number.POSITIVE_INFINITY); - } -}; - -const checkFederationConfig = async (config: Config): Promise => { - const logger = getLogger("server"); - - if (!(config.instance.keys.public && config.instance.keys.private)) { - logger.fatal`The federation keys are not set in the config`; - logger.fatal`Below are generated keys for you to copy in the config at instance.keys.public and instance.keys.private`; - - // Generate a key for them - const { public_key, private_key } = await User.generateKeys(); - - logger.fatal`Generated public key: ${chalk.gray(public_key)}`; - logger.fatal`Generated private key: ${chalk.gray(private_key)}`; - - // Hang until Ctrl+C is pressed - await Bun.sleep(Number.POSITIVE_INFINITY); - } - - // Try and import the key - const privateKey = await crypto.subtle - .importKey( - "pkcs8", - Buffer.from(config.instance.keys.private, "base64"), - "Ed25519", - false, - ["sign"], - ) - .catch((e) => e as Error); - - // Try and import the key - const publicKey = await crypto.subtle - .importKey( - "spki", - Buffer.from(config.instance.keys.public, "base64"), - "Ed25519", - false, - ["verify"], - ) - .catch((e) => e as Error); - - if (privateKey instanceof Error || publicKey instanceof Error) { - throw new Error( - "The federation keys could not be imported! You may generate new ones by removing the old ones from the config and restarting the server.", - ); - } -}; - -const checkVapidConfig = async (config: Config): Promise => { - const logger = getLogger("server"); - - if ( - config.notifications.push.enabled && - !( - config.notifications.push.vapid.public || - config.notifications.push.vapid.private - ) - ) { - logger.fatal`The VAPID keys are not set in the config, but push notifications are enabled.`; - logger.fatal`Below are generated keys for you to copy in the config at notifications.push.vapid`; - - const { privateKey, publicKey } = await generateVAPIDKeys(); - - logger.fatal`Generated public key: ${chalk.gray(publicKey)}`; - logger.fatal`Generated private key: ${chalk.gray(privateKey)}`; - - // Hang until Ctrl+C is pressed - await Bun.sleep(Number.POSITIVE_INFINITY); - } - - // These use a format I don't understand, so I'm just going to check the length - const validateKey = (key: string): boolean => key.length > 10; - - if ( - !( - validateKey(config.notifications.push.vapid.public) && - validateKey(config.notifications.push.vapid.private) - ) - ) { - throw new Error( - "The VAPID keys could not be imported! You may generate new ones by removing the old ones from the config and restarting the server.", - ); - } -}; diff --git a/utils/loggers.ts b/utils/loggers.ts index 4589cec8..558f0537 100644 --- a/utils/loggers.ts +++ b/utils/loggers.ts @@ -19,7 +19,7 @@ import { getLevelFilter, } from "@logtape/logtape"; import chalk from "chalk"; -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; // HACK: This is a workaround for the lack of type exports in the Logtape package. // biome-ignore format: Biome formatter bug @@ -156,7 +156,7 @@ export const configureLoggers = (silent = false): Promise => console: getConsoleSink({ formatter: defaultConsoleFormatter, }), - file: getBaseRotatingFileSink(config.logging.storage.requests, { + file: getBaseRotatingFileSink(config.logging.log_file_path, { maxFiles: 10, maxSize: 10 * 1024 * 1024, formatter: defaultTextFormatter, diff --git a/utils/markdown.ts b/utils/markdown.ts deleted file mode 100644 index 85ce5ddf..00000000 --- a/utils/markdown.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { getLogger } from "@logtape/logtape"; -import { markdownParse } from "~/classes/functions/status"; -import { sentry } from "./sentry.ts"; - -export const renderMarkdownInPath = async ( - path: string, - defaultText?: string, -): Promise<{ - content: string; - lastModified: Date; -}> => { - let content = await markdownParse(defaultText ?? ""); - let lastModified = new Date(1970, 0, 0); - - const extendedDescriptionFile = Bun.file(path || ""); - - if (path && (await extendedDescriptionFile.exists())) { - content = - (await markdownParse( - (await extendedDescriptionFile.text().catch(async (e) => { - await getLogger("server").error`${e}`; - sentry?.captureException(e); - return ""; - })) || - defaultText || - "", - )) || ""; - lastModified = new Date(extendedDescriptionFile.lastModified); - } - - return { - content, - lastModified, - }; -}; diff --git a/utils/redis.ts b/utils/redis.ts index 066b2510..09ab1bac 100644 --- a/utils/redis.ts +++ b/utils/redis.ts @@ -1,5 +1,5 @@ import IORedis from "ioredis"; -import { config } from "~/packages/config-manager/index.ts"; +import { config } from "~/config.ts"; export const connection = new IORedis({ host: config.redis.queue.host, diff --git a/utils/response.ts b/utils/response.ts index e2beb84f..6aae4e0f 100644 --- a/utils/response.ts +++ b/utils/response.ts @@ -1,4 +1,4 @@ -import { config } from "~/packages/config-manager"; +import { config } from "~/config.ts"; export type Json = | string diff --git a/utils/sentry.ts b/utils/sentry.ts index 6840d515..2502fe29 100644 --- a/utils/sentry.ts +++ b/utils/sentry.ts @@ -1,11 +1,11 @@ import * as Sentry from "@sentry/bun"; +import { config } from "~/config.ts"; import pkg from "~/package.json"; -import { config } from "~/packages/config-manager/index.ts"; const sentryInstance = - config.logging.sentry.enabled && + config.logging.sentry && Sentry.init({ - dsn: config.logging.sentry.dsn, + dsn: config.logging.sentry.dsn.origin, debug: config.logging.sentry.debug, sampleRate: config.logging.sentry.sample_rate, maxBreadcrumbs: config.logging.sentry.max_breadcrumbs, diff --git a/utils/server.ts b/utils/server.ts index 5e5128cd..6288145a 100644 --- a/utils/server.ts +++ b/utils/server.ts @@ -1,31 +1,29 @@ -import type { OpenAPIHono } from "@hono/zod-openapi"; +import type { OpenAPIHono, z } from "@hono/zod-openapi"; import type { Server } from "bun"; -import type { Config } from "~/packages/config-manager/config.type"; +import type { ConfigSchema } from "~/classes/config/schema.ts"; import type { HonoEnv } from "~/types/api"; import { debugResponse } from "./api.ts"; export const createServer = ( - config: Config, + config: z.infer, app: OpenAPIHono, ): Server => Bun.serve({ port: config.http.bind_port, reusePort: true, - tls: config.http.tls.enabled + tls: config.http.tls ? { - key: Bun.file(config.http.tls.key), - cert: Bun.file(config.http.tls.cert), + key: config.http.tls.key.file, + cert: config.http.tls.cert.file, passphrase: config.http.tls.passphrase, - ca: config.http.tls.ca - ? Bun.file(config.http.tls.ca) - : undefined, + ca: config.http.tls.ca?.file, } : undefined, - hostname: config.http.bind || "0.0.0.0", // defaults to "0.0.0.0" + hostname: config.http.bind, async fetch(req, server): Promise { const output = await app.fetch(req, { ip: server.requestIP(req) }); - if (config.logging.log_responses) { + if (config.logging.types.responses) { await debugResponse(output.clone()); }