@fedify/fedify
Version:
An ActivityPub server framework
1,147 lines • 176 kB
JavaScript
import { Temporal } from "@js-temporal/polyfill";
import "urlpattern-polyfill";
globalThis.addEventListener = () => {};
import { t as esm_default } from "../esm-DVILvP5e.mjs";
import { n as RouterError } from "../router-CrMLXoOr.mjs";
import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
import "../std__assert-CRDpx_HF.mjs";
import { n as assertFalse, t as assertRejects } from "../assert_rejects-B-qJtC9Z.mjs";
import { t as assertInstanceOf } from "../assert_instance_of-C4Ri6VuN.mjs";
import { t as assertThrows } from "../assert_throws-4NwKEy2q.mjs";
import { t as assertNotEquals } from "../assert_not_equals--wG9hV7u.mjs";
import { t as assertStrictEquals } from "../assert_strict_equals-Dmjbg-bA.mjs";
import { t as assert } from "../assert-DikXweDx.mjs";
import { l as verifyRequest, s as signRequest } from "../http-C_edJspG.mjs";
import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DGu1NFwu.mjs";
import { t as getAuthenticatedDocumentLoader } from "../docloader-Da15YRxG.mjs";
import { a as compactJsonLd, h as verifyJsonLd, p as signJsonLd, s as detachSignature } from "../ld-tusP_XxG.mjs";
import { t as doesActorOwnKey } from "../owner-DRHNR5YO.mjs";
import { i as verifyObject, r as signObject } from "../proof-DLhLRv3m.mjs";
import { t as MemoryKvStore } from "../kv-rV3vodCc.mjs";
import { i as KvSpecDeterminer, n as FederationImpl, o as createFederation, r as InboxContextImpl, t as ContextImpl } from "../middleware-D9k0Knum.mjs";
import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
import * as vocab from "@fedify/vocab";
import { Create, Offer, Person, getTypeId, lookupObject } from "@fedify/vocab";
import { FetchError, getDocumentLoader } from "@fedify/vocab-runtime";
import { configure, reset } from "@logtape/logtape";
import serialize from "json-canon";
//#region ../fixture/src/fixtures/example.com/create.json
var id$2 = "https://example.com/create";
var create_default = {
"@context": "https://www.w3.org/ns/activitystreams",
type: "Create",
id: id$2,
actor: "https://example.com/person"
};
//#endregion
//#region ../fixture/src/fixtures/example.com/person.json
var person_default = {
"@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"],
id: "https://example.com/person",
type: "Person",
name: "John Doe",
publicKey: [{
"id": "https://example.com/key",
"owner": "https://example.com/person",
"type": "CryptographicKey",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyIB9rotX8G6r6/6toT+x\n24BUiQ/HaPH1Em9dOt4c94s+OPFoEdH7DY7Iym9A8LlH4JaGF8KD38bLHWe1S4x0\njV3gHJKhK7veJfGZCKUENcQecBZ+YWUs5HWvUIX1vVB//0luHrg6BQKGOrSOE+WI\nAxyr0qsWCFfZzQrvSnUD2yvg1arJX2xhms14uxoRd5Kg9efKSCmmQaNEapicARUm\nFWrIEpGFa/nUUnqimssAGw1eZFqf3wA4TjhsuARBhGaJtv/3KEa016eMZxy3kDlO\njZnXZTaTgWkXdodwUvy8563fes3Al6BlcS2iJ9qbtha8rSm0FHqoUKH73JsLPKQI\nwQIDAQAB\n-----END PUBLIC KEY-----"
}, {
"id": "https://example.com/key2",
"type": "CryptographicKey",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoRmBtnxbdFutoRd1GLGw\nwGTrsqlRRWUe11hHQaoRLGf5LwQ0tIc6I9q+dynliw+2kxYsLn9SH2je6HcTYOol\ngW7F/cOWXZQN04b+OiYcU1ConAhLjmn4k1uKawJ614y0ScPNd8PQ+CljsnlPxbq9\nofaCMe2BV3B6y09aCuGFJ0nxn1/ubjmIBIWWFTAznoz1J9BhJDGyt3IO3ABy3f9z\nDVlR32L/n5VIkXnxkjUKdzMAOzYb62kuKOp1iznRTPrV71SNtivJMwSh/LVgBrmZ\njtIn/oim+KyX/fdLU3tQ7VClyqmJzyAjccOH6Qj6nFTPh+vX07gqN8IlLT2uye4w\nawIDAQAB\n-----END PUBLIC KEY-----"
}]
};
//#endregion
//#region ../fixture/src/fixtures/example.com/person2.json
var person2_default = {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
"https://w3id.org/security/multikey/v1",
"https://w3id.org/security/data-integrity/v1",
"https://www.w3.org/ns/did/v1"
],
id: "https://example.com/person2",
type: "Person",
name: "Jane Doe",
publicKey: [{
"id": "https://example.com/person2#key3",
"type": "CryptographicKey",
"owner": "https://example.com/person2",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4GUqWgdiYlN3Su5Gr4l6\ni+xRS8gDDVKZ718vpGk6eIpvqs33q430nRbHIzbHRXRaAhc/1++rUBcK0V4/kjZl\nCSzVtRgGU6HMkmjcD+uE56a8XbTczfltbEDj7afoEuB2F3UhQEWrSz+QJ29DPXaL\nMIa1Yv61NR2vxGqNbdtoMjDORMBYtg77CYbcFkiJHw65PDa7+f/yjLxuCRPye5L7\nhncN0UZuuFoRJmHNRLSg5omBad9WTvQXmSyXEhEdk9fHwlI022AqAzlWbT79hldc\nDSKGGLLbQIs1c3JZIG8G5i6Uh5Vy0Z7tSNBcxbhqoI9i9je4f/x/OPIVc19f04BE\n1LgWuHsftZzRgW9Sdqz53W83XxVdxlyHeywXOnstSWT11f8dkLyQUcHKTH+E6urb\nH+aiPLiRpYK8W7D9KTQA9kZ5JXaEuveBd5vJX7wakhbzAn8pWJU7GYIHNY38Ycok\nmivkU5pY8S2cKFMwY0b7ade3MComlir5P3ZYSjF+n6gRVsT96P+9mNfCu9gXt/f8\nXCyjKlH89kGwuJ7HhR8CuVdm0l+jYozVt6GsDy0hHYyn79NCCAEzP7ZbhBMR0T5V\nrkl+TIGXoJH9WFiz4VxO+NnglF6dNQjDS5IzYLoFRXIK1f3cmQiEB4FZmL70l9HL\nrgwR+Xys83xia79OqFDRezMCAwEAAQ==\n-----END PUBLIC KEY-----\n"
}, {
"id": "https://example.com/person2#key4",
"type": "CryptographicKey",
"owner": "https://example.com/person2",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEALR8epAGDe+cVq5p2Tx49CCfphpk1rNhkNoY9i+XEUfg=\n-----END PUBLIC KEY-----\n"
}],
assertionMethod: [{
"id": "https://example.com/peson2#key3",
"type": "Multikey",
"controller": "https://example.com/person2",
"publicKeyMultibase": "zgghBUVkqmWS8e1j4aN2yowLAEkJC6wowB9wWmLRACYCok7UzstWcTBp3waKiDUM7wqL9bbBD9W9FvNaXEK2KPCZ9ffhvd5dxChJL9bdPQSrMwa28FEYMGDtcF1uocrYNmZm2dBBMaWrCu8U3s4PpVVhn4hsWDL8GLuE466pkJs9Hy8xmECoaaVgAZLiYDw2gwrjHDiX2i7aDHKfE7aSZWUWmC8nAGNZ7DX5pXoyXK3pxuaCWxNxXwPmaFwgKDyy9uhtBJ8znp9NZXkXHBTQe5uAi8GFwHY5asvqCmYPrAGWxcT6pdbZaJHdWkM7nw6apBHfakKs42oMqdBoJ2WkkresoT1qHrX2GW7gNP9PLtveF4vfEd6cwgHKQCdYgayG3muGfZiPvML75cyfkNrjkctvuQUfMxY9umbd2TG3V3mPnLrvQnqHpuRMZYtCn3nX1qfZaqFhTwT4NFPqVNLqvgR6k9vcuGXn6Ndaumhd5xtTK64jk3e2gPBit9iq6MrFUSoxNsbTty4kqcHAodtkK8CMSxUxbFP1kK3nyy8ZfeMgDCts1KboBcT2m5FMpQpYxKtNBfvhTuyeDDC34uhbY8itmTAnDwSr5mKrniwwDUGPZFejda51TYs1N9D9Ejzaw5Mvr8qN6wahHmsDBWTbWwV6YKVMD1MjAhJBUopWJWB5x6mEBAX25MssKfAEhJyDtqYWjq63uQHUJCsPJp"
}, {
"id": "https://example.com/person2#key4",
"type": "Multikey",
"controller": "https://example.com/person2",
"publicKeyMultibase": "z6MkhVPuyvgG1RkMv67azDqDCDERPXVrUg1i3qchXY5EACE3"
}]
};
//#endregion
//#region src/federation/middleware.test.ts
const documentLoader = getDocumentLoader();
let logtapeLock = Promise.resolve();
async function withLogtapeLock(fn) {
const run = logtapeLock.then(fn, fn);
logtapeLock = run.then(() => void 0, () => void 0);
return await run;
}
test("createFederation()", async (t) => {
const kv = new MemoryKvStore();
await t.step("allowPrivateAddress", () => {
assertThrows(() => createFederation({
kv,
contextLoaderFactory: () => mockDocumentLoader,
allowPrivateAddress: true
}), TypeError);
assertThrows(() => createFederation({
kv,
authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
allowPrivateAddress: true
}), TypeError);
});
await t.step("origin", () => {
const f = createFederation({
kv,
origin: "http://example.com:8080"
});
assertInstanceOf(f, FederationImpl);
assertEquals(f.origin, {
handleHost: "example.com:8080",
webOrigin: "http://example.com:8080"
});
assertThrows(() => createFederation({
kv,
origin: "example.com"
}), TypeError);
assertThrows(() => createFederation({
kv,
origin: "ftp://example.com"
}), TypeError);
assertThrows(() => createFederation({
kv,
origin: "https://example.com/foo"
}), TypeError);
assertThrows(() => createFederation({
kv,
origin: "https://example.com/?foo"
}), TypeError);
assertThrows(() => createFederation({
kv,
origin: "https://example.com/#foo"
}), TypeError);
const f2 = createFederation({
kv,
origin: {
handleHost: "example.com:8080",
webOrigin: "https://ap.example.com"
}
});
assertInstanceOf(f2, FederationImpl);
assertEquals(f2.origin, {
handleHost: "example.com:8080",
webOrigin: "https://ap.example.com"
});
assertThrows(() => createFederation({
kv,
origin: {
handleHost: "https://example.com",
webOrigin: "https://example.com"
}
}), TypeError);
assertThrows(() => createFederation({
kv,
origin: {
handleHost: "example.com/",
webOrigin: "https://example.com"
}
}), TypeError);
assertThrows(() => createFederation({
kv,
origin: {
handleHost: "example.com",
webOrigin: "example.com"
}
}), TypeError);
assertThrows(() => createFederation({
kv,
origin: {
handleHost: "example.com",
webOrigin: "ftp://example.com"
}
}), TypeError);
assertThrows(() => createFederation({
kv,
origin: {
handleHost: "example.com",
webOrigin: "https://example.com/foo"
}
}), TypeError);
assertThrows(() => createFederation({
kv,
origin: {
handleHost: "example.com",
webOrigin: "https://example.com/?foo"
}
}), TypeError);
assertThrows(() => createFederation({
kv,
origin: {
handleHost: "example.com",
webOrigin: "https://example.com/#foo"
}
}), TypeError);
});
});
test({
name: "Federation.createContext()",
permissions: {
env: true,
read: true
},
async fn(t) {
const kv = new MemoryKvStore();
esm_default.spyGlobal();
esm_default.get("https://example.com/auth-check", async (cl) => {
const v = await verifyRequest(cl.request, {
contextLoader: mockDocumentLoader,
documentLoader: mockDocumentLoader,
currentTime: Temporal.Now.instant()
});
return new Response(JSON.stringify(v != null), { headers: { "Content-Type": "application/json" } });
});
await t.step("Context", async () => {
const rejectingLoader = (_url) => Promise.reject(/* @__PURE__ */ new Error("Not found"));
const federation = createFederation({
kv,
documentLoaderFactory: () => rejectingLoader,
contextLoaderFactory: () => mockDocumentLoader
});
let ctx = federation.createContext(new URL("https://example.com:1234/"), 123);
assertEquals(ctx.data, 123);
assertEquals(ctx.origin, "https://example.com:1234");
assertEquals(ctx.canonicalOrigin, "https://example.com:1234");
assertEquals(ctx.host, "example.com:1234");
assertEquals(ctx.hostname, "example.com");
assertStrictEquals(ctx.documentLoader, rejectingLoader);
assertStrictEquals(ctx.contextLoader, mockDocumentLoader);
assertStrictEquals(ctx.federation, federation);
assertThrows(() => ctx.getNodeInfoUri(), RouterError);
assertThrows(() => ctx.getActorUri("handle"), RouterError);
assertThrows(() => ctx.getObjectUri(vocab.Note, {
handle: "handle",
id: "id"
}), RouterError);
assertThrows(() => ctx.getInboxUri(), RouterError);
assertThrows(() => ctx.getInboxUri("handle"), RouterError);
assertThrows(() => ctx.getOutboxUri("handle"), RouterError);
assertThrows(() => ctx.getFollowingUri("handle"), RouterError);
assertThrows(() => ctx.getFollowersUri("handle"), RouterError);
assertThrows(() => ctx.getLikedUri("handle"), RouterError);
assertThrows(() => ctx.getFeaturedUri("handle"), RouterError);
assertThrows(() => ctx.getFeaturedTagsUri("handle"), RouterError);
assertThrows(() => ctx.getCollectionUri("test", { id: "123" }), RouterError);
assertEquals(ctx.parseUri(new URL("https://example.com/")), null);
assertEquals(ctx.parseUri(null), null);
assertEquals(await ctx.getActorKeyPairs("handle"), []);
await assertRejects(() => ctx.getDocumentLoader({ identifier: "handle" }), Error, "No actor key pairs dispatcher registered");
await assertRejects(() => ctx.sendActivity({ identifier: "handle" }, [], new vocab.Create({})), Error, "No actor key pairs dispatcher registered");
federation.setNodeInfoDispatcher("/nodeinfo/2.1", () => ({
software: {
name: "Example",
version: "1.2.3"
},
protocols: ["activitypub"],
usage: {
users: {},
localPosts: 123,
localComments: 456
}
}));
ctx = federation.createContext(new URL("https://example.com/"), 123);
assertEquals(ctx.getNodeInfoUri(), new URL("https://example.com/nodeinfo/2.1"));
federation.setActorDispatcher("/users/{identifier}", () => new vocab.Person({})).setKeyPairsDispatcher(() => [{
privateKey: rsaPrivateKey2,
publicKey: rsaPublicKey2.publicKey
}, {
privateKey: ed25519PrivateKey,
publicKey: ed25519PublicKey.publicKey
}]).mapHandle((_, username) => username === "HANDLE" ? "handle" : null);
ctx = federation.createContext(new URL("https://example.com/"), 123);
assertEquals(ctx.getActorUri("handle"), new URL("https://example.com/users/handle"));
assertEquals(ctx.parseUri(new URL("https://example.com/")), null);
assertEquals(ctx.parseUri(new URL("https://example.com/users/handle")), {
type: "actor",
identifier: "handle"
});
assertEquals(ctx.parseUri(null), null);
assertEquals(await ctx.getActorKeyPairs("handle"), [{
keyId: new URL("https://example.com/users/handle#main-key"),
privateKey: rsaPrivateKey2,
publicKey: rsaPublicKey2.publicKey,
cryptographicKey: rsaPublicKey2.clone({
id: new URL("https://example.com/users/handle#main-key"),
owner: new URL("https://example.com/users/handle")
}),
multikey: new vocab.Multikey({
id: new URL("https://example.com/users/handle#multikey-1"),
controller: new URL("https://example.com/users/handle"),
publicKey: rsaPublicKey2.publicKey
})
}, {
keyId: new URL("https://example.com/users/handle#key-2"),
privateKey: ed25519PrivateKey,
publicKey: ed25519PublicKey.publicKey,
cryptographicKey: ed25519PublicKey.clone({
id: new URL("https://example.com/users/handle#key-2"),
owner: new URL("https://example.com/users/handle")
}),
multikey: new vocab.Multikey({
id: new URL("https://example.com/users/handle#multikey-2"),
controller: new URL("https://example.com/users/handle"),
publicKey: ed25519PublicKey.publicKey
})
}]);
assertEquals(await (await ctx.getDocumentLoader({ identifier: "handle" }))("https://example.com/auth-check"), {
contextUrl: null,
documentUrl: "https://example.com/auth-check",
document: true
});
assertEquals(await (await ctx.getDocumentLoader({ username: "HANDLE" }))("https://example.com/auth-check"), {
contextUrl: null,
documentUrl: "https://example.com/auth-check",
document: true
});
assertEquals(await ctx.getDocumentLoader({
keyId: new URL("https://example.com/key2"),
privateKey: rsaPrivateKey2
})("https://example.com/auth-check"), {
contextUrl: null,
documentUrl: "https://example.com/auth-check",
document: true
});
assertEquals(await ctx.lookupObject("https://example.com/object"), null);
await assertRejects(() => ctx.sendActivity({ identifier: "handle" }, [], new vocab.Create({})), TypeError, "The activity to send must have at least one actor property.");
await ctx.sendActivity({ identifier: "handle" }, [], new vocab.Create({ actor: new URL("https://example.com/users/handle") }));
esm_default.get("https://example.com/object", () => new Response(JSON.stringify({
"@context": "https://www.w3.org/ns/activitystreams",
type: "Object",
id: "https://example.com/object",
name: "Fetched object"
}), { headers: { "Content-Type": "application/activity+json" } }));
assertEquals(await createFederation({
kv,
documentLoaderFactory: () => documentLoader,
contextLoaderFactory: () => mockDocumentLoader
}).createContext(new URL("https://example.com/"), 123).lookupObject("https://example.com/object"), new vocab.Object({
id: new URL("https://example.com/object"),
name: "Fetched object"
}));
federation.setObjectDispatcher(vocab.Note, "/users/{identifier}/notes/{id}", (_ctx, values) => {
return new vocab.Note({ summary: `Note ${values.id} by ${values.identifier}` });
});
ctx = federation.createContext(new URL("https://example.com/"), 123);
assertEquals(ctx.getObjectUri(vocab.Note, {
identifier: "john",
id: "123"
}), new URL("https://example.com/users/john/notes/123"));
assertEquals(ctx.parseUri(new URL("https://example.com/users/john/notes/123")), {
type: "object",
class: vocab.Note,
typeId: new URL("https://www.w3.org/ns/activitystreams#Note"),
values: {
identifier: "john",
id: "123"
}
});
assertEquals(ctx.parseUri(null), null);
federation.setInboxListeners("/users/{identifier}/inbox", "/inbox");
ctx = federation.createContext(new URL("https://example.com/"), 123);
assertEquals(ctx.getInboxUri(), new URL("https://example.com/inbox"));
assertEquals(ctx.getInboxUri("handle"), new URL("https://example.com/users/handle/inbox"));
assertEquals(ctx.parseUri(new URL("https://example.com/inbox")), {
type: "inbox",
identifier: void 0
});
assertEquals(ctx.parseUri(new URL("https://example.com/users/handle/inbox")), {
type: "inbox",
identifier: "handle"
});
assertEquals(ctx.parseUri(null), null);
federation.setOutboxDispatcher("/users/{identifier}/outbox", () => ({ items: [] }));
ctx = federation.createContext(new URL("https://example.com/"), 123);
assertEquals(ctx.getOutboxUri("handle"), new URL("https://example.com/users/handle/outbox"));
assertEquals(ctx.parseUri(new URL("https://example.com/users/handle/outbox")), {
type: "outbox",
identifier: "handle"
});
assertEquals(ctx.parseUri(null), null);
federation.setFollowingDispatcher("/users/{identifier}/following", () => ({ items: [] }));
ctx = federation.createContext(new URL("https://example.com/"), 123);
assertEquals(ctx.getFollowingUri("handle"), new URL("https://example.com/users/handle/following"));
assertEquals(ctx.parseUri(new URL("https://example.com/users/handle/following")), {
type: "following",
identifier: "handle"
});
assertEquals(ctx.parseUri(null), null);
federation.setFollowersDispatcher("/users/{identifier}/followers", () => ({ items: [] }));
ctx = federation.createContext(new URL("https://example.com/"), 123);
assertEquals(ctx.getFollowersUri("handle"), new URL("https://example.com/users/handle/followers"));
assertEquals(ctx.parseUri(new URL("https://example.com/users/handle/followers")), {
type: "followers",
identifier: "handle"
});
assertEquals(ctx.parseUri(null), null);
federation.setLikedDispatcher("/users/{identifier}/liked", () => ({ items: [] }));
ctx = federation.createContext(new URL("https://example.com/"), 123);
assertEquals(ctx.getLikedUri("handle"), new URL("https://example.com/users/handle/liked"));
assertEquals(ctx.parseUri(new URL("https://example.com/users/handle/liked")), {
type: "liked",
identifier: "handle"
});
assertEquals(ctx.parseUri(null), null);
federation.setFeaturedDispatcher("/users/{identifier}/featured", () => ({ items: [] }));
ctx = federation.createContext(new URL("https://example.com/"), 123);
assertEquals(ctx.getFeaturedUri("handle"), new URL("https://example.com/users/handle/featured"));
assertEquals(ctx.parseUri(new URL("https://example.com/users/handle/featured")), {
type: "featured",
identifier: "handle"
});
assertEquals(ctx.parseUri(null), null);
federation.setFeaturedTagsDispatcher("/users/{identifier}/tags", () => ({ items: [] }));
ctx = federation.createContext(new URL("https://example.com/"), 123);
assertEquals(ctx.getFeaturedTagsUri("handle"), new URL("https://example.com/users/handle/tags"));
assertEquals(ctx.parseUri(new URL("https://example.com/users/handle/tags")), {
type: "featuredTags",
identifier: "handle"
});
assertEquals(ctx.parseUri(null), null);
});
await t.step("Context with origin", () => {
const federation = createFederation({
kv,
origin: "https://ap.example.com",
documentLoaderFactory: () => mockDocumentLoader,
contextLoaderFactory: () => mockDocumentLoader
});
const ctx = federation.createContext(new URL("https://example.com:1234/"));
assertEquals(ctx.origin, "https://example.com:1234");
assertEquals(ctx.canonicalOrigin, "https://ap.example.com");
assertEquals(ctx.host, "example.com:1234");
assertEquals(ctx.hostname, "example.com");
federation.setNodeInfoDispatcher("/nodeinfo/2.1", () => ({
software: {
name: "Example",
version: "1.2.3"
},
protocols: ["activitypub"],
usage: {
users: {},
localPosts: 123,
localComments: 456
}
}));
assertEquals(ctx.getNodeInfoUri(), new URL("https://ap.example.com/nodeinfo/2.1"));
federation.setActorDispatcher("/users/{identifier}", () => new vocab.Person({}));
assertEquals(ctx.getActorUri("handle"), new URL("https://ap.example.com/users/handle"));
assertEquals(ctx.parseUri(new URL("https://ap.example.com/users/handle")), {
type: "actor",
identifier: "handle"
});
assertEquals(ctx.parseUri(new URL("https://example.com:1234/users/handle")), {
type: "actor",
identifier: "handle"
});
federation.setObjectDispatcher(vocab.Note, "/users/{identifier}/notes/{id}", (_ctx, values) => {
return new vocab.Note({ summary: `Note ${values.id} by ${values.identifier}` });
});
assertEquals(ctx.getObjectUri(vocab.Note, {
identifier: "john",
id: "123"
}), new URL("https://ap.example.com/users/john/notes/123"));
assertEquals(ctx.parseUri(new URL("https://ap.example.com/users/john/notes/123")), {
type: "object",
class: vocab.Note,
typeId: new URL("https://www.w3.org/ns/activitystreams#Note"),
values: {
identifier: "john",
id: "123"
}
});
assertEquals(ctx.parseUri(new URL("https://example.com:1234/users/john/notes/123")), {
type: "object",
class: vocab.Note,
typeId: new URL("https://www.w3.org/ns/activitystreams#Note"),
values: {
identifier: "john",
id: "123"
}
});
federation.setInboxListeners("/users/{identifier}/inbox", "/inbox");
assertEquals(ctx.getInboxUri(), new URL("https://ap.example.com/inbox"));
assertEquals(ctx.getInboxUri("handle"), new URL("https://ap.example.com/users/handle/inbox"));
assertEquals(ctx.parseUri(new URL("https://ap.example.com/inbox")), {
type: "inbox",
identifier: void 0
});
assertEquals(ctx.parseUri(new URL("https://example.com:1234/inbox")), {
type: "inbox",
identifier: void 0
});
assertEquals(ctx.parseUri(new URL("https://ap.example.com/users/handle/inbox")), {
type: "inbox",
identifier: "handle"
});
assertEquals(ctx.parseUri(new URL("https://example.com:1234/users/handle/inbox")), {
type: "inbox",
identifier: "handle"
});
federation.setOutboxDispatcher("/users/{identifier}/outbox", () => ({ items: [] }));
assertEquals(ctx.getOutboxUri("handle"), new URL("https://ap.example.com/users/handle/outbox"));
assertEquals(ctx.parseUri(new URL("https://ap.example.com/users/handle/outbox")), {
type: "outbox",
identifier: "handle"
});
assertEquals(ctx.parseUri(new URL("https://example.com:1234/users/handle/outbox")), {
type: "outbox",
identifier: "handle"
});
federation.setFollowingDispatcher("/users/{identifier}/following", () => ({ items: [] }));
assertEquals(ctx.getFollowingUri("handle"), new URL("https://ap.example.com/users/handle/following"));
assertEquals(ctx.parseUri(new URL("https://ap.example.com/users/handle/following")), {
type: "following",
identifier: "handle"
});
assertEquals(ctx.parseUri(new URL("https://example.com:1234/users/handle/following")), {
type: "following",
identifier: "handle"
});
federation.setFollowersDispatcher("/users/{identifier}/followers", () => ({ items: [] }));
assertEquals(ctx.getFollowersUri("handle"), new URL("https://ap.example.com/users/handle/followers"));
assertEquals(ctx.parseUri(new URL("https://ap.example.com/users/handle/followers")), {
type: "followers",
identifier: "handle"
});
assertEquals(ctx.parseUri(new URL("https://example.com:1234/users/handle/followers")), {
type: "followers",
identifier: "handle"
});
federation.setLikedDispatcher("/users/{identifier}/liked", () => ({ items: [] }));
assertEquals(ctx.getLikedUri("handle"), new URL("https://ap.example.com/users/handle/liked"));
assertEquals(ctx.parseUri(new URL("https://ap.example.com/users/handle/liked")), {
type: "liked",
identifier: "handle"
});
assertEquals(ctx.parseUri(new URL("https://example.com:1234/users/handle/liked")), {
type: "liked",
identifier: "handle"
});
federation.setFeaturedDispatcher("/users/{identifier}/featured", () => ({ items: [] }));
assertEquals(ctx.getFeaturedUri("handle"), new URL("https://ap.example.com/users/handle/featured"));
assertEquals(ctx.parseUri(new URL("https://ap.example.com/users/handle/featured")), {
type: "featured",
identifier: "handle"
});
assertEquals(ctx.parseUri(new URL("https://example.com:1234/users/handle/featured")), {
type: "featured",
identifier: "handle"
});
federation.setFeaturedTagsDispatcher("/users/{identifier}/tags", () => ({ items: [] }));
assertEquals(ctx.getFeaturedTagsUri("handle"), new URL("https://ap.example.com/users/handle/tags"));
assertEquals(ctx.parseUri(new URL("https://ap.example.com/users/handle/tags")), {
type: "featuredTags",
identifier: "handle"
});
assertEquals(ctx.parseUri(new URL("https://example.com:1234/users/handle/tags")), {
type: "featuredTags",
identifier: "handle"
});
});
await t.step("Context.clone()", () => {
const ctx = createFederation({ kv }).createContext(new URL("https://example.com/"), 123);
const clone = ctx.clone(456);
assertStrictEquals(clone.canonicalOrigin, ctx.canonicalOrigin);
assertStrictEquals(clone.origin, ctx.origin);
assertEquals(clone.data, 456);
assertEquals(clone.host, ctx.host);
assertEquals(clone.hostname, ctx.hostname);
assertStrictEquals(clone.documentLoader, ctx.documentLoader);
assertStrictEquals(clone.contextLoader, ctx.contextLoader);
assertStrictEquals(clone.federation, ctx.federation);
});
esm_default.get("https://example.com/.well-known/nodeinfo", (cl) => {
const headers = cl.options.headers ?? {};
assertEquals(new Headers(headers).get("User-Agent"), "CustomUserAgent/1.2.3");
return new Response(JSON.stringify({ links: [{
rel: "http://nodeinfo.diaspora.software/ns/schema/2.1",
href: "https://example.com/nodeinfo/2.1"
}] }));
});
esm_default.get("https://example.com/nodeinfo/2.1", (cl) => {
const headers = cl.options.headers ?? {};
assertEquals(new Headers(headers).get("User-Agent"), "CustomUserAgent/1.2.3");
return new Response(JSON.stringify({
software: {
name: "foo",
version: "1.2.3"
},
protocols: ["activitypub", "diaspora"],
usage: {
users: {},
localPosts: 123,
localComments: 456
}
}));
});
await t.step("Context.lookupNodeInfo()", async () => {
const ctx = createFederation({
kv,
userAgent: "CustomUserAgent/1.2.3"
}).createContext(new URL("https://example.com/"), 123);
assertEquals(await ctx.lookupNodeInfo("https://example.com/"), {
software: {
name: "foo",
version: "1.2.3"
},
protocols: ["activitypub", "diaspora"],
usage: {
users: {},
localPosts: 123,
localComments: 456
}
});
assertEquals(await ctx.lookupNodeInfo("https://example.com/", { parse: "none" }), {
software: {
name: "foo",
version: "1.2.3"
},
protocols: ["activitypub", "diaspora"],
usage: {
users: {},
localPosts: 123,
localComments: 456
}
});
});
await t.step("RequestContext", async () => {
const federation = createFederation({
kv,
documentLoaderFactory: () => mockDocumentLoader
});
const req = new Request("https://example.com/", { headers: { "accept": "application/ld+json" } });
const ctx = federation.createContext(req, 123);
assertEquals(ctx.request, req);
assertEquals(ctx.url, new URL("https://example.com/"));
assertEquals(ctx.origin, "https://example.com");
assertEquals(ctx.host, "example.com");
assertEquals(ctx.hostname, "example.com");
assertEquals(ctx.data, 123);
await assertRejects(() => ctx.getActor("someone"), Error);
await assertRejects(() => ctx.getObject(vocab.Note, {
handle: "someone",
id: "123"
}), Error);
assertEquals(await ctx.getSignedKey(), null);
assertEquals(await ctx.getSignedKeyOwner(), null);
assertEquals(await ctx.getSignedKey(), null);
assertEquals(await ctx.getSignedKeyOwner(), null);
await assertRejects(() => ctx.getActor("someone"), Error, "No actor dispatcher registered");
const signedReq = await signRequest(new Request("https://example.com/", { headers: { "accept": "application/ld+json" } }), rsaPrivateKey2, rsaPublicKey2.id);
const signedCtx = federation.createContext(signedReq, 456);
assertEquals(signedCtx.request, signedReq);
assertEquals(signedCtx.url, new URL("https://example.com/"));
assertEquals(signedCtx.data, 456);
assertEquals(await signedCtx.getSignedKey(), rsaPublicKey2);
assertEquals(await signedCtx.getSignedKeyOwner(), null);
assertEquals(await signedCtx.getSignedKey(), rsaPublicKey2);
assertEquals(await signedCtx.getSignedKeyOwner(), null);
const signedReq2 = await signRequest(new Request("https://example.com/", { headers: { "accept": "application/ld+json" } }), rsaPrivateKey3, rsaPublicKey3.id);
const signedCtx2 = federation.createContext(signedReq2, 456);
assertEquals(signedCtx2.request, signedReq2);
assertEquals(signedCtx2.url, new URL("https://example.com/"));
assertEquals(signedCtx2.data, 456);
assertEquals(await signedCtx2.getSignedKey(), rsaPublicKey3);
const expectedOwner = await lookupObject("https://example.com/person2", {
documentLoader: mockDocumentLoader,
contextLoader: mockDocumentLoader
});
assertEquals(await signedCtx2.getSignedKeyOwner(), expectedOwner);
assertEquals(await signedCtx2.getSignedKey(), rsaPublicKey3);
assertEquals(await signedCtx2.getSignedKeyOwner(), expectedOwner);
federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => identifier === "gone" ? new vocab.Tombstone({
id: ctx.getActorUri(identifier),
deleted: Temporal.Instant.from("2024-01-15T00:00:00Z")
}) : new vocab.Person({ preferredUsername: identifier }));
const ctx2 = federation.createContext(req, 789);
assertEquals(ctx2.request, req);
assertEquals(ctx2.url, new URL("https://example.com/"));
assertEquals(ctx2.data, 789);
assertEquals(await ctx2.getActor("john"), new vocab.Person({ preferredUsername: "john" }));
assertEquals(await ctx2.getActor("gone"), null);
assertEquals(await ctx2.getActor("gone", { tombstone: "passthrough" }), new vocab.Tombstone({
id: new URL("https://example.com/users/gone"),
deleted: Temporal.Instant.from("2024-01-15T00:00:00Z")
}));
assertEquals(await ctx2.getActor("gone", { tombstone: "passthrough" }), new vocab.Tombstone({
id: new URL("https://example.com/users/gone"),
deleted: Temporal.Instant.from("2024-01-15T00:00:00Z")
}));
federation.setObjectDispatcher(vocab.Note, "/users/{identifier}/notes/{id}", (_ctx, values) => {
return new vocab.Note({ summary: `Note ${values.id} by ${values.identifier}` });
});
const ctx3 = federation.createContext(req, 123);
assertEquals(ctx3.request, req);
assertEquals(ctx3.url, new URL("https://example.com/"));
assertEquals(ctx3.data, 123);
assertEquals(await ctx2.getObject(vocab.Note, {
identifier: "john",
id: "123"
}), new vocab.Note({ summary: "Note 123 by john" }));
});
await t.step("RequestContext.getSignedKeyOwner() returns null on FetchError", async () => {
const customDocumentLoader = async (url) => {
if (url === "https://example.com/person2#key3") return await mockDocumentLoader("https://example.com/person2");
if (url === "https://example.com/person2") throw new FetchError(new URL(url), "HTTP 401: Unauthorized");
return mockDocumentLoader(url);
};
const signedReq = await signRequest(new Request("https://example.com/", { headers: { accept: "application/activity+json" } }), rsaPrivateKey3, rsaPublicKey3.id);
assertEquals(await createFederation({
kv,
documentLoaderFactory: () => customDocumentLoader,
contextLoaderFactory: () => mockDocumentLoader
}).createContext(signedReq, void 0).getSignedKeyOwner(), null);
});
await t.step("RequestContext.clone()", () => {
const federation = createFederation({ kv });
const req = new Request("https://example.com/", { headers: { "accept": "application/ld+json" } });
const ctx = federation.createContext(req, 123);
const clone = ctx.clone(456);
assertStrictEquals(clone.request, ctx.request);
assertEquals(clone.url, ctx.url);
assertEquals(clone.data, 456);
assertEquals(clone.origin, ctx.origin);
assertEquals(clone.host, ctx.host);
assertEquals(clone.hostname, ctx.hostname);
assertStrictEquals(clone.documentLoader, ctx.documentLoader);
assertStrictEquals(clone.contextLoader, ctx.contextLoader);
assertStrictEquals(clone.federation, ctx.federation);
});
esm_default.hardReset();
}
});
test("Federation.fetch()", async (t) => {
esm_default.spyGlobal();
esm_default.get("https://example.com/key2", {
headers: { "Content-Type": "application/activity+json" },
body: await rsaPublicKey2.toJsonLd({ contextLoader: mockDocumentLoader })
});
esm_default.get("begin:https://example.com/person", {
headers: { "Content-Type": "application/activity+json" },
body: person_default
});
const createTestContext = () => {
const kv = new MemoryKvStore();
const inbox = [];
const dispatches = [];
const federation = createFederation({
kv,
documentLoaderFactory: () => mockDocumentLoader,
authenticatedDocumentLoaderFactory(identity) {
const docLoader = getAuthenticatedDocumentLoader(identity);
return (url) => {
if (new URL(url).host === "example.com") return docLoader(url);
return mockDocumentLoader(url);
};
}
});
federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => {
dispatches.push(identifier);
if (identifier === "gone") return new vocab.Tombstone({
id: ctx.getActorUri(identifier),
deleted: Temporal.Instant.from("2024-01-15T00:00:00Z")
});
return new vocab.Person({
id: ctx.getActorUri(identifier),
inbox: ctx.getInboxUri(identifier),
preferredUsername: identifier
});
}).setKeyPairsDispatcher(() => {
return [{
privateKey: rsaPrivateKey2,
publicKey: rsaPublicKey2.publicKey
}];
});
federation.setInboxDispatcher("/users/{identifier}/inbox", () => {
return { items: [] };
});
federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(vocab.Create, (_ctx, activity) => {
inbox.push(activity.id.toString());
});
return {
federation,
inbox,
dispatches
};
};
await t.step("GET without accepts header", async () => {
const { federation, dispatches } = createTestContext();
const response = await federation.fetch(new Request("https://example.com/users/actor", { method: "GET" }), { contextData: void 0 });
assertEquals(dispatches, []);
assertEquals(response.status, 406);
});
await t.step("POST with application/json", async () => {
const { federation, inbox } = createTestContext();
const request = await signRequest(new Request("https://example.com/users/json/inbox", {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify(create_default)
}), rsaPrivateKey2, rsaPublicKey2.id);
assertEquals((await federation.fetch(request, { contextData: void 0 })).status, 202);
assertEquals(inbox.length, 1, "Expected one item in the inbox, json");
assertEquals(inbox[0], id$2);
});
await t.step("GET with application/json", async () => {
const { federation, dispatches } = createTestContext();
const response = await federation.fetch(new Request("https://example.com/users/json", {
method: "GET",
headers: { "Accept": "application/json" }
}), { contextData: void 0 });
assertEquals(dispatches, ["json"]);
assertEquals(response.status, 200);
});
await t.step("POST with application/ld+json", async () => {
const { federation, inbox } = createTestContext();
const request = await signRequest(new Request("https://example.com/users/ld/inbox", {
method: "POST",
headers: {
"Accept": "application/ld+json",
"Content-Type": "application/activity+json"
},
body: JSON.stringify(create_default)
}), rsaPrivateKey2, rsaPublicKey2.id);
assertEquals((await federation.fetch(request, { contextData: void 0 })).status, 202);
assertEquals(inbox.length, 1, "Expected one inbox activity, ld+json");
assertEquals(inbox[0], id$2);
});
await t.step("GET with application/ld+json", async () => {
const { federation, dispatches } = createTestContext();
const request = new Request("https://example.com/users/ld", {
method: "GET",
headers: { "Accept": "application/ld+json" }
});
const response = await federation.fetch(request, { contextData: void 0 });
assertEquals(dispatches, ["ld"]);
assertEquals(response.status, 200);
});
await t.step("POST with application/activity+json", async () => {
const { federation, inbox } = createTestContext();
const request = await signRequest(new Request("https://example.com/users/activity/inbox", {
method: "POST",
headers: {
"Accept": "application/activity+json",
"Content-Type": "application/activity+json"
},
body: JSON.stringify(create_default)
}), rsaPrivateKey2, rsaPublicKey2.id);
assertEquals((await federation.fetch(request, { contextData: void 0 })).status, 202);
assertEquals(inbox.length, 1);
assertEquals(inbox[0], id$2);
});
await t.step("GET with application/activity+json", async () => {
const { federation, dispatches } = createTestContext();
const request = new Request("https://example.com/users/activity", {
method: "GET",
headers: { "Accept": "application/ld+json" }
});
const response = await federation.fetch(request, { contextData: void 0 });
assertEquals(dispatches, ["activity"]);
assertEquals(response.status, 200);
});
await t.step("GET tombstoned actor returns 410 Gone", async () => {
const { federation, dispatches } = createTestContext();
const response = await federation.fetch(new Request("https://example.com/users/gone", {
method: "GET",
headers: { "Accept": "application/activity+json" }
}), { contextData: void 0 });
assertEquals(dispatches, ["gone"]);
assertEquals(response.status, 410);
assertEquals(await response.json(), {
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/data-integrity/v1",
"https://gotosocial.org/ns"
],
id: "https://example.com/users/gone",
type: "Tombstone",
deleted: "2024-01-15T00:00:00Z"
});
});
await t.step("WebFinger for tombstoned actor returns 410 Gone", async () => {
const { federation, dispatches } = createTestContext();
const response = await federation.fetch(new Request("https://example.com/.well-known/webfinger?resource=acct:gone@example.com"), { contextData: void 0 });
assertEquals(dispatches, ["gone"]);
assertEquals(response.status, 410);
assertEquals(response.headers.get("Access-Control-Allow-Origin"), "*");
});
await t.step("POST to tombstoned inbox returns not found", async () => {
const { federation, inbox } = createTestContext();
const response = await federation.fetch(new Request("https://example.com/users/gone/inbox", {
method: "POST",
headers: { "accept": "application/ld+json" }
}), { contextData: void 0 });
assertEquals(inbox, []);
assertEquals(response.status, 404);
});
await t.step("onNotAcceptable with GET", async () => {
const { federation } = createTestContext();
let notAcceptableCalled = false;
const response = await federation.fetch(new Request("https://example.com/users/html", {
method: "GET",
headers: { "Accept": "text/html" }
}), {
contextData: void 0,
onNotAcceptable: () => {
notAcceptableCalled = true;
return new Response("handled by onNotAcceptable", { status: 200 });
}
});
assertEquals(notAcceptableCalled, true);
assertEquals(response.status, 200);
assertEquals(await response.text(), "handled by onNotAcceptable");
});
esm_default.hardReset();
});
test("Federation.setInboxListeners()", async (t) => {
const kv = new MemoryKvStore();
esm_default.spyGlobal();
esm_default.get("https://example.com/key2", {
headers: { "Content-Type": "application/activity+json" },
body: await rsaPublicKey2.toJsonLd({ contextLoader: mockDocumentLoader })
});
esm_default.get("begin:https://example.com/person2", {
headers: { "Content-Type": "application/activity+json" },
body: person2_default
});
esm_default.get("begin:https://example.com/person", {
headers: { "Content-Type": "application/activity+json" },
body: person_default
});
await t.step("path match", () => {
const federation = createFederation({
kv,
documentLoaderFactory: () => mockDocumentLoader
});
federation.setInboxDispatcher("/users/{identifier}/inbox", () => ({ items: [] }));
assertThrows(() => federation.setInboxListeners("/users/{identifier}/inbox2"), RouterError);
});
await t.step("wrong variables in path", () => {
const federation = createFederation({
kv,
documentLoaderFactory: () => mockDocumentLoader
});
assertThrows(() => federation.setInboxListeners("/users/inbox"), RouterError);
assertThrows(() => federation.setInboxListeners("/users/{identifier}/inbox/{id2}"), RouterError);
assertThrows(() => federation.setInboxListeners("/users/{identifier}/inbox/{extra}"), RouterError);
assertThrows(() => federation.setInboxListeners("/users/{identifier2}/inbox"), RouterError);
});
await t.step("on()", async () => {
const authenticatedRequests = [];
const federation = createFederation({
kv,
documentLoaderFactory: () => mockDocumentLoader,
authenticatedDocumentLoaderFactory(identity) {
const docLoader = getAuthenticatedDocumentLoader(identity);
return (url) => {
const urlObj = new URL(url);
authenticatedRequests.push([url, identity.keyId.href]);
if (urlObj.host === "example.com") return docLoader(url);
return mockDocumentLoader(url);
};
}
});
const inbox = [];
federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(vocab.Create, (ctx, create) => {
inbox.push([ctx, create]);
});
let response = await federation.fetch(new Request("https://example.com/inbox", {
method: "POST",
headers: { "accept": "application/ld+json" }
}), { contextData: void 0 });
assertEquals(inbox, []);
assertEquals(response.status, 404);
federation.setActorDispatcher("/users/{identifier}", (_, identifier) => identifier === "john" ? new vocab.Person({}) : null).setKeyPairsDispatcher(() => [{
privateKey: rsaPrivateKey2,
publicKey: rsaPublicKey2.publicKey
}]);
const options = {
documentLoader: mockDocumentLoader,
contextLoader: mockDocumentLoader
};
const activity = () => new vocab.Create({
id: new URL("https://example.com/activities/" + crypto.randomUUID()),
actor: new URL("https://example.com/person2")
});
response = await federation.fetch(new Request("https://example.com/inbox", {
method: "POST",
body: JSON.stringify(await activity().toJsonLd(options)),
headers: {
"accept": "application/ld+json",
"content-type": "application/ld+json"
}
}), { contextData: void 0 });
assertEquals(inbox, []);
assertEquals(response.status, 401);
response = await federation.fetch(new Request("https://example.com/users/no-one/inbox", {
method: "POST",
headers: { "accept": "application/ld+json" }
}), { contextData: void 0 });
assertEquals(inbox, []);
assertEquals(response.status, 404);
response = await federation.fetch(new Request("https://example.com/users/john/inbox", {
method: "POST",
body: JSON.stringify(await activity().toJsonLd(options)),
headers: {
"accept": "application/ld+json",
"content-type": "application/ld+json"
}
}), { contextData: void 0 });
assertEquals(inbox, []);
assertEquals(response.status, 401);
const activityPayload = await activity().toJsonLd(options);
let request = new Request("https://example.com/users/john/inbox", {
method: "POST",
headers: {
"Content-Type": "application/activity+json",
accept: "application/ld+json"
},
body: JSON.stringify(activityPayload)
});
request = await signRequest(request, rsaPrivateKey3, new URL("https://example.com/person2#key3"));
response = await federation.fetch(request, { contextData: void 0 });
assertEquals(inbox.length, 1);
assertEquals(inbox[0][1].actorId, new URL("https://example.com/person2"));
assertEquals(response.status, 202);
while (authenticatedRequests.length > 0) authenticatedRequests.shift();
assertEquals(authenticatedRequests, []);
await inbox[0][0].documentLoader("https://example.com/person");
assertEquals(authenticatedRequests, [["https://example.com/person", "https://example.com/users/john#main-key"]]);
response = await federation.fetch(request, { contextData: void 0 });
assertEquals(inbox.length, 1);
inbox.shift();
request = new Request("https://another.host/users/john/inbox", {
method: "POST",
headers: {
"Content-Type": "application/activity+json",
"accept": "application/ld+json"
},
body: JSON.stringify(activityPayload)
});
request = await signRequest(request, rsaPrivateKey3, new URL("https://example.com/person2#key3"));
response = await federation.fetch(request, { contextData: void 0 });
assertEquals(inbox.length, 1);
assertEquals(inbox[0][1].actorId, new URL("https://example.com/person2"));
assertEquals(response.status, 202);
while (authenticatedRequests.length > 0) authenticatedRequests.shift();
assertEquals(authenticatedRequests, []);
await inbox[0][0].documentLoader("https://example.com/person");
assertEquals(authenticatedRequests, [["https://example.com/person", "https://another.host/users/john#main-key"]]);
inbox.shift();
request = new Request("https://example.com/inbox", {
method: "POST",
headers: {
"Content-Type": "application/activity+json",
"accept": "application/ld+json"
},
body: JSON.stringify(await activity().toJsonLd(options))
});
request = await signRequest(request, rsaPrivateKey3, new URL("https://example.com/person2#key3"));
response = await federation.fetch(request, { contextData: void 0 });
assertEquals(inbox.length, 1);
assertEquals(inbox[0][1].actorId, new URL("https://example.com/person2"));
assertEquals(response.status, 202);
while (authenticatedRequests.length > 0) authenticatedRequests.shift();
assertEquals(authenticatedRequests, []);
await inbox[0][0].documentLoader("https://example.com/person");
assertEquals(authenticatedRequests, []);
inbox.shift();
request = new Request("https://example.com/users/john/inbox", {
method: "POST",
headers: {
"Content-Type": "application/activity+json",
"accept": "application/ld+json"
},
body: JSON.stringify(await (await signObject(activity(), ed25519PrivateKey, ed25519Multikey.id, options)).toJsonLd(options))
});
response = await federation.fetch(request, { contextData: void 0 });
assertEquals(inbox.length, 1);
assertEquals(inbox[0][1].actorId, new URL("https://example.com/person2"));
assertEquals(response.status, 202);
while (authenticatedRequests.length > 0) authenticatedRequests.shift();
assertEquals(authenticatedRequests, []);
await inbox[0][0].documentLoader("https://example.com/person");
assertEquals(authenticatedRequests, [["https://example.com/person", "https://example.com/users/john#main-key"]]);
});
await t.step("onUnverifiedActivity()", async (t) => {
const options = {
documentLoader: mockDocumentLoader,
contextLoader: mockDocumentLoader
};
async function createInboxRequest(activity, signature) {
let request = new Request("https://example.com/inbox", {
method: "POST",
headers: {
"Content-Type": "application/activity+json",
accept: "application/ld+json"
},
body: JSON.stringify(await activity.toJsonLd(options))
});
if (signature != null) request = await signRequest(request, signature.privateKey, signature.keyId);
return request;
}
function createFederationWithLoader(documentLoader) {
const federation = createFederation({
kv: new MemoryKvStore(),
documentLoaderFactory: () => documentLoader,
contextLoaderFactory: () => mockDocumentLoader
});
const verified = [];
federation.setActorDispatcher("/users/{identifier}", () => {
return new vocab.Person({});
});
return {
federation,
verified,
inboxListeners: federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(vocab.Create, (_ctx, activity) => {
verified.push(activity);
})
};
}
await t.step("receives noSignature reason", async () => {
const { federation, verified, inboxListeners } = createFederationWithLoader(mockDocumentLoader);
let receivedReason = null;
inboxListeners.onUnverifiedActivity((_ctx, _activity, reason) => {
receivedReason = reason;
return new Response(null, { status: 202 });
});
assertEquals((await federation.fetch(await createInboxRequest(new vocab.Create({
id: new URL("https://remote.example/activities/no-signature"),
actor: new URL("https://remote.example/actors/alice")
})), { contextData: void 0 })).status, 202);
assertEquals(receivedReason, { type: "noSignature" });
assertEquals(verified, []);
});
await t.step("receives keyFetchError for 410 responses", async () => {
const goneKeyId = new URL("https://gone.exam