refactor: 🚚 Organize code into sub-packages, instead of a single large package

This commit is contained in:
Jesse Wierzbinski 2025-06-15 04:38:20 +02:00
parent 79742f47dc
commit a6d3ebbeef
No known key found for this signature in database
366 changed files with 942 additions and 833 deletions

View file

@ -0,0 +1,38 @@
import { config } from "@versia-server/config";
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute } from "@/api";
export default apiRoute((app) =>
app.get(
"/.well-known/host-meta",
describeRoute({
summary: "Well-known host-meta",
tags: ["Federation"],
responses: {
200: {
description: "Host-meta",
content: {
"application/xrd+xml": {
schema: resolver(z.any()),
},
},
},
},
}),
(context) => {
context.header("Content-Type", "application/xrd+xml");
context.status(200);
return context.body(
`<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" template="${new URL(
"/.well-known/webfinger",
config.http.base_url,
).toString()}?resource={uri}"/></XRD>`,
200,
// biome-ignore lint/suspicious/noExplicitAny: Hono doesn't type this response so this has a TS error, it's joever
) as any;
},
),
);

View file

@ -0,0 +1,80 @@
import { Note, User } from "@versia/kit/db";
import { config } from "@versia-server/config";
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute } from "@/api";
import manifest from "~/package.json" with { type: "json" };
export default apiRoute((app) =>
app.get(
"/.well-known/nodeinfo/2.0",
describeRoute({
summary: "Well-known nodeinfo 2.0",
tags: ["Federation"],
responses: {
200: {
description: "Nodeinfo 2.0",
content: {
"application/json": {
schema: resolver(
z.object({
version: z.string(),
software: z.object({
name: z.string(),
version: z.string(),
}),
protocols: z.array(z.string()),
services: z.object({
outbound: z.array(z.string()),
inbound: z.array(z.string()),
}),
usage: z.object({
users: z.object({
total: z.number(),
activeMonth: z.number(),
activeHalfyear: z.number(),
}),
localPosts: z.number(),
}),
openRegistrations: z.boolean(),
metadata: z.object({}),
}),
),
},
},
},
},
}),
async (context) => {
const userCount = await User.getCount();
const userActiveMonth = await User.getActiveInPeriod(
1000 * 60 * 60 * 24 * 30,
);
const userActiveHalfyear = await User.getActiveInPeriod(
1000 * 60 * 60 * 24 * 30 * 6,
);
const noteCount = await Note.getCount();
return context.json({
version: "2.0",
software: { name: "versia-server", version: manifest.version },
protocols: ["versia"],
services: { outbound: [], inbound: [] },
usage: {
users: {
total: userCount,
activeMonth: userActiveMonth,
activeHalfyear: userActiveHalfyear,
},
localPosts: noteCount,
},
openRegistrations: config.registration.allow,
metadata: {
nodeName: config.instance.name,
nodeDescription: config.instance.description,
},
});
},
),
);

View file

@ -0,0 +1,47 @@
import { config } from "@versia-server/config";
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute } from "@/api";
export default apiRoute((app) =>
app.get(
"/.well-known/nodeinfo",
describeRoute({
summary: "Well-known nodeinfo",
tags: ["Federation"],
responses: {
200: {
description: "Nodeinfo links",
content: {
"application/json": {
schema: resolver(
z.object({
links: z.array(
z.object({
rel: z.string(),
href: z.string(),
}),
),
}),
),
},
},
},
},
}),
(context) => {
return context.json({
links: [
{
rel: "http://nodeinfo.diaspora.software/ns/schema/2.0",
href: new URL(
"/.well-known/nodeinfo/2.0",
config.http.base_url,
).toString(),
},
],
});
},
),
);

View file

@ -0,0 +1,66 @@
import { config } from "@versia-server/config";
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute } from "@/api";
export default apiRoute((app) =>
app.get(
"/.well-known/openid-configuration",
describeRoute({
summary: "OpenID Configuration",
tags: ["OpenID"],
responses: {
200: {
description: "OpenID Configuration",
content: {
"application/json": {
schema: resolver(
z.object({
issuer: z.string(),
authorization_endpoint: z.string(),
token_endpoint: z.string(),
userinfo_endpoint: z.string(),
jwks_uri: z.string(),
response_types_supported: z.array(
z.string(),
),
subject_types_supported: z.array(
z.string(),
),
id_token_signing_alg_values_supported:
z.array(z.string()),
scopes_supported: z.array(z.string()),
token_endpoint_auth_methods_supported:
z.array(z.string()),
claims_supported: z.array(z.string()),
}),
),
},
},
},
},
}),
(context) => {
const baseUrl = config.http.base_url;
return context.json(
{
issuer: baseUrl.origin.toString(),
authorization_endpoint: `${baseUrl.origin}/oauth/authorize`,
token_endpoint: `${baseUrl.origin}/oauth/token`,
userinfo_endpoint: `${baseUrl.origin}/api/v1/accounts/verify_credentials`,
jwks_uri: `${baseUrl.origin}/.well-known/jwks`,
response_types_supported: ["code"],
subject_types_supported: ["public"],
id_token_signing_alg_values_supported: ["EdDSA"],
scopes_supported: ["openid", "profile", "email"],
token_endpoint_auth_methods_supported: [
"client_secret_basic",
],
claims_supported: ["sub"],
},
200,
);
},
),
);

View file

@ -0,0 +1,93 @@
import { User } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { InstanceMetadataSchema } from "@versia/sdk/schemas";
import { config } from "@versia-server/config";
import { asc } from "drizzle-orm";
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/zod";
import { apiRoute } from "@/api";
import { urlToContentFormat } from "@/content_types";
import pkg from "~/package.json" with { type: "json" };
export default apiRoute((app) =>
app.get(
"/.well-known/versia",
describeRoute({
summary: "Get instance metadata",
tags: ["Federation"],
responses: {
200: {
description: "Instance metadata",
content: {
"application/json": {
schema: resolver(InstanceMetadataSchema),
},
},
},
},
}),
async (context) => {
// 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,
compatibility: {
extensions: [
"pub.versia:custom_emojis",
"pub.versia:instance_messaging",
"pub.versia:likes",
"pub.versia:shares",
"pub.versia:reactions",
],
versions: ["0.5.0"],
},
host: config.http.base_url.host,
name: config.instance.name,
description: config.instance.description,
public_key: {
key: publicKey,
algorithm: "ed25519" as const,
},
software: {
name: "Versia Server",
version: pkg.version,
},
banner: config.instance.branding.banner
? urlToContentFormat(config.instance.branding.banner)
: undefined,
logo: config.instance.branding.logo
? urlToContentFormat(config.instance.branding.logo)
: undefined,
shared_inbox: new URL(
"/inbox",
config.http.base_url,
).toString(),
created_at: new Date(
firstUser?.data.createdAt ?? 0,
).toISOString(),
extensions: {
"pub.versia:instance_messaging": {
endpoint: new URL(
"/messaging",
config.http.base_url,
).toString(),
},
},
},
200,
);
},
),
);

View file

@ -0,0 +1,144 @@
import { getLogger } from "@logtape/logtape";
import { ApiError } from "@versia/kit";
import { User } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { FederationRequester } from "@versia/sdk/http";
import { WebFingerSchema } from "@versia/sdk/schemas";
import { config } from "@versia-server/config";
import { and, eq, isNull } from "drizzle-orm";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import {
apiRoute,
handleZodError,
idValidator,
parseUserAddress,
webfingerMention,
} from "@/api";
export default apiRoute((app) =>
app.get(
"/.well-known/webfinger",
describeRoute({
summary: "Get user information",
tags: ["Federation"],
responses: {
200: {
description: "User information",
content: {
"application/json": {
schema: resolver(WebFingerSchema),
},
},
},
404: ApiError.accountNotFound().schema,
},
}),
validator(
"query",
z.object({
resource: z
.string()
.trim()
.min(1)
.max(512)
.startsWith("acct:")
.regex(
webfingerMention,
"Invalid resource (should be acct:(id or username)@domain)",
),
}),
handleZodError,
),
async (context) => {
const { resource } = context.req.valid("query");
const requestedUser = resource.split("acct:")[1];
const host = config.http.base_url.host;
const { username, domain } = parseUserAddress(requestedUser);
// Check if user is a local user
if (domain !== host) {
throw new ApiError(
404,
`User domain ${domain} does not match ${host}`,
);
}
const isUuid = username.match(idValidator);
const user = await User.fromSql(
and(
eq(isUuid ? Users.id : Users.username, username),
isNull(Users.instanceId),
),
);
if (!user) {
throw ApiError.accountNotFound();
}
let activityPubUrl: URL | null = null;
if (config.federation.bridge) {
try {
activityPubUrl = await FederationRequester.resolveWebFinger(
user.data.username,
config.http.base_url.host,
"application/activity+json",
config.federation.bridge.url.origin,
);
} catch (e) {
const error = e as ApiError;
getLogger(["federation", "bridge"])
.error`Error from bridge: ${error.message}`;
}
}
return context.json(
{
subject: `acct:${
isUuid ? user.id : user.data.username
}@${host}`,
links: [
// Keep the ActivityPub link first, because Misskey only searches
// for the first link with rel="self" and doesn't check the type.
activityPubUrl
? {
rel: "self",
type: "application/activity+json",
href: activityPubUrl.href,
}
: undefined,
{
rel: "self",
type: "application/json",
href: new URL(
`/users/${user.id}`,
config.http.base_url,
).toString(),
},
{
rel: "avatar",
// Default avatars are SVGs
type:
user.avatar?.getPreferredMimeType() ??
"image/svg+xml",
href: user.getAvatarUrl(),
},
].filter(Boolean) as {
rel: string;
type: string;
href: string;
}[],
},
200,
);
},
),
);