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:
Jesse Wierzbinski 2025-02-15 02:47:29 +01:00
parent d4afd84019
commit 54fd81f076
No known key found for this signature in database
118 changed files with 3892 additions and 5291 deletions

View file

@ -1,7 +1,7 @@
import { afterAll, describe, expect, test } from "bun:test";
import { randomString } from "@/math";
import { Application } from "@versia/kit/db";
import { config } from "~/packages/config-manager";
import { config } from "~/config.ts";
import { fakeRequest, getTestUsers } from "~/tests/utils";
const { users, deleteUsers, passwords } = await getTestUsers(1);

View file

@ -7,7 +7,7 @@ import type { Context } from "hono";
import { setCookie } from "hono/cookie";
import { SignJWT } from "jose";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/packages/config-manager";
import { config } from "~/config.ts";
const schemas = {
form: z.object({

View file

@ -3,7 +3,7 @@ import { createRoute, z } from "@hono/zod-openapi";
import { db } from "@versia/kit/db";
import { Applications, Tokens } from "@versia/kit/tables";
import { and, eq } from "drizzle-orm";
import { config } from "~/packages/config-manager";
import { config } from "~/config.ts";
const schemas = {
query: z.object({

View file

@ -1,7 +1,7 @@
import { afterAll, describe, expect, test } from "bun:test";
import { randomString } from "@/math";
import { Application } from "@versia/kit/db";
import { config } from "~/packages/config-manager";
import { config } from "~/config.ts";
import { fakeRequest, getTestUsers } from "~/tests/utils";
const { users, deleteUsers, passwords } = await getTestUsers(1);

View file

@ -4,7 +4,7 @@ import { User } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { eq } from "drizzle-orm";
import type { Context } from "hono";
import { config } from "~/packages/config-manager";
import { config } from "~/config.ts";
const schemas = {
form: z.object({

View file

@ -10,8 +10,8 @@ import { Timeline } from "@versia/kit/db";
import { Notes, RolePermissions } from "@versia/kit/tables";
import { and, eq, gt, gte, inArray, isNull, lt, or, sql } from "drizzle-orm";
import { Account as AccountSchema } from "~/classes/schemas/account";
import { zBoolean } from "~/classes/schemas/common.ts";
import { Status as StatusSchema } from "~/classes/schemas/status";
import { zBoolean } from "~/packages/config-manager/config.type";
const route = createRoute({
method: "get",

View file

@ -6,8 +6,8 @@ import { Users } from "@versia/kit/tables";
import { and, eq, isNull } from "drizzle-orm";
import ISO6391 from "iso-639-1";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/packages/config-manager";
import { zBoolean } from "~/packages/config-manager/config.type";
import { zBoolean } from "~/classes/schemas/common";
import { config } from "~/config.ts";
const schema = z.object({
username: z.string().openapi({
@ -157,7 +157,7 @@ export default apiRoute((app) =>
const { username, email, password, agreement, locale } =
context.req.valid("json");
if (!config.signups.registration) {
if (!config.registration.allow) {
throw new ApiError(422, "Registration is disabled");
}
@ -217,7 +217,11 @@ export default apiRoute((app) =>
}
// Check if username doesnt match filters
if (config.filters.username.some((filter) => username?.match(filter))) {
if (
config.validation.filters.username.some((filter) =>
filter.test(username),
)
) {
errors.details.username.push({
error: "ERR_INVALID",
description: "contains blocked words",
@ -225,10 +229,13 @@ export default apiRoute((app) =>
}
// Check if username is too long
if ((username?.length ?? 0) > config.validation.max_username_size) {
if (
(username?.length ?? 0) >
config.validation.accounts.max_username_characters
) {
errors.details.username.push({
error: "ERR_TOO_LONG",
description: `is too long (maximum is ${config.validation.max_username_size} characters)`,
description: `is too long (maximum is ${config.validation.accounts.max_username_characters} characters)`,
});
}
@ -241,7 +248,11 @@ export default apiRoute((app) =>
}
// Check if username is reserved
if (config.validation.username_blacklist.includes(username ?? "")) {
if (
config.validation.accounts.disallowed_usernames.some((filter) =>
filter.test(username),
)
) {
errors.details.username.push({
error: "ERR_RESERVED",
description: "is reserved",
@ -274,9 +285,11 @@ export default apiRoute((app) =>
// Check if email is blocked
if (
config.validation.email_blacklist.includes(email) ||
(config.validation.blacklist_tempmail &&
tempmailDomains.domains.includes((email ?? "").split("@")[1]))
config.validation.emails.disallowed_domains.some((f) =>
f.test(email.split("@")[1]),
) ||
(config.validation.emails.disallow_tempmail &&
tempmailDomains.domains.includes(email.split("@")[1]))
) {
errors.details.email.push({
error: "ERR_BLOCKED",

View file

@ -12,7 +12,7 @@ import { and, eq, isNull } from "drizzle-orm";
import { ApiError } from "~/classes/errors/api-error";
import { Account } from "~/classes/schemas/account";
import { Account as AccountSchema } from "~/classes/schemas/account";
import { config } from "~/packages/config-manager";
import { config } from "~/config.ts";
const route = createRoute({
method: "get",

View file

@ -3,8 +3,8 @@ import { createRoute, z } from "@hono/zod-openapi";
import { Relationship } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { Account as AccountSchema } from "~/classes/schemas/account";
import { zBoolean } from "~/classes/schemas/common.ts";
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
import { zBoolean } from "~/packages/config-manager/config.type";
const route = createRoute({
method: "get",

View file

@ -6,7 +6,7 @@ import { eq, ilike, not, or, sql } from "drizzle-orm";
import stringComparison from "string-comparison";
import { ApiError } from "~/classes/errors/api-error";
import { Account as AccountSchema } from "~/classes/schemas/account";
import { zBoolean } from "~/packages/config-manager/config.type";
import { zBoolean } from "~/classes/schemas/common.ts";
export const route = createRoute({
method: "get",

View file

@ -1,6 +1,6 @@
import { afterAll, describe, expect, test } from "bun:test";
import type { Account as APIAccount } from "@versia/client/types";
import { config } from "~/packages/config-manager/index.ts";
import { config } from "~/config.ts";
import { fakeRequest, getTestUsers } from "~/tests/utils";
const { tokens, deleteUsers } = await getTestUsers(1);

View file

@ -8,8 +8,8 @@ import { and, eq, isNull } from "drizzle-orm";
import { ApiError } from "~/classes/errors/api-error";
import { contentToHtml } from "~/classes/functions/status";
import { Account as AccountSchema } from "~/classes/schemas/account";
import { zBoolean } from "~/packages/config-manager/config.type";
import { config } from "~/packages/config-manager/index.ts";
import { zBoolean } from "~/classes/schemas/common.ts";
import { config } from "~/config.ts";
const route = createRoute({
method: "patch",
@ -62,9 +62,9 @@ const route = createRoute({
.refine(
(v) =>
v.size <=
config.validation
.max_avatar_size,
`Avatar must be less than ${config.validation.max_avatar_size} bytes`,
config.validation.accounts
.max_avatar_bytes,
`Avatar must be less than ${config.validation.accounts.max_avatar_bytes} bytes`,
)
.openapi({
description:
@ -84,9 +84,9 @@ const route = createRoute({
.refine(
(v) =>
v.size <=
config.validation
.max_header_size,
`Header must be less than ${config.validation.max_header_size} bytes`,
config.validation.accounts
.max_header_bytes,
`Header must be less than ${config.validation.accounts.max_header_bytes} bytes`,
)
.openapi({
description:
@ -144,7 +144,9 @@ const route = createRoute({
.element.shape.value,
}),
)
.max(config.validation.max_field_count),
.max(
config.validation.accounts.max_field_count,
),
})
.partial(),
},

View file

@ -2,7 +2,7 @@ import { apiRoute, auth } from "@/api";
import { generateChallenge } from "@/challenges";
import { createRoute, z } from "@hono/zod-openapi";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/packages/config-manager";
import { config } from "~/config.ts";
import { ErrorSchema } from "~/types/api";
const route = createRoute({
@ -45,7 +45,7 @@ const route = createRoute({
export default apiRoute((app) =>
app.openapi(route, async (context) => {
if (!config.validation.challenges.enabled) {
if (!config.validation.challenges) {
throw new ApiError(400, "Challenges are disabled in config");
}

View file

@ -10,7 +10,7 @@ import { createRoute, z } from "@hono/zod-openapi";
import { RolePermissions } from "@versia/kit/tables";
import { ApiError } from "~/classes/errors/api-error";
import { CustomEmoji as CustomEmojiSchema } from "~/classes/schemas/emoji";
import { config } from "~/packages/config-manager";
import { config } from "~/config.ts";
import { ErrorSchema } from "~/types/api";
const schema = z
@ -31,8 +31,8 @@ const schema = z
"Emoji image encoded using multipart/form-data",
})
.refine(
(v) => v.size <= config.validation.max_emoji_size,
`Emoji must be less than ${config.validation.max_emoji_size} bytes`,
(v) => v.size <= config.validation.emojis.max_bytes,
`Emoji must be less than ${config.validation.emojis.max_bytes} bytes`,
),
),
category: CustomEmojiSchema.shape.category.optional(),

View file

@ -6,7 +6,7 @@ import { Emojis, RolePermissions } from "@versia/kit/tables";
import { and, eq, isNull, or } from "drizzle-orm";
import { ApiError } from "~/classes/errors/api-error";
import { CustomEmoji as CustomEmojiSchema } from "~/classes/schemas/emoji";
import { config } from "~/packages/config-manager";
import { config } from "~/config.ts";
const schema = z.object({
shortcode: CustomEmojiSchema.shape.shortcode,
@ -25,8 +25,8 @@ const schema = z.object({
"Emoji image encoded using multipart/form-data",
})
.refine(
(v) => v.size <= config.validation.max_emoji_size,
`Emoji must be less than ${config.validation.max_emoji_size} bytes`,
(v) => v.size <= config.validation.emojis.max_bytes,
`Emoji must be less than ${config.validation.emojis.max_bytes} bytes`,
),
),
category: CustomEmojiSchema.shape.category.optional(),

View file

@ -1,6 +1,6 @@
import { apiRoute } from "@/api";
import { createRoute, z } from "@hono/zod-openapi";
import { config } from "~/packages/config-manager";
import { config } from "~/config.ts";
const route = createRoute({
method: "get",

View file

@ -12,7 +12,7 @@ describe("/api/v1/instance/extended_description", () => {
const json = await response.json();
expect(json).toEqual({
updated_at: new Date(1970, 0, 0).toISOString(),
updated_at: new Date(0).toISOString(),
content:
'<p>This is a <a href="https://versia.pub">Versia</a> server with the default extended description.</p>\n',
});

View file

@ -1,8 +1,8 @@
import { apiRoute } from "@/api";
import { renderMarkdownInPath } from "@/markdown";
import { createRoute } from "@hono/zod-openapi";
import { markdownParse } from "~/classes/functions/status";
import { ExtendedDescription as ExtendedDescriptionSchema } from "~/classes/schemas/extended-description";
import { config } from "~/packages/config-manager";
import { config } from "~/config.ts";
const route = createRoute({
method: "get",
@ -27,14 +27,17 @@ const route = createRoute({
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { content, lastModified } = await renderMarkdownInPath(
config.instance.extended_description_path ?? "",
"This is a [Versia](https://versia.pub) server with the default extended description.",
const content = await markdownParse(
config.instance.extended_description_path?.content ??
"This is a [Versia](https://versia.pub) server with the default extended description.",
);
return context.json(
{
updated_at: lastModified.toISOString(),
updated_at: new Date(
config.instance.extended_description_path?.file
.lastModified ?? 0,
).toISOString(),
content,
},
200,

View file

@ -1,13 +1,13 @@
import { apiRoute, auth } from "@/api";
import { renderMarkdownInPath } from "@/markdown";
import { proxyUrl } from "@/response";
import { createRoute, type z } from "@hono/zod-openapi";
import { Instance, Note, User } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { and, eq, isNull } from "drizzle-orm";
import { markdownParse } from "~/classes/functions/status";
import { InstanceV1 as InstanceV1Schema } from "~/classes/schemas/instance-v1";
import { config } from "~/config.ts";
import manifest from "~/package.json";
import { config } from "~/packages/config-manager";
const route = createRoute({
method: "get",
@ -65,35 +65,38 @@ export default apiRoute((app) =>
}
| undefined;
const { content } = await renderMarkdownInPath(
config.instance.extended_description_path ?? "",
"This is a [Versia](https://versia.pub) server with the default extended description.",
const content = await markdownParse(
config.instance.extended_description_path?.content ??
"This is a [Versia](https://versia.pub) server with the default extended description.",
);
// TODO: fill in more values
return context.json({
approval_required: false,
approval_required: config.registration.require_approval,
configuration: {
polls: {
max_characters_per_option:
config.validation.max_poll_option_size,
max_expiration: config.validation.max_poll_duration,
max_options: config.validation.max_poll_options,
min_expiration: config.validation.min_poll_duration,
config.validation.polls.max_option_characters,
max_expiration:
config.validation.polls.max_duration_seconds,
max_options: config.validation.polls.max_options,
min_expiration:
config.validation.polls.min_duration_seconds,
},
statuses: {
characters_reserved_per_url: 0,
max_characters: config.validation.max_note_size,
max_characters: config.validation.notes.max_characters,
max_media_attachments:
config.validation.max_media_attachments,
config.validation.notes.max_attachments,
},
media_attachments: {
supported_mime_types: config.validation.allowed_mime_types,
image_size_limit: config.validation.max_media_size,
image_matrix_limit: config.validation.max_media_size,
video_size_limit: config.validation.max_media_size,
video_frame_rate_limit: config.validation.max_media_size,
video_matrix_limit: config.validation.max_media_size,
supported_mime_types:
config.validation.media.allowed_mime_types,
image_size_limit: config.validation.media.max_bytes,
// TODO: Implement
image_matrix_limit: 1 ** 10,
video_size_limit: 1 ** 10,
video_frame_rate_limit: 60,
video_matrix_limit: 1 ** 10,
},
accounts: {
max_featured_tags: 100,
@ -101,23 +104,22 @@ export default apiRoute((app) =>
},
short_description: config.instance.description,
description: content,
// TODO: Add contact email
email: "",
email: config.instance.contact.email,
invites_enabled: false,
registrations: config.signups.registration,
// TODO: Implement
languages: ["en"],
rules: config.signups.rules.map((r, index) => ({
registrations: config.registration.allow,
languages: config.instance.languages,
rules: config.instance.rules.map((r, index) => ({
id: String(index),
text: r,
text: r.text,
hint: r.hint,
})),
stats: {
domain_count: knownDomainsCount,
status_count: statusCount,
user_count: userCount,
},
thumbnail: config.instance.logo
? proxyUrl(config.instance.logo).toString()
thumbnail: config.instance.branding.logo
? proxyUrl(config.instance.branding.logo).toString()
: null,
title: config.instance.name,
uri: config.http.base_url.host,

View file

@ -10,7 +10,7 @@ describe("/api/v1/instance/privacy_policy", () => {
const json = await response.json();
expect(json).toEqual({
updated_at: new Date(1970, 0, 0).toISOString(),
updated_at: new Date(0).toISOString(),
// This instance has not provided any privacy policy.
content:
"<p>This instance has not provided any privacy policy.</p>\n",

View file

@ -1,8 +1,8 @@
import { apiRoute, auth } from "@/api";
import { renderMarkdownInPath } from "@/markdown";
import { createRoute } from "@hono/zod-openapi";
import { markdownParse } from "~/classes/functions/status";
import { PrivacyPolicy as PrivacyPolicySchema } from "~/classes/schemas/privacy-policy";
import { config } from "~/packages/config-manager";
import { config } from "~/config.ts";
const route = createRoute({
method: "get",
@ -32,13 +32,15 @@ const route = createRoute({
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { content, lastModified } = await renderMarkdownInPath(
config.instance.privacy_policy_path ?? "",
"This instance has not provided any privacy policy.",
const content = await markdownParse(
config.instance.privacy_policy_path?.content ??
"This instance has not provided any privacy policy.",
);
return context.json({
updated_at: lastModified.toISOString(),
updated_at: new Date(
config.instance.privacy_policy_path?.file.lastModified ?? 0,
).toISOString(),
content,
});
}),

View file

@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { config } from "~/packages/config-manager/index.ts";
import { config } from "~/config.ts";
import { fakeRequest } from "~/tests/utils";
// /api/v1/instance/rules
@ -11,10 +11,10 @@ describe("/api/v1/instance/rules", () => {
const json = await response.json();
expect(json).toEqual(
config.signups.rules.map((rule, index) => ({
config.instance.rules.map((r, index) => ({
id: String(index),
text: rule,
hint: "",
text: r.text,
hint: r.hint,
})),
);
});

View file

@ -1,7 +1,7 @@
import { apiRoute, auth } from "@/api";
import { createRoute, z } from "@hono/zod-openapi";
import { Rule as RuleSchema } from "~/classes/schemas/rule";
import { config } from "~/packages/config-manager";
import { config } from "~/config.ts";
const route = createRoute({
method: "get",
@ -32,10 +32,10 @@ const route = createRoute({
export default apiRoute((app) =>
app.openapi(route, (context) => {
return context.json(
config.signups.rules.map((rule, index) => ({
config.instance.rules.map((r, index) => ({
id: String(index),
text: rule,
hint: "",
text: r.text,
hint: r.hint,
})),
);
}),

View file

@ -10,7 +10,7 @@ describe("/api/v1/instance/terms_of_service", () => {
const json = await response.json();
expect(json).toEqual({
updated_at: new Date(1970, 0, 0).toISOString(),
updated_at: new Date(0).toISOString(),
// This instance has not provided any terms of service.
content:
"<p>This instance has not provided any terms of service.</p>\n",

View file

@ -1,8 +1,8 @@
import { apiRoute, auth } from "@/api";
import { renderMarkdownInPath } from "@/markdown";
import { createRoute } from "@hono/zod-openapi";
import { markdownParse } from "~/classes/functions/status";
import { TermsOfService as TermsOfServiceSchema } from "~/classes/schemas/tos";
import { config } from "~/packages/config-manager";
import { config } from "~/config.ts";
const route = createRoute({
method: "get",
@ -33,13 +33,15 @@ const route = createRoute({
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { content, lastModified } = await renderMarkdownInPath(
config.instance.tos_path ?? "",
"This instance has not provided any terms of service.",
const content = await markdownParse(
config.instance.tos_path?.content ??
"This instance has not provided any terms of service.",
);
return context.json({
updated_at: lastModified.toISOString(),
updated_at: new Date(
config.instance.tos_path?.file.lastModified ?? 0,
).toISOString(),
content,
});
}),

View file

@ -4,8 +4,8 @@ import { Timeline } from "@versia/kit/db";
import { Notifications, RolePermissions } from "@versia/kit/tables";
import { and, eq, gt, gte, inArray, lt, not, sql } from "drizzle-orm";
import { Account as AccountSchema } from "~/classes/schemas/account";
import { zBoolean } from "~/classes/schemas/common.ts";
import { Notification as NotificationSchema } from "~/classes/schemas/notification.ts";
import { zBoolean } from "~/packages/config-manager/config.type";
const route = createRoute({
method: "get",

View file

@ -1,7 +1,7 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { Role } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { config } from "~/packages/config-manager/index.ts";
import { config } from "~/config.ts";
import { fakeRequest, getTestUsers } from "~/tests/utils";
const { users, deleteUsers, tokens } = await getTestUsers(1);

View file

@ -11,13 +11,13 @@ import { Media } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { ApiError } from "~/classes/errors/api-error";
import { Attachment as AttachmentSchema } from "~/classes/schemas/attachment";
import { zBoolean } from "~/classes/schemas/common.ts";
import { PollOption } from "~/classes/schemas/poll";
import {
Status as StatusSchema,
StatusSource as StatusSourceSchema,
} from "~/classes/schemas/status";
import { zBoolean } from "~/packages/config-manager/config.type";
import { config } from "~/packages/config-manager/index.ts";
import { config } from "~/config.ts";
const schema = z
.object({
@ -35,7 +35,7 @@ const schema = z
}),
media_ids: z
.array(AttachmentSchema.shape.id)
.max(config.validation.max_media_attachments)
.max(config.validation.notes.max_attachments)
.default([])
.openapi({
description:
@ -51,7 +51,7 @@ const schema = z
language: StatusSchema.shape.language.optional(),
"poll[options]": z
.array(PollOption.shape.title)
.max(config.validation.max_poll_options)
.max(config.validation.polls.max_options)
.optional()
.openapi({
description:
@ -60,8 +60,8 @@ const schema = z
"poll[expires_in]": z.coerce
.number()
.int()
.min(config.validation.min_poll_duration)
.max(config.validation.max_poll_duration)
.min(config.validation.polls.min_duration_seconds)
.max(config.validation.polls.max_duration_seconds)
.optional()
.openapi({
description:

View file

@ -3,7 +3,7 @@ import type { Status as ApiStatus } from "@versia/client/types";
import { Media, db } from "@versia/kit/db";
import { Emojis } from "@versia/kit/tables";
import { eq } from "drizzle-orm";
import { config } from "~/packages/config-manager/index.ts";
import { config } from "~/config.ts";
import { fakeRequest, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(5);
@ -61,7 +61,7 @@ describe("/api/v1/statuses", () => {
Authorization: `Bearer ${tokens[0].data.accessToken}`,
},
body: new URLSearchParams({
status: "a".repeat(config.validation.max_note_size + 1),
status: "a".repeat(config.validation.notes.max_characters + 1),
local_only: "true",
}),
});

View file

@ -4,13 +4,13 @@ import { Media, Note } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables";
import { ApiError } from "~/classes/errors/api-error";
import { Attachment as AttachmentSchema } from "~/classes/schemas/attachment";
import { zBoolean } from "~/classes/schemas/common.ts";
import { PollOption } from "~/classes/schemas/poll";
import {
Status as StatusSchema,
StatusSource as StatusSourceSchema,
} from "~/classes/schemas/status";
import { zBoolean } from "~/packages/config-manager/config.type";
import { config } from "~/packages/config-manager/index.ts";
import { config } from "~/config.ts";
const schema = z
.object({
@ -28,7 +28,7 @@ const schema = z
}),
media_ids: z
.array(AttachmentSchema.shape.id)
.max(config.validation.max_media_attachments)
.max(config.validation.notes.max_attachments)
.default([])
.openapi({
description:
@ -44,7 +44,7 @@ const schema = z
language: StatusSchema.shape.language.optional(),
"poll[options]": z
.array(PollOption.shape.title)
.max(config.validation.max_poll_options)
.max(config.validation.polls.max_options)
.optional()
.openapi({
description:
@ -53,8 +53,8 @@ const schema = z
"poll[expires_in]": z.coerce
.number()
.int()
.min(config.validation.min_poll_duration)
.max(config.validation.max_poll_duration)
.min(config.validation.polls.min_duration_seconds)
.max(config.validation.polls.max_duration_seconds)
.optional()
.openapi({
description:

View file

@ -1,6 +1,6 @@
import { afterAll, describe, expect, test } from "bun:test";
import type { Status as ApiStatus } from "@versia/client/types";
import { config } from "~/packages/config-manager/index.ts";
import { config } from "~/config.ts";
import { fakeRequest, getTestStatuses, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(5);

View file

@ -1,6 +1,6 @@
import { afterAll, describe, expect, test } from "bun:test";
import type { Status as ApiStatus } from "@versia/client/types";
import { config } from "~/packages/config-manager/index.ts";
import { config } from "~/config.ts";
import { fakeRequest, getTestStatuses, getTestUsers } from "~/tests/utils";
const { users, tokens, deleteUsers } = await getTestUsers(5);

View file

@ -3,8 +3,8 @@ import { createRoute, z } from "@hono/zod-openapi";
import { Timeline } from "@versia/kit/db";
import { Notes, RolePermissions } from "@versia/kit/tables";
import { and, eq, gt, gte, inArray, lt, or, sql } from "drizzle-orm";
import { zBoolean } from "~/classes/schemas/common.ts";
import { Status as StatusSchema } from "~/classes/schemas/status";
import { zBoolean } from "~/packages/config-manager/config.type";
const route = createRoute({
method: "get",

View file

@ -4,11 +4,11 @@ import { db } from "@versia/kit/db";
import { FilterKeywords, Filters, RolePermissions } from "@versia/kit/tables";
import { type SQL, and, eq, inArray } from "drizzle-orm";
import { ApiError } from "~/classes/errors/api-error";
import { zBoolean } from "~/classes/schemas/common.ts";
import {
FilterKeyword as FilterKeywordSchema,
Filter as FilterSchema,
} from "~/classes/schemas/filters";
import { zBoolean } from "~/packages/config-manager/config.type";
import { ErrorSchema } from "~/types/api";
const routeGet = createRoute({

View file

@ -5,8 +5,8 @@ import { User } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { and, eq, isNull } from "drizzle-orm";
import { Instance as InstanceSchema } from "~/classes/schemas/instance";
import { config } from "~/config.ts";
import pkg from "~/package.json";
import { config } from "~/packages/config-manager";
const route = createRoute({
method: "get",
@ -69,92 +69,99 @@ export default apiRoute((app) =>
mastodon: 1,
},
thumbnail: {
url: config.instance.logo
? proxyUrl(config.instance.logo).toString()
url: config.instance.branding.logo
? proxyUrl(config.instance.branding.logo).toString()
: pkg.icon,
},
banner: {
url: config.instance.banner
? proxyUrl(config.instance.banner).toString()
url: config.instance.branding.banner
? proxyUrl(config.instance.branding.banner).toString()
: null,
},
icon: [],
languages: ["en"],
languages: config.instance.languages,
configuration: {
urls: {
// TODO: Implement Streaming API
streaming: "",
},
vapid: {
// TODO: Fill in vapid values
public_key: "",
public_key:
config.notifications.push?.vapid_keys.public ?? "",
},
accounts: {
max_featured_tags: 100,
max_displayname_characters:
config.validation.max_displayname_size,
avatar_limit: config.validation.max_avatar_size,
header_limit: config.validation.max_header_size,
config.validation.accounts.max_displayname_characters,
avatar_limit: config.validation.accounts.max_avatar_bytes,
header_limit: config.validation.accounts.max_header_bytes,
max_username_characters:
config.validation.max_username_size,
max_note_characters: config.validation.max_bio_size,
max_pinned_statuses: 100,
config.validation.accounts.max_username_characters,
max_note_characters:
config.validation.accounts.max_bio_characters,
max_pinned_statuses:
config.validation.accounts.max_pinned_notes,
fields: {
max_fields: config.validation.max_field_count,
max_fields: config.validation.accounts.max_field_count,
max_name_characters:
config.validation.max_field_name_size,
config.validation.accounts
.max_field_name_characters,
max_value_characters:
config.validation.max_field_value_size,
config.validation.accounts
.max_field_value_characters,
},
},
statuses: {
max_characters: config.validation.max_note_size,
max_characters: config.validation.notes.max_characters,
max_media_attachments:
config.validation.max_media_attachments,
characters_reserved_per_url: 0,
config.validation.notes.max_attachments,
// TODO: Implement
characters_reserved_per_url: 13,
},
media_attachments: {
supported_mime_types: config.validation.allowed_mime_types,
image_size_limit: config.validation.max_media_size,
image_matrix_limit: config.validation.max_media_size,
video_size_limit: config.validation.max_media_size,
video_frame_rate_limit: config.validation.max_media_size,
video_matrix_limit: config.validation.max_media_size,
supported_mime_types:
config.validation.media.allowed_mime_types,
image_size_limit: config.validation.media.max_bytes,
image_matrix_limit: 1 ** 10,
video_size_limit: 1 ** 10,
video_frame_rate_limit: 60,
video_matrix_limit: 1 ** 10,
description_limit:
config.validation.max_media_description_size,
config.validation.media.max_description_characters,
},
emojis: {
emoji_size_limit: config.validation.max_emoji_size,
emoji_size_limit: config.validation.emojis.max_bytes,
max_shortcode_characters:
config.validation.max_emoji_shortcode_size,
config.validation.emojis.max_shortcode_characters,
max_description_characters:
config.validation.max_emoji_description_size,
config.validation.emojis.max_description_characters,
},
polls: {
max_characters_per_option:
config.validation.max_poll_option_size,
max_expiration: config.validation.max_poll_duration,
max_options: config.validation.max_poll_options,
min_expiration: config.validation.min_poll_duration,
config.validation.polls.max_option_characters,
max_expiration:
config.validation.polls.max_duration_seconds,
max_options: config.validation.polls.max_options,
min_expiration:
config.validation.polls.min_duration_seconds,
},
translation: {
enabled: false,
},
},
registrations: {
enabled: config.signups.registration,
approval_required: false,
message: null,
enabled: config.registration.allow,
approval_required: config.registration.require_approval,
message: config.registration.message ?? null,
},
contact: {
// TODO: Add contact email
email: "",
email: config.instance.contact.email,
account: (contactAccount as User)?.toApi(),
},
rules: config.signups.rules.map((rule, index) => ({
rules: config.instance.rules.map((r, index) => ({
id: String(index),
text: rule,
hint: "",
text: r.text,
hint: r.hint,
})),
sso: {
forced: oidcConfig?.forced ?? false,

View file

@ -12,10 +12,10 @@ import { and, eq, inArray, isNull, sql } from "drizzle-orm";
import { ApiError } from "~/classes/errors/api-error";
import { Account as AccountSchema } from "~/classes/schemas/account";
import { Id } from "~/classes/schemas/common";
import { zBoolean } from "~/classes/schemas/common.ts";
import { Search as SearchSchema } from "~/classes/schemas/search";
import { searchManager } from "~/classes/search/search-manager";
import { config } from "~/packages/config-manager";
import { zBoolean } from "~/packages/config-manager/config.type";
import { config } from "~/config.ts";
import { ErrorSchema } from "~/types/api";
const route = createRoute({
@ -133,7 +133,7 @@ export default apiRoute((app) =>
);
}
if (!config.sonic.enabled) {
if (!config.search.enabled) {
throw new ApiError(501, "Search is not enabled on this server");
}

View file

@ -3,7 +3,7 @@ import { createRoute, z } from "@hono/zod-openapi";
import { proxy } from "hono/proxy";
import type { ContentfulStatusCode, StatusCode } from "hono/utils/http-status";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/packages/config-manager";
import { config } from "~/config.ts";
import { ErrorSchema } from "~/types/api";
const schemas = {
@ -56,7 +56,7 @@ export default apiRoute((app) =>
const media = await proxy(id, {
// @ts-expect-error Proxy is a Bun-specific feature
proxy: config.http.proxy.address,
proxy: config.http.proxy_address,
});
// Check if file extension ends in svg or svg

View file

@ -8,7 +8,7 @@ import { Like, Note, User } from "@versia/kit/db";
import { Likes, Notes } from "@versia/kit/tables";
import { and, eq, inArray, sql } from "drizzle-orm";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/packages/config-manager";
import { config } from "~/config.ts";
import { ErrorSchema, type KnownEntity } from "~/types/api";
const route = createRoute({

View file

@ -8,7 +8,7 @@ import { Note, User, db } from "@versia/kit/db";
import { Notes } from "@versia/kit/tables";
import { and, eq, inArray } from "drizzle-orm";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/packages/config-manager";
import { config } from "~/config.ts";
import { ErrorSchema } from "~/types/api";
const schemas = {

View file

@ -1,6 +1,6 @@
import { apiRoute } from "@/api";
import { createRoute, z } from "@hono/zod-openapi";
import { config } from "~/packages/config-manager";
import { config } from "~/config.ts";
const route = createRoute({
method: "get",

View file

@ -1,8 +1,8 @@
import { apiRoute } from "@/api";
import { createRoute, z } from "@hono/zod-openapi";
import { Note, User } from "@versia/kit/db";
import { config } from "~/config.ts";
import manifest from "~/package.json";
import { config } from "~/packages/config-manager";
const route = createRoute({
method: "get",
@ -65,7 +65,7 @@ export default apiRoute((app) =>
},
localPosts: noteCount,
},
openRegistrations: config.signups.registration,
openRegistrations: config.registration.allow,
metadata: {
nodeName: config.instance.name,
nodeDescription: config.instance.description,

View file

@ -1,6 +1,6 @@
import { apiRoute } from "@/api";
import { createRoute, z } from "@hono/zod-openapi";
import { config } from "~/packages/config-manager";
import { config } from "~/config.ts";
const route = createRoute({
method: "get",

View file

@ -1,6 +1,6 @@
import { apiRoute } from "@/api";
import { createRoute, z } from "@hono/zod-openapi";
import { config } from "~/packages/config-manager";
import { config } from "~/config.ts";
const route = createRoute({
method: "get",

View file

@ -5,8 +5,8 @@ import { InstanceMetadata as InstanceMetadataSchema } from "@versia/federation/s
import { User } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { asc } from "drizzle-orm";
import { config } from "~/config.ts";
import pkg from "~/package.json";
import { config } from "~/packages/config-manager";
const route = createRoute({
method: "get",
@ -29,6 +29,10 @@ export default apiRoute((app) =>
// Get date of first user creation
const firstUser = await User.fromSql(undefined, asc(Users.createdAt));
const publicKey = Buffer.from(
await crypto.subtle.exportKey("spki", config.instance.keys.public),
).toString("base64");
return context.json(
{
type: "InstanceMetadata" as const,
@ -43,18 +47,18 @@ export default apiRoute((app) =>
name: config.instance.name,
description: config.instance.description,
public_key: {
key: config.instance.keys.public,
key: publicKey,
algorithm: "ed25519" as const,
},
software: {
name: "Versia Server",
version: pkg.version,
},
banner: config.instance.banner
? urlToContentFormat(config.instance.banner)
banner: config.instance.branding.banner
? urlToContentFormat(config.instance.branding.banner)
: undefined,
logo: config.instance.logo
? urlToContentFormat(config.instance.logo)
logo: config.instance.branding.logo
? urlToContentFormat(config.instance.branding.logo)
: undefined,
shared_inbox: new URL(
"/inbox",

View file

@ -12,7 +12,7 @@ import { User } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { and, eq, isNull } from "drizzle-orm";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/packages/config-manager";
import { config } from "~/config.ts";
import { ErrorSchema } from "~/types/api";
const schemas = {
@ -90,7 +90,7 @@ export default apiRoute((app) =>
let activityPubUrl = "";
if (config.federation.bridge.enabled) {
if (config.federation.bridge) {
const manager = await User.getFederationRequester();
try {
@ -98,7 +98,7 @@ export default apiRoute((app) =>
user.data.username,
config.http.base_url.host,
"application/activity+json",
config.federation.bridge.url?.toString(),
config.federation.bridge.url.origin,
);
} catch (e) {
const error = e as ResponseError;
@ -136,7 +136,7 @@ export default apiRoute((app) =>
type:
user.avatar?.getPreferredMimeType() ??
"image/svg+xml",
href: user.getAvatarUrl(config),
href: user.getAvatarUrl(),
},
].filter(Boolean) as {
rel: string;