mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 16:38:19 +01:00
refactor(config): ♻️ Redo config structure from scratch, simplify validation code, improve checks, add support for loading sensitive data from paths
This commit is contained in:
parent
d4afd84019
commit
54fd81f076
502
.github/config.workflow.toml
vendored
502
.github/config.workflow.toml
vendored
|
|
@ -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"
|
host = "localhost"
|
||||||
port = 5432
|
port = 5432
|
||||||
username = "versia"
|
username = "versia"
|
||||||
|
# Sensitive value
|
||||||
password = "versia"
|
password = "versia"
|
||||||
database = "versia"
|
database = "versia"
|
||||||
|
|
||||||
|
# Additional read-only replicas
|
||||||
|
# [[postgres.replicas]]
|
||||||
|
# host = "other-host"
|
||||||
|
# port = 5432
|
||||||
|
# username = "versia"
|
||||||
|
# password = "mycoolpassword2"
|
||||||
|
# database = "replica1"
|
||||||
|
|
||||||
[redis.queue]
|
[redis.queue]
|
||||||
|
# A Redis database used for managing queues.
|
||||||
|
# Required for federation
|
||||||
host = "localhost"
|
host = "localhost"
|
||||||
port = 6379
|
port = 6379
|
||||||
password = ""
|
# Sensitive value
|
||||||
|
# password = "test"
|
||||||
database = 0
|
database = 0
|
||||||
|
|
||||||
[redis.cache]
|
# A Redis database used for caching SQL queries.
|
||||||
host = "localhost"
|
# Optional, can be the same as the queue instance
|
||||||
port = 6379
|
# [redis.cache]
|
||||||
password = ""
|
# host = "localhost"
|
||||||
database = 1
|
# port = 6380
|
||||||
|
# database = 1
|
||||||
|
# password = ""
|
||||||
|
|
||||||
|
# Search and indexing configuration
|
||||||
|
[search]
|
||||||
|
# Enable indexing and searching?
|
||||||
enabled = false
|
enabled = false
|
||||||
|
|
||||||
[sonic]
|
# Optional if search is disabled
|
||||||
host = "localhost"
|
# [search.sonic]
|
||||||
port = 40007
|
# host = "localhost"
|
||||||
password = ""
|
# port = 7700
|
||||||
enabled = false
|
# Sensitive value
|
||||||
|
# password = "test"
|
||||||
|
|
||||||
[signups]
|
[registration]
|
||||||
# Whether to enable registrations or not
|
# Can users sign up freely?
|
||||||
registration = true
|
allow = true
|
||||||
rules = [
|
# NOT IMPLEMENTED
|
||||||
"Do not harass others",
|
require_approval = false
|
||||||
"Be nice to people",
|
# Message to show to users when registration is disabled
|
||||||
"Don't spam",
|
# message = "ran out of spoons to moderate registrations, sorry"
|
||||||
"Don't post illegal content",
|
|
||||||
]
|
|
||||||
|
|
||||||
[http]
|
[http]
|
||||||
|
# URL that the instance will be accessible at
|
||||||
base_url = "http://0.0.0.0:8080"
|
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 = "0.0.0.0"
|
||||||
bind_port = 8080
|
bind_port = 8080
|
||||||
|
|
||||||
# Bans IPv4 or IPv6 IPs (wildcards, networks and ranges are supported)
|
# Bans IPv4 or IPv6 IPs (wildcards, networks and ranges are supported)
|
||||||
banned_ips = []
|
banned_ips = []
|
||||||
|
# Banned user agents, regex format
|
||||||
|
banned_user_agents = [
|
||||||
|
# "curl\/7.68.0",
|
||||||
|
# "wget\/1.20.3",
|
||||||
|
]
|
||||||
|
|
||||||
[smtp]
|
# 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"
|
||||||
|
|
||||||
|
[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
|
# SMTP server to use for sending emails
|
||||||
server = "smtp.example.com"
|
# server = "smtp.example.com"
|
||||||
port = 465
|
# port = 465
|
||||||
username = "test@example.com"
|
# username = "test@example.com"
|
||||||
password = "password123"
|
# Sensitive value
|
||||||
tls = true
|
# password = "password123"
|
||||||
|
# tls = true
|
||||||
|
|
||||||
[media]
|
[media]
|
||||||
# Can be "s3" or "local", where "local" uploads the file to the local filesystem
|
# 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
|
# Changing this value will not retroactively apply to existing data
|
||||||
# from one backend to the other manually
|
# Don't forget to fill in the s3 config :3
|
||||||
backend = "local"
|
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
|
# 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]
|
[media.conversion]
|
||||||
|
# Whether to automatically convert images to another format on upload
|
||||||
convert_images = false
|
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
|
# JXL support will likely not work
|
||||||
convert_to = "webp"
|
convert_to = "image/webp"
|
||||||
|
# Also convert SVG images?
|
||||||
|
convert_vectors = false
|
||||||
|
|
||||||
# [s3]
|
# [s3]
|
||||||
# Can be left blank if you don't use the S3 media backend
|
# Can be left commented if you don't use the S3 media backend
|
||||||
# endpoint = "https://s3-us-west-2.amazonaws.com"
|
# endpoint = "https://s3.example.com"
|
||||||
# access_key = ""
|
# Sensitive value
|
||||||
# secret_access_key = ""
|
# access_key = "XXXXX"
|
||||||
# region = "us-west-2"
|
# Sensitive value
|
||||||
|
# secret_access_key = "XXX"
|
||||||
|
# region = "us-east-1"
|
||||||
# bucket_name = "versia"
|
# bucket_name = "versia"
|
||||||
# public_url = "https://cdn.example.com"
|
# public_url = "https://cdn.example.com"
|
||||||
|
|
||||||
[validation]
|
[validation]
|
||||||
# Self explanatory
|
# Checks user data
|
||||||
max_displayname_size = 50
|
# Does not retroactively apply to previously entered data
|
||||||
max_bio_size = 160
|
[validation.accounts]
|
||||||
max_note_size = 5000
|
max_displayname_characters = 50
|
||||||
max_avatar_size = 5_000_000
|
max_username_characters = 30
|
||||||
max_header_size = 5_000_000
|
max_bio_characters = 5000
|
||||||
max_media_size = 40_000_000
|
max_avatar_bytes = 5_000_000
|
||||||
max_media_attachments = 10
|
max_header_bytes = 5_000_000
|
||||||
max_media_description_size = 1000
|
# Regex is allowed here
|
||||||
max_poll_options = 20
|
disallowed_usernames = [
|
||||||
max_poll_option_size = 500
|
"well-known",
|
||||||
min_poll_duration = 60
|
|
||||||
max_poll_duration = 1893456000
|
|
||||||
max_username_size = 30
|
|
||||||
# An array of strings, defaults are from Akkoma
|
|
||||||
username_blacklist = [
|
|
||||||
".well-known",
|
|
||||||
"~",
|
|
||||||
"about",
|
"about",
|
||||||
"activities",
|
"activities",
|
||||||
"api",
|
"api",
|
||||||
|
|
@ -119,12 +188,14 @@ username_blacklist = [
|
||||||
"search",
|
"search",
|
||||||
"mfa",
|
"mfa",
|
||||||
]
|
]
|
||||||
# Whether to blacklist known temporary email providers
|
max_field_count = 10
|
||||||
blacklist_tempmail = false
|
max_field_name_characters = 1000
|
||||||
# Additional email providers to blacklist
|
max_field_value_characters = 1000
|
||||||
email_blacklist = []
|
max_pinned_notes = 20
|
||||||
# Valid URL schemes, otherwise the URL is parsed as text
|
|
||||||
url_scheme_whitelist = [
|
[validation.notes]
|
||||||
|
max_characters = 5000
|
||||||
|
allowed_url_schemes = [
|
||||||
"http",
|
"http",
|
||||||
"https",
|
"https",
|
||||||
"ftp",
|
"ftp",
|
||||||
|
|
@ -142,76 +213,122 @@ url_scheme_whitelist = [
|
||||||
"mumble",
|
"mumble",
|
||||||
"ssb",
|
"ssb",
|
||||||
"gemini",
|
"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.challenges]
|
[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
|
# "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
|
# This means that they do not require any user interaction, instead
|
||||||
# they require the user's computer to do a small amount of work
|
# they require the user's computer to do a small amount of work
|
||||||
enabled = true
|
# The difficulty of the challenge, higher is will take more time to solve
|
||||||
# The difficulty of the challenge, higher is harder
|
# difficulty = 50000
|
||||||
difficulty = 50000
|
|
||||||
# Challenge expiration time in seconds
|
# Challenge expiration time in seconds
|
||||||
expiration = 300 # 5 minutes
|
# expiration = 300 # 5 minutes
|
||||||
# Leave this empty to generate a new key
|
# Leave this empty to generate a new key
|
||||||
key = "YBpAV0KZOeM/MZ4kOb2E9moH9gCUr00Co9V7ncGRJ3wbd/a9tLDKKFdI0BtOcnlpfx0ZBh0+w3WSvsl0TsesTg=="
|
# Sensitive value
|
||||||
|
# key = ""
|
||||||
|
|
||||||
|
# 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]
|
[notifications]
|
||||||
|
|
||||||
|
# Web Push Notifications configuration.
|
||||||
|
# Leave out to disable.
|
||||||
[notifications.push]
|
[notifications.push]
|
||||||
# Whether to enable push notifications
|
# Subject field embedded in the push notification
|
||||||
enabled = true
|
# subject = "mailto:joe@example.com"
|
||||||
|
#
|
||||||
[notifications.push.vapid]
|
[notifications.push.vapid_keys]
|
||||||
# VAPID keys for push notifications
|
# VAPID keys for push notifications
|
||||||
# Run Versia Server with those values missing to generate new keys
|
# Run Versia Server with those values missing to generate new keys
|
||||||
|
# Sensitive value
|
||||||
public = "BBanhyj2_xWwbTsWld3T49VcAoKZHrVJTzF1f6Av2JwQY_wUi3CF9vZ0WeEcACRj6EEqQ7N35CkUh5epF7n4P_s"
|
public = "BBanhyj2_xWwbTsWld3T49VcAoKZHrVJTzF1f6Av2JwQY_wUi3CF9vZ0WeEcACRj6EEqQ7N35CkUh5epF7n4P_s"
|
||||||
|
# Sensitive value
|
||||||
private = "Eujaz7NsF0rKZOVrAFL7mMpFdl96f591ERsRn81unq0"
|
private = "Eujaz7NsF0rKZOVrAFL7mMpFdl96f591ERsRn81unq0"
|
||||||
# Optional
|
|
||||||
# subject = "mailto:joe@example.com"
|
|
||||||
|
|
||||||
[defaults]
|
[defaults]
|
||||||
# Default visibility for new notes
|
# 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"
|
visibility = "public"
|
||||||
# Default language for new notes
|
# Default language for new notes (ISO code)
|
||||||
language = "en"
|
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 = ""
|
# avatar = ""
|
||||||
# Default header, must be a valid URL or ""
|
# Default header, must be a valid URL or left out for none
|
||||||
# header = ""
|
# 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]
|
[federation]
|
||||||
# This is a list of domain names, such as "mastodon.social" or "pleroma.site"
|
# This is a list of domain names, such as "mastodon.social" or "pleroma.site"
|
||||||
|
|
@ -236,57 +353,140 @@ reactions = []
|
||||||
banners = []
|
banners = []
|
||||||
avatars = []
|
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]
|
[instance]
|
||||||
name = "Versia"
|
name = "Versia"
|
||||||
description = "A test instance of Versia Server"
|
description = "A Versia Server instance"
|
||||||
# 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 = ""
|
|
||||||
|
|
||||||
|
# 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]
|
# Primary instance languages. ISO 639-1 codes.
|
||||||
# Regex filters for federated and local data
|
languages = ["en"]
|
||||||
# Drops data matching the filters
|
|
||||||
# Does not apply retroactively to existing data
|
|
||||||
|
|
||||||
# Note contents
|
[instance.contact]
|
||||||
note_content = [
|
email = "staff@yourinstance.com"
|
||||||
# "(https?://)?(www\\.)?youtube\\.com/watch\\?v=[a-zA-Z0-9_-]+",
|
|
||||||
# "(https?://)?(www\\.)?youtu\\.be/[a-zA-Z0-9_-]+",
|
[instance.branding]
|
||||||
]
|
# logo = "https://cdn.example.com/logo.png"
|
||||||
emoji = []
|
# banner = "https://cdn.example.com/banner.png"
|
||||||
# These will drop users matching the filters
|
|
||||||
username = []
|
# Used for federation. If left empty or missing, the server will generate one for you.
|
||||||
displayname = []
|
[instance.keys]
|
||||||
bio = []
|
# 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]
|
[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
|
# Available levels: debug, info, warning, error, fatal
|
||||||
log_filters = true
|
log_level = "debug"
|
||||||
|
|
||||||
[ratelimits]
|
log_file_path = "logs/versia.log"
|
||||||
# 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]
|
[logging.types]
|
||||||
# Add in any API route in this style here
|
# Either pass a boolean
|
||||||
# Applies before the global ratelimit changes
|
# requests = true
|
||||||
# "/api/v1/accounts/:id/block" = { duration = 30, max = 60 }
|
# Or a table with the following keys:
|
||||||
# "/api/v1/timelines/public" = { duration = 60, max = 200 }
|
# 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]
|
[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]
|
[plugins.config."@versia/openid".keys]
|
||||||
private = "MC4CAQAwBQYDK2VwBCIEID+H5n9PY3zVKZQcq4jrnE1IiRd2EWWr8ApuHUXmuOzl"
|
# Run Versia Server with those values missing to generate a new key
|
||||||
public = "MCowBQYDK2VwAyEAzenliNkgpXYsh3gXTnAoUWzlCPjIOppmAVx2DBlLsC8="
|
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:
|
||||||
|
# <base_url>/oauth/sso/<provider_id>/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):
|
||||||
|
# <base_url>/oauth/sso/<provider_id>/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"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { randomString } from "@/math";
|
import { randomString } from "@/math";
|
||||||
import { Application } from "@versia/kit/db";
|
import { Application } from "@versia/kit/db";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
import { fakeRequest, getTestUsers } from "~/tests/utils";
|
import { fakeRequest, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, deleteUsers, passwords } = await getTestUsers(1);
|
const { users, deleteUsers, passwords } = await getTestUsers(1);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import type { Context } from "hono";
|
||||||
import { setCookie } from "hono/cookie";
|
import { setCookie } from "hono/cookie";
|
||||||
import { SignJWT } from "jose";
|
import { SignJWT } from "jose";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
const schemas = {
|
const schemas = {
|
||||||
form: z.object({
|
form: z.object({
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { db } from "@versia/kit/db";
|
import { db } from "@versia/kit/db";
|
||||||
import { Applications, Tokens } from "@versia/kit/tables";
|
import { Applications, Tokens } from "@versia/kit/tables";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
const schemas = {
|
const schemas = {
|
||||||
query: z.object({
|
query: z.object({
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { randomString } from "@/math";
|
import { randomString } from "@/math";
|
||||||
import { Application } from "@versia/kit/db";
|
import { Application } from "@versia/kit/db";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
import { fakeRequest, getTestUsers } from "~/tests/utils";
|
import { fakeRequest, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, deleteUsers, passwords } = await getTestUsers(1);
|
const { users, deleteUsers, passwords } = await getTestUsers(1);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { User } from "@versia/kit/db";
|
||||||
import { Users } from "@versia/kit/tables";
|
import { Users } from "@versia/kit/tables";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import type { Context } from "hono";
|
import type { Context } from "hono";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
const schemas = {
|
const schemas = {
|
||||||
form: z.object({
|
form: z.object({
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ import { Timeline } from "@versia/kit/db";
|
||||||
import { Notes, RolePermissions } from "@versia/kit/tables";
|
import { Notes, RolePermissions } from "@versia/kit/tables";
|
||||||
import { and, eq, gt, gte, inArray, isNull, lt, or, sql } from "drizzle-orm";
|
import { and, eq, gt, gte, inArray, isNull, lt, or, sql } from "drizzle-orm";
|
||||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
|
import { zBoolean } from "~/classes/schemas/common.ts";
|
||||||
import { Status as StatusSchema } from "~/classes/schemas/status";
|
import { Status as StatusSchema } from "~/classes/schemas/status";
|
||||||
import { zBoolean } from "~/packages/config-manager/config.type";
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ import { Users } from "@versia/kit/tables";
|
||||||
import { and, eq, isNull } from "drizzle-orm";
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
import ISO6391 from "iso-639-1";
|
import ISO6391 from "iso-639-1";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
import { config } from "~/packages/config-manager";
|
import { zBoolean } from "~/classes/schemas/common";
|
||||||
import { zBoolean } from "~/packages/config-manager/config.type";
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
username: z.string().openapi({
|
username: z.string().openapi({
|
||||||
|
|
@ -157,7 +157,7 @@ export default apiRoute((app) =>
|
||||||
const { username, email, password, agreement, locale } =
|
const { username, email, password, agreement, locale } =
|
||||||
context.req.valid("json");
|
context.req.valid("json");
|
||||||
|
|
||||||
if (!config.signups.registration) {
|
if (!config.registration.allow) {
|
||||||
throw new ApiError(422, "Registration is disabled");
|
throw new ApiError(422, "Registration is disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -217,7 +217,11 @@ export default apiRoute((app) =>
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if username doesnt match filters
|
// 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({
|
errors.details.username.push({
|
||||||
error: "ERR_INVALID",
|
error: "ERR_INVALID",
|
||||||
description: "contains blocked words",
|
description: "contains blocked words",
|
||||||
|
|
@ -225,10 +229,13 @@ export default apiRoute((app) =>
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if username is too long
|
// 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({
|
errors.details.username.push({
|
||||||
error: "ERR_TOO_LONG",
|
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
|
// 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({
|
errors.details.username.push({
|
||||||
error: "ERR_RESERVED",
|
error: "ERR_RESERVED",
|
||||||
description: "is reserved",
|
description: "is reserved",
|
||||||
|
|
@ -274,9 +285,11 @@ export default apiRoute((app) =>
|
||||||
|
|
||||||
// Check if email is blocked
|
// Check if email is blocked
|
||||||
if (
|
if (
|
||||||
config.validation.email_blacklist.includes(email) ||
|
config.validation.emails.disallowed_domains.some((f) =>
|
||||||
(config.validation.blacklist_tempmail &&
|
f.test(email.split("@")[1]),
|
||||||
tempmailDomains.domains.includes((email ?? "").split("@")[1]))
|
) ||
|
||||||
|
(config.validation.emails.disallow_tempmail &&
|
||||||
|
tempmailDomains.domains.includes(email.split("@")[1]))
|
||||||
) {
|
) {
|
||||||
errors.details.email.push({
|
errors.details.email.push({
|
||||||
error: "ERR_BLOCKED",
|
error: "ERR_BLOCKED",
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { and, eq, isNull } from "drizzle-orm";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
import { Account } from "~/classes/schemas/account";
|
import { Account } from "~/classes/schemas/account";
|
||||||
import { Account as AccountSchema } 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({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Relationship } from "@versia/kit/db";
|
import { Relationship } from "@versia/kit/db";
|
||||||
import { RolePermissions } from "@versia/kit/tables";
|
import { RolePermissions } from "@versia/kit/tables";
|
||||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
|
import { zBoolean } from "~/classes/schemas/common.ts";
|
||||||
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
||||||
import { zBoolean } from "~/packages/config-manager/config.type";
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { eq, ilike, not, or, sql } from "drizzle-orm";
|
||||||
import stringComparison from "string-comparison";
|
import stringComparison from "string-comparison";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
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({
|
export const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import type { Account as APIAccount } from "@versia/client/types";
|
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";
|
import { fakeRequest, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { tokens, deleteUsers } = await getTestUsers(1);
|
const { tokens, deleteUsers } = await getTestUsers(1);
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ import { and, eq, isNull } from "drizzle-orm";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
import { contentToHtml } from "~/classes/functions/status";
|
import { contentToHtml } from "~/classes/functions/status";
|
||||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
import { zBoolean } from "~/packages/config-manager/config.type";
|
import { zBoolean } from "~/classes/schemas/common.ts";
|
||||||
import { config } from "~/packages/config-manager/index.ts";
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "patch",
|
method: "patch",
|
||||||
|
|
@ -62,9 +62,9 @@ const route = createRoute({
|
||||||
.refine(
|
.refine(
|
||||||
(v) =>
|
(v) =>
|
||||||
v.size <=
|
v.size <=
|
||||||
config.validation
|
config.validation.accounts
|
||||||
.max_avatar_size,
|
.max_avatar_bytes,
|
||||||
`Avatar must be less than ${config.validation.max_avatar_size} bytes`,
|
`Avatar must be less than ${config.validation.accounts.max_avatar_bytes} bytes`,
|
||||||
)
|
)
|
||||||
.openapi({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
|
|
@ -84,9 +84,9 @@ const route = createRoute({
|
||||||
.refine(
|
.refine(
|
||||||
(v) =>
|
(v) =>
|
||||||
v.size <=
|
v.size <=
|
||||||
config.validation
|
config.validation.accounts
|
||||||
.max_header_size,
|
.max_header_bytes,
|
||||||
`Header must be less than ${config.validation.max_header_size} bytes`,
|
`Header must be less than ${config.validation.accounts.max_header_bytes} bytes`,
|
||||||
)
|
)
|
||||||
.openapi({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
|
|
@ -144,7 +144,9 @@ const route = createRoute({
|
||||||
.element.shape.value,
|
.element.shape.value,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.max(config.validation.max_field_count),
|
.max(
|
||||||
|
config.validation.accounts.max_field_count,
|
||||||
|
),
|
||||||
})
|
})
|
||||||
.partial(),
|
.partial(),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { apiRoute, auth } from "@/api";
|
||||||
import { generateChallenge } from "@/challenges";
|
import { generateChallenge } from "@/challenges";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
import { ErrorSchema } from "~/types/api";
|
import { ErrorSchema } from "~/types/api";
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
|
|
@ -45,7 +45,7 @@ const route = createRoute({
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.openapi(route, async (context) => {
|
app.openapi(route, async (context) => {
|
||||||
if (!config.validation.challenges.enabled) {
|
if (!config.validation.challenges) {
|
||||||
throw new ApiError(400, "Challenges are disabled in config");
|
throw new ApiError(400, "Challenges are disabled in config");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { RolePermissions } from "@versia/kit/tables";
|
import { RolePermissions } from "@versia/kit/tables";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
import { CustomEmoji as CustomEmojiSchema } from "~/classes/schemas/emoji";
|
import { CustomEmoji as CustomEmojiSchema } from "~/classes/schemas/emoji";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
import { ErrorSchema } from "~/types/api";
|
import { ErrorSchema } from "~/types/api";
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
|
|
@ -31,8 +31,8 @@ const schema = z
|
||||||
"Emoji image encoded using multipart/form-data",
|
"Emoji image encoded using multipart/form-data",
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(v) => v.size <= config.validation.max_emoji_size,
|
(v) => v.size <= config.validation.emojis.max_bytes,
|
||||||
`Emoji must be less than ${config.validation.max_emoji_size} bytes`,
|
`Emoji must be less than ${config.validation.emojis.max_bytes} bytes`,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
category: CustomEmojiSchema.shape.category.optional(),
|
category: CustomEmojiSchema.shape.category.optional(),
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { Emojis, RolePermissions } from "@versia/kit/tables";
|
||||||
import { and, eq, isNull, or } from "drizzle-orm";
|
import { and, eq, isNull, or } from "drizzle-orm";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
import { CustomEmoji as CustomEmojiSchema } from "~/classes/schemas/emoji";
|
import { CustomEmoji as CustomEmojiSchema } from "~/classes/schemas/emoji";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
shortcode: CustomEmojiSchema.shape.shortcode,
|
shortcode: CustomEmojiSchema.shape.shortcode,
|
||||||
|
|
@ -25,8 +25,8 @@ const schema = z.object({
|
||||||
"Emoji image encoded using multipart/form-data",
|
"Emoji image encoded using multipart/form-data",
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(v) => v.size <= config.validation.max_emoji_size,
|
(v) => v.size <= config.validation.emojis.max_bytes,
|
||||||
`Emoji must be less than ${config.validation.max_emoji_size} bytes`,
|
`Emoji must be less than ${config.validation.emojis.max_bytes} bytes`,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
category: CustomEmojiSchema.shape.category.optional(),
|
category: CustomEmojiSchema.shape.category.optional(),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { apiRoute } from "@/api";
|
import { apiRoute } from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ describe("/api/v1/instance/extended_description", () => {
|
||||||
|
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
expect(json).toEqual({
|
expect(json).toEqual({
|
||||||
updated_at: new Date(1970, 0, 0).toISOString(),
|
updated_at: new Date(0).toISOString(),
|
||||||
content:
|
content:
|
||||||
'<p>This is a <a href="https://versia.pub">Versia</a> server with the default extended description.</p>\n',
|
'<p>This is a <a href="https://versia.pub">Versia</a> server with the default extended description.</p>\n',
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { apiRoute } from "@/api";
|
import { apiRoute } from "@/api";
|
||||||
import { renderMarkdownInPath } from "@/markdown";
|
|
||||||
import { createRoute } from "@hono/zod-openapi";
|
import { createRoute } from "@hono/zod-openapi";
|
||||||
|
import { markdownParse } from "~/classes/functions/status";
|
||||||
import { ExtendedDescription as ExtendedDescriptionSchema } from "~/classes/schemas/extended-description";
|
import { ExtendedDescription as ExtendedDescriptionSchema } from "~/classes/schemas/extended-description";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
|
|
@ -27,14 +27,17 @@ const route = createRoute({
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.openapi(route, async (context) => {
|
app.openapi(route, async (context) => {
|
||||||
const { content, lastModified } = await renderMarkdownInPath(
|
const content = await markdownParse(
|
||||||
config.instance.extended_description_path ?? "",
|
config.instance.extended_description_path?.content ??
|
||||||
"This is a [Versia](https://versia.pub) server with the default extended description.",
|
"This is a [Versia](https://versia.pub) server with the default extended description.",
|
||||||
);
|
);
|
||||||
|
|
||||||
return context.json(
|
return context.json(
|
||||||
{
|
{
|
||||||
updated_at: lastModified.toISOString(),
|
updated_at: new Date(
|
||||||
|
config.instance.extended_description_path?.file
|
||||||
|
.lastModified ?? 0,
|
||||||
|
).toISOString(),
|
||||||
content,
|
content,
|
||||||
},
|
},
|
||||||
200,
|
200,
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { apiRoute, auth } from "@/api";
|
import { apiRoute, auth } from "@/api";
|
||||||
import { renderMarkdownInPath } from "@/markdown";
|
|
||||||
import { proxyUrl } from "@/response";
|
import { proxyUrl } from "@/response";
|
||||||
import { createRoute, type z } from "@hono/zod-openapi";
|
import { createRoute, type z } from "@hono/zod-openapi";
|
||||||
import { Instance, Note, User } from "@versia/kit/db";
|
import { Instance, Note, User } from "@versia/kit/db";
|
||||||
import { Users } from "@versia/kit/tables";
|
import { Users } from "@versia/kit/tables";
|
||||||
import { and, eq, isNull } from "drizzle-orm";
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
|
import { markdownParse } from "~/classes/functions/status";
|
||||||
import { InstanceV1 as InstanceV1Schema } from "~/classes/schemas/instance-v1";
|
import { InstanceV1 as InstanceV1Schema } from "~/classes/schemas/instance-v1";
|
||||||
|
import { config } from "~/config.ts";
|
||||||
import manifest from "~/package.json";
|
import manifest from "~/package.json";
|
||||||
import { config } from "~/packages/config-manager";
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
|
|
@ -65,35 +65,38 @@ export default apiRoute((app) =>
|
||||||
}
|
}
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
const { content } = await renderMarkdownInPath(
|
const content = await markdownParse(
|
||||||
config.instance.extended_description_path ?? "",
|
config.instance.extended_description_path?.content ??
|
||||||
"This is a [Versia](https://versia.pub) server with the default extended description.",
|
"This is a [Versia](https://versia.pub) server with the default extended description.",
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: fill in more values
|
|
||||||
return context.json({
|
return context.json({
|
||||||
approval_required: false,
|
approval_required: config.registration.require_approval,
|
||||||
configuration: {
|
configuration: {
|
||||||
polls: {
|
polls: {
|
||||||
max_characters_per_option:
|
max_characters_per_option:
|
||||||
config.validation.max_poll_option_size,
|
config.validation.polls.max_option_characters,
|
||||||
max_expiration: config.validation.max_poll_duration,
|
max_expiration:
|
||||||
max_options: config.validation.max_poll_options,
|
config.validation.polls.max_duration_seconds,
|
||||||
min_expiration: config.validation.min_poll_duration,
|
max_options: config.validation.polls.max_options,
|
||||||
|
min_expiration:
|
||||||
|
config.validation.polls.min_duration_seconds,
|
||||||
},
|
},
|
||||||
statuses: {
|
statuses: {
|
||||||
characters_reserved_per_url: 0,
|
characters_reserved_per_url: 0,
|
||||||
max_characters: config.validation.max_note_size,
|
max_characters: config.validation.notes.max_characters,
|
||||||
max_media_attachments:
|
max_media_attachments:
|
||||||
config.validation.max_media_attachments,
|
config.validation.notes.max_attachments,
|
||||||
},
|
},
|
||||||
media_attachments: {
|
media_attachments: {
|
||||||
supported_mime_types: config.validation.allowed_mime_types,
|
supported_mime_types:
|
||||||
image_size_limit: config.validation.max_media_size,
|
config.validation.media.allowed_mime_types,
|
||||||
image_matrix_limit: config.validation.max_media_size,
|
image_size_limit: config.validation.media.max_bytes,
|
||||||
video_size_limit: config.validation.max_media_size,
|
// TODO: Implement
|
||||||
video_frame_rate_limit: config.validation.max_media_size,
|
image_matrix_limit: 1 ** 10,
|
||||||
video_matrix_limit: config.validation.max_media_size,
|
video_size_limit: 1 ** 10,
|
||||||
|
video_frame_rate_limit: 60,
|
||||||
|
video_matrix_limit: 1 ** 10,
|
||||||
},
|
},
|
||||||
accounts: {
|
accounts: {
|
||||||
max_featured_tags: 100,
|
max_featured_tags: 100,
|
||||||
|
|
@ -101,23 +104,22 @@ export default apiRoute((app) =>
|
||||||
},
|
},
|
||||||
short_description: config.instance.description,
|
short_description: config.instance.description,
|
||||||
description: content,
|
description: content,
|
||||||
// TODO: Add contact email
|
email: config.instance.contact.email,
|
||||||
email: "",
|
|
||||||
invites_enabled: false,
|
invites_enabled: false,
|
||||||
registrations: config.signups.registration,
|
registrations: config.registration.allow,
|
||||||
// TODO: Implement
|
languages: config.instance.languages,
|
||||||
languages: ["en"],
|
rules: config.instance.rules.map((r, index) => ({
|
||||||
rules: config.signups.rules.map((r, index) => ({
|
|
||||||
id: String(index),
|
id: String(index),
|
||||||
text: r,
|
text: r.text,
|
||||||
|
hint: r.hint,
|
||||||
})),
|
})),
|
||||||
stats: {
|
stats: {
|
||||||
domain_count: knownDomainsCount,
|
domain_count: knownDomainsCount,
|
||||||
status_count: statusCount,
|
status_count: statusCount,
|
||||||
user_count: userCount,
|
user_count: userCount,
|
||||||
},
|
},
|
||||||
thumbnail: config.instance.logo
|
thumbnail: config.instance.branding.logo
|
||||||
? proxyUrl(config.instance.logo).toString()
|
? proxyUrl(config.instance.branding.logo).toString()
|
||||||
: null,
|
: null,
|
||||||
title: config.instance.name,
|
title: config.instance.name,
|
||||||
uri: config.http.base_url.host,
|
uri: config.http.base_url.host,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ describe("/api/v1/instance/privacy_policy", () => {
|
||||||
|
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
expect(json).toEqual({
|
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.
|
// This instance has not provided any privacy policy.
|
||||||
content:
|
content:
|
||||||
"<p>This instance has not provided any privacy policy.</p>\n",
|
"<p>This instance has not provided any privacy policy.</p>\n",
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { apiRoute, auth } from "@/api";
|
import { apiRoute, auth } from "@/api";
|
||||||
import { renderMarkdownInPath } from "@/markdown";
|
|
||||||
import { createRoute } from "@hono/zod-openapi";
|
import { createRoute } from "@hono/zod-openapi";
|
||||||
|
import { markdownParse } from "~/classes/functions/status";
|
||||||
import { PrivacyPolicy as PrivacyPolicySchema } from "~/classes/schemas/privacy-policy";
|
import { PrivacyPolicy as PrivacyPolicySchema } from "~/classes/schemas/privacy-policy";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
|
|
@ -32,13 +32,15 @@ const route = createRoute({
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.openapi(route, async (context) => {
|
app.openapi(route, async (context) => {
|
||||||
const { content, lastModified } = await renderMarkdownInPath(
|
const content = await markdownParse(
|
||||||
config.instance.privacy_policy_path ?? "",
|
config.instance.privacy_policy_path?.content ??
|
||||||
"This instance has not provided any privacy policy.",
|
"This instance has not provided any privacy policy.",
|
||||||
);
|
);
|
||||||
|
|
||||||
return context.json({
|
return context.json({
|
||||||
updated_at: lastModified.toISOString(),
|
updated_at: new Date(
|
||||||
|
config.instance.privacy_policy_path?.file.lastModified ?? 0,
|
||||||
|
).toISOString(),
|
||||||
content,
|
content,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, expect, test } from "bun:test";
|
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";
|
import { fakeRequest } from "~/tests/utils";
|
||||||
|
|
||||||
// /api/v1/instance/rules
|
// /api/v1/instance/rules
|
||||||
|
|
@ -11,10 +11,10 @@ describe("/api/v1/instance/rules", () => {
|
||||||
|
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
expect(json).toEqual(
|
expect(json).toEqual(
|
||||||
config.signups.rules.map((rule, index) => ({
|
config.instance.rules.map((r, index) => ({
|
||||||
id: String(index),
|
id: String(index),
|
||||||
text: rule,
|
text: r.text,
|
||||||
hint: "",
|
hint: r.hint,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { apiRoute, auth } from "@/api";
|
import { apiRoute, auth } from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Rule as RuleSchema } from "~/classes/schemas/rule";
|
import { Rule as RuleSchema } from "~/classes/schemas/rule";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
|
|
@ -32,10 +32,10 @@ const route = createRoute({
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.openapi(route, (context) => {
|
app.openapi(route, (context) => {
|
||||||
return context.json(
|
return context.json(
|
||||||
config.signups.rules.map((rule, index) => ({
|
config.instance.rules.map((r, index) => ({
|
||||||
id: String(index),
|
id: String(index),
|
||||||
text: rule,
|
text: r.text,
|
||||||
hint: "",
|
hint: r.hint,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ describe("/api/v1/instance/terms_of_service", () => {
|
||||||
|
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
expect(json).toEqual({
|
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.
|
// This instance has not provided any terms of service.
|
||||||
content:
|
content:
|
||||||
"<p>This instance has not provided any terms of service.</p>\n",
|
"<p>This instance has not provided any terms of service.</p>\n",
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { apiRoute, auth } from "@/api";
|
import { apiRoute, auth } from "@/api";
|
||||||
import { renderMarkdownInPath } from "@/markdown";
|
|
||||||
import { createRoute } from "@hono/zod-openapi";
|
import { createRoute } from "@hono/zod-openapi";
|
||||||
|
import { markdownParse } from "~/classes/functions/status";
|
||||||
import { TermsOfService as TermsOfServiceSchema } from "~/classes/schemas/tos";
|
import { TermsOfService as TermsOfServiceSchema } from "~/classes/schemas/tos";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
|
|
@ -33,13 +33,15 @@ const route = createRoute({
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.openapi(route, async (context) => {
|
app.openapi(route, async (context) => {
|
||||||
const { content, lastModified } = await renderMarkdownInPath(
|
const content = await markdownParse(
|
||||||
config.instance.tos_path ?? "",
|
config.instance.tos_path?.content ??
|
||||||
"This instance has not provided any terms of service.",
|
"This instance has not provided any terms of service.",
|
||||||
);
|
);
|
||||||
|
|
||||||
return context.json({
|
return context.json({
|
||||||
updated_at: lastModified.toISOString(),
|
updated_at: new Date(
|
||||||
|
config.instance.tos_path?.file.lastModified ?? 0,
|
||||||
|
).toISOString(),
|
||||||
content,
|
content,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ import { Timeline } from "@versia/kit/db";
|
||||||
import { Notifications, RolePermissions } from "@versia/kit/tables";
|
import { Notifications, RolePermissions } from "@versia/kit/tables";
|
||||||
import { and, eq, gt, gte, inArray, lt, not, sql } from "drizzle-orm";
|
import { and, eq, gt, gte, inArray, lt, not, sql } from "drizzle-orm";
|
||||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
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 { Notification as NotificationSchema } from "~/classes/schemas/notification.ts";
|
||||||
import { zBoolean } from "~/packages/config-manager/config.type";
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { Role } from "@versia/kit/db";
|
import { Role } from "@versia/kit/db";
|
||||||
import { RolePermissions } from "@versia/kit/tables";
|
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";
|
import { fakeRequest, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, deleteUsers, tokens } = await getTestUsers(1);
|
const { users, deleteUsers, tokens } = await getTestUsers(1);
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,13 @@ import { Media } from "@versia/kit/db";
|
||||||
import { RolePermissions } from "@versia/kit/tables";
|
import { RolePermissions } from "@versia/kit/tables";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
import { Attachment as AttachmentSchema } from "~/classes/schemas/attachment";
|
import { Attachment as AttachmentSchema } from "~/classes/schemas/attachment";
|
||||||
|
import { zBoolean } from "~/classes/schemas/common.ts";
|
||||||
import { PollOption } from "~/classes/schemas/poll";
|
import { PollOption } from "~/classes/schemas/poll";
|
||||||
import {
|
import {
|
||||||
Status as StatusSchema,
|
Status as StatusSchema,
|
||||||
StatusSource as StatusSourceSchema,
|
StatusSource as StatusSourceSchema,
|
||||||
} from "~/classes/schemas/status";
|
} from "~/classes/schemas/status";
|
||||||
import { zBoolean } from "~/packages/config-manager/config.type";
|
import { config } from "~/config.ts";
|
||||||
import { config } from "~/packages/config-manager/index.ts";
|
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
|
|
@ -35,7 +35,7 @@ const schema = z
|
||||||
}),
|
}),
|
||||||
media_ids: z
|
media_ids: z
|
||||||
.array(AttachmentSchema.shape.id)
|
.array(AttachmentSchema.shape.id)
|
||||||
.max(config.validation.max_media_attachments)
|
.max(config.validation.notes.max_attachments)
|
||||||
.default([])
|
.default([])
|
||||||
.openapi({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
|
|
@ -51,7 +51,7 @@ const schema = z
|
||||||
language: StatusSchema.shape.language.optional(),
|
language: StatusSchema.shape.language.optional(),
|
||||||
"poll[options]": z
|
"poll[options]": z
|
||||||
.array(PollOption.shape.title)
|
.array(PollOption.shape.title)
|
||||||
.max(config.validation.max_poll_options)
|
.max(config.validation.polls.max_options)
|
||||||
.optional()
|
.optional()
|
||||||
.openapi({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
|
|
@ -60,8 +60,8 @@ const schema = z
|
||||||
"poll[expires_in]": z.coerce
|
"poll[expires_in]": z.coerce
|
||||||
.number()
|
.number()
|
||||||
.int()
|
.int()
|
||||||
.min(config.validation.min_poll_duration)
|
.min(config.validation.polls.min_duration_seconds)
|
||||||
.max(config.validation.max_poll_duration)
|
.max(config.validation.polls.max_duration_seconds)
|
||||||
.optional()
|
.optional()
|
||||||
.openapi({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import type { Status as ApiStatus } from "@versia/client/types";
|
||||||
import { Media, db } from "@versia/kit/db";
|
import { Media, db } from "@versia/kit/db";
|
||||||
import { Emojis } from "@versia/kit/tables";
|
import { Emojis } from "@versia/kit/tables";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { config } from "~/packages/config-manager/index.ts";
|
import { config } from "~/config.ts";
|
||||||
import { fakeRequest, getTestUsers } from "~/tests/utils";
|
import { fakeRequest, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, tokens, deleteUsers } = await getTestUsers(5);
|
const { users, tokens, deleteUsers } = await getTestUsers(5);
|
||||||
|
|
@ -61,7 +61,7 @@ describe("/api/v1/statuses", () => {
|
||||||
Authorization: `Bearer ${tokens[0].data.accessToken}`,
|
Authorization: `Bearer ${tokens[0].data.accessToken}`,
|
||||||
},
|
},
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
status: "a".repeat(config.validation.max_note_size + 1),
|
status: "a".repeat(config.validation.notes.max_characters + 1),
|
||||||
local_only: "true",
|
local_only: "true",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,13 @@ import { Media, Note } from "@versia/kit/db";
|
||||||
import { RolePermissions } from "@versia/kit/tables";
|
import { RolePermissions } from "@versia/kit/tables";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
import { Attachment as AttachmentSchema } from "~/classes/schemas/attachment";
|
import { Attachment as AttachmentSchema } from "~/classes/schemas/attachment";
|
||||||
|
import { zBoolean } from "~/classes/schemas/common.ts";
|
||||||
import { PollOption } from "~/classes/schemas/poll";
|
import { PollOption } from "~/classes/schemas/poll";
|
||||||
import {
|
import {
|
||||||
Status as StatusSchema,
|
Status as StatusSchema,
|
||||||
StatusSource as StatusSourceSchema,
|
StatusSource as StatusSourceSchema,
|
||||||
} from "~/classes/schemas/status";
|
} from "~/classes/schemas/status";
|
||||||
import { zBoolean } from "~/packages/config-manager/config.type";
|
import { config } from "~/config.ts";
|
||||||
import { config } from "~/packages/config-manager/index.ts";
|
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
|
|
@ -28,7 +28,7 @@ const schema = z
|
||||||
}),
|
}),
|
||||||
media_ids: z
|
media_ids: z
|
||||||
.array(AttachmentSchema.shape.id)
|
.array(AttachmentSchema.shape.id)
|
||||||
.max(config.validation.max_media_attachments)
|
.max(config.validation.notes.max_attachments)
|
||||||
.default([])
|
.default([])
|
||||||
.openapi({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
|
|
@ -44,7 +44,7 @@ const schema = z
|
||||||
language: StatusSchema.shape.language.optional(),
|
language: StatusSchema.shape.language.optional(),
|
||||||
"poll[options]": z
|
"poll[options]": z
|
||||||
.array(PollOption.shape.title)
|
.array(PollOption.shape.title)
|
||||||
.max(config.validation.max_poll_options)
|
.max(config.validation.polls.max_options)
|
||||||
.optional()
|
.optional()
|
||||||
.openapi({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
|
|
@ -53,8 +53,8 @@ const schema = z
|
||||||
"poll[expires_in]": z.coerce
|
"poll[expires_in]": z.coerce
|
||||||
.number()
|
.number()
|
||||||
.int()
|
.int()
|
||||||
.min(config.validation.min_poll_duration)
|
.min(config.validation.polls.min_duration_seconds)
|
||||||
.max(config.validation.max_poll_duration)
|
.max(config.validation.polls.max_duration_seconds)
|
||||||
.optional()
|
.optional()
|
||||||
.openapi({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import type { Status as ApiStatus } from "@versia/client/types";
|
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";
|
import { fakeRequest, getTestStatuses, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, tokens, deleteUsers } = await getTestUsers(5);
|
const { users, tokens, deleteUsers } = await getTestUsers(5);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import type { Status as ApiStatus } from "@versia/client/types";
|
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";
|
import { fakeRequest, getTestStatuses, getTestUsers } from "~/tests/utils";
|
||||||
|
|
||||||
const { users, tokens, deleteUsers } = await getTestUsers(5);
|
const { users, tokens, deleteUsers } = await getTestUsers(5);
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Timeline } from "@versia/kit/db";
|
import { Timeline } from "@versia/kit/db";
|
||||||
import { Notes, RolePermissions } from "@versia/kit/tables";
|
import { Notes, RolePermissions } from "@versia/kit/tables";
|
||||||
import { and, eq, gt, gte, inArray, lt, or, sql } from "drizzle-orm";
|
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 { Status as StatusSchema } from "~/classes/schemas/status";
|
||||||
import { zBoolean } from "~/packages/config-manager/config.type";
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ import { db } from "@versia/kit/db";
|
||||||
import { FilterKeywords, Filters, RolePermissions } from "@versia/kit/tables";
|
import { FilterKeywords, Filters, RolePermissions } from "@versia/kit/tables";
|
||||||
import { type SQL, and, eq, inArray } from "drizzle-orm";
|
import { type SQL, and, eq, inArray } from "drizzle-orm";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
import { zBoolean } from "~/classes/schemas/common.ts";
|
||||||
import {
|
import {
|
||||||
FilterKeyword as FilterKeywordSchema,
|
FilterKeyword as FilterKeywordSchema,
|
||||||
Filter as FilterSchema,
|
Filter as FilterSchema,
|
||||||
} from "~/classes/schemas/filters";
|
} from "~/classes/schemas/filters";
|
||||||
import { zBoolean } from "~/packages/config-manager/config.type";
|
|
||||||
import { ErrorSchema } from "~/types/api";
|
import { ErrorSchema } from "~/types/api";
|
||||||
|
|
||||||
const routeGet = createRoute({
|
const routeGet = createRoute({
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import { User } from "@versia/kit/db";
|
||||||
import { Users } from "@versia/kit/tables";
|
import { Users } from "@versia/kit/tables";
|
||||||
import { and, eq, isNull } from "drizzle-orm";
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
import { Instance as InstanceSchema } from "~/classes/schemas/instance";
|
import { Instance as InstanceSchema } from "~/classes/schemas/instance";
|
||||||
|
import { config } from "~/config.ts";
|
||||||
import pkg from "~/package.json";
|
import pkg from "~/package.json";
|
||||||
import { config } from "~/packages/config-manager";
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
|
|
@ -69,92 +69,99 @@ export default apiRoute((app) =>
|
||||||
mastodon: 1,
|
mastodon: 1,
|
||||||
},
|
},
|
||||||
thumbnail: {
|
thumbnail: {
|
||||||
url: config.instance.logo
|
url: config.instance.branding.logo
|
||||||
? proxyUrl(config.instance.logo).toString()
|
? proxyUrl(config.instance.branding.logo).toString()
|
||||||
: pkg.icon,
|
: pkg.icon,
|
||||||
},
|
},
|
||||||
banner: {
|
banner: {
|
||||||
url: config.instance.banner
|
url: config.instance.branding.banner
|
||||||
? proxyUrl(config.instance.banner).toString()
|
? proxyUrl(config.instance.branding.banner).toString()
|
||||||
: null,
|
: null,
|
||||||
},
|
},
|
||||||
icon: [],
|
icon: [],
|
||||||
languages: ["en"],
|
languages: config.instance.languages,
|
||||||
configuration: {
|
configuration: {
|
||||||
urls: {
|
urls: {
|
||||||
// TODO: Implement Streaming API
|
// TODO: Implement Streaming API
|
||||||
streaming: "",
|
streaming: "",
|
||||||
},
|
},
|
||||||
vapid: {
|
vapid: {
|
||||||
// TODO: Fill in vapid values
|
public_key:
|
||||||
public_key: "",
|
config.notifications.push?.vapid_keys.public ?? "",
|
||||||
},
|
},
|
||||||
accounts: {
|
accounts: {
|
||||||
max_featured_tags: 100,
|
max_featured_tags: 100,
|
||||||
max_displayname_characters:
|
max_displayname_characters:
|
||||||
config.validation.max_displayname_size,
|
config.validation.accounts.max_displayname_characters,
|
||||||
avatar_limit: config.validation.max_avatar_size,
|
avatar_limit: config.validation.accounts.max_avatar_bytes,
|
||||||
header_limit: config.validation.max_header_size,
|
header_limit: config.validation.accounts.max_header_bytes,
|
||||||
max_username_characters:
|
max_username_characters:
|
||||||
config.validation.max_username_size,
|
config.validation.accounts.max_username_characters,
|
||||||
max_note_characters: config.validation.max_bio_size,
|
max_note_characters:
|
||||||
max_pinned_statuses: 100,
|
config.validation.accounts.max_bio_characters,
|
||||||
|
max_pinned_statuses:
|
||||||
|
config.validation.accounts.max_pinned_notes,
|
||||||
fields: {
|
fields: {
|
||||||
max_fields: config.validation.max_field_count,
|
max_fields: config.validation.accounts.max_field_count,
|
||||||
max_name_characters:
|
max_name_characters:
|
||||||
config.validation.max_field_name_size,
|
config.validation.accounts
|
||||||
|
.max_field_name_characters,
|
||||||
max_value_characters:
|
max_value_characters:
|
||||||
config.validation.max_field_value_size,
|
config.validation.accounts
|
||||||
|
.max_field_value_characters,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
statuses: {
|
statuses: {
|
||||||
max_characters: config.validation.max_note_size,
|
max_characters: config.validation.notes.max_characters,
|
||||||
max_media_attachments:
|
max_media_attachments:
|
||||||
config.validation.max_media_attachments,
|
config.validation.notes.max_attachments,
|
||||||
characters_reserved_per_url: 0,
|
// TODO: Implement
|
||||||
|
characters_reserved_per_url: 13,
|
||||||
},
|
},
|
||||||
media_attachments: {
|
media_attachments: {
|
||||||
supported_mime_types: config.validation.allowed_mime_types,
|
supported_mime_types:
|
||||||
image_size_limit: config.validation.max_media_size,
|
config.validation.media.allowed_mime_types,
|
||||||
image_matrix_limit: config.validation.max_media_size,
|
image_size_limit: config.validation.media.max_bytes,
|
||||||
video_size_limit: config.validation.max_media_size,
|
image_matrix_limit: 1 ** 10,
|
||||||
video_frame_rate_limit: config.validation.max_media_size,
|
video_size_limit: 1 ** 10,
|
||||||
video_matrix_limit: config.validation.max_media_size,
|
video_frame_rate_limit: 60,
|
||||||
|
video_matrix_limit: 1 ** 10,
|
||||||
description_limit:
|
description_limit:
|
||||||
config.validation.max_media_description_size,
|
config.validation.media.max_description_characters,
|
||||||
},
|
},
|
||||||
emojis: {
|
emojis: {
|
||||||
emoji_size_limit: config.validation.max_emoji_size,
|
emoji_size_limit: config.validation.emojis.max_bytes,
|
||||||
max_shortcode_characters:
|
max_shortcode_characters:
|
||||||
config.validation.max_emoji_shortcode_size,
|
config.validation.emojis.max_shortcode_characters,
|
||||||
max_description_characters:
|
max_description_characters:
|
||||||
config.validation.max_emoji_description_size,
|
config.validation.emojis.max_description_characters,
|
||||||
},
|
},
|
||||||
polls: {
|
polls: {
|
||||||
max_characters_per_option:
|
max_characters_per_option:
|
||||||
config.validation.max_poll_option_size,
|
config.validation.polls.max_option_characters,
|
||||||
max_expiration: config.validation.max_poll_duration,
|
max_expiration:
|
||||||
max_options: config.validation.max_poll_options,
|
config.validation.polls.max_duration_seconds,
|
||||||
min_expiration: config.validation.min_poll_duration,
|
max_options: config.validation.polls.max_options,
|
||||||
|
min_expiration:
|
||||||
|
config.validation.polls.min_duration_seconds,
|
||||||
},
|
},
|
||||||
translation: {
|
translation: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
registrations: {
|
registrations: {
|
||||||
enabled: config.signups.registration,
|
enabled: config.registration.allow,
|
||||||
approval_required: false,
|
approval_required: config.registration.require_approval,
|
||||||
message: null,
|
message: config.registration.message ?? null,
|
||||||
},
|
},
|
||||||
contact: {
|
contact: {
|
||||||
// TODO: Add contact email
|
email: config.instance.contact.email,
|
||||||
email: "",
|
|
||||||
account: (contactAccount as User)?.toApi(),
|
account: (contactAccount as User)?.toApi(),
|
||||||
},
|
},
|
||||||
rules: config.signups.rules.map((rule, index) => ({
|
rules: config.instance.rules.map((r, index) => ({
|
||||||
id: String(index),
|
id: String(index),
|
||||||
text: rule,
|
text: r.text,
|
||||||
hint: "",
|
hint: r.hint,
|
||||||
})),
|
})),
|
||||||
sso: {
|
sso: {
|
||||||
forced: oidcConfig?.forced ?? false,
|
forced: oidcConfig?.forced ?? false,
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,10 @@ import { and, eq, inArray, isNull, sql } from "drizzle-orm";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
import { Id } from "~/classes/schemas/common";
|
import { Id } from "~/classes/schemas/common";
|
||||||
|
import { zBoolean } from "~/classes/schemas/common.ts";
|
||||||
import { Search as SearchSchema } from "~/classes/schemas/search";
|
import { Search as SearchSchema } from "~/classes/schemas/search";
|
||||||
import { searchManager } from "~/classes/search/search-manager";
|
import { searchManager } from "~/classes/search/search-manager";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
import { zBoolean } from "~/packages/config-manager/config.type";
|
|
||||||
import { ErrorSchema } from "~/types/api";
|
import { ErrorSchema } from "~/types/api";
|
||||||
|
|
||||||
const route = createRoute({
|
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");
|
throw new ApiError(501, "Search is not enabled on this server");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { proxy } from "hono/proxy";
|
import { proxy } from "hono/proxy";
|
||||||
import type { ContentfulStatusCode, StatusCode } from "hono/utils/http-status";
|
import type { ContentfulStatusCode, StatusCode } from "hono/utils/http-status";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
import { ErrorSchema } from "~/types/api";
|
import { ErrorSchema } from "~/types/api";
|
||||||
|
|
||||||
const schemas = {
|
const schemas = {
|
||||||
|
|
@ -56,7 +56,7 @@ export default apiRoute((app) =>
|
||||||
|
|
||||||
const media = await proxy(id, {
|
const media = await proxy(id, {
|
||||||
// @ts-expect-error Proxy is a Bun-specific feature
|
// @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
|
// Check if file extension ends in svg or svg
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { Like, Note, User } from "@versia/kit/db";
|
||||||
import { Likes, Notes } from "@versia/kit/tables";
|
import { Likes, Notes } from "@versia/kit/tables";
|
||||||
import { and, eq, inArray, sql } from "drizzle-orm";
|
import { and, eq, inArray, sql } from "drizzle-orm";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
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";
|
import { ErrorSchema, type KnownEntity } from "~/types/api";
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { Note, User, db } from "@versia/kit/db";
|
||||||
import { Notes } from "@versia/kit/tables";
|
import { Notes } from "@versia/kit/tables";
|
||||||
import { and, eq, inArray } from "drizzle-orm";
|
import { and, eq, inArray } from "drizzle-orm";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
import { ErrorSchema } from "~/types/api";
|
import { ErrorSchema } from "~/types/api";
|
||||||
|
|
||||||
const schemas = {
|
const schemas = {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { apiRoute } from "@/api";
|
import { apiRoute } from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { apiRoute } from "@/api";
|
import { apiRoute } from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Note, User } from "@versia/kit/db";
|
import { Note, User } from "@versia/kit/db";
|
||||||
|
import { config } from "~/config.ts";
|
||||||
import manifest from "~/package.json";
|
import manifest from "~/package.json";
|
||||||
import { config } from "~/packages/config-manager";
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
|
|
@ -65,7 +65,7 @@ export default apiRoute((app) =>
|
||||||
},
|
},
|
||||||
localPosts: noteCount,
|
localPosts: noteCount,
|
||||||
},
|
},
|
||||||
openRegistrations: config.signups.registration,
|
openRegistrations: config.registration.allow,
|
||||||
metadata: {
|
metadata: {
|
||||||
nodeName: config.instance.name,
|
nodeName: config.instance.name,
|
||||||
nodeDescription: config.instance.description,
|
nodeDescription: config.instance.description,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { apiRoute } from "@/api";
|
import { apiRoute } from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { apiRoute } from "@/api";
|
import { apiRoute } from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import { InstanceMetadata as InstanceMetadataSchema } from "@versia/federation/s
|
||||||
import { User } from "@versia/kit/db";
|
import { User } from "@versia/kit/db";
|
||||||
import { Users } from "@versia/kit/tables";
|
import { Users } from "@versia/kit/tables";
|
||||||
import { asc } from "drizzle-orm";
|
import { asc } from "drizzle-orm";
|
||||||
|
import { config } from "~/config.ts";
|
||||||
import pkg from "~/package.json";
|
import pkg from "~/package.json";
|
||||||
import { config } from "~/packages/config-manager";
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
|
|
@ -29,6 +29,10 @@ export default apiRoute((app) =>
|
||||||
// Get date of first user creation
|
// Get date of first user creation
|
||||||
const firstUser = await User.fromSql(undefined, asc(Users.createdAt));
|
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(
|
return context.json(
|
||||||
{
|
{
|
||||||
type: "InstanceMetadata" as const,
|
type: "InstanceMetadata" as const,
|
||||||
|
|
@ -43,18 +47,18 @@ export default apiRoute((app) =>
|
||||||
name: config.instance.name,
|
name: config.instance.name,
|
||||||
description: config.instance.description,
|
description: config.instance.description,
|
||||||
public_key: {
|
public_key: {
|
||||||
key: config.instance.keys.public,
|
key: publicKey,
|
||||||
algorithm: "ed25519" as const,
|
algorithm: "ed25519" as const,
|
||||||
},
|
},
|
||||||
software: {
|
software: {
|
||||||
name: "Versia Server",
|
name: "Versia Server",
|
||||||
version: pkg.version,
|
version: pkg.version,
|
||||||
},
|
},
|
||||||
banner: config.instance.banner
|
banner: config.instance.branding.banner
|
||||||
? urlToContentFormat(config.instance.banner)
|
? urlToContentFormat(config.instance.branding.banner)
|
||||||
: undefined,
|
: undefined,
|
||||||
logo: config.instance.logo
|
logo: config.instance.branding.logo
|
||||||
? urlToContentFormat(config.instance.logo)
|
? urlToContentFormat(config.instance.branding.logo)
|
||||||
: undefined,
|
: undefined,
|
||||||
shared_inbox: new URL(
|
shared_inbox: new URL(
|
||||||
"/inbox",
|
"/inbox",
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { User } from "@versia/kit/db";
|
||||||
import { Users } from "@versia/kit/tables";
|
import { Users } from "@versia/kit/tables";
|
||||||
import { and, eq, isNull } from "drizzle-orm";
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
import { ErrorSchema } from "~/types/api";
|
import { ErrorSchema } from "~/types/api";
|
||||||
|
|
||||||
const schemas = {
|
const schemas = {
|
||||||
|
|
@ -90,7 +90,7 @@ export default apiRoute((app) =>
|
||||||
|
|
||||||
let activityPubUrl = "";
|
let activityPubUrl = "";
|
||||||
|
|
||||||
if (config.federation.bridge.enabled) {
|
if (config.federation.bridge) {
|
||||||
const manager = await User.getFederationRequester();
|
const manager = await User.getFederationRequester();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -98,7 +98,7 @@ export default apiRoute((app) =>
|
||||||
user.data.username,
|
user.data.username,
|
||||||
config.http.base_url.host,
|
config.http.base_url.host,
|
||||||
"application/activity+json",
|
"application/activity+json",
|
||||||
config.federation.bridge.url?.toString(),
|
config.federation.bridge.url.origin,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const error = e as ResponseError;
|
const error = e as ResponseError;
|
||||||
|
|
@ -136,7 +136,7 @@ export default apiRoute((app) =>
|
||||||
type:
|
type:
|
||||||
user.avatar?.getPreferredMimeType() ??
|
user.avatar?.getPreferredMimeType() ??
|
||||||
"image/svg+xml",
|
"image/svg+xml",
|
||||||
href: user.getAvatarUrl(config),
|
href: user.getAvatarUrl(),
|
||||||
},
|
},
|
||||||
].filter(Boolean) as {
|
].filter(Boolean) as {
|
||||||
rel: string;
|
rel: string;
|
||||||
|
|
|
||||||
16
app.ts
16
app.ts
|
|
@ -12,12 +12,11 @@ import { cors } from "hono/cors";
|
||||||
import { createMiddleware } from "hono/factory";
|
import { createMiddleware } from "hono/factory";
|
||||||
import { prettyJSON } from "hono/pretty-json";
|
import { prettyJSON } from "hono/pretty-json";
|
||||||
import { secureHeaders } from "hono/secure-headers";
|
import { secureHeaders } from "hono/secure-headers";
|
||||||
|
import { config } from "~/config.ts";
|
||||||
import pkg from "~/package.json" with { type: "application/json" };
|
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 { ApiError } from "./classes/errors/api-error.ts";
|
||||||
import { PluginLoader } from "./classes/plugin/loader.ts";
|
import { PluginLoader } from "./classes/plugin/loader.ts";
|
||||||
import { agentBans } from "./middlewares/agent-bans.ts";
|
import { agentBans } from "./middlewares/agent-bans.ts";
|
||||||
import { bait } from "./middlewares/bait.ts";
|
|
||||||
import { boundaryCheck } from "./middlewares/boundary-check.ts";
|
import { boundaryCheck } from "./middlewares/boundary-check.ts";
|
||||||
import { ipBans } from "./middlewares/ip-bans.ts";
|
import { ipBans } from "./middlewares/ip-bans.ts";
|
||||||
import { logger } from "./middlewares/logger.ts";
|
import { logger } from "./middlewares/logger.ts";
|
||||||
|
|
@ -33,21 +32,8 @@ export const appFactory = async (): Promise<OpenAPIHono<HonoEnv>> => {
|
||||||
defaultHook: handleZodError,
|
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(ipBans);
|
||||||
app.use(agentBans);
|
app.use(agentBans);
|
||||||
app.use(bait);
|
|
||||||
app.use(logger);
|
app.use(logger);
|
||||||
app.use(boundaryCheck);
|
app.use(boundaryCheck);
|
||||||
app.use(
|
app.use(
|
||||||
|
|
|
||||||
740
classes/config/schema.ts
Normal file
740
classes/config/schema.ts
Normal file
|
|
@ -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",
|
||||||
|
);
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { zodToJsonSchema } from "zod-to-json-schema";
|
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`);
|
console.write(`${JSON.stringify(jsonSchema, null, 4)}\n`);
|
||||||
|
|
@ -12,7 +12,7 @@ import {
|
||||||
eq,
|
eq,
|
||||||
inArray,
|
inArray,
|
||||||
} from "drizzle-orm";
|
} from "drizzle-orm";
|
||||||
import { config } from "~/packages/config-manager/index.ts";
|
import { config } from "~/config.ts";
|
||||||
import { ApiError } from "../errors/api-error.ts";
|
import { ApiError } from "../errors/api-error.ts";
|
||||||
import { BaseInterface } from "./base.ts";
|
import { BaseInterface } from "./base.ts";
|
||||||
import { User } from "./user.ts";
|
import { User } from "./user.ts";
|
||||||
|
|
@ -147,7 +147,7 @@ export class Instance extends BaseInterface<typeof Instances> {
|
||||||
const { ok, raw, data } = await requester
|
const { ok, raw, data } = await requester
|
||||||
.get(wellKnownUrl, {
|
.get(wellKnownUrl, {
|
||||||
// @ts-expect-error Bun extension
|
// @ts-expect-error Bun extension
|
||||||
proxy: config.http.proxy.address,
|
proxy: config.http.proxy_address,
|
||||||
})
|
})
|
||||||
.catch((e) => ({
|
.catch((e) => ({
|
||||||
...(e as ResponseError).response,
|
...(e as ResponseError).response,
|
||||||
|
|
@ -204,7 +204,7 @@ export class Instance extends BaseInterface<typeof Instances> {
|
||||||
links: { rel: string; href: string }[];
|
links: { rel: string; href: string }[];
|
||||||
}>(wellKnownUrl, {
|
}>(wellKnownUrl, {
|
||||||
// @ts-expect-error Bun extension
|
// @ts-expect-error Bun extension
|
||||||
proxy: config.http.proxy.address,
|
proxy: config.http.proxy_address,
|
||||||
})
|
})
|
||||||
.catch((e) => ({
|
.catch((e) => ({
|
||||||
...(
|
...(
|
||||||
|
|
@ -256,7 +256,7 @@ export class Instance extends BaseInterface<typeof Instances> {
|
||||||
software: { version: string };
|
software: { version: string };
|
||||||
}>(metadataUrl.href, {
|
}>(metadataUrl.href, {
|
||||||
// @ts-expect-error Bun extension
|
// @ts-expect-error Bun extension
|
||||||
proxy: config.http.proxy.address,
|
proxy: config.http.proxy_address,
|
||||||
})
|
})
|
||||||
.catch((e) => ({
|
.catch((e) => ({
|
||||||
...(
|
...(
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import {
|
||||||
eq,
|
eq,
|
||||||
inArray,
|
inArray,
|
||||||
} from "drizzle-orm";
|
} from "drizzle-orm";
|
||||||
import { config } from "~/packages/config-manager/index.ts";
|
import { config } from "~/config.ts";
|
||||||
import { BaseInterface } from "./base.ts";
|
import { BaseInterface } from "./base.ts";
|
||||||
import { Note } from "./note.ts";
|
import { Note } from "./note.ts";
|
||||||
import { User } from "./user.ts";
|
import { User } from "./user.ts";
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,9 @@ import {
|
||||||
inArray,
|
inArray,
|
||||||
} from "drizzle-orm";
|
} from "drizzle-orm";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
|
import { MediaBackendType } from "~/classes/config/schema.ts";
|
||||||
import type { Attachment as AttachmentSchema } from "~/classes/schemas/attachment.ts";
|
import type { Attachment as AttachmentSchema } from "~/classes/schemas/attachment.ts";
|
||||||
import { MediaBackendType } from "~/packages/config-manager/config.type";
|
import { config } from "~/config.ts";
|
||||||
import { config } from "~/packages/config-manager/index.ts";
|
|
||||||
import { ApiError } from "../errors/api-error.ts";
|
import { ApiError } from "../errors/api-error.ts";
|
||||||
import { getMediaHash } from "../media/media-hasher.ts";
|
import { getMediaHash } from "../media/media-hasher.ts";
|
||||||
import { MediaJobType, mediaQueue } from "../queues/media.ts";
|
import { MediaJobType, mediaQueue } from "../queues/media.ts";
|
||||||
|
|
@ -135,11 +135,7 @@ export class Media extends BaseInterface<typeof Medias> {
|
||||||
|
|
||||||
switch (config.media.backend) {
|
switch (config.media.backend) {
|
||||||
case MediaBackendType.Local: {
|
case MediaBackendType.Local: {
|
||||||
const path = join(
|
const path = join(config.media.uploads_path, hash, fileName);
|
||||||
config.media.local_uploads_folder,
|
|
||||||
hash,
|
|
||||||
fileName,
|
|
||||||
);
|
|
||||||
|
|
||||||
await write(path, file);
|
await write(path, file);
|
||||||
|
|
||||||
|
|
@ -154,7 +150,7 @@ export class Media extends BaseInterface<typeof Medias> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = new S3Client({
|
const client = new S3Client({
|
||||||
endpoint: config.s3.endpoint,
|
endpoint: config.s3.endpoint.origin,
|
||||||
region: config.s3.region,
|
region: config.s3.region,
|
||||||
bucket: config.s3.bucket_name,
|
bucket: config.s3.bucket_name,
|
||||||
accessKeyId: config.s3.access_key,
|
accessKeyId: config.s3.access_key,
|
||||||
|
|
@ -260,21 +256,21 @@ export class Media extends BaseInterface<typeof Medias> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static checkFile(file: File): void {
|
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(
|
throw new ApiError(
|
||||||
413,
|
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 (
|
if (
|
||||||
config.validation.enforce_mime_types &&
|
config.validation.media.allowed_mime_types.length > 0 &&
|
||||||
!config.validation.allowed_mime_types.includes(file.type)
|
!config.validation.media.allowed_mime_types.includes(file.type)
|
||||||
) {
|
) {
|
||||||
throw new ApiError(
|
throw new ApiError(
|
||||||
415,
|
415,
|
||||||
`File type ${file.type} is not allowed`,
|
`File type ${file.type} is not allowed`,
|
||||||
`Allowed types: ${config.validation.allowed_mime_types.join(", ")}`,
|
`Allowed types: ${config.validation.media.allowed_mime_types.join(", ")}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ import {
|
||||||
parseTextMentions,
|
parseTextMentions,
|
||||||
} from "~/classes/functions/status";
|
} from "~/classes/functions/status";
|
||||||
import type { Status as StatusSchema } from "~/classes/schemas/status.ts";
|
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 { DeliveryJobType, deliveryQueue } from "../queues/delivery.ts";
|
||||||
import type { Status } from "../schemas/status.ts";
|
import type { Status } from "../schemas/status.ts";
|
||||||
import { Application } from "./application.ts";
|
import { Application } from "./application.ts";
|
||||||
|
|
@ -594,7 +594,7 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
||||||
|
|
||||||
const { data } = await requester.get(uri, {
|
const { data } = await requester.get(uri, {
|
||||||
// @ts-expect-error Bun extension
|
// @ts-expect-error Bun extension
|
||||||
proxy: config.http.proxy.address,
|
proxy: config.http.proxy_address,
|
||||||
});
|
});
|
||||||
|
|
||||||
const note = await new EntityValidator().Note(data);
|
const note = await new EntityValidator().Note(data);
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
eq,
|
eq,
|
||||||
inArray,
|
inArray,
|
||||||
} from "drizzle-orm";
|
} from "drizzle-orm";
|
||||||
import { config } from "~/packages/config-manager/index.ts";
|
import { config } from "~/config.ts";
|
||||||
import { BaseInterface } from "./base.ts";
|
import { BaseInterface } from "./base.ts";
|
||||||
|
|
||||||
type ReactionType = InferSelectModel<typeof Reactions> & {
|
type ReactionType = InferSelectModel<typeof Reactions> & {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import {
|
||||||
eq,
|
eq,
|
||||||
inArray,
|
inArray,
|
||||||
} from "drizzle-orm";
|
} from "drizzle-orm";
|
||||||
import { config } from "~/packages/config-manager/index.ts";
|
import { config } from "~/config.ts";
|
||||||
import { BaseInterface } from "./base.ts";
|
import { BaseInterface } from "./base.ts";
|
||||||
|
|
||||||
type RoleType = InferSelectModel<typeof Roles>;
|
type RoleType = InferSelectModel<typeof Roles>;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Notes, Notifications, Users } from "@versia/kit/tables";
|
import { Notes, Notifications, Users } from "@versia/kit/tables";
|
||||||
import { type SQL, gt } from "drizzle-orm";
|
import { type SQL, gt } from "drizzle-orm";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
import { Note } from "./note.ts";
|
import { Note } from "./note.ts";
|
||||||
import { Notification } from "./notification.ts";
|
import { Notification } from "./notification.ts";
|
||||||
import { User } from "./user.ts";
|
import { User } from "./user.ts";
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ import {
|
||||||
import { htmlToText } from "html-to-text";
|
import { htmlToText } from "html-to-text";
|
||||||
import { findManyUsers } from "~/classes/functions/user";
|
import { findManyUsers } from "~/classes/functions/user";
|
||||||
import { searchManager } from "~/classes/search/search-manager";
|
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 type { KnownEntity } from "~/types/api.ts";
|
||||||
import { DeliveryJobType, deliveryQueue } from "../queues/delivery.ts";
|
import { DeliveryJobType, deliveryQueue } from "../queues/delivery.ts";
|
||||||
import { PushJobType, pushQueue } from "../queues/push.ts";
|
import { PushJobType, pushQueue } from "../queues/push.ts";
|
||||||
|
|
@ -522,7 +522,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also do push notifications
|
// Also do push notifications
|
||||||
if (config.notifications.push.enabled) {
|
if (config.notifications.push) {
|
||||||
await this.notifyPush(notification.id, type, relatedUser, note);
|
await this.notifyPush(notification.id, type, relatedUser, note);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -603,7 +603,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instance.data.protocol === "activitypub") {
|
if (instance.data.protocol === "activitypub") {
|
||||||
if (!config.federation.bridge.enabled) {
|
if (!config.federation.bridge) {
|
||||||
throw new Error("ActivityPub bridge is not enabled");
|
throw new Error("ActivityPub bridge is not enabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -627,7 +627,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
const requester = await User.getFederationRequester();
|
const requester = await User.getFederationRequester();
|
||||||
const output = await requester.get<Partial<VersiaUser>>(uri, {
|
const output = await requester.get<Partial<VersiaUser>>(uri, {
|
||||||
// @ts-expect-error Bun extension
|
// @ts-expect-error Bun extension
|
||||||
proxy: config.http.proxy.address,
|
proxy: config.http.proxy_address,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: json } = output;
|
const { data: json } = output;
|
||||||
|
|
@ -815,10 +815,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the user's avatar in raw URL format
|
* Get the user's avatar in raw URL format
|
||||||
* @param config The config to use
|
|
||||||
* @returns The raw URL for the user's avatar
|
* @returns The raw URL for the user's avatar
|
||||||
*/
|
*/
|
||||||
public getAvatarUrl(config: Config): URL {
|
public getAvatarUrl(): URL {
|
||||||
if (!this.avatar) {
|
if (!this.avatar) {
|
||||||
return (
|
return (
|
||||||
config.defaults.avatar ||
|
config.defaults.avatar ||
|
||||||
|
|
@ -912,10 +911,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the user's header in raw URL format
|
* Get the user's header in raw URL format
|
||||||
* @param config The config to use
|
|
||||||
* @returns The raw URL for the user's header
|
* @returns The raw URL for the user's header
|
||||||
*/
|
*/
|
||||||
public getHeaderUrl(config: Config): URL | null {
|
public getHeaderUrl(): URL | null {
|
||||||
if (!this.header) {
|
if (!this.header) {
|
||||||
return config.defaults.header ?? null;
|
return config.defaults.header ?? null;
|
||||||
}
|
}
|
||||||
|
|
@ -996,7 +994,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
JSON.stringify(entity),
|
JSON.stringify(entity),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (config.debug.federation) {
|
if (config.debug?.federation) {
|
||||||
const logger = getLogger("federation");
|
const logger = getLogger("federation");
|
||||||
|
|
||||||
// Log public key
|
// Log public key
|
||||||
|
|
@ -1014,8 +1012,8 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
*
|
*
|
||||||
* @returns The requester
|
* @returns The requester
|
||||||
*/
|
*/
|
||||||
public static async getFederationRequester(): Promise<FederationRequester> {
|
public static getFederationRequester(): FederationRequester {
|
||||||
const signatureConstructor = await SignatureConstructor.fromStringKey(
|
const signatureConstructor = new SignatureConstructor(
|
||||||
config.instance.keys.private,
|
config.instance.keys.private,
|
||||||
config.http.base_url,
|
config.http.base_url,
|
||||||
);
|
);
|
||||||
|
|
@ -1087,7 +1085,7 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
try {
|
try {
|
||||||
await new FederationRequester().post(inbox, entity, {
|
await new FederationRequester().post(inbox, entity, {
|
||||||
// @ts-expect-error Bun extension
|
// @ts-expect-error Bun extension
|
||||||
proxy: config.http.proxy.address,
|
proxy: config.http.proxy_address,
|
||||||
headers: {
|
headers: {
|
||||||
...headers.toJSON(),
|
...headers.toJSON(),
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
|
@ -1117,9 +1115,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
url:
|
url:
|
||||||
user.uri ||
|
user.uri ||
|
||||||
new URL(`/@${user.username}`, config.http.base_url).toString(),
|
new URL(`/@${user.username}`, config.http.base_url).toString(),
|
||||||
avatar: proxyUrl(this.getAvatarUrl(config)).toString(),
|
avatar: proxyUrl(this.getAvatarUrl()).toString(),
|
||||||
header: this.getHeaderUrl(config)
|
header: this.getHeaderUrl()
|
||||||
? proxyUrl(this.getHeaderUrl(config) as URL).toString()
|
? proxyUrl(this.getHeaderUrl() as URL).toString()
|
||||||
: "",
|
: "",
|
||||||
locked: user.isLocked,
|
locked: user.isLocked,
|
||||||
created_at: new Date(user.createdAt).toISOString(),
|
created_at: new Date(user.createdAt).toISOString(),
|
||||||
|
|
@ -1135,9 +1133,9 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
bot: user.isBot,
|
bot: user.isBot,
|
||||||
source: isOwnAccount ? user.source : undefined,
|
source: isOwnAccount ? user.source : undefined,
|
||||||
// TODO: Add static avatar and header
|
// TODO: Add static avatar and header
|
||||||
avatar_static: proxyUrl(this.getAvatarUrl(config)).toString(),
|
avatar_static: proxyUrl(this.getAvatarUrl()).toString(),
|
||||||
header_static: this.getHeaderUrl(config)
|
header_static: this.getHeaderUrl()
|
||||||
? proxyUrl(this.getHeaderUrl(config) as URL).toString()
|
? proxyUrl(this.getHeaderUrl() as URL).toString()
|
||||||
: "",
|
: "",
|
||||||
acct: this.getAcct(),
|
acct: this.getAcct(),
|
||||||
// TODO: Add these fields
|
// TODO: Add these fields
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import {
|
||||||
import MarkdownIt from "markdown-it";
|
import MarkdownIt from "markdown-it";
|
||||||
import markdownItContainer from "markdown-it-container";
|
import markdownItContainer from "markdown-it-container";
|
||||||
import markdownItTocDoneRight from "markdown-it-toc-done-right";
|
import markdownItTocDoneRight from "markdown-it-toc-done-right";
|
||||||
import { config } from "~/packages/config-manager/index.ts";
|
import { config } from "~/config.ts";
|
||||||
import {
|
import {
|
||||||
transformOutputToUserWithRelations,
|
transformOutputToUserWithRelations,
|
||||||
userExtrasTemplate,
|
userExtrasTemplate,
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,10 @@ import {
|
||||||
User,
|
User,
|
||||||
} from "@versia/kit/db";
|
} from "@versia/kit/db";
|
||||||
import type { SocketAddress } from "bun";
|
import type { SocketAddress } from "bun";
|
||||||
|
import type { z } from "zod";
|
||||||
import { ValidationError } from "zod-validation-error";
|
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";
|
import { InboxProcessor } from "./processor.ts";
|
||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
|
|
@ -58,7 +60,7 @@ mock.module("@versia/federation", () => ({
|
||||||
RequestParserHandler: jest.fn(),
|
RequestParserHandler: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
mock.module("~/packages/config-manager/index.ts", () => ({
|
mock.module("~/config.ts", () => ({
|
||||||
config: {
|
config: {
|
||||||
debug: {
|
debug: {
|
||||||
federation: false,
|
federation: false,
|
||||||
|
|
@ -172,9 +174,13 @@ describe("InboxProcessor", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("returns false for valid bridge request", () => {
|
test("returns false for valid bridge request", () => {
|
||||||
config.federation.bridge.enabled = true;
|
config.federation.bridge = {
|
||||||
config.federation.bridge.token = "valid-token";
|
token: "valid-token",
|
||||||
config.federation.bridge.allowed_ips = ["127.0.0.1"];
|
allowed_ips: ["127.0.0.1"],
|
||||||
|
url: new URL("https://test.com"),
|
||||||
|
software: "versia-ap",
|
||||||
|
};
|
||||||
|
|
||||||
mockHeaders.authorization = "Bearer valid-token";
|
mockHeaders.authorization = "Bearer valid-token";
|
||||||
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
||||||
|
|
@ -183,7 +189,9 @@ describe("InboxProcessor", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("returns error response for invalid token", () => {
|
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";
|
mockHeaders.authorization = "Bearer invalid-token";
|
||||||
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import { eq } from "drizzle-orm";
|
||||||
import type { StatusCode } from "hono/utils/http-status";
|
import type { StatusCode } from "hono/utils/http-status";
|
||||||
import { matches } from "ip-matching";
|
import { matches } from "ip-matching";
|
||||||
import { type ValidationError, isValidationError } from "zod-validation-error";
|
import { type ValidationError, isValidationError } from "zod-validation-error";
|
||||||
import { config } from "~/packages/config-manager/index.ts";
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
type ResponseBody = {
|
type ResponseBody = {
|
||||||
message?: string;
|
message?: string;
|
||||||
|
|
@ -98,7 +98,7 @@ export class InboxProcessor {
|
||||||
throw new Error("Sender is not defined");
|
throw new Error("Sender is not defined");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.debug.federation) {
|
if (config.debug?.federation) {
|
||||||
this.logger.debug`Sender public key: ${chalk.gray(
|
this.logger.debug`Sender public key: ${chalk.gray(
|
||||||
this.sender.key,
|
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.
|
* @returns {boolean | ResponseBody} - Whether to skip signature checks. May include a response body if there are errors.
|
||||||
*/
|
*/
|
||||||
private shouldCheckSignature(): boolean | ResponseBody {
|
private shouldCheckSignature(): boolean | ResponseBody {
|
||||||
if (config.federation.bridge.enabled) {
|
if (config.federation.bridge) {
|
||||||
const token = this.headers.authorization?.split("Bearer ")[1];
|
const token = this.headers.authorization?.split("Bearer ")[1];
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
|
|
@ -158,6 +158,14 @@ export class InboxProcessor {
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
private isRequestFromBridge(token: string): boolean | ResponseBody {
|
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) {
|
if (token !== config.federation.bridge.token) {
|
||||||
return {
|
return {
|
||||||
message:
|
message:
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { beforeEach, describe, expect, it, mock } from "bun:test";
|
import { beforeEach, describe, expect, it, mock } from "bun:test";
|
||||||
import sharp from "sharp";
|
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";
|
import { convertImage } from "./image-conversion.ts";
|
||||||
|
|
||||||
describe("ImageConversionPreprocessor", () => {
|
describe("ImageConversionPreprocessor", () => {
|
||||||
let mockConfig: Config;
|
let mockConfig: typeof config;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockConfig = {
|
mockConfig = {
|
||||||
|
|
@ -15,9 +15,9 @@ describe("ImageConversionPreprocessor", () => {
|
||||||
convert_vector: false,
|
convert_vector: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as Config;
|
} as unknown as typeof config;
|
||||||
|
|
||||||
mock.module("~/packages/config-manager/index.ts", () => ({
|
mock.module("~/config.ts", () => ({
|
||||||
config: mockConfig,
|
config: mockConfig,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
@ -59,7 +59,7 @@ describe("ImageConversionPreprocessor", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should convert SVG when convert_vector is true", async () => {
|
it("should convert SVG when convert_vector is true", async () => {
|
||||||
mockConfig.media.conversion.convert_vector = true;
|
mockConfig.media.conversion.convert_vectors = true;
|
||||||
|
|
||||||
const svgContent =
|
const svgContent =
|
||||||
'<svg xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100" fill="red"/></svg>';
|
'<svg xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100" fill="red"/></svg>';
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
import { config } from "~/packages/config-manager/index.ts";
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Supported input media formats.
|
* Supported input media formats.
|
||||||
|
|
@ -39,7 +39,7 @@ const supportedOutputFormats = [
|
||||||
const isConvertible = (file: File): boolean => {
|
const isConvertible = (file: File): boolean => {
|
||||||
if (
|
if (
|
||||||
file.type === "image/svg+xml" &&
|
file.type === "image/svg+xml" &&
|
||||||
!config.media.conversion.convert_vector
|
!config.media.conversion.convert_vectors
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import chalk from "chalk";
|
||||||
import { parseJSON5, parseJSONC } from "confbox";
|
import { parseJSON5, parseJSONC } from "confbox";
|
||||||
import type { ZodTypeAny } from "zod";
|
import type { ZodTypeAny } from "zod";
|
||||||
import { type ValidationError, fromZodError } from "zod-validation-error";
|
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 { Plugin } from "~/packages/plugin-kit/plugin";
|
||||||
import { type Manifest, manifestSchema } from "~/packages/plugin-kit/schema";
|
import { type Manifest, manifestSchema } from "~/packages/plugin-kit/schema";
|
||||||
import type { HonoEnv } from "~/types/api";
|
import type { HonoEnv } from "~/types/api";
|
||||||
|
|
@ -230,10 +230,13 @@ export class PluginLoader {
|
||||||
config.plugins?.config?.[data.manifest.name],
|
config.plugins?.config?.[data.manifest.name],
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} 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(
|
logger.fatal`Put your configuration at ${chalk.blueBright(
|
||||||
"plugins.config.<plugin-name>",
|
"plugins.config.<plugin-name>",
|
||||||
)}`;
|
)}`;
|
||||||
|
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);
|
await Bun.sleep(Number.POSITIVE_INFINITY);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { userAddressValidator } from "@/api.ts";
|
import { userAddressValidator } from "@/api.ts";
|
||||||
import { z } from "@hono/zod-openapi";
|
import { z } from "@hono/zod-openapi";
|
||||||
import type { Account as ApiAccount } from "@versia/client/types";
|
import type { Account as ApiAccount } from "@versia/client/types";
|
||||||
import { config } from "~/packages/config-manager";
|
import { zBoolean } from "~/classes/schemas/common.ts";
|
||||||
import { zBoolean } from "~/packages/config-manager/config.type";
|
import { config } from "~/config.ts";
|
||||||
import { iso631 } from "./common.ts";
|
import { iso631 } from "./common.ts";
|
||||||
import { CustomEmoji } from "./emoji.ts";
|
import { CustomEmoji } from "./emoji.ts";
|
||||||
import { Role } from "./versia.ts";
|
import { Role } from "./versia.ts";
|
||||||
|
|
@ -12,7 +12,7 @@ export const Field = z.object({
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.min(1)
|
.min(1)
|
||||||
.max(config.validation.max_field_name_size)
|
.max(config.validation.accounts.max_field_name_characters)
|
||||||
.openapi({
|
.openapi({
|
||||||
description: "The key of a given field’s key-value pair.",
|
description: "The key of a given field’s key-value pair.",
|
||||||
example: "Freak level",
|
example: "Freak level",
|
||||||
|
|
@ -24,7 +24,7 @@ export const Field = z.object({
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.min(1)
|
.min(1)
|
||||||
.max(config.validation.max_field_value_size)
|
.max(config.validation.accounts.max_field_value_characters)
|
||||||
.openapi({
|
.openapi({
|
||||||
description: "The value associated with the name key.",
|
description: "The value associated with the name key.",
|
||||||
example: "<p>High</p>",
|
example: "<p>High</p>",
|
||||||
|
|
@ -87,9 +87,12 @@ export const Source = z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.min(0)
|
.min(0)
|
||||||
.max(config.validation.max_bio_size)
|
.max(config.validation.accounts.max_bio_characters)
|
||||||
.refine(
|
.refine(
|
||||||
(s) => !config.filters.bio.some((filter) => s.match(filter)),
|
(s) =>
|
||||||
|
!config.validation.filters.bio.some((filter) =>
|
||||||
|
filter.test(s),
|
||||||
|
),
|
||||||
"Bio contains blocked words",
|
"Bio contains blocked words",
|
||||||
)
|
)
|
||||||
.openapi({
|
.openapi({
|
||||||
|
|
@ -99,9 +102,12 @@ export const Source = z
|
||||||
url: "https://docs.joinmastodon.org/entities/Account/#source-note",
|
url: "https://docs.joinmastodon.org/entities/Account/#source-note",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
fields: z.array(Field).max(config.validation.max_field_count).openapi({
|
fields: z
|
||||||
description: "Metadata about the account.",
|
.array(Field)
|
||||||
}),
|
.max(config.validation.accounts.max_field_count)
|
||||||
|
.openapi({
|
||||||
|
description: "Metadata about the account.",
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
.openapi({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
|
|
@ -126,15 +132,25 @@ export const Account = z.object({
|
||||||
.string()
|
.string()
|
||||||
.min(3)
|
.min(3)
|
||||||
.trim()
|
.trim()
|
||||||
.max(config.validation.max_username_size)
|
.max(config.validation.accounts.max_username_characters)
|
||||||
.regex(
|
.regex(
|
||||||
/^[a-z0-9_-]+$/,
|
/^[a-z0-9_-]+$/,
|
||||||
"Username can only contain letters, numbers, underscores and hyphens",
|
"Username can only contain letters, numbers, underscores and hyphens",
|
||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
(s) => !config.filters.username.some((filter) => s.match(filter)),
|
(s) =>
|
||||||
|
!config.validation.filters.username.some((filter) =>
|
||||||
|
filter.test(s),
|
||||||
|
),
|
||||||
"Username contains blocked words",
|
"Username contains blocked words",
|
||||||
)
|
)
|
||||||
|
.refine(
|
||||||
|
(s) =>
|
||||||
|
!config.validation.accounts.disallowed_usernames.some((u) =>
|
||||||
|
u.test(s),
|
||||||
|
),
|
||||||
|
"Username is disallowed",
|
||||||
|
)
|
||||||
.openapi({
|
.openapi({
|
||||||
description: "The username of the account, not including domain.",
|
description: "The username of the account, not including domain.",
|
||||||
example: "lexi",
|
example: "lexi",
|
||||||
|
|
@ -169,10 +185,12 @@ export const Account = z.object({
|
||||||
.string()
|
.string()
|
||||||
.min(3)
|
.min(3)
|
||||||
.trim()
|
.trim()
|
||||||
.max(config.validation.max_displayname_size)
|
.max(config.validation.accounts.max_displayname_characters)
|
||||||
.refine(
|
.refine(
|
||||||
(s) =>
|
(s) =>
|
||||||
!config.filters.displayname.some((filter) => s.match(filter)),
|
!config.validation.filters.displayname.some((filter) =>
|
||||||
|
filter.test(s),
|
||||||
|
),
|
||||||
"Display name contains blocked words",
|
"Display name contains blocked words",
|
||||||
)
|
)
|
||||||
.openapi({
|
.openapi({
|
||||||
|
|
@ -185,10 +203,11 @@ export const Account = z.object({
|
||||||
note: z
|
note: z
|
||||||
.string()
|
.string()
|
||||||
.min(0)
|
.min(0)
|
||||||
.max(config.validation.max_bio_size)
|
.max(config.validation.accounts.max_bio_characters)
|
||||||
.trim()
|
.trim()
|
||||||
.refine(
|
.refine(
|
||||||
(s) => !config.filters.bio.some((filter) => s.match(filter)),
|
(s) =>
|
||||||
|
!config.validation.filters.bio.some((filter) => filter.test(s)),
|
||||||
"Bio contains blocked words",
|
"Bio contains blocked words",
|
||||||
)
|
)
|
||||||
.openapi({
|
.openapi({
|
||||||
|
|
@ -255,7 +274,7 @@ export const Account = z.object({
|
||||||
}),
|
}),
|
||||||
fields: z
|
fields: z
|
||||||
.array(Field)
|
.array(Field)
|
||||||
.max(config.validation.max_field_count)
|
.max(config.validation.accounts.max_field_count)
|
||||||
.openapi({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
"Additional metadata attached to a profile as name-value pairs.",
|
"Additional metadata attached to a profile as name-value pairs.",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { z } from "@hono/zod-openapi";
|
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 { Id } from "./common.ts";
|
||||||
|
|
||||||
export const Attachment = z
|
export const Attachment = z
|
||||||
|
|
@ -54,7 +54,7 @@ export const Attachment = z
|
||||||
description: z
|
description: z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.max(config.validation.max_media_description_size)
|
.max(config.validation.media.max_description_characters)
|
||||||
.nullable()
|
.nullable()
|
||||||
.openapi({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,8 @@ import ISO6391 from "iso-639-1";
|
||||||
export const Id = z.string().uuid();
|
export const Id = z.string().uuid();
|
||||||
|
|
||||||
export const iso631 = z.enum(ISO6391.getAllCodes() as [string, ...string[]]);
|
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());
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { emojiValidator } from "@/api.ts";
|
import { emojiValidator } from "@/api.ts";
|
||||||
import { z } from "@hono/zod-openapi";
|
import { z } from "@hono/zod-openapi";
|
||||||
import { zBoolean } from "~/packages/config-manager/config.type";
|
import { zBoolean } from "~/classes/schemas/common.ts";
|
||||||
import { config } from "~/packages/config-manager/index.ts";
|
import { config } from "~/config.ts";
|
||||||
import { Id } from "./common.ts";
|
import { Id } from "./common.ts";
|
||||||
|
|
||||||
export const CustomEmoji = z
|
export const CustomEmoji = z
|
||||||
|
|
@ -15,7 +15,7 @@ export const CustomEmoji = z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.min(1)
|
.min(1)
|
||||||
.max(config.validation.max_emoji_shortcode_size)
|
.max(config.validation.emojis.max_shortcode_characters)
|
||||||
.regex(
|
.regex(
|
||||||
emojiValidator,
|
emojiValidator,
|
||||||
"Shortcode must only contain letters (any case), numbers, dashes or underscores.",
|
"Shortcode must only contain letters (any case), numbers, dashes or underscores.",
|
||||||
|
|
@ -77,7 +77,7 @@ export const CustomEmoji = z
|
||||||
/* Versia Server API extension */
|
/* Versia Server API extension */
|
||||||
description: z
|
description: z
|
||||||
.string()
|
.string()
|
||||||
.max(config.validation.max_emoji_description_size)
|
.max(config.validation.emojis.max_description_characters)
|
||||||
.nullable()
|
.nullable()
|
||||||
.openapi({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { z } from "@hono/zod-openapi";
|
import { z } from "@hono/zod-openapi";
|
||||||
import { zBoolean } from "~/packages/config-manager/config.type.ts";
|
import { Id, zBoolean } from "./common.ts";
|
||||||
import { Id } from "./common.ts";
|
|
||||||
|
|
||||||
export const FilterStatus = z
|
export const FilterStatus = z
|
||||||
.object({
|
.object({
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { z } from "@hono/zod-openapi";
|
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 { Id } from "./common.ts";
|
||||||
import { CustomEmoji } from "./emoji.ts";
|
import { CustomEmoji } from "./emoji.ts";
|
||||||
|
|
||||||
|
|
@ -9,7 +9,7 @@ export const PollOption = z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.min(1)
|
.min(1)
|
||||||
.max(config.validation.max_poll_option_size)
|
.max(config.validation.polls.max_option_characters)
|
||||||
.openapi({
|
.openapi({
|
||||||
description: "The text value of the poll option.",
|
description: "The text value of the poll option.",
|
||||||
example: "yes",
|
example: "yes",
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import { z } from "@hono/zod-openapi";
|
import { z } from "@hono/zod-openapi";
|
||||||
import type { Status as ApiNote } from "@versia/client/types";
|
import type { Status as ApiNote } from "@versia/client/types";
|
||||||
import { zBoolean } from "~/packages/config-manager/config.type.ts";
|
import { config } from "~/config.ts";
|
||||||
import { config } from "~/packages/config-manager/index.ts";
|
|
||||||
import { Account } from "./account.ts";
|
import { Account } from "./account.ts";
|
||||||
import { Attachment } from "./attachment.ts";
|
import { Attachment } from "./attachment.ts";
|
||||||
import { PreviewCard } from "./card.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 { CustomEmoji } from "./emoji.ts";
|
||||||
import { FilterResult } from "./filters.ts";
|
import { FilterResult } from "./filters.ts";
|
||||||
import { Poll } from "./poll.ts";
|
import { Poll } from "./poll.ts";
|
||||||
|
|
@ -58,12 +57,12 @@ export const StatusSource = z
|
||||||
}),
|
}),
|
||||||
text: z
|
text: z
|
||||||
.string()
|
.string()
|
||||||
.max(config.validation.max_note_size)
|
.max(config.validation.notes.max_characters)
|
||||||
.trim()
|
.trim()
|
||||||
.refine(
|
.refine(
|
||||||
(s) =>
|
(s) =>
|
||||||
!config.filters.note_content.some((filter) =>
|
!config.validation.filters.note_content.some((filter) =>
|
||||||
s.match(filter),
|
filter.test(s),
|
||||||
),
|
),
|
||||||
"Status contains blocked words",
|
"Status contains blocked words",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { z } from "@hono/zod-openapi";
|
import { z } from "@hono/zod-openapi";
|
||||||
import { RolePermission } from "@versia/client/types";
|
import { RolePermission } from "@versia/client/types";
|
||||||
import { config } from "~/packages/config-manager/index.ts";
|
import { config } from "~/config.ts";
|
||||||
import { Id } from "./common.ts";
|
import { Id } from "./common.ts";
|
||||||
|
|
||||||
/* Versia Server API extension */
|
/* Versia Server API extension */
|
||||||
|
|
@ -56,7 +56,7 @@ export const NoteReaction = z
|
||||||
name: z
|
name: z
|
||||||
.string()
|
.string()
|
||||||
.min(1)
|
.min(1)
|
||||||
.max(config.validation.max_emoji_shortcode_size)
|
.max(config.validation.emojis.max_shortcode_characters)
|
||||||
.trim()
|
.trim()
|
||||||
.openapi({
|
.openapi({
|
||||||
description: "Custom Emoji shortcode or Unicode emoji.",
|
description: "Custom Emoji shortcode or Unicode emoji.",
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import {
|
||||||
Ingest as SonicChannelIngest,
|
Ingest as SonicChannelIngest,
|
||||||
Search as SonicChannelSearch,
|
Search as SonicChannelSearch,
|
||||||
} from "sonic-channel";
|
} from "sonic-channel";
|
||||||
import { type Config, config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enum for Sonic index types
|
* Enum for Sonic index types
|
||||||
|
|
@ -32,17 +32,21 @@ export class SonicSearchManager {
|
||||||
/**
|
/**
|
||||||
* @param config Configuration for Sonic
|
* @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({
|
this.searchChannel = new SonicChannelSearch({
|
||||||
host: config.sonic.host,
|
host: config.search.sonic.host,
|
||||||
port: config.sonic.port,
|
port: config.search.sonic.port,
|
||||||
auth: config.sonic.password,
|
auth: config.search.sonic.password,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.ingestChannel = new SonicChannelIngest({
|
this.ingestChannel = new SonicChannelIngest({
|
||||||
host: config.sonic.host,
|
host: config.search.sonic.host,
|
||||||
port: config.sonic.port,
|
port: config.search.sonic.port,
|
||||||
auth: config.sonic.password,
|
auth: config.search.sonic.password,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,7 +54,7 @@ export class SonicSearchManager {
|
||||||
* Connect to Sonic
|
* Connect to Sonic
|
||||||
*/
|
*/
|
||||||
public async connect(silent = false): Promise<void> {
|
public async connect(silent = false): Promise<void> {
|
||||||
if (!this.config.sonic.enabled) {
|
if (!config.search.enabled) {
|
||||||
!silent && this.logger.info`Sonic search is disabled`;
|
!silent && this.logger.info`Sonic search is disabled`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -127,7 +131,7 @@ export class SonicSearchManager {
|
||||||
* @param user User to add
|
* @param user User to add
|
||||||
*/
|
*/
|
||||||
public async addUser(user: User): Promise<void> {
|
public async addUser(user: User): Promise<void> {
|
||||||
if (!this.config.sonic.enabled) {
|
if (!config.search.enabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -310,4 +314,4 @@ export class SonicSearchManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const searchManager = new SonicSearchManager(config);
|
export const searchManager = new SonicSearchManager();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { User } from "@versia/kit/db";
|
import { User } from "@versia/kit/db";
|
||||||
import { Worker } from "bullmq";
|
import { Worker } from "bullmq";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
import { connection } from "~/utils/redis.ts";
|
import { connection } from "~/utils/redis.ts";
|
||||||
import {
|
import {
|
||||||
type DeliveryJobData,
|
type DeliveryJobData,
|
||||||
|
|
@ -52,10 +52,10 @@ export const getDeliveryWorker = (): Worker<
|
||||||
{
|
{
|
||||||
connection,
|
connection,
|
||||||
removeOnComplete: {
|
removeOnComplete: {
|
||||||
age: config.queues.delivery.remove_on_complete,
|
age: config.queues.delivery?.remove_after_complete_seconds,
|
||||||
},
|
},
|
||||||
removeOnFail: {
|
removeOnFail: {
|
||||||
age: config.queues.delivery.remove_on_failure,
|
age: config.queues.delivery?.remove_after_failure_seconds,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { Instance } from "@versia/kit/db";
|
||||||
import { Instances } from "@versia/kit/tables";
|
import { Instances } from "@versia/kit/tables";
|
||||||
import { Worker } from "bullmq";
|
import { Worker } from "bullmq";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
import { connection } from "~/utils/redis.ts";
|
import { connection } from "~/utils/redis.ts";
|
||||||
import {
|
import {
|
||||||
type FetchJobData,
|
type FetchJobData,
|
||||||
|
|
@ -52,10 +52,10 @@ export const getFetchWorker = (): Worker<FetchJobData, void, FetchJobType> =>
|
||||||
{
|
{
|
||||||
connection,
|
connection,
|
||||||
removeOnComplete: {
|
removeOnComplete: {
|
||||||
age: config.queues.fetch.remove_on_complete,
|
age: config.queues.fetch?.remove_after_complete_seconds,
|
||||||
},
|
},
|
||||||
removeOnFail: {
|
removeOnFail: {
|
||||||
age: config.queues.fetch.remove_on_failure,
|
age: config.queues.fetch?.remove_after_failure_seconds,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { getLogger } from "@logtape/logtape";
|
import { getLogger } from "@logtape/logtape";
|
||||||
import { Instance, User } from "@versia/kit/db";
|
import { Instance, User } from "@versia/kit/db";
|
||||||
import { Worker } from "bullmq";
|
import { Worker } from "bullmq";
|
||||||
import { config } from "~/packages/config-manager/index.ts";
|
import { config } from "~/config.ts";
|
||||||
import { connection } from "~/utils/redis.ts";
|
import { connection } from "~/utils/redis.ts";
|
||||||
import { InboxProcessor } from "../inbox/processor.ts";
|
import { InboxProcessor } from "../inbox/processor.ts";
|
||||||
import {
|
import {
|
||||||
|
|
@ -168,10 +168,10 @@ export const getInboxWorker = (): Worker<InboxJobData, void, InboxJobType> =>
|
||||||
{
|
{
|
||||||
connection,
|
connection,
|
||||||
removeOnComplete: {
|
removeOnComplete: {
|
||||||
age: config.queues.inbox.remove_on_complete,
|
age: config.queues.inbox?.remove_after_complete_seconds,
|
||||||
},
|
},
|
||||||
removeOnFail: {
|
removeOnFail: {
|
||||||
age: config.queues.inbox.remove_on_failure,
|
age: config.queues.inbox?.remove_after_failure_seconds,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Media } from "@versia/kit/db";
|
import { Media } from "@versia/kit/db";
|
||||||
import { Worker } from "bullmq";
|
import { Worker } from "bullmq";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
import { connection } from "~/utils/redis.ts";
|
import { connection } from "~/utils/redis.ts";
|
||||||
import { calculateBlurhash } from "../media/preprocessors/blurhash.ts";
|
import { calculateBlurhash } from "../media/preprocessors/blurhash.ts";
|
||||||
import { convertImage } from "../media/preprocessors/image-conversion.ts";
|
import { convertImage } from "../media/preprocessors/image-conversion.ts";
|
||||||
|
|
@ -100,10 +100,10 @@ export const getMediaWorker = (): Worker<MediaJobData, void, MediaJobType> =>
|
||||||
{
|
{
|
||||||
connection,
|
connection,
|
||||||
removeOnComplete: {
|
removeOnComplete: {
|
||||||
age: config.queues.media.remove_on_complete,
|
age: config.queues.media?.remove_after_complete_seconds,
|
||||||
},
|
},
|
||||||
removeOnFail: {
|
removeOnFail: {
|
||||||
age: config.queues.media.remove_on_failure,
|
age: config.queues.media?.remove_after_failure_seconds,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { htmlToText } from "@/content_types.ts";
|
||||||
import { Note, PushSubscription, Token, User } from "@versia/kit/db";
|
import { Note, PushSubscription, Token, User } from "@versia/kit/db";
|
||||||
import { Worker } from "bullmq";
|
import { Worker } from "bullmq";
|
||||||
import { sendNotification } from "web-push";
|
import { sendNotification } from "web-push";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
import { connection } from "~/utils/redis.ts";
|
import { connection } from "~/utils/redis.ts";
|
||||||
import {
|
import {
|
||||||
type PushJobData,
|
type PushJobData,
|
||||||
|
|
@ -18,6 +18,11 @@ export const getPushWorker = (): Worker<PushJobData, void, PushJobType> =>
|
||||||
data: { psId, relatedUserId, type, noteId, notificationId },
|
data: { psId, relatedUserId, type, noteId, notificationId },
|
||||||
} = job;
|
} = job;
|
||||||
|
|
||||||
|
if (!config.notifications.push) {
|
||||||
|
await job.log("Push notifications are disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await job.log(
|
await job.log(
|
||||||
`Sending push notification for note [${notificationId}]`,
|
`Sending push notification for note [${notificationId}]`,
|
||||||
);
|
);
|
||||||
|
|
@ -105,17 +110,18 @@ export const getPushWorker = (): Worker<PushJobData, void, PushJobType> =>
|
||||||
preferred_locale: "en-US",
|
preferred_locale: "en-US",
|
||||||
notification_id: notificationId,
|
notification_id: notificationId,
|
||||||
notification_type: type,
|
notification_type: type,
|
||||||
icon: relatedUser.getAvatarUrl(config),
|
icon: relatedUser.getAvatarUrl(),
|
||||||
title,
|
title,
|
||||||
body: truncate(body, 140),
|
body: truncate(body, 140),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
vapidDetails: {
|
vapidDetails: {
|
||||||
subject:
|
subject:
|
||||||
config.notifications.push.vapid.subject ||
|
config.notifications.push.subject ||
|
||||||
config.http.base_url.origin,
|
config.http.base_url.origin,
|
||||||
privateKey: config.notifications.push.vapid.private,
|
privateKey:
|
||||||
publicKey: config.notifications.push.vapid.public,
|
config.notifications.push.vapid_keys.private,
|
||||||
|
publicKey: config.notifications.push.vapid_keys.public,
|
||||||
},
|
},
|
||||||
contentEncoding: "aesgcm",
|
contentEncoding: "aesgcm",
|
||||||
},
|
},
|
||||||
|
|
@ -128,10 +134,10 @@ export const getPushWorker = (): Worker<PushJobData, void, PushJobType> =>
|
||||||
{
|
{
|
||||||
connection,
|
connection,
|
||||||
removeOnComplete: {
|
removeOnComplete: {
|
||||||
age: config.queues.push.remove_on_complete,
|
age: config.queues.push?.remove_after_complete_seconds,
|
||||||
},
|
},
|
||||||
removeOnFail: {
|
removeOnFail: {
|
||||||
age: config.queues.push.remove_on_failure,
|
age: config.queues.push?.remove_after_failure_seconds,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import chalk from "chalk";
|
||||||
import { and, eq, isNull } from "drizzle-orm";
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
import ora from "ora";
|
import ora from "ora";
|
||||||
import { BaseCommand } from "~/cli/base";
|
import { BaseCommand } from "~/cli/base";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
export default class EmojiAdd extends BaseCommand<typeof EmojiAdd> {
|
export default class EmojiAdd extends BaseCommand<typeof EmojiAdd> {
|
||||||
public static override args = {
|
public static override args = {
|
||||||
|
|
@ -62,7 +62,7 @@ export default class EmojiAdd extends BaseCommand<typeof EmojiAdd> {
|
||||||
"Accept-Encoding": "identity",
|
"Accept-Encoding": "identity",
|
||||||
},
|
},
|
||||||
// @ts-expect-error Proxy is a Bun-specific feature
|
// @ts-expect-error Proxy is a Bun-specific feature
|
||||||
proxy: config.http.proxy.address,
|
proxy: config.http.proxy_address,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { lookup } from "mime-types";
|
||||||
import ora from "ora";
|
import ora from "ora";
|
||||||
import { unzip } from "unzipit";
|
import { unzip } from "unzipit";
|
||||||
import { BaseCommand } from "~/cli/base";
|
import { BaseCommand } from "~/cli/base";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
type MetaType = {
|
type MetaType = {
|
||||||
emojis: {
|
emojis: {
|
||||||
|
|
@ -69,7 +69,7 @@ export default class EmojiImport extends BaseCommand<typeof EmojiImport> {
|
||||||
"Accept-Encoding": "identity",
|
"Accept-Encoding": "identity",
|
||||||
},
|
},
|
||||||
// @ts-expect-error Proxy is a Bun-specific feature
|
// @ts-expect-error Proxy is a Bun-specific feature
|
||||||
proxy: config.http.proxy.address,
|
proxy: config.http.proxy_address,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { Args, Flags } from "@oclif/core";
|
||||||
import ora from "ora";
|
import ora from "ora";
|
||||||
import { SonicIndexType, searchManager } from "~/classes/search/search-manager";
|
import { SonicIndexType, searchManager } from "~/classes/search/search-manager";
|
||||||
import { BaseCommand } from "~/cli/base";
|
import { BaseCommand } from "~/cli/base";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
export default class IndexRebuild extends BaseCommand<typeof IndexRebuild> {
|
export default class IndexRebuild extends BaseCommand<typeof IndexRebuild> {
|
||||||
public static override args = {
|
public static override args = {
|
||||||
|
|
@ -28,8 +28,8 @@ export default class IndexRebuild extends BaseCommand<typeof IndexRebuild> {
|
||||||
public async run(): Promise<void> {
|
public async run(): Promise<void> {
|
||||||
const { flags, args } = await this.parse(IndexRebuild);
|
const { flags, args } = await this.parse(IndexRebuild);
|
||||||
|
|
||||||
if (!config.sonic.enabled) {
|
if (!config.search.enabled) {
|
||||||
this.error("Sonic search is disabled");
|
this.error("Search is disabled");
|
||||||
this.exit(1);
|
this.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import chalk from "chalk";
|
||||||
import { renderUnicodeCompact } from "uqr";
|
import { renderUnicodeCompact } from "uqr";
|
||||||
import { UserFinderCommand } from "~/cli/classes";
|
import { UserFinderCommand } from "~/cli/classes";
|
||||||
import { formatArray } from "~/cli/utils/format";
|
import { formatArray } from "~/cli/utils/format";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
export default class UserReset extends UserFinderCommand<typeof UserReset> {
|
export default class UserReset extends UserFinderCommand<typeof UserReset> {
|
||||||
public static override description = "Resets users' passwords";
|
public static override description = "Resets users' passwords";
|
||||||
|
|
|
||||||
50
config.ts
Normal file
50
config.ts
Normal file
|
|
@ -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<z.infer<typeof ConfigSchema>>({
|
||||||
|
configFile: "./config/config.toml",
|
||||||
|
overrides:
|
||||||
|
(
|
||||||
|
await loadConfig<z.infer<typeof ConfigSchema>>({
|
||||||
|
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);
|
||||||
|
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportedConfig = parsed.data;
|
||||||
|
|
||||||
|
export { exportedConfig as config };
|
||||||
|
|
@ -1,16 +1,20 @@
|
||||||
# You can change the URL to the commit/tag you are using
|
# 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
|
#:schema https://raw.githubusercontent.com/versia-pub/server/main/config/config.schema.json
|
||||||
|
|
||||||
[database]
|
# 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)
|
||||||
# Main PostgreSQL database connection
|
|
||||||
|
|
||||||
|
[postgres]
|
||||||
|
# PostgreSQL database configuration
|
||||||
host = "localhost"
|
host = "localhost"
|
||||||
port = 5432
|
port = 5432
|
||||||
username = "versia"
|
username = "versia"
|
||||||
|
# Sensitive value
|
||||||
password = "mycoolpassword"
|
password = "mycoolpassword"
|
||||||
database = "versia"
|
database = "versia"
|
||||||
|
|
||||||
# Add any eventual read-only database replicas here
|
# Additional read-only replicas
|
||||||
# [[database.replicas]]
|
# [[postgres.replicas]]
|
||||||
# host = "other-host"
|
# host = "other-host"
|
||||||
# port = 5432
|
# port = 5432
|
||||||
# username = "versia"
|
# username = "versia"
|
||||||
|
|
@ -18,45 +22,48 @@ database = "versia"
|
||||||
# database = "replica1"
|
# database = "replica1"
|
||||||
|
|
||||||
[redis.queue]
|
[redis.queue]
|
||||||
# Redis instance for storing the federation queue
|
# A Redis database used for managing queues.
|
||||||
# Required for federation
|
# Required for federation
|
||||||
host = "localhost"
|
host = "localhost"
|
||||||
port = 6379
|
port = 6379
|
||||||
password = ""
|
# Sensitive value
|
||||||
|
# password = "test"
|
||||||
database = 0
|
database = 0
|
||||||
|
|
||||||
[redis.cache]
|
# A Redis database used for caching SQL queries.
|
||||||
# Redis instance to be used as a timeline cache
|
|
||||||
# Optional, can be the same as the queue instance
|
# Optional, can be the same as the queue instance
|
||||||
host = "localhost"
|
# [redis.cache]
|
||||||
port = 6380
|
# host = "localhost"
|
||||||
password = ""
|
# port = 6380
|
||||||
database = 1
|
# database = 1
|
||||||
|
# password = ""
|
||||||
|
|
||||||
|
# Search and indexing configuration
|
||||||
|
[search]
|
||||||
|
# Enable indexing and searching?
|
||||||
enabled = false
|
enabled = false
|
||||||
|
|
||||||
[sonic]
|
# Optional if search is disabled
|
||||||
# If Sonic is not configured, search will not be enabled
|
# [search.sonic]
|
||||||
host = "localhost"
|
# host = "localhost"
|
||||||
port = 7700
|
# port = 7700
|
||||||
password = ""
|
# Sensitive value
|
||||||
enabled = true
|
# password = "test"
|
||||||
|
|
||||||
[signups]
|
[registration]
|
||||||
# Whether to enable registrations or not
|
# Can users sign up freely?
|
||||||
registration = true
|
allow = true
|
||||||
rules = [
|
# NOT IMPLEMENTED
|
||||||
"Do not harass others",
|
require_approval = false
|
||||||
"Be nice to people",
|
# Message to show to users when registration is disabled
|
||||||
"Don't spam",
|
# message = "ran out of spoons to moderate registrations, sorry"
|
||||||
"Don't post illegal content",
|
|
||||||
]
|
|
||||||
|
|
||||||
[http]
|
[http]
|
||||||
# The full URL Versia Server will be reachable by (paths are not supported)
|
# URL that the instance will be accessible at
|
||||||
base_url = "https://versia.localhost:9900"
|
base_url = "https://example.com"
|
||||||
# Address to bind to (0.0.0.0 is suggested for proxies)
|
# Address to bind to (0.0.0.0 is suggested for proxies)
|
||||||
bind = "versia.localhost"
|
bind = "0.0.0.0"
|
||||||
bind_port = 9900
|
bind_port = 8080
|
||||||
|
|
||||||
# Bans IPv4 or IPv6 IPs (wildcards, networks and ranges are supported)
|
# Bans IPv4 or IPv6 IPs (wildcards, networks and ranges are supported)
|
||||||
banned_ips = []
|
banned_ips = []
|
||||||
|
|
@ -66,29 +73,17 @@ banned_user_agents = [
|
||||||
# "wget\/1.20.3",
|
# "wget\/1.20.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[http.proxy]
|
# URL to an eventual HTTP proxy
|
||||||
# For HTTP proxies (e.g. Tor proxies)
|
|
||||||
# Will be used for all outgoing requests
|
# Will be used for all outgoing requests
|
||||||
enabled = false
|
# proxy_address = "http://localhost:8118"
|
||||||
address = "http://localhost:8118"
|
|
||||||
|
|
||||||
[http.tls]
|
# TLS configuration. You should probably be using a reverse proxy instead of this
|
||||||
# If these values are set, Versia Server will use these files for TLS
|
# [http.tls]
|
||||||
enabled = false
|
# key = "/path/to/key.pem"
|
||||||
key = ""
|
# cert = "/path/to/cert.pem"
|
||||||
cert = ""
|
# Sensitive value
|
||||||
passphrase = ""
|
# passphrase = "awawa"
|
||||||
ca = ""
|
# ca = "/path/to/ca.pem"
|
||||||
|
|
||||||
[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"]
|
|
||||||
|
|
||||||
[frontend]
|
[frontend]
|
||||||
# Enable custom frontends (warning: not enabling this will make Versia Server only accessible via the Mastodon API)
|
# 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.
|
# This can be used to set up custom themes, etc on supported frontends.
|
||||||
# theme = "dark"
|
# 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
|
# SMTP server to use for sending emails
|
||||||
server = "smtp.example.com"
|
# server = "smtp.example.com"
|
||||||
port = 465
|
# port = 465
|
||||||
username = "test@example.com"
|
# username = "test@example.com"
|
||||||
password = "password123"
|
# Sensitive value
|
||||||
tls = true
|
# password = "password123"
|
||||||
# Disable all email functions (this will allow people to sign up without verifying
|
# tls = true
|
||||||
# their email)
|
|
||||||
enabled = false
|
|
||||||
|
|
||||||
[media]
|
[media]
|
||||||
# Can be "s3" or "local", where "local" uploads the file to the local filesystem
|
# Can be "s3" or "local", where "local" uploads the file to the local filesystem
|
||||||
# Changing this value will not retroactively apply to existing data
|
# Changing this value will not retroactively apply to existing data
|
||||||
# Don't forget to fill in the s3 config :3
|
# Don't forget to fill in the s3 config :3
|
||||||
backend = "s3"
|
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
|
# If media backend is "local", this is the folder where the files will be stored
|
||||||
# Can be any path
|
# Can be any path
|
||||||
local_uploads_folder = "uploads"
|
uploads_path = "uploads"
|
||||||
|
|
||||||
[media.conversion]
|
[media.conversion]
|
||||||
# Whether to automatically convert images to another format on upload
|
# Whether to automatically convert images to another format on upload
|
||||||
|
|
@ -141,41 +138,30 @@ convert_images = true
|
||||||
# JXL support will likely not work
|
# JXL support will likely not work
|
||||||
convert_to = "image/webp"
|
convert_to = "image/webp"
|
||||||
# Also convert SVG images?
|
# Also convert SVG images?
|
||||||
convert_vector = false
|
convert_vectors = false
|
||||||
|
|
||||||
# [s3]
|
# [s3]
|
||||||
# Can be left commented if you don't use the S3 media backend
|
# Can be left commented if you don't use the S3 media backend
|
||||||
# endpoint = ""
|
# endpoint = "https://s3.example.com"
|
||||||
|
# Sensitive value
|
||||||
# access_key = "XXXXX"
|
# access_key = "XXXXX"
|
||||||
|
# Sensitive value
|
||||||
# secret_access_key = "XXX"
|
# secret_access_key = "XXX"
|
||||||
# region = ""
|
# region = "us-east-1"
|
||||||
# bucket_name = "versia"
|
# bucket_name = "versia"
|
||||||
# public_url = "https://cdn.example.com"
|
# public_url = "https://cdn.example.com"
|
||||||
|
|
||||||
[validation]
|
[validation]
|
||||||
# Checks user data
|
# Checks user data
|
||||||
# Does not retroactively apply to previously entered data
|
# Does not retroactively apply to previously entered data
|
||||||
max_displayname_size = 50 # Character length
|
[validation.accounts]
|
||||||
max_bio_size = 5000
|
max_displayname_characters = 50
|
||||||
max_note_size = 5000
|
max_username_characters = 30
|
||||||
max_avatar_size = 5_000_000 # Bytes
|
max_bio_characters = 5000
|
||||||
max_header_size = 5_000_000
|
max_avatar_bytes = 5_000_000
|
||||||
max_media_size = 40_000_000
|
max_header_bytes = 5_000_000
|
||||||
max_media_attachments = 10
|
# Regex is allowed here
|
||||||
max_media_description_size = 1000
|
disallowed_usernames = [
|
||||||
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 = [
|
|
||||||
"well-known",
|
"well-known",
|
||||||
"about",
|
"about",
|
||||||
"activities",
|
"activities",
|
||||||
|
|
@ -202,12 +188,14 @@ username_blacklist = [
|
||||||
"search",
|
"search",
|
||||||
"mfa",
|
"mfa",
|
||||||
]
|
]
|
||||||
# Whether to blacklist known temporary email providers
|
max_field_count = 10
|
||||||
blacklist_tempmail = false
|
max_field_name_characters = 1000
|
||||||
# Additional email providers to blacklist (list of domains)
|
max_field_value_characters = 1000
|
||||||
email_blacklist = []
|
max_pinned_notes = 20
|
||||||
# Valid URL schemes, otherwise the URL is parsed as text
|
|
||||||
url_scheme_whitelist = [
|
[validation.notes]
|
||||||
|
max_characters = 5000
|
||||||
|
allowed_url_schemes = [
|
||||||
"http",
|
"http",
|
||||||
"https",
|
"https",
|
||||||
"ftp",
|
"ftp",
|
||||||
|
|
@ -226,39 +214,71 @@ url_scheme_whitelist = [
|
||||||
"ssb",
|
"ssb",
|
||||||
"gemini",
|
"gemini",
|
||||||
]
|
]
|
||||||
# Only allow those MIME types of data to be uploaded
|
max_attachments = 16
|
||||||
# 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]
|
[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
|
# "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
|
# This means that they do not require any user interaction, instead
|
||||||
# they require the user's computer to do a small amount of work
|
# 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
|
# The difficulty of the challenge, higher is will take more time to solve
|
||||||
difficulty = 50000
|
# difficulty = 50000
|
||||||
# Challenge expiration time in seconds
|
# Challenge expiration time in seconds
|
||||||
expiration = 300 # 5 minutes
|
# expiration = 300 # 5 minutes
|
||||||
# Leave this empty to generate a new key
|
# Leave this empty to generate a new key
|
||||||
key = ""
|
# Sensitive value
|
||||||
|
# key = ""
|
||||||
|
|
||||||
|
# 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]
|
[notifications]
|
||||||
|
|
||||||
[notifications.push]
|
# Web Push Notifications configuration.
|
||||||
# Whether to enable push notifications
|
# Leave out to disable.
|
||||||
enabled = true
|
# [notifications.push]
|
||||||
|
# Subject field embedded in the push notification
|
||||||
[notifications.push.vapid]
|
# subject = "mailto:joe@example.com"
|
||||||
|
#
|
||||||
|
# [notifications.push.vapid_keys]
|
||||||
# VAPID keys for push notifications
|
# VAPID keys for push notifications
|
||||||
# Run Versia Server with those values missing to generate new keys
|
# Run Versia Server with those values missing to generate new keys
|
||||||
public = ""
|
# Sensitive value
|
||||||
private = ""
|
# public = ""
|
||||||
# Optional
|
# Sensitive value
|
||||||
# subject = "mailto:joe@example.com"
|
# private = ""
|
||||||
|
|
||||||
[defaults]
|
[defaults]
|
||||||
# Default visibility for new notes
|
# Default visibility for new notes
|
||||||
|
|
@ -278,37 +298,37 @@ placeholder_style = "thumbs"
|
||||||
# Controls the delivery queue (for outbound federation)
|
# Controls the delivery queue (for outbound federation)
|
||||||
[queues.delivery]
|
[queues.delivery]
|
||||||
# Time in seconds to remove completed jobs
|
# Time in seconds to remove completed jobs
|
||||||
remove_on_complete = 31536000
|
remove_after_complete_seconds = 31536000
|
||||||
# Time in seconds to remove failed jobs
|
# Time in seconds to remove failed jobs
|
||||||
remove_on_failure = 31536000
|
remove_after_failure_seconds = 31536000
|
||||||
|
|
||||||
# Controls the inbox processing queue (for inbound federation)
|
# Controls the inbox processing queue (for inbound federation)
|
||||||
[queues.inbox]
|
[queues.inbox]
|
||||||
# Time in seconds to remove completed jobs
|
# Time in seconds to remove completed jobs
|
||||||
remove_on_complete = 31536000
|
remove_after_complete_seconds = 31536000
|
||||||
# Time in seconds to remove failed jobs
|
# Time in seconds to remove failed jobs
|
||||||
remove_on_failure = 31536000
|
remove_after_failure_seconds = 31536000
|
||||||
|
|
||||||
# Controls the fetch queue (for remote data refreshes)
|
# Controls the fetch queue (for remote data refreshes)
|
||||||
[queues.fetch]
|
[queues.fetch]
|
||||||
# Time in seconds to remove completed jobs
|
# Time in seconds to remove completed jobs
|
||||||
remove_on_complete = 31536000
|
remove_after_complete_seconds = 31536000
|
||||||
# Time in seconds to remove failed jobs
|
# Time in seconds to remove failed jobs
|
||||||
remove_on_failure = 31536000
|
remove_after_failure_seconds = 31536000
|
||||||
|
|
||||||
# Controls the push queue (for push notification delivery)
|
# Controls the push queue (for push notification delivery)
|
||||||
[queues.push]
|
[queues.push]
|
||||||
# Time in seconds to remove completed jobs
|
# Time in seconds to remove completed jobs
|
||||||
remove_on_complete = 31536000
|
remove_after_complete_seconds = 31536000
|
||||||
# Time in seconds to remove failed jobs
|
# Time in seconds to remove failed jobs
|
||||||
remove_on_failure = 31536000
|
remove_after_failure_seconds = 31536000
|
||||||
|
|
||||||
# Controls the media queue (for media processing)
|
# Controls the media queue (for media processing)
|
||||||
[queues.media]
|
[queues.media]
|
||||||
# Time in seconds to remove completed jobs
|
# Time in seconds to remove completed jobs
|
||||||
remove_on_complete = 31536000
|
remove_after_complete_seconds = 31536000
|
||||||
# Time in seconds to remove failed jobs
|
# Time in seconds to remove failed jobs
|
||||||
remove_on_failure = 31536000
|
remove_after_failure_seconds = 31536000
|
||||||
|
|
||||||
[federation]
|
[federation]
|
||||||
# This is a list of domain names, such as "mastodon.social" or "pleroma.site"
|
# 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
|
# For bridge software, such as versia-pub/activitypub
|
||||||
# Bridges must be hosted separately from the main Versia Server process
|
# Bridges must be hosted separately from the main Versia Server process
|
||||||
[federation.bridge]
|
# [federation.bridge]
|
||||||
enabled = false
|
|
||||||
# Only versia-ap exists for now
|
# Only versia-ap exists for now
|
||||||
software = "versia-ap"
|
# software = "versia-ap"
|
||||||
# If this is empty, any bridge with the correct token
|
# If this is empty, any bridge with the correct token
|
||||||
# will be able to send data to your instance
|
# 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
|
# Token for the bridge software
|
||||||
# Bridge must have the same token!
|
# Bridge must have the same token!
|
||||||
token = "mycooltoken"
|
# Sensitive value
|
||||||
url = "https://ap.versia.social"
|
# token = "mycooltoken"
|
||||||
|
# url = "https://ap.versia.social"
|
||||||
|
|
||||||
[instance]
|
[instance]
|
||||||
name = "Versia"
|
name = "Versia"
|
||||||
description = "A Versia Server instance"
|
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"
|
# 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"
|
# 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"
|
# privacy_policy_path = "config/privacy_policy.md"
|
||||||
# URL to your instance logo
|
|
||||||
# logo = ""
|
# Primary instance languages. ISO 639-1 codes.
|
||||||
# URL to your instance banner
|
languages = ["en"]
|
||||||
# banner = ""
|
|
||||||
|
[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.
|
# Used for federation. If left empty or missing, the server will generate one for you.
|
||||||
[instance.keys]
|
# [instance.keys]
|
||||||
public = ""
|
# Sensitive value
|
||||||
private = ""
|
# 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]
|
[permissions]
|
||||||
# Control default permissions for users
|
# Control default permissions for users
|
||||||
|
|
@ -385,67 +424,34 @@ private = ""
|
||||||
# Defaults to being able to manage all instance data, content, and users
|
# Defaults to being able to manage all instance data, content, and users
|
||||||
# admin = []
|
# 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]
|
[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
|
# Available levels: debug, info, warning, error, fatal
|
||||||
log_level = "debug"
|
log_level = "debug"
|
||||||
# For GDPR compliance, you can disable logging of IPs
|
|
||||||
log_ip = false
|
|
||||||
|
|
||||||
# Log all filtered objects
|
log_file_path = "logs/versia.log"
|
||||||
log_filters = true
|
|
||||||
|
|
||||||
[logging.sentry]
|
[logging.types]
|
||||||
# Whether to enable https://sentry.io error logging
|
# Either pass a boolean
|
||||||
enabled = false
|
# 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
|
# Sentry DSN for error logging
|
||||||
dsn = ""
|
# dsn = "https://example.com"
|
||||||
debug = false
|
# debug = false
|
||||||
|
|
||||||
sample_rate = 1.0
|
# sample_rate = 1.0
|
||||||
traces_sample_rate = 1.0
|
# traces_sample_rate = 1.0
|
||||||
# Can also be regex
|
# Can also be regex
|
||||||
trace_propagation_targets = []
|
# trace_propagation_targets = []
|
||||||
max_breadcrumbs = 100
|
# max_breadcrumbs = 100
|
||||||
# environment = "production"
|
# 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]
|
[plugins]
|
||||||
# Whether to automatically load all plugins in the plugins directory
|
# Whether to automatically load all plugins in the plugins directory
|
||||||
autoload = true
|
autoload = true
|
||||||
|
|
@ -481,5 +487,6 @@ allow_registration = true
|
||||||
# This MUST match the provider's issuer URI, including the trailing slash (or lack thereof)
|
# This MUST match the provider's issuer URI, including the trailing slash (or lack thereof)
|
||||||
# url = "https://id.cpluspatch.com/application/o/versia-testing/"
|
# url = "https://id.cpluspatch.com/application/o/versia-testing/"
|
||||||
# client_id = "XXXX"
|
# client_id = "XXXX"
|
||||||
|
# Sensitive value
|
||||||
# client_secret = "XXXXX"
|
# client_secret = "XXXXX"
|
||||||
# icon = "https://cpluspatch.com/images/icons/logo.svg"
|
# icon = "https://cpluspatch.com/images/icons/logo.svg"
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Config } from "drizzle-kit";
|
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
|
* Drizzle can't properly resolve imports with top-level await, so uncomment
|
||||||
|
|
@ -15,11 +15,11 @@ export default {
|
||||||
user: "lysand",
|
user: "lysand",
|
||||||
password: "lysand",
|
password: "lysand",
|
||||||
database: "lysand", */
|
database: "lysand", */
|
||||||
host: config.database.host,
|
host: config.postgres.host,
|
||||||
port: Number(config.database.port),
|
port: config.postgres.port,
|
||||||
user: config.database.username,
|
user: config.postgres.username,
|
||||||
password: config.database.password,
|
password: config.postgres.password,
|
||||||
database: config.database.database,
|
database: config.postgres.database,
|
||||||
},
|
},
|
||||||
// Print all statements
|
// Print all statements
|
||||||
verbose: true,
|
verbose: true,
|
||||||
|
|
|
||||||
|
|
@ -4,28 +4,27 @@ import { type NodePgDatabase, drizzle } from "drizzle-orm/node-postgres";
|
||||||
import { withReplicas } from "drizzle-orm/pg-core";
|
import { withReplicas } from "drizzle-orm/pg-core";
|
||||||
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
||||||
import { Pool } from "pg";
|
import { Pool } from "pg";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
import * as schema from "./schema.ts";
|
import * as schema from "./schema.ts";
|
||||||
|
|
||||||
const primaryDb = new Pool({
|
const primaryDb = new Pool({
|
||||||
host: config.database.host,
|
host: config.postgres.host,
|
||||||
port: Number(config.database.port),
|
port: config.postgres.port,
|
||||||
user: config.database.username,
|
user: config.postgres.username,
|
||||||
password: config.database.password,
|
password: config.postgres.password,
|
||||||
database: config.database.database,
|
database: config.postgres.database,
|
||||||
});
|
});
|
||||||
|
|
||||||
const replicas =
|
const replicas = config.postgres.replicas.map(
|
||||||
config.database.replicas?.map(
|
(replica) =>
|
||||||
(replica) =>
|
new Pool({
|
||||||
new Pool({
|
host: replica.host,
|
||||||
host: replica.host,
|
port: replica.port,
|
||||||
port: Number(replica.port),
|
user: replica.username,
|
||||||
user: replica.username,
|
password: replica.password,
|
||||||
password: replica.password,
|
database: replica.database,
|
||||||
database: replica.database,
|
}),
|
||||||
}),
|
);
|
||||||
) ?? [];
|
|
||||||
|
|
||||||
export const db =
|
export const db =
|
||||||
(replicas.length ?? 0) > 0
|
(replicas.length ?? 0) > 0
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import cluster from "node:cluster";
|
||||||
import { sentry } from "@/sentry";
|
import { sentry } from "@/sentry";
|
||||||
import { createServer } from "@/server";
|
import { createServer } from "@/server";
|
||||||
import { appFactory } from "~/app";
|
import { appFactory } from "~/app";
|
||||||
import { config } from "~/packages/config-manager/index.ts";
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
process.on("SIGINT", () => {
|
||||||
process.exit();
|
process.exit();
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { checkConfig } from "@/init";
|
|
||||||
import { configureLoggers } from "@/loggers";
|
import { configureLoggers } from "@/loggers";
|
||||||
import { getLogger } from "@logtape/logtape";
|
import { getLogger } from "@logtape/logtape";
|
||||||
import { Note } from "@versia/kit/db";
|
import { Note } from "@versia/kit/db";
|
||||||
import IORedis from "ioredis";
|
import IORedis from "ioredis";
|
||||||
|
import { config } from "~/config.ts";
|
||||||
import { setupDatabase } from "~/drizzle/db";
|
import { setupDatabase } from "~/drizzle/db";
|
||||||
import { config } from "~/packages/config-manager/index.ts";
|
|
||||||
import { searchManager } from "../../classes/search/search-manager.ts";
|
import { searchManager } from "../../classes/search/search-manager.ts";
|
||||||
|
|
||||||
const timeAtStart = performance.now();
|
const timeAtStart = performance.now();
|
||||||
|
|
@ -26,15 +25,13 @@ serverLogger.info`Starting Versia Server...`;
|
||||||
|
|
||||||
await setupDatabase();
|
await setupDatabase();
|
||||||
|
|
||||||
if (config.sonic.enabled) {
|
if (config.search.enabled) {
|
||||||
await searchManager.connect();
|
await searchManager.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if database is reachable
|
// Check if database is reachable
|
||||||
const postCount = await Note.getCount();
|
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`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`;
|
serverLogger.info`Database is online, now serving ${postCount} posts`;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import { checkConfig } from "@/init";
|
|
||||||
import { configureLoggers } from "@/loggers";
|
import { configureLoggers } from "@/loggers";
|
||||||
import { getLogger } from "@logtape/logtape";
|
import { getLogger } from "@logtape/logtape";
|
||||||
import { Note } from "@versia/kit/db";
|
import { Note } from "@versia/kit/db";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import IORedis from "ioredis";
|
import IORedis from "ioredis";
|
||||||
|
import { config } from "~/config.ts";
|
||||||
import { setupDatabase } from "~/drizzle/db";
|
import { setupDatabase } from "~/drizzle/db";
|
||||||
import { config } from "~/packages/config-manager/index.ts";
|
|
||||||
import { searchManager } from "../../classes/search/search-manager.ts";
|
import { searchManager } from "../../classes/search/search-manager.ts";
|
||||||
|
|
||||||
const timeAtStart = performance.now();
|
const timeAtStart = performance.now();
|
||||||
|
|
@ -28,15 +27,13 @@ serverLogger.info`Starting Versia Server Worker...`;
|
||||||
|
|
||||||
await setupDatabase();
|
await setupDatabase();
|
||||||
|
|
||||||
if (config.sonic.enabled) {
|
if (config.search.enabled) {
|
||||||
await searchManager.connect();
|
await searchManager.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if database is reachable
|
// Check if database is reachable
|
||||||
const postCount = await Note.getCount();
|
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`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`;
|
serverLogger.info`Database is online, containing ${postCount} posts`;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createMiddleware } from "hono/factory";
|
import { createMiddleware } from "hono/factory";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
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) => {
|
export const agentBans = createMiddleware(async (context, next) => {
|
||||||
// Check for banned user agents (regex)
|
// Check for banned user agents (regex)
|
||||||
|
|
|
||||||
|
|
@ -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<BunFile | undefined> => {
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
|
|
@ -4,7 +4,7 @@ import type { SocketAddress } from "bun";
|
||||||
import { createMiddleware } from "hono/factory";
|
import { createMiddleware } from "hono/factory";
|
||||||
import { matches } from "ip-matching";
|
import { matches } from "ip-matching";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
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) => {
|
export const ipBans = createMiddleware(async (context, next) => {
|
||||||
// Check for banned IPs
|
// Check for banned IPs
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { getLogger } from "@logtape/logtape";
|
import { getLogger } from "@logtape/logtape";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { createMiddleware } from "hono/factory";
|
import { createMiddleware } from "hono/factory";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
export const logger = createMiddleware(async (context, next) => {
|
export const logger = createMiddleware(async (context, next) => {
|
||||||
if (config.logging.log_requests) {
|
if (config.logging.types.requests) {
|
||||||
const serverLogger = getLogger("server");
|
const serverLogger = getLogger("server");
|
||||||
const body = await context.req.raw.clone().text();
|
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)}`;
|
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}`;
|
serverLogger.debug`${urlAndMethod}\n${hash}\n${headers}\n${bodyLog}`;
|
||||||
} else {
|
} else {
|
||||||
serverLogger.debug`${urlAndMethod}`;
|
serverLogger.debug`${urlAndMethod}`;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { createMiddleware } from "hono/factory";
|
import { createMiddleware } from "hono/factory";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/config.ts";
|
||||||
|
|
||||||
export const urlCheck = createMiddleware(async (context, next) => {
|
export const urlCheck = createMiddleware(async (context, next) => {
|
||||||
// Check that request URL matches base_url
|
// Check that request URL matches base_url
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@
|
||||||
"wc": "find server database *.ts docs packages types utils drizzle tests -type f -print0 | wc -m --files0-from=-",
|
"wc": "find server database *.ts docs packages types utils drizzle tests -type f -print0 | wc -m --files0-from=-",
|
||||||
"cli": "bun run cli/index.ts",
|
"cli": "bun run cli/index.ts",
|
||||||
"prune": "ts-prune | grep -v server/ | grep -v dist/ | grep -v '(used in module)'",
|
"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 .",
|
"check": "bunx tsc -p .",
|
||||||
"test": "find . -name \"*.test.ts\" -not -path \"./node_modules/*\" | xargs -I {} sh -c 'bun test {} || exit 255'",
|
"test": "find . -name \"*.test.ts\" -not -path \"./node_modules/*\" | xargs -I {} sh -c 'bun test {} || exit 255'",
|
||||||
"docs:dev": "vitepress dev docs",
|
"docs:dev": "vitepress dev docs",
|
||||||
|
|
|
||||||
|
|
@ -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<typeof configValidator>;
|
|
||||||
|
|
@ -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<Config>({
|
|
||||||
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 };
|
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
"format": "uri"
|
"format": "uri"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["name"],
|
"required": ["name", "email", "url"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -75,10 +75,18 @@
|
||||||
"format": "uri"
|
"format": "uri"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"required": ["type", "url"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["name", "version", "description"],
|
"required": [
|
||||||
|
"$schema",
|
||||||
|
"name",
|
||||||
|
"version",
|
||||||
|
"description",
|
||||||
|
"authors",
|
||||||
|
"repository"
|
||||||
|
],
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#"
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ export class Plugin<ConfigSchema extends z.ZodTypeAny> {
|
||||||
try {
|
try {
|
||||||
this.store = await this.configSchema.parseAsync(config);
|
this.store = await this.configSchema.parseAsync(config);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw fromZodError(error as ZodError).message;
|
throw fromZodError(error as ZodError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { z } from "@hono/zod-openapi";
|
import { z } from "@hono/zod-openapi";
|
||||||
import { Hooks, Plugin } from "@versia/kit";
|
import { Hooks, Plugin } from "@versia/kit";
|
||||||
import { User } from "@versia/kit/db";
|
import { User } from "@versia/kit/db";
|
||||||
import chalk from "chalk";
|
|
||||||
import { getCookie } from "hono/cookie";
|
import { getCookie } from "hono/cookie";
|
||||||
import { jwtVerify } from "jose";
|
import { jwtVerify } from "jose";
|
||||||
import { JOSEError, JWTExpired } from "jose/errors";
|
import { JOSEError, JWTExpired } from "jose/errors";
|
||||||
|
import { keyPair, sensitiveString } from "~/classes/config/schema.ts";
|
||||||
import { ApiError } from "~/classes/errors/api-error.ts";
|
import { ApiError } from "~/classes/errors/api-error.ts";
|
||||||
import { RolePermissions } from "~/drizzle/schema.ts";
|
import { RolePermissions } from "~/drizzle/schema.ts";
|
||||||
import authorizeRoute from "./routes/authorize.ts";
|
import authorizeRoute from "./routes/authorize.ts";
|
||||||
|
|
@ -26,64 +26,12 @@ const configSchema = z.object({
|
||||||
id: z.string().min(1),
|
id: z.string().min(1),
|
||||||
url: z.string().min(1),
|
url: z.string().min(1),
|
||||||
client_id: 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(),
|
icon: z.string().min(1).optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.default([]),
|
.default([]),
|
||||||
keys: z
|
keys: keyPair,
|
||||||
.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<typeof v, undefined>;
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const plugin = new Plugin(configSchema);
|
const plugin = new Plugin(configSchema);
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue