UNPKG

@fedify/fedify

Version:

An ActivityPub server framework

1,200 lines • 268 kB
import { Temporal } from "@js-temporal/polyfill"; import "urlpattern-polyfill"; globalThis.addEventListener = () => {}; import { t as esm_default } from "../esm-vrlUxr60.mjs"; import { t as assertEquals } from "../assert_equals-C-ZRDbaf.mjs"; import { r as assertExists } from "../std__assert-BBjXFNOb.mjs"; import { r as assertFalse, t as assertRejects } from "../assert_rejects-DN60FHPX.mjs"; import { t as assertInstanceOf } from "../assert_instance_of-DBC5X09g.mjs"; import { t as assertThrows } from "../assert_throws-BOkhLGYc.mjs"; import { t as assertNotEquals } from "../assert_not_equals-DkVK8oqV.mjs"; import { t as assertStrictEquals } from "../assert_strict_equals-XEgZAlrj.mjs"; import { t as assert } from "../assert-OguE97r2.mjs"; import { g as recordInboxActivity } from "../metrics-Pue1x4ta.mjs"; import { l as verifyRequest, s as signRequest } from "../http-PCt0862M.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-YZA0m_3O.mjs"; import { a as compactJsonLd, h as verifyJsonLd, p as signJsonLd, s as detachSignature } from "../ld-Dcw2skfn.mjs"; import { t as doesActorOwnKey } from "../owner-pVJ0Twi7.mjs"; import { i as verifyObject, r as signObject } from "../proof-pXYODmNV.mjs"; import { t as MemoryKvStore } from "../kv-rV3vodCc.mjs"; import { g as handleBenchmarkTrigger, i as KvSpecDeterminer, n as FederationImpl, o as createFederation, r as InboxContextImpl, t as ContextImpl } from "../middleware-WU_5fbGS.mjs"; import { t as CircuitBreaker } from "../circuit-breaker-CSWsyoef.mjs"; import { configure, reset } from "@logtape/logtape"; import { RouterError } from "@fedify/uri-template"; import * as vocab from "@fedify/vocab"; import { Create, Offer, Person, getTypeId, lookupObject } from "@fedify/vocab"; import { SpanStatusCode, metrics } from "@opentelemetry/api"; import { createTestMeterProvider, createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture"; import { FetchError, getDocumentLoader } from "@fedify/vocab-runtime"; import serialize from "json-canon"; import { DataPointType, MeterProvider as MeterProvider$1, MetricReader } from "@opentelemetry/sdk-metrics"; //#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; } var TestMetricReader = class extends MetricReader { onShutdown() { return Promise.resolve(); } onForceFlush() { return Promise.resolve(); } }; 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("benchmarkMode applies cooperative benchmark defaults", () => { const federation = createFederation({ kv, benchmarkMode: true }); assertInstanceOf(federation, FederationImpl); assertEquals(federation.allowPrivateAddress, true); assertEquals(federation.signatureTimeWindow, false); }); await t.step("benchmarkMode preserves explicit option overrides", () => { const federation = createFederation({ kv, benchmarkMode: true, allowPrivateAddress: false, signatureTimeWindow: { minutes: 10 } }); assertInstanceOf(federation, FederationImpl); assertEquals(federation.allowPrivateAddress, false); assertEquals(federation.signatureTimeWindow, { minutes: 10 }); }); await t.step("benchmarkMode tolerates calendar time windows", () => { const federation = createFederation({ kv, benchmarkMode: true, signatureTimeWindow: { months: 1 } }); assertInstanceOf(federation, FederationImpl); assertEquals(federation.signatureTimeWindow, { months: 1 }); }); await t.step("benchmarkMode leaves custom loader factories alone", () => { const federation = createFederation({ kv, benchmarkMode: true, documentLoaderFactory: () => mockDocumentLoader, contextLoaderFactory: () => mockDocumentLoader, authenticatedDocumentLoaderFactory: () => mockDocumentLoader }); assertInstanceOf(federation, FederationImpl); assertEquals(federation.allowPrivateAddress, false); }); await t.step("benchmarkMode keeps private-address default with auth loader only", () => { const federation = createFederation({ kv, benchmarkMode: true, authenticatedDocumentLoaderFactory: () => mockDocumentLoader }); assertInstanceOf(federation, FederationImpl); assertEquals(federation.allowPrivateAddress, true); }); await t.step("benchmarkMode rejects an explicit meterProvider", () => { const [meterProvider] = createTestMeterProvider(); assertThrows(() => createFederation({ kv, benchmarkMode: true, meterProvider }), TypeError, "benchmarkMode requires Fedify to own the meterProvider"); }); await t.step("benchmarkMode warns that benchmark-only relaxations are on", async () => { await withLogtapeLock(async () => { const records = []; await reset(); try { await configure({ sinks: { test(record) { records.push(record); } }, loggers: [{ category: [ "fedify", "federation", "benchmark" ], lowestLevel: "warning", sinks: ["test"] }] }); createFederation({ kv, benchmarkMode: true }); assertEquals(records.length, 1); assertEquals(records[0].level, "warning"); assertEquals(records[0].rawMessage, "Fedify benchmarkMode is enabled; private address checks disabled (allowPrivateAddress=true); HTTP Signature time window disabled (signatureTimeWindow=false). Benchmark endpoints are active and must not be used in production."); assertEquals(records[0].properties.relaxations, [{ protection: "private_address_checks", effect: "disabled", effectiveValue: true }, { protection: "http_signature_time_window", effect: "disabled", effectiveValue: false, secureDefaultSeconds: 3600 }]); } finally { await reset(); } }); }); 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("benchmarkMode stats endpoint", async (t) => { await t.step("is absent when benchmarkMode is off", async () => { assertEquals((await createFederation({ kv: new MemoryKvStore() }).fetch(new Request("https://example.com/.well-known/fedify/bench/stats"), { contextData: void 0 })).status, 404); }); await t.step("returns a v1 in-process metrics snapshot", async () => { const federation = createFederation({ kv: new MemoryKvStore(), benchmarkMode: true, queue: { enqueue() { return Promise.resolve(); }, listen() { return Promise.resolve(); }, getDepth() { return Promise.resolve({ queued: 3, ready: 2, delayed: 1 }); } } }); recordInboxActivity(federation.meterProvider, "processed", vocab.Create.typeId.href); const response = await federation.fetch(new Request("https://example.com/.well-known/fedify/bench/stats"), { contextData: void 0 }); assertEquals(response.status, 200); assertEquals(response.headers.get("Content-Type"), "application/json"); const body = await response.json(); assertEquals(body.version, 1); assertEquals(body.source, "server"); assertEquals(Number.isNaN(Date.parse(body.generatedAt)), false); const metrics = body.scopeMetrics.flatMap((scope) => scope.metrics); assertExists(metrics.find((metric) => metric.name === "activitypub.inbox.activity")); const queueDepth = metrics.find((metric) => metric.name === "fedify.queue.depth"); assertExists(queueDepth); assertEquals(queueDepth.dataPointType, "gauge"); assertEquals(queueDepth.dataPoints.map((point) => ({ state: point.attributes["fedify.queue.depth.state"], role: point.attributes["fedify.queue.role"], value: point.value })).sort((a, b) => String(a.state).localeCompare(String(b.state))), [ { state: "delayed", role: "shared", value: 1 }, { state: "queued", role: "shared", value: 3 }, { state: "ready", role: "shared", value: 2 } ]); }); }); test("createFederation() registers queue depth for regular metrics", async () => { const reader = new TestMetricReader(); const meterProvider = new MeterProvider$1({ readers: [reader] }); try { createFederation({ kv: new MemoryKvStore(), meterProvider, queue: { enqueue() { return Promise.resolve(); }, listen() { return Promise.resolve(); }, getDepth() { return Promise.resolve({ queued: 5, ready: 4, delayed: 3 }); } } }); const queueDepth = (await reader.collect()).resourceMetrics.scopeMetrics.flatMap((scope) => scope.metrics).find((metric) => metric.descriptor.name === "fedify.queue.depth"); assertExists(queueDepth); assertEquals(queueDepth.dataPointType, DataPointType.GAUGE); assertEquals(queueDepth.dataPoints.map((point) => ({ state: point.attributes["fedify.queue.depth.state"], role: point.attributes["fedify.queue.role"], value: point.value })).sort((a, b) => String(a.state).localeCompare(String(b.state))), [ { state: "delayed", role: "shared", value: 3 }, { state: "queued", role: "shared", value: 5 }, { state: "ready", role: "shared", value: 4 } ]); } finally { await meterProvider.shutdown(); } }); test("createFederation() registers queue depth after global meterProvider is set", async () => { metrics.disable(); const federation = createFederation({ kv: new MemoryKvStore(), queue: { enqueue() { return Promise.resolve(); }, listen() { return Promise.resolve(); }, getDepth() { return Promise.resolve({ queued: 8 }); } } }); const reader = new TestMetricReader(); const meterProvider = new MeterProvider$1({ readers: [reader] }); try { metrics.setGlobalMeterProvider(meterProvider); federation.meterProvider; const queueDepth = (await reader.collect()).resourceMetrics.scopeMetrics.flatMap((scope) => scope.metrics).find((metric) => metric.descriptor.name === "fedify.queue.depth"); assertExists(queueDepth); assertEquals(queueDepth.dataPointType, DataPointType.GAUGE); assertEquals(queueDepth.dataPoints.map((point) => ({ state: point.attributes["fedify.queue.depth.state"], role: point.attributes["fedify.queue.role"], value: point.value })), [{ state: "queued", role: "shared", value: 8 }]); } finally { metrics.disable(); await meterProvider.shutdown(); } }); test("createFederation() distinguishes queue depth series per federation", async () => { const reader = new TestMetricReader(); const meterProvider = new MeterProvider$1({ readers: [reader] }); try { const createQueue = (queued) => ({ enqueue() { return Promise.resolve(); }, listen() { return Promise.resolve(); }, getDepth() { return Promise.resolve({ queued }); } }); createFederation({ kv: new MemoryKvStore(), meterProvider, queue: createQueue(1) }); createFederation({ kv: new MemoryKvStore(), meterProvider, queue: createQueue(2) }); const queueDepth = (await reader.collect()).resourceMetrics.scopeMetrics.flatMap((scope) => scope.metrics).find((metric) => metric.descriptor.name === "fedify.queue.depth"); assertExists(queueDepth); const queuedPoints = queueDepth.dataPoints.filter((point) => point.attributes["fedify.queue.depth.state"] === "queued"); assertEquals(queuedPoints.map((point) => point.value).sort(), [1, 2]); const instanceIds = queuedPoints.map((point) => point.attributes["fedify.federation.instance_id"]); assertEquals(instanceIds.every((id) => typeof id === "string"), true); assertEquals(new Set(instanceIds).size, 2); } finally { await meterProvider.shutdown(); } }); test("benchmarkMode trigger endpoint", async (t) => { const createTriggerTarget = (options = {}) => { const messages = []; const federation = createFederation({ kv: new MemoryKvStore(), benchmarkMode: { triggerSinks: ["https://sink.example/inbox"], allowUnsafeTriggerRecipients: options.allowUnsafeTriggerRecipients }, contextLoaderFactory: () => mockDocumentLoader, queue: { outbox: { enqueue(message) { messages.push(message); return Promise.resolve(); }, listen() { return Promise.resolve(); } } } }); federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => new vocab.Person({ id: ctx.getActorUri(identifier), inbox: ctx.getInboxUri(identifier) })).setKeyPairsDispatcher(() => [{ privateKey: rsaPrivateKey2, publicKey: rsaPublicKey2.publicKey }]); return { federation, messages }; }; const createTriggerBody = async (options = {}) => ({ sender: { identifier: "alice" }, sinks: options.sinks, recipients: options.recipients ?? [{ "@context": "https://www.w3.org/ns/activitystreams", type: "Service", id: "https://sink.example/actors/bob", inbox: options.recipientInbox ?? "https://sink.example/inbox" }], activity: await new vocab.Create({ id: new URL("https://example.com/activities/bench-1"), actor: new URL("https://example.com/users/alice"), object: new vocab.Note({ id: new URL("https://example.com/notes/bench-1"), attribution: new URL("https://example.com/users/alice"), content: "benchmark" }) }).toJsonLd({ contextLoader: mockDocumentLoader }), allowUnsafeRecipients: options.allowUnsafeRecipients }); await t.step("is absent when benchmarkMode is off", async () => { assertEquals((await createFederation({ kv: new MemoryKvStore() }).fetch(new Request("https://example.com/.well-known/fedify/bench/trigger", { method: "POST" }), { contextData: void 0 })).status, 404); }); await t.step("rejects unreadable JSON request bodies", async () => { const response = await handleBenchmarkTrigger({ method: "POST", json() { throw new TypeError("body is unavailable"); } }, {}); assertEquals(response.status, 400); assertEquals(await response.json(), { error: "Invalid JSON request body." }); }); await t.step("rejects empty recipient lists", async () => { const { federation, messages } = createTriggerTarget(); const response = await federation.fetch(new Request("https://example.com/.well-known/fedify/bench/trigger", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(await createTriggerBody({ recipients: [] })) }), { contextData: void 0 }); assertEquals(response.status, 400); assertEquals(await response.json(), { error: "No valid recipient inboxes found. The recipients list must not be empty." }); assertEquals(messages, []); }); await t.step("rejects recipients outside configured trigger sinks", async () => { const { federation, messages } = createTriggerTarget(); assertEquals((await federation.fetch(new Request("https://example.com/.well-known/fedify/bench/trigger", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(await createTriggerBody({ recipientInbox: "https://not-a-sink.example/inbox" })) }), { contextData: void 0 })).status, 403); assertEquals(messages, []); }); await t.step("does not trust request-provided trigger sinks or bypasses", async () => { const { federation, messages } = createTriggerTarget(); assertEquals((await federation.fetch(new Request("https://example.com/.well-known/fedify/bench/trigger", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(await createTriggerBody({ recipientInbox: "https://not-a-sink.example/inbox", sinks: ["https://not-a-sink.example/inbox"], allowUnsafeRecipients: true })) }), { contextData: void 0 })).status, 403); assertEquals(messages, []); }); await t.step("allows unsafe recipients only with a server override", async () => { const { federation, messages } = createTriggerTarget({ allowUnsafeTriggerRecipients: true }); assertEquals((await federation.fetch(new Request("https://example.com/.well-known/fedify/bench/trigger", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(await createTriggerBody({ recipientInbox: "https://not-a-sink.example/inbox" })) }), { contextData: void 0 })).status, 202); assertEquals(messages.length, 1); assertEquals(messages[0].inbox, "https://not-a-sink.example/inbox"); }); await t.step("sends the activity to explicit sink recipients", async () => { const { federation, messages } = createTriggerTarget(); const response = await federation.fetch(new Request("https://example.com/.well-known/fedify/bench/trigger", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(await createTriggerBody()) }), { contextData: void 0 }); assertEquals(response.status, 202); const body = await response.json(); assertEquals(body.version, 1); assertEquals(body.activityId, "https://example.com/activities/bench-1"); assertEquals(body.queueCorrelationId, "https://example.com/activities/bench-1"); assertEquals(body.recipientCount, 1); assertEquals(body.inboxCount, 1); assertEquals(messages.length, 1); assertEquals(messages[0].type, "outbox"); assertEquals(messages[0].activityId, body.queueCorrelationId); assertEquals(messages[0].inbox, "https://sink.example/inbox"); }); }); 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")); assertThrows(() => createFederation({ kv: new MemoryKvStore() }).setActorDispatcher("/users/{identifier}", () => null).mapActorAlias("/actor/{id}", "instance"), RouterError, "Path for actor alias must have no variables."); federation.setActorDispatcher("/users/{identifier}", () => new vocab.Person({})).mapActorAlias("/bot", "bot").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.getActorUri("bot"), new URL("https://example.com/bot")); 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(new URL("https://example.com/bot")), { type: "actor", identifier: "bot" }); 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: Tempor