@fedify/fedify
Version:
An ActivityPub server framework
1,200 lines • 268 kB
JavaScript
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