UNPKG

@fedify/fedify

Version:

An ActivityPub server framework

1,247 lines (1,243 loc) • 36.9 kB
import { Temporal } from "@js-temporal/polyfill"; import { URLPattern } from "urlpattern-polyfill"; globalThis.addEventListener = () => {}; import { assertEquals } from "../assert_equals-CTYbeopb.js"; import { assert } from "../assert-DmFG7ppO.js"; import "../assert_instance_of-CF09JHYM.js"; import { MemoryKvStore } from "../kv-BMY6Qf_A.js"; import { acceptsJsonLd, createFederation, handleActor, handleCollection, handleInbox, handleObject, respondWithObject, respondWithObjectIfAcceptable } from "../middleware-DlU2_Djj.js"; import "../docloader-C8mmLN-Y.js"; import "../url-kTAI6_KP.js"; import "../semver-DWClQt_5.js"; import "../client-rv6n0nKR.js"; import "../router-D_aVZZUc.js"; import "../types-C7C_l-jz.js"; import "../multibase-DeCHcK8L.js"; import { Create, Note, Person } from "../vocab-dDpPQ0fF.js"; import "../langstr-DbWheeIS.js"; import "../lookup-Ozuw-rxN.js"; import "../type-D2s5lmbZ.js"; import "../actor-BP-NTJ2m.js"; import "../key-CPSlDkCj.js"; import { signRequest } from "../http-BmfI6U39.js"; import "../authdocloader-Banw__l6.js"; import "../ld-D9kk1Uy2.js"; import "../owner-DF6Fg4-s.js"; import "../proof-13QPysOU.js"; import "../lookup-DNXR408t.js"; import "../inbox-AwdJVoMZ.js"; import "../builder-EdzpUb9N.js"; import "../collection-Dfb0TPno.js"; import "../keycache-BPwY_Up0.js"; import "../retry-BiIhZWgD.js"; import "../send-CaL5Xv1F.js"; import { test } from "../testing-BZ0dJ4qn.js"; import "../std__assert-vp0TKMS1.js"; import { assertFalse } from "../assert_rejects-C-sxEMM5.js"; import "../assert_is_error-nrwA1GeT.js"; import "../assert_not_equals-Dc7y-V5Q.js"; import "../assert_throws-Cn9C6Jur.js"; import { mockDocumentLoader } from "../docloader-09nVWLAZ.js"; import { createInboxContext, createRequestContext } from "../context-Buzj1YKD.js"; import { rsaPrivateKey3, rsaPublicKey2, rsaPublicKey3 } from "../keys-CtFc865v.js"; //#region federation/handler.test.ts test("acceptsJsonLd()", () => { assert(acceptsJsonLd(new Request("https://example.com/", { headers: { Accept: "application/activity+json" } }))); assert(acceptsJsonLd(new Request("https://example.com/", { headers: { Accept: "application/ld+json" } }))); assert(acceptsJsonLd(new Request("https://example.com/", { headers: { Accept: "application/json" } }))); assertFalse(acceptsJsonLd(new Request("https://example.com/", { headers: { Accept: "application/ld+json; q=0.5, text/html; q=0.8" } }))); assertFalse(acceptsJsonLd(new Request("https://example.com/", { headers: { Accept: "application/ld+json; q=0.4, application/xhtml+xml; q=0.9" } }))); }); test("handleActor()", async () => { const federation = createFederation({ kv: new MemoryKvStore() }); 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, handle) => { if (handle !== "someone") return null; return new Person({ id: ctx.getActorUri(handle), name: "Someone" }); }; let onNotFoundCalled = null; const onNotFound = (request) => { onNotFoundCalled = request; return new Response("Not found", { status: 404 }); }; let onNotAcceptableCalled = null; const onNotAcceptable = (request) => { onNotAcceptableCalled = request; return new Response("Not acceptable", { status: 406 }); }; let onUnauthorizedCalled = null; const onUnauthorized = (request) => { onUnauthorizedCalled = request; return new Response("Unauthorized", { status: 401 }); }; let response = await handleActor(context.request, { context, identifier: "someone", onNotFound, onNotAcceptable, onUnauthorized }); assertEquals(response.status, 404); assertEquals(onNotFoundCalled, context.request); assertEquals(onNotAcceptableCalled, null); assertEquals(onUnauthorizedCalled, null); onNotFoundCalled = null; context = createRequestContext({ ...context, getActor(handle) { return Promise.resolve(actorDispatcher(context, handle)); } }); response = await handleActor(context.request, { context, identifier: "someone", actorDispatcher, onNotFound, onNotAcceptable, onUnauthorized }); assertEquals(response.status, 406); assertEquals(onNotFoundCalled, null); assertEquals(onNotAcceptableCalled, context.request); assertEquals(onUnauthorizedCalled, null); onNotAcceptableCalled = null; response = await handleActor(context.request, { context, identifier: "no-one", actorDispatcher, onNotFound, onNotAcceptable, onUnauthorized }); assertEquals(response.status, 404); assertEquals(onNotFoundCalled, context.request); assertEquals(onNotAcceptableCalled, null); 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, onNotAcceptable, 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", { 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(onNotAcceptableCalled, null); assertEquals(onUnauthorizedCalled, null); response = await handleActor(context.request, { context, identifier: "no-one", actorDispatcher, onNotFound, onNotAcceptable, onUnauthorized }); assertEquals(response.status, 404); assertEquals(onNotFoundCalled, context.request); assertEquals(onNotAcceptableCalled, null); assertEquals(onUnauthorizedCalled, null); onNotFoundCalled = null; response = await handleActor(context.request, { context, identifier: "someone", actorDispatcher, authorizePredicate: (_ctx, _handle, signedKey, signedKeyOwner) => signedKey != null && signedKeyOwner != null, onNotFound, onNotAcceptable, onUnauthorized }); assertEquals(response.status, 401); assertEquals(onNotFoundCalled, null); assertEquals(onNotAcceptableCalled, 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: (_ctx, _handle, signedKey, signedKeyOwner) => signedKey != null && signedKeyOwner != null, onNotFound, onNotAcceptable, 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", { 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(onNotAcceptableCalled, null); assertEquals(onUnauthorizedCalled, null); }); test("handleObject()", async () => { const federation = createFederation({ kv: new MemoryKvStore() }); let context = createRequestContext({ federation, data: void 0, url: new URL("https://example.com/"), getObjectUri(_cls, values) { return new URL(`https://example.com/users/${values.handle}/notes/${values.id}`); } }); const objectDispatcher = (ctx, values) => { if (values.handle !== "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 onNotAcceptableCalled = null; const onNotAcceptable = (request) => { onNotAcceptableCalled = request; return new Response("Not acceptable", { status: 406 }); }; let onUnauthorizedCalled = null; const onUnauthorized = (request) => { onUnauthorizedCalled = request; return new Response("Unauthorized", { status: 401 }); }; let response = await handleObject(context.request, { context, values: { handle: "someone", id: "123" }, onNotFound, onNotAcceptable, onUnauthorized }); assertEquals(response.status, 404); assertEquals(onNotFoundCalled, context.request); assertEquals(onNotAcceptableCalled, null); assertEquals(onUnauthorizedCalled, null); onNotFoundCalled = null; response = await handleObject(context.request, { context, values: { handle: "someone", id: "123" }, objectDispatcher, onNotFound, onNotAcceptable, onUnauthorized }); assertEquals(response.status, 406); assertEquals(onNotFoundCalled, null); assertEquals(onNotAcceptableCalled, context.request); assertEquals(onUnauthorizedCalled, null); onNotAcceptableCalled = null; response = await handleObject(context.request, { context, values: { handle: "no-one", id: "123" }, objectDispatcher, onNotFound, onNotAcceptable, onUnauthorized }); assertEquals(response.status, 404); assertEquals(onNotFoundCalled, context.request); assertEquals(onNotAcceptableCalled, null); assertEquals(onUnauthorizedCalled, null); onNotFoundCalled = null; response = await handleObject(context.request, { context, values: { handle: "someone", id: "not-exist" }, objectDispatcher, onNotFound, onNotAcceptable, onUnauthorized }); assertEquals(response.status, 404); assertEquals(onNotFoundCalled, context.request); assertEquals(onNotAcceptableCalled, null); 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: { handle: "someone", id: "123" }, objectDispatcher, onNotFound, onNotAcceptable, 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", { 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#", 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(onNotAcceptableCalled, null); assertEquals(onUnauthorizedCalled, null); response = await handleObject(context.request, { context, values: { handle: "no-one", id: "123" }, objectDispatcher, onNotFound, onNotAcceptable, onUnauthorized }); assertEquals(response.status, 404); assertEquals(onNotFoundCalled, context.request); assertEquals(onNotAcceptableCalled, null); assertEquals(onUnauthorizedCalled, null); onNotFoundCalled = null; response = await handleObject(context.request, { context, values: { handle: "someone", id: "not-exist" }, objectDispatcher, onNotFound, onNotAcceptable, onUnauthorized }); assertEquals(response.status, 404); assertEquals(onNotFoundCalled, context.request); assertEquals(onNotAcceptableCalled, null); assertEquals(onUnauthorizedCalled, null); onNotFoundCalled = null; response = await handleObject(context.request, { context, values: { handle: "someone", id: "123" }, objectDispatcher, authorizePredicate: (_ctx, _values, signedKey, signedKeyOwner) => signedKey != null && signedKeyOwner != null, onNotFound, onNotAcceptable, onUnauthorized }); assertEquals(response.status, 401); assertEquals(onNotFoundCalled, null); assertEquals(onNotAcceptableCalled, 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: { handle: "someone", id: "123" }, objectDispatcher, authorizePredicate: (_ctx, _values, signedKey, signedKeyOwner) => signedKey != null && signedKeyOwner != null, onNotFound, onNotAcceptable, 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", { 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#", 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(onNotAcceptableCalled, null); assertEquals(onUnauthorizedCalled, null); }); test("handleCollection()", async () => { const federation = createFederation({ kv: new MemoryKvStore() }); let context = createRequestContext({ federation, data: void 0, url: new URL("https://example.com/"), getActorUri(identifier) { return new URL(`https://example.com/users/${identifier}`); } }); const dispatcher = (_ctx, handle, cursor) => { if (handle !== "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, handle) => handle === "someone" ? 3 : null; const firstCursor = (_ctx, handle) => handle === "someone" ? "0" : null; const lastCursor = (_ctx, handle) => handle === "someone" ? "2" : null; let onNotFoundCalled = null; const onNotFound = (request) => { onNotFoundCalled = request; return new Response("Not found", { status: 404 }); }; let onNotAcceptableCalled = null; const onNotAcceptable = (request) => { onNotAcceptableCalled = request; return new Response("Not acceptable", { status: 406 }); }; 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, onNotAcceptable, onUnauthorized }); assertEquals(response.status, 404); assertEquals(onNotFoundCalled, context.request); assertEquals(onNotAcceptableCalled, null); 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, onNotAcceptable, onUnauthorized }); assertEquals(response.status, 406); assertEquals(onNotFoundCalled, null); assertEquals(onNotAcceptableCalled, context.request); assertEquals(onUnauthorizedCalled, null); onNotAcceptableCalled = 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, onNotAcceptable, onUnauthorized }); assertEquals(response.status, 404); assertEquals(onNotFoundCalled, context.request); assertEquals(onNotAcceptableCalled, null); 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, onNotAcceptable, onUnauthorized }); assertEquals(response.status, 404); assertEquals(onNotFoundCalled, context.request); assertEquals(onNotAcceptableCalled, null); 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, onNotAcceptable, 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", { 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", 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", { 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", 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(onNotAcceptableCalled, 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: (_ctx, _handle, key, keyOwner) => key != null && keyOwner != null }, onNotFound, onNotAcceptable, onUnauthorized }); assertEquals(response.status, 401); assertEquals(onNotFoundCalled, null); assertEquals(onNotAcceptableCalled, 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: (_ctx, _handle, key, keyOwner) => key != null && keyOwner != null }, onNotFound, onNotAcceptable, 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", { 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", 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(onNotAcceptableCalled, 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, onNotAcceptable, 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", { 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", 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(onNotAcceptableCalled, 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, onNotAcceptable, 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", { 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", 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(onNotAcceptableCalled, 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, onNotAcceptable, 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", { 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", 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(onNotAcceptableCalled, 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 inboxOptions = { kv: new MemoryKvStore(), kvPrefixes: { activityIdempotence: ["_fedify", "activityIdempotence"], publicKey: ["_fedify", "publicKey"] }, 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); 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, ""]); response = await handleInbox(signedRequest, { recipient: "someone", context: signedContext, inboxContextFactory(_activity) { return createInboxContext({ ...unsignedContext, clone: void 0, recipient: "someone" }); }, ...inboxOptions }); assertEquals(onNotFoundCalled, null); assertEquals([response.status, await response.text()], [202, ""]); response = await handleInbox(unsignedRequest, { recipient: null, context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext({ ...unsignedContext, clone: void 0 }); }, ...inboxOptions, skipSignatureVerification: true }); assertEquals(onNotFoundCalled, null); assertEquals(response.status, 202); response = await handleInbox(unsignedRequest, { recipient: "someone", context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext({ ...unsignedContext, clone: void 0, recipient: "someone" }); }, ...inboxOptions, skipSignatureVerification: true }); assertEquals(onNotFoundCalled, null); assertEquals(response.status, 202); const invalidRequest = new Request("https://example.com/", { method: "POST", body: JSON.stringify({ "@context": [ "https://www.w3.org/ns/activitystreams", true, 23 ], type: "Create", object: { type: "Note", content: "Hello, world!" }, actor: "https://example.com/users/alice" }) }); const signedInvalidRequest = await signRequest(invalidRequest, rsaPrivateKey3, rsaPublicKey3.id); const signedInvalidContext = createRequestContext({ federation, request: signedInvalidRequest, url: new URL(signedInvalidRequest.url), data: void 0, documentLoader: mockDocumentLoader }); response = await handleInbox(signedInvalidRequest, { recipient: null, context: signedContext, inboxContextFactory(_activity) { return createInboxContext({ ...signedInvalidContext, clone: void 0 }); }, ...inboxOptions }); assertEquals(onNotFoundCalled, null); assertEquals(response.status, 400); }); test("respondWithObject()", async () => { const response = await respondWithObject(new Note({ id: new URL("https://example.com/notes/1"), content: "Hello, world!" }), { contextLoader: mockDocumentLoader }); assert(response.ok); 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", { 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#", quoteUri: "fedibird:quoteUri", quoteUrl: "as:quoteUrl", emojiReactions: { "@id": "fedibird:emojiReactions", "@type": "@id" } } ], id: "https://example.com/notes/1", type: "Note", content: "Hello, world!" }); }); test("respondWithObjectIfAcceptable", async () => { let request = new Request("https://example.com/", { headers: { Accept: "application/activity+json" } }); let response = await respondWithObjectIfAcceptable(new Note({ id: new URL("https://example.com/notes/1"), content: "Hello, world!" }), request, { contextLoader: mockDocumentLoader }); assert(response != null); assert(response.ok); 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", { 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#", quoteUri: "fedibird:quoteUri", quoteUrl: "as:quoteUrl", emojiReactions: { "@id": "fedibird:emojiReactions", "@type": "@id" } } ], id: "https://example.com/notes/1", type: "Note", content: "Hello, world!" }); request = new Request("https://example.com/", { headers: { Accept: "text/html" } }); response = await respondWithObjectIfAcceptable(new Note({ id: new URL("https://example.com/notes/1"), content: "Hello, world!" }), request, { contextLoader: mockDocumentLoader }); assertEquals(response, null); }); //#endregion