UNPKG

@fedify/fedify

Version:

An ActivityPub server framework

1,147 lines • 176 kB
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