UNPKG

@fedify/fedify

Version:

An ActivityPub server framework

1,613 lines • 132 kB
import { Temporal } from "@js-temporal/polyfill"; import "urlpattern-polyfill"; globalThis.addEventListener = () => {}; import { n as createOutboxContext, r as createRequestContext, t as createInboxContext } from "../context-Dk_tacqz.mjs"; import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs"; import "../std__assert-CRDpx_HF.mjs"; import { t as assertRejects } from "../assert_rejects-B-qJtC9Z.mjs"; import { t as assertInstanceOf } from "../assert_instance_of-C4Ri6VuN.mjs"; import { t as assert } from "../assert-DikXweDx.mjs"; import { r as parseAcceptSignature } from "../accept-CPkZzmGN.mjs"; import { s as signRequest } from "../http-C_edJspG.mjs"; import { a as rsaPrivateKey3, c as rsaPublicKey3, s as rsaPublicKey2 } from "../keys-DGu1NFwu.mjs"; import { a as compactJsonLd, p as signJsonLd } from "../ld-tusP_XxG.mjs"; import { t as MemoryKvStore } from "../kv-rV3vodCc.mjs"; import { c as handleActor, d as handleInbox, f as handleObject, h as respondWithObjectIfAcceptable, l as handleCollection, m as respondWithObject, o as createFederation, p as handleOutbox, u as handleCustomCollection } from "../middleware-D9k0Knum.mjs"; import { t as ActivityListenerSet } from "../activity-listener-ell7W1s9.mjs"; import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture"; import { Activity, Create, Note, Person, Tombstone } from "@fedify/vocab"; import { FetchError } from "@fedify/vocab-runtime"; //#region src/federation/handler.test.ts const QUOTE_CONTEXT_TERMS = { QuoteAuthorization: "https://w3id.org/fep/044f#QuoteAuthorization", quote: { "@id": "https://w3id.org/fep/044f#quote", "@type": "@id" }, quoteAuthorization: { "@id": "https://w3id.org/fep/044f#quoteAuthorization", "@type": "@id" } }; const WRAPPER_QUOTE_CONTEXT_TERMS = { ...QUOTE_CONTEXT_TERMS, QuoteRequest: "https://w3id.org/fep/044f#QuoteRequest" }; test("handleActor()", async () => { const federation = createFederation({ kv: new MemoryKvStore() }); const deletedAt = Temporal.Instant.from("2024-01-15T00:00:00Z"); let context = createRequestContext({ federation, data: void 0, url: new URL("https://example.com/"), getActorUri(identifier) { return new URL(`https://example.com/users/${identifier}`); } }); const actorDispatcher = (ctx, identifier) => { if (identifier !== "someone") return null; return new Person({ id: ctx.getActorUri(identifier), name: "Someone" }); }; const tombstoneDispatcher = (ctx, identifier) => { if (identifier !== "gone") return null; return new Tombstone({ id: ctx.getActorUri(identifier), formerType: Person, deleted: deletedAt }); }; let onNotFoundCalled = null; const onNotFound = (request) => { onNotFoundCalled = request; return new Response("Not found", { status: 404 }); }; let onUnauthorizedCalled = null; const onUnauthorized = (request) => { onUnauthorizedCalled = request; return new Response("Unauthorized", { status: 401 }); }; let response = await handleActor(context.request, { context, identifier: "someone", onNotFound, onUnauthorized }); assertEquals(response.status, 404); assertEquals(onNotFoundCalled, context.request); assertEquals(onUnauthorizedCalled, null); onNotFoundCalled = null; response = await handleActor(context.request, { context, identifier: "no-one", actorDispatcher, onNotFound, onUnauthorized }); assertEquals(response.status, 404); assertEquals(onNotFoundCalled, context.request); assertEquals(onUnauthorizedCalled, null); onNotFoundCalled = null; context = createRequestContext({ ...context, request: new Request(context.url, { headers: { Accept: "application/activity+json" } }) }); response = await handleActor(context.request, { context, identifier: "someone", actorDispatcher, onNotFound, onUnauthorized }); assertEquals(response.status, 200); assertEquals(response.headers.get("Content-Type"), "application/activity+json"); assertEquals(await response.json(), { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", "https://w3id.org/security/data-integrity/v1", "https://www.w3.org/ns/did/v1", "https://w3id.org/security/multikey/v1", "https://gotosocial.org/ns", { alsoKnownAs: { "@id": "as:alsoKnownAs", "@type": "@id" }, manuallyApprovesFollowers: "as:manuallyApprovesFollowers", movedTo: { "@id": "as:movedTo", "@type": "@id" }, featured: { "@id": "toot:featured", "@type": "@id" }, featuredTags: { "@id": "toot:featuredTags", "@type": "@id" }, discoverable: "toot:discoverable", indexable: "toot:indexable", memorial: "toot:memorial", suspended: "toot:suspended", toot: "http://joinmastodon.org/ns#", schema: "http://schema.org#", PropertyValue: "schema:PropertyValue", value: "schema:value", misskey: "https://misskey-hub.net/ns#", _misskey_followedMessage: "misskey:_misskey_followedMessage", isCat: "misskey:isCat", Emoji: "toot:Emoji" } ], id: "https://example.com/users/someone", type: "Person", name: "Someone" }); assertEquals(onNotFoundCalled, null); assertEquals(onUnauthorizedCalled, null); response = await handleActor(context.request, { context, identifier: "no-one", actorDispatcher, onNotFound, onUnauthorized }); assertEquals(response.status, 404); assertEquals(onNotFoundCalled, context.request); assertEquals(onUnauthorizedCalled, null); onNotFoundCalled = null; response = await handleActor(context.request, { context, identifier: "someone", actorDispatcher, authorizePredicate: async (ctx, _handle) => await ctx.getSignedKey() != null && await ctx.getSignedKeyOwner() != null, onNotFound, onUnauthorized }); assertEquals(response.status, 401); assertEquals(onNotFoundCalled, null); assertEquals(onUnauthorizedCalled, context.request); onUnauthorizedCalled = null; context = createRequestContext({ ...context, getSignedKey: () => Promise.resolve(rsaPublicKey2), getSignedKeyOwner: () => Promise.resolve(new Person({})) }); response = await handleActor(context.request, { context, identifier: "someone", actorDispatcher, authorizePredicate: async (ctx, _handle) => await ctx.getSignedKey() != null && await ctx.getSignedKeyOwner() != null, onNotFound, onUnauthorized }); assertEquals(response.status, 200); assertEquals(response.headers.get("Content-Type"), "application/activity+json"); assertEquals(await response.json(), { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", "https://w3id.org/security/data-integrity/v1", "https://www.w3.org/ns/did/v1", "https://w3id.org/security/multikey/v1", "https://gotosocial.org/ns", { alsoKnownAs: { "@id": "as:alsoKnownAs", "@type": "@id" }, manuallyApprovesFollowers: "as:manuallyApprovesFollowers", movedTo: { "@id": "as:movedTo", "@type": "@id" }, featured: { "@id": "toot:featured", "@type": "@id" }, featuredTags: { "@id": "toot:featuredTags", "@type": "@id" }, discoverable: "toot:discoverable", indexable: "toot:indexable", memorial: "toot:memorial", suspended: "toot:suspended", toot: "http://joinmastodon.org/ns#", schema: "http://schema.org#", PropertyValue: "schema:PropertyValue", value: "schema:value", misskey: "https://misskey-hub.net/ns#", _misskey_followedMessage: "misskey:_misskey_followedMessage", isCat: "misskey:isCat", Emoji: "toot:Emoji" } ], id: "https://example.com/users/someone", type: "Person", name: "Someone" }); assertEquals(onNotFoundCalled, null); assertEquals(onUnauthorizedCalled, null); onNotFoundCalled = null; response = await handleActor(context.request, { context, identifier: "gone", actorDispatcher: tombstoneDispatcher, authorizePredicate: () => false, onNotFound, onUnauthorized }); assertEquals(response.status, 401); assertEquals(onNotFoundCalled, null); assertEquals(onUnauthorizedCalled, context.request); onUnauthorizedCalled = null; response = await handleActor(context.request, { context, identifier: "gone", actorDispatcher: tombstoneDispatcher, authorizePredicate: () => true, onNotFound, onUnauthorized }); assertEquals(response.status, 410); assertEquals(response.headers.get("Content-Type"), "application/activity+json"); assertEquals(response.headers.get("Vary"), "Accept"); assertEquals(await response.json(), { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1", "https://gotosocial.org/ns" ], id: "https://example.com/users/gone", type: "Tombstone", formerType: "as:Person", deleted: "2024-01-15T00:00:00Z" }); assertEquals(onNotFoundCalled, null); assertEquals(onUnauthorizedCalled, null); }); test("handleObject()", async () => { let context = createRequestContext({ federation: createFederation({ kv: new MemoryKvStore() }), data: void 0, url: new URL("https://example.com/"), getObjectUri(_cls, values) { return new URL(`https://example.com/users/${values.identifier}/notes/${values.id}`); } }); const objectDispatcher = (ctx, values) => { if (values.identifier !== "someone" || values.id !== "123") return null; return new Note({ id: ctx.getObjectUri(Note, values), summary: "Hello, world!" }); }; let onNotFoundCalled = null; const onNotFound = (request) => { onNotFoundCalled = request; return new Response("Not found", { status: 404 }); }; let onUnauthorizedCalled = null; const onUnauthorized = (request) => { onUnauthorizedCalled = request; return new Response("Unauthorized", { status: 401 }); }; let response = await handleObject(context.request, { context, values: { identifier: "someone", id: "123" }, onNotFound, onUnauthorized }); assertEquals(response.status, 404); assertEquals(onNotFoundCalled, context.request); assertEquals(onUnauthorizedCalled, null); onNotFoundCalled = null; response = await handleObject(context.request, { context, values: { identifier: "someone", id: "123" }, objectDispatcher, onNotFound, onUnauthorized }); assertEquals(response.status, 200); assertEquals(onNotFoundCalled, null); assertEquals(onUnauthorizedCalled, null); response = await handleObject(context.request, { context, values: { identifier: "no-one", id: "123" }, objectDispatcher, onNotFound, onUnauthorized }); assertEquals(response.status, 404); assertEquals(onNotFoundCalled, context.request); assertEquals(onUnauthorizedCalled, null); onNotFoundCalled = null; response = await handleObject(context.request, { context, values: { identifier: "someone", id: "not-exist" }, objectDispatcher, onNotFound, onUnauthorized }); assertEquals(response.status, 404); assertEquals(onNotFoundCalled, context.request); assertEquals(onUnauthorizedCalled, null); onNotFoundCalled = null; context = createRequestContext({ ...context, request: new Request(context.url, { headers: { Accept: "application/activity+json" } }) }); response = await handleObject(context.request, { context, values: { identifier: "someone", id: "123" }, objectDispatcher, onNotFound, onUnauthorized }); assertEquals(response.status, 200); assertEquals(response.headers.get("Content-Type"), "application/activity+json"); assertEquals(await response.json(), { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1", "https://gotosocial.org/ns", { Emoji: "toot:Emoji", Hashtag: "as:Hashtag", sensitive: "as:sensitive", toot: "http://joinmastodon.org/ns#", _misskey_quote: "misskey:_misskey_quote", fedibird: "http://fedibird.com/ns#", misskey: "https://misskey-hub.net/ns#", ...QUOTE_CONTEXT_TERMS, quoteUri: "fedibird:quoteUri", quoteUrl: "as:quoteUrl", emojiReactions: { "@id": "fedibird:emojiReactions", "@type": "@id" } } ], id: "https://example.com/users/someone/notes/123", summary: "Hello, world!", type: "Note" }); assertEquals(onNotFoundCalled, null); assertEquals(onUnauthorizedCalled, null); response = await handleObject(context.request, { context, values: { identifier: "no-one", id: "123" }, objectDispatcher, onNotFound, onUnauthorized }); assertEquals(response.status, 404); assertEquals(onNotFoundCalled, context.request); assertEquals(onUnauthorizedCalled, null); onNotFoundCalled = null; response = await handleObject(context.request, { context, values: { identifier: "someone", id: "not-exist" }, objectDispatcher, onNotFound, onUnauthorized }); assertEquals(response.status, 404); assertEquals(onNotFoundCalled, context.request); assertEquals(onUnauthorizedCalled, null); onNotFoundCalled = null; response = await handleObject(context.request, { context, values: { identifier: "someone", id: "123" }, objectDispatcher, authorizePredicate: async (ctx, _values) => await ctx.getSignedKey() != null && await ctx.getSignedKeyOwner() != null, onNotFound, onUnauthorized }); assertEquals(response.status, 401); assertEquals(onNotFoundCalled, null); assertEquals(onUnauthorizedCalled, context.request); onUnauthorizedCalled = null; context = createRequestContext({ ...context, getSignedKey: () => Promise.resolve(rsaPublicKey2), getSignedKeyOwner: () => Promise.resolve(new Person({})) }); response = await handleObject(context.request, { context, values: { identifier: "someone", id: "123" }, objectDispatcher, authorizePredicate: async (ctx, _values) => await ctx.getSignedKey() != null && await ctx.getSignedKeyOwner() != null, onNotFound, onUnauthorized }); assertEquals(response.status, 200); assertEquals(response.headers.get("Content-Type"), "application/activity+json"); assertEquals(await response.json(), { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1", "https://gotosocial.org/ns", { Emoji: "toot:Emoji", Hashtag: "as:Hashtag", sensitive: "as:sensitive", toot: "http://joinmastodon.org/ns#", _misskey_quote: "misskey:_misskey_quote", fedibird: "http://fedibird.com/ns#", misskey: "https://misskey-hub.net/ns#", ...QUOTE_CONTEXT_TERMS, quoteUri: "fedibird:quoteUri", quoteUrl: "as:quoteUrl", emojiReactions: { "@id": "fedibird:emojiReactions", "@type": "@id" } } ], id: "https://example.com/users/someone/notes/123", summary: "Hello, world!", type: "Note" }); assertEquals(onNotFoundCalled, null); assertEquals(onUnauthorizedCalled, null); }); test("handleCollection()", async () => { let context = createRequestContext({ federation: createFederation({ kv: new MemoryKvStore() }), data: void 0, url: new URL("https://example.com/"), getActorUri(identifier) { return new URL(`https://example.com/users/${identifier}`); } }); const dispatcher = (_ctx, identifier, cursor) => { if (identifier !== "someone") return null; const items = [ new Create({ id: new URL("https://example.com/activities/1") }), new Create({ id: new URL("https://example.com/activities/2") }), new Create({ id: new URL("https://example.com/activities/3") }) ]; if (cursor != null) { const idx = parseInt(cursor); return { items: [items[idx]], nextCursor: idx < items.length - 1 ? (idx + 1).toString() : null, prevCursor: idx > 0 ? (idx - 1).toString() : null }; } return { items }; }; const counter = (_ctx, identifier) => identifier === "someone" ? 3 : null; const firstCursor = (_ctx, identifier) => identifier === "someone" ? "0" : null; const lastCursor = (_ctx, identifier) => identifier === "someone" ? "2" : null; let onNotFoundCalled = null; const onNotFound = (request) => { onNotFoundCalled = request; return new Response("Not found", { status: 404 }); }; let onUnauthorizedCalled = null; const onUnauthorized = (request) => { onUnauthorizedCalled = request; return new Response("Unauthorized", { status: 401 }); }; let response = await handleCollection(context.request, { context, name: "collection", identifier: "someone", uriGetter(identifier) { return new URL(`https://example.com/users/${identifier}`); }, onNotFound, onUnauthorized }); assertEquals(response.status, 404); assertEquals(onNotFoundCalled, context.request); assertEquals(onUnauthorizedCalled, null); onNotFoundCalled = null; response = await handleCollection(context.request, { context, name: "collection", identifier: "someone", uriGetter(identifier) { return new URL(`https://example.com/users/${identifier}`); }, collectionCallbacks: { dispatcher }, onNotFound, onUnauthorized }); assertEquals(response.status, 200); assertEquals(onNotFoundCalled, null); assertEquals(onUnauthorizedCalled, null); response = await handleCollection(context.request, { context, name: "collection", identifier: "no-one", uriGetter(identifier) { return new URL(`https://example.com/users/${identifier}`); }, collectionCallbacks: { dispatcher }, onNotFound, onUnauthorized }); assertEquals(response.status, 404); assertEquals(onNotFoundCalled, context.request); assertEquals(onUnauthorizedCalled, null); onNotFoundCalled = null; context = createRequestContext({ ...context, request: new Request(context.url, { headers: { Accept: "application/activity+json" } }) }); response = await handleCollection(context.request, { context, name: "collection", identifier: "no-one", uriGetter(identifier) { return new URL(`https://example.com/users/${identifier}`); }, collectionCallbacks: { dispatcher }, onNotFound, onUnauthorized }); assertEquals(response.status, 404); assertEquals(onNotFoundCalled, context.request); assertEquals(onUnauthorizedCalled, null); onNotFoundCalled = null; response = await handleCollection(context.request, { context, name: "collection", identifier: "someone", uriGetter(identifier) { return new URL(`https://example.com/users/${identifier}`); }, collectionCallbacks: { dispatcher }, onNotFound, onUnauthorized }); assertEquals(response.status, 200); assertEquals(response.headers.get("Content-Type"), "application/activity+json"); const createCtx = [ "https://w3id.org/identity/v1", "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1", "https://gotosocial.org/ns", { toot: "http://joinmastodon.org/ns#", misskey: "https://misskey-hub.net/ns#", fedibird: "http://fedibird.com/ns#", ChatMessage: "http://litepub.social/ns#ChatMessage", Emoji: "toot:Emoji", Hashtag: "as:Hashtag", sensitive: "as:sensitive", votersCount: { "@id": "toot:votersCount", "@type": "http://www.w3.org/2001/XMLSchema#nonNegativeInteger" }, _misskey_quote: "misskey:_misskey_quote", ...WRAPPER_QUOTE_CONTEXT_TERMS, quoteUri: "fedibird:quoteUri", quoteUrl: "as:quoteUrl", emojiReactions: { "@id": "fedibird:emojiReactions", "@type": "@id" } } ]; assertEquals(await response.json(), { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1", "https://gotosocial.org/ns", { toot: "http://joinmastodon.org/ns#", misskey: "https://misskey-hub.net/ns#", fedibird: "http://fedibird.com/ns#", ChatMessage: "http://litepub.social/ns#ChatMessage", Emoji: "toot:Emoji", Hashtag: "as:Hashtag", sensitive: "as:sensitive", votersCount: "toot:votersCount", _misskey_quote: "misskey:_misskey_quote", ...WRAPPER_QUOTE_CONTEXT_TERMS, quoteUri: "fedibird:quoteUri", quoteUrl: "as:quoteUrl", emojiReactions: { "@id": "fedibird:emojiReactions", "@type": "@id" } } ], id: "https://example.com/users/someone", type: "OrderedCollection", orderedItems: [ { "@context": createCtx, type: "Create", id: "https://example.com/activities/1" }, { "@context": createCtx, type: "Create", id: "https://example.com/activities/2" }, { "@context": createCtx, type: "Create", id: "https://example.com/activities/3" } ] }); assertEquals(onNotFoundCalled, null); assertEquals(onUnauthorizedCalled, null); response = await handleCollection(context.request, { context, name: "collection", identifier: "someone", uriGetter(identifier) { return new URL(`https://example.com/users/${identifier}`); }, collectionCallbacks: { dispatcher, authorizePredicate: async (ctx, _handle) => await ctx.getSignedKey() != null && await ctx.getSignedKeyOwner() != null }, onNotFound, onUnauthorized }); assertEquals(response.status, 401); assertEquals(onNotFoundCalled, null); assertEquals(onUnauthorizedCalled, context.request); onUnauthorizedCalled = null; context = createRequestContext({ ...context, getSignedKey: () => Promise.resolve(rsaPublicKey2), getSignedKeyOwner: () => Promise.resolve(new Person({})) }); response = await handleCollection(context.request, { context, name: "collection", identifier: "someone", uriGetter(identifier) { return new URL(`https://example.com/users/${identifier}`); }, collectionCallbacks: { dispatcher, authorizePredicate: async (ctx, _handle) => await ctx.getSignedKey() != null && await ctx.getSignedKeyOwner() != null }, onNotFound, onUnauthorized }); assertEquals(response.status, 200); assertEquals(response.headers.get("Content-Type"), "application/activity+json"); assertEquals(await response.json(), { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1", "https://gotosocial.org/ns", { toot: "http://joinmastodon.org/ns#", misskey: "https://misskey-hub.net/ns#", fedibird: "http://fedibird.com/ns#", ChatMessage: "http://litepub.social/ns#ChatMessage", Emoji: "toot:Emoji", Hashtag: "as:Hashtag", sensitive: "as:sensitive", votersCount: "toot:votersCount", _misskey_quote: "misskey:_misskey_quote", ...WRAPPER_QUOTE_CONTEXT_TERMS, quoteUri: "fedibird:quoteUri", quoteUrl: "as:quoteUrl", emojiReactions: { "@id": "fedibird:emojiReactions", "@type": "@id" } } ], id: "https://example.com/users/someone", type: "OrderedCollection", orderedItems: [ { "@context": createCtx, type: "Create", id: "https://example.com/activities/1" }, { "@context": createCtx, type: "Create", id: "https://example.com/activities/2" }, { "@context": createCtx, type: "Create", id: "https://example.com/activities/3" } ] }); assertEquals(onNotFoundCalled, null); assertEquals(onUnauthorizedCalled, null); response = await handleCollection(context.request, { context, name: "collection", identifier: "someone", uriGetter(identifier) { return new URL(`https://example.com/users/${identifier}`); }, collectionCallbacks: { dispatcher, counter, firstCursor, lastCursor }, onNotFound, onUnauthorized }); assertEquals(response.status, 200); assertEquals(response.headers.get("Content-Type"), "application/activity+json"); assertEquals(await response.json(), { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1", "https://gotosocial.org/ns", { toot: "http://joinmastodon.org/ns#", misskey: "https://misskey-hub.net/ns#", fedibird: "http://fedibird.com/ns#", ChatMessage: "http://litepub.social/ns#ChatMessage", Emoji: "toot:Emoji", Hashtag: "as:Hashtag", sensitive: "as:sensitive", votersCount: "toot:votersCount", _misskey_quote: "misskey:_misskey_quote", ...WRAPPER_QUOTE_CONTEXT_TERMS, quoteUri: "fedibird:quoteUri", quoteUrl: "as:quoteUrl", emojiReactions: { "@id": "fedibird:emojiReactions", "@type": "@id" } } ], id: "https://example.com/users/someone", type: "OrderedCollection", totalItems: 3, first: "https://example.com/?cursor=0", last: "https://example.com/?cursor=2" }); assertEquals(onNotFoundCalled, null); assertEquals(onUnauthorizedCalled, null); let url = new URL("https://example.com/?cursor=0"); context = createRequestContext({ ...context, url, request: new Request(url, { headers: { Accept: "application/activity+json" } }) }); response = await handleCollection(context.request, { context, name: "collection", identifier: "someone", uriGetter(identifier) { return new URL(`https://example.com/users/${identifier}`); }, collectionCallbacks: { dispatcher, counter, firstCursor, lastCursor }, onNotFound, onUnauthorized }); assertEquals(response.status, 200); assertEquals(response.headers.get("Content-Type"), "application/activity+json"); assertEquals(await response.json(), { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1", "https://gotosocial.org/ns", { toot: "http://joinmastodon.org/ns#", misskey: "https://misskey-hub.net/ns#", fedibird: "http://fedibird.com/ns#", ChatMessage: "http://litepub.social/ns#ChatMessage", Emoji: "toot:Emoji", Hashtag: "as:Hashtag", sensitive: "as:sensitive", votersCount: "toot:votersCount", _misskey_quote: "misskey:_misskey_quote", ...WRAPPER_QUOTE_CONTEXT_TERMS, quoteUri: "fedibird:quoteUri", quoteUrl: "as:quoteUrl", emojiReactions: { "@id": "fedibird:emojiReactions", "@type": "@id" } } ], id: "https://example.com/users/someone?cursor=0", type: "OrderedCollectionPage", partOf: "https://example.com/", next: "https://example.com/?cursor=1", orderedItems: [{ "@context": createCtx, id: "https://example.com/activities/1", type: "Create" }] }); assertEquals(onNotFoundCalled, null); assertEquals(onUnauthorizedCalled, null); url = new URL("https://example.com/?cursor=2"); context = createRequestContext({ ...context, url, request: new Request(url, { headers: { Accept: "application/activity+json" } }) }); response = await handleCollection(context.request, { context, name: "collection", identifier: "someone", uriGetter(identifier) { return new URL(`https://example.com/users/${identifier}`); }, collectionCallbacks: { dispatcher, counter, firstCursor, lastCursor }, onNotFound, onUnauthorized }); assertEquals(response.status, 200); assertEquals(response.headers.get("Content-Type"), "application/activity+json"); assertEquals(await response.json(), { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1", "https://gotosocial.org/ns", { toot: "http://joinmastodon.org/ns#", misskey: "https://misskey-hub.net/ns#", fedibird: "http://fedibird.com/ns#", ChatMessage: "http://litepub.social/ns#ChatMessage", Emoji: "toot:Emoji", Hashtag: "as:Hashtag", sensitive: "as:sensitive", votersCount: "toot:votersCount", _misskey_quote: "misskey:_misskey_quote", ...WRAPPER_QUOTE_CONTEXT_TERMS, quoteUri: "fedibird:quoteUri", quoteUrl: "as:quoteUrl", emojiReactions: { "@id": "fedibird:emojiReactions", "@type": "@id" } } ], id: "https://example.com/users/someone?cursor=2", type: "OrderedCollectionPage", partOf: "https://example.com/", prev: "https://example.com/?cursor=1", orderedItems: [{ "@context": createCtx, id: "https://example.com/activities/3", type: "Create" }] }); assertEquals(onNotFoundCalled, null); assertEquals(onUnauthorizedCalled, null); }); test("handleInbox()", async () => { const activity = new Create({ id: new URL("https://example.com/activities/1"), actor: new URL("https://example.com/person2"), object: new Note({ id: new URL("https://example.com/notes/1"), attribution: new URL("https://example.com/person2"), content: "Hello, world!" }) }); const unsignedRequest = new Request("https://example.com/", { method: "POST", body: JSON.stringify(await activity.toJsonLd()) }); const federation = createFederation({ kv: new MemoryKvStore() }); const unsignedContext = createRequestContext({ federation, request: unsignedRequest, url: new URL(unsignedRequest.url), data: void 0 }); let onNotFoundCalled = null; const onNotFound = (request) => { onNotFoundCalled = request; return new Response("Not found", { status: 404 }); }; const actorDispatcher = (_ctx, identifier) => { if (identifier !== "someone") return null; return new Person({ name: "Someone" }); }; const restrictiveContextLoader = async (resource) => { const url = new URL(resource).href; if (url === "https://www.w3.org/ns/activitystreams" || url === "https://w3id.org/identity/v1") return await mockDocumentLoader(url); throw new Error(`Unexpected context: ${url}`); }; const inboxOptions = { kv: new MemoryKvStore(), kvPrefixes: { activityIdempotence: ["_fedify", "activityIdempotence"], publicKey: ["_fedify", "publicKey"], acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"] }, actorDispatcher, onNotFound, signatureTimeWindow: { minutes: 5 }, skipSignatureVerification: false }; let response = await handleInbox(unsignedRequest, { recipient: null, context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext({ ...unsignedContext, clone: void 0 }); }, ...inboxOptions, actorDispatcher: void 0 }); assertEquals(onNotFoundCalled, unsignedRequest); assertEquals(response.status, 404); onNotFoundCalled = null; response = await handleInbox(unsignedRequest, { recipient: "nobody", context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext({ ...unsignedContext, clone: void 0, recipient: "nobody" }); }, ...inboxOptions }); assertEquals(onNotFoundCalled, unsignedRequest); assertEquals(response.status, 404); onNotFoundCalled = null; response = await handleInbox(unsignedRequest, { recipient: null, context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext({ ...unsignedContext, clone: void 0 }); }, ...inboxOptions }); assertEquals(onNotFoundCalled, null); assertEquals(response.status, 401); response = await handleInbox(unsignedRequest, { recipient: "someone", context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext({ ...unsignedContext, clone: void 0, recipient: "someone" }); }, ...inboxOptions }); assertEquals(onNotFoundCalled, null); assertEquals(response.status, 401); const malformedProofCreatedRequest = new Request("https://example.com/", { method: "POST", body: JSON.stringify({ "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"], id: "https://example.com/activities/invalid-proof-created", type: "Create", actor: "https://example.com/person2", object: { id: "https://example.com/notes/invalid-proof-created", type: "Note", attributedTo: "https://example.com/person2", content: "Hello, world!" }, proof: { type: "DataIntegrityProof", cryptosuite: "eddsa-jcs-2022", verificationMethod: "https://example.com/person2#main-key", proofPurpose: "assertionMethod", created: { "@value": "not-a-date" }, proofValue: "zLaewdp4H9kqtwyrLatK4cjY5oRHwVcw4gibPSUDYDMhi4M49v8pcYk3ZB6D69dNpAPbUmY8ocuJ3m9KhKJEEg7z" } }) }); const malformedProofCreatedContext = createRequestContext({ federation, request: malformedProofCreatedRequest, url: new URL(malformedProofCreatedRequest.url), data: void 0, documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader }); response = await handleInbox(malformedProofCreatedRequest, { recipient: null, context: malformedProofCreatedContext, inboxContextFactory(_activity) { return createInboxContext({ ...malformedProofCreatedContext, clone: void 0 }); }, ...inboxOptions }); assertEquals([response.status, await response.text()], [400, "Invalid activity."]); onNotFoundCalled = null; const signedRequest = await signRequest(unsignedRequest.clone(), rsaPrivateKey3, rsaPublicKey3.id); const signedContext = createRequestContext({ federation, request: signedRequest, url: new URL(signedRequest.url), data: void 0, documentLoader: mockDocumentLoader }); response = await handleInbox(signedRequest, { recipient: null, context: signedContext, inboxContextFactory(_activity) { return createInboxContext({ ...unsignedContext, clone: void 0 }); }, ...inboxOptions }); assertEquals(onNotFoundCalled, null); assertEquals([response.status, await response.text()], [202, ""]); const ldSignedRequest = new Request("https://example.com/", { method: "POST", body: JSON.stringify(await signJsonLd({ "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/identity/v1", "https://w3id.org/security/v1", "https://w3id.org/security/data-integrity/v1" ], id: "https://example.com/activities/ld-signed", type: "Create", actor: "https://example.com/person2", object: { id: "https://example.com/notes/ld-signed", type: "Note", attributedTo: "https://example.com/person2", content: "Hello, world!" } }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader })) }); const ldSignedContext = createRequestContext({ federation, request: ldSignedRequest, url: new URL(ldSignedRequest.url), data: void 0, documentLoader: mockDocumentLoader, contextLoader: restrictiveContextLoader }); response = await handleInbox(ldSignedRequest, { recipient: null, context: ldSignedContext, inboxContextFactory(_activity) { return createInboxContext({ ...ldSignedContext, clone: void 0 }); }, ...inboxOptions }); assertEquals(onNotFoundCalled, null); assertEquals([response.status, await response.text()], [202, ""]); const remoteContextUrl = "https://remote.example/contexts/ext"; let failRemoteContextOnce = true; const flakyContextLoader = async (resource) => { const url = new URL(resource).href; if (url === remoteContextUrl) { if (failRemoteContextOnce) { failRemoteContextOnce = false; throw new Error(`Unexpected context: ${url}`); } return { contextUrl: null, documentUrl: url, document: { "@context": { ext: "https://example.com/ext" } } }; } return await mockDocumentLoader(url); }; const httpSignedLdBody = { "@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"], id: "https://example.com/activities/http-signed-ld", type: "Create", actor: "https://example.com/person2", ext: "preserve-me", object: { id: "https://example.com/notes/http-signed-ld", type: "Note", attributedTo: "https://example.com/person2", content: "Hello, world!" }, signature: { type: "RsaSignature2017", creator: rsaPublicKey3.id.href, created: "2024-01-01T00:00:00Z", signatureValue: "bogus" } }; const httpSignedLdRequest = await signRequest(new Request("https://example.com/", { method: "POST", body: JSON.stringify(httpSignedLdBody) }), rsaPrivateKey3, rsaPublicKey3.id); const httpSignedLdContext = createRequestContext({ federation, request: httpSignedLdRequest, url: new URL(httpSignedLdRequest.url), data: void 0, documentLoader: mockDocumentLoader, contextLoader: flakyContextLoader }); response = await handleInbox(httpSignedLdRequest, { recipient: null, context: httpSignedLdContext, inboxContextFactory(_activity) { return createInboxContext({ ...httpSignedLdContext, clone: void 0 }); }, ...inboxOptions }); assertEquals(onNotFoundCalled, null); assertEquals([response.status, await response.text()], [202, ""]); const ldSignedOnlyBody = await signJsonLd({ "@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"], id: "https://example.com/activities/ld-only-transient", type: "Create", actor: "https://example.com/person2", ext: "preserve-me", object: { id: "https://example.com/notes/ld-only-transient", type: "Note", attributedTo: "https://example.com/person2", content: "Hello, world!" } }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: async (resource) => { const url = new URL(resource).href; if (url === remoteContextUrl) return { contextUrl: null, documentUrl: url, document: { "@context": { ext: "https://example.com/ext" } } }; return await mockDocumentLoader(url); } }); const malformedTemporalLdSignedBody = await signJsonLd({ "@context": "https://www.w3.org/ns/activitystreams", id: "https://example.com/activities/ld-only-invalid-published", type: "Create", actor: "https://example.com/person2", published: { "@value": "not-a-date" }, object: { id: "https://example.com/notes/ld-only-invalid-published", type: "Note", attributedTo: "https://example.com/person2", content: "Hello, world!" } }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader }); const malformedTemporalLdSignedRequest = new Request("https://example.com/", { method: "POST", body: JSON.stringify(malformedTemporalLdSignedBody) }); const malformedTemporalLdSignedContext = createRequestContext({ federation, request: malformedTemporalLdSignedRequest, url: new URL(malformedTemporalLdSignedRequest.url), data: void 0, documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader }); response = await handleInbox(malformedTemporalLdSignedRequest, { recipient: null, context: malformedTemporalLdSignedContext, inboxContextFactory(_activity) { return createInboxContext({ ...malformedTemporalLdSignedContext, clone: void 0 }); }, ...inboxOptions }); assertEquals([response.status, await response.text()], [400, "Invalid activity."]); const malformedClosedLdSignedBody = await signJsonLd({ "@context": "https://www.w3.org/ns/activitystreams", id: "https://example.com/questions/ld-only-invalid-closed", type: "Question", closed: "2024-02-31T00:00:00Z" }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader }); const malformedClosedLdSignedRequest = new Request("https://example.com/", { method: "POST", body: JSON.stringify(malformedClosedLdSignedBody) }); const malformedClosedLdSignedContext = createRequestContext({ federation, request: malformedClosedLdSignedRequest, url: new URL(malformedClosedLdSignedRequest.url), data: void 0, documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader }); response = await handleInbox(malformedClosedLdSignedRequest, { recipient: null, context: malformedClosedLdSignedContext, inboxContextFactory(_activity) { return createInboxContext({ ...malformedClosedLdSignedContext, clone: void 0 }); }, ...inboxOptions }); assertEquals([response.status, await response.text()], [400, "Invalid activity."]); const malformedIriHttpSignedRequest = await signRequest(new Request("https://example.com/", { method: "POST", body: JSON.stringify({ "@context": "https://www.w3.org/ns/activitystreams", id: "http://[", type: "Create", actor: "https://example.com/person2", object: { id: "https://example.com/notes/http-signed-invalid-iri", type: "Note", attributedTo: "https://example.com/person2", content: "Hello, world!" } }) }), rsaPrivateKey3, rsaPublicKey3.id); const malformedIriHttpSignedContext = createRequestContext({ federation, request: malformedIriHttpSignedRequest, url: new URL(malformedIriHttpSignedRequest.url), data: void 0, documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader }); response = await handleInbox(malformedIriHttpSignedRequest, { recipient: null, context: malformedIriHttpSignedContext, inboxContextFactory(_activity) { return createInboxContext({ ...malformedIriHttpSignedContext, clone: void 0 }); }, ...inboxOptions }); assertEquals([response.status, await response.text()], [400, "Invalid activity."]); const ldSignedOnlyRequest = new Request("https://example.com/", { method: "POST", body: JSON.stringify(ldSignedOnlyBody) }); const ldSignedOnlyContext = createRequestContext({ federation, request: ldSignedOnlyRequest, url: new URL(ldSignedOnlyRequest.url), data: void 0, documentLoader: mockDocumentLoader, contextLoader: async (resource) => { const url = new URL(resource).href; if (url === remoteContextUrl) throw new Error(`Unexpected context: ${url}`); return await mockDocumentLoader(url); } }); await assertRejects(() => handleInbox(ldSignedOnlyRequest, { recipient: null, context: ldSignedOnlyContext, inboxContextFactory(_activity) { return createInboxContext({ ...ldSignedOnlyContext, clone: void 0 }); }, ...inboxOptions }), Error); failRemoteContextOnce = true; const invalidHttpFallbackRequest = new Request("https://example.com/", { method: "POST", body: JSON.stringify(ldSignedOnlyBody), headers: { Signature: "bogus" } }); const invalidHttpFallbackContext = createRequestContext({ federation, request: invalidHttpFallbackRequest, url: new URL(invalidHttpFallbackRequest.url), data: void 0, documentLoader: mockDocumentLoader, contextLoader: flakyContextLoader }); await assertRejects(() => handleInbox(invalidHttpFallbackRequest, { recipient: null, context: invalidHttpFallbackContext, inboxContextFactory(_activity) { return createInboxContext({ ...invalidHttpFallbackContext, clone: void 0 }); }, ...inboxOptions }), Error); const transientKeyContextUrl = "https://remote.example/contexts/key"; const transientCreatorUrl = "https://remote.example/keys/transient#main-key"; const verificationFailureLdSignedBody = await signJsonLd({ "@context": "https://www.w3.org/ns/activitystreams", id: "https://example.com/activities/ld-key-fetch-transient", type: "Create", actor: "https://example.com/person2", object: { id: "https://example.com/notes/ld-key-fetch-transient", type: "Note", attributedTo: "https://example.com/person2", content: "Hello, world!" } }, rsaPrivateKey3, new URL(transientCreatorUrl), { contextLoader: mockDocumentLoader }); const verificationFailureLdSignedRequest = new Request("https://example.com/", { method: "POST", body: JSON.stringify(verificationFailureLdSignedBody), headers: { Signature: "bogus" } }); const verificationFailureLdSignedContext = createRequestContext({ federation, request: verificationFailureLdSignedRequest, url: new URL(verificationFailureLdSignedRequest.url), data: void 0, documentLoader: async (resource) => { if (resource === transientCreatorUrl) return { contextUrl: null, documentUrl: resource, document: { "@context": [transientKeyContextUrl], id: resource } }; return await mockDocumentLoader(new URL(resource).href); }, contextLoader: async (resource) => { if (resource === transientKeyContextUrl) throw new Error(`Transient key context failure: ${resource}`); return await mockDocumentLoader(new URL(resource).href); } }); response = await handleInbox(verificationFailureLdSignedRequest, { recipient: null, context: verificationFailureLdSignedContext, inboxContextFactory(_activity) { return createInboxContext({ ...verificationFailureLdSignedContext, clone: void 0 }); }, ...inboxOptions }); assertEquals([response.status, await response.text()], [401, "Failed to verify the request signature."]); failRemoteContextOnce = true; const deferredMalformedTemporalLdSignedBody = await signJsonLd({ "@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"], id: "https://example.com/activities/deferred-invalid-published", type: "Create", actor: "https://example.com/person2", ext: "preserve-me", published: { "@value": "not-a-date" }, object: { id: "https://example.com/notes/deferred-invalid-published", type: "Note", attributedTo: "https://example.com/person2", content: "Hello, world!" } }, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: async (resource) => { const url = new URL(resource).href; if (url === remoteContextUrl) return { contextUrl: null, documentUrl: url, document: { "@context": { ext: "https://example.com/ext" } } }; return await mockDocumentLoader(url); } }); const deferredMalformedTemporalLdSignedRequest = new Request("https://example.com/", { method: "POST", body: JSON.stringify(deferredMalformedTemporalLdSignedBody), headers: { Signature: "bogus" } }); const deferredMalformedTemporalLdSignedContext = createRequestContext({ federation, request: deferredMalformedTemporalLdSignedRequest, url: new URL(deferredMalformedTemporalLdSignedRequest.url), data: void 0, documentLoader: mockDocumentLoader, contextLoader: flakyContextLoader }); response = await handleInbox(deferredMalformedTemporalLdSignedRequest, { recipient: null, context: deferredMalformedTemporalLdSignedContext, inboxContextFactory(_activity) { return createInboxContext({ ...deferredMalformedTemporalLdSignedContext, clone: void 0 }); }, ...inboxOptions }); assertEquals([response.status, await response.text()], [400, "Invalid activity."]); const malformedLdSignedRequest = new Request("https://example.com/", { method: "POST", body: JSON.stringify({ ...ldSignedOnlyBody, "@context": ["not a url", "https://www.w3.org/ns/activitystreams"] }) }); const malformedLdSignedContext = createRequestContext({ federation, request: malformedLdSignedRequest, url: new URL(malformedLdSignedRequest.url), data: void 0, documentLoader: mockDocumentLoader, contextLoader: mockDocumentLoader }); response = await handleInbox(malformedLdSignedRequest, { recipient: null, context: malformedLdSignedContext, inboxContextFactory(_activity) { return createInboxContext({ ...malformedLdSignedContext, clone: void 0 }); }, ...inboxOptions }); assertEquals([response.status, await response.text()], [400, "Invalid JSON-LD."]); const dualSignedInvalidCreatorRequest = await signRequest(new Request("https://example.com/", { method: "POST", body: JSON.stringify({ ...httpSignedLdBody, signature: { ...httpSignedLdBody.signature, creator: "not a url" } }) }), rsaPrivateKey3, rsaPublicKey3.id); const dualSignedInvalidCreatorContext = createRequestContext({ federation, request: dualSignedInvalidCreatorRequest, url: new URL(dualSignedInvalidCreatorRequest.url), data: void 0, documentLoader: mockDocumentLoader, contextLoader: flakyContextLoader }); response = await handleInbox(dualSignedInvalidCreatorRequest, { recipient: null, context: dualSignedInvalidCreatorContext, inboxContextFactory(_activity) { return createInboxContext({ ...dualSignedInvalidCreatorContext, clone: void 0 }); }, ...inboxOptions }); assertEquals(onNotFoundCalled, null); assertEquals([response.status, await response.text()], [202, ""]); const invalidUrlHttpSignedRequest = await signRequest(new Request("https://example.com/", { method: "POST", body: JSON.stringify({ "@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"], id: "https://example.com/activities/http-signed-invalid-context", type: "Create", actor: "https://example.com/person2", ext: "preserve-me", object: { id: "https://example.com/notes/http-signed-invalid-context", type: "Note", attributedTo: "https://example.com/person2", content: "Hello, world!" } }) }), rsaPrivateKey3, rsaPublicKey3.id); const invalidUrlHttpSignedContext = createRequestContext({ federation, request: invalidUrlHttpSignedRequest, url: new URL(invalidUrlHttpSignedRequest.url), data: void 0, documentLoader: mockDocumentLoader, contextLoader: async (resource) => { const url = new URL(resource).href; if (url === remoteContextUrl) { const error = /* @__PURE__ */ new Error(`Transient remote context failure: ${url}`); error.name = "jsonld.InvalidUrl"; error.details = { code: "loading remote context failed", url }; throw error; } return await mockDocumentLoader(url); } }); await assertRejects(() => handleInbox(invalidUrlHttpSignedRequest, { recipient: null, context: invalidUrlHttpSignedContext, inboxContextFactory(_activity) { return createInboxContext({ ...invalidUrlHttpSignedContext, clone: void 0 }); }, ...inboxOptions }), Error); const opaqueContextIdHttpSignedRequest = await signRequest(new Request("https://example.com/", { method: "POST", body: JSON.stringify({ "@context": ["app-context", "https://www.w3.org/ns/activitystreams"], id: "https://example.com/activities/http-signed-opaque-context", type: "Create", actor: "https://example.com/person2", object: { id: "https://example.com/notes/http-signed-opaque-context", type: "Note", attributedTo: "https://example.com/person2", content: "Hello, world!" } }) }), rsaPrivateKey3, rsaPublicKey3.id); const opaqueContextIdHttpSignedContext = createRequestContext({ federation, request: opaqueContextIdHttpSignedRequest, url: new URL(opaqueContextIdHttpSignedRequest.url), data: void 0, documentLoader: mockDocumentLoader, contextLoader: async (resource) => { if (resource === "app-context") { const error = /* @__PURE__ */ new Error(`Opaque context backend is unavailable: ${resource}`); error.name = "jsonld.InvalidUrl"; error.details = { code: "loading remote context failed", url: resource }; throw error; } return await