mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
fix(federation): 🚑 Fix broken inbound federation and add end-to-end testing for federation
This commit is contained in:
parent
85ef96fc7f
commit
8ae4f3815a
170
api/inbox/index.test.ts
Normal file
170
api/inbox/index.test.ts
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
import { afterAll, describe, expect, test } from "bun:test";
|
||||||
|
import { randomUUIDv7, sleep } from "bun";
|
||||||
|
import {
|
||||||
|
clearMocks,
|
||||||
|
disableRealRequests,
|
||||||
|
enableRealRequests,
|
||||||
|
mock,
|
||||||
|
} from "bun-bagel";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { Instance } from "~/classes/database/instance";
|
||||||
|
import { Note } from "~/classes/database/note";
|
||||||
|
import { User } from "~/classes/database/user";
|
||||||
|
import { config } from "~/config";
|
||||||
|
import { Notes } from "~/drizzle/schema";
|
||||||
|
import { sign } from "~/packages/sdk/crypto";
|
||||||
|
import * as VersiaEntities from "~/packages/sdk/entities";
|
||||||
|
import { fakeRequest } from "~/tests/utils";
|
||||||
|
|
||||||
|
const instanceUrl = new URL("https://versia.example.com");
|
||||||
|
const noteId = randomUUIDv7();
|
||||||
|
const userId = randomUUIDv7();
|
||||||
|
const userKeys = await User.generateKeys();
|
||||||
|
const privateKey = await crypto.subtle.importKey(
|
||||||
|
"pkcs8",
|
||||||
|
Buffer.from(userKeys.private_key, "base64"),
|
||||||
|
"Ed25519",
|
||||||
|
false,
|
||||||
|
["sign"],
|
||||||
|
);
|
||||||
|
const instanceKeys = await User.generateKeys();
|
||||||
|
const inboxUrl = new URL("/inbox", config.http.base_url);
|
||||||
|
|
||||||
|
disableRealRequests();
|
||||||
|
|
||||||
|
mock(new URL("/.well-known/versia", instanceUrl).href, {
|
||||||
|
response: {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
data: new VersiaEntities.InstanceMetadata({
|
||||||
|
type: "InstanceMetadata",
|
||||||
|
name: "Versia",
|
||||||
|
description: "Versia instance",
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
host: instanceUrl.hostname,
|
||||||
|
software: {
|
||||||
|
name: "Versia",
|
||||||
|
version: "1.0.0",
|
||||||
|
},
|
||||||
|
compatibility: {
|
||||||
|
extensions: [],
|
||||||
|
versions: ["0.5.0"],
|
||||||
|
},
|
||||||
|
public_key: {
|
||||||
|
algorithm: "ed25519",
|
||||||
|
key: instanceKeys.public_key,
|
||||||
|
},
|
||||||
|
}).toJSON(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mock(new URL(`/users/${userId}`, instanceUrl).href, {
|
||||||
|
response: {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
data: new VersiaEntities.User({
|
||||||
|
id: userId,
|
||||||
|
created_at: "2025-04-18T10:32:01.427Z",
|
||||||
|
uri: new URL(`/users/${userId}`, instanceUrl).href,
|
||||||
|
type: "User",
|
||||||
|
username: "testuser",
|
||||||
|
public_key: {
|
||||||
|
algorithm: "ed25519",
|
||||||
|
key: userKeys.public_key,
|
||||||
|
actor: new URL(`/users/${userId}`, instanceUrl).href,
|
||||||
|
},
|
||||||
|
inbox: new URL(`/users/${userId}/inbox`, instanceUrl).href,
|
||||||
|
collections: {
|
||||||
|
featured: new URL(`/users/${userId}/featured`, instanceUrl)
|
||||||
|
.href,
|
||||||
|
followers: new URL(`/users/${userId}/followers`, instanceUrl)
|
||||||
|
.href,
|
||||||
|
following: new URL(`/users/${userId}/following`, instanceUrl)
|
||||||
|
.href,
|
||||||
|
outbox: new URL(`/users/${userId}/outbox`, instanceUrl).href,
|
||||||
|
},
|
||||||
|
}).toJSON(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
// Delete the instance in database
|
||||||
|
const instance = await Instance.resolve(instanceUrl);
|
||||||
|
|
||||||
|
if (!instance) {
|
||||||
|
throw new Error("Instance not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
await instance.delete();
|
||||||
|
clearMocks();
|
||||||
|
enableRealRequests();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Inbox Tests", () => {
|
||||||
|
test("should correctly process inbox request", async () => {
|
||||||
|
const exampleRequest = new VersiaEntities.Note({
|
||||||
|
id: noteId,
|
||||||
|
created_at: "2025-04-18T10:32:01.427Z",
|
||||||
|
uri: new URL(`/notes/${noteId}`, instanceUrl).href,
|
||||||
|
type: "Note",
|
||||||
|
extensions: {
|
||||||
|
"pub.versia:custom_emojis": {
|
||||||
|
emojis: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attachments: [],
|
||||||
|
author: new URL(`/users/${userId}`, instanceUrl).href,
|
||||||
|
content: {
|
||||||
|
"text/html": {
|
||||||
|
content: "<p>Hello!</p>",
|
||||||
|
remote: false,
|
||||||
|
},
|
||||||
|
"text/plain": {
|
||||||
|
content: "Hello!",
|
||||||
|
remote: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
collections: {
|
||||||
|
replies: new URL(`/notes/${noteId}/replies`, instanceUrl).href,
|
||||||
|
quotes: new URL(`/notes/${noteId}/quotes`, instanceUrl).href,
|
||||||
|
},
|
||||||
|
group: "public",
|
||||||
|
is_sensitive: false,
|
||||||
|
mentions: [],
|
||||||
|
quotes: null,
|
||||||
|
replies_to: null,
|
||||||
|
subject: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const signedRequest = await sign(
|
||||||
|
privateKey,
|
||||||
|
new URL(exampleRequest.data.author),
|
||||||
|
new Request(inboxUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
"User-Agent": "Versia/1.0.0",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(exampleRequest.toJSON()),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await fakeRequest(inboxUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: signedRequest.headers,
|
||||||
|
body: signedRequest.body,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
|
// Check if note was created in the database
|
||||||
|
const note = await Note.fromSql(eq(Notes.uri, exampleRequest.data.uri));
|
||||||
|
|
||||||
|
expect(note).not.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -16,7 +16,6 @@ export default apiRoute((app) =>
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
validator("json", z.any(), handleZodError),
|
|
||||||
validator(
|
validator(
|
||||||
"header",
|
"header",
|
||||||
z.object({
|
z.object({
|
||||||
|
|
@ -32,7 +31,7 @@ export default apiRoute((app) =>
|
||||||
handleZodError,
|
handleZodError,
|
||||||
),
|
),
|
||||||
async (context) => {
|
async (context) => {
|
||||||
const body = await context.req.valid("json");
|
const body = await context.req.json();
|
||||||
const {
|
const {
|
||||||
"versia-signature": signature,
|
"versia-signature": signature,
|
||||||
"versia-signed-at": signedAt,
|
"versia-signed-at": signedAt,
|
||||||
|
|
@ -53,7 +52,7 @@ export default apiRoute((app) =>
|
||||||
method: context.req.method,
|
method: context.req.method,
|
||||||
url: context.req.url,
|
url: context.req.url,
|
||||||
},
|
},
|
||||||
ip: context.env.ip ?? null,
|
ip: context.env?.ip ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
return context.body(
|
return context.body(
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import { z } from "zod";
|
||||||
import { apiRoute, handleZodError } from "@/api";
|
import { apiRoute, handleZodError } from "@/api";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
import { InboxJobType, inboxQueue } from "~/classes/queues/inbox";
|
import { InboxJobType, inboxQueue } from "~/classes/queues/inbox";
|
||||||
import type { JSONObject } from "~/packages/sdk/types";
|
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.post(
|
app.post(
|
||||||
|
|
@ -87,9 +86,8 @@ export default apiRoute((app) =>
|
||||||
}),
|
}),
|
||||||
handleZodError,
|
handleZodError,
|
||||||
),
|
),
|
||||||
validator("json", z.any(), handleZodError),
|
|
||||||
async (context) => {
|
async (context) => {
|
||||||
const body: JSONObject = await context.req.valid("json");
|
const body = await context.req.json();
|
||||||
const {
|
const {
|
||||||
"versia-signature": signature,
|
"versia-signature": signature,
|
||||||
"versia-signed-at": signedAt,
|
"versia-signed-at": signedAt,
|
||||||
|
|
|
||||||
3
bun.lock
3
bun.lock
|
|
@ -71,6 +71,7 @@
|
||||||
"@types/pg": "^8.11.13",
|
"@types/pg": "^8.11.13",
|
||||||
"@types/qs": "^6.9.18",
|
"@types/qs": "^6.9.18",
|
||||||
"@types/web-push": "^3.6.4",
|
"@types/web-push": "^3.6.4",
|
||||||
|
"bun-bagel": "^1.2.0",
|
||||||
"drizzle-kit": "^0.31.0",
|
"drizzle-kit": "^0.31.0",
|
||||||
"markdown-it-image-figures": "^2.1.1",
|
"markdown-it-image-figures": "^2.1.1",
|
||||||
"ts-prune": "^0.10.3",
|
"ts-prune": "^0.10.3",
|
||||||
|
|
@ -660,6 +661,8 @@
|
||||||
|
|
||||||
"bullmq": ["bullmq@5.49.0", "", { "dependencies": { "cron-parser": "^4.9.0", "ioredis": "^5.4.1", "msgpackr": "^1.11.2", "node-abort-controller": "^3.1.1", "semver": "^7.5.4", "tslib": "^2.0.0", "uuid": "^9.0.0" } }, "sha512-qwNOgUfD3kHI33bU2HOsn6YKEoSdUfHTledQUy95V8HogVdgGg/HS3+TQh7Y7YH19h5yK2Oo/FFEnzyXJhgy3w=="],
|
"bullmq": ["bullmq@5.49.0", "", { "dependencies": { "cron-parser": "^4.9.0", "ioredis": "^5.4.1", "msgpackr": "^1.11.2", "node-abort-controller": "^3.1.1", "semver": "^7.5.4", "tslib": "^2.0.0", "uuid": "^9.0.0" } }, "sha512-qwNOgUfD3kHI33bU2HOsn6YKEoSdUfHTledQUy95V8HogVdgGg/HS3+TQh7Y7YH19h5yK2Oo/FFEnzyXJhgy3w=="],
|
||||||
|
|
||||||
|
"bun-bagel": ["bun-bagel@1.2.0", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-c4S68dNddpnog9nxXp9PAhcep0alOy49jpRlC1yACoxplUvgX22NZxeQUIIov5TCJJDH/snT5R9bMyix7AG0KQ=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.2.9", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-dk/kOEfQbajENN/D6FyiSgOKEuUi9PWfqKQJEgwKrCMWbjS/S6tEXp178mWvWAcUSYm9ArDlWHZKO3T/4cLXiw=="],
|
"bun-types": ["bun-types@1.2.9", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-dk/kOEfQbajENN/D6FyiSgOKEuUi9PWfqKQJEgwKrCMWbjS/S6tEXp178mWvWAcUSYm9ArDlWHZKO3T/4cLXiw=="],
|
||||||
|
|
||||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ in
|
||||||
|
|
||||||
pnpmDeps = pnpm.fetchDeps {
|
pnpmDeps = pnpm.fetchDeps {
|
||||||
inherit (finalAttrs) pname version src pnpmInstallFlags;
|
inherit (finalAttrs) pname version src pnpmInstallFlags;
|
||||||
hash = "sha256-miwjCxel9mgLcJ8Gwzyr7dLZe18yKZ8PeMlIvduJYwk=";
|
hash = "sha256-bDgLkz0aT3/jM2inVsfMoJBKZacxqfHFi8GtIg7zc+M=";
|
||||||
};
|
};
|
||||||
|
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
#!/usr/bin/env nix-shell
|
|
||||||
#! nix-shell -i bash -p nix-prefetch-github
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SOURCE=$(nix-prefetch-github --nix versia-pub server | tail -n 6)
|
|
||||||
|
|
||||||
cat > ./nix/source.nix << EOF
|
|
||||||
{
|
|
||||||
lib,
|
|
||||||
fetchFromGitHub,
|
|
||||||
}: {
|
|
||||||
outputHash.x86_64-linux = lib.fakeHash;
|
|
||||||
outputHash.aarch64-linux = lib.fakeHash;
|
|
||||||
src = fetchFromGitHub {
|
|
||||||
${SOURCE};
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "Done."
|
|
||||||
echo "Please update the attributes of 'outputHash' in nix/source.nix."
|
|
||||||
|
|
@ -68,6 +68,7 @@
|
||||||
"@types/pg": "^8.11.13",
|
"@types/pg": "^8.11.13",
|
||||||
"@types/qs": "^6.9.18",
|
"@types/qs": "^6.9.18",
|
||||||
"@types/web-push": "^3.6.4",
|
"@types/web-push": "^3.6.4",
|
||||||
|
"bun-bagel": "^1.2.0",
|
||||||
"drizzle-kit": "^0.31.0",
|
"drizzle-kit": "^0.31.0",
|
||||||
"markdown-it-image-figures": "^2.1.1",
|
"markdown-it-image-figures": "^2.1.1",
|
||||||
"ts-prune": "^0.10.3",
|
"ts-prune": "^0.10.3",
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ export const sign = async (
|
||||||
const body = await req.clone().text();
|
const body = await req.clone().text();
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
|
|
||||||
const digest = stringToBase64Hash(body);
|
const digest = await stringToBase64Hash(body);
|
||||||
const timestampSecs = Math.floor(timestamp.getTime() / 1000);
|
const timestampSecs = Math.floor(timestamp.getTime() / 1000);
|
||||||
|
|
||||||
const signedString = `${req.method.toLowerCase()} ${encodeURI(
|
const signedString = `${req.method.toLowerCase()} ${encodeURI(
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,9 @@ importers:
|
||||||
'@types/web-push':
|
'@types/web-push':
|
||||||
specifier: ^3.6.4
|
specifier: ^3.6.4
|
||||||
version: 3.6.4
|
version: 3.6.4
|
||||||
|
bun-bagel:
|
||||||
|
specifier: ^1.2.0
|
||||||
|
version: 1.2.0(typescript@5.8.3)
|
||||||
drizzle-kit:
|
drizzle-kit:
|
||||||
specifier: ^0.31.0
|
specifier: ^0.31.0
|
||||||
version: 0.31.0
|
version: 0.31.0
|
||||||
|
|
@ -1832,6 +1835,11 @@ packages:
|
||||||
bullmq@5.49.0:
|
bullmq@5.49.0:
|
||||||
resolution: {integrity: sha512-qwNOgUfD3kHI33bU2HOsn6YKEoSdUfHTledQUy95V8HogVdgGg/HS3+TQh7Y7YH19h5yK2Oo/FFEnzyXJhgy3w==}
|
resolution: {integrity: sha512-qwNOgUfD3kHI33bU2HOsn6YKEoSdUfHTledQUy95V8HogVdgGg/HS3+TQh7Y7YH19h5yK2Oo/FFEnzyXJhgy3w==}
|
||||||
|
|
||||||
|
bun-bagel@1.2.0:
|
||||||
|
resolution: {integrity: sha512-c4S68dNddpnog9nxXp9PAhcep0alOy49jpRlC1yACoxplUvgX22NZxeQUIIov5TCJJDH/snT5R9bMyix7AG0KQ==}
|
||||||
|
peerDependencies:
|
||||||
|
typescript: ^5.0.0
|
||||||
|
|
||||||
bun-types@1.2.9:
|
bun-types@1.2.9:
|
||||||
resolution: {integrity: sha512-dk/kOEfQbajENN/D6FyiSgOKEuUi9PWfqKQJEgwKrCMWbjS/S6tEXp178mWvWAcUSYm9ArDlWHZKO3T/4cLXiw==}
|
resolution: {integrity: sha512-dk/kOEfQbajENN/D6FyiSgOKEuUi9PWfqKQJEgwKrCMWbjS/S6tEXp178mWvWAcUSYm9ArDlWHZKO3T/4cLXiw==}
|
||||||
|
|
||||||
|
|
@ -3105,6 +3113,7 @@ packages:
|
||||||
|
|
||||||
speech-rule-engine@4.1.0:
|
speech-rule-engine@4.1.0:
|
||||||
resolution: {integrity: sha512-jRP6QUyvi+C94QvcQR8wBfla2ySD5KXBr6XnmEBy8DcW/R1DN08Yykuwq7xEX90VM5E4Iv7UJoMvhW6U660x8A==}
|
resolution: {integrity: sha512-jRP6QUyvi+C94QvcQR8wBfla2ySD5KXBr6XnmEBy8DcW/R1DN08Yykuwq7xEX90VM5E4Iv7UJoMvhW6U660x8A==}
|
||||||
|
deprecated: breaking change
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
split2@4.2.0:
|
split2@4.2.0:
|
||||||
|
|
@ -4854,6 +4863,10 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
bun-bagel@1.2.0(typescript@5.8.3):
|
||||||
|
dependencies:
|
||||||
|
typescript: 5.8.3
|
||||||
|
|
||||||
bun-types@1.2.9:
|
bun-types@1.2.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.14.1
|
'@types/node': 22.14.1
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue