This release is 17 versions behind 1.4.6 — the latest version of @fedify/fedify. Jump to latest
Built and signed on GitHub ActionsBuilt and signed on GitHub Actions
Built and signed on GitHub Actions
An ActivityPub/fediverse server framework
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293import { assert, assertEquals, assertFalse } from "jsr:@std/assert@^0.226.0"; import { signRequest } from "../sig/http.ts"; import { createInboxContext, createRequestContext, } from "../testing/context.ts"; import { mockDocumentLoader } from "../testing/docloader.ts"; import { rsaPrivateKey3, rsaPublicKey2, rsaPublicKey3, } from "../testing/keys.ts"; import { test } from "../testing/mod.ts"; import { type Activity, Create, Note, type Object, Person, } from "../vocab/vocab.ts"; import type { ActorDispatcher, CollectionCounter, CollectionCursor, CollectionDispatcher, ObjectDispatcher, } from "./callback.ts"; import type { RequestContext } from "./context.ts"; import { acceptsJsonLd, handleActor, handleCollection, handleInbox, handleObject, respondWithObject, respondWithObjectIfAcceptable, } from "./handler.ts"; import { MemoryKvStore } from "./kv.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 () => { let context = createRequestContext<void>({ data: undefined, url: new URL("https://example.com/"), getActorUri(identifier) { return new URL(`https://example.com/users/${identifier}`); }, }); const actorDispatcher: ActorDispatcher<void> = (ctx, handle) => { if (handle !== "someone") return null; return new Person({ id: ctx.getActorUri(handle), name: "Someone", }); }; let onNotFoundCalled: Request | null = null; const onNotFound = (request: Request) => { onNotFoundCalled = request; return new Response("Not found", { status: 404 }); }; let onNotAcceptableCalled: Request | null = null; const onNotAcceptable = (request: Request) => { onNotAcceptableCalled = request; return new Response("Not acceptable", { status: 406 }); }; let onUnauthorizedCalled: Request | null = null; const onUnauthorized = (request: 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<void>({ ...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<void>({ ...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", }, ], 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<void>({ ...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", }, ], id: "https://example.com/users/someone", type: "Person", name: "Someone", }); assertEquals(onNotFoundCalled, null); assertEquals(onNotAcceptableCalled, null); assertEquals(onUnauthorizedCalled, null); }); test("handleObject()", async () => { let context = createRequestContext<void>({ data: undefined, url: new URL("https://example.com/"), getObjectUri(_cls, values) { return new URL( `https://example.com/users/${values.handle}/notes/${values.id}`, ); }, }); const objectDispatcher: ObjectDispatcher<void, Object, string> = ( ctx, values, ) => { if (values.handle !== "someone" || values.id !== "123") return null; return new Note({ id: ctx.getObjectUri(Note, values), summary: "Hello, world!", }); }; let onNotFoundCalled: Request | null = null; const onNotFound = (request: Request) => { onNotFoundCalled = request; return new Response("Not found", { status: 404 }); }; let onNotAcceptableCalled: Request | null = null; const onNotAcceptable = (request: Request) => { onNotAcceptableCalled = request; return new Response("Not acceptable", { status: 406 }); }; let onUnauthorizedCalled: Request | null = null; const onUnauthorized = (request: 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<void>({ ...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", }, ], 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<void>({ ...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", }, ], 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 () => { let context = createRequestContext<void>({ data: undefined, url: new URL("https://example.com/"), getActorUri(identifier) { return new URL(`https://example.com/users/${identifier}`); }, }); const dispatcher: CollectionDispatcher< Activity, RequestContext<void>, void, void > = ( _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: CollectionCounter<void, void> = (_ctx, handle) => handle === "someone" ? 3 : null; const firstCursor: CollectionCursor<RequestContext<void>, void, void> = ( _ctx, handle, ) => handle === "someone" ? "0" : null; const lastCursor: CollectionCursor<RequestContext<void>, void, void> = ( _ctx, handle, ) => handle === "someone" ? "2" : null; let onNotFoundCalled: Request | null = null; const onNotFound = (request: Request) => { onNotFoundCalled = request; return new Response("Not found", { status: 404 }); }; let onNotAcceptableCalled: Request | null = null; const onNotAcceptable = (request: Request) => { onNotAcceptableCalled = request; return new Response("Not acceptable", { status: 406 }); }; let onUnauthorizedCalled: Request | null = null; const onUnauthorized = (request: 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<void>({ ...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: "toot:votersCount", _misskey_quote: "misskey:_misskey_quote", quoteUri: "fedibird:quoteUri", quoteUrl: "as:quoteUrl", }, ]; 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#", }, ], 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<void>({ ...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", { Emoji: "toot:Emoji", Hashtag: "as:Hashtag", sensitive: "as:sensitive", toot: "http://joinmastodon.org/ns#", }, ], 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", { Emoji: "toot:Emoji", Hashtag: "as:Hashtag", sensitive: "as:sensitive", toot: "http://joinmastodon.org/ns#", }, ], 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", { Emoji: "toot:Emoji", Hashtag: "as:Hashtag", sensitive: "as:sensitive", toot: "http://joinmastodon.org/ns#", }, ], 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", { Emoji: "toot:Emoji", Hashtag: "as:Hashtag", sensitive: "as:sensitive", toot: "http://joinmastodon.org/ns#", }, ], 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 unsignedContext = createRequestContext({ request: unsignedRequest, url: new URL(unsignedRequest.url), data: undefined, }); let onNotFoundCalled: Request | null = null; const onNotFound = (request: Request) => { onNotFoundCalled = request; return new Response("Not found", { status: 404 }); }; const actorDispatcher: ActorDispatcher<void> = (_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, } as const; let response = await handleInbox(unsignedRequest, { recipient: null, context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); }, ...inboxOptions, actorDispatcher: undefined, }); assertEquals(onNotFoundCalled, unsignedRequest); assertEquals(response.status, 404); onNotFoundCalled = null; response = await handleInbox(unsignedRequest, { recipient: "nobody", context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext({ ...unsignedContext, 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); }, ...inboxOptions, }); assertEquals(onNotFoundCalled, null); assertEquals(response.status, 401); response = await handleInbox(unsignedRequest, { recipient: "someone", context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext({ ...unsignedContext, recipient: "someone" }); }, ...inboxOptions, }); assertEquals(onNotFoundCalled, null); assertEquals(response.status, 401); onNotFoundCalled = null; const signedRequest = await signRequest( unsignedRequest.clone(), rsaPrivateKey3, rsaPublicKey3.id!, ); const signedContext = createRequestContext({ request: signedRequest, url: new URL(signedRequest.url), data: undefined, documentLoader: mockDocumentLoader, }); response = await handleInbox(signedRequest, { recipient: null, context: signedContext, inboxContextFactory(_activity) { return createInboxContext(unsignedContext); }, ...inboxOptions, }); assertEquals(onNotFoundCalled, null); assertEquals([response.status, await response.text()], [202, ""]); response = await handleInbox(signedRequest, { recipient: "someone", context: signedContext, inboxContextFactory(_activity) { return createInboxContext({ ...unsignedContext, 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); }, ...inboxOptions, skipSignatureVerification: true, }); assertEquals(onNotFoundCalled, null); assertEquals(response.status, 202); response = await handleInbox(unsignedRequest, { recipient: "someone", context: unsignedContext, inboxContextFactory(_activity) { return createInboxContext({ ...unsignedContext, recipient: "someone" }); }, ...inboxOptions, skipSignatureVerification: true, }); assertEquals(onNotFoundCalled, null); assertEquals(response.status, 202); }); 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", }, ], 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", }, ], 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); });