mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 00:18:19 +01:00
Merge pull request #41 from versia-pub/refactor/packages
Refactor/packages
This commit is contained in:
commit
278bf960cb
|
|
@ -1,9 +0,0 @@
|
||||||
# Bun doesn't run well on Musl but this seems to work
|
|
||||||
FROM oven/bun:1.2.15-alpine as base
|
|
||||||
|
|
||||||
# Switch to Bash by editing /etc/passwd
|
|
||||||
RUN apk add --no-cache libstdc++ git bash curl openssh cloc && \
|
|
||||||
sed -i -e 's|/bin/ash|/bin/bash|g' /etc/passwd
|
|
||||||
|
|
||||||
# Extract Node from its docker image (node:22-alpine)
|
|
||||||
COPY --from=node:22-alpine /usr/local/bin/node /usr/local/bin/node
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
{
|
|
||||||
"name": "versia Dev Container",
|
|
||||||
"dockerFile": "Dockerfile",
|
|
||||||
"runArgs": [
|
|
||||||
"-v",
|
|
||||||
"${localWorkspaceFolder}/config:/workspace/config",
|
|
||||||
"-v",
|
|
||||||
"${localWorkspaceFolder}/logs:/workspace/logs",
|
|
||||||
"-v",
|
|
||||||
"${localWorkspaceFolder}/uploads:/workspace/uploads",
|
|
||||||
"--network=host"
|
|
||||||
],
|
|
||||||
"mounts": [
|
|
||||||
"source=node_modules,target=/workspace/node_modules,type=bind,consistency=cached",
|
|
||||||
"type=bind,source=/home/${localEnv:USER}/.ssh,target=/root/.ssh,readonly"
|
|
||||||
],
|
|
||||||
"customizations": {
|
|
||||||
"vscode": {
|
|
||||||
"settings": {
|
|
||||||
"terminal.integrated.shell.linux": "/bin/bash"
|
|
||||||
},
|
|
||||||
"extensions": [
|
|
||||||
"biomejs.biome",
|
|
||||||
"ms-vscode-remote.remote-containers",
|
|
||||||
"oven.bun-vscode",
|
|
||||||
"vivaxy.vscode-conventional-commits",
|
|
||||||
"EditorConfig.EditorConfig",
|
|
||||||
"tamasfe.even-better-toml",
|
|
||||||
"YoavBls.pretty-ts-errors",
|
|
||||||
"eamodio.gitlens"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
25
.github/config.workflow.toml
vendored
25
.github/config.workflow.toml
vendored
|
|
@ -429,31 +429,28 @@ text = "No spam"
|
||||||
|
|
||||||
[logging]
|
[logging]
|
||||||
|
|
||||||
# Available levels: debug, info, warning, error, fatal
|
# Available levels: trace, debug, info, warning, error, fatal
|
||||||
log_level = "debug"
|
log_level = "info" # For console output
|
||||||
|
|
||||||
log_file_path = "logs/versia.log"
|
|
||||||
|
|
||||||
[logging.types]
|
|
||||||
# Either pass a boolean
|
|
||||||
# requests = true
|
|
||||||
# Or a table with the following keys:
|
|
||||||
# requests_content = { level = "debug", log_file_path = "logs/requests.log" }
|
|
||||||
# Available types are: requests, responses, requests_content, filters
|
|
||||||
|
|
||||||
|
# [logging.file]
|
||||||
|
# path = "logs/versia.log"
|
||||||
|
# log_level = "info"
|
||||||
|
#
|
||||||
|
# [logging.file.rotation]
|
||||||
|
# max_size = 10_000_000 # 10 MB
|
||||||
|
# max_files = 10 # Keep 10 rotated files
|
||||||
|
#
|
||||||
# https://sentry.io support
|
# https://sentry.io support
|
||||||
# Uncomment to enable
|
|
||||||
# [logging.sentry]
|
# [logging.sentry]
|
||||||
# Sentry DSN for error logging
|
|
||||||
# dsn = "https://example.com"
|
# 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"
|
||||||
|
# log_level = "info"
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
# Whether to automatically load all plugins in the plugins directory
|
# Whether to automatically load all plugins in the plugins directory
|
||||||
|
|
|
||||||
8
.github/copilot-instructions.md
vendored
8
.github/copilot-instructions.md
vendored
|
|
@ -13,4 +13,10 @@ const add = (a: number, b: number): number => a + b;
|
||||||
|
|
||||||
We always write TypeScript with double quotes and four spaces for indentation, so when your responses include TypeScript code, please follow those conventions.
|
We always write TypeScript with double quotes and four spaces for indentation, so when your responses include TypeScript code, please follow those conventions.
|
||||||
|
|
||||||
Our codebase uses Drizzle as an ORM, with custom abstractions in `classes/database/` for interacting with the database. The `@versia/kit/db` and `@versia/kit/tables` packages are aliases for these abstractions.
|
Our codebase uses Drizzle as an ORM, which is exposed in the `@versia-server/kit/db` and `@versia-server/kit/tables` packages. This project uses a monorepo structure with Bun as the package manager.
|
||||||
|
|
||||||
|
The app has two modes: worker and API. The worker mode is used for background tasks, while the API mode serves HTTP requests. The entry point for the worker is `worker.ts`, and for the API, it is `api.ts`.
|
||||||
|
|
||||||
|
Run the typechecker with `bun run typecheck` to ensure that all TypeScript code is type-checked correctly. Run tests with `bun test` to ensure that all tests pass. Run the linter and formatter with `bun lint` to ensure that the code adheres to our style guidelines, and `bun lint --write` to automatically fix minor/formatting issues.
|
||||||
|
|
||||||
|
Cover all new functionality with tests, and ensure that all tests pass before submitting your code.
|
||||||
|
|
|
||||||
2
.github/workflows/check.yml
vendored
2
.github/workflows/check.yml
vendored
|
|
@ -24,4 +24,4 @@ jobs:
|
||||||
|
|
||||||
- name: Run typechecks
|
- name: Run typechecks
|
||||||
run: |
|
run: |
|
||||||
bun run check
|
bun run typecheck
|
||||||
|
|
|
||||||
27
.github/workflows/circular-imports.yml
vendored
Normal file
27
.github/workflows/circular-imports.yml
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
name: Check Circular Imports
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
|
||||||
|
- name: Install NPM packages
|
||||||
|
run: |
|
||||||
|
bun install
|
||||||
|
|
||||||
|
- name: Run typechecks
|
||||||
|
run: |
|
||||||
|
bun run detect-circular
|
||||||
3
.github/workflows/docker.yml
vendored
3
.github/workflows/docker.yml
vendored
|
|
@ -18,6 +18,9 @@ jobs:
|
||||||
tests:
|
tests:
|
||||||
uses: ./.github/workflows/tests.yml
|
uses: ./.github/workflows/tests.yml
|
||||||
|
|
||||||
|
detect-circular:
|
||||||
|
uses: ./.github/workflows/circular-imports.yml
|
||||||
|
|
||||||
build:
|
build:
|
||||||
if: ${{ success() }}
|
if: ${{ success() }}
|
||||||
needs: [lint, check, tests]
|
needs: [lint, check, tests]
|
||||||
|
|
|
||||||
7
.madgerc
Normal file
7
.madgerc
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"detectiveOptions": {
|
||||||
|
"ts": {
|
||||||
|
"skipTypeImports": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
|
|
||||||
- [x] 🚀 Upgraded Bun to `1.2.15`
|
- [x] 🚀 Upgraded Bun to `1.2.18`
|
||||||
|
|
||||||
# `0.8.0` • Federation 2: Electric Boogaloo
|
# `0.8.0` • Federation 2: Electric Boogaloo
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ TypeScript errors should be ignored with `// @ts-expect-error` comments, as well
|
||||||
|
|
||||||
To scan for all TypeScript errors, run:
|
To scan for all TypeScript errors, run:
|
||||||
```sh
|
```sh
|
||||||
bun check
|
bun typecheck
|
||||||
```
|
```
|
||||||
|
|
||||||
### Commit messages
|
### Commit messages
|
||||||
|
|
@ -153,4 +153,4 @@ If you find a bug, please open an issue on GitHub. Please make sure to include t
|
||||||
|
|
||||||
# License
|
# License
|
||||||
|
|
||||||
Versia Server is licensed under the [AGPLv3 or later](https://www.gnu.org/licenses/agpl-3.0.en.html) license. By contributing to Versia, you agree to license your contributions under the same license.
|
Versia Server is licensed under the [AGPLv3 or later](https://www.gnu.org/licenses/agpl-3.0.en.html) license. By contributing to Versia, you agree to license your contributions under the same license.
|
||||||
|
|
|
||||||
15
Dockerfile
15
Dockerfile
|
|
@ -1,7 +1,5 @@
|
||||||
# Node is required for building the project
|
# Node is required for building the project
|
||||||
FROM imbios/bun-node:1-20-alpine AS base
|
FROM imbios/bun-node:latest-23-alpine AS base
|
||||||
|
|
||||||
RUN apk add --no-cache libstdc++
|
|
||||||
|
|
||||||
# Install dependencies into temp directory
|
# Install dependencies into temp directory
|
||||||
# This will cache them and speed up future builds
|
# This will cache them and speed up future builds
|
||||||
|
|
@ -22,20 +20,19 @@ COPY --from=install /temp/node_modules /temp/node_modules
|
||||||
|
|
||||||
# Build the project
|
# Build the project
|
||||||
WORKDIR /temp
|
WORKDIR /temp
|
||||||
RUN bun run build
|
RUN bun run build api
|
||||||
WORKDIR /temp/dist
|
WORKDIR /temp/dist
|
||||||
|
|
||||||
# Copy production dependencies and source code into final image
|
# Copy production dependencies and source code into final image
|
||||||
FROM oven/bun:1.2.15-alpine
|
FROM oven/bun:1.2.18-alpine
|
||||||
|
|
||||||
# Install libstdc++ for Bun and create app directory
|
# Install libstdc++ for Bun and create app directory
|
||||||
RUN apk add --no-cache libstdc++ && \
|
RUN mkdir -p /app
|
||||||
mkdir -p /app
|
|
||||||
|
|
||||||
COPY --from=build /temp/dist /app/dist
|
COPY --from=build /temp/dist /app/dist
|
||||||
COPY entrypoint.sh /app
|
COPY entrypoint.sh /app
|
||||||
|
|
||||||
LABEL org.opencontainers.image.authors="Gaspard Wierzbinski (https://cpluspatch.dev)"
|
LABEL org.opencontainers.image.authors="Gaspard Wierzbinski (https://cpluspatch.com)"
|
||||||
LABEL org.opencontainers.image.source="https://github.com/versia-pub/server"
|
LABEL org.opencontainers.image.source="https://github.com/versia-pub/server"
|
||||||
LABEL org.opencontainers.image.vendor="Versia Pub"
|
LABEL org.opencontainers.image.vendor="Versia Pub"
|
||||||
LABEL org.opencontainers.image.licenses="AGPL-3.0-or-later"
|
LABEL org.opencontainers.image.licenses="AGPL-3.0-or-later"
|
||||||
|
|
@ -51,4 +48,4 @@ WORKDIR /app
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENTRYPOINT [ "/bin/sh", "/app/entrypoint.sh" ]
|
ENTRYPOINT [ "/bin/sh", "/app/entrypoint.sh" ]
|
||||||
# Run migrations and start the server
|
# Run migrations and start the server
|
||||||
CMD [ "bun", "run", "index.js" ]
|
CMD [ "bun", "run", "api.js" ]
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
# Node is required for building the project
|
# Node is required for building the project
|
||||||
FROM imbios/bun-node:1-20-alpine AS base
|
FROM imbios/bun-node:latest-23-alpine AS base
|
||||||
|
|
||||||
RUN apk add --no-cache libstdc++
|
|
||||||
|
|
||||||
# Install dependencies into temp directory
|
# Install dependencies into temp directory
|
||||||
# This will cache them and speed up future builds
|
# This will cache them and speed up future builds
|
||||||
|
|
@ -22,20 +20,19 @@ COPY --from=install /temp/node_modules /temp/node_modules
|
||||||
|
|
||||||
# Build the project
|
# Build the project
|
||||||
WORKDIR /temp
|
WORKDIR /temp
|
||||||
RUN bun run build:worker
|
RUN bun run build worker
|
||||||
WORKDIR /temp/dist
|
WORKDIR /temp/dist
|
||||||
|
|
||||||
# Copy production dependencies and source code into final image
|
# Copy production dependencies and source code into final image
|
||||||
FROM oven/bun:1.2.15-alpine
|
FROM oven/bun:1.2.18-alpine
|
||||||
|
|
||||||
# Install libstdc++ for Bun and create app directory
|
# Install libstdc++ for Bun and create app directory
|
||||||
RUN apk add --no-cache libstdc++ && \
|
RUN mkdir -p /app
|
||||||
mkdir -p /app
|
|
||||||
|
|
||||||
COPY --from=build /temp/dist /app/dist
|
COPY --from=build /temp/dist /app/dist
|
||||||
COPY entrypoint.sh /app
|
COPY entrypoint.sh /app
|
||||||
|
|
||||||
LABEL org.opencontainers.image.authors="Gaspard Wierzbinski (https://cpluspatch.dev)"
|
LABEL org.opencontainers.image.authors="Gaspard Wierzbinski (https://cpluspatch.com)"
|
||||||
LABEL org.opencontainers.image.source="https://github.com/versia-pub/server"
|
LABEL org.opencontainers.image.source="https://github.com/versia-pub/server"
|
||||||
LABEL org.opencontainers.image.vendor="Versia Pub"
|
LABEL org.opencontainers.image.vendor="Versia Pub"
|
||||||
LABEL org.opencontainers.image.licenses="AGPL-3.0-or-later"
|
LABEL org.opencontainers.image.licenses="AGPL-3.0-or-later"
|
||||||
|
|
@ -47,7 +44,8 @@ ARG GIT_COMMIT
|
||||||
ENV GIT_COMMIT=$GIT_COMMIT
|
ENV GIT_COMMIT=$GIT_COMMIT
|
||||||
|
|
||||||
# CD to app
|
# CD to app
|
||||||
WORKDIR /app/dist
|
WORKDIR /app
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
ENTRYPOINT [ "/bin/sh", "/app/entrypoint.sh" ]
|
||||||
# Run migrations and start the server
|
# Run migrations and start the server
|
||||||
CMD [ "bun", "run", "worker.js" ]
|
CMD [ "bun", "run", "worker.js" ]
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import process from "node:process";
|
import process from "node:process";
|
||||||
|
import { appFactory } from "@versia-server/api";
|
||||||
|
import { config } from "@versia-server/config";
|
||||||
import { Youch } from "youch";
|
import { Youch } from "youch";
|
||||||
import { sentry } from "@/sentry";
|
|
||||||
import { createServer } from "@/server";
|
import { createServer } from "@/server";
|
||||||
import { appFactory } from "~/app";
|
|
||||||
import { config } from "~/config.ts";
|
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
process.on("SIGINT", () => {
|
||||||
process.exit();
|
process.exit();
|
||||||
|
|
@ -15,7 +14,6 @@ process.on("uncaughtException", async (error) => {
|
||||||
console.error(await youch.toANSI(error));
|
console.error(await youch.toANSI(error));
|
||||||
});
|
});
|
||||||
|
|
||||||
await import("~/entrypoints/api/setup.ts");
|
await import("@versia-server/api/setup");
|
||||||
sentry?.captureMessage("Server started", "info");
|
|
||||||
|
|
||||||
createServer(config, await appFactory());
|
createServer(config, await appFactory());
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import type { Status } from "@versia/client/schemas";
|
import type { Status } from "@versia/client/schemas";
|
||||||
|
import {
|
||||||
|
fakeRequest,
|
||||||
|
getTestStatuses,
|
||||||
|
getTestUsers,
|
||||||
|
} from "@versia-server/tests";
|
||||||
import { bench, run } from "mitata";
|
import { bench, run } from "mitata";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod/v4";
|
||||||
import { configureLoggers } from "@/loggers";
|
|
||||||
import { fakeRequest, getTestStatuses, getTestUsers } from "~/tests/utils";
|
|
||||||
|
|
||||||
await configureLoggers(true);
|
|
||||||
|
|
||||||
const { users, tokens, deleteUsers } = await getTestUsers(5);
|
const { users, tokens, deleteUsers } = await getTestUsers(5);
|
||||||
await getTestStatuses(40, users[0]);
|
await getTestStatuses(40, users[0]);
|
||||||
|
|
|
||||||
19
biome.json
19
biome.json
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/2.0.0-beta.5/schema.json",
|
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
|
||||||
"assist": {
|
"assist": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"source": {
|
"source": {
|
||||||
|
|
@ -49,7 +49,6 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"useLiteralEnumMembers": "error",
|
"useLiteralEnumMembers": "error",
|
||||||
"noCommaOperator": "error",
|
|
||||||
"noNegationElse": "error",
|
"noNegationElse": "error",
|
||||||
"noYodaExpression": "error",
|
"noYodaExpression": "error",
|
||||||
"useBlockStatements": "error",
|
"useBlockStatements": "error",
|
||||||
|
|
@ -89,7 +88,6 @@
|
||||||
"accessibility": "explicit"
|
"accessibility": "explicit"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"noArguments": "error",
|
|
||||||
"useImportType": "error",
|
"useImportType": "error",
|
||||||
"useExportType": "error",
|
"useExportType": "error",
|
||||||
"noUselessElse": "error",
|
"noUselessElse": "error",
|
||||||
|
|
@ -99,7 +97,15 @@
|
||||||
"noCommonJs": "warn",
|
"noCommonJs": "warn",
|
||||||
"noExportedImports": "warn",
|
"noExportedImports": "warn",
|
||||||
"noSubstr": "warn",
|
"noSubstr": "warn",
|
||||||
"useTrimStartEnd": "warn"
|
"useTrimStartEnd": "warn",
|
||||||
|
"noRestrictedImports": {
|
||||||
|
"options": {
|
||||||
|
"paths": {
|
||||||
|
"~/packages/": "Use the appropriate package instead of importing from the packages directory directly."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"level": "error"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"performance": {
|
"performance": {
|
||||||
"noDynamicNamespaceImportAccess": "warn"
|
"noDynamicNamespaceImportAccess": "warn"
|
||||||
|
|
@ -119,6 +125,7 @@
|
||||||
"noGlobalDirnameFilename": "error",
|
"noGlobalDirnameFilename": "error",
|
||||||
"noProcessGlobal": "warn",
|
"noProcessGlobal": "warn",
|
||||||
"noTsIgnore": "warn",
|
"noTsIgnore": "warn",
|
||||||
|
"useReadonlyClassProperties": "error",
|
||||||
"useConsistentObjectDefinition": {
|
"useConsistentObjectDefinition": {
|
||||||
"level": "warn",
|
"level": "warn",
|
||||||
"options": {
|
"options": {
|
||||||
|
|
@ -135,7 +142,9 @@
|
||||||
"noUselessEscapeInRegex": "warn",
|
"noUselessEscapeInRegex": "warn",
|
||||||
"useSimplifiedLogicExpression": "error",
|
"useSimplifiedLogicExpression": "error",
|
||||||
"useWhile": "error",
|
"useWhile": "error",
|
||||||
"useNumericLiterals": "error"
|
"useNumericLiterals": "error",
|
||||||
|
"noArguments": "error",
|
||||||
|
"noCommaOperator": "error"
|
||||||
},
|
},
|
||||||
"suspicious": {
|
"suspicious": {
|
||||||
"noDuplicateTestHooks": "error",
|
"noDuplicateTestHooks": "error",
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import { $, build } from "bun";
|
|
||||||
|
|
||||||
console.log("Building...");
|
|
||||||
|
|
||||||
await $`rm -rf dist && mkdir dist`;
|
|
||||||
|
|
||||||
await build({
|
|
||||||
entrypoints: [
|
|
||||||
"worker.ts",
|
|
||||||
// HACK: Include to avoid cyclical import errors
|
|
||||||
"config.ts",
|
|
||||||
],
|
|
||||||
outdir: "dist",
|
|
||||||
target: "bun",
|
|
||||||
splitting: true,
|
|
||||||
minify: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Copying files...");
|
|
||||||
|
|
||||||
// Copy Drizzle migrations to dist
|
|
||||||
await $`cp -rL drizzle dist/drizzle`;
|
|
||||||
|
|
||||||
// Copy Sharp to dist
|
|
||||||
await $`mkdir -p dist/node_modules/@img`;
|
|
||||||
await $`cp -rL node_modules/@img/sharp-libvips-linux* dist/node_modules/@img`;
|
|
||||||
await $`cp -rL node_modules/@img/sharp-linux* dist/node_modules/@img`;
|
|
||||||
|
|
||||||
await $`cp -rL node_modules/detect-libc dist/node_modules/`;
|
|
||||||
|
|
||||||
console.log("Build complete!");
|
|
||||||
84
build.ts
84
build.ts
|
|
@ -1,63 +1,55 @@
|
||||||
import { readdir } from "node:fs/promises";
|
import process from "node:process";
|
||||||
import { $, build } from "bun";
|
import { $, build, file, write } from "bun";
|
||||||
import { routes } from "~/routes";
|
import manifest from "./package.json" with { type: "json" };
|
||||||
|
|
||||||
console.log("Building...");
|
console.log("Building...");
|
||||||
|
|
||||||
await $`rm -rf dist && mkdir dist`;
|
await $`rm -rf dist && mkdir dist`;
|
||||||
|
|
||||||
// Get all directories under the plugins/ directory
|
const type = process.argv[2] as "api" | "worker";
|
||||||
const pluginDirs = await readdir("plugins", { withFileTypes: true });
|
|
||||||
|
if (type !== "api" && type !== "worker") {
|
||||||
|
throw new Error("Invalid build type. Use 'api' or 'worker'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const packages = Object.keys(manifest.dependencies)
|
||||||
|
.filter((dep) => dep.startsWith("@versia"))
|
||||||
|
.filter((dep) => dep !== "@versia-server/tests");
|
||||||
|
|
||||||
await build({
|
await build({
|
||||||
entrypoints: [
|
entrypoints: [`./${type}.ts`],
|
||||||
"index.ts",
|
|
||||||
// HACK: Include to avoid cyclical import errors
|
|
||||||
"config.ts",
|
|
||||||
"cli/index.ts",
|
|
||||||
// Force Bun to include endpoints
|
|
||||||
...Object.values(routes),
|
|
||||||
// Include all plugins
|
|
||||||
...pluginDirs
|
|
||||||
.filter((dir) => dir.isDirectory())
|
|
||||||
.map((dir) => `plugins/${dir.name}/index.ts`),
|
|
||||||
],
|
|
||||||
outdir: "dist",
|
outdir: "dist",
|
||||||
target: "bun",
|
target: "bun",
|
||||||
splitting: true,
|
splitting: true,
|
||||||
minify: false,
|
minify: true,
|
||||||
external: ["acorn", "@bull-board/ui"],
|
external: [...packages],
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Copying files...");
|
console.log("Copying files...");
|
||||||
|
|
||||||
// Copy Drizzle migrations to dist
|
// Copy each package into dist/node_modules
|
||||||
await $`cp -r drizzle dist/drizzle`;
|
for (const pkg of packages) {
|
||||||
|
const directory = pkg.split("/")[1] || pkg;
|
||||||
|
await $`mkdir -p dist/node_modules/${pkg}`;
|
||||||
|
// Copy the built package files
|
||||||
|
await $`cp -rL packages/${directory}/{dist,package.json} dist/node_modules/${pkg}`;
|
||||||
|
|
||||||
// Copy plugin manifests
|
// Rewrite package.json "exports" field to point to the dist directory and use .js extension
|
||||||
await $`cp plugins/openid/manifest.json dist/plugins/openid/manifest.json`;
|
const packageJsonPath = `dist/node_modules/${pkg}/package.json`;
|
||||||
|
const packageJson = await file(packageJsonPath).json();
|
||||||
await $`mkdir -p dist/node_modules`;
|
for (const [key, value] of Object.entries(packageJson.exports) as [
|
||||||
|
string,
|
||||||
// Copy Sharp to dist
|
{ import?: string },
|
||||||
await $`mkdir -p dist/node_modules/@img`;
|
][]) {
|
||||||
await $`cp -rL node_modules/@img/sharp-libvips-linux* dist/node_modules/@img`;
|
if (value.import) {
|
||||||
await $`cp -rL node_modules/@img/sharp-linux* dist/node_modules/@img`;
|
packageJson.exports[key] = {
|
||||||
|
import: value.import
|
||||||
// Copy acorn to dist
|
.replace("./", "./dist/")
|
||||||
await $`cp -rL node_modules/acorn dist/node_modules/acorn`;
|
.replace(/\.ts$/, ".js"),
|
||||||
|
};
|
||||||
// Copy bull-board to dist
|
}
|
||||||
await $`mkdir -p dist/node_modules/@bull-board`;
|
}
|
||||||
await $`cp -rL node_modules/@bull-board/ui dist/node_modules/@bull-board/ui`;
|
await write(packageJsonPath, JSON.stringify(packageJson, null, 4));
|
||||||
|
}
|
||||||
// Copy the Bee Movie script from pages
|
|
||||||
await $`cp beemovie.txt dist/beemovie.txt`;
|
|
||||||
|
|
||||||
// Copy package.json
|
|
||||||
await $`cp package.json dist/package.json`;
|
|
||||||
|
|
||||||
// Fixes issues with sharp
|
|
||||||
await $`cp -rL node_modules/detect-libc dist/node_modules/`;
|
|
||||||
|
|
||||||
console.log("Build complete!");
|
console.log("Build complete!");
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,4 @@
|
||||||
"@jsr" = "https://npm.jsr.io"
|
"@jsr" = "https://npm.jsr.io"
|
||||||
|
|
||||||
[test]
|
[test]
|
||||||
preload = ["./tests/setup.ts"]
|
preload = ["./packages/tests/setup.ts"]
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
||||||
|
|
||||||
await import("~/config.ts");
|
|
||||||
|
|
||||||
// This is an awkward way to avoid import cycles for some reason
|
|
||||||
await (async () => {
|
|
||||||
const { ConfigSchema } = await import("./schema.ts");
|
|
||||||
|
|
||||||
const jsonSchema = zodToJsonSchema(ConfigSchema, {});
|
|
||||||
|
|
||||||
console.write(`${JSON.stringify(jsonSchema, null, 4)}\n`);
|
|
||||||
})();
|
|
||||||
|
|
@ -1,359 +0,0 @@
|
||||||
import markdownItTaskLists from "@hackmd/markdown-it-task-lists";
|
|
||||||
import { db, type Note, User } from "@versia/kit/db";
|
|
||||||
import { Instances, Users } from "@versia/kit/tables";
|
|
||||||
import { FederationRequester } from "@versia/sdk/http";
|
|
||||||
import { and, eq, inArray, isNull, or, sql } from "drizzle-orm";
|
|
||||||
import linkifyHtml from "linkify-html";
|
|
||||||
import {
|
|
||||||
anyOf,
|
|
||||||
charIn,
|
|
||||||
createRegExp,
|
|
||||||
digit,
|
|
||||||
exactly,
|
|
||||||
global,
|
|
||||||
letter,
|
|
||||||
} from "magic-regexp";
|
|
||||||
import MarkdownIt from "markdown-it";
|
|
||||||
import markdownItContainer from "markdown-it-container";
|
|
||||||
import markdownItTocDoneRight from "markdown-it-toc-done-right";
|
|
||||||
import { mentionValidator } from "@/api";
|
|
||||||
import { sanitizeHtml, sanitizeHtmlInline } from "@/sanitization";
|
|
||||||
import { config } from "~/config.ts";
|
|
||||||
import type * as VersiaEntities from "~/packages/sdk/entities/index.ts";
|
|
||||||
import { transformOutputToUserWithRelations, userRelations } from "./user.ts";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper against the Status object to make it easier to work with
|
|
||||||
* @param query
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const findManyNotes = async (
|
|
||||||
query: Parameters<typeof db.query.Notes.findMany>[0],
|
|
||||||
userId?: string,
|
|
||||||
): Promise<(typeof Note.$type)[]> => {
|
|
||||||
const output = await db.query.Notes.findMany({
|
|
||||||
...query,
|
|
||||||
with: {
|
|
||||||
...query?.with,
|
|
||||||
attachments: {
|
|
||||||
with: {
|
|
||||||
media: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
reactions: {
|
|
||||||
with: {
|
|
||||||
emoji: {
|
|
||||||
with: {
|
|
||||||
instance: true,
|
|
||||||
media: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emojis: {
|
|
||||||
with: {
|
|
||||||
emoji: {
|
|
||||||
with: {
|
|
||||||
instance: true,
|
|
||||||
media: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
author: {
|
|
||||||
with: {
|
|
||||||
...userRelations,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mentions: {
|
|
||||||
with: {
|
|
||||||
user: {
|
|
||||||
with: {
|
|
||||||
instance: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
reblog: {
|
|
||||||
with: {
|
|
||||||
attachments: {
|
|
||||||
with: {
|
|
||||||
media: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
reactions: {
|
|
||||||
with: {
|
|
||||||
emoji: {
|
|
||||||
with: {
|
|
||||||
instance: true,
|
|
||||||
media: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emojis: {
|
|
||||||
with: {
|
|
||||||
emoji: {
|
|
||||||
with: {
|
|
||||||
instance: true,
|
|
||||||
media: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
likes: true,
|
|
||||||
application: true,
|
|
||||||
mentions: {
|
|
||||||
with: {
|
|
||||||
user: {
|
|
||||||
with: userRelations,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
author: {
|
|
||||||
with: {
|
|
||||||
...userRelations,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
extras: {
|
|
||||||
pinned: userId
|
|
||||||
? sql`EXISTS (SELECT 1 FROM "UserToPinnedNotes" WHERE "UserToPinnedNotes"."noteId" = "Notes_reblog".id AND "UserToPinnedNotes"."userId" = ${userId})`.as(
|
|
||||||
"pinned",
|
|
||||||
)
|
|
||||||
: sql`false`.as("pinned"),
|
|
||||||
reblogged: userId
|
|
||||||
? sql`EXISTS (SELECT 1 FROM "Notes" WHERE "Notes"."authorId" = ${userId} AND "Notes"."reblogId" = "Notes_reblog".id)`.as(
|
|
||||||
"reblogged",
|
|
||||||
)
|
|
||||||
: sql`false`.as("reblogged"),
|
|
||||||
muted: userId
|
|
||||||
? sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."ownerId" = ${userId} AND "Relationships"."subjectId" = "Notes_reblog"."authorId" AND "Relationships"."muting" = true)`.as(
|
|
||||||
"muted",
|
|
||||||
)
|
|
||||||
: sql`false`.as("muted"),
|
|
||||||
liked: userId
|
|
||||||
? sql`EXISTS (SELECT 1 FROM "Likes" WHERE "Likes"."likedId" = "Notes_reblog".id AND "Likes"."likerId" = ${userId})`.as(
|
|
||||||
"liked",
|
|
||||||
)
|
|
||||||
: sql`false`.as("liked"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
reply: true,
|
|
||||||
quote: true,
|
|
||||||
},
|
|
||||||
extras: {
|
|
||||||
pinned: userId
|
|
||||||
? sql`EXISTS (SELECT 1 FROM "UserToPinnedNotes" WHERE "UserToPinnedNotes"."noteId" = "Notes".id AND "UserToPinnedNotes"."userId" = ${userId})`.as(
|
|
||||||
"pinned",
|
|
||||||
)
|
|
||||||
: sql`false`.as("pinned"),
|
|
||||||
reblogged: userId
|
|
||||||
? sql`EXISTS (SELECT 1 FROM "Notes" WHERE "Notes"."authorId" = ${userId} AND "Notes"."reblogId" = "Notes".id)`.as(
|
|
||||||
"reblogged",
|
|
||||||
)
|
|
||||||
: sql`false`.as("reblogged"),
|
|
||||||
muted: userId
|
|
||||||
? sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."ownerId" = ${userId} AND "Relationships"."subjectId" = "Notes"."authorId" AND "Relationships"."muting" = true)`.as(
|
|
||||||
"muted",
|
|
||||||
)
|
|
||||||
: sql`false`.as("muted"),
|
|
||||||
liked: userId
|
|
||||||
? sql`EXISTS (SELECT 1 FROM "Likes" WHERE "Likes"."likedId" = "Notes".id AND "Likes"."likerId" = ${userId})`.as(
|
|
||||||
"liked",
|
|
||||||
)
|
|
||||||
: sql`false`.as("liked"),
|
|
||||||
...query?.extras,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return output.map((post) => ({
|
|
||||||
...post,
|
|
||||||
author: transformOutputToUserWithRelations(post.author),
|
|
||||||
mentions: post.mentions.map((mention) => ({
|
|
||||||
...mention.user,
|
|
||||||
endpoints: mention.user.endpoints,
|
|
||||||
})),
|
|
||||||
attachments: post.attachments.map((attachment) => attachment.media),
|
|
||||||
emojis: (post.emojis ?? []).map((emoji) => emoji.emoji),
|
|
||||||
reblog: post.reblog && {
|
|
||||||
...post.reblog,
|
|
||||||
author: transformOutputToUserWithRelations(post.reblog.author),
|
|
||||||
mentions: post.reblog.mentions.map((mention) => ({
|
|
||||||
...mention.user,
|
|
||||||
endpoints: mention.user.endpoints,
|
|
||||||
})),
|
|
||||||
attachments: post.reblog.attachments.map(
|
|
||||||
(attachment) => attachment.media,
|
|
||||||
),
|
|
||||||
emojis: (post.reblog.emojis ?? []).map((emoji) => emoji.emoji),
|
|
||||||
pinned: Boolean(post.reblog.pinned),
|
|
||||||
reblogged: Boolean(post.reblog.reblogged),
|
|
||||||
muted: Boolean(post.reblog.muted),
|
|
||||||
liked: Boolean(post.reblog.liked),
|
|
||||||
},
|
|
||||||
pinned: Boolean(post.pinned),
|
|
||||||
reblogged: Boolean(post.reblogged),
|
|
||||||
muted: Boolean(post.muted),
|
|
||||||
liked: Boolean(post.liked),
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get people mentioned in the content (match @username or @username@domain.com mentions)
|
|
||||||
* @param text The text to parse mentions from.
|
|
||||||
* @returns An array of users mentioned in the text.
|
|
||||||
*/
|
|
||||||
export const parseTextMentions = async (text: string): Promise<User[]> => {
|
|
||||||
const mentionedPeople = [...text.matchAll(mentionValidator)];
|
|
||||||
if (mentionedPeople.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const baseUrlHost = config.http.base_url.host;
|
|
||||||
const isLocal = (host?: string): boolean => host === baseUrlHost || !host;
|
|
||||||
|
|
||||||
// Find local and matching users
|
|
||||||
const foundUsers = await db
|
|
||||||
.select({
|
|
||||||
id: Users.id,
|
|
||||||
username: Users.username,
|
|
||||||
baseUrl: Instances.baseUrl,
|
|
||||||
})
|
|
||||||
.from(Users)
|
|
||||||
.leftJoin(Instances, eq(Users.instanceId, Instances.id))
|
|
||||||
.where(
|
|
||||||
or(
|
|
||||||
...mentionedPeople.map((person) =>
|
|
||||||
and(
|
|
||||||
eq(Users.username, person[1] ?? ""),
|
|
||||||
isLocal(person[2])
|
|
||||||
? isNull(Users.instanceId)
|
|
||||||
: eq(Instances.baseUrl, person[2] ?? ""),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Separate found and unresolved users
|
|
||||||
const finalList = await User.manyFromSql(
|
|
||||||
inArray(
|
|
||||||
Users.id,
|
|
||||||
foundUsers.map((u) => u.id),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Every remote user that isn't in database
|
|
||||||
const notFoundRemoteUsers = mentionedPeople.filter(
|
|
||||||
(p) =>
|
|
||||||
!(
|
|
||||||
foundUsers.some(
|
|
||||||
(user) => user.username === p[1] && user.baseUrl === p[2],
|
|
||||||
) || isLocal(p[2])
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Resolve remote mentions not in database
|
|
||||||
for (const person of notFoundRemoteUsers) {
|
|
||||||
const url = await FederationRequester.resolveWebFinger(
|
|
||||||
person[1] ?? "",
|
|
||||||
person[2] ?? "",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (url) {
|
|
||||||
const user = await User.resolve(url);
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
finalList.push(user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return finalList;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const replaceTextMentions = (text: string, mentions: User[]): string => {
|
|
||||||
return mentions.reduce((finalText, mention) => {
|
|
||||||
const { username, instance } = mention.data;
|
|
||||||
const { uri } = mention;
|
|
||||||
const baseHost = config.http.base_url.host;
|
|
||||||
const linkTemplate = (displayText: string): string =>
|
|
||||||
`<a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href="${uri}">${displayText}</a>`;
|
|
||||||
|
|
||||||
if (mention.remote) {
|
|
||||||
return finalText.replaceAll(
|
|
||||||
`@${username}@${instance?.baseUrl}`,
|
|
||||||
linkTemplate(`@${username}@${instance?.baseUrl}`),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return finalText.replace(
|
|
||||||
createRegExp(
|
|
||||||
exactly(
|
|
||||||
exactly(`@${username}`)
|
|
||||||
.notBefore(anyOf(letter, digit, charIn("@")))
|
|
||||||
.notAfter(anyOf(letter, digit, charIn("@"))),
|
|
||||||
).or(exactly(`@${username}@${baseHost}`)),
|
|
||||||
[global],
|
|
||||||
),
|
|
||||||
linkTemplate(`@${username}@${baseHost}`),
|
|
||||||
);
|
|
||||||
}, text);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const contentToHtml = async (
|
|
||||||
content: VersiaEntities.TextContentFormat,
|
|
||||||
mentions: User[] = [],
|
|
||||||
inline = false,
|
|
||||||
): Promise<string> => {
|
|
||||||
const sanitizer = inline ? sanitizeHtmlInline : sanitizeHtml;
|
|
||||||
let htmlContent = "";
|
|
||||||
|
|
||||||
if (content.data["text/html"]) {
|
|
||||||
htmlContent = await sanitizer(content.data["text/html"].content);
|
|
||||||
} else if (content.data["text/markdown"]) {
|
|
||||||
htmlContent = await sanitizer(
|
|
||||||
await markdownParse(content.data["text/markdown"].content),
|
|
||||||
);
|
|
||||||
} else if (content.data["text/plain"]?.content) {
|
|
||||||
htmlContent = (await sanitizer(content.data["text/plain"].content))
|
|
||||||
.split("\n")
|
|
||||||
.map((line) => `<p>${line}</p>`)
|
|
||||||
.join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
htmlContent = replaceTextMentions(htmlContent, mentions);
|
|
||||||
|
|
||||||
return linkifyHtml(htmlContent, {
|
|
||||||
defaultProtocol: "https",
|
|
||||||
validate: { email: (): false => false },
|
|
||||||
target: "_blank",
|
|
||||||
rel: "nofollow noopener noreferrer",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const markdownParse = async (content: string): Promise<string> => {
|
|
||||||
return (await getMarkdownRenderer()).render(content);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getMarkdownRenderer = (): MarkdownIt => {
|
|
||||||
const renderer = MarkdownIt({
|
|
||||||
html: true,
|
|
||||||
linkify: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
renderer.use(markdownItTocDoneRight, {
|
|
||||||
containerClass: "toc",
|
|
||||||
level: [1, 2, 3, 4],
|
|
||||||
listType: "ul",
|
|
||||||
listClass: "toc-list",
|
|
||||||
itemClass: "toc-item",
|
|
||||||
linkClass: "toc-link",
|
|
||||||
});
|
|
||||||
|
|
||||||
renderer.use(markdownItTaskLists);
|
|
||||||
|
|
||||||
renderer.use(markdownItContainer);
|
|
||||||
|
|
||||||
return renderer;
|
|
||||||
};
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
import {
|
|
||||||
type Application,
|
|
||||||
db,
|
|
||||||
type Emoji,
|
|
||||||
type Instance,
|
|
||||||
type Media,
|
|
||||||
type Role,
|
|
||||||
type Token,
|
|
||||||
type User,
|
|
||||||
} from "@versia/kit/db";
|
|
||||||
import type { Users } from "@versia/kit/tables";
|
|
||||||
import type { InferSelectModel } from "drizzle-orm";
|
|
||||||
|
|
||||||
export const userRelations = {
|
|
||||||
instance: true,
|
|
||||||
emojis: {
|
|
||||||
with: {
|
|
||||||
emoji: {
|
|
||||||
with: {
|
|
||||||
instance: true,
|
|
||||||
media: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
avatar: true,
|
|
||||||
header: true,
|
|
||||||
roles: {
|
|
||||||
with: {
|
|
||||||
role: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export interface AuthData {
|
|
||||||
user: User | null;
|
|
||||||
token: Token | null;
|
|
||||||
application: Application | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const transformOutputToUserWithRelations = (
|
|
||||||
user: Omit<InferSelectModel<typeof Users>, "endpoints"> & {
|
|
||||||
followerCount: unknown;
|
|
||||||
followingCount: unknown;
|
|
||||||
statusCount: unknown;
|
|
||||||
avatar: typeof Media.$type | null;
|
|
||||||
header: typeof Media.$type | null;
|
|
||||||
emojis: {
|
|
||||||
userId: string;
|
|
||||||
emojiId: string;
|
|
||||||
emoji?: typeof Emoji.$type;
|
|
||||||
}[];
|
|
||||||
instance: typeof Instance.$type | null;
|
|
||||||
roles: {
|
|
||||||
userId: string;
|
|
||||||
roleId: string;
|
|
||||||
role?: typeof Role.$type;
|
|
||||||
}[];
|
|
||||||
endpoints: unknown;
|
|
||||||
},
|
|
||||||
): typeof User.$type => {
|
|
||||||
return {
|
|
||||||
...user,
|
|
||||||
followerCount: Number(user.followerCount),
|
|
||||||
followingCount: Number(user.followingCount),
|
|
||||||
statusCount: Number(user.statusCount),
|
|
||||||
endpoints:
|
|
||||||
user.endpoints ??
|
|
||||||
({} as Partial<{
|
|
||||||
dislikes: string;
|
|
||||||
featured: string;
|
|
||||||
likes: string;
|
|
||||||
followers: string;
|
|
||||||
following: string;
|
|
||||||
inbox: string;
|
|
||||||
outbox: string;
|
|
||||||
}>),
|
|
||||||
emojis: user.emojis.map(
|
|
||||||
(emoji) =>
|
|
||||||
(emoji as unknown as Record<string, object>)
|
|
||||||
.emoji as typeof Emoji.$type,
|
|
||||||
),
|
|
||||||
roles: user.roles
|
|
||||||
.map((role) => role.role)
|
|
||||||
.filter(Boolean) as (typeof Role.$type)[],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findManyUsers = async (
|
|
||||||
query: Parameters<typeof db.query.Users.findMany>[0],
|
|
||||||
): Promise<(typeof User.$type)[]> => {
|
|
||||||
const output = await db.query.Users.findMany({
|
|
||||||
...query,
|
|
||||||
with: {
|
|
||||||
...userRelations,
|
|
||||||
...query?.with,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return output.map((user) => transformOutputToUserWithRelations(user));
|
|
||||||
};
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { describe, expect, it } from "bun:test";
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import { mockModule } from "@versia-server/tests";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
import { mockModule } from "~/tests/utils.ts";
|
|
||||||
import { calculateBlurhash } from "./blurhash.ts";
|
import { calculateBlurhash } from "./blurhash.ts";
|
||||||
|
|
||||||
describe("BlurhashPreprocessor", () => {
|
describe("BlurhashPreprocessor", () => {
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
import { config } from "~/config.ts";
|
|
||||||
|
|
||||||
export class ProxiableUrl extends URL {
|
|
||||||
private isAllowedOrigin(): boolean {
|
|
||||||
const allowedOrigins: URL[] = [config.http.base_url].concat(
|
|
||||||
config.s3?.public_url ?? [],
|
|
||||||
);
|
|
||||||
|
|
||||||
return allowedOrigins.some((origin) =>
|
|
||||||
this.hostname.endsWith(origin.hostname),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get proxied(): string {
|
|
||||||
// Don't proxy from CDN and self, since those sources are trusted
|
|
||||||
if (this.isAllowedOrigin()) {
|
|
||||||
return this.href;
|
|
||||||
}
|
|
||||||
|
|
||||||
const urlAsBase64Url = Buffer.from(this.href).toString("base64url");
|
|
||||||
|
|
||||||
return new URL(`/media/proxy/${urlAsBase64Url}`, config.http.base_url)
|
|
||||||
.href;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,221 +0,0 @@
|
||||||
/* import {
|
|
||||||
afterEach,
|
|
||||||
beforeEach,
|
|
||||||
describe,
|
|
||||||
expect,
|
|
||||||
jest,
|
|
||||||
mock,
|
|
||||||
test,
|
|
||||||
} from "bun:test";
|
|
||||||
import { ZodError, type ZodTypeAny, z } from "zod";
|
|
||||||
import { Plugin } from "~/packages/plugin-kit";
|
|
||||||
import { type Manifest, manifestSchema } from "~/packages/plugin-kit/schema";
|
|
||||||
import { PluginLoader } from "./loader.ts";
|
|
||||||
|
|
||||||
const mockReaddir = jest.fn();
|
|
||||||
const mockGetLogger = jest.fn(() => ({
|
|
||||||
fatal: jest.fn(),
|
|
||||||
}));
|
|
||||||
const mockParseJSON5 = jest.fn();
|
|
||||||
const mockParseJSONC = jest.fn();
|
|
||||||
const mockFromZodError = jest.fn();
|
|
||||||
|
|
||||||
mock.module("node:fs/promises", () => ({
|
|
||||||
readdir: mockReaddir,
|
|
||||||
}));
|
|
||||||
|
|
||||||
mock.module("@logtape/logtape", () => ({
|
|
||||||
getLogger: mockGetLogger,
|
|
||||||
}));
|
|
||||||
|
|
||||||
mock.module("confbox", () => ({
|
|
||||||
parseJSON5: mockParseJSON5,
|
|
||||||
parseJSONC: mockParseJSONC,
|
|
||||||
}));
|
|
||||||
|
|
||||||
mock.module("zod-validation-error", () => ({
|
|
||||||
fromZodError: mockFromZodError,
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("PluginLoader", () => {
|
|
||||||
let pluginLoader: PluginLoader;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
pluginLoader = new PluginLoader();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("getDirectories should return directories", async () => {
|
|
||||||
mockReaddir.mockResolvedValue([
|
|
||||||
{ name: "dir1", isDirectory: (): true => true },
|
|
||||||
{ name: "file1", isDirectory: (): false => false },
|
|
||||||
{ name: "dir2", isDirectory: (): true => true },
|
|
||||||
]);
|
|
||||||
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
|
||||||
const directories = await PluginLoader["getDirectories"]("/some/path");
|
|
||||||
expect(directories).toEqual(["dir1", "dir2"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("findManifestFile should return manifest file if found", async () => {
|
|
||||||
mockReaddir.mockResolvedValue(["manifest.json", "otherfile.txt"]);
|
|
||||||
|
|
||||||
const manifestFile =
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
|
||||||
await PluginLoader["findManifestFile"]("/some/path");
|
|
||||||
expect(manifestFile).toBe("manifest.json");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("hasEntrypoint should return true if entrypoint file is found", async () => {
|
|
||||||
mockReaddir.mockResolvedValue(["index.ts", "otherfile.txt"]);
|
|
||||||
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
|
||||||
const hasEntrypoint = await PluginLoader["hasEntrypoint"]("/some/path");
|
|
||||||
expect(hasEntrypoint).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("parseManifestFile should parse JSON manifest", async () => {
|
|
||||||
const manifestContent = { name: "test-plugin" };
|
|
||||||
Bun.file = jest.fn().mockReturnValue({
|
|
||||||
text: (): Promise<string> =>
|
|
||||||
Promise.resolve(JSON.stringify(manifestContent)),
|
|
||||||
});
|
|
||||||
|
|
||||||
// biome-ignore lint/complexity/useLiteralKeys: Private method
|
|
||||||
const manifest = await pluginLoader["parseManifestFile"](
|
|
||||||
"/some/path/manifest.json",
|
|
||||||
"manifest.json",
|
|
||||||
);
|
|
||||||
expect(manifest).toEqual(manifestContent);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("findPlugins should return plugin directories with valid manifest and entrypoint", async () => {
|
|
||||||
mockReaddir
|
|
||||||
.mockResolvedValueOnce([
|
|
||||||
{ name: "plugin1", isDirectory: (): true => true },
|
|
||||||
{ name: "plugin2", isDirectory: (): true => true },
|
|
||||||
])
|
|
||||||
.mockResolvedValue(["manifest.json", "index.ts"]);
|
|
||||||
|
|
||||||
const plugins = await PluginLoader.findPlugins("/some/path");
|
|
||||||
expect(plugins).toEqual(["plugin1", "plugin2"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("parseManifest should parse and validate manifest", async () => {
|
|
||||||
const manifestContent: Manifest = {
|
|
||||||
name: "test-plugin",
|
|
||||||
version: "1.1.0",
|
|
||||||
description: "Doobaee",
|
|
||||||
};
|
|
||||||
mockReaddir.mockResolvedValue(["manifest.json"]);
|
|
||||||
Bun.file = jest.fn().mockReturnValue({
|
|
||||||
text: (): Promise<string> =>
|
|
||||||
Promise.resolve(JSON.stringify(manifestContent)),
|
|
||||||
});
|
|
||||||
manifestSchema.safeParseAsync = jest.fn().mockResolvedValue({
|
|
||||||
success: true,
|
|
||||||
data: manifestContent,
|
|
||||||
});
|
|
||||||
|
|
||||||
const manifest = await pluginLoader.parseManifest(
|
|
||||||
"/some/path",
|
|
||||||
"plugin1",
|
|
||||||
);
|
|
||||||
expect(manifest).toEqual(manifestContent);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("parseManifest should throw error if manifest is missing", async () => {
|
|
||||||
mockReaddir.mockResolvedValue([]);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
pluginLoader.parseManifest("/some/path", "plugin1"),
|
|
||||||
).rejects.toThrow("Plugin plugin1 is missing a manifest file");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("parseManifest should throw error if manifest is invalid", async () => {
|
|
||||||
// @ts-expect-error trying to cause a type error here
|
|
||||||
const manifestContent: Manifest = {
|
|
||||||
name: "test-plugin",
|
|
||||||
version: "1.1.0",
|
|
||||||
};
|
|
||||||
mockReaddir.mockResolvedValue(["manifest.json"]);
|
|
||||||
Bun.file = jest.fn().mockReturnValue({
|
|
||||||
text: (): Promise<string> =>
|
|
||||||
Promise.resolve(JSON.stringify(manifestContent)),
|
|
||||||
});
|
|
||||||
manifestSchema.safeParseAsync = jest.fn().mockResolvedValue({
|
|
||||||
success: false,
|
|
||||||
error: new ZodError([]),
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
pluginLoader.parseManifest("/some/path", "plugin1"),
|
|
||||||
).rejects.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("loadPlugin should load and return a Plugin instance", async () => {
|
|
||||||
const mockPlugin = new Plugin(z.object({}));
|
|
||||||
mock.module("/some/path/index.ts", () => ({
|
|
||||||
default: mockPlugin,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const plugin = await pluginLoader.loadPlugin("/some/path", "index.ts");
|
|
||||||
expect(plugin).toBeInstanceOf(Plugin);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("loadPlugin should throw error if default export is not a Plugin", async () => {
|
|
||||||
mock.module("/some/path/index.ts", () => ({
|
|
||||||
default: "cheese",
|
|
||||||
}));
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
pluginLoader.loadPlugin("/some/path", "index.ts"),
|
|
||||||
).rejects.toThrow("Entrypoint is not a Plugin");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("loadPlugins should load all plugins in a directory", async () => {
|
|
||||||
const manifestContent: Manifest = {
|
|
||||||
name: "test-plugin",
|
|
||||||
version: "1.1.0",
|
|
||||||
description: "Doobaee",
|
|
||||||
};
|
|
||||||
const mockPlugin = new Plugin(z.object({}));
|
|
||||||
|
|
||||||
mockReaddir
|
|
||||||
.mockResolvedValueOnce([
|
|
||||||
{ name: "plugin1", isDirectory: (): true => true },
|
|
||||||
{ name: "plugin2", isDirectory: (): true => true },
|
|
||||||
])
|
|
||||||
.mockResolvedValue(["manifest.json", "index.ts"]);
|
|
||||||
Bun.file = jest.fn().mockReturnValue({
|
|
||||||
text: (): Promise<string> =>
|
|
||||||
Promise.resolve(JSON.stringify(manifestContent)),
|
|
||||||
});
|
|
||||||
manifestSchema.safeParseAsync = jest.fn().mockResolvedValue({
|
|
||||||
success: true,
|
|
||||||
data: manifestContent,
|
|
||||||
});
|
|
||||||
mock.module("/some/path/plugin1/index", () => ({
|
|
||||||
default: mockPlugin,
|
|
||||||
}));
|
|
||||||
mock.module("/some/path/plugin2/index", () => ({
|
|
||||||
default: mockPlugin,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const plugins = await pluginLoader.loadPlugins("/some/path", true);
|
|
||||||
expect(plugins).toEqual([
|
|
||||||
{
|
|
||||||
manifest: manifestContent,
|
|
||||||
plugin: mockPlugin as unknown as Plugin<ZodTypeAny>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
manifest: manifestContent,
|
|
||||||
plugin: mockPlugin as unknown as Plugin<ZodTypeAny>,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
@ -1,317 +0,0 @@
|
||||||
/**
|
|
||||||
* @file search-manager.ts
|
|
||||||
* @description Sonic search integration for indexing and searching accounts and statuses
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { getLogger } from "@logtape/logtape";
|
|
||||||
import { db, Note, User } from "@versia/kit/db";
|
|
||||||
import type { SQL, ValueOrArray } from "drizzle-orm";
|
|
||||||
import {
|
|
||||||
Ingest as SonicChannelIngest,
|
|
||||||
Search as SonicChannelSearch,
|
|
||||||
} from "sonic-channel";
|
|
||||||
import { config } from "~/config.ts";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enum for Sonic index types
|
|
||||||
*/
|
|
||||||
export enum SonicIndexType {
|
|
||||||
Accounts = "accounts",
|
|
||||||
Statuses = "statuses",
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class for managing Sonic search operations
|
|
||||||
*/
|
|
||||||
export class SonicSearchManager {
|
|
||||||
private searchChannel: SonicChannelSearch;
|
|
||||||
private ingestChannel: SonicChannelIngest;
|
|
||||||
private connected = false;
|
|
||||||
private logger = getLogger("sonic");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param config Configuration for Sonic
|
|
||||||
*/
|
|
||||||
public constructor() {
|
|
||||||
if (!config.search.sonic) {
|
|
||||||
throw new Error("Sonic configuration is missing");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.searchChannel = new SonicChannelSearch({
|
|
||||||
host: config.search.sonic.host,
|
|
||||||
port: config.search.sonic.port,
|
|
||||||
auth: config.search.sonic.password,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.ingestChannel = new SonicChannelIngest({
|
|
||||||
host: config.search.sonic.host,
|
|
||||||
port: config.search.sonic.port,
|
|
||||||
auth: config.search.sonic.password,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connect to Sonic
|
|
||||||
*/
|
|
||||||
public async connect(silent = false): Promise<void> {
|
|
||||||
if (!config.search.enabled) {
|
|
||||||
!silent && this.logger.info`Sonic search is disabled`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.connected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
!silent && this.logger.info`Connecting to Sonic...`;
|
|
||||||
|
|
||||||
// Connect to Sonic
|
|
||||||
await new Promise<boolean>((resolve, reject) => {
|
|
||||||
this.searchChannel.connect({
|
|
||||||
connected: (): void => {
|
|
||||||
!silent &&
|
|
||||||
this.logger.info`Connected to Sonic Search Channel`;
|
|
||||||
resolve(true);
|
|
||||||
},
|
|
||||||
disconnected: (): void =>
|
|
||||||
this.logger
|
|
||||||
.error`Disconnected from Sonic Search Channel. You might be using an incorrect password.`,
|
|
||||||
timeout: (): void =>
|
|
||||||
this.logger
|
|
||||||
.error`Sonic Search Channel connection timed out`,
|
|
||||||
retrying: (): void =>
|
|
||||||
this.logger
|
|
||||||
.warn`Retrying connection to Sonic Search Channel`,
|
|
||||||
error: (error): void => {
|
|
||||||
this.logger
|
|
||||||
.error`Failed to connect to Sonic Search Channel: ${error}`;
|
|
||||||
reject(error);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise<boolean>((resolve, reject) => {
|
|
||||||
this.ingestChannel.connect({
|
|
||||||
connected: (): void => {
|
|
||||||
!silent &&
|
|
||||||
this.logger.info`Connected to Sonic Ingest Channel`;
|
|
||||||
resolve(true);
|
|
||||||
},
|
|
||||||
disconnected: (): void =>
|
|
||||||
this.logger.error`Disconnected from Sonic Ingest Channel`,
|
|
||||||
timeout: (): void =>
|
|
||||||
this.logger
|
|
||||||
.error`Sonic Ingest Channel connection timed out`,
|
|
||||||
retrying: (): void =>
|
|
||||||
this.logger
|
|
||||||
.warn`Retrying connection to Sonic Ingest Channel`,
|
|
||||||
error: (error): void => {
|
|
||||||
this.logger
|
|
||||||
.error`Failed to connect to Sonic Ingest Channel: ${error}`;
|
|
||||||
reject(error);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await Promise.all([
|
|
||||||
this.searchChannel.ping(),
|
|
||||||
this.ingestChannel.ping(),
|
|
||||||
]);
|
|
||||||
this.connected = true;
|
|
||||||
!silent && this.logger.info`Connected to Sonic`;
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.fatal`Error while connecting to Sonic: ${error}`;
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a user to Sonic
|
|
||||||
* @param user User to add
|
|
||||||
*/
|
|
||||||
public async addUser(user: User): Promise<void> {
|
|
||||||
if (!config.search.enabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.ingestChannel.push(
|
|
||||||
SonicIndexType.Accounts,
|
|
||||||
"users",
|
|
||||||
user.id,
|
|
||||||
`${user.data.username} ${user.data.displayName} ${user.data.note}`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error`Failed to add user to Sonic: ${error}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a batch of accounts from the database
|
|
||||||
* @param n Batch number
|
|
||||||
* @param batchSize Size of the batch
|
|
||||||
*/
|
|
||||||
private static getNthDatabaseAccountBatch(
|
|
||||||
n: number,
|
|
||||||
batchSize = 1000,
|
|
||||||
): Promise<Record<string, string | null | Date>[]> {
|
|
||||||
return db.query.Users.findMany({
|
|
||||||
offset: n * batchSize,
|
|
||||||
limit: batchSize,
|
|
||||||
columns: {
|
|
||||||
id: true,
|
|
||||||
username: true,
|
|
||||||
displayName: true,
|
|
||||||
note: true,
|
|
||||||
createdAt: true,
|
|
||||||
},
|
|
||||||
orderBy: (user, { asc }): ValueOrArray<SQL> => asc(user.createdAt),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a batch of statuses from the database
|
|
||||||
* @param n Batch number
|
|
||||||
* @param batchSize Size of the batch
|
|
||||||
*/
|
|
||||||
private static getNthDatabaseStatusBatch(
|
|
||||||
n: number,
|
|
||||||
batchSize = 1000,
|
|
||||||
): Promise<Record<string, string | Date>[]> {
|
|
||||||
return db.query.Notes.findMany({
|
|
||||||
offset: n * batchSize,
|
|
||||||
limit: batchSize,
|
|
||||||
columns: {
|
|
||||||
id: true,
|
|
||||||
content: true,
|
|
||||||
createdAt: true,
|
|
||||||
},
|
|
||||||
orderBy: (status, { asc }): ValueOrArray<SQL> =>
|
|
||||||
asc(status.createdAt),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rebuild search indexes
|
|
||||||
* @param indexes Indexes to rebuild
|
|
||||||
* @param batchSize Size of each batch
|
|
||||||
* @param progressCallback Callback for progress updates
|
|
||||||
*/
|
|
||||||
public async rebuildSearchIndexes(
|
|
||||||
indexes: SonicIndexType[],
|
|
||||||
batchSize = 100,
|
|
||||||
progressCallback?: (progress: number) => void,
|
|
||||||
): Promise<void> {
|
|
||||||
for (const index of indexes) {
|
|
||||||
if (index === SonicIndexType.Accounts) {
|
|
||||||
await this.rebuildAccountsIndex(batchSize, progressCallback);
|
|
||||||
} else if (index === SonicIndexType.Statuses) {
|
|
||||||
await this.rebuildStatusesIndex(batchSize, progressCallback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rebuild accounts index
|
|
||||||
* @param batchSize Size of each batch
|
|
||||||
* @param progressCallback Callback for progress updates
|
|
||||||
*/
|
|
||||||
private async rebuildAccountsIndex(
|
|
||||||
batchSize: number,
|
|
||||||
progressCallback?: (progress: number) => void,
|
|
||||||
): Promise<void> {
|
|
||||||
const accountCount = await User.getCount();
|
|
||||||
const batchCount = Math.ceil(accountCount / batchSize);
|
|
||||||
|
|
||||||
for (let i = 0; i < batchCount; i++) {
|
|
||||||
const accounts =
|
|
||||||
await SonicSearchManager.getNthDatabaseAccountBatch(
|
|
||||||
i,
|
|
||||||
batchSize,
|
|
||||||
);
|
|
||||||
await Promise.all(
|
|
||||||
accounts.map((account) =>
|
|
||||||
this.ingestChannel.push(
|
|
||||||
SonicIndexType.Accounts,
|
|
||||||
"users",
|
|
||||||
account.id as string,
|
|
||||||
`${account.username} ${account.displayName} ${account.note}`,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
progressCallback?.((i + 1) / batchCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rebuild statuses index
|
|
||||||
* @param batchSize Size of each batch
|
|
||||||
* @param progressCallback Callback for progress updates
|
|
||||||
*/
|
|
||||||
private async rebuildStatusesIndex(
|
|
||||||
batchSize: number,
|
|
||||||
progressCallback?: (progress: number) => void,
|
|
||||||
): Promise<void> {
|
|
||||||
const statusCount = await Note.getCount();
|
|
||||||
const batchCount = Math.ceil(statusCount / batchSize);
|
|
||||||
|
|
||||||
for (let i = 0; i < batchCount; i++) {
|
|
||||||
const statuses = await SonicSearchManager.getNthDatabaseStatusBatch(
|
|
||||||
i,
|
|
||||||
batchSize,
|
|
||||||
);
|
|
||||||
await Promise.all(
|
|
||||||
statuses.map((status) =>
|
|
||||||
this.ingestChannel.push(
|
|
||||||
SonicIndexType.Statuses,
|
|
||||||
"notes",
|
|
||||||
status.id as string,
|
|
||||||
status.content as string,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
progressCallback?.((i + 1) / batchCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search for accounts
|
|
||||||
* @param query Search query
|
|
||||||
* @param limit Maximum number of results
|
|
||||||
* @param offset Offset for pagination
|
|
||||||
*/
|
|
||||||
public searchAccounts(
|
|
||||||
query: string,
|
|
||||||
limit = 10,
|
|
||||||
offset = 0,
|
|
||||||
): Promise<string[]> {
|
|
||||||
return this.searchChannel.query(
|
|
||||||
SonicIndexType.Accounts,
|
|
||||||
"users",
|
|
||||||
query,
|
|
||||||
{ limit, offset },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search for statuses
|
|
||||||
* @param query Search query
|
|
||||||
* @param limit Maximum number of results
|
|
||||||
* @param offset Offset for pagination
|
|
||||||
*/
|
|
||||||
public searchStatuses(
|
|
||||||
query: string,
|
|
||||||
limit = 10,
|
|
||||||
offset = 0,
|
|
||||||
): Promise<string[]> {
|
|
||||||
return this.searchChannel.query(
|
|
||||||
SonicIndexType.Statuses,
|
|
||||||
"notes",
|
|
||||||
query,
|
|
||||||
{ limit, offset },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const searchManager = new SonicSearchManager();
|
|
||||||
|
|
@ -3,10 +3,10 @@ import { friendlyErrorPlugin } from "@clerc/plugin-friendly-error";
|
||||||
import { helpPlugin } from "@clerc/plugin-help";
|
import { helpPlugin } from "@clerc/plugin-help";
|
||||||
import { notFoundPlugin } from "@clerc/plugin-not-found";
|
import { notFoundPlugin } from "@clerc/plugin-not-found";
|
||||||
import { versionPlugin } from "@clerc/plugin-version";
|
import { versionPlugin } from "@clerc/plugin-version";
|
||||||
|
import { setupDatabase } from "@versia-server/kit/db";
|
||||||
|
import { searchManager } from "@versia-server/kit/search";
|
||||||
import { Clerc } from "clerc";
|
import { Clerc } from "clerc";
|
||||||
import { searchManager } from "~/classes/search/search-manager.ts";
|
import pkg from "../package.json" with { type: "json" };
|
||||||
import { setupDatabase } from "~/drizzle/db.ts";
|
|
||||||
import pkg from "~/package.json" with { type: "json" };
|
|
||||||
import { rebuildIndexCommand } from "./index/rebuild.ts";
|
import { rebuildIndexCommand } from "./index/rebuild.ts";
|
||||||
import { refetchInstanceCommand } from "./instance/refetch.ts";
|
import { refetchInstanceCommand } from "./instance/refetch.ts";
|
||||||
import { createUserCommand } from "./user/create.ts";
|
import { createUserCommand } from "./user/create.ts";
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
|
import { config } from "@versia-server/config";
|
||||||
|
import { SonicIndexType, searchManager } from "@versia-server/kit/search";
|
||||||
// @ts-expect-error - Root import is required or the Clec type definitions won't work
|
// @ts-expect-error - Root import is required or the Clec type definitions won't work
|
||||||
// biome-ignore lint/correctness/noUnusedImports: Root import is required or the Clec type definitions won't work
|
// biome-ignore lint/correctness/noUnusedImports: Root import is required or the Clec type definitions won't work
|
||||||
import { defineCommand, type Root } from "clerc";
|
import { defineCommand, type Root } from "clerc";
|
||||||
import ora from "ora";
|
import ora from "ora";
|
||||||
import {
|
|
||||||
SonicIndexType,
|
|
||||||
searchManager,
|
|
||||||
} from "~/classes/search/search-manager.ts";
|
|
||||||
import { config } from "~/config.ts";
|
|
||||||
|
|
||||||
export const rebuildIndexCommand = defineCommand(
|
export const rebuildIndexCommand = defineCommand(
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
|
import { Instance } from "@versia-server/kit/db";
|
||||||
|
import { FetchJobType, fetchQueue } from "@versia-server/kit/queues/fetch";
|
||||||
|
import { Instances } from "@versia-server/kit/tables";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
// @ts-expect-error - Root import is required or the Clec type definitions won't work
|
// @ts-expect-error - Root import is required or the Clec type definitions won't work
|
||||||
// biome-ignore lint/correctness/noUnusedImports: Root import is required or the Clec type definitions won't work
|
// biome-ignore lint/correctness/noUnusedImports: Root import is required or the Clec type definitions won't work
|
||||||
import { defineCommand, type Root } from "clerc";
|
import { defineCommand, type Root } from "clerc";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { Instance } from "~/classes/database/instance.ts";
|
|
||||||
import { FetchJobType, fetchQueue } from "~/classes/queues/fetch.ts";
|
|
||||||
import { Instances } from "~/drizzle/schema.ts";
|
|
||||||
|
|
||||||
export const refetchInstanceCommand = defineCommand(
|
export const refetchInstanceCommand = defineCommand(
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
|
import { config } from "@versia-server/config";
|
||||||
|
import { User } from "@versia-server/kit/db";
|
||||||
|
import { searchManager } from "@versia-server/kit/search";
|
||||||
|
import { Users } from "@versia-server/kit/tables";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
// @ts-expect-error - Root import is required or the Clec type definitions won't work
|
// @ts-expect-error - Root import is required or the Clec type definitions won't work
|
||||||
// biome-ignore lint/correctness/noUnusedImports: Root import is required or the Clec type definitions won't work
|
// biome-ignore lint/correctness/noUnusedImports: Root import is required or the Clec type definitions won't work
|
||||||
import { defineCommand, type Root } from "clerc";
|
import { defineCommand, type Root } from "clerc";
|
||||||
import { and, eq, isNull } from "drizzle-orm";
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
import { renderUnicodeCompact } from "uqr";
|
import { renderUnicodeCompact } from "uqr";
|
||||||
import { User } from "~/classes/database/user";
|
|
||||||
import { config } from "~/config";
|
|
||||||
import { Users } from "~/drizzle/schema";
|
|
||||||
|
|
||||||
export const createUserCommand = defineCommand(
|
export const createUserCommand = defineCommand(
|
||||||
{
|
{
|
||||||
|
|
@ -54,6 +55,9 @@ export const createUserCommand = defineCommand(
|
||||||
isAdmin: admin,
|
isAdmin: admin,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add to search index
|
||||||
|
await searchManager.addUser(user);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error("Failed to create user.");
|
throw new Error("Failed to create user.");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
|
import { User } from "@versia-server/kit/db";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
// @ts-expect-error - Root import is required or the Clec type definitions won't work
|
// @ts-expect-error - Root import is required or the Clec type definitions won't work
|
||||||
// biome-ignore lint/correctness/noUnusedImports: Root import is required or the Clec type definitions won't work
|
// biome-ignore lint/correctness/noUnusedImports: Root import is required or the Clec type definitions won't work
|
||||||
import { defineCommand, type Root } from "clerc";
|
import { defineCommand, type Root } from "clerc";
|
||||||
import ora from "ora";
|
import ora from "ora";
|
||||||
import { User } from "~/classes/database/user.ts";
|
|
||||||
import { retrieveUser } from "../utils.ts";
|
import { retrieveUser } from "../utils.ts";
|
||||||
|
|
||||||
export const refetchUserCommand = defineCommand(
|
export const refetchUserCommand = defineCommand(
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
|
import { Token } from "@versia-server/kit/db";
|
||||||
import { randomUUIDv7 } from "bun";
|
import { randomUUIDv7 } from "bun";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
// @ts-expect-error - Root import is required or the Clec type definitions won't work
|
// @ts-expect-error - Root import is required or the Clec type definitions won't work
|
||||||
// biome-ignore lint/correctness/noUnusedImports: Root import is required or the Clec type definitions won't work
|
// biome-ignore lint/correctness/noUnusedImports: Root import is required or the Clec type definitions won't work
|
||||||
import { defineCommand, type Root } from "clerc";
|
import { defineCommand, type Root } from "clerc";
|
||||||
import { randomString } from "@/math.ts";
|
import { randomString } from "@/math.ts";
|
||||||
import { Token } from "~/classes/database/token.ts";
|
|
||||||
import { retrieveUser } from "../utils.ts";
|
import { retrieveUser } from "../utils.ts";
|
||||||
|
|
||||||
export const generateTokenCommand = defineCommand(
|
export const generateTokenCommand = defineCommand(
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
|
import { Instance, User } from "@versia-server/kit/db";
|
||||||
|
import { parseUserAddress } from "@versia-server/kit/parsers";
|
||||||
|
import { Users } from "@versia-server/kit/tables";
|
||||||
import { and, eq, isNull } from "drizzle-orm";
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
import { parseUserAddress } from "@/api";
|
|
||||||
import { Instance } from "~/classes/database/instance";
|
|
||||||
import { User } from "~/classes/database/user";
|
|
||||||
import { Users } from "~/drizzle/schema";
|
|
||||||
|
|
||||||
export const retrieveUser = async (
|
export const retrieveUser = async (
|
||||||
usernameOrHandle: string,
|
usernameOrHandle: string,
|
||||||
|
|
|
||||||
48
config.ts
48
config.ts
|
|
@ -1,48 +0,0 @@
|
||||||
/**
|
|
||||||
* @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 { env, file } from "bun";
|
|
||||||
import chalk from "chalk";
|
|
||||||
import { parseTOML } from "confbox";
|
|
||||||
import type { z } from "zod";
|
|
||||||
import { fromZodError } from "zod-validation-error";
|
|
||||||
import { ConfigSchema } from "./classes/config/schema.ts";
|
|
||||||
|
|
||||||
const CONFIG_LOCATION = env.CONFIG_LOCATION ?? "./config/config.toml";
|
|
||||||
const configFile = file(CONFIG_LOCATION);
|
|
||||||
|
|
||||||
if (!(await configFile.exists())) {
|
|
||||||
throw new Error(
|
|
||||||
`config file at "${CONFIG_LOCATION}" does not exist or is not accessible.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const configText = await configFile.text();
|
|
||||||
const config = await parseTOML<z.infer<typeof ConfigSchema>>(configText);
|
|
||||||
|
|
||||||
const parsed = await ConfigSchema.safeParseAsync(config);
|
|
||||||
|
|
||||||
if (!parsed.success) {
|
|
||||||
console.error(
|
|
||||||
`⚠ Error encountered while loading ${chalk.gray(CONFIG_LOCATION)}.`,
|
|
||||||
);
|
|
||||||
console.error(
|
|
||||||
"⚠ This is due to invalid, missing or incorrect values in the configuration file.",
|
|
||||||
);
|
|
||||||
console.error(
|
|
||||||
"⚠ Here is the error message, please fix the configuration file accordingly:",
|
|
||||||
);
|
|
||||||
const errorMessage = fromZodError(parsed.error).message;
|
|
||||||
|
|
||||||
console.info(errorMessage);
|
|
||||||
|
|
||||||
throw new Error("Configuration file is invalid.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const exportedConfig = parsed.data;
|
|
||||||
|
|
||||||
export { exportedConfig as config };
|
|
||||||
1
config/config
Symbolic link
1
config/config
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
../config
|
||||||
|
|
@ -435,31 +435,29 @@ text = "No spam"
|
||||||
|
|
||||||
[logging]
|
[logging]
|
||||||
|
|
||||||
# Available levels: debug, info, warning, error, fatal
|
# Available levels: trace, debug, info, warning, error, fatal
|
||||||
log_level = "debug"
|
log_level = "info" # For console output
|
||||||
|
|
||||||
log_file_path = "logs/versia.log"
|
|
||||||
|
|
||||||
[logging.types]
|
|
||||||
# Either pass a boolean
|
|
||||||
# requests = true
|
|
||||||
# Or a table with the following keys:
|
|
||||||
# requests_content = { level = "debug", log_file_path = "logs/requests.log" }
|
|
||||||
# Available types are: requests, responses, requests_content, filters
|
|
||||||
|
|
||||||
|
# [logging.file]
|
||||||
|
# path = "logs/versia.log"
|
||||||
|
# log_level = "info"
|
||||||
|
#
|
||||||
|
# [logging.file.rotation]
|
||||||
|
# max_size = 10_000_000 # 10 MB
|
||||||
|
# max_files = 10 # Keep 10 rotated files
|
||||||
|
#
|
||||||
# https://sentry.io support
|
# https://sentry.io support
|
||||||
# Uncomment to enable
|
|
||||||
# [logging.sentry]
|
# [logging.sentry]
|
||||||
# Sentry DSN for error logging
|
|
||||||
# dsn = "https://example.com"
|
# 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"
|
||||||
|
# log_level = "info"
|
||||||
|
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
# Whether to automatically load all plugins in the plugins directory
|
# Whether to automatically load all plugins in the plugins directory
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
import { config } from "@versia-server/config";
|
||||||
import type { Config } from "drizzle-kit";
|
import type { Config } from "drizzle-kit";
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
import process from "node:process";
|
|
||||||
import { getLogger } from "@logtape/logtape";
|
|
||||||
import chalk from "chalk";
|
|
||||||
import { sentry } from "@/sentry";
|
|
||||||
import { getDeliveryWorker } from "~/classes/queues/delivery";
|
|
||||||
import { getFetchWorker } from "~/classes/queues/fetch";
|
|
||||||
import { getInboxWorker } from "~/classes/queues/inbox";
|
|
||||||
import { getMediaWorker } from "~/classes/queues/media";
|
|
||||||
import { getPushWorker } from "~/classes/queues/push";
|
|
||||||
import { getRelationshipWorker } from "~/classes/queues/relationships";
|
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
|
||||||
process.exit();
|
|
||||||
});
|
|
||||||
|
|
||||||
await import("~/entrypoints/worker/setup.ts");
|
|
||||||
sentry?.captureMessage("Server started", "info");
|
|
||||||
|
|
||||||
const serverLogger = getLogger("server");
|
|
||||||
|
|
||||||
serverLogger.info`Starting Fetch Worker...`;
|
|
||||||
getFetchWorker();
|
|
||||||
serverLogger.info`${chalk.green("✔")} Fetch Worker started`;
|
|
||||||
|
|
||||||
serverLogger.info`Starting Delivery Worker...`;
|
|
||||||
getDeliveryWorker();
|
|
||||||
serverLogger.info`${chalk.green("✔")} Delivery Worker started`;
|
|
||||||
|
|
||||||
serverLogger.info`Starting Inbox Worker...`;
|
|
||||||
getInboxWorker();
|
|
||||||
serverLogger.info`${chalk.green("✔")} Inbox Worker started`;
|
|
||||||
|
|
||||||
serverLogger.info`Starting Push Worker...`;
|
|
||||||
getPushWorker();
|
|
||||||
serverLogger.info`${chalk.green("✔")} Push Worker started`;
|
|
||||||
|
|
||||||
serverLogger.info`Starting Media Worker...`;
|
|
||||||
getMediaWorker();
|
|
||||||
serverLogger.info`${chalk.green("✔")} Media Worker started`;
|
|
||||||
|
|
||||||
serverLogger.info`Starting Relationship Worker...`;
|
|
||||||
getRelationshipWorker();
|
|
||||||
serverLogger.info`${chalk.green("✔")} Relationship Worker started`;
|
|
||||||
|
|
||||||
serverLogger.info`${chalk.green("✔✔✔✔✔✔")} All workers started`;
|
|
||||||
12
flake.lock
12
flake.lock
|
|
@ -20,16 +20,16 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1744536153,
|
"lastModified": 1751637120,
|
||||||
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
|
"narHash": "sha256-xVNy/XopSfIG9c46nRmPaKfH1Gn/56vQ8++xWA8itO4=",
|
||||||
"owner": "nixos",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
|
"rev": "5c724ed1388e53cc231ed98330a60eb2f7be4be3",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "nixos",
|
"owner": "NixOS",
|
||||||
"ref": "nixpkgs-unstable",
|
"ref": "nixos-unstable",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
description = "Versia Server";
|
description = "Versia Server";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
};
|
};
|
||||||
|
|
@ -15,7 +15,9 @@
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
overlays.default = final: prev: rec {
|
overlays.default = final: prev: rec {
|
||||||
versia-server = final.callPackage ./nix/package.nix {};
|
versia-server =
|
||||||
|
final.callPackage ./nix/package.nix {
|
||||||
|
};
|
||||||
versia-server-worker = final.callPackage ./nix/package-worker.nix {
|
versia-server-worker = final.callPackage ./nix/package-worker.nix {
|
||||||
inherit versia-server;
|
inherit versia-server;
|
||||||
};
|
};
|
||||||
|
|
@ -54,7 +56,6 @@
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
bun
|
bun
|
||||||
vips
|
vips
|
||||||
pnpm
|
|
||||||
nodePackages.typescript
|
nodePackages.typescript
|
||||||
nodePackages.typescript-language-server
|
nodePackages.typescript-language-server
|
||||||
nix-ld
|
nix-ld
|
||||||
|
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
import { getLogger } from "@logtape/logtape";
|
|
||||||
import { SHA256 } from "bun";
|
|
||||||
import chalk from "chalk";
|
|
||||||
import { createMiddleware } from "hono/factory";
|
|
||||||
import { config } from "~/config.ts";
|
|
||||||
|
|
||||||
export const logger = createMiddleware(async (context, next) => {
|
|
||||||
if (config.logging.types.requests) {
|
|
||||||
const serverLogger = getLogger("server");
|
|
||||||
const body = await context.req.raw.clone().text();
|
|
||||||
|
|
||||||
const urlAndMethod = `${chalk.green(context.req.method)} ${chalk.blue(context.req.url)}`;
|
|
||||||
|
|
||||||
const hash = `${chalk.bold("Hash")}: ${chalk.yellow(
|
|
||||||
new SHA256().update(body).digest("hex"),
|
|
||||||
)}`;
|
|
||||||
|
|
||||||
const headers = `${chalk.bold("Headers")}:\n${Array.from(
|
|
||||||
context.req.raw.headers.entries(),
|
|
||||||
)
|
|
||||||
.map(
|
|
||||||
([key, value]) =>
|
|
||||||
` - ${chalk.cyan(key)}: ${chalk.white(value)}`,
|
|
||||||
)
|
|
||||||
.join("\n")}`;
|
|
||||||
|
|
||||||
const bodyLog = `${chalk.bold("Body")}: ${chalk.gray(body)}`;
|
|
||||||
|
|
||||||
if (config.logging.types.requests_content) {
|
|
||||||
serverLogger.debug`${urlAndMethod}\n${hash}\n${headers}\n${bodyLog}`;
|
|
||||||
} else {
|
|
||||||
serverLogger.debug`${urlAndMethod}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await next();
|
|
||||||
});
|
|
||||||
|
|
@ -2,14 +2,8 @@
|
||||||
{versia-server, ...}:
|
{versia-server, ...}:
|
||||||
versia-server.overrideAttrs (oldAttrs: {
|
versia-server.overrideAttrs (oldAttrs: {
|
||||||
pname = "${oldAttrs.pname}-worker";
|
pname = "${oldAttrs.pname}-worker";
|
||||||
buildPhase = ''
|
|
||||||
runHook preBuild
|
|
||||||
|
|
||||||
bun run build:worker
|
buildType = "worker";
|
||||||
|
|
||||||
runHook postBuild
|
|
||||||
'';
|
|
||||||
entrypointPath = "worker.js";
|
|
||||||
|
|
||||||
meta =
|
meta =
|
||||||
oldAttrs.meta
|
oldAttrs.meta
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
{
|
{
|
||||||
lib,
|
lib,
|
||||||
stdenv,
|
stdenv,
|
||||||
pnpm,
|
|
||||||
bun,
|
bun,
|
||||||
nodejs,
|
nodejs,
|
||||||
vips,
|
vips,
|
||||||
makeWrapper,
|
makeWrapper,
|
||||||
|
stdenvNoCC,
|
||||||
|
writableTmpDirAsHomeHook,
|
||||||
...
|
...
|
||||||
}: let
|
}: let
|
||||||
packageJson = builtins.fromJSON (builtins.readFile ../package.json);
|
packageJson = builtins.fromJSON (builtins.readFile ../package.json);
|
||||||
|
|
@ -16,35 +17,70 @@ in
|
||||||
|
|
||||||
src = ../.;
|
src = ../.;
|
||||||
|
|
||||||
# Fixes the build script mv usage
|
node_modules = stdenvNoCC.mkDerivation {
|
||||||
pnpmInstallFlags = ["--shamefully-hoist"];
|
pname = "${finalAttrs.pname}-node_modules";
|
||||||
|
inherit (finalAttrs) version src;
|
||||||
|
|
||||||
pnpmDeps = pnpm.fetchDeps {
|
nativeBuildInputs = [
|
||||||
inherit (finalAttrs) pname version src pnpmInstallFlags;
|
bun
|
||||||
hash = "sha256-/VCzDp8EfvQkaz/5W3rcoEyOlSB4zeW97qqOTJf6WvA=";
|
nodejs
|
||||||
|
writableTmpDirAsHomeHook
|
||||||
|
];
|
||||||
|
|
||||||
|
dontConfigure = true;
|
||||||
|
|
||||||
|
buildPhase = ''
|
||||||
|
runHook preBuild
|
||||||
|
|
||||||
|
export BUN_INSTALL_CACHE_DIR=$(mktemp -d)
|
||||||
|
|
||||||
|
bun install \
|
||||||
|
--force \
|
||||||
|
--frozen-lockfile \
|
||||||
|
--no-progress
|
||||||
|
|
||||||
|
runHook postBuild
|
||||||
|
'';
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
runHook preInstall
|
||||||
|
|
||||||
|
mkdir -p $out/node_modules
|
||||||
|
cp -R ./node_modules $out
|
||||||
|
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Required else we get errors that our fixed-output derivation references store paths
|
||||||
|
dontFixup = true;
|
||||||
|
|
||||||
|
outputHash = "sha256-/RQv87hjLdH6+41yR7+bGp3j200DVhIrKWoI1MKIqJs=";
|
||||||
|
outputHashAlgo = "sha256";
|
||||||
|
outputHashMode = "recursive";
|
||||||
};
|
};
|
||||||
|
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
pnpm
|
|
||||||
pnpm.configHook
|
|
||||||
bun
|
bun
|
||||||
nodejs
|
|
||||||
makeWrapper
|
makeWrapper
|
||||||
];
|
];
|
||||||
|
|
||||||
buildInputs = [
|
configurePhase = ''
|
||||||
vips
|
runHook preConfigure
|
||||||
];
|
|
||||||
|
cp -R ${finalAttrs.node_modules}/node_modules .
|
||||||
|
|
||||||
|
runHook postConfigure
|
||||||
|
'';
|
||||||
|
|
||||||
buildPhase = ''
|
buildPhase = ''
|
||||||
runHook preBuild
|
runHook preBuild
|
||||||
|
|
||||||
bun run build
|
bun run build ${finalAttrs.buildType}
|
||||||
|
|
||||||
runHook postBuild
|
runHook postBuild
|
||||||
'';
|
'';
|
||||||
|
|
||||||
entrypointPath = "index.js";
|
buildType = "api";
|
||||||
|
|
||||||
installPhase = let
|
installPhase = let
|
||||||
libPath = lib.makeLibraryPath [
|
libPath = lib.makeLibraryPath [
|
||||||
|
|
@ -62,7 +98,7 @@ in
|
||||||
cp -r dist $out/${finalAttrs.pname}
|
cp -r dist $out/${finalAttrs.pname}
|
||||||
|
|
||||||
makeWrapper ${lib.getExe bun} $out/bin/${finalAttrs.pname} \
|
makeWrapper ${lib.getExe bun} $out/bin/${finalAttrs.pname} \
|
||||||
--add-flags "run $out/${finalAttrs.pname}/${finalAttrs.entrypointPath}" \
|
--add-flags "run $out/${finalAttrs.pname}/${finalAttrs.buildType}.js" \
|
||||||
--set NODE_PATH $out/${finalAttrs.pname}/node_modules \
|
--set NODE_PATH $out/${finalAttrs.pname}/node_modules \
|
||||||
--set MSGPACKR_NATIVE_ACCELERATION_DISABLED true \
|
--set MSGPACKR_NATIVE_ACCELERATION_DISABLED true \
|
||||||
--prefix PATH : ${binPath} \
|
--prefix PATH : ${binPath} \
|
||||||
|
|
|
||||||
255
package.json
255
package.json
|
|
@ -20,9 +20,88 @@
|
||||||
"activitypub",
|
"activitypub",
|
||||||
"bun"
|
"bun"
|
||||||
],
|
],
|
||||||
"workspaces": [
|
"workspaces": {
|
||||||
"packages/*"
|
"packages": [
|
||||||
],
|
"packages/*"
|
||||||
|
],
|
||||||
|
"catalog": {
|
||||||
|
"@biomejs/biome": "^2.0.6",
|
||||||
|
"@types/bun": "^1.2.18",
|
||||||
|
"@types/html-to-text": "^9.0.4",
|
||||||
|
"@types/markdown-it-container": "^2.0.10",
|
||||||
|
"@types/mime-types": "^3.0.1",
|
||||||
|
"@types/qs": "^6.14.0",
|
||||||
|
"@types/web-push": "^3.6.4",
|
||||||
|
"bun-bagel": "^1.2.0",
|
||||||
|
"drizzle-kit": "^0.31.4",
|
||||||
|
"mitt": "^3.0.1",
|
||||||
|
"markdown-it-image-figures": "^2.1.1",
|
||||||
|
"ts-prune": "^0.10.3",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"vitepress": "^1.6.3",
|
||||||
|
"vitepress-plugin-tabs": "^0.7.1",
|
||||||
|
"vitepress-sidebar": "^1.32.1",
|
||||||
|
"vue": "^3.5.17",
|
||||||
|
"@bull-board/api": "^6.11.0",
|
||||||
|
"@bull-board/hono": "^6.11.0",
|
||||||
|
"@clerc/plugin-completions": "^0.44.0",
|
||||||
|
"@clerc/plugin-friendly-error": "^0.44.0",
|
||||||
|
"@clerc/plugin-help": "^0.44.0",
|
||||||
|
"@clerc/plugin-not-found": "^0.44.0",
|
||||||
|
"@clerc/plugin-version": "^0.44.0",
|
||||||
|
"@hackmd/markdown-it-task-lists": "^2.1.4",
|
||||||
|
"@hono/standard-validator": "^0.1.2",
|
||||||
|
"@inquirer/confirm": "^5.1.13",
|
||||||
|
"@logtape/file": "^1.0.0",
|
||||||
|
"@logtape/logtape": "^1.0.0",
|
||||||
|
"@logtape/sentry": "^1.0.0",
|
||||||
|
"@logtape/otel": "^1.0.0",
|
||||||
|
"@scalar/hono-api-reference": "^0.9.7",
|
||||||
|
"@sentry/bun": "^9.35.0",
|
||||||
|
"altcha-lib": "^1.3.0",
|
||||||
|
"blurhash": "^2.0.5",
|
||||||
|
"bullmq": "^5.56.1",
|
||||||
|
"chalk": "^5.4.1",
|
||||||
|
"clerc": "^0.44.0",
|
||||||
|
"confbox": "^0.2.2",
|
||||||
|
"drizzle-orm": "^0.44.2",
|
||||||
|
"feed": "^5.1.0",
|
||||||
|
"hono": "^4.8.4",
|
||||||
|
"hono-openapi": "npm:@cpluspatch/hono-openapi@0.5.1",
|
||||||
|
"hono-rate-limiter": "^0.4.2",
|
||||||
|
"html-to-text": "^9.0.5",
|
||||||
|
"ioredis": "^5.6.1",
|
||||||
|
"ip-matching": "^2.1.2",
|
||||||
|
"iso-639-1": "^3.1.5",
|
||||||
|
"jose": "^6.0.11",
|
||||||
|
"linkify-html": "^4.3.1",
|
||||||
|
"linkify-string": "^4.3.1",
|
||||||
|
"linkifyjs": "^4.3.1",
|
||||||
|
"magic-regexp": "^0.10.0",
|
||||||
|
"markdown-it": "^14.1.0",
|
||||||
|
"markdown-it-anchor": "^9.2.0",
|
||||||
|
"markdown-it-container": "^4.0.0",
|
||||||
|
"markdown-it-mathjax3": "^4.3.2",
|
||||||
|
"markdown-it-toc-done-right": "^4.2.0",
|
||||||
|
"mime-types": "^3.0.1",
|
||||||
|
"mitata": "^1.0.34",
|
||||||
|
"oauth4webapi": "^3.5.5",
|
||||||
|
"ora": "^8.2.0",
|
||||||
|
"qs": "^6.14.0",
|
||||||
|
"sharp": "^0.34.2",
|
||||||
|
"sonic-channel": "^1.3.1",
|
||||||
|
"string-comparison": "^1.3.0",
|
||||||
|
"stringify-entities": "^4.0.4",
|
||||||
|
"unicode-emoji-json": "^0.8.0",
|
||||||
|
"uqr": "^0.1.2",
|
||||||
|
"web-push": "^3.6.7",
|
||||||
|
"xss": "^1.0.15",
|
||||||
|
"youch": "^4.1.0-beta.7",
|
||||||
|
"zod": "^3.25.74",
|
||||||
|
"zod-openapi": "^5.0.0",
|
||||||
|
"zod-validation-error": "^4.0.0-beta.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"maintainers": [
|
"maintainers": [
|
||||||
{
|
{
|
||||||
"email": "contact@cpluspatch.com",
|
"email": "contact@cpluspatch.com",
|
||||||
|
|
@ -36,107 +115,107 @@
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run --hot index.ts",
|
|
||||||
"start": "NODE_ENV=production bun run dist/index.js --prod",
|
|
||||||
"lint": "biome check .",
|
"lint": "biome check .",
|
||||||
"build": "bun run build.ts",
|
|
||||||
"build:worker": "bun run build-worker.ts",
|
|
||||||
"cloc": "cloc . --exclude-dir node_modules,dist,.output,.nuxt,meta,logs --exclude-ext sql,log,pem",
|
"cloc": "cloc . --exclude-dir node_modules,dist,.output,.nuxt,meta,logs --exclude-ext sql,log,pem",
|
||||||
"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)'",
|
"typecheck": "bunx tsc -p .",
|
||||||
"schema:generate": "bun run classes/config/to-json-schema.ts > config/config.schema.json && bun run packages/plugin-kit/json-schema.ts > packages/plugin-kit/manifest.schema.json",
|
|
||||||
"check": "bunx tsc -p .",
|
|
||||||
"test": "bun test",
|
"test": "bun test",
|
||||||
"docs:dev": "vitepress dev docs",
|
"build": "bun run --filter \"*\" build && bun run build.ts",
|
||||||
"docs:build": "vitepress build docs",
|
"detect-circular": "bunx madge --circular --extensions ts ./",
|
||||||
"docs:preview": "vitepress preview docs"
|
"update-nix-hashes": "bash scripts/update-nix.sh",
|
||||||
|
"run-api": "bun run build api && cd dist && ln -s ../config config && bun run api.js",
|
||||||
|
"run-worker": "bun run build worker && cd dist && ln -s ../config config && bun run worker.js",
|
||||||
|
"dev": "bun run --hot api.ts",
|
||||||
|
"worker:dev": "bun run --hot worker.ts"
|
||||||
},
|
},
|
||||||
"trustedDependencies": [
|
"trustedDependencies": [
|
||||||
"@biomejs/biome",
|
"@biomejs/biome",
|
||||||
"es5-ext",
|
"es5-ext",
|
||||||
"esbuild",
|
"esbuild",
|
||||||
"msgpackr-extract",
|
"msgpackr-extract",
|
||||||
|
"protobufjs",
|
||||||
"sharp"
|
"sharp"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.0.0-beta.5",
|
"@biomejs/biome": "catalog:",
|
||||||
"@types/bun": "^1.2.16",
|
"@types/bun": "catalog:",
|
||||||
"@types/html-to-text": "^9.0.4",
|
"@types/html-to-text": "catalog:",
|
||||||
"@types/markdown-it-container": "^2.0.10",
|
"@types/markdown-it-container": "catalog:",
|
||||||
"@types/mime-types": "^3.0.1",
|
"@types/mime-types": "catalog:",
|
||||||
"@types/qs": "^6.14.0",
|
"@types/qs": "catalog:",
|
||||||
"@types/web-push": "^3.6.4",
|
"@types/web-push": "catalog:",
|
||||||
"bun-bagel": "^1.2.0",
|
"bun-bagel": "catalog:",
|
||||||
"drizzle-kit": "^0.31.1",
|
"drizzle-kit": "catalog:",
|
||||||
"markdown-it-image-figures": "^2.1.1",
|
"markdown-it-image-figures": "catalog:",
|
||||||
"ts-prune": "^0.10.3",
|
"ts-prune": "catalog:",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "catalog:",
|
||||||
"vitepress": "^1.6.3",
|
"vitepress": "catalog:",
|
||||||
"vitepress-plugin-tabs": "^0.7.1",
|
"vitepress-plugin-tabs": "catalog:",
|
||||||
"vitepress-sidebar": "^1.31.1",
|
"vitepress-sidebar": "catalog:",
|
||||||
"vue": "^3.5.16",
|
"vue": "catalog:"
|
||||||
"zod-to-json-schema": "^3.24.5"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bull-board/api": "^6.10.1",
|
"@bull-board/api": "catalog:",
|
||||||
"@bull-board/hono": "^6.10.1",
|
"@bull-board/hono": "catalog:",
|
||||||
"@clerc/plugin-completions": "^0.44.0",
|
"@clerc/plugin-completions": "catalog:",
|
||||||
"@clerc/plugin-friendly-error": "^0.44.0",
|
"@clerc/plugin-friendly-error": "catalog:",
|
||||||
"@clerc/plugin-help": "^0.44.0",
|
"@clerc/plugin-help": "catalog:",
|
||||||
"@clerc/plugin-not-found": "^0.44.0",
|
"@clerc/plugin-not-found": "catalog:",
|
||||||
"@clerc/plugin-version": "^0.44.0",
|
"@clerc/plugin-version": "catalog:",
|
||||||
"@hackmd/markdown-it-task-lists": "^2.1.4",
|
"@hackmd/markdown-it-task-lists": "catalog:",
|
||||||
"@hono/zod-validator": "^0.7.0",
|
"@hono/standard-validator": "catalog:",
|
||||||
"@inquirer/confirm": "^5.1.12",
|
"@inquirer/confirm": "catalog:",
|
||||||
"@logtape/file": "^0.12.0",
|
"@scalar/hono-api-reference": "catalog:",
|
||||||
"@logtape/logtape": "^0.12.0",
|
"@sentry/bun": "catalog:",
|
||||||
"@scalar/hono-api-reference": "^0.9.4",
|
"@versia-server/api": "workspace:*",
|
||||||
"@sentry/bun": "^9.29.0",
|
"@versia-server/config": "workspace:*",
|
||||||
|
"@versia-server/kit": "workspace:*",
|
||||||
|
"@versia-server/logging": "workspace:*",
|
||||||
|
"@versia-server/tests": "workspace:*",
|
||||||
|
"@versia-server/worker": "workspace:*",
|
||||||
"@versia/client": "workspace:*",
|
"@versia/client": "workspace:*",
|
||||||
"@versia/kit": "workspace:*",
|
|
||||||
"@versia/sdk": "workspace:*",
|
"@versia/sdk": "workspace:*",
|
||||||
"altcha-lib": "^1.3.0",
|
"altcha-lib": "catalog:",
|
||||||
"blurhash": "^2.0.5",
|
"blurhash": "catalog:",
|
||||||
"bullmq": "^5.53.3",
|
"bullmq": "catalog:",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "catalog:",
|
||||||
"clerc": "^0.44.0",
|
"clerc": "catalog:",
|
||||||
"confbox": "^0.2.2",
|
"confbox": "catalog:",
|
||||||
"drizzle-orm": "^0.44.2",
|
"drizzle-orm": "catalog:",
|
||||||
"feed": "^5.1.0",
|
"feed": "catalog:",
|
||||||
"hono": "^4.7.11",
|
"hono": "catalog:",
|
||||||
"hono-openapi": "^0.4.8",
|
"hono-openapi": "catalog:",
|
||||||
"hono-rate-limiter": "^0.4.2",
|
"hono-rate-limiter": "catalog:",
|
||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "catalog:",
|
||||||
"ioredis": "^5.6.1",
|
"ioredis": "catalog:",
|
||||||
"ip-matching": "^2.1.2",
|
"ip-matching": "catalog:",
|
||||||
"iso-639-1": "^3.1.5",
|
"iso-639-1": "catalog:",
|
||||||
"jose": "^6.0.11",
|
"jose": "catalog:",
|
||||||
"linkify-html": "^4.3.1",
|
"linkify-html": "catalog:",
|
||||||
"linkify-string": "^4.3.1",
|
"linkify-string": "catalog:",
|
||||||
"linkifyjs": "^4.3.1",
|
"linkifyjs": "catalog:",
|
||||||
"magic-regexp": "^0.10.0",
|
"magic-regexp": "catalog:",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "catalog:",
|
||||||
"markdown-it-anchor": "^9.2.0",
|
"markdown-it-anchor": "catalog:",
|
||||||
"markdown-it-container": "^4.0.0",
|
"markdown-it-container": "catalog:",
|
||||||
"markdown-it-mathjax3": "^4.3.2",
|
"markdown-it-mathjax3": "catalog:",
|
||||||
"markdown-it-toc-done-right": "^4.2.0",
|
"markdown-it-toc-done-right": "catalog:",
|
||||||
"mime-types": "^3.0.1",
|
"mime-types": "catalog:",
|
||||||
"mitata": "^1.0.34",
|
"mitata": "catalog:",
|
||||||
"oauth4webapi": "^3.5.2",
|
"oauth4webapi": "catalog:",
|
||||||
"ora": "^8.2.0",
|
"ora": "catalog:",
|
||||||
"qs": "^6.14.0",
|
"qs": "catalog:",
|
||||||
"sharp": "^0.34.2",
|
"sharp": "catalog:",
|
||||||
"sonic-channel": "^1.3.1",
|
"sonic-channel": "catalog:",
|
||||||
"string-comparison": "^1.3.0",
|
"string-comparison": "catalog:",
|
||||||
"stringify-entities": "^4.0.4",
|
"stringify-entities": "catalog:",
|
||||||
"unicode-emoji-json": "^0.8.0",
|
"unicode-emoji-json": "catalog:",
|
||||||
"uqr": "^0.1.2",
|
"uqr": "catalog:",
|
||||||
"web-push": "^3.6.7",
|
"web-push": "catalog:",
|
||||||
"xss": "^1.0.15",
|
"xss": "catalog:",
|
||||||
"youch": "^4.1.0-beta.7",
|
"youch": "catalog:",
|
||||||
"zod": "^3.25.64",
|
"zod": "catalog:",
|
||||||
"zod-openapi": "^4.2.4",
|
"zod-openapi": "catalog:",
|
||||||
"zod-validation-error": "^3.5.0"
|
"zod-validation-error": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import { resolve } from "node:path";
|
import { join } from "node:path";
|
||||||
import { getLogger } from "@logtape/logtape";
|
|
||||||
import { Scalar } from "@scalar/hono-api-reference";
|
import { Scalar } from "@scalar/hono-api-reference";
|
||||||
|
import { config } from "@versia-server/config";
|
||||||
|
import { ApiError } from "@versia-server/kit";
|
||||||
|
import { serverLogger } from "@versia-server/logging";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { serveStatic } from "hono/bun";
|
import { serveStatic } from "hono/bun";
|
||||||
|
|
@ -8,29 +10,20 @@ 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 { openAPISpecs } from "hono-openapi";
|
import { generateSpecs } from "hono-openapi";
|
||||||
import { Youch } from "youch";
|
import { Youch } from "youch";
|
||||||
import { applyToHono } from "@/bull-board.ts";
|
import { applyToHono } from "@/bull-board.ts";
|
||||||
import { configureLoggers } from "@/loggers";
|
import pkg from "../../package.json" with { type: "application/json" };
|
||||||
import { sentry } from "@/sentry";
|
import type { ApiRouteExports, HonoEnv } from "../../types/api.ts";
|
||||||
import { config } from "~/config.ts";
|
|
||||||
import pkg from "~/package.json" with { type: "application/json" };
|
|
||||||
import { ApiError } from "./classes/errors/api-error.ts";
|
|
||||||
import { PluginLoader } from "./classes/plugin/loader.ts";
|
|
||||||
import { agentBans } from "./middlewares/agent-bans.ts";
|
import { agentBans } from "./middlewares/agent-bans.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";
|
||||||
import { rateLimit } from "./middlewares/rate-limit.ts";
|
import { rateLimit } from "./middlewares/rate-limit.ts";
|
||||||
|
import { PluginLoader } from "./plugin-loader.ts";
|
||||||
import { routes } from "./routes.ts";
|
import { routes } from "./routes.ts";
|
||||||
import type { ApiRouteExports, HonoEnv } from "./types/api.ts";
|
|
||||||
// Extends Zod with OpenAPI schema generation
|
|
||||||
import "zod-openapi/extend";
|
|
||||||
|
|
||||||
export const appFactory = async (): Promise<Hono<HonoEnv>> => {
|
export const appFactory = async (): Promise<Hono<HonoEnv>> => {
|
||||||
await configureLoggers();
|
|
||||||
const serverLogger = getLogger("server");
|
|
||||||
|
|
||||||
const app = new Hono<HonoEnv>({
|
const app = new Hono<HonoEnv>({
|
||||||
strict: false,
|
strict: false,
|
||||||
});
|
});
|
||||||
|
|
@ -118,13 +111,13 @@ export const appFactory = async (): Promise<Hono<HonoEnv>> => {
|
||||||
const loader = new PluginLoader();
|
const loader = new PluginLoader();
|
||||||
|
|
||||||
const plugins = await loader.loadPlugins(
|
const plugins = await loader.loadPlugins(
|
||||||
resolve("./plugins"),
|
join(import.meta.dir, "plugins"),
|
||||||
config.plugins?.autoload ?? true,
|
config.plugins?.autoload ?? true,
|
||||||
config.plugins?.overrides.enabled,
|
config.plugins?.overrides.enabled,
|
||||||
config.plugins?.overrides.disabled,
|
config.plugins?.overrides.disabled,
|
||||||
);
|
);
|
||||||
|
|
||||||
await PluginLoader.addToApp(plugins, app, serverLogger);
|
await PluginLoader.addToApp(plugins, app);
|
||||||
|
|
||||||
const time2 = performance.now();
|
const time2 = performance.now();
|
||||||
|
|
||||||
|
|
@ -132,22 +125,23 @@ export const appFactory = async (): Promise<Hono<HonoEnv>> => {
|
||||||
(time2 - time1).toFixed(2),
|
(time2 - time1).toFixed(2),
|
||||||
)}ms`}`;
|
)}ms`}`;
|
||||||
|
|
||||||
app.get(
|
const openApiSpecs = await generateSpecs(app, {
|
||||||
"/openapi.json",
|
documentation: {
|
||||||
openAPISpecs(app, {
|
info: {
|
||||||
documentation: {
|
title: "Versia Server API",
|
||||||
info: {
|
version: pkg.version,
|
||||||
title: "Versia Server API",
|
license: {
|
||||||
version: pkg.version,
|
name: "AGPL-3.0",
|
||||||
license: {
|
url: "https://www.gnu.org/licenses/agpl-3.0.html",
|
||||||
name: "AGPL-3.0",
|
|
||||||
url: "https://www.gnu.org/licenses/agpl-3.0.html",
|
|
||||||
},
|
|
||||||
contact: pkg.author,
|
|
||||||
},
|
},
|
||||||
|
contact: pkg.author,
|
||||||
},
|
},
|
||||||
}),
|
},
|
||||||
);
|
});
|
||||||
|
|
||||||
|
app.get("/openapi.json", (context) => {
|
||||||
|
return context.json(openApiSpecs, 200);
|
||||||
|
});
|
||||||
|
|
||||||
app.get(
|
app.get(
|
||||||
"/docs",
|
"/docs",
|
||||||
|
|
@ -193,7 +187,6 @@ export const appFactory = async (): Promise<Hono<HonoEnv>> => {
|
||||||
const youch = new Youch();
|
const youch = new Youch();
|
||||||
console.error(await youch.toANSI(error));
|
console.error(await youch.toANSI(error));
|
||||||
|
|
||||||
sentry?.captureException(error);
|
|
||||||
return c.json(
|
return c.json(
|
||||||
{
|
{
|
||||||
error: "A server error occured",
|
error: "A server error occured",
|
||||||
49
packages/api/build.ts
Normal file
49
packages/api/build.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { readdir } from "node:fs/promises";
|
||||||
|
import { $, build } from "bun";
|
||||||
|
import manifest from "./package.json" with { type: "json" };
|
||||||
|
import { routes } from "./routes.ts";
|
||||||
|
|
||||||
|
console.log("Building...");
|
||||||
|
|
||||||
|
await $`rm -rf dist && mkdir dist`;
|
||||||
|
|
||||||
|
// Get all directories under the plugins/ directory
|
||||||
|
const pluginDirs = await readdir("plugins", { withFileTypes: true });
|
||||||
|
|
||||||
|
await build({
|
||||||
|
entrypoints: [
|
||||||
|
...Object.values(manifest.exports).map((entry) => entry.import),
|
||||||
|
// Force Bun to include endpoints
|
||||||
|
...Object.values(routes),
|
||||||
|
// Include all plugins
|
||||||
|
...pluginDirs
|
||||||
|
.filter((dir) => dir.isDirectory())
|
||||||
|
.map((dir) => `plugins/${dir.name}/index.ts`),
|
||||||
|
],
|
||||||
|
outdir: "dist",
|
||||||
|
target: "bun",
|
||||||
|
splitting: true,
|
||||||
|
minify: true,
|
||||||
|
external: [
|
||||||
|
...Object.keys(manifest.dependencies).filter((dep) =>
|
||||||
|
dep.startsWith("@versia"),
|
||||||
|
),
|
||||||
|
"@bull-board/ui",
|
||||||
|
// Excluded because Standard Schema imports those, but the code is never executed
|
||||||
|
"@valibot/to-json-schema",
|
||||||
|
"effect",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Copying files...");
|
||||||
|
|
||||||
|
// Copy plugin manifests
|
||||||
|
await $`cp plugins/openid/manifest.json dist/plugins/openid/manifest.json`;
|
||||||
|
|
||||||
|
await $`mkdir -p dist/node_modules`;
|
||||||
|
|
||||||
|
// Copy bull-board to dist
|
||||||
|
await $`mkdir -p dist/node_modules/@bull-board`;
|
||||||
|
await $`cp -rL ../../node_modules/@bull-board/ui dist/node_modules/@bull-board/ui`;
|
||||||
|
|
||||||
|
console.log("Build complete!");
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
|
import { config } from "@versia-server/config";
|
||||||
|
import { ApiError } from "@versia-server/kit";
|
||||||
import { createMiddleware } from "hono/factory";
|
import { createMiddleware } from "hono/factory";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
|
||||||
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,5 +1,5 @@
|
||||||
|
import { ApiError } from "@versia-server/kit";
|
||||||
import { createMiddleware } from "hono/factory";
|
import { createMiddleware } from "hono/factory";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
|
||||||
|
|
||||||
export const boundaryCheck = createMiddleware(async (context, next) => {
|
export const boundaryCheck = createMiddleware(async (context, next) => {
|
||||||
// Checks that FormData boundary is present
|
// Checks that FormData boundary is present
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { getLogger } from "@logtape/logtape";
|
import { config } from "@versia-server/config";
|
||||||
|
import { ApiError } from "@versia-server/kit";
|
||||||
|
import { serverLogger } from "@versia-server/logging";
|
||||||
import type { SocketAddress } from "bun";
|
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 { sentry } from "@/sentry";
|
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
|
||||||
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
|
||||||
|
|
@ -22,11 +21,8 @@ export const ipBans = createMiddleware(async (context, next) => {
|
||||||
throw new ApiError(403, "Forbidden");
|
throw new ApiError(403, "Forbidden");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const logger = getLogger("server");
|
serverLogger.error`Error while parsing banned IP "${ip}" `;
|
||||||
|
serverLogger.error`${e}`;
|
||||||
logger.error`Error while parsing banned IP "${ip}" `;
|
|
||||||
logger.error`${e}`;
|
|
||||||
sentry?.captureException(e);
|
|
||||||
|
|
||||||
return context.json(
|
return context.json(
|
||||||
{ error: `A server error occured: ${(e as Error).message}` },
|
{ error: `A server error occured: ${(e as Error).message}` },
|
||||||
26
packages/api/middlewares/logger.ts
Normal file
26
packages/api/middlewares/logger.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { serverLogger } from "@versia-server/logging";
|
||||||
|
import { SHA256 } from "bun";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { createMiddleware } from "hono/factory";
|
||||||
|
|
||||||
|
export const logger = createMiddleware(async (context, next) => {
|
||||||
|
const body = await context.req.raw.clone().text();
|
||||||
|
|
||||||
|
const urlAndMethod = `${chalk.green(context.req.method)} ${chalk.blue(context.req.url)}`;
|
||||||
|
|
||||||
|
const hash = `${chalk.bold("Hash")}: ${chalk.yellow(
|
||||||
|
new SHA256().update(body).digest("hex"),
|
||||||
|
)}`;
|
||||||
|
|
||||||
|
const headers = `${chalk.bold("Headers")}:\n${Array.from(
|
||||||
|
context.req.raw.headers.entries(),
|
||||||
|
)
|
||||||
|
.map(([key, value]) => ` - ${chalk.cyan(key)}: ${chalk.white(value)}`)
|
||||||
|
.join("\n")}`;
|
||||||
|
|
||||||
|
const bodyLog = `${chalk.bold("Body")}: ${chalk.gray(body)}`;
|
||||||
|
|
||||||
|
serverLogger.debug`${urlAndMethod}\n${hash}\n${headers}\n${bodyLog}`;
|
||||||
|
|
||||||
|
await next();
|
||||||
|
});
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
|
import type { ApiError } from "@versia-server/kit";
|
||||||
import { env } from "bun";
|
import { env } from "bun";
|
||||||
import type { MiddlewareHandler } from "hono";
|
import type { MiddlewareHandler } from "hono";
|
||||||
import { rateLimiter } from "hono-rate-limiter";
|
import { rateLimiter } from "hono-rate-limiter";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod/v4";
|
||||||
import type { ApiError } from "~/classes/errors/api-error";
|
|
||||||
import type { HonoEnv } from "~/types/api";
|
import type { HonoEnv } from "~/types/api";
|
||||||
|
|
||||||
// Not exported by hono-rate-limiter
|
// Not exported by hono-rate-limiter
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
import { config } from "@versia-server/config";
|
||||||
import { createMiddleware } from "hono/factory";
|
import { createMiddleware } from "hono/factory";
|
||||||
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
|
||||||
82
packages/api/package.json
Normal file
82
packages/api/package.json
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
{
|
||||||
|
"name": "@versia-server/api",
|
||||||
|
"module": "index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.9.0-alpha.0",
|
||||||
|
"description": "Powerful, configurable and modular federated server using the Versia Protocol.",
|
||||||
|
"homepage": "https://versia.pub",
|
||||||
|
"author": {
|
||||||
|
"email": "contact@cpluspatch.com",
|
||||||
|
"name": "Jesse Wierzbinski",
|
||||||
|
"url": "https://cpluspatch.com"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/versia-pub/server/issues"
|
||||||
|
},
|
||||||
|
"icon": "https://cdn.versia.pub/branding/icon.svg",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"keywords": [
|
||||||
|
"federated",
|
||||||
|
"activitypub",
|
||||||
|
"bun"
|
||||||
|
],
|
||||||
|
"maintainers": [
|
||||||
|
{
|
||||||
|
"email": "contact@cpluspatch.com",
|
||||||
|
"name": "Jesse Wierzbinski",
|
||||||
|
"url": "https://cpluspatch.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/versia-pub/server.git",
|
||||||
|
"directory": "packages/api"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun run --hot index.ts",
|
||||||
|
"build": "bun run build.ts",
|
||||||
|
"schema:generate": "bun run classes/config/to-json-schema.ts > config/config.schema.json && bun run packages/kit/json-schema.ts > packages/kit/manifest.schema.json",
|
||||||
|
"docs:dev": "vitepress dev docs",
|
||||||
|
"docs:build": "vitepress build docs",
|
||||||
|
"docs:preview": "vitepress preview docs"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./app.ts"
|
||||||
|
},
|
||||||
|
"./setup": {
|
||||||
|
"import": "./setup.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@versia-server/config": "workspace:*",
|
||||||
|
"@versia-server/tests": "workspace:*",
|
||||||
|
"@versia-server/kit": "workspace:*",
|
||||||
|
"@versia-server/logging": "workspace:*",
|
||||||
|
"@versia/client": "workspace:*",
|
||||||
|
"@versia/sdk": "workspace:*",
|
||||||
|
"youch": "catalog:",
|
||||||
|
"hono": "catalog:",
|
||||||
|
"hono-openapi": "catalog:",
|
||||||
|
"zod": "catalog:",
|
||||||
|
"drizzle-orm": "catalog:",
|
||||||
|
"string-comparison": "catalog:",
|
||||||
|
"bun-bagel": "catalog:",
|
||||||
|
"chalk": "catalog:",
|
||||||
|
"unicode-emoji-json": "catalog:",
|
||||||
|
"sharp": "catalog:",
|
||||||
|
"iso-639-1": "catalog:",
|
||||||
|
"jose": "catalog:",
|
||||||
|
"zod-openapi": "catalog:",
|
||||||
|
"@scalar/hono-api-reference": "catalog:",
|
||||||
|
"hono-rate-limiter": "catalog:",
|
||||||
|
"ip-matching": "catalog:",
|
||||||
|
"qs": "catalog:",
|
||||||
|
"altcha-lib": "catalog:",
|
||||||
|
"@hono/standard-validator": "catalog:",
|
||||||
|
"zod-validation-error": "catalog:",
|
||||||
|
"confbox": "catalog:",
|
||||||
|
"oauth4webapi": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,22 +1,19 @@
|
||||||
import { readdir } from "node:fs/promises";
|
import { readdir } from "node:fs/promises";
|
||||||
import { getLogger, type Logger } from "@logtape/logtape";
|
import { config } from "@versia-server/config";
|
||||||
|
import { type Manifest, manifestSchema, Plugin } from "@versia-server/kit";
|
||||||
|
import { pluginLogger, serverLogger } from "@versia-server/logging";
|
||||||
import { file, sleep } from "bun";
|
import { file, sleep } from "bun";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { parseJSON5, parseJSONC } from "confbox";
|
import { parseJSON5, parseJSONC } from "confbox";
|
||||||
import type { Hono } from "hono";
|
import type { Hono } from "hono";
|
||||||
import type { ZodTypeAny } from "zod";
|
import type { ZodTypeAny } from "zod/v4";
|
||||||
import { fromZodError, type ValidationError } from "zod-validation-error";
|
import { fromZodError, type ValidationError } from "zod-validation-error";
|
||||||
import { config } from "~/config.ts";
|
|
||||||
import { Plugin } from "~/packages/plugin-kit/plugin";
|
|
||||||
import { type Manifest, manifestSchema } from "~/packages/plugin-kit/schema";
|
|
||||||
import type { HonoEnv } from "~/types/api";
|
import type { HonoEnv } from "~/types/api";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to manage plugins.
|
* Class to manage plugins.
|
||||||
*/
|
*/
|
||||||
export class PluginLoader {
|
export class PluginLoader {
|
||||||
private logger = getLogger("plugin");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all directories in a given directory.
|
* Get all directories in a given directory.
|
||||||
* @param {string} dir - The directory to search.
|
* @param {string} dir - The directory to search.
|
||||||
|
|
@ -56,7 +53,7 @@ export class PluginLoader {
|
||||||
* @returns {Promise<unknown>} - The parsed manifest content.
|
* @returns {Promise<unknown>} - The parsed manifest content.
|
||||||
* @throws Will throw an error if the manifest file cannot be parsed.
|
* @throws Will throw an error if the manifest file cannot be parsed.
|
||||||
*/
|
*/
|
||||||
private async parseManifestFile(
|
private static async parseManifestFile(
|
||||||
manifestPath: string,
|
manifestPath: string,
|
||||||
manifestFile: string,
|
manifestFile: string,
|
||||||
): Promise<unknown> {
|
): Promise<unknown> {
|
||||||
|
|
@ -75,8 +72,7 @@ export class PluginLoader {
|
||||||
|
|
||||||
throw new Error(`Unsupported manifest file type: ${manifestFile}`);
|
throw new Error(`Unsupported manifest file type: ${manifestFile}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger
|
pluginLogger.fatal`Could not parse plugin manifest ${chalk.blue(manifestPath)} as ${manifestFile.split(".").pop()?.toUpperCase()}.`;
|
||||||
.fatal`Could not parse plugin manifest ${chalk.blue(manifestPath)} as ${manifestFile.split(".").pop()?.toUpperCase()}.`;
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -112,7 +108,10 @@ export class PluginLoader {
|
||||||
* @returns {Promise<Manifest>} - The parsed manifest object.
|
* @returns {Promise<Manifest>} - The parsed manifest object.
|
||||||
* @throws Will throw an error if the manifest file is missing or invalid.
|
* @throws Will throw an error if the manifest file is missing or invalid.
|
||||||
*/
|
*/
|
||||||
public async parseManifest(dir: string, plugin: string): Promise<Manifest> {
|
public static async parseManifest(
|
||||||
|
dir: string,
|
||||||
|
plugin: string,
|
||||||
|
): Promise<Manifest> {
|
||||||
const manifestFile = await PluginLoader.findManifestFile(
|
const manifestFile = await PluginLoader.findManifestFile(
|
||||||
`${dir}/${plugin}`,
|
`${dir}/${plugin}`,
|
||||||
);
|
);
|
||||||
|
|
@ -122,7 +121,7 @@ export class PluginLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
const manifestPath = `${dir}/${plugin}/${manifestFile}`;
|
const manifestPath = `${dir}/${plugin}/${manifestFile}`;
|
||||||
const manifest = await this.parseManifestFile(
|
const manifest = await PluginLoader.parseManifestFile(
|
||||||
manifestPath,
|
manifestPath,
|
||||||
manifestFile,
|
manifestFile,
|
||||||
);
|
);
|
||||||
|
|
@ -130,8 +129,7 @@ export class PluginLoader {
|
||||||
const result = await manifestSchema.safeParseAsync(manifest);
|
const result = await manifestSchema.safeParseAsync(manifest);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
this.logger
|
pluginLogger.fatal`Plugin manifest ${chalk.blue(manifestPath)} is invalid.`;
|
||||||
.fatal`Plugin manifest ${chalk.blue(manifestPath)} is invalid.`;
|
|
||||||
throw fromZodError(result.error);
|
throw fromZodError(result.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,7 +143,7 @@ export class PluginLoader {
|
||||||
* @returns {Promise<Plugin<ZodTypeAny>>} - The loaded Plugin instance.
|
* @returns {Promise<Plugin<ZodTypeAny>>} - The loaded Plugin instance.
|
||||||
* @throws Will throw an error if the entrypoint's default export is not a Plugin.
|
* @throws Will throw an error if the entrypoint's default export is not a Plugin.
|
||||||
*/
|
*/
|
||||||
public async loadPlugin(
|
public static async loadPlugin(
|
||||||
dir: string,
|
dir: string,
|
||||||
entrypoint: string,
|
entrypoint: string,
|
||||||
): Promise<Plugin<ZodTypeAny>> {
|
): Promise<Plugin<ZodTypeAny>> {
|
||||||
|
|
@ -155,8 +153,7 @@ export class PluginLoader {
|
||||||
return plugin;
|
return plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger
|
pluginLogger.fatal`Default export of entrypoint ${chalk.blue(entrypoint)} at ${chalk.blue(dir)} is not a Plugin.`;
|
||||||
.fatal`Default export of entrypoint ${chalk.blue(entrypoint)} at ${chalk.blue(dir)} is not a Plugin.`;
|
|
||||||
|
|
||||||
throw new Error("Entrypoint is not a Plugin");
|
throw new Error("Entrypoint is not a Plugin");
|
||||||
}
|
}
|
||||||
|
|
@ -178,14 +175,13 @@ export class PluginLoader {
|
||||||
const disabledOn = (disabled?.length ?? 0) > 0;
|
const disabledOn = (disabled?.length ?? 0) > 0;
|
||||||
|
|
||||||
if (enabledOn && disabledOn) {
|
if (enabledOn && disabledOn) {
|
||||||
this.logger
|
pluginLogger.fatal`Both enabled and disabled lists are specified. Only one of them can be used.`;
|
||||||
.fatal`Both enabled and disabled lists are specified. Only one of them can be used.`;
|
|
||||||
throw new Error("Invalid configuration");
|
throw new Error("Invalid configuration");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
plugins.map(async (plugin) => {
|
plugins.map(async (plugin) => {
|
||||||
const manifest = await this.parseManifest(dir, plugin);
|
const manifest = await PluginLoader.parseManifest(dir, plugin);
|
||||||
|
|
||||||
// If autoload is disabled, only load plugins explicitly enabled
|
// If autoload is disabled, only load plugins explicitly enabled
|
||||||
if (
|
if (
|
||||||
|
|
@ -204,7 +200,7 @@ export class PluginLoader {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pluginInstance = await this.loadPlugin(
|
const pluginInstance = await PluginLoader.loadPlugin(
|
||||||
dir,
|
dir,
|
||||||
`${plugin}/index`,
|
`${plugin}/index`,
|
||||||
);
|
);
|
||||||
|
|
@ -220,10 +216,9 @@ export class PluginLoader {
|
||||||
plugin: Plugin<ZodTypeAny>;
|
plugin: Plugin<ZodTypeAny>;
|
||||||
}[],
|
}[],
|
||||||
app: Hono<HonoEnv>,
|
app: Hono<HonoEnv>,
|
||||||
logger: Logger,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
for (const data of plugins) {
|
for (const data of plugins) {
|
||||||
logger.info`Loading plugin ${chalk.blueBright(data.manifest.name)} ${chalk.blueBright(data.manifest.version)} ${chalk.gray(`[${plugins.indexOf(data) + 1}/${plugins.length}]`)}`;
|
serverLogger.info`Loading plugin ${chalk.blueBright(data.manifest.name)} ${chalk.blueBright(data.manifest.version)} ${chalk.gray(`[${plugins.indexOf(data) + 1}/${plugins.length}]`)}`;
|
||||||
|
|
||||||
const time1 = performance.now();
|
const time1 = performance.now();
|
||||||
|
|
||||||
|
|
@ -233,13 +228,13 @@ export class PluginLoader {
|
||||||
config.plugins?.config?.[data.manifest.name],
|
config.plugins?.config?.[data.manifest.name],
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.fatal`Error encountered while loading plugin ${chalk.blueBright(data.manifest.name)} ${chalk.blueBright(data.manifest.version)} configuration.`;
|
serverLogger.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.`;
|
serverLogger.fatal`This is due to invalid, missing or incomplete configuration.`;
|
||||||
logger.fatal`Put your configuration at ${chalk.blueBright(
|
serverLogger.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:`;
|
serverLogger.fatal`Here is the error message, please fix the configuration file accordingly:`;
|
||||||
logger.fatal`${(e as ValidationError).message}`;
|
serverLogger.fatal`${(e as ValidationError).message}`;
|
||||||
|
|
||||||
await sleep(Number.POSITIVE_INFINITY);
|
await sleep(Number.POSITIVE_INFINITY);
|
||||||
}
|
}
|
||||||
|
|
@ -251,7 +246,7 @@ export class PluginLoader {
|
||||||
|
|
||||||
const time3 = performance.now();
|
const time3 = performance.now();
|
||||||
|
|
||||||
logger.info`Plugin ${chalk.blueBright(data.manifest.name)} ${chalk.blueBright(
|
serverLogger.info`Plugin ${chalk.blueBright(data.manifest.name)} ${chalk.blueBright(
|
||||||
data.manifest.version,
|
data.manifest.version,
|
||||||
)} loaded in ${chalk.gray(
|
)} loaded in ${chalk.gray(
|
||||||
`${(time2 - time1).toFixed(2)}ms`,
|
`${(time2 - time1).toFixed(2)}ms`,
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import { RolePermission } from "@versia/client/schemas";
|
import { RolePermission } from "@versia/client/schemas";
|
||||||
import { Hooks, Plugin } from "@versia/kit";
|
import { keyPair, sensitiveString, url } from "@versia-server/config";
|
||||||
import { User } from "@versia/kit/db";
|
import { ApiError, Hooks, Plugin } from "@versia-server/kit";
|
||||||
|
import { User } from "@versia-server/kit/db";
|
||||||
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 { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
import { keyPair, sensitiveString, url } from "~/classes/config/schema.ts";
|
|
||||||
import { ApiError } from "~/classes/errors/api-error.ts";
|
|
||||||
import authorizeRoute from "./routes/authorize.ts";
|
import authorizeRoute from "./routes/authorize.ts";
|
||||||
import jwksRoute from "./routes/jwks.ts";
|
import jwksRoute from "./routes/jwks.ts";
|
||||||
import ssoLoginCallbackRoute from "./routes/oauth/callback.ts";
|
import ssoLoginCallbackRoute from "./routes/oauth/callback.ts";
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/versia-pub/server/refs/heads/main/packages/plugin-kit/manifest.schema.json",
|
"$schema": "https://raw.githubusercontent.com/versia-pub/server/refs/heads/main/packages/kit/manifest.schema.json",
|
||||||
"name": "@versia/openid",
|
"name": "@versia/openid",
|
||||||
"description": "OpenID authentication.",
|
"description": "OpenID authentication.",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { RolePermission } from "@versia/client/schemas";
|
import { RolePermission } from "@versia/client/schemas";
|
||||||
import { Application } from "@versia/kit/db";
|
import { config } from "@versia-server/config";
|
||||||
|
import { Application } from "@versia-server/kit/db";
|
||||||
|
import { fakeRequest, getTestUsers } from "@versia-server/tests";
|
||||||
import { randomUUIDv7 } from "bun";
|
import { randomUUIDv7 } from "bun";
|
||||||
import { SignJWT } from "jose";
|
import { SignJWT } from "jose";
|
||||||
import { randomString } from "@/math";
|
import { randomString } from "@/math";
|
||||||
import { config } from "~/config.ts";
|
|
||||||
import { fakeRequest, getTestUsers } from "~/tests/utils";
|
|
||||||
|
|
||||||
const { deleteUsers, tokens, users } = await getTestUsers(1);
|
const { deleteUsers, tokens, users } = await getTestUsers(1);
|
||||||
const privateKey = await crypto.subtle.importKey(
|
const privateKey = await crypto.subtle.importKey(
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import { RolePermission } from "@versia/client/schemas";
|
import { RolePermission } from "@versia/client/schemas";
|
||||||
import { Application, Token, User } from "@versia/kit/db";
|
import { auth, handleZodError, jsonOrForm } from "@versia-server/kit/api";
|
||||||
|
import { Application, Token, User } from "@versia-server/kit/db";
|
||||||
import { randomUUIDv7 } from "bun";
|
import { randomUUIDv7 } from "bun";
|
||||||
import { describeRoute } from "hono-openapi";
|
import { describeRoute, validator } from "hono-openapi";
|
||||||
import { validator } from "hono-openapi/zod";
|
|
||||||
import { type JWTPayload, jwtVerify, SignJWT } from "jose";
|
import { type JWTPayload, jwtVerify, SignJWT } from "jose";
|
||||||
import { JOSEError } from "jose/errors";
|
import { JOSEError } from "jose/errors";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
import { auth, handleZodError, jsonOrForm } from "@/api";
|
|
||||||
import { randomString } from "@/math";
|
import { randomString } from "@/math";
|
||||||
import { errorRedirect, errors } from "../errors.ts";
|
import { errorRedirect, errors } from "../errors.ts";
|
||||||
import type { PluginType } from "../index.ts";
|
import type { PluginType } from "../index.ts";
|
||||||
|
|
@ -50,7 +49,6 @@ export default (plugin: PluginType): void =>
|
||||||
.object({
|
.object({
|
||||||
scope: z.string().optional(),
|
scope: z.string().optional(),
|
||||||
redirect_uri: z
|
redirect_uri: z
|
||||||
.string()
|
|
||||||
.url()
|
.url()
|
||||||
.optional()
|
.optional()
|
||||||
.or(z.literal("urn:ietf:wg:oauth:2.0:oob")),
|
.or(z.literal("urn:ietf:wg:oauth:2.0:oob")),
|
||||||
|
|
@ -141,7 +139,7 @@ export default (plugin: PluginType): void =>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!z.string().uuid().safeParse(sub).success) {
|
if (!z.uuid().safeParse(sub).success) {
|
||||||
return errorRedirect(
|
return errorRedirect(
|
||||||
context,
|
context,
|
||||||
errors.InvalidSub,
|
errors.InvalidSub,
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { Application } from "@versia/kit/db";
|
import { Application } from "@versia-server/kit/db";
|
||||||
|
import { fakeRequest } from "@versia-server/tests";
|
||||||
import { randomUUIDv7 } from "bun";
|
import { randomUUIDv7 } from "bun";
|
||||||
import { fakeRequest } from "~/tests/utils";
|
|
||||||
|
|
||||||
const application = await Application.insert({
|
const application = await Application.insert({
|
||||||
id: randomUUIDv7(),
|
id: randomUUIDv7(),
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { describeRoute } from "hono-openapi";
|
import { auth } from "@versia-server/kit/api";
|
||||||
import { resolver } from "hono-openapi/zod";
|
import { describeRoute, resolver } from "hono-openapi";
|
||||||
import { exportJWK } from "jose";
|
import { exportJWK } from "jose";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
import { auth } from "@/api";
|
|
||||||
import type { PluginType } from "../index.ts";
|
import type { PluginType } from "../index.ts";
|
||||||
|
|
||||||
export default (plugin: PluginType): void => {
|
export default (plugin: PluginType): void => {
|
||||||
|
|
@ -1,19 +1,20 @@
|
||||||
import {
|
import {
|
||||||
Account as AccountSchema,
|
Account as AccountSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
|
zBoolean,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { db, Media, Token, User } from "@versia/kit/db";
|
import { ApiError } from "@versia-server/kit";
|
||||||
import { and, eq, isNull, type SQL } from "@versia/kit/drizzle";
|
import { handleZodError } from "@versia-server/kit/api";
|
||||||
import { OpenIdAccounts, Users } from "@versia/kit/tables";
|
import { db, Media, Token, User } from "@versia-server/kit/db";
|
||||||
|
import { searchManager } from "@versia-server/kit/search";
|
||||||
|
import { OpenIdAccounts, Users } from "@versia-server/kit/tables";
|
||||||
import { randomUUIDv7 } from "bun";
|
import { randomUUIDv7 } from "bun";
|
||||||
|
import { and, eq, isNull, type SQL } from "drizzle-orm";
|
||||||
import { setCookie } from "hono/cookie";
|
import { setCookie } from "hono/cookie";
|
||||||
import { describeRoute } from "hono-openapi";
|
import { describeRoute, validator } from "hono-openapi";
|
||||||
import { validator } from "hono-openapi/zod";
|
|
||||||
import { SignJWT } from "jose";
|
import { SignJWT } from "jose";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
import { handleZodError } from "@/api";
|
|
||||||
import { randomString } from "@/math.ts";
|
import { randomString } from "@/math.ts";
|
||||||
import { ApiError } from "~/classes/errors/api-error.ts";
|
|
||||||
import type { PluginType } from "../../index.ts";
|
import type { PluginType } from "../../index.ts";
|
||||||
import { automaticOidcFlow } from "../../utils.ts";
|
import { automaticOidcFlow } from "../../utils.ts";
|
||||||
|
|
||||||
|
|
@ -46,13 +47,8 @@ export default (plugin: PluginType): void => {
|
||||||
z.object({
|
z.object({
|
||||||
client_id: z.string().optional(),
|
client_id: z.string().optional(),
|
||||||
flow: z.string(),
|
flow: z.string(),
|
||||||
link: z
|
link: zBoolean.optional(),
|
||||||
.string()
|
user_id: z.uuid().optional(),
|
||||||
.transform((v) =>
|
|
||||||
["true", "1", "on"].includes(v.toLowerCase()),
|
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
user_id: z.string().uuid().optional(),
|
|
||||||
}),
|
}),
|
||||||
handleZodError,
|
handleZodError,
|
||||||
),
|
),
|
||||||
|
|
@ -242,6 +238,9 @@ export default (plugin: PluginType): void => {
|
||||||
avatar: avatar ?? undefined,
|
avatar: avatar ?? undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add to search index
|
||||||
|
await searchManager.addUser(user);
|
||||||
|
|
||||||
// Link account
|
// Link account
|
||||||
await db.insert(OpenIdAccounts).values({
|
await db.insert(OpenIdAccounts).values({
|
||||||
id: randomUUIDv7(),
|
id: randomUUIDv7(),
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { Application, Token } from "@versia/kit/db";
|
import { Application, Token } from "@versia-server/kit/db";
|
||||||
|
import { fakeRequest, getTestUsers } from "@versia-server/tests";
|
||||||
import { randomUUIDv7 } from "bun";
|
import { randomUUIDv7 } from "bun";
|
||||||
import { fakeRequest, getTestUsers } from "~/tests/utils";
|
|
||||||
|
|
||||||
const { deleteUsers, users } = await getTestUsers(1);
|
const { deleteUsers, users } = await getTestUsers(1);
|
||||||
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { db, Token } from "@versia/kit/db";
|
import { handleZodError, jsonOrForm } from "@versia-server/kit/api";
|
||||||
import { and, eq } from "@versia/kit/drizzle";
|
import { db, Token } from "@versia-server/kit/db";
|
||||||
import { Tokens } from "@versia/kit/tables";
|
import { Tokens } from "@versia-server/kit/tables";
|
||||||
import { describeRoute } from "hono-openapi";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { resolver, validator } from "hono-openapi/zod";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
import { handleZodError, jsonOrForm } from "@/api";
|
|
||||||
import type { PluginType } from "../../index.ts";
|
import type { PluginType } from "../../index.ts";
|
||||||
|
|
||||||
export default (plugin: PluginType): void => {
|
export default (plugin: PluginType): void => {
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
import { Application, db } from "@versia/kit/db";
|
import { handleZodError } from "@versia-server/kit/api";
|
||||||
import { OpenIdLoginFlows } from "@versia/kit/tables";
|
import { Application, db } from "@versia-server/kit/db";
|
||||||
|
import { OpenIdLoginFlows } from "@versia-server/kit/tables";
|
||||||
import { randomUUIDv7 } from "bun";
|
import { randomUUIDv7 } from "bun";
|
||||||
import { describeRoute } from "hono-openapi";
|
import { describeRoute, validator } from "hono-openapi";
|
||||||
import { validator } from "hono-openapi/zod";
|
|
||||||
import {
|
import {
|
||||||
calculatePKCECodeChallenge,
|
calculatePKCECodeChallenge,
|
||||||
discoveryRequest,
|
discoveryRequest,
|
||||||
generateRandomCodeVerifier,
|
generateRandomCodeVerifier,
|
||||||
processDiscoveryResponse,
|
processDiscoveryResponse,
|
||||||
} from "oauth4webapi";
|
} from "oauth4webapi";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
import { handleZodError } from "@/api.ts";
|
|
||||||
import type { PluginType } from "../../index.ts";
|
import type { PluginType } from "../../index.ts";
|
||||||
import { oauthRedirectUri } from "../../utils.ts";
|
import { oauthRedirectUri } from "../../utils.ts";
|
||||||
|
|
||||||
|
|
@ -34,7 +33,7 @@ export default (plugin: PluginType): void => {
|
||||||
z.object({
|
z.object({
|
||||||
issuer: z.string(),
|
issuer: z.string(),
|
||||||
client_id: z.string().optional(),
|
client_id: z.string().optional(),
|
||||||
redirect_uri: z.string().url().optional(),
|
redirect_uri: z.url().optional(),
|
||||||
scope: z.string().optional(),
|
scope: z.string().optional(),
|
||||||
response_type: z.enum(["code"]).optional(),
|
response_type: z.enum(["code"]).optional(),
|
||||||
}),
|
}),
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { Application, Token } from "@versia/kit/db";
|
import { Application, Token } from "@versia-server/kit/db";
|
||||||
|
import { fakeRequest, getTestUsers } from "@versia-server/tests";
|
||||||
import { randomUUIDv7 } from "bun";
|
import { randomUUIDv7 } from "bun";
|
||||||
import { fakeRequest, getTestUsers } from "~/tests/utils";
|
|
||||||
|
|
||||||
const { deleteUsers, users } = await getTestUsers(1);
|
const { deleteUsers, users } = await getTestUsers(1);
|
||||||
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { Application, Token } from "@versia/kit/db";
|
import { handleZodError, jsonOrForm } from "@versia-server/kit/api";
|
||||||
import { and, eq } from "@versia/kit/drizzle";
|
import { Application, Token } from "@versia-server/kit/db";
|
||||||
import { Tokens } from "@versia/kit/tables";
|
import { Tokens } from "@versia-server/kit/tables";
|
||||||
import { describeRoute } from "hono-openapi";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { resolver, validator } from "hono-openapi/zod";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
import { handleZodError, jsonOrForm } from "@/api";
|
|
||||||
import type { PluginType } from "../../index.ts";
|
import type { PluginType } from "../../index.ts";
|
||||||
|
|
||||||
export default (plugin: PluginType): void => {
|
export default (plugin: PluginType): void => {
|
||||||
|
|
@ -80,7 +79,7 @@ export default (plugin: PluginType): void => {
|
||||||
client_secret: z.string().optional(),
|
client_secret: z.string().optional(),
|
||||||
username: z.string().trim().optional(),
|
username: z.string().trim().optional(),
|
||||||
password: z.string().trim().optional(),
|
password: z.string().trim().optional(),
|
||||||
redirect_uri: z.string().url().optional(),
|
redirect_uri: z.url().optional(),
|
||||||
refresh_token: z.string().optional(),
|
refresh_token: z.string().optional(),
|
||||||
scope: z.string().optional(),
|
scope: z.string().optional(),
|
||||||
assertion: z.string().optional(),
|
assertion: z.string().optional(),
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { fakeRequest, getTestUsers } from "~/tests/utils";
|
import { fakeRequest, getTestUsers } from "@versia-server/tests";
|
||||||
|
|
||||||
const { deleteUsers, tokens } = await getTestUsers(1);
|
const { deleteUsers, tokens } = await getTestUsers(1);
|
||||||
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
import { RolePermission } from "@versia/client/schemas";
|
import { RolePermission } from "@versia/client/schemas";
|
||||||
import { db } from "@versia/kit/db";
|
import { ApiError } from "@versia-server/kit";
|
||||||
import { and, eq, type SQL } from "@versia/kit/drizzle";
|
import { auth, handleZodError } from "@versia-server/kit/api";
|
||||||
import { OpenIdAccounts } from "@versia/kit/tables";
|
import { db } from "@versia-server/kit/db";
|
||||||
import { describeRoute } from "hono-openapi";
|
import { OpenIdAccounts } from "@versia-server/kit/tables";
|
||||||
import { resolver, validator } from "hono-openapi/zod";
|
import { and, eq, type SQL } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { auth, handleZodError } from "@/api";
|
import { z } from "zod/v4";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import type { PluginType } from "../../../index.ts";
|
||||||
import type { PluginType } from "~/plugins/openid";
|
|
||||||
|
|
||||||
export default (plugin: PluginType): void => {
|
export default (plugin: PluginType): void => {
|
||||||
plugin.registerRoute("/api/v1/sso/:id", (app) => {
|
plugin.registerRoute("/api/v1/sso/:id", (app) => {
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { fakeRequest, getTestUsers } from "~/tests/utils";
|
import { fakeRequest, getTestUsers } from "@versia-server/tests";
|
||||||
|
|
||||||
const { deleteUsers, tokens } = await getTestUsers(1);
|
const { deleteUsers, tokens } = await getTestUsers(1);
|
||||||
|
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
import { RolePermission } from "@versia/client/schemas";
|
import { RolePermission } from "@versia/client/schemas";
|
||||||
import { Application, db } from "@versia/kit/db";
|
import { ApiError } from "@versia-server/kit";
|
||||||
import { OpenIdLoginFlows } from "@versia/kit/tables";
|
import { auth, handleZodError } from "@versia-server/kit/api";
|
||||||
|
import { Application, db } from "@versia-server/kit/db";
|
||||||
|
import { OpenIdLoginFlows } from "@versia-server/kit/tables";
|
||||||
import { randomUUIDv7 } from "bun";
|
import { randomUUIDv7 } from "bun";
|
||||||
import { describeRoute } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { resolver, validator } from "hono-openapi/zod";
|
|
||||||
import {
|
import {
|
||||||
calculatePKCECodeChallenge,
|
calculatePKCECodeChallenge,
|
||||||
generateRandomCodeVerifier,
|
generateRandomCodeVerifier,
|
||||||
} from "oauth4webapi";
|
} from "oauth4webapi";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
import { auth, handleZodError } from "@/api";
|
|
||||||
import { ApiError } from "~/classes/errors/api-error.ts";
|
|
||||||
import type { PluginType } from "../../index.ts";
|
import type { PluginType } from "../../index.ts";
|
||||||
import { oauthDiscoveryRequest, oauthRedirectUri } from "../../utils.ts";
|
import { oauthDiscoveryRequest, oauthRedirectUri } from "../../utils.ts";
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { type Application, db } from "@versia/kit/db";
|
import { type Application, db } from "@versia-server/kit/db";
|
||||||
import { eq, type InferSelectModel, type SQL } from "@versia/kit/drizzle";
|
import type { OpenIdLoginFlows } from "@versia-server/kit/tables";
|
||||||
import type { OpenIdLoginFlows } from "@versia/kit/tables";
|
import { eq, type InferSelectModel, type SQL } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
type AuthorizationResponseError,
|
type AuthorizationResponseError,
|
||||||
type AuthorizationServer,
|
type AuthorizationServer,
|
||||||
|
|
@ -165,7 +165,7 @@ export const automaticOidcFlow = async (
|
||||||
|
|
||||||
const authServer = await getAuthServer(issuerUrl);
|
const authServer = await getAuthServer(issuerUrl);
|
||||||
|
|
||||||
const parameters = await getParameters(
|
const parameters = getParameters(
|
||||||
authServer,
|
authServer,
|
||||||
issuer.client_id,
|
issuer.client_id,
|
||||||
currentUrl,
|
currentUrl,
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
|
import { join } from "node:path";
|
||||||
import { FileSystemRouter } from "bun";
|
import { FileSystemRouter } from "bun";
|
||||||
|
|
||||||
// Returns the route filesystem path when given a URL
|
// Returns the route filesystem path when given a URL
|
||||||
export const routeMatcher = new FileSystemRouter({
|
export const routeMatcher = new FileSystemRouter({
|
||||||
style: "nextjs",
|
style: "nextjs",
|
||||||
dir: "api",
|
dir: join(import.meta.dir, "routes"),
|
||||||
fileExtensions: [".ts", ".js"],
|
fileExtensions: [".ts", ".js"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { Application } from "@versia/kit/db";
|
import { config } from "@versia-server/config";
|
||||||
|
import { Application } from "@versia-server/kit/db";
|
||||||
|
import { fakeRequest, getTestUsers } from "@versia-server/tests";
|
||||||
import { randomUUIDv7 } from "bun";
|
import { randomUUIDv7 } from "bun";
|
||||||
import { randomString } from "@/math";
|
import { randomString } from "@/math";
|
||||||
import { config } from "~/config.ts";
|
|
||||||
import { fakeRequest, getTestUsers } from "~/tests/utils";
|
|
||||||
|
|
||||||
const { users, deleteUsers, passwords } = await getTestUsers(1);
|
const { users, deleteUsers, passwords } = await getTestUsers(1);
|
||||||
|
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
import { Application, User } from "@versia/kit/db";
|
import { config } from "@versia-server/config";
|
||||||
import { Users } from "@versia/kit/tables";
|
import { ApiError } from "@versia-server/kit";
|
||||||
|
import { apiRoute, handleZodError } from "@versia-server/kit/api";
|
||||||
|
import { Application, User } from "@versia-server/kit/db";
|
||||||
|
import { Users } from "@versia-server/kit/tables";
|
||||||
import { password as bunPassword } from "bun";
|
import { password as bunPassword } from "bun";
|
||||||
import { eq, or } from "drizzle-orm";
|
import { eq, or } from "drizzle-orm";
|
||||||
import type { Context } from "hono";
|
import type { Context } from "hono";
|
||||||
import { setCookie } from "hono/cookie";
|
import { setCookie } from "hono/cookie";
|
||||||
import { describeRoute } from "hono-openapi";
|
import { describeRoute, validator } from "hono-openapi";
|
||||||
import { validator } from "hono-openapi/zod";
|
|
||||||
import { SignJWT } from "jose";
|
import { SignJWT } from "jose";
|
||||||
import { z } from "zod";
|
import { z } from "zod/v4";
|
||||||
import { apiRoute, handleZodError } from "@/api";
|
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
|
||||||
import { config } from "~/config.ts";
|
|
||||||
|
|
||||||
const returnError = (
|
const returnError = (
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|
@ -59,7 +58,7 @@ export default apiRoute((app) =>
|
||||||
"query",
|
"query",
|
||||||
z.object({
|
z.object({
|
||||||
scope: z.string().optional(),
|
scope: z.string().optional(),
|
||||||
redirect_uri: z.string().url().optional(),
|
redirect_uri: z.url().optional(),
|
||||||
response_type: z.enum([
|
response_type: z.enum([
|
||||||
"code",
|
"code",
|
||||||
"token",
|
"token",
|
||||||
|
|
@ -90,7 +89,6 @@ export default apiRoute((app) =>
|
||||||
"form",
|
"form",
|
||||||
z.object({
|
z.object({
|
||||||
identifier: z
|
identifier: z
|
||||||
.string()
|
|
||||||
.email()
|
.email()
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.or(z.string().toLowerCase()),
|
.or(z.string().toLowerCase()),
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import { db } from "@versia/kit/db";
|
import { config } from "@versia-server/config";
|
||||||
import { Applications, Tokens } from "@versia/kit/tables";
|
import { apiRoute, handleZodError } from "@versia-server/kit/api";
|
||||||
|
import { db } from "@versia-server/kit/db";
|
||||||
|
import { Applications, Tokens } from "@versia-server/kit/tables";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { describeRoute } from "hono-openapi";
|
import { describeRoute, validator } from "hono-openapi";
|
||||||
import { validator } from "hono-openapi/zod";
|
import { z } from "zod/v4";
|
||||||
import { z } from "zod";
|
|
||||||
import { apiRoute, handleZodError } from "@/api";
|
|
||||||
import { config } from "~/config.ts";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OAuth Code flow
|
* OAuth Code flow
|
||||||
|
|
@ -28,7 +27,7 @@ export default apiRoute((app) =>
|
||||||
validator(
|
validator(
|
||||||
"query",
|
"query",
|
||||||
z.object({
|
z.object({
|
||||||
redirect_uri: z.string().url(),
|
redirect_uri: z.url(),
|
||||||
client_id: z.string(),
|
client_id: z.string(),
|
||||||
code: z.string(),
|
code: z.string(),
|
||||||
}),
|
}),
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { Application } from "@versia/kit/db";
|
import { config } from "@versia-server/config";
|
||||||
|
import { Application } from "@versia-server/kit/db";
|
||||||
|
import { fakeRequest, getTestUsers } from "@versia-server/tests";
|
||||||
import { randomUUIDv7 } from "bun";
|
import { randomUUIDv7 } from "bun";
|
||||||
import { randomString } from "@/math";
|
import { randomString } from "@/math";
|
||||||
import { config } from "~/config.ts";
|
|
||||||
import { fakeRequest, getTestUsers } from "~/tests/utils";
|
|
||||||
|
|
||||||
const { users, deleteUsers, passwords } = await getTestUsers(1);
|
const { users, deleteUsers, passwords } = await getTestUsers(1);
|
||||||
const token = randomString(32, "hex");
|
const token = randomString(32, "hex");
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
import { User } from "@versia/kit/db";
|
import { config } from "@versia-server/config";
|
||||||
import { Users } from "@versia/kit/tables";
|
import { apiRoute, handleZodError } from "@versia-server/kit/api";
|
||||||
|
import { User } from "@versia-server/kit/db";
|
||||||
|
import { Users } from "@versia-server/kit/tables";
|
||||||
import { password as bunPassword } from "bun";
|
import { password as bunPassword } from "bun";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import type { Context } from "hono";
|
import type { Context } from "hono";
|
||||||
import { describeRoute } from "hono-openapi";
|
import { describeRoute, validator } from "hono-openapi";
|
||||||
import { validator } from "hono-openapi/zod";
|
import { z } from "zod/v4";
|
||||||
import { z } from "zod";
|
|
||||||
import { apiRoute, handleZodError } from "@/api";
|
|
||||||
import { config } from "~/config.ts";
|
|
||||||
|
|
||||||
const returnError = (
|
const returnError = (
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { generateClient, getTestUsers } from "~/tests/utils";
|
import { generateClient, getTestUsers } from "@versia-server/tests";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(2);
|
const { users, deleteUsers } = await getTestUsers(2);
|
||||||
|
|
||||||
|
|
@ -2,11 +2,10 @@ import {
|
||||||
Relationship as RelationshipSchema,
|
Relationship as RelationshipSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { Relationship } from "@versia/kit/db";
|
import { ApiError } from "@versia-server/kit";
|
||||||
import { describeRoute } from "hono-openapi";
|
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
|
||||||
import { resolver } from "hono-openapi/zod";
|
import { Relationship } from "@versia-server/kit/db";
|
||||||
import { apiRoute, auth, withUserParam } from "@/api";
|
import { describeRoute, resolver } from "hono-openapi";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
import { RolePermission } from "@versia/client/schemas";
|
import { RolePermission } from "@versia/client/schemas";
|
||||||
import { describeRoute } from "hono-openapi";
|
import { ApiError } from "@versia-server/kit";
|
||||||
import { resolver, validator } from "hono-openapi/zod";
|
import {
|
||||||
import { z } from "zod";
|
apiRoute,
|
||||||
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
|
auth,
|
||||||
|
handleZodError,
|
||||||
|
withUserParam,
|
||||||
|
} from "@versia-server/kit/api";
|
||||||
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
|
import { z } from "zod/v4";
|
||||||
import { getFeed } from "@/rss";
|
import { getFeed } from "@/rss";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -34,12 +38,13 @@ export default apiRoute((app) =>
|
||||||
RolePermission.ViewNotes,
|
RolePermission.ViewNotes,
|
||||||
RolePermission.ViewAccounts,
|
RolePermission.ViewAccounts,
|
||||||
],
|
],
|
||||||
|
|
||||||
scopes: ["read:statuses"],
|
scopes: ["read:statuses"],
|
||||||
}),
|
}),
|
||||||
validator(
|
validator(
|
||||||
"query",
|
"query",
|
||||||
z.object({
|
z.object({
|
||||||
page: z.coerce.number().default(0).openapi({
|
page: z.coerce.number().default(0).meta({
|
||||||
description: "Page number to fetch. Defaults to 0.",
|
description: "Page number to fetch. Defaults to 0.",
|
||||||
example: 2,
|
example: 2,
|
||||||
}),
|
}),
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
import { RolePermission } from "@versia/client/schemas";
|
import { RolePermission } from "@versia/client/schemas";
|
||||||
import { describeRoute } from "hono-openapi";
|
import { ApiError } from "@versia-server/kit";
|
||||||
import { resolver, validator } from "hono-openapi/zod";
|
import {
|
||||||
import { z } from "zod";
|
apiRoute,
|
||||||
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
|
auth,
|
||||||
|
handleZodError,
|
||||||
|
withUserParam,
|
||||||
|
} from "@versia-server/kit/api";
|
||||||
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
|
import { z } from "zod/v4";
|
||||||
import { getFeed } from "@/rss";
|
import { getFeed } from "@/rss";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -33,12 +37,13 @@ export default apiRoute((app) =>
|
||||||
RolePermission.ViewNotes,
|
RolePermission.ViewNotes,
|
||||||
RolePermission.ViewAccounts,
|
RolePermission.ViewAccounts,
|
||||||
],
|
],
|
||||||
|
|
||||||
scopes: ["read:statuses"],
|
scopes: ["read:statuses"],
|
||||||
}),
|
}),
|
||||||
validator(
|
validator(
|
||||||
"query",
|
"query",
|
||||||
z.object({
|
z.object({
|
||||||
page: z.coerce.number().default(0).openapi({
|
page: z.coerce.number().default(0).meta({
|
||||||
description: "Page number to fetch. Defaults to 0.",
|
description: "Page number to fetch. Defaults to 0.",
|
||||||
example: 2,
|
example: 2,
|
||||||
}),
|
}),
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { generateClient, getTestUsers } from "~/tests/utils";
|
import { generateClient, getTestUsers } from "@versia-server/tests";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(3);
|
const { users, deleteUsers } = await getTestUsers(3);
|
||||||
|
|
||||||
|
|
@ -3,12 +3,16 @@ import {
|
||||||
Relationship as RelationshipSchema,
|
Relationship as RelationshipSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { Relationship } from "@versia/kit/db";
|
import { ApiError } from "@versia-server/kit";
|
||||||
import { describeRoute } from "hono-openapi";
|
import {
|
||||||
import { resolver, validator } from "hono-openapi/zod";
|
apiRoute,
|
||||||
import { z } from "zod";
|
auth,
|
||||||
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
|
handleZodError,
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
withUserParam,
|
||||||
|
} from "@versia-server/kit/api";
|
||||||
|
import { Relationship } from "@versia-server/kit/db";
|
||||||
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
@ -57,12 +61,12 @@ export default apiRoute((app) =>
|
||||||
validator(
|
validator(
|
||||||
"json",
|
"json",
|
||||||
z.object({
|
z.object({
|
||||||
reblogs: z.boolean().default(true).openapi({
|
reblogs: z.boolean().default(true).meta({
|
||||||
description:
|
description:
|
||||||
"Receive this account’s reblogs in home timeline?",
|
"Receive this account’s reblogs in home timeline?",
|
||||||
example: true,
|
example: true,
|
||||||
}),
|
}),
|
||||||
notify: z.boolean().default(false).openapi({
|
notify: z.boolean().default(false).meta({
|
||||||
description:
|
description:
|
||||||
"Receive notifications when this account posts a status?",
|
"Receive notifications when this account posts a status?",
|
||||||
example: false,
|
example: false,
|
||||||
|
|
@ -70,7 +74,7 @@ export default apiRoute((app) =>
|
||||||
languages: z
|
languages: z
|
||||||
.array(iso631)
|
.array(iso631)
|
||||||
.default([])
|
.default([])
|
||||||
.openapi({
|
.meta({
|
||||||
description:
|
description:
|
||||||
"Array of String (ISO 639-1 language two-letter code). Filter received statuses for these languages. If not provided, you will receive this account’s posts in all languages.",
|
"Array of String (ISO 639-1 language two-letter code). Filter received statuses for these languages. If not provided, you will receive this account’s posts in all languages.",
|
||||||
example: ["en", "fr"],
|
example: ["en", "fr"],
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { generateClient, getTestUsers } from "~/tests/utils";
|
import { generateClient, getTestUsers } from "@versia-server/tests";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(5);
|
const { users, deleteUsers } = await getTestUsers(5);
|
||||||
|
|
||||||
|
|
@ -2,14 +2,18 @@ import {
|
||||||
Account as AccountSchema,
|
Account as AccountSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { Timeline } from "@versia/kit/db";
|
import { ApiError } from "@versia-server/kit";
|
||||||
import { Users } from "@versia/kit/tables";
|
import {
|
||||||
|
apiRoute,
|
||||||
|
auth,
|
||||||
|
handleZodError,
|
||||||
|
withUserParam,
|
||||||
|
} from "@versia-server/kit/api";
|
||||||
|
import { Timeline } from "@versia-server/kit/db";
|
||||||
|
import { Users } from "@versia-server/kit/tables";
|
||||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||||
import { describeRoute } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { resolver, validator } from "hono-openapi/zod";
|
import { z } from "zod/v4";
|
||||||
import { z } from "zod";
|
|
||||||
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
|
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -34,7 +38,7 @@ export default apiRoute((app) =>
|
||||||
link: z
|
link: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.openapi({
|
.meta({
|
||||||
description:
|
description:
|
||||||
"Links to the next and previous pages",
|
"Links to the next and previous pages",
|
||||||
example:
|
example:
|
||||||
|
|
@ -61,22 +65,22 @@ export default apiRoute((app) =>
|
||||||
validator(
|
validator(
|
||||||
"query",
|
"query",
|
||||||
z.object({
|
z.object({
|
||||||
max_id: AccountSchema.shape.id.optional().openapi({
|
max_id: AccountSchema.shape.id.optional().meta({
|
||||||
description:
|
description:
|
||||||
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
||||||
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
|
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
|
||||||
}),
|
}),
|
||||||
since_id: AccountSchema.shape.id.optional().openapi({
|
since_id: AccountSchema.shape.id.optional().meta({
|
||||||
description:
|
description:
|
||||||
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
|
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
|
||||||
example: undefined,
|
example: undefined,
|
||||||
}),
|
}),
|
||||||
min_id: AccountSchema.shape.id.optional().openapi({
|
min_id: AccountSchema.shape.id.optional().meta({
|
||||||
description:
|
description:
|
||||||
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
||||||
example: undefined,
|
example: undefined,
|
||||||
}),
|
}),
|
||||||
limit: z.number().int().min(1).max(40).default(20).openapi({
|
limit: z.number().int().min(1).max(40).default(20).meta({
|
||||||
description: "Maximum number of results to return.",
|
description: "Maximum number of results to return.",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { generateClient, getTestUsers } from "~/tests/utils";
|
import { generateClient, getTestUsers } from "@versia-server/tests";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(5);
|
const { users, deleteUsers } = await getTestUsers(5);
|
||||||
|
|
||||||
|
|
@ -2,14 +2,18 @@ import {
|
||||||
Account as AccountSchema,
|
Account as AccountSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { Timeline } from "@versia/kit/db";
|
import { ApiError } from "@versia-server/kit";
|
||||||
import { Users } from "@versia/kit/tables";
|
import {
|
||||||
|
apiRoute,
|
||||||
|
auth,
|
||||||
|
handleZodError,
|
||||||
|
withUserParam,
|
||||||
|
} from "@versia-server/kit/api";
|
||||||
|
import { Timeline } from "@versia-server/kit/db";
|
||||||
|
import { Users } from "@versia-server/kit/tables";
|
||||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||||
import { describeRoute } from "hono-openapi";
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
import { resolver, validator } from "hono-openapi/zod";
|
import { z } from "zod/v4";
|
||||||
import { z } from "zod";
|
|
||||||
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
|
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -35,7 +39,7 @@ export default apiRoute((app) =>
|
||||||
link: z
|
link: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.openapi({
|
.meta({
|
||||||
description:
|
description:
|
||||||
"Links to the next and previous pages",
|
"Links to the next and previous pages",
|
||||||
example:
|
example:
|
||||||
|
|
@ -62,22 +66,22 @@ export default apiRoute((app) =>
|
||||||
validator(
|
validator(
|
||||||
"query",
|
"query",
|
||||||
z.object({
|
z.object({
|
||||||
max_id: AccountSchema.shape.id.optional().openapi({
|
max_id: AccountSchema.shape.id.optional().meta({
|
||||||
description:
|
description:
|
||||||
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
||||||
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
|
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
|
||||||
}),
|
}),
|
||||||
since_id: AccountSchema.shape.id.optional().openapi({
|
since_id: AccountSchema.shape.id.optional().meta({
|
||||||
description:
|
description:
|
||||||
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
|
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
|
||||||
example: undefined,
|
example: undefined,
|
||||||
}),
|
}),
|
||||||
min_id: AccountSchema.shape.id.optional().openapi({
|
min_id: AccountSchema.shape.id.optional().meta({
|
||||||
description:
|
description:
|
||||||
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
||||||
example: undefined,
|
example: undefined,
|
||||||
}),
|
}),
|
||||||
limit: z.number().int().min(1).max(40).default(20).openapi({
|
limit: z.number().int().min(1).max(40).default(20).meta({
|
||||||
description: "Maximum number of results to return.",
|
description: "Maximum number of results to return.",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { generateClient, getTestStatuses, getTestUsers } from "~/tests/utils";
|
import {
|
||||||
|
generateClient,
|
||||||
|
getTestStatuses,
|
||||||
|
getTestUsers,
|
||||||
|
} from "@versia-server/tests";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(5);
|
const { users, deleteUsers } = await getTestUsers(5);
|
||||||
const timeline = (await getTestStatuses(5, users[0])).toReversed();
|
const timeline = (await getTestStatuses(5, users[0])).toReversed();
|
||||||
|
|
@ -2,10 +2,9 @@ import {
|
||||||
Account as AccountSchema,
|
Account as AccountSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { describeRoute } from "hono-openapi";
|
import { ApiError } from "@versia-server/kit";
|
||||||
import { resolver } from "hono-openapi/zod";
|
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
|
||||||
import { apiRoute, auth, withUserParam } from "@/api";
|
import { describeRoute, resolver } from "hono-openapi";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.get(
|
app.get(
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { generateClient, getTestUsers } from "~/tests/utils";
|
import { generateClient, getTestUsers } from "@versia-server/tests";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(2);
|
const { users, deleteUsers } = await getTestUsers(2);
|
||||||
|
|
||||||
|
|
@ -2,16 +2,20 @@ import {
|
||||||
Relationship as RelationshipSchema,
|
Relationship as RelationshipSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { Relationship } from "@versia/kit/db";
|
import { ApiError } from "@versia-server/kit";
|
||||||
import { describeRoute } from "hono-openapi";
|
import {
|
||||||
import { resolver, validator } from "hono-openapi/zod";
|
apiRoute,
|
||||||
import { z } from "zod";
|
auth,
|
||||||
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
|
handleZodError,
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
withUserParam,
|
||||||
|
} from "@versia-server/kit/api";
|
||||||
|
import { Relationship } from "@versia-server/kit/db";
|
||||||
import {
|
import {
|
||||||
RelationshipJobType,
|
RelationshipJobType,
|
||||||
relationshipQueue,
|
relationshipQueue,
|
||||||
} from "~/classes/queues/relationships";
|
} from "@versia-server/kit/queues/relationships";
|
||||||
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
@ -51,7 +55,7 @@ export default apiRoute((app) =>
|
||||||
validator(
|
validator(
|
||||||
"json",
|
"json",
|
||||||
z.object({
|
z.object({
|
||||||
notifications: z.boolean().default(true).openapi({
|
notifications: z.boolean().default(true).meta({
|
||||||
description: "Mute notifications in addition to statuses?",
|
description: "Mute notifications in addition to statuses?",
|
||||||
}),
|
}),
|
||||||
duration: z
|
duration: z
|
||||||
|
|
@ -60,7 +64,7 @@ export default apiRoute((app) =>
|
||||||
.min(0)
|
.min(0)
|
||||||
.max(60 * 60 * 24 * 365 * 5)
|
.max(60 * 60 * 24 * 365 * 5)
|
||||||
.default(0)
|
.default(0)
|
||||||
.openapi({
|
.meta({
|
||||||
description:
|
description:
|
||||||
"How long the mute should last, in seconds. 0 means indefinite.",
|
"How long the mute should last, in seconds. 0 means indefinite.",
|
||||||
}),
|
}),
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { generateClient, getTestUsers } from "~/tests/utils";
|
import { generateClient, getTestUsers } from "@versia-server/tests";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(2);
|
const { users, deleteUsers } = await getTestUsers(2);
|
||||||
|
|
||||||
|
|
@ -2,12 +2,16 @@ import {
|
||||||
Relationship as RelationshipSchema,
|
Relationship as RelationshipSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { Relationship } from "@versia/kit/db";
|
import { ApiError } from "@versia-server/kit";
|
||||||
import { describeRoute } from "hono-openapi";
|
import {
|
||||||
import { resolver, validator } from "hono-openapi/zod";
|
apiRoute,
|
||||||
import { z } from "zod";
|
auth,
|
||||||
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
|
handleZodError,
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
withUserParam,
|
||||||
|
} from "@versia-server/kit/api";
|
||||||
|
import { Relationship } from "@versia-server/kit/db";
|
||||||
|
import { describeRoute, resolver, validator } from "hono-openapi";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
@ -45,7 +49,7 @@ export default apiRoute((app) =>
|
||||||
validator(
|
validator(
|
||||||
"json",
|
"json",
|
||||||
z.object({
|
z.object({
|
||||||
comment: RelationshipSchema.shape.note.optional().openapi({
|
comment: RelationshipSchema.shape.note.optional().meta({
|
||||||
description:
|
description:
|
||||||
"The comment to be set on that user. Provide an empty string or leave out this parameter to clear the currently set note.",
|
"The comment to be set on that user. Provide an empty string or leave out this parameter to clear the currently set note.",
|
||||||
}),
|
}),
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { afterAll, describe, expect, test } from "bun:test";
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
import { generateClient, getTestUsers } from "~/tests/utils";
|
import { generateClient, getTestUsers } from "@versia-server/tests";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(2);
|
const { users, deleteUsers } = await getTestUsers(2);
|
||||||
|
|
||||||
|
|
@ -2,10 +2,9 @@ import {
|
||||||
Relationship as RelationshipSchema,
|
Relationship as RelationshipSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { Relationship } from "@versia/kit/db";
|
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
|
||||||
import { describeRoute } from "hono-openapi";
|
import { Relationship } from "@versia-server/kit/db";
|
||||||
import { resolver } from "hono-openapi/zod";
|
import { describeRoute, resolver } from "hono-openapi";
|
||||||
import { apiRoute, auth, withUserParam } from "@/api";
|
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
@ -2,11 +2,10 @@ import {
|
||||||
Account as AccountSchema,
|
Account as AccountSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { describeRoute } from "hono-openapi";
|
import { ApiError } from "@versia-server/kit";
|
||||||
import { resolver } from "hono-openapi/zod";
|
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
|
||||||
import { apiRoute, auth, withUserParam } from "@/api";
|
import { User } from "@versia-server/kit/db";
|
||||||
import { User } from "~/classes/database/user";
|
import { describeRoute, resolver } from "hono-openapi";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { generateClient, getTestUsers } from "~/tests/utils";
|
import { generateClient, getTestUsers } from "@versia-server/tests";
|
||||||
|
|
||||||
const { users, deleteUsers } = await getTestUsers(2);
|
const { users, deleteUsers } = await getTestUsers(2);
|
||||||
|
|
||||||
|
|
@ -2,11 +2,10 @@ import {
|
||||||
Relationship as RelationshipSchema,
|
Relationship as RelationshipSchema,
|
||||||
RolePermission,
|
RolePermission,
|
||||||
} from "@versia/client/schemas";
|
} from "@versia/client/schemas";
|
||||||
import { Relationship } from "@versia/kit/db";
|
import { ApiError } from "@versia-server/kit";
|
||||||
import { describeRoute } from "hono-openapi";
|
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
|
||||||
import { resolver } from "hono-openapi/zod";
|
import { Relationship } from "@versia-server/kit/db";
|
||||||
import { apiRoute, auth, withUserParam } from "@/api";
|
import { describeRoute, resolver } from "hono-openapi";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue