@fedify/fedify 2.3.0-dev.994 → 2.3.0-pr.809.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -7
- package/dist/{assert-DikXweDx.mjs → assert-OguE97r2.mjs} +1 -1
- package/dist/{assert_instance_of-C4Ri6VuN.mjs → assert_instance_of-DBC5X09g.mjs} +1 -1
- package/dist/{assert_not_equals--wG9hV7u.mjs → assert_not_equals-DkVK8oqV.mjs} +1 -1
- package/dist/{assert_rejects-B-qJtC9Z.mjs → assert_rejects-DN60FHPX.mjs} +28 -3
- package/dist/{assert_strict_equals-Dmjbg-bA.mjs → assert_strict_equals-XEgZAlrj.mjs} +1 -1
- package/dist/{assert_throws-4NwKEy2q.mjs → assert_throws-BOkhLGYc.mjs} +1 -1
- package/dist/{builder-yAlpiIqP.mjs → builder-DSZ7FZmg.mjs} +96 -42
- package/dist/{chunk-nlSIicah.js → chunk-CRNNMoPX.js} +2 -2
- package/dist/circuit-breaker-CSWsyoef.mjs +337 -0
- package/dist/{client-z-8dc-e1.d.cts → client-CAM_bQXx.d.cts} +1 -0
- package/dist/{client-AtlibPOU.d.ts → client-CSddvgWN.d.ts} +1 -2
- package/dist/compat/mod.d.cts +2 -1
- package/dist/compat/mod.d.ts +2 -3
- package/dist/compat/mod.js +3 -3
- package/dist/compat/outgoing-jsonld.test.mjs +4 -4
- package/dist/compat/public-audience.test.mjs +4 -4
- package/dist/compat/transformers.test.mjs +5 -5
- package/dist/{context-BzH2-ajs.d.ts → context-BBVLF7lx.d.cts} +342 -269
- package/dist/{context-DJGagtNd.d.cts → context-BU6jSQdo.d.ts} +344 -266
- package/dist/{context-Dk_tacqz.mjs → context-DVoTs_wM.mjs} +64 -5
- package/dist/{deno-XBVlKnOX.mjs → deno-DCdM7d93.mjs} +1 -1
- package/dist/{docloader-DPp-X6gk.mjs → docloader-B7mVwA5_.mjs} +4 -4
- package/dist/{esm-DVILvP5e.mjs → esm-vrlUxr60.mjs} +4 -7
- package/dist/federation/builder.test.mjs +163 -11
- package/dist/federation/circuit-breaker.test.d.mts +2 -0
- package/dist/federation/circuit-breaker.test.mjs +446 -0
- package/dist/federation/collection.test.mjs +3 -3
- package/dist/federation/handler.test.mjs +1770 -29
- package/dist/federation/idempotency.test.mjs +5 -5
- package/dist/federation/inbox.test.mjs +4 -4
- package/dist/federation/keycache.test.mjs +5 -5
- package/dist/federation/kv.test.mjs +2 -2
- package/dist/federation/metrics.test.d.mts +2 -0
- package/dist/federation/metrics.test.mjs +634 -0
- package/dist/federation/middleware.test.mjs +4036 -146
- package/dist/federation/mod.cjs +195 -14
- package/dist/federation/mod.d.cts +6 -4
- package/dist/federation/mod.d.ts +6 -6
- package/dist/federation/mod.js +192 -14
- package/dist/federation/mq.test.mjs +176 -15
- package/dist/federation/negotiation.test.mjs +4 -4
- package/dist/federation/retry.test.mjs +3 -3
- package/dist/federation/router.test.mjs +190 -9
- package/dist/federation/send.test.mjs +153 -15
- package/dist/federation/temporal.test.d.mts +2 -0
- package/dist/federation/temporal.test.mjs +71 -0
- package/dist/federation/webfinger.test.mjs +150 -5
- package/dist/{http-CSlLA54v.mjs → http-BufbkLuW.mjs} +146 -47
- package/dist/{http-B4rrzxtg.js → http-Dh18nwJg.js} +994 -64
- package/dist/{http-aQzN9Ayi.d.ts → http-VyDTd4G3.d.cts} +15 -3
- package/dist/{http-BrGqJDXL.cjs → http-gOiudB3g.cjs} +1099 -61
- package/dist/{http-CrGuipxe.d.cts → http-lf8Hsd91.d.ts} +15 -1
- package/dist/{key-Bzx--sHD.mjs → key-DZdKLJXT.mjs} +43 -18
- package/dist/{kv-CbLNp3zQ.d.cts → kv-D6hNiMTK.d.ts} +1 -0
- package/dist/{kv-cache-DI-Sk7-I.cjs → kv-cache-BuK_nlpY.cjs} +20 -3
- package/dist/{kv-cache-U__xU4qR.mjs → kv-cache-C7SQnkGI.mjs} +21 -3
- package/dist/{kv-cache-EMIqoIuB.js → kv-cache-DNDSb6Po.js} +20 -3
- package/dist/{kv-GFYnFoOl.d.ts → kv-gJ8LYbxX.d.cts} +1 -3
- package/dist/ld-0Lsznacf.mjs +626 -0
- package/dist/metrics-BHeWd24f.mjs +815 -0
- package/dist/{middleware-zjZ6AFCW.mjs → middleware-BNANvDh2.mjs} +1 -1
- package/dist/{middleware-nidH7VZH.js → middleware-BgzRkDcm.js} +2260 -556
- package/dist/{middleware-An4DjES4.cjs → middleware-D-iES1nQ.cjs} +2318 -623
- package/dist/{middleware-K1g6bEkV.mjs → middleware-EV6J_eAc.mjs} +1639 -373
- package/dist/{mod-CR8soWa9.d.ts → mod-B0hW12_O.d.cts} +26 -4
- package/dist/mod-C0F6kvgS.d.cts +182 -0
- package/dist/{mod-Cr3f-ACa.d.cts → mod-COIAjwRS.d.ts} +26 -2
- package/dist/{mod-CMEbIaNh.d.cts → mod-DFvNJcNb.d.ts} +56 -4
- package/dist/mod-vPYVoa5n.d.ts +182 -0
- package/dist/{mod-CLgIXe9w.d.ts → mod-yvIXFAEi.d.cts} +56 -6
- package/dist/mod.cjs +9 -6
- package/dist/mod.d.cts +12 -10
- package/dist/mod.d.ts +12 -12
- package/dist/mod.js +11 -11
- package/dist/mq-D-nlpY04.d.ts +208 -0
- package/dist/mq-D8uSFzxe.d.cts +208 -0
- package/dist/{negotiation-SQvQgUqe.mjs → negotiation-DDstyBvc.mjs} +29 -0
- package/dist/nodeinfo/client.test.mjs +4 -4
- package/dist/nodeinfo/handler.test.mjs +4 -4
- package/dist/nodeinfo/mod.d.cts +2 -1
- package/dist/nodeinfo/mod.d.ts +2 -3
- package/dist/nodeinfo/mod.js +3 -3
- package/dist/nodeinfo/types.test.mjs +3 -3
- package/dist/otel/exporter.test.mjs +28 -25
- package/dist/otel/mod.cjs +6 -5
- package/dist/otel/mod.d.cts +5 -3
- package/dist/otel/mod.d.ts +5 -5
- package/dist/otel/mod.js +8 -7
- package/dist/{outgoing-jsonld-CNmZLixq.mjs → outgoing-jsonld-L_DbOaFe.mjs} +2 -2
- package/dist/{owner-C-tqfvxn.mjs → owner-Cl-1cAR8.mjs} +2 -2
- package/dist/{owner-CptqhsOy.d.cts → owner-CnngXDNJ.d.ts} +2 -1
- package/dist/{owner-74ARJ5TL.d.ts → owner-DEvZuyOE.d.cts} +2 -3
- package/dist/{proof-Bbbwf8_x.js → proof-CyXndf6-.js} +378 -15
- package/dist/{proof-BlmRPzrK.mjs → proof-Dnz8jtiQ.mjs} +26 -8
- package/dist/{proof-BW89QMVB.cjs → proof-tz35unOj.cjs} +432 -15
- package/dist/runtime/mod.d.cts +1 -0
- package/dist/runtime/mod.d.ts +1 -2
- package/dist/runtime/mod.js +3 -3
- package/dist/{send-05pJLcb6.mjs → send-DIbBYN5P.mjs} +72 -26
- package/dist/sig/http.test.mjs +356 -33
- package/dist/sig/key.test.mjs +103 -6
- package/dist/sig/ld.test.mjs +696 -7
- package/dist/sig/mod.cjs +2 -2
- package/dist/sig/mod.d.cts +4 -3
- package/dist/sig/mod.d.ts +4 -5
- package/dist/sig/mod.js +4 -4
- package/dist/sig/owner.test.mjs +6 -6
- package/dist/sig/proof.test.mjs +169 -8
- package/dist/{std__assert-CRDpx_HF.mjs → std__assert-BBjXFNOb.mjs} +5 -30
- package/dist/temporal-BEwob1Vg.mjs +95 -0
- package/dist/testing/mod.d.mts +110 -10
- package/dist/testing/mod.mjs +1 -2
- package/dist/{transformers-ve6e2xcg.js → transformers-BGMIq1cs.js} +2 -2
- package/dist/{types-hvL8ElAs.js → types-CAY3OdLq.js} +2 -2
- package/dist/utils/docloader.test.mjs +6 -6
- package/dist/utils/kv-cache.test.mjs +67 -2
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.d.cts +2 -1
- package/dist/utils/mod.d.ts +2 -3
- package/dist/utils/mod.js +3 -3
- package/dist/vocab/cjs.test.mjs +1 -1
- package/dist/vocab/mod.d.cts +1 -0
- package/dist/vocab/mod.d.ts +1 -2
- package/dist/vocab/mod.js +2 -2
- package/package.json +16 -16
- package/dist/ld-DR78beiv.mjs +0 -279
- package/dist/middleware-BFBaR0mF.cjs +0 -4
- package/dist/mod-2d12ffz3.d.ts +0 -64
- package/dist/mod-D35TRn09.d.cts +0 -62
- package/dist/router-CrMLXoOr.mjs +0 -114
- package/dist/{activity-listener-ell7W1s9.mjs → activity-listener-tztVvlNb.mjs} +0 -0
- package/dist/{assert_equals-Ew3jOFa3.mjs → assert_equals-C-ZRDbaf.mjs} +0 -0
- package/dist/{client-D_1QpnWt.mjs → client-ByXmQhYD.mjs} +1 -1
- package/dist/{collection-D-HqUuA2.mjs → collection-Cc3DVAhE.mjs} +0 -0
- package/dist/{keycache-EGATflN-.mjs → keycache-BeU0LCII.mjs} +0 -0
- package/dist/{public-audience-DYFHzm_c.mjs → public-audience-Cvbr2Gzt.mjs} +1 -1
- /package/dist/{retry-bMXBL97A.mjs → retry-CXg_MBI-.mjs} +0 -0
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { Temporal } from "@js-temporal/polyfill";
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
|
-
import { n as createOutboxContext, r as createRequestContext, t as createInboxContext } from "../context-
|
|
5
|
-
import { t as assertEquals } from "../assert_equals-
|
|
6
|
-
import "../std__assert-
|
|
7
|
-
import { t as
|
|
8
|
-
import { t as
|
|
4
|
+
import { n as createOutboxContext, r as createRequestContext, t as createInboxContext } from "../context-DVoTs_wM.mjs";
|
|
5
|
+
import { t as assertEquals } from "../assert_equals-C-ZRDbaf.mjs";
|
|
6
|
+
import "../std__assert-BBjXFNOb.mjs";
|
|
7
|
+
import { n as assertGreaterOrEqual, t as assertRejects } from "../assert_rejects-DN60FHPX.mjs";
|
|
8
|
+
import { t as assertInstanceOf } from "../assert_instance_of-DBC5X09g.mjs";
|
|
9
|
+
import { t as assert } from "../assert-OguE97r2.mjs";
|
|
9
10
|
import { r as parseAcceptSignature } from "../accept-CPkZzmGN.mjs";
|
|
10
|
-
import { s as signRequest } from "../http-
|
|
11
|
+
import { s as signRequest } from "../http-BufbkLuW.mjs";
|
|
11
12
|
import { a as rsaPrivateKey3, c as rsaPublicKey3, s as rsaPublicKey2 } from "../keys-DGu1NFwu.mjs";
|
|
13
|
+
import { a as compactJsonLd, p as signJsonLd } from "../ld-0Lsznacf.mjs";
|
|
12
14
|
import { t as MemoryKvStore } from "../kv-rV3vodCc.mjs";
|
|
13
|
-
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-
|
|
14
|
-
import { t as ActivityListenerSet } from "../activity-listener-
|
|
15
|
-
import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
|
|
15
|
+
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-EV6J_eAc.mjs";
|
|
16
|
+
import { t as ActivityListenerSet } from "../activity-listener-tztVvlNb.mjs";
|
|
16
17
|
import { Activity, Create, Note, Person, Tombstone } from "@fedify/vocab";
|
|
18
|
+
import { createTestMeterProvider, createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
|
|
17
19
|
import { FetchError } from "@fedify/vocab-runtime";
|
|
18
20
|
//#region src/federation/handler.test.ts
|
|
19
21
|
const QUOTE_CONTEXT_TERMS = {
|
|
@@ -926,6 +928,192 @@ test("handleCollection()", async () => {
|
|
|
926
928
|
assertEquals(onNotFoundCalled, null);
|
|
927
929
|
assertEquals(onUnauthorizedCalled, null);
|
|
928
930
|
});
|
|
931
|
+
test("handleCollection() records OpenTelemetry collection metrics", async () => {
|
|
932
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
933
|
+
const context = createRequestContext({
|
|
934
|
+
federation: createFederation({
|
|
935
|
+
kv: new MemoryKvStore(),
|
|
936
|
+
meterProvider
|
|
937
|
+
}),
|
|
938
|
+
data: void 0,
|
|
939
|
+
url: new URL("https://example.com/users/someone/followers"),
|
|
940
|
+
request: new Request("https://example.com/users/someone/followers", { headers: { Accept: "application/activity+json" } }),
|
|
941
|
+
getActorUri(identifier) {
|
|
942
|
+
return new URL(`https://example.com/users/${identifier}`);
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
const dispatcher = (_ctx, identifier) => identifier === "someone" ? { items: [
|
|
946
|
+
new Create({ id: new URL("https://example.com/activities/1") }),
|
|
947
|
+
new Create({ id: new URL("https://example.com/activities/2") }),
|
|
948
|
+
new Create({ id: new URL("https://example.com/activities/3") })
|
|
949
|
+
] } : null;
|
|
950
|
+
const counter = (_ctx, identifier) => identifier === "someone" ? 3 : null;
|
|
951
|
+
assertEquals((await handleCollection(context.request, {
|
|
952
|
+
context,
|
|
953
|
+
name: "followers",
|
|
954
|
+
identifier: "someone",
|
|
955
|
+
uriGetter(identifier) {
|
|
956
|
+
return new URL(`https://example.com/users/${identifier}/followers`);
|
|
957
|
+
},
|
|
958
|
+
collectionCallbacks: {
|
|
959
|
+
dispatcher,
|
|
960
|
+
counter
|
|
961
|
+
},
|
|
962
|
+
meterProvider,
|
|
963
|
+
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
964
|
+
onUnauthorized: () => new Response("Unauthorized", { status: 401 })
|
|
965
|
+
})).status, 200);
|
|
966
|
+
const requests = recorder.getMeasurements("activitypub.collection.request");
|
|
967
|
+
assertEquals(requests.length, 1);
|
|
968
|
+
assertEquals(requests[0].type, "counter");
|
|
969
|
+
assertEquals(requests[0].value, 1);
|
|
970
|
+
assertEquals(requests[0].attributes["activitypub.collection.kind"], "followers");
|
|
971
|
+
assertEquals(requests[0].attributes["activitypub.collection.page"], false);
|
|
972
|
+
assertEquals(requests[0].attributes["fedify.collection.dispatcher"], "built_in");
|
|
973
|
+
assertEquals(requests[0].attributes["activitypub.collection.result"], "served");
|
|
974
|
+
assertEquals(requests[0].attributes["http.response.status_code"], 200);
|
|
975
|
+
assertEquals("activitypub.collection.id" in requests[0].attributes, false);
|
|
976
|
+
const durations = recorder.getMeasurements("activitypub.collection.dispatch.duration");
|
|
977
|
+
assertEquals(durations.length, 1);
|
|
978
|
+
assertEquals(durations[0].type, "histogram");
|
|
979
|
+
assert(durations[0].value >= 0);
|
|
980
|
+
assertEquals(durations[0].attributes["activitypub.collection.result"], "served");
|
|
981
|
+
const items = recorder.getMeasurements("activitypub.collection.page.items");
|
|
982
|
+
assertEquals(items.length, 1);
|
|
983
|
+
assertEquals(items[0].type, "histogram");
|
|
984
|
+
assertEquals(items[0].value, 3);
|
|
985
|
+
assertEquals(items[0].attributes["activitypub.collection.page"], false);
|
|
986
|
+
const totalItems = recorder.getMeasurements("activitypub.collection.total_items");
|
|
987
|
+
assertEquals(totalItems.length, 1);
|
|
988
|
+
assertEquals(totalItems[0].type, "histogram");
|
|
989
|
+
assertEquals(totalItems[0].value, 3);
|
|
990
|
+
});
|
|
991
|
+
test("handleCollection() classifies camelCase collection metric names", async () => {
|
|
992
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
993
|
+
const context = createRequestContext({
|
|
994
|
+
federation: createFederation({
|
|
995
|
+
kv: new MemoryKvStore(),
|
|
996
|
+
meterProvider
|
|
997
|
+
}),
|
|
998
|
+
data: void 0,
|
|
999
|
+
url: new URL("https://example.com/users/someone/collections/featured"),
|
|
1000
|
+
request: new Request("https://example.com/users/someone/collections/featured", { headers: { Accept: "application/activity+json" } })
|
|
1001
|
+
});
|
|
1002
|
+
const dispatcher = () => ({ items: [] });
|
|
1003
|
+
assertEquals((await handleCollection(context.request, {
|
|
1004
|
+
context,
|
|
1005
|
+
name: "featuredTags",
|
|
1006
|
+
identifier: "someone",
|
|
1007
|
+
uriGetter(identifier) {
|
|
1008
|
+
return new URL(`https://example.com/users/${identifier}/collections/featured`);
|
|
1009
|
+
},
|
|
1010
|
+
collectionCallbacks: { dispatcher },
|
|
1011
|
+
meterProvider,
|
|
1012
|
+
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
1013
|
+
onUnauthorized: () => new Response("Unauthorized", { status: 401 })
|
|
1014
|
+
})).status, 200);
|
|
1015
|
+
const requests = recorder.getMeasurements("activitypub.collection.request");
|
|
1016
|
+
assertEquals(requests.length, 1);
|
|
1017
|
+
assertEquals(requests[0].attributes["activitypub.collection.kind"], "featured_tags");
|
|
1018
|
+
});
|
|
1019
|
+
test("handleCollection() records filtered collection item metrics", async () => {
|
|
1020
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1021
|
+
const context = createRequestContext({
|
|
1022
|
+
federation: createFederation({
|
|
1023
|
+
kv: new MemoryKvStore(),
|
|
1024
|
+
meterProvider
|
|
1025
|
+
}),
|
|
1026
|
+
data: void 0,
|
|
1027
|
+
url: new URL("https://example.com/users/someone/followers"),
|
|
1028
|
+
request: new Request("https://example.com/users/someone/followers", { headers: { Accept: "application/activity+json" } })
|
|
1029
|
+
});
|
|
1030
|
+
const dispatcher = () => ({ items: [new Create({ id: new URL("https://example.com/activities/1") }), new Create({ id: new URL("https://example.com/activities/2") })] });
|
|
1031
|
+
assertEquals((await handleCollection(context.request, {
|
|
1032
|
+
context,
|
|
1033
|
+
name: "followers",
|
|
1034
|
+
identifier: "someone",
|
|
1035
|
+
uriGetter(identifier) {
|
|
1036
|
+
return new URL(`https://example.com/users/${identifier}/followers`);
|
|
1037
|
+
},
|
|
1038
|
+
collectionCallbacks: { dispatcher },
|
|
1039
|
+
filterPredicate: (item) => item.id?.href === "https://example.com/activities/1",
|
|
1040
|
+
meterProvider,
|
|
1041
|
+
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
1042
|
+
onUnauthorized: () => new Response("Unauthorized", { status: 401 })
|
|
1043
|
+
})).status, 200);
|
|
1044
|
+
const items = recorder.getMeasurements("activitypub.collection.page.items");
|
|
1045
|
+
assertEquals(items.length, 1);
|
|
1046
|
+
assertEquals(items[0].value, 1);
|
|
1047
|
+
assertEquals(items[0].attributes["activitypub.collection.page"], false);
|
|
1048
|
+
assertEquals(items[0].attributes["activitypub.collection.result"], "served");
|
|
1049
|
+
assertEquals(items[0].attributes["http.response.status_code"], 200);
|
|
1050
|
+
});
|
|
1051
|
+
test("handleCollection() records filtered collection page item metrics", async () => {
|
|
1052
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1053
|
+
const context = createRequestContext({
|
|
1054
|
+
federation: createFederation({
|
|
1055
|
+
kv: new MemoryKvStore(),
|
|
1056
|
+
meterProvider
|
|
1057
|
+
}),
|
|
1058
|
+
data: void 0,
|
|
1059
|
+
url: new URL("https://example.com/users/someone/followers?cursor=2"),
|
|
1060
|
+
request: new Request("https://example.com/users/someone/followers?cursor=2", { headers: { Accept: "application/activity+json" } })
|
|
1061
|
+
});
|
|
1062
|
+
const dispatcher = () => ({ items: [new Create({ id: new URL("https://example.com/activities/1") }), new Create({ id: new URL("https://example.com/activities/2") })] });
|
|
1063
|
+
assertEquals((await handleCollection(context.request, {
|
|
1064
|
+
context,
|
|
1065
|
+
name: "followers",
|
|
1066
|
+
identifier: "someone",
|
|
1067
|
+
uriGetter(identifier) {
|
|
1068
|
+
return new URL(`https://example.com/users/${identifier}/followers`);
|
|
1069
|
+
},
|
|
1070
|
+
collectionCallbacks: { dispatcher },
|
|
1071
|
+
filterPredicate: (item) => item.id?.href === "https://example.com/activities/1",
|
|
1072
|
+
meterProvider,
|
|
1073
|
+
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
1074
|
+
onUnauthorized: () => new Response("Unauthorized", { status: 401 })
|
|
1075
|
+
})).status, 200);
|
|
1076
|
+
const items = recorder.getMeasurements("activitypub.collection.page.items");
|
|
1077
|
+
assertEquals(items.length, 1);
|
|
1078
|
+
assertEquals(items[0].value, 1);
|
|
1079
|
+
assertEquals(items[0].attributes["activitypub.collection.page"], true);
|
|
1080
|
+
assertEquals(items[0].attributes["activitypub.collection.result"], "served");
|
|
1081
|
+
assertEquals(items[0].attributes["http.response.status_code"], 200);
|
|
1082
|
+
});
|
|
1083
|
+
test("handleCollection() records not_found collection metrics", async () => {
|
|
1084
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1085
|
+
const context = createRequestContext({
|
|
1086
|
+
federation: createFederation({
|
|
1087
|
+
kv: new MemoryKvStore(),
|
|
1088
|
+
meterProvider
|
|
1089
|
+
}),
|
|
1090
|
+
data: void 0,
|
|
1091
|
+
url: new URL("https://example.com/users/nobody/outbox"),
|
|
1092
|
+
request: new Request("https://example.com/users/nobody/outbox", { headers: { Accept: "application/activity+json" } })
|
|
1093
|
+
});
|
|
1094
|
+
const dispatcher = () => null;
|
|
1095
|
+
assertEquals((await handleCollection(context.request, {
|
|
1096
|
+
context,
|
|
1097
|
+
name: "outbox",
|
|
1098
|
+
identifier: "nobody",
|
|
1099
|
+
uriGetter(identifier) {
|
|
1100
|
+
return new URL(`https://example.com/users/${identifier}/outbox`);
|
|
1101
|
+
},
|
|
1102
|
+
collectionCallbacks: { dispatcher },
|
|
1103
|
+
meterProvider,
|
|
1104
|
+
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
1105
|
+
onUnauthorized: () => new Response("Unauthorized", { status: 401 })
|
|
1106
|
+
})).status, 404);
|
|
1107
|
+
const requests = recorder.getMeasurements("activitypub.collection.request");
|
|
1108
|
+
assertEquals(requests.length, 1);
|
|
1109
|
+
assertEquals(requests[0].attributes["activitypub.collection.kind"], "outbox");
|
|
1110
|
+
assertEquals(requests[0].attributes["activitypub.collection.result"], "not_found");
|
|
1111
|
+
assertEquals(requests[0].attributes["http.response.status_code"], 404);
|
|
1112
|
+
const durations = recorder.getMeasurements("activitypub.collection.dispatch.duration");
|
|
1113
|
+
assertEquals(durations.length, 1);
|
|
1114
|
+
assertEquals(durations[0].attributes["activitypub.collection.result"], "not_found");
|
|
1115
|
+
assertEquals(recorder.getMeasurements("activitypub.collection.page.items").length, 0);
|
|
1116
|
+
});
|
|
929
1117
|
test("handleInbox()", async () => {
|
|
930
1118
|
const activity = new Create({
|
|
931
1119
|
id: new URL("https://example.com/activities/1"),
|
|
@@ -956,6 +1144,11 @@ test("handleInbox()", async () => {
|
|
|
956
1144
|
if (identifier !== "someone") return null;
|
|
957
1145
|
return new Person({ name: "Someone" });
|
|
958
1146
|
};
|
|
1147
|
+
const restrictiveContextLoader = async (resource) => {
|
|
1148
|
+
const url = new URL(resource).href;
|
|
1149
|
+
if (url === "https://www.w3.org/ns/activitystreams" || url === "https://w3id.org/identity/v1") return await mockDocumentLoader(url);
|
|
1150
|
+
throw new Error(`Unexpected context: ${url}`);
|
|
1151
|
+
};
|
|
959
1152
|
const inboxOptions = {
|
|
960
1153
|
kv: new MemoryKvStore(),
|
|
961
1154
|
kvPrefixes: {
|
|
@@ -1016,37 +1209,1035 @@ test("handleInbox()", async () => {
|
|
|
1016
1209
|
context: unsignedContext,
|
|
1017
1210
|
inboxContextFactory(_activity) {
|
|
1018
1211
|
return createInboxContext({
|
|
1019
|
-
...unsignedContext,
|
|
1020
|
-
clone: void 0,
|
|
1021
|
-
recipient: "someone"
|
|
1212
|
+
...unsignedContext,
|
|
1213
|
+
clone: void 0,
|
|
1214
|
+
recipient: "someone"
|
|
1215
|
+
});
|
|
1216
|
+
},
|
|
1217
|
+
...inboxOptions
|
|
1218
|
+
});
|
|
1219
|
+
assertEquals(onNotFoundCalled, null);
|
|
1220
|
+
assertEquals(response.status, 401);
|
|
1221
|
+
const malformedProofCreatedRequest = new Request("https://example.com/", {
|
|
1222
|
+
method: "POST",
|
|
1223
|
+
body: JSON.stringify({
|
|
1224
|
+
"@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"],
|
|
1225
|
+
id: "https://example.com/activities/invalid-proof-created",
|
|
1226
|
+
type: "Create",
|
|
1227
|
+
actor: "https://example.com/person2",
|
|
1228
|
+
object: {
|
|
1229
|
+
id: "https://example.com/notes/invalid-proof-created",
|
|
1230
|
+
type: "Note",
|
|
1231
|
+
attributedTo: "https://example.com/person2",
|
|
1232
|
+
content: "Hello, world!"
|
|
1233
|
+
},
|
|
1234
|
+
proof: {
|
|
1235
|
+
type: "DataIntegrityProof",
|
|
1236
|
+
cryptosuite: "eddsa-jcs-2022",
|
|
1237
|
+
verificationMethod: "https://example.com/person2#main-key",
|
|
1238
|
+
proofPurpose: "assertionMethod",
|
|
1239
|
+
created: { "@value": "not-a-date" },
|
|
1240
|
+
proofValue: "zLaewdp4H9kqtwyrLatK4cjY5oRHwVcw4gibPSUDYDMhi4M49v8pcYk3ZB6D69dNpAPbUmY8ocuJ3m9KhKJEEg7z"
|
|
1241
|
+
}
|
|
1242
|
+
})
|
|
1243
|
+
});
|
|
1244
|
+
const malformedProofCreatedContext = createRequestContext({
|
|
1245
|
+
federation,
|
|
1246
|
+
request: malformedProofCreatedRequest,
|
|
1247
|
+
url: new URL(malformedProofCreatedRequest.url),
|
|
1248
|
+
data: void 0,
|
|
1249
|
+
documentLoader: mockDocumentLoader,
|
|
1250
|
+
contextLoader: mockDocumentLoader
|
|
1251
|
+
});
|
|
1252
|
+
response = await handleInbox(malformedProofCreatedRequest, {
|
|
1253
|
+
recipient: null,
|
|
1254
|
+
context: malformedProofCreatedContext,
|
|
1255
|
+
inboxContextFactory(_activity) {
|
|
1256
|
+
return createInboxContext({
|
|
1257
|
+
...malformedProofCreatedContext,
|
|
1258
|
+
clone: void 0
|
|
1259
|
+
});
|
|
1260
|
+
},
|
|
1261
|
+
...inboxOptions
|
|
1262
|
+
});
|
|
1263
|
+
assertEquals([response.status, await response.text()], [400, "Invalid activity."]);
|
|
1264
|
+
onNotFoundCalled = null;
|
|
1265
|
+
const signedRequest = await signRequest(unsignedRequest.clone(), rsaPrivateKey3, rsaPublicKey3.id);
|
|
1266
|
+
const signedContext = createRequestContext({
|
|
1267
|
+
federation,
|
|
1268
|
+
request: signedRequest,
|
|
1269
|
+
url: new URL(signedRequest.url),
|
|
1270
|
+
data: void 0,
|
|
1271
|
+
documentLoader: mockDocumentLoader
|
|
1272
|
+
});
|
|
1273
|
+
response = await handleInbox(signedRequest, {
|
|
1274
|
+
recipient: null,
|
|
1275
|
+
context: signedContext,
|
|
1276
|
+
inboxContextFactory(_activity) {
|
|
1277
|
+
return createInboxContext({
|
|
1278
|
+
...unsignedContext,
|
|
1279
|
+
clone: void 0
|
|
1280
|
+
});
|
|
1281
|
+
},
|
|
1282
|
+
...inboxOptions
|
|
1283
|
+
});
|
|
1284
|
+
assertEquals(onNotFoundCalled, null);
|
|
1285
|
+
assertEquals([response.status, await response.text()], [202, ""]);
|
|
1286
|
+
const ldSignedRequest = new Request("https://example.com/", {
|
|
1287
|
+
method: "POST",
|
|
1288
|
+
body: JSON.stringify(await signJsonLd({
|
|
1289
|
+
"@context": [
|
|
1290
|
+
"https://www.w3.org/ns/activitystreams",
|
|
1291
|
+
"https://w3id.org/identity/v1",
|
|
1292
|
+
"https://w3id.org/security/v1",
|
|
1293
|
+
"https://w3id.org/security/data-integrity/v1"
|
|
1294
|
+
],
|
|
1295
|
+
id: "https://example.com/activities/ld-signed",
|
|
1296
|
+
type: "Create",
|
|
1297
|
+
actor: "https://example.com/person2",
|
|
1298
|
+
object: {
|
|
1299
|
+
id: "https://example.com/notes/ld-signed",
|
|
1300
|
+
type: "Note",
|
|
1301
|
+
attributedTo: "https://example.com/person2",
|
|
1302
|
+
content: "Hello, world!"
|
|
1303
|
+
}
|
|
1304
|
+
}, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader }))
|
|
1305
|
+
});
|
|
1306
|
+
const ldSignedContext = createRequestContext({
|
|
1307
|
+
federation,
|
|
1308
|
+
request: ldSignedRequest,
|
|
1309
|
+
url: new URL(ldSignedRequest.url),
|
|
1310
|
+
data: void 0,
|
|
1311
|
+
documentLoader: mockDocumentLoader,
|
|
1312
|
+
contextLoader: restrictiveContextLoader
|
|
1313
|
+
});
|
|
1314
|
+
response = await handleInbox(ldSignedRequest, {
|
|
1315
|
+
recipient: null,
|
|
1316
|
+
context: ldSignedContext,
|
|
1317
|
+
inboxContextFactory(_activity) {
|
|
1318
|
+
return createInboxContext({
|
|
1319
|
+
...ldSignedContext,
|
|
1320
|
+
clone: void 0
|
|
1321
|
+
});
|
|
1322
|
+
},
|
|
1323
|
+
...inboxOptions
|
|
1324
|
+
});
|
|
1325
|
+
assertEquals(onNotFoundCalled, null);
|
|
1326
|
+
assertEquals([response.status, await response.text()], [202, ""]);
|
|
1327
|
+
const remoteContextUrl = "https://remote.example/contexts/ext";
|
|
1328
|
+
let failRemoteContextOnce = true;
|
|
1329
|
+
const flakyContextLoader = async (resource) => {
|
|
1330
|
+
const url = new URL(resource).href;
|
|
1331
|
+
if (url === remoteContextUrl) {
|
|
1332
|
+
if (failRemoteContextOnce) {
|
|
1333
|
+
failRemoteContextOnce = false;
|
|
1334
|
+
throw new Error(`Unexpected context: ${url}`);
|
|
1335
|
+
}
|
|
1336
|
+
return {
|
|
1337
|
+
contextUrl: null,
|
|
1338
|
+
documentUrl: url,
|
|
1339
|
+
document: { "@context": { ext: "https://example.com/ext" } }
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
return await mockDocumentLoader(url);
|
|
1343
|
+
};
|
|
1344
|
+
const httpSignedLdBody = {
|
|
1345
|
+
"@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"],
|
|
1346
|
+
id: "https://example.com/activities/http-signed-ld",
|
|
1347
|
+
type: "Create",
|
|
1348
|
+
actor: "https://example.com/person2",
|
|
1349
|
+
ext: "preserve-me",
|
|
1350
|
+
object: {
|
|
1351
|
+
id: "https://example.com/notes/http-signed-ld",
|
|
1352
|
+
type: "Note",
|
|
1353
|
+
attributedTo: "https://example.com/person2",
|
|
1354
|
+
content: "Hello, world!"
|
|
1355
|
+
},
|
|
1356
|
+
signature: {
|
|
1357
|
+
type: "RsaSignature2017",
|
|
1358
|
+
creator: rsaPublicKey3.id.href,
|
|
1359
|
+
created: "2024-01-01T00:00:00Z",
|
|
1360
|
+
signatureValue: "bogus"
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
const httpSignedLdRequest = await signRequest(new Request("https://example.com/", {
|
|
1364
|
+
method: "POST",
|
|
1365
|
+
body: JSON.stringify(httpSignedLdBody)
|
|
1366
|
+
}), rsaPrivateKey3, rsaPublicKey3.id);
|
|
1367
|
+
const httpSignedLdContext = createRequestContext({
|
|
1368
|
+
federation,
|
|
1369
|
+
request: httpSignedLdRequest,
|
|
1370
|
+
url: new URL(httpSignedLdRequest.url),
|
|
1371
|
+
data: void 0,
|
|
1372
|
+
documentLoader: mockDocumentLoader,
|
|
1373
|
+
contextLoader: flakyContextLoader
|
|
1374
|
+
});
|
|
1375
|
+
response = await handleInbox(httpSignedLdRequest, {
|
|
1376
|
+
recipient: null,
|
|
1377
|
+
context: httpSignedLdContext,
|
|
1378
|
+
inboxContextFactory(_activity) {
|
|
1379
|
+
return createInboxContext({
|
|
1380
|
+
...httpSignedLdContext,
|
|
1381
|
+
clone: void 0
|
|
1382
|
+
});
|
|
1383
|
+
},
|
|
1384
|
+
...inboxOptions
|
|
1385
|
+
});
|
|
1386
|
+
assertEquals(onNotFoundCalled, null);
|
|
1387
|
+
assertEquals([response.status, await response.text()], [202, ""]);
|
|
1388
|
+
const ldSignedOnlyBody = await signJsonLd({
|
|
1389
|
+
"@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"],
|
|
1390
|
+
id: "https://example.com/activities/ld-only-transient",
|
|
1391
|
+
type: "Create",
|
|
1392
|
+
actor: "https://example.com/person2",
|
|
1393
|
+
ext: "preserve-me",
|
|
1394
|
+
object: {
|
|
1395
|
+
id: "https://example.com/notes/ld-only-transient",
|
|
1396
|
+
type: "Note",
|
|
1397
|
+
attributedTo: "https://example.com/person2",
|
|
1398
|
+
content: "Hello, world!"
|
|
1399
|
+
}
|
|
1400
|
+
}, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: async (resource) => {
|
|
1401
|
+
const url = new URL(resource).href;
|
|
1402
|
+
if (url === remoteContextUrl) return {
|
|
1403
|
+
contextUrl: null,
|
|
1404
|
+
documentUrl: url,
|
|
1405
|
+
document: { "@context": { ext: "https://example.com/ext" } }
|
|
1406
|
+
};
|
|
1407
|
+
return await mockDocumentLoader(url);
|
|
1408
|
+
} });
|
|
1409
|
+
const malformedTemporalLdSignedBody = await signJsonLd({
|
|
1410
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1411
|
+
id: "https://example.com/activities/ld-only-invalid-published",
|
|
1412
|
+
type: "Create",
|
|
1413
|
+
actor: "https://example.com/person2",
|
|
1414
|
+
published: { "@value": "not-a-date" },
|
|
1415
|
+
object: {
|
|
1416
|
+
id: "https://example.com/notes/ld-only-invalid-published",
|
|
1417
|
+
type: "Note",
|
|
1418
|
+
attributedTo: "https://example.com/person2",
|
|
1419
|
+
content: "Hello, world!"
|
|
1420
|
+
}
|
|
1421
|
+
}, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader });
|
|
1422
|
+
const malformedTemporalLdSignedRequest = new Request("https://example.com/", {
|
|
1423
|
+
method: "POST",
|
|
1424
|
+
body: JSON.stringify(malformedTemporalLdSignedBody)
|
|
1425
|
+
});
|
|
1426
|
+
const malformedTemporalLdSignedContext = createRequestContext({
|
|
1427
|
+
federation,
|
|
1428
|
+
request: malformedTemporalLdSignedRequest,
|
|
1429
|
+
url: new URL(malformedTemporalLdSignedRequest.url),
|
|
1430
|
+
data: void 0,
|
|
1431
|
+
documentLoader: mockDocumentLoader,
|
|
1432
|
+
contextLoader: mockDocumentLoader
|
|
1433
|
+
});
|
|
1434
|
+
response = await handleInbox(malformedTemporalLdSignedRequest, {
|
|
1435
|
+
recipient: null,
|
|
1436
|
+
context: malformedTemporalLdSignedContext,
|
|
1437
|
+
inboxContextFactory(_activity) {
|
|
1438
|
+
return createInboxContext({
|
|
1439
|
+
...malformedTemporalLdSignedContext,
|
|
1440
|
+
clone: void 0
|
|
1441
|
+
});
|
|
1442
|
+
},
|
|
1443
|
+
...inboxOptions
|
|
1444
|
+
});
|
|
1445
|
+
assertEquals([response.status, await response.text()], [400, "Invalid activity."]);
|
|
1446
|
+
const malformedClosedLdSignedBody = await signJsonLd({
|
|
1447
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1448
|
+
id: "https://example.com/questions/ld-only-invalid-closed",
|
|
1449
|
+
type: "Question",
|
|
1450
|
+
closed: "2024-02-31T00:00:00Z"
|
|
1451
|
+
}, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader });
|
|
1452
|
+
const malformedClosedLdSignedRequest = new Request("https://example.com/", {
|
|
1453
|
+
method: "POST",
|
|
1454
|
+
body: JSON.stringify(malformedClosedLdSignedBody)
|
|
1455
|
+
});
|
|
1456
|
+
const malformedClosedLdSignedContext = createRequestContext({
|
|
1457
|
+
federation,
|
|
1458
|
+
request: malformedClosedLdSignedRequest,
|
|
1459
|
+
url: new URL(malformedClosedLdSignedRequest.url),
|
|
1460
|
+
data: void 0,
|
|
1461
|
+
documentLoader: mockDocumentLoader,
|
|
1462
|
+
contextLoader: mockDocumentLoader
|
|
1463
|
+
});
|
|
1464
|
+
response = await handleInbox(malformedClosedLdSignedRequest, {
|
|
1465
|
+
recipient: null,
|
|
1466
|
+
context: malformedClosedLdSignedContext,
|
|
1467
|
+
inboxContextFactory(_activity) {
|
|
1468
|
+
return createInboxContext({
|
|
1469
|
+
...malformedClosedLdSignedContext,
|
|
1470
|
+
clone: void 0
|
|
1471
|
+
});
|
|
1472
|
+
},
|
|
1473
|
+
...inboxOptions
|
|
1474
|
+
});
|
|
1475
|
+
assertEquals([response.status, await response.text()], [400, "Invalid activity."]);
|
|
1476
|
+
const malformedIriHttpSignedRequest = await signRequest(new Request("https://example.com/", {
|
|
1477
|
+
method: "POST",
|
|
1478
|
+
body: JSON.stringify({
|
|
1479
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1480
|
+
id: "http://[",
|
|
1481
|
+
type: "Create",
|
|
1482
|
+
actor: "https://example.com/person2",
|
|
1483
|
+
object: {
|
|
1484
|
+
id: "https://example.com/notes/http-signed-invalid-iri",
|
|
1485
|
+
type: "Note",
|
|
1486
|
+
attributedTo: "https://example.com/person2",
|
|
1487
|
+
content: "Hello, world!"
|
|
1488
|
+
}
|
|
1489
|
+
})
|
|
1490
|
+
}), rsaPrivateKey3, rsaPublicKey3.id);
|
|
1491
|
+
const malformedIriHttpSignedContext = createRequestContext({
|
|
1492
|
+
federation,
|
|
1493
|
+
request: malformedIriHttpSignedRequest,
|
|
1494
|
+
url: new URL(malformedIriHttpSignedRequest.url),
|
|
1495
|
+
data: void 0,
|
|
1496
|
+
documentLoader: mockDocumentLoader,
|
|
1497
|
+
contextLoader: mockDocumentLoader
|
|
1498
|
+
});
|
|
1499
|
+
response = await handleInbox(malformedIriHttpSignedRequest, {
|
|
1500
|
+
recipient: null,
|
|
1501
|
+
context: malformedIriHttpSignedContext,
|
|
1502
|
+
inboxContextFactory(_activity) {
|
|
1503
|
+
return createInboxContext({
|
|
1504
|
+
...malformedIriHttpSignedContext,
|
|
1505
|
+
clone: void 0
|
|
1506
|
+
});
|
|
1507
|
+
},
|
|
1508
|
+
...inboxOptions
|
|
1509
|
+
});
|
|
1510
|
+
assertEquals([response.status, await response.text()], [400, "Invalid activity."]);
|
|
1511
|
+
const ldSignedOnlyRequest = new Request("https://example.com/", {
|
|
1512
|
+
method: "POST",
|
|
1513
|
+
body: JSON.stringify(ldSignedOnlyBody)
|
|
1514
|
+
});
|
|
1515
|
+
const ldSignedOnlyContext = createRequestContext({
|
|
1516
|
+
federation,
|
|
1517
|
+
request: ldSignedOnlyRequest,
|
|
1518
|
+
url: new URL(ldSignedOnlyRequest.url),
|
|
1519
|
+
data: void 0,
|
|
1520
|
+
documentLoader: mockDocumentLoader,
|
|
1521
|
+
contextLoader: async (resource) => {
|
|
1522
|
+
const url = new URL(resource).href;
|
|
1523
|
+
if (url === remoteContextUrl) throw new Error(`Unexpected context: ${url}`);
|
|
1524
|
+
return await mockDocumentLoader(url);
|
|
1525
|
+
}
|
|
1526
|
+
});
|
|
1527
|
+
await assertRejects(() => handleInbox(ldSignedOnlyRequest, {
|
|
1528
|
+
recipient: null,
|
|
1529
|
+
context: ldSignedOnlyContext,
|
|
1530
|
+
inboxContextFactory(_activity) {
|
|
1531
|
+
return createInboxContext({
|
|
1532
|
+
...ldSignedOnlyContext,
|
|
1533
|
+
clone: void 0
|
|
1534
|
+
});
|
|
1535
|
+
},
|
|
1536
|
+
...inboxOptions
|
|
1537
|
+
}), Error);
|
|
1538
|
+
failRemoteContextOnce = true;
|
|
1539
|
+
const invalidHttpFallbackRequest = new Request("https://example.com/", {
|
|
1540
|
+
method: "POST",
|
|
1541
|
+
body: JSON.stringify(ldSignedOnlyBody),
|
|
1542
|
+
headers: { Signature: "bogus" }
|
|
1543
|
+
});
|
|
1544
|
+
const invalidHttpFallbackContext = createRequestContext({
|
|
1545
|
+
federation,
|
|
1546
|
+
request: invalidHttpFallbackRequest,
|
|
1547
|
+
url: new URL(invalidHttpFallbackRequest.url),
|
|
1548
|
+
data: void 0,
|
|
1549
|
+
documentLoader: mockDocumentLoader,
|
|
1550
|
+
contextLoader: flakyContextLoader
|
|
1551
|
+
});
|
|
1552
|
+
await assertRejects(() => handleInbox(invalidHttpFallbackRequest, {
|
|
1553
|
+
recipient: null,
|
|
1554
|
+
context: invalidHttpFallbackContext,
|
|
1555
|
+
inboxContextFactory(_activity) {
|
|
1556
|
+
return createInboxContext({
|
|
1557
|
+
...invalidHttpFallbackContext,
|
|
1558
|
+
clone: void 0
|
|
1559
|
+
});
|
|
1560
|
+
},
|
|
1561
|
+
...inboxOptions
|
|
1562
|
+
}), Error);
|
|
1563
|
+
const transientKeyContextUrl = "https://remote.example/contexts/key";
|
|
1564
|
+
const transientCreatorUrl = "https://remote.example/keys/transient#main-key";
|
|
1565
|
+
const verificationFailureLdSignedBody = await signJsonLd({
|
|
1566
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
1567
|
+
id: "https://example.com/activities/ld-key-fetch-transient",
|
|
1568
|
+
type: "Create",
|
|
1569
|
+
actor: "https://example.com/person2",
|
|
1570
|
+
object: {
|
|
1571
|
+
id: "https://example.com/notes/ld-key-fetch-transient",
|
|
1572
|
+
type: "Note",
|
|
1573
|
+
attributedTo: "https://example.com/person2",
|
|
1574
|
+
content: "Hello, world!"
|
|
1575
|
+
}
|
|
1576
|
+
}, rsaPrivateKey3, new URL(transientCreatorUrl), { contextLoader: mockDocumentLoader });
|
|
1577
|
+
const verificationFailureLdSignedRequest = new Request("https://example.com/", {
|
|
1578
|
+
method: "POST",
|
|
1579
|
+
body: JSON.stringify(verificationFailureLdSignedBody),
|
|
1580
|
+
headers: { Signature: "bogus" }
|
|
1581
|
+
});
|
|
1582
|
+
const verificationFailureLdSignedContext = createRequestContext({
|
|
1583
|
+
federation,
|
|
1584
|
+
request: verificationFailureLdSignedRequest,
|
|
1585
|
+
url: new URL(verificationFailureLdSignedRequest.url),
|
|
1586
|
+
data: void 0,
|
|
1587
|
+
documentLoader: async (resource) => {
|
|
1588
|
+
if (resource === transientCreatorUrl) return {
|
|
1589
|
+
contextUrl: null,
|
|
1590
|
+
documentUrl: resource,
|
|
1591
|
+
document: {
|
|
1592
|
+
"@context": [transientKeyContextUrl],
|
|
1593
|
+
id: resource
|
|
1594
|
+
}
|
|
1595
|
+
};
|
|
1596
|
+
return await mockDocumentLoader(new URL(resource).href);
|
|
1597
|
+
},
|
|
1598
|
+
contextLoader: async (resource) => {
|
|
1599
|
+
if (resource === transientKeyContextUrl) throw new Error(`Transient key context failure: ${resource}`);
|
|
1600
|
+
return await mockDocumentLoader(new URL(resource).href);
|
|
1601
|
+
}
|
|
1602
|
+
});
|
|
1603
|
+
response = await handleInbox(verificationFailureLdSignedRequest, {
|
|
1604
|
+
recipient: null,
|
|
1605
|
+
context: verificationFailureLdSignedContext,
|
|
1606
|
+
inboxContextFactory(_activity) {
|
|
1607
|
+
return createInboxContext({
|
|
1608
|
+
...verificationFailureLdSignedContext,
|
|
1609
|
+
clone: void 0
|
|
1610
|
+
});
|
|
1611
|
+
},
|
|
1612
|
+
...inboxOptions
|
|
1613
|
+
});
|
|
1614
|
+
assertEquals([response.status, await response.text()], [401, "Failed to verify the request signature."]);
|
|
1615
|
+
failRemoteContextOnce = true;
|
|
1616
|
+
const deferredMalformedTemporalLdSignedBody = await signJsonLd({
|
|
1617
|
+
"@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"],
|
|
1618
|
+
id: "https://example.com/activities/deferred-invalid-published",
|
|
1619
|
+
type: "Create",
|
|
1620
|
+
actor: "https://example.com/person2",
|
|
1621
|
+
ext: "preserve-me",
|
|
1622
|
+
published: { "@value": "not-a-date" },
|
|
1623
|
+
object: {
|
|
1624
|
+
id: "https://example.com/notes/deferred-invalid-published",
|
|
1625
|
+
type: "Note",
|
|
1626
|
+
attributedTo: "https://example.com/person2",
|
|
1627
|
+
content: "Hello, world!"
|
|
1628
|
+
}
|
|
1629
|
+
}, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: async (resource) => {
|
|
1630
|
+
const url = new URL(resource).href;
|
|
1631
|
+
if (url === remoteContextUrl) return {
|
|
1632
|
+
contextUrl: null,
|
|
1633
|
+
documentUrl: url,
|
|
1634
|
+
document: { "@context": { ext: "https://example.com/ext" } }
|
|
1635
|
+
};
|
|
1636
|
+
return await mockDocumentLoader(url);
|
|
1637
|
+
} });
|
|
1638
|
+
const deferredMalformedTemporalLdSignedRequest = new Request("https://example.com/", {
|
|
1639
|
+
method: "POST",
|
|
1640
|
+
body: JSON.stringify(deferredMalformedTemporalLdSignedBody),
|
|
1641
|
+
headers: { Signature: "bogus" }
|
|
1642
|
+
});
|
|
1643
|
+
const deferredMalformedTemporalLdSignedContext = createRequestContext({
|
|
1644
|
+
federation,
|
|
1645
|
+
request: deferredMalformedTemporalLdSignedRequest,
|
|
1646
|
+
url: new URL(deferredMalformedTemporalLdSignedRequest.url),
|
|
1647
|
+
data: void 0,
|
|
1648
|
+
documentLoader: mockDocumentLoader,
|
|
1649
|
+
contextLoader: flakyContextLoader
|
|
1650
|
+
});
|
|
1651
|
+
response = await handleInbox(deferredMalformedTemporalLdSignedRequest, {
|
|
1652
|
+
recipient: null,
|
|
1653
|
+
context: deferredMalformedTemporalLdSignedContext,
|
|
1654
|
+
inboxContextFactory(_activity) {
|
|
1655
|
+
return createInboxContext({
|
|
1656
|
+
...deferredMalformedTemporalLdSignedContext,
|
|
1657
|
+
clone: void 0
|
|
1658
|
+
});
|
|
1659
|
+
},
|
|
1660
|
+
...inboxOptions
|
|
1661
|
+
});
|
|
1662
|
+
assertEquals([response.status, await response.text()], [400, "Invalid activity."]);
|
|
1663
|
+
const malformedLdSignedRequest = new Request("https://example.com/", {
|
|
1664
|
+
method: "POST",
|
|
1665
|
+
body: JSON.stringify({
|
|
1666
|
+
...ldSignedOnlyBody,
|
|
1667
|
+
"@context": ["not a url", "https://www.w3.org/ns/activitystreams"]
|
|
1668
|
+
})
|
|
1669
|
+
});
|
|
1670
|
+
const malformedLdSignedContext = createRequestContext({
|
|
1671
|
+
federation,
|
|
1672
|
+
request: malformedLdSignedRequest,
|
|
1673
|
+
url: new URL(malformedLdSignedRequest.url),
|
|
1674
|
+
data: void 0,
|
|
1675
|
+
documentLoader: mockDocumentLoader,
|
|
1676
|
+
contextLoader: mockDocumentLoader
|
|
1677
|
+
});
|
|
1678
|
+
response = await handleInbox(malformedLdSignedRequest, {
|
|
1679
|
+
recipient: null,
|
|
1680
|
+
context: malformedLdSignedContext,
|
|
1681
|
+
inboxContextFactory(_activity) {
|
|
1682
|
+
return createInboxContext({
|
|
1683
|
+
...malformedLdSignedContext,
|
|
1684
|
+
clone: void 0
|
|
1685
|
+
});
|
|
1686
|
+
},
|
|
1687
|
+
...inboxOptions
|
|
1688
|
+
});
|
|
1689
|
+
assertEquals([response.status, await response.text()], [400, "Invalid JSON-LD."]);
|
|
1690
|
+
const dualSignedInvalidCreatorRequest = await signRequest(new Request("https://example.com/", {
|
|
1691
|
+
method: "POST",
|
|
1692
|
+
body: JSON.stringify({
|
|
1693
|
+
...httpSignedLdBody,
|
|
1694
|
+
signature: {
|
|
1695
|
+
...httpSignedLdBody.signature,
|
|
1696
|
+
creator: "not a url"
|
|
1697
|
+
}
|
|
1698
|
+
})
|
|
1699
|
+
}), rsaPrivateKey3, rsaPublicKey3.id);
|
|
1700
|
+
const dualSignedInvalidCreatorContext = createRequestContext({
|
|
1701
|
+
federation,
|
|
1702
|
+
request: dualSignedInvalidCreatorRequest,
|
|
1703
|
+
url: new URL(dualSignedInvalidCreatorRequest.url),
|
|
1704
|
+
data: void 0,
|
|
1705
|
+
documentLoader: mockDocumentLoader,
|
|
1706
|
+
contextLoader: flakyContextLoader
|
|
1707
|
+
});
|
|
1708
|
+
response = await handleInbox(dualSignedInvalidCreatorRequest, {
|
|
1709
|
+
recipient: null,
|
|
1710
|
+
context: dualSignedInvalidCreatorContext,
|
|
1711
|
+
inboxContextFactory(_activity) {
|
|
1712
|
+
return createInboxContext({
|
|
1713
|
+
...dualSignedInvalidCreatorContext,
|
|
1714
|
+
clone: void 0
|
|
1715
|
+
});
|
|
1716
|
+
},
|
|
1717
|
+
...inboxOptions
|
|
1718
|
+
});
|
|
1719
|
+
assertEquals(onNotFoundCalled, null);
|
|
1720
|
+
assertEquals([response.status, await response.text()], [202, ""]);
|
|
1721
|
+
const invalidUrlHttpSignedRequest = await signRequest(new Request("https://example.com/", {
|
|
1722
|
+
method: "POST",
|
|
1723
|
+
body: JSON.stringify({
|
|
1724
|
+
"@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"],
|
|
1725
|
+
id: "https://example.com/activities/http-signed-invalid-context",
|
|
1726
|
+
type: "Create",
|
|
1727
|
+
actor: "https://example.com/person2",
|
|
1728
|
+
ext: "preserve-me",
|
|
1729
|
+
object: {
|
|
1730
|
+
id: "https://example.com/notes/http-signed-invalid-context",
|
|
1731
|
+
type: "Note",
|
|
1732
|
+
attributedTo: "https://example.com/person2",
|
|
1733
|
+
content: "Hello, world!"
|
|
1734
|
+
}
|
|
1735
|
+
})
|
|
1736
|
+
}), rsaPrivateKey3, rsaPublicKey3.id);
|
|
1737
|
+
const invalidUrlHttpSignedContext = createRequestContext({
|
|
1738
|
+
federation,
|
|
1739
|
+
request: invalidUrlHttpSignedRequest,
|
|
1740
|
+
url: new URL(invalidUrlHttpSignedRequest.url),
|
|
1741
|
+
data: void 0,
|
|
1742
|
+
documentLoader: mockDocumentLoader,
|
|
1743
|
+
contextLoader: async (resource) => {
|
|
1744
|
+
const url = new URL(resource).href;
|
|
1745
|
+
if (url === remoteContextUrl) {
|
|
1746
|
+
const error = /* @__PURE__ */ new Error(`Transient remote context failure: ${url}`);
|
|
1747
|
+
error.name = "jsonld.InvalidUrl";
|
|
1748
|
+
error.details = {
|
|
1749
|
+
code: "loading remote context failed",
|
|
1750
|
+
url
|
|
1751
|
+
};
|
|
1752
|
+
throw error;
|
|
1753
|
+
}
|
|
1754
|
+
return await mockDocumentLoader(url);
|
|
1755
|
+
}
|
|
1756
|
+
});
|
|
1757
|
+
await assertRejects(() => handleInbox(invalidUrlHttpSignedRequest, {
|
|
1758
|
+
recipient: null,
|
|
1759
|
+
context: invalidUrlHttpSignedContext,
|
|
1760
|
+
inboxContextFactory(_activity) {
|
|
1761
|
+
return createInboxContext({
|
|
1762
|
+
...invalidUrlHttpSignedContext,
|
|
1763
|
+
clone: void 0
|
|
1764
|
+
});
|
|
1765
|
+
},
|
|
1766
|
+
...inboxOptions
|
|
1767
|
+
}), Error);
|
|
1768
|
+
const opaqueContextIdHttpSignedRequest = await signRequest(new Request("https://example.com/", {
|
|
1769
|
+
method: "POST",
|
|
1770
|
+
body: JSON.stringify({
|
|
1771
|
+
"@context": ["app-context", "https://www.w3.org/ns/activitystreams"],
|
|
1772
|
+
id: "https://example.com/activities/http-signed-opaque-context",
|
|
1773
|
+
type: "Create",
|
|
1774
|
+
actor: "https://example.com/person2",
|
|
1775
|
+
object: {
|
|
1776
|
+
id: "https://example.com/notes/http-signed-opaque-context",
|
|
1777
|
+
type: "Note",
|
|
1778
|
+
attributedTo: "https://example.com/person2",
|
|
1779
|
+
content: "Hello, world!"
|
|
1780
|
+
}
|
|
1781
|
+
})
|
|
1782
|
+
}), rsaPrivateKey3, rsaPublicKey3.id);
|
|
1783
|
+
const opaqueContextIdHttpSignedContext = createRequestContext({
|
|
1784
|
+
federation,
|
|
1785
|
+
request: opaqueContextIdHttpSignedRequest,
|
|
1786
|
+
url: new URL(opaqueContextIdHttpSignedRequest.url),
|
|
1787
|
+
data: void 0,
|
|
1788
|
+
documentLoader: mockDocumentLoader,
|
|
1789
|
+
contextLoader: async (resource) => {
|
|
1790
|
+
if (resource === "app-context") {
|
|
1791
|
+
const error = /* @__PURE__ */ new Error(`Opaque context backend is unavailable: ${resource}`);
|
|
1792
|
+
error.name = "jsonld.InvalidUrl";
|
|
1793
|
+
error.details = {
|
|
1794
|
+
code: "loading remote context failed",
|
|
1795
|
+
url: resource
|
|
1796
|
+
};
|
|
1797
|
+
throw error;
|
|
1798
|
+
}
|
|
1799
|
+
return await mockDocumentLoader(new URL(resource).href);
|
|
1800
|
+
}
|
|
1801
|
+
});
|
|
1802
|
+
await assertRejects(() => handleInbox(opaqueContextIdHttpSignedRequest, {
|
|
1803
|
+
recipient: null,
|
|
1804
|
+
context: opaqueContextIdHttpSignedContext,
|
|
1805
|
+
inboxContextFactory(_activity) {
|
|
1806
|
+
return createInboxContext({
|
|
1807
|
+
...opaqueContextIdHttpSignedContext,
|
|
1808
|
+
clone: void 0
|
|
1809
|
+
});
|
|
1810
|
+
},
|
|
1811
|
+
...inboxOptions
|
|
1812
|
+
}), Error);
|
|
1813
|
+
const opaqueContextTypeErrorHttpSignedRequest = await signRequest(new Request("https://example.com/", {
|
|
1814
|
+
method: "POST",
|
|
1815
|
+
body: JSON.stringify({
|
|
1816
|
+
"@context": ["app:context", "https://www.w3.org/ns/activitystreams"],
|
|
1817
|
+
id: "https://example.com/activities/http-signed-opaque-typeerror",
|
|
1818
|
+
type: "Create",
|
|
1819
|
+
actor: "https://example.com/person2",
|
|
1820
|
+
object: {
|
|
1821
|
+
id: "https://example.com/notes/http-signed-opaque-typeerror",
|
|
1822
|
+
type: "Note",
|
|
1823
|
+
attributedTo: "https://example.com/person2",
|
|
1824
|
+
content: "Hello, world!"
|
|
1825
|
+
}
|
|
1826
|
+
})
|
|
1827
|
+
}), rsaPrivateKey3, rsaPublicKey3.id);
|
|
1828
|
+
const opaqueContextTypeErrorHttpSignedContext = createRequestContext({
|
|
1829
|
+
federation,
|
|
1830
|
+
request: opaqueContextTypeErrorHttpSignedRequest,
|
|
1831
|
+
url: new URL(opaqueContextTypeErrorHttpSignedRequest.url),
|
|
1832
|
+
data: void 0,
|
|
1833
|
+
documentLoader: mockDocumentLoader,
|
|
1834
|
+
contextLoader: async (resource) => {
|
|
1835
|
+
if (resource === "app:context") throw new TypeError(`Invalid URL: ${resource}`);
|
|
1836
|
+
return await mockDocumentLoader(new URL(resource).href);
|
|
1837
|
+
}
|
|
1838
|
+
});
|
|
1839
|
+
await assertRejects(() => handleInbox(opaqueContextTypeErrorHttpSignedRequest, {
|
|
1840
|
+
recipient: null,
|
|
1841
|
+
context: opaqueContextTypeErrorHttpSignedContext,
|
|
1842
|
+
inboxContextFactory(_activity) {
|
|
1843
|
+
return createInboxContext({
|
|
1844
|
+
...opaqueContextTypeErrorHttpSignedContext,
|
|
1845
|
+
clone: void 0
|
|
1846
|
+
});
|
|
1847
|
+
},
|
|
1848
|
+
...inboxOptions
|
|
1849
|
+
}), Error);
|
|
1850
|
+
const networkPathContextHttpSignedRequest = await signRequest(new Request("https://example.com/", {
|
|
1851
|
+
method: "POST",
|
|
1852
|
+
body: JSON.stringify({
|
|
1853
|
+
"@context": ["//cdn.example/ctx", "https://www.w3.org/ns/activitystreams"],
|
|
1854
|
+
id: "https://example.com/activities/http-signed-network-path-context",
|
|
1855
|
+
type: "Create",
|
|
1856
|
+
actor: "https://example.com/person2",
|
|
1857
|
+
object: {
|
|
1858
|
+
id: "https://example.com/notes/http-signed-network-path-context",
|
|
1859
|
+
type: "Note",
|
|
1860
|
+
attributedTo: "https://example.com/person2",
|
|
1861
|
+
content: "Hello, world!"
|
|
1862
|
+
}
|
|
1863
|
+
})
|
|
1864
|
+
}), rsaPrivateKey3, rsaPublicKey3.id);
|
|
1865
|
+
const networkPathContextHttpSignedContext = createRequestContext({
|
|
1866
|
+
federation,
|
|
1867
|
+
request: networkPathContextHttpSignedRequest,
|
|
1868
|
+
url: new URL(networkPathContextHttpSignedRequest.url),
|
|
1869
|
+
data: void 0,
|
|
1870
|
+
documentLoader: mockDocumentLoader,
|
|
1871
|
+
contextLoader: async (resource) => {
|
|
1872
|
+
if (resource === "//cdn.example/ctx") {
|
|
1873
|
+
const error = /* @__PURE__ */ new Error(`Network-path context backend is unavailable: ${resource}`);
|
|
1874
|
+
error.name = "jsonld.InvalidUrl";
|
|
1875
|
+
error.details = {
|
|
1876
|
+
code: "loading remote context failed",
|
|
1877
|
+
url: resource
|
|
1878
|
+
};
|
|
1879
|
+
throw error;
|
|
1880
|
+
}
|
|
1881
|
+
return await mockDocumentLoader(new URL(resource).href);
|
|
1882
|
+
}
|
|
1883
|
+
});
|
|
1884
|
+
await assertRejects(() => handleInbox(networkPathContextHttpSignedRequest, {
|
|
1885
|
+
recipient: null,
|
|
1886
|
+
context: networkPathContextHttpSignedContext,
|
|
1887
|
+
inboxContextFactory(_activity) {
|
|
1888
|
+
return createInboxContext({
|
|
1889
|
+
...networkPathContextHttpSignedContext,
|
|
1890
|
+
clone: void 0
|
|
1891
|
+
});
|
|
1892
|
+
},
|
|
1893
|
+
...inboxOptions
|
|
1894
|
+
}), Error);
|
|
1895
|
+
const malformedNetworkPathContextHttpSignedRequest = await signRequest(new Request("https://example.com/", {
|
|
1896
|
+
method: "POST",
|
|
1897
|
+
body: JSON.stringify({
|
|
1898
|
+
"@context": ["//[", "https://www.w3.org/ns/activitystreams"],
|
|
1899
|
+
id: "https://example.com/activities/http-signed-malformed-network-path-context",
|
|
1900
|
+
type: "Create",
|
|
1901
|
+
actor: "https://example.com/person2",
|
|
1902
|
+
object: {
|
|
1903
|
+
id: "https://example.com/notes/http-signed-malformed-network-path-context",
|
|
1904
|
+
type: "Note",
|
|
1905
|
+
attributedTo: "https://example.com/person2",
|
|
1906
|
+
content: "Hello, world!"
|
|
1907
|
+
}
|
|
1908
|
+
})
|
|
1909
|
+
}), rsaPrivateKey3, rsaPublicKey3.id);
|
|
1910
|
+
const malformedNetworkPathContextHttpSignedContext = createRequestContext({
|
|
1911
|
+
federation,
|
|
1912
|
+
request: malformedNetworkPathContextHttpSignedRequest,
|
|
1913
|
+
url: new URL(malformedNetworkPathContextHttpSignedRequest.url),
|
|
1914
|
+
data: void 0,
|
|
1915
|
+
documentLoader: mockDocumentLoader,
|
|
1916
|
+
contextLoader: async (resource) => {
|
|
1917
|
+
if (resource === "//[") {
|
|
1918
|
+
const error = /* @__PURE__ */ new Error(`Malformed network-path context: ${resource}`);
|
|
1919
|
+
error.name = "jsonld.InvalidUrl";
|
|
1920
|
+
error.details = {
|
|
1921
|
+
code: "loading remote context failed",
|
|
1922
|
+
url: resource
|
|
1923
|
+
};
|
|
1924
|
+
throw error;
|
|
1925
|
+
}
|
|
1926
|
+
return await mockDocumentLoader(new URL(resource).href);
|
|
1927
|
+
}
|
|
1928
|
+
});
|
|
1929
|
+
response = await handleInbox(malformedNetworkPathContextHttpSignedRequest, {
|
|
1930
|
+
recipient: null,
|
|
1931
|
+
context: malformedNetworkPathContextHttpSignedContext,
|
|
1932
|
+
inboxContextFactory(_activity) {
|
|
1933
|
+
return createInboxContext({
|
|
1934
|
+
...malformedNetworkPathContextHttpSignedContext,
|
|
1935
|
+
clone: void 0
|
|
1936
|
+
});
|
|
1937
|
+
},
|
|
1938
|
+
...inboxOptions
|
|
1939
|
+
});
|
|
1940
|
+
assertEquals(response.status, 400);
|
|
1941
|
+
const malformedUrlLikeContextHttpSignedRequest = await signRequest(new Request("https://example.com/", {
|
|
1942
|
+
method: "POST",
|
|
1943
|
+
body: JSON.stringify({
|
|
1944
|
+
"@context": ["http://[", "https://www.w3.org/ns/activitystreams"],
|
|
1945
|
+
id: "https://example.com/activities/http-signed-malformed-url-like-context",
|
|
1946
|
+
type: "Create",
|
|
1947
|
+
actor: "https://example.com/person2",
|
|
1948
|
+
object: {
|
|
1949
|
+
id: "https://example.com/notes/http-signed-malformed-url-like-context",
|
|
1950
|
+
type: "Note",
|
|
1951
|
+
attributedTo: "https://example.com/person2",
|
|
1952
|
+
content: "Hello, world!"
|
|
1953
|
+
}
|
|
1954
|
+
})
|
|
1955
|
+
}), rsaPrivateKey3, rsaPublicKey3.id);
|
|
1956
|
+
const malformedUrlLikeContextHttpSignedContext = createRequestContext({
|
|
1957
|
+
federation,
|
|
1958
|
+
request: malformedUrlLikeContextHttpSignedRequest,
|
|
1959
|
+
url: new URL(malformedUrlLikeContextHttpSignedRequest.url),
|
|
1960
|
+
data: void 0,
|
|
1961
|
+
documentLoader: mockDocumentLoader,
|
|
1962
|
+
contextLoader: async (resource) => {
|
|
1963
|
+
if (resource === "http://[") {
|
|
1964
|
+
const error = /* @__PURE__ */ new Error(`Invalid remote context URL: ${resource}`);
|
|
1965
|
+
error.name = "jsonld.InvalidUrl";
|
|
1966
|
+
error.details = {
|
|
1967
|
+
code: "loading remote context failed",
|
|
1968
|
+
url: resource
|
|
1969
|
+
};
|
|
1970
|
+
throw error;
|
|
1971
|
+
}
|
|
1972
|
+
return await mockDocumentLoader(new URL(resource).href);
|
|
1973
|
+
}
|
|
1974
|
+
});
|
|
1975
|
+
response = await handleInbox(malformedUrlLikeContextHttpSignedRequest, {
|
|
1976
|
+
recipient: null,
|
|
1977
|
+
context: malformedUrlLikeContextHttpSignedContext,
|
|
1978
|
+
inboxContextFactory(_activity) {
|
|
1979
|
+
return createInboxContext({
|
|
1980
|
+
...malformedUrlLikeContextHttpSignedContext,
|
|
1981
|
+
clone: void 0
|
|
1982
|
+
});
|
|
1983
|
+
},
|
|
1984
|
+
...inboxOptions
|
|
1985
|
+
});
|
|
1986
|
+
assertEquals(response.status, 400);
|
|
1987
|
+
const malformedContextUrlHttpSignedRequest = await signRequest(new Request("https://example.com/", {
|
|
1988
|
+
method: "POST",
|
|
1989
|
+
body: JSON.stringify({
|
|
1990
|
+
"@context": ["not a url", "https://www.w3.org/ns/activitystreams"],
|
|
1991
|
+
id: "https://example.com/activities/http-signed-malformed-context",
|
|
1992
|
+
type: "Create",
|
|
1993
|
+
actor: "https://example.com/person2",
|
|
1994
|
+
object: {
|
|
1995
|
+
id: "https://example.com/notes/http-signed-malformed-context",
|
|
1996
|
+
type: "Note",
|
|
1997
|
+
attributedTo: "https://example.com/person2",
|
|
1998
|
+
content: "Hello, world!"
|
|
1999
|
+
}
|
|
2000
|
+
})
|
|
2001
|
+
}), rsaPrivateKey3, rsaPublicKey3.id);
|
|
2002
|
+
const malformedContextUrlHttpSignedContext = createRequestContext({
|
|
2003
|
+
federation,
|
|
2004
|
+
request: malformedContextUrlHttpSignedRequest,
|
|
2005
|
+
url: new URL(malformedContextUrlHttpSignedRequest.url),
|
|
2006
|
+
data: void 0,
|
|
2007
|
+
documentLoader: mockDocumentLoader,
|
|
2008
|
+
contextLoader: async (resource) => {
|
|
2009
|
+
if (resource === "not a url") {
|
|
2010
|
+
const error = /* @__PURE__ */ new Error(`Invalid remote context URL: ${resource}`);
|
|
2011
|
+
error.name = "jsonld.InvalidUrl";
|
|
2012
|
+
error.details = {
|
|
2013
|
+
code: "loading remote context failed",
|
|
2014
|
+
url: resource
|
|
2015
|
+
};
|
|
2016
|
+
throw error;
|
|
2017
|
+
}
|
|
2018
|
+
return await mockDocumentLoader(new URL(resource).href);
|
|
2019
|
+
}
|
|
2020
|
+
});
|
|
2021
|
+
response = await handleInbox(malformedContextUrlHttpSignedRequest, {
|
|
2022
|
+
recipient: null,
|
|
2023
|
+
context: malformedContextUrlHttpSignedContext,
|
|
2024
|
+
inboxContextFactory(_activity) {
|
|
2025
|
+
return createInboxContext({
|
|
2026
|
+
...malformedContextUrlHttpSignedContext,
|
|
2027
|
+
clone: void 0
|
|
2028
|
+
});
|
|
2029
|
+
},
|
|
2030
|
+
...inboxOptions
|
|
2031
|
+
});
|
|
2032
|
+
assertEquals(response.status, 400);
|
|
2033
|
+
const invalidRemoteContextHttpSignedRequest = await signRequest(new Request("https://example.com/", {
|
|
2034
|
+
method: "POST",
|
|
2035
|
+
body: JSON.stringify({
|
|
2036
|
+
"@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"],
|
|
2037
|
+
id: "https://example.com/activities/http-signed-invalid-remote-context",
|
|
2038
|
+
type: "Create",
|
|
2039
|
+
actor: "https://example.com/person2",
|
|
2040
|
+
object: {
|
|
2041
|
+
id: "https://example.com/notes/http-signed-invalid-remote-context",
|
|
2042
|
+
type: "Note",
|
|
2043
|
+
attributedTo: "https://example.com/person2",
|
|
2044
|
+
content: "Hello, world!"
|
|
2045
|
+
}
|
|
2046
|
+
})
|
|
2047
|
+
}), rsaPrivateKey3, rsaPublicKey3.id);
|
|
2048
|
+
const invalidRemoteContextHttpSignedContext = createRequestContext({
|
|
2049
|
+
federation,
|
|
2050
|
+
request: invalidRemoteContextHttpSignedRequest,
|
|
2051
|
+
url: new URL(invalidRemoteContextHttpSignedRequest.url),
|
|
2052
|
+
data: void 0,
|
|
2053
|
+
documentLoader: mockDocumentLoader,
|
|
2054
|
+
contextLoader: async (resource) => {
|
|
2055
|
+
const url = new URL(resource).href;
|
|
2056
|
+
if (url === remoteContextUrl) return {
|
|
2057
|
+
contextUrl: null,
|
|
2058
|
+
documentUrl: url,
|
|
2059
|
+
document: [
|
|
2060
|
+
"not",
|
|
2061
|
+
"an",
|
|
2062
|
+
"object"
|
|
2063
|
+
]
|
|
2064
|
+
};
|
|
2065
|
+
return await mockDocumentLoader(url);
|
|
2066
|
+
}
|
|
2067
|
+
});
|
|
2068
|
+
response = await handleInbox(invalidRemoteContextHttpSignedRequest, {
|
|
2069
|
+
recipient: null,
|
|
2070
|
+
context: invalidRemoteContextHttpSignedContext,
|
|
2071
|
+
inboxContextFactory(_activity) {
|
|
2072
|
+
return createInboxContext({
|
|
2073
|
+
...invalidRemoteContextHttpSignedContext,
|
|
2074
|
+
clone: void 0
|
|
2075
|
+
});
|
|
2076
|
+
},
|
|
2077
|
+
...inboxOptions
|
|
2078
|
+
});
|
|
2079
|
+
assertEquals(response.status, 400);
|
|
2080
|
+
const invalidUrlAbsoluteContextHttpSignedRequest = await signRequest(new Request("https://example.com/", {
|
|
2081
|
+
method: "POST",
|
|
2082
|
+
body: JSON.stringify({
|
|
2083
|
+
"@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"],
|
|
2084
|
+
id: "https://example.com/activities/http-signed-invalid-url-context",
|
|
2085
|
+
type: "Create",
|
|
2086
|
+
actor: "https://example.com/person2",
|
|
2087
|
+
ext: "preserve-me",
|
|
2088
|
+
object: {
|
|
2089
|
+
id: "https://example.com/notes/http-signed-invalid-url-context",
|
|
2090
|
+
type: "Note",
|
|
2091
|
+
attributedTo: "https://example.com/person2",
|
|
2092
|
+
content: "Hello, world!"
|
|
2093
|
+
}
|
|
2094
|
+
})
|
|
2095
|
+
}), rsaPrivateKey3, rsaPublicKey3.id);
|
|
2096
|
+
const invalidUrlAbsoluteContextHttpSignedContext = createRequestContext({
|
|
2097
|
+
federation,
|
|
2098
|
+
request: invalidUrlAbsoluteContextHttpSignedRequest,
|
|
2099
|
+
url: new URL(invalidUrlAbsoluteContextHttpSignedRequest.url),
|
|
2100
|
+
data: void 0,
|
|
2101
|
+
documentLoader: mockDocumentLoader,
|
|
2102
|
+
contextLoader: async (resource) => {
|
|
2103
|
+
const url = new URL(resource).href;
|
|
2104
|
+
if (url === remoteContextUrl) throw new TypeError(`Invalid URL: ${url}`);
|
|
2105
|
+
return await mockDocumentLoader(url);
|
|
2106
|
+
}
|
|
2107
|
+
});
|
|
2108
|
+
await assertRejects(() => handleInbox(invalidUrlAbsoluteContextHttpSignedRequest, {
|
|
2109
|
+
recipient: null,
|
|
2110
|
+
context: invalidUrlAbsoluteContextHttpSignedContext,
|
|
2111
|
+
inboxContextFactory(_activity) {
|
|
2112
|
+
return createInboxContext({
|
|
2113
|
+
...invalidUrlAbsoluteContextHttpSignedContext,
|
|
2114
|
+
clone: void 0
|
|
2115
|
+
});
|
|
2116
|
+
},
|
|
2117
|
+
...inboxOptions
|
|
2118
|
+
}), Error);
|
|
2119
|
+
const typeErrorHttpSignedRequest = await signRequest(new Request("https://example.com/", {
|
|
2120
|
+
method: "POST",
|
|
2121
|
+
body: JSON.stringify({
|
|
2122
|
+
"@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"],
|
|
2123
|
+
id: "https://example.com/activities/http-signed-typeerror-context",
|
|
2124
|
+
type: "Create",
|
|
2125
|
+
actor: "https://example.com/person2",
|
|
2126
|
+
ext: "preserve-me",
|
|
2127
|
+
object: {
|
|
2128
|
+
id: "https://example.com/notes/http-signed-typeerror-context",
|
|
2129
|
+
type: "Note",
|
|
2130
|
+
attributedTo: "https://example.com/person2",
|
|
2131
|
+
content: "Hello, world!"
|
|
2132
|
+
}
|
|
2133
|
+
})
|
|
2134
|
+
}), rsaPrivateKey3, rsaPublicKey3.id);
|
|
2135
|
+
const typeErrorHttpSignedContext = createRequestContext({
|
|
2136
|
+
federation,
|
|
2137
|
+
request: typeErrorHttpSignedRequest,
|
|
2138
|
+
url: new URL(typeErrorHttpSignedRequest.url),
|
|
2139
|
+
data: void 0,
|
|
2140
|
+
documentLoader: mockDocumentLoader,
|
|
2141
|
+
contextLoader: async (resource) => {
|
|
2142
|
+
const url = new URL(resource).href;
|
|
2143
|
+
if (url === remoteContextUrl) throw new TypeError(`The remote context host timed out: ${url}`);
|
|
2144
|
+
return await mockDocumentLoader(url);
|
|
2145
|
+
}
|
|
2146
|
+
});
|
|
2147
|
+
await assertRejects(() => handleInbox(typeErrorHttpSignedRequest, {
|
|
2148
|
+
recipient: null,
|
|
2149
|
+
context: typeErrorHttpSignedContext,
|
|
2150
|
+
inboxContextFactory(_activity) {
|
|
2151
|
+
return createInboxContext({
|
|
2152
|
+
...typeErrorHttpSignedContext,
|
|
2153
|
+
clone: void 0
|
|
2154
|
+
});
|
|
2155
|
+
},
|
|
2156
|
+
...inboxOptions
|
|
2157
|
+
}), Error);
|
|
2158
|
+
const rangeErrorHttpSignedRequest = await signRequest(new Request("https://example.com/", {
|
|
2159
|
+
method: "POST",
|
|
2160
|
+
body: JSON.stringify({
|
|
2161
|
+
"@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"],
|
|
2162
|
+
id: "https://example.com/activities/http-signed-rangeerror-context",
|
|
2163
|
+
type: "Create",
|
|
2164
|
+
actor: "https://example.com/person2",
|
|
2165
|
+
ext: "preserve-me",
|
|
2166
|
+
object: {
|
|
2167
|
+
id: "https://example.com/notes/http-signed-rangeerror-context",
|
|
2168
|
+
type: "Note",
|
|
2169
|
+
attributedTo: "https://example.com/person2",
|
|
2170
|
+
content: "Hello, world!"
|
|
2171
|
+
}
|
|
2172
|
+
})
|
|
2173
|
+
}), rsaPrivateKey3, rsaPublicKey3.id);
|
|
2174
|
+
const rangeErrorHttpSignedContext = createRequestContext({
|
|
2175
|
+
federation,
|
|
2176
|
+
request: rangeErrorHttpSignedRequest,
|
|
2177
|
+
url: new URL(rangeErrorHttpSignedRequest.url),
|
|
2178
|
+
data: void 0,
|
|
2179
|
+
documentLoader: mockDocumentLoader,
|
|
2180
|
+
contextLoader: async (resource) => {
|
|
2181
|
+
const url = new URL(resource).href;
|
|
2182
|
+
if (url === remoteContextUrl) throw new RangeError(`Temporary remote context cache window exceeded: ${url}`);
|
|
2183
|
+
return await mockDocumentLoader(url);
|
|
2184
|
+
}
|
|
2185
|
+
});
|
|
2186
|
+
await assertRejects(() => handleInbox(rangeErrorHttpSignedRequest, {
|
|
2187
|
+
recipient: null,
|
|
2188
|
+
context: rangeErrorHttpSignedContext,
|
|
2189
|
+
inboxContextFactory(_activity) {
|
|
2190
|
+
return createInboxContext({
|
|
2191
|
+
...rangeErrorHttpSignedContext,
|
|
2192
|
+
clone: void 0
|
|
1022
2193
|
});
|
|
1023
2194
|
},
|
|
1024
2195
|
...inboxOptions
|
|
1025
|
-
});
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
2196
|
+
}), Error);
|
|
2197
|
+
const syntaxErrorHttpSignedRequest = await signRequest(new Request("https://example.com/", {
|
|
2198
|
+
method: "POST",
|
|
2199
|
+
body: JSON.stringify({
|
|
2200
|
+
"@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"],
|
|
2201
|
+
id: "https://example.com/activities/http-signed-syntax-context",
|
|
2202
|
+
type: "Create",
|
|
2203
|
+
actor: "https://example.com/person2",
|
|
2204
|
+
ext: "preserve-me",
|
|
2205
|
+
object: {
|
|
2206
|
+
id: "https://example.com/notes/http-signed-syntax-context",
|
|
2207
|
+
type: "Note",
|
|
2208
|
+
attributedTo: "https://example.com/person2",
|
|
2209
|
+
content: "Hello, world!"
|
|
2210
|
+
}
|
|
2211
|
+
})
|
|
2212
|
+
}), rsaPrivateKey3, rsaPublicKey3.id);
|
|
2213
|
+
const syntaxErrorHttpSignedContext = createRequestContext({
|
|
1031
2214
|
federation,
|
|
1032
|
-
request:
|
|
1033
|
-
url: new URL(
|
|
2215
|
+
request: syntaxErrorHttpSignedRequest,
|
|
2216
|
+
url: new URL(syntaxErrorHttpSignedRequest.url),
|
|
1034
2217
|
data: void 0,
|
|
1035
|
-
documentLoader: mockDocumentLoader
|
|
2218
|
+
documentLoader: mockDocumentLoader,
|
|
2219
|
+
contextLoader: async (resource) => {
|
|
2220
|
+
const url = new URL(resource).href;
|
|
2221
|
+
if (url === remoteContextUrl) {
|
|
2222
|
+
const error = /* @__PURE__ */ new Error(`Transient syntax failure: ${url}`);
|
|
2223
|
+
error.name = "jsonld.SyntaxError";
|
|
2224
|
+
error.details = { code: "loading remote context failed" };
|
|
2225
|
+
throw error;
|
|
2226
|
+
}
|
|
2227
|
+
return await mockDocumentLoader(url);
|
|
2228
|
+
}
|
|
1036
2229
|
});
|
|
1037
|
-
|
|
2230
|
+
await assertRejects(() => handleInbox(syntaxErrorHttpSignedRequest, {
|
|
1038
2231
|
recipient: null,
|
|
1039
|
-
context:
|
|
2232
|
+
context: syntaxErrorHttpSignedContext,
|
|
1040
2233
|
inboxContextFactory(_activity) {
|
|
1041
2234
|
return createInboxContext({
|
|
1042
|
-
...
|
|
2235
|
+
...syntaxErrorHttpSignedContext,
|
|
1043
2236
|
clone: void 0
|
|
1044
2237
|
});
|
|
1045
2238
|
},
|
|
1046
2239
|
...inboxOptions
|
|
1047
|
-
});
|
|
1048
|
-
assertEquals(onNotFoundCalled, null);
|
|
1049
|
-
assertEquals([response.status, await response.text()], [202, ""]);
|
|
2240
|
+
}), Error);
|
|
1050
2241
|
response = await handleInbox(signedRequest, {
|
|
1051
2242
|
recipient: "someone",
|
|
1052
2243
|
context: signedContext,
|
|
@@ -1090,6 +2281,64 @@ test("handleInbox()", async () => {
|
|
|
1090
2281
|
});
|
|
1091
2282
|
assertEquals(onNotFoundCalled, null);
|
|
1092
2283
|
assertEquals(response.status, 202);
|
|
2284
|
+
const unsafeJson = {
|
|
2285
|
+
"@context": ["https://www.w3.org/ns/activitystreams", { rev: "@reverse" }],
|
|
2286
|
+
id: "https://example.com/activities/unsafe",
|
|
2287
|
+
type: "Announce",
|
|
2288
|
+
actor: "https://example.com/person2",
|
|
2289
|
+
object: "https://example.com/notes/1",
|
|
2290
|
+
rev: { object: {
|
|
2291
|
+
id: "https://example.com/activities/undo",
|
|
2292
|
+
type: "Undo",
|
|
2293
|
+
actor: "https://example.com/person2"
|
|
2294
|
+
} }
|
|
2295
|
+
};
|
|
2296
|
+
const unsafeRequest = await signRequest(new Request("https://example.com/", {
|
|
2297
|
+
method: "POST",
|
|
2298
|
+
body: JSON.stringify(unsafeJson)
|
|
2299
|
+
}), rsaPrivateKey3, rsaPublicKey3.id);
|
|
2300
|
+
const unsafeContext = createRequestContext({
|
|
2301
|
+
federation,
|
|
2302
|
+
request: unsafeRequest,
|
|
2303
|
+
url: new URL(unsafeRequest.url),
|
|
2304
|
+
data: void 0,
|
|
2305
|
+
documentLoader: mockDocumentLoader
|
|
2306
|
+
});
|
|
2307
|
+
response = await handleInbox(unsafeRequest, {
|
|
2308
|
+
recipient: null,
|
|
2309
|
+
context: unsafeContext,
|
|
2310
|
+
inboxContextFactory(_activity) {
|
|
2311
|
+
return createInboxContext({
|
|
2312
|
+
...unsafeContext,
|
|
2313
|
+
clone: void 0
|
|
2314
|
+
});
|
|
2315
|
+
},
|
|
2316
|
+
...inboxOptions
|
|
2317
|
+
});
|
|
2318
|
+
assertEquals(response.status, 202);
|
|
2319
|
+
const unsafeLdRequest = new Request("https://example.com/", {
|
|
2320
|
+
method: "POST",
|
|
2321
|
+
body: JSON.stringify(await signJsonLd(unsafeJson, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: mockDocumentLoader }))
|
|
2322
|
+
});
|
|
2323
|
+
const unsafeLdContext = createRequestContext({
|
|
2324
|
+
federation,
|
|
2325
|
+
request: unsafeLdRequest,
|
|
2326
|
+
url: new URL(unsafeLdRequest.url),
|
|
2327
|
+
data: void 0,
|
|
2328
|
+
documentLoader: mockDocumentLoader
|
|
2329
|
+
});
|
|
2330
|
+
response = await handleInbox(unsafeLdRequest, {
|
|
2331
|
+
recipient: null,
|
|
2332
|
+
context: unsafeLdContext,
|
|
2333
|
+
inboxContextFactory(_activity) {
|
|
2334
|
+
return createInboxContext({
|
|
2335
|
+
...unsafeLdContext,
|
|
2336
|
+
clone: void 0
|
|
2337
|
+
});
|
|
2338
|
+
},
|
|
2339
|
+
...inboxOptions
|
|
2340
|
+
});
|
|
2341
|
+
assertEquals(response.status, 400);
|
|
1093
2342
|
const signedInvalidRequest = await signRequest(new Request("https://example.com/", {
|
|
1094
2343
|
method: "POST",
|
|
1095
2344
|
body: JSON.stringify({
|
|
@@ -1487,6 +2736,263 @@ test("handleOutbox()", async () => {
|
|
|
1487
2736
|
assertEquals(response.status, 500);
|
|
1488
2737
|
assertEquals(onErrorCalled, true);
|
|
1489
2738
|
});
|
|
2739
|
+
test("handleInbox() preserves the raw signed payload for inboxContextFactory", async () => {
|
|
2740
|
+
const federation = createFederation({ kv: new MemoryKvStore() });
|
|
2741
|
+
const remoteContextUrl = "https://remote.example/contexts/ext";
|
|
2742
|
+
const sourceContextLoader = async (resource) => {
|
|
2743
|
+
const url = new URL(resource).href;
|
|
2744
|
+
if (url === remoteContextUrl) return {
|
|
2745
|
+
contextUrl: null,
|
|
2746
|
+
documentUrl: url,
|
|
2747
|
+
document: { "@context": { ext: "https://example.com/ext" } }
|
|
2748
|
+
};
|
|
2749
|
+
return await mockDocumentLoader(url);
|
|
2750
|
+
};
|
|
2751
|
+
const signed = await signJsonLd({
|
|
2752
|
+
"@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"],
|
|
2753
|
+
id: "https://example.com/activities/preserve-raw",
|
|
2754
|
+
type: "Create",
|
|
2755
|
+
actor: "https://example.com/person2",
|
|
2756
|
+
ext: "preserve-me",
|
|
2757
|
+
object: {
|
|
2758
|
+
id: "https://example.com/notes/preserve-raw",
|
|
2759
|
+
type: "Note",
|
|
2760
|
+
attributedTo: "https://example.com/person2",
|
|
2761
|
+
content: "Hello, world!"
|
|
2762
|
+
}
|
|
2763
|
+
}, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: sourceContextLoader });
|
|
2764
|
+
const request = new Request("https://example.com/", {
|
|
2765
|
+
method: "POST",
|
|
2766
|
+
body: JSON.stringify(signed)
|
|
2767
|
+
});
|
|
2768
|
+
const context = createRequestContext({
|
|
2769
|
+
federation,
|
|
2770
|
+
request,
|
|
2771
|
+
url: new URL(request.url),
|
|
2772
|
+
data: void 0,
|
|
2773
|
+
documentLoader: mockDocumentLoader,
|
|
2774
|
+
contextLoader: sourceContextLoader
|
|
2775
|
+
});
|
|
2776
|
+
let receivedRaw = null;
|
|
2777
|
+
let receivedTyped = null;
|
|
2778
|
+
const inboxListeners = new ActivityListenerSet();
|
|
2779
|
+
inboxListeners.add(Create, (ctx, activity) => {
|
|
2780
|
+
receivedRaw = ctx.activity;
|
|
2781
|
+
receivedTyped = activity;
|
|
2782
|
+
});
|
|
2783
|
+
const response = await handleInbox(request, {
|
|
2784
|
+
recipient: "someone",
|
|
2785
|
+
context,
|
|
2786
|
+
inboxContextFactory(recipient, activity, activityId, activityType) {
|
|
2787
|
+
return {
|
|
2788
|
+
...createInboxContext({
|
|
2789
|
+
...context,
|
|
2790
|
+
clone: void 0,
|
|
2791
|
+
recipient
|
|
2792
|
+
}),
|
|
2793
|
+
activity,
|
|
2794
|
+
activityId,
|
|
2795
|
+
activityType
|
|
2796
|
+
};
|
|
2797
|
+
},
|
|
2798
|
+
kv: new MemoryKvStore(),
|
|
2799
|
+
kvPrefixes: {
|
|
2800
|
+
activityIdempotence: ["_fedify", "activityIdempotence"],
|
|
2801
|
+
publicKey: ["_fedify", "publicKey"],
|
|
2802
|
+
acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"]
|
|
2803
|
+
},
|
|
2804
|
+
actorDispatcher: (_ctx, identifier) => identifier === "someone" ? new Person({ name: "Someone" }) : null,
|
|
2805
|
+
inboxListeners,
|
|
2806
|
+
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
2807
|
+
signatureTimeWindow: { minutes: 5 },
|
|
2808
|
+
skipSignatureVerification: false
|
|
2809
|
+
});
|
|
2810
|
+
assertEquals([response.status, await response.text()], [202, ""]);
|
|
2811
|
+
assertEquals(receivedRaw, signed);
|
|
2812
|
+
const delivered = receivedTyped;
|
|
2813
|
+
assert(delivered != null);
|
|
2814
|
+
assertEquals(delivered.id?.href, "https://example.com/activities/preserve-raw");
|
|
2815
|
+
});
|
|
2816
|
+
test("handleInbox() enqueues normalizedActivity for LD-signed inbox work", async () => {
|
|
2817
|
+
const remoteContextUrl = "https://remote.example/contexts/ext";
|
|
2818
|
+
const sourceContextLoader = async (resource) => {
|
|
2819
|
+
const url = new URL(resource).href;
|
|
2820
|
+
if (url === remoteContextUrl) return {
|
|
2821
|
+
contextUrl: null,
|
|
2822
|
+
documentUrl: url,
|
|
2823
|
+
document: { "@context": { ext: "https://example.com/ext" } }
|
|
2824
|
+
};
|
|
2825
|
+
return await mockDocumentLoader(url);
|
|
2826
|
+
};
|
|
2827
|
+
let queuedMessage = null;
|
|
2828
|
+
const queue = {
|
|
2829
|
+
enqueue(message) {
|
|
2830
|
+
queuedMessage = message;
|
|
2831
|
+
return Promise.resolve();
|
|
2832
|
+
},
|
|
2833
|
+
async listen() {}
|
|
2834
|
+
};
|
|
2835
|
+
const federation = createFederation({
|
|
2836
|
+
kv: new MemoryKvStore(),
|
|
2837
|
+
queue
|
|
2838
|
+
});
|
|
2839
|
+
const signed = await signJsonLd({
|
|
2840
|
+
"@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"],
|
|
2841
|
+
id: "https://example.com/activities/enqueued-normalized",
|
|
2842
|
+
type: "Create",
|
|
2843
|
+
actor: "https://example.com/person2",
|
|
2844
|
+
ext: "preserve-me",
|
|
2845
|
+
object: {
|
|
2846
|
+
id: "https://example.com/notes/enqueued-normalized",
|
|
2847
|
+
type: "Note",
|
|
2848
|
+
attributedTo: "https://example.com/person2",
|
|
2849
|
+
content: "Hello, world!"
|
|
2850
|
+
}
|
|
2851
|
+
}, rsaPrivateKey3, rsaPublicKey3.id, { contextLoader: sourceContextLoader });
|
|
2852
|
+
const request = new Request("https://example.com/", {
|
|
2853
|
+
method: "POST",
|
|
2854
|
+
body: JSON.stringify(signed)
|
|
2855
|
+
});
|
|
2856
|
+
const context = createRequestContext({
|
|
2857
|
+
federation,
|
|
2858
|
+
request,
|
|
2859
|
+
url: new URL(request.url),
|
|
2860
|
+
data: void 0,
|
|
2861
|
+
documentLoader: mockDocumentLoader,
|
|
2862
|
+
contextLoader: sourceContextLoader
|
|
2863
|
+
});
|
|
2864
|
+
const response = await handleInbox(request, {
|
|
2865
|
+
recipient: "someone",
|
|
2866
|
+
context,
|
|
2867
|
+
inboxContextFactory(recipient, activity, activityId, activityType) {
|
|
2868
|
+
return {
|
|
2869
|
+
...createInboxContext({
|
|
2870
|
+
...context,
|
|
2871
|
+
clone: void 0,
|
|
2872
|
+
recipient
|
|
2873
|
+
}),
|
|
2874
|
+
activity,
|
|
2875
|
+
activityId,
|
|
2876
|
+
activityType
|
|
2877
|
+
};
|
|
2878
|
+
},
|
|
2879
|
+
kv: new MemoryKvStore(),
|
|
2880
|
+
kvPrefixes: {
|
|
2881
|
+
activityIdempotence: ["_fedify", "activityIdempotence"],
|
|
2882
|
+
publicKey: ["_fedify", "publicKey"],
|
|
2883
|
+
acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"]
|
|
2884
|
+
},
|
|
2885
|
+
queue,
|
|
2886
|
+
actorDispatcher: (_ctx, identifier) => identifier === "someone" ? new Person({ name: "Someone" }) : null,
|
|
2887
|
+
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
2888
|
+
signatureTimeWindow: { minutes: 5 },
|
|
2889
|
+
skipSignatureVerification: false
|
|
2890
|
+
});
|
|
2891
|
+
assertEquals([response.status, await response.text()], [202, "Activity is enqueued."]);
|
|
2892
|
+
const enqueued = queuedMessage;
|
|
2893
|
+
assert(enqueued != null);
|
|
2894
|
+
const inboxMessage = enqueued;
|
|
2895
|
+
assertEquals(inboxMessage.activity, signed);
|
|
2896
|
+
assertEquals(inboxMessage.normalizedActivity, await compactJsonLd(signed, sourceContextLoader));
|
|
2897
|
+
assertEquals(inboxMessage.ldSignatureVerified, true);
|
|
2898
|
+
});
|
|
2899
|
+
test("handleInbox() caches normalizedActivity for queued signature-bearing fallback traffic", async () => {
|
|
2900
|
+
const remoteContextUrl = "https://remote.example/contexts/ext";
|
|
2901
|
+
let queuedMessage = null;
|
|
2902
|
+
const queue = {
|
|
2903
|
+
enqueue(message) {
|
|
2904
|
+
queuedMessage = message;
|
|
2905
|
+
return Promise.resolve();
|
|
2906
|
+
},
|
|
2907
|
+
async listen() {}
|
|
2908
|
+
};
|
|
2909
|
+
const federation = createFederation({
|
|
2910
|
+
kv: new MemoryKvStore(),
|
|
2911
|
+
queue
|
|
2912
|
+
});
|
|
2913
|
+
const sourceContextLoader = async (resource) => {
|
|
2914
|
+
const url = new URL(resource).href;
|
|
2915
|
+
if (url === remoteContextUrl) return {
|
|
2916
|
+
contextUrl: null,
|
|
2917
|
+
documentUrl: url,
|
|
2918
|
+
document: { "@context": { ext: "https://example.com/ext" } }
|
|
2919
|
+
};
|
|
2920
|
+
return await mockDocumentLoader(url);
|
|
2921
|
+
};
|
|
2922
|
+
const unsignedBody = {
|
|
2923
|
+
"@context": [remoteContextUrl, "https://www.w3.org/ns/activitystreams"],
|
|
2924
|
+
id: "https://example.com/activities/non-lds-queued-signature",
|
|
2925
|
+
type: "Create",
|
|
2926
|
+
actor: "https://example.com/person2",
|
|
2927
|
+
ext: "preserve-me",
|
|
2928
|
+
object: {
|
|
2929
|
+
id: "https://example.com/notes/non-lds-queued-signature",
|
|
2930
|
+
type: "Note",
|
|
2931
|
+
attributedTo: "https://example.com/person2",
|
|
2932
|
+
content: "Hello, world!"
|
|
2933
|
+
},
|
|
2934
|
+
signature: {
|
|
2935
|
+
type: "RsaSignature2017",
|
|
2936
|
+
creator: "not a url",
|
|
2937
|
+
created: "2024-09-12T16:50:46Z",
|
|
2938
|
+
signatureValue: "Zm9v"
|
|
2939
|
+
}
|
|
2940
|
+
};
|
|
2941
|
+
const request = await signRequest(new Request("https://example.com/", {
|
|
2942
|
+
method: "POST",
|
|
2943
|
+
body: JSON.stringify(unsignedBody)
|
|
2944
|
+
}), rsaPrivateKey3, rsaPublicKey3.id);
|
|
2945
|
+
const context = createRequestContext({
|
|
2946
|
+
federation,
|
|
2947
|
+
request,
|
|
2948
|
+
url: new URL(request.url),
|
|
2949
|
+
data: void 0,
|
|
2950
|
+
documentLoader: mockDocumentLoader,
|
|
2951
|
+
contextLoader: sourceContextLoader
|
|
2952
|
+
});
|
|
2953
|
+
const response = await handleInbox(request, {
|
|
2954
|
+
recipient: "someone",
|
|
2955
|
+
context,
|
|
2956
|
+
inboxContextFactory(recipient, activity, activityId, activityType) {
|
|
2957
|
+
return {
|
|
2958
|
+
...createInboxContext({
|
|
2959
|
+
...context,
|
|
2960
|
+
clone: void 0,
|
|
2961
|
+
recipient
|
|
2962
|
+
}),
|
|
2963
|
+
activity,
|
|
2964
|
+
activityId,
|
|
2965
|
+
activityType
|
|
2966
|
+
};
|
|
2967
|
+
},
|
|
2968
|
+
kv: new MemoryKvStore(),
|
|
2969
|
+
kvPrefixes: {
|
|
2970
|
+
activityIdempotence: ["_fedify", "activityIdempotence"],
|
|
2971
|
+
publicKey: ["_fedify", "publicKey"],
|
|
2972
|
+
acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"]
|
|
2973
|
+
},
|
|
2974
|
+
queue,
|
|
2975
|
+
actorDispatcher: (_ctx, identifier) => identifier === "someone" ? new Person({ name: "Someone" }) : null,
|
|
2976
|
+
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
2977
|
+
signatureTimeWindow: { minutes: 5 },
|
|
2978
|
+
skipSignatureVerification: false
|
|
2979
|
+
});
|
|
2980
|
+
assertEquals([response.status, await response.text()], [202, "Activity is enqueued."]);
|
|
2981
|
+
if (queuedMessage == null) throw new Error("Inbox message not queued.");
|
|
2982
|
+
const inboxMessage = queuedMessage;
|
|
2983
|
+
assertEquals(inboxMessage, {
|
|
2984
|
+
type: "inbox",
|
|
2985
|
+
id: inboxMessage.id,
|
|
2986
|
+
baseUrl: "https://example.com",
|
|
2987
|
+
activity: unsignedBody,
|
|
2988
|
+
normalizedActivity: await compactJsonLd(unsignedBody, sourceContextLoader),
|
|
2989
|
+
ldSignatureVerified: false,
|
|
2990
|
+
started: inboxMessage.started,
|
|
2991
|
+
attempt: 0,
|
|
2992
|
+
identifier: "someone",
|
|
2993
|
+
traceContext: inboxMessage.traceContext
|
|
2994
|
+
});
|
|
2995
|
+
});
|
|
1490
2996
|
test("respondWithObject()", async () => {
|
|
1491
2997
|
const response = await respondWithObject(new Note({
|
|
1492
2998
|
id: new URL("https://example.com/notes/1"),
|
|
@@ -1908,11 +3414,129 @@ test("handleCustomCollection()", async () => {
|
|
|
1908
3414
|
assertEquals(onNotFoundCalled, null);
|
|
1909
3415
|
assertEquals(onUnauthorizedCalled, null);
|
|
1910
3416
|
});
|
|
3417
|
+
test("handleCustomCollection() records OpenTelemetry collection metrics", async () => {
|
|
3418
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
3419
|
+
const context = createRequestContext({
|
|
3420
|
+
federation: createFederation({
|
|
3421
|
+
kv: new MemoryKvStore(),
|
|
3422
|
+
meterProvider
|
|
3423
|
+
}),
|
|
3424
|
+
data: void 0,
|
|
3425
|
+
url: new URL("https://example.com/users/someone/custom"),
|
|
3426
|
+
request: new Request("https://example.com/users/someone/custom", { headers: { Accept: "application/activity+json" } })
|
|
3427
|
+
});
|
|
3428
|
+
const dispatcher = (_ctx, values) => values.identifier === "someone" ? { items: [new Create({ id: new URL("https://example.com/activities/1") }), new Create({ id: new URL("https://example.com/activities/2") })] } : null;
|
|
3429
|
+
const counter = (_ctx, values) => values.identifier === "someone" ? 2 : null;
|
|
3430
|
+
assertEquals((await handleCustomCollection(context.request, {
|
|
3431
|
+
context,
|
|
3432
|
+
name: "custom collection",
|
|
3433
|
+
values: { identifier: "someone" },
|
|
3434
|
+
collectionCallbacks: {
|
|
3435
|
+
dispatcher,
|
|
3436
|
+
counter
|
|
3437
|
+
},
|
|
3438
|
+
filterPredicate: (item) => item.id?.href === "https://example.com/activities/1",
|
|
3439
|
+
meterProvider,
|
|
3440
|
+
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
3441
|
+
onUnauthorized: () => new Response("Unauthorized", { status: 401 })
|
|
3442
|
+
})).status, 200);
|
|
3443
|
+
const requests = recorder.getMeasurements("activitypub.collection.request");
|
|
3444
|
+
assertEquals(requests.length, 1);
|
|
3445
|
+
assertEquals(requests[0].attributes["activitypub.collection.kind"], "custom");
|
|
3446
|
+
assertEquals(requests[0].attributes["activitypub.collection.page"], false);
|
|
3447
|
+
assertEquals(requests[0].attributes["fedify.collection.dispatcher"], "custom");
|
|
3448
|
+
assertEquals(requests[0].attributes["activitypub.collection.result"], "served");
|
|
3449
|
+
assertEquals(requests[0].attributes["http.response.status_code"], 200);
|
|
3450
|
+
const durations = recorder.getMeasurements("activitypub.collection.dispatch.duration");
|
|
3451
|
+
assertEquals(durations.length, 1);
|
|
3452
|
+
assertEquals(durations[0].attributes["fedify.collection.dispatcher"], "custom");
|
|
3453
|
+
assertEquals(durations[0].attributes["http.response.status_code"], 200);
|
|
3454
|
+
const items = recorder.getMeasurements("activitypub.collection.page.items");
|
|
3455
|
+
assertEquals(items.length, 1);
|
|
3456
|
+
assertEquals(items[0].value, 1);
|
|
3457
|
+
assertEquals(items[0].attributes["http.response.status_code"], 200);
|
|
3458
|
+
const totalItems = recorder.getMeasurements("activitypub.collection.total_items");
|
|
3459
|
+
assertEquals(totalItems.length, 1);
|
|
3460
|
+
assertEquals(totalItems[0].value, 2);
|
|
3461
|
+
assertEquals(totalItems[0].attributes["http.response.status_code"], 200);
|
|
3462
|
+
});
|
|
3463
|
+
test("handleCustomCollection() records not_found status on dispatch metrics", async () => {
|
|
3464
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
3465
|
+
const context = createRequestContext({
|
|
3466
|
+
federation: createFederation({
|
|
3467
|
+
kv: new MemoryKvStore(),
|
|
3468
|
+
meterProvider
|
|
3469
|
+
}),
|
|
3470
|
+
data: void 0,
|
|
3471
|
+
url: new URL("https://example.com/users/nobody/custom"),
|
|
3472
|
+
request: new Request("https://example.com/users/nobody/custom", { headers: { Accept: "application/activity+json" } })
|
|
3473
|
+
});
|
|
3474
|
+
const dispatcher = () => null;
|
|
3475
|
+
assertEquals((await handleCustomCollection(context.request, {
|
|
3476
|
+
context,
|
|
3477
|
+
name: "custom collection",
|
|
3478
|
+
values: { identifier: "nobody" },
|
|
3479
|
+
collectionCallbacks: { dispatcher },
|
|
3480
|
+
meterProvider,
|
|
3481
|
+
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
3482
|
+
onUnauthorized: () => new Response("Unauthorized", { status: 401 })
|
|
3483
|
+
})).status, 404);
|
|
3484
|
+
const durations = recorder.getMeasurements("activitypub.collection.dispatch.duration");
|
|
3485
|
+
assertEquals(durations.length, 1);
|
|
3486
|
+
assertEquals(durations[0].attributes["activitypub.collection.result"], "not_found");
|
|
3487
|
+
assertEquals(durations[0].attributes["http.response.status_code"], 404);
|
|
3488
|
+
});
|
|
3489
|
+
test("handleCustomCollection() classifies deferred collection metrics as error", async () => {
|
|
3490
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
3491
|
+
const context = createRequestContext({
|
|
3492
|
+
federation: createFederation({
|
|
3493
|
+
kv: new MemoryKvStore(),
|
|
3494
|
+
meterProvider
|
|
3495
|
+
}),
|
|
3496
|
+
data: void 0,
|
|
3497
|
+
url: new URL("https://example.com/users/someone/custom"),
|
|
3498
|
+
request: new Request("https://example.com/users/someone/custom", { headers: { Accept: "application/activity+json" } })
|
|
3499
|
+
});
|
|
3500
|
+
const brokenActivity = new Create({ id: new URL("https://example.com/activities/1") });
|
|
3501
|
+
globalThis.Object.defineProperty(brokenActivity, "toJsonLd", { value: () => {
|
|
3502
|
+
throw new Error("serialization failed");
|
|
3503
|
+
} });
|
|
3504
|
+
const dispatcher = () => ({ items: [brokenActivity] });
|
|
3505
|
+
const counter = () => 1;
|
|
3506
|
+
await assertRejects(() => handleCustomCollection(context.request, {
|
|
3507
|
+
context,
|
|
3508
|
+
name: "custom collection",
|
|
3509
|
+
values: { identifier: "someone" },
|
|
3510
|
+
collectionCallbacks: {
|
|
3511
|
+
dispatcher,
|
|
3512
|
+
counter
|
|
3513
|
+
},
|
|
3514
|
+
meterProvider,
|
|
3515
|
+
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
3516
|
+
onUnauthorized: () => new Response("Unauthorized", { status: 401 })
|
|
3517
|
+
}), Error, "serialization failed");
|
|
3518
|
+
const requests = recorder.getMeasurements("activitypub.collection.request");
|
|
3519
|
+
assertEquals(requests.length, 1);
|
|
3520
|
+
assertEquals(requests[0].attributes["activitypub.collection.result"], "error");
|
|
3521
|
+
const durations = recorder.getMeasurements("activitypub.collection.dispatch.duration");
|
|
3522
|
+
assertEquals(durations.length, 1);
|
|
3523
|
+
assertEquals(durations[0].attributes["activitypub.collection.result"], "error");
|
|
3524
|
+
const items = recorder.getMeasurements("activitypub.collection.page.items");
|
|
3525
|
+
assertEquals(items.length, 1);
|
|
3526
|
+
assertEquals(items[0].value, 1);
|
|
3527
|
+
assertEquals(items[0].attributes["activitypub.collection.result"], "error");
|
|
3528
|
+
const totalItems = recorder.getMeasurements("activitypub.collection.total_items");
|
|
3529
|
+
assertEquals(totalItems.length, 1);
|
|
3530
|
+
assertEquals(totalItems[0].value, 1);
|
|
3531
|
+
assertEquals(totalItems[0].attributes["activitypub.collection.result"], "error");
|
|
3532
|
+
});
|
|
1911
3533
|
test("handleInbox() records OpenTelemetry span events", async () => {
|
|
1912
3534
|
const [tracerProvider, exporter] = createTestTracerProvider();
|
|
3535
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1913
3536
|
const kv = new MemoryKvStore();
|
|
1914
3537
|
const federation = createFederation({
|
|
1915
3538
|
kv,
|
|
3539
|
+
meterProvider,
|
|
1916
3540
|
tracerProvider
|
|
1917
3541
|
});
|
|
1918
3542
|
const activity = new Create({
|
|
@@ -1974,6 +3598,7 @@ test("handleInbox() records OpenTelemetry span events", async () => {
|
|
|
1974
3598
|
onNotFound: (_request) => new Response("Not found", { status: 404 }),
|
|
1975
3599
|
signatureTimeWindow: false,
|
|
1976
3600
|
skipSignatureVerification: true,
|
|
3601
|
+
meterProvider,
|
|
1977
3602
|
tracerProvider
|
|
1978
3603
|
})).status, 202);
|
|
1979
3604
|
assert(receivedActivity != null);
|
|
@@ -1992,12 +3617,101 @@ test("handleInbox() records OpenTelemetry span events", async () => {
|
|
|
1992
3617
|
const recordedActivity = JSON.parse(event.attributes["activitypub.activity.json"]);
|
|
1993
3618
|
assertEquals(recordedActivity.id, "https://example.com/activity");
|
|
1994
3619
|
assertEquals(recordedActivity.type, "Create");
|
|
3620
|
+
const durations = recorder.getMeasurements("activitypub.inbox.processing_duration");
|
|
3621
|
+
assertEquals(durations.length, 1);
|
|
3622
|
+
assertEquals(durations[0].type, "histogram");
|
|
3623
|
+
assertGreaterOrEqual(durations[0].value, 0);
|
|
3624
|
+
assertEquals(durations[0].attributes["activitypub.activity.type"], "https://www.w3.org/ns/activitystreams#Create");
|
|
3625
|
+
});
|
|
3626
|
+
test("handleInbox() records fedify.queue.task.enqueued when queued", async () => {
|
|
3627
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
3628
|
+
const kv = new MemoryKvStore();
|
|
3629
|
+
const federation = createFederation({
|
|
3630
|
+
kv,
|
|
3631
|
+
meterProvider
|
|
3632
|
+
});
|
|
3633
|
+
const activity = new Create({
|
|
3634
|
+
id: new URL("https://example.com/activities/queued"),
|
|
3635
|
+
actor: new URL("https://example.com/users/someone"),
|
|
3636
|
+
object: new Note({
|
|
3637
|
+
id: new URL("https://example.com/note-queued"),
|
|
3638
|
+
content: "Queue me up"
|
|
3639
|
+
})
|
|
3640
|
+
});
|
|
3641
|
+
const signed = await signRequest(new Request("https://example.com/users/someone/inbox", {
|
|
3642
|
+
method: "POST",
|
|
3643
|
+
headers: { "Content-Type": "application/activity+json" },
|
|
3644
|
+
body: JSON.stringify(await activity.toJsonLd())
|
|
3645
|
+
}), rsaPrivateKey3, new URL("https://example.com/users/someone#main-key"));
|
|
3646
|
+
const context = createRequestContext({
|
|
3647
|
+
federation,
|
|
3648
|
+
request: signed,
|
|
3649
|
+
url: new URL(signed.url),
|
|
3650
|
+
data: void 0,
|
|
3651
|
+
documentLoader: mockDocumentLoader,
|
|
3652
|
+
contextLoader: mockDocumentLoader,
|
|
3653
|
+
getActorUri(identifier) {
|
|
3654
|
+
return new URL(`https://example.com/users/${identifier}`);
|
|
3655
|
+
}
|
|
3656
|
+
});
|
|
3657
|
+
const actorDispatcher = (ctx, identifier) => {
|
|
3658
|
+
if (identifier !== "someone") return null;
|
|
3659
|
+
return new Person({
|
|
3660
|
+
id: ctx.getActorUri(identifier),
|
|
3661
|
+
name: "Someone",
|
|
3662
|
+
inbox: new URL("https://example.com/users/someone/inbox"),
|
|
3663
|
+
publicKey: rsaPublicKey2
|
|
3664
|
+
});
|
|
3665
|
+
};
|
|
3666
|
+
const queuedMessages = [];
|
|
3667
|
+
assertEquals((await handleInbox(signed, {
|
|
3668
|
+
recipient: "someone",
|
|
3669
|
+
context,
|
|
3670
|
+
inboxContextFactory(_activity) {
|
|
3671
|
+
return createInboxContext({
|
|
3672
|
+
...context,
|
|
3673
|
+
clone: void 0
|
|
3674
|
+
});
|
|
3675
|
+
},
|
|
3676
|
+
kv,
|
|
3677
|
+
kvPrefixes: {
|
|
3678
|
+
activityIdempotence: ["activityIdempotence"],
|
|
3679
|
+
publicKey: ["publicKey"],
|
|
3680
|
+
acceptSignatureNonce: ["acceptSignatureNonce"]
|
|
3681
|
+
},
|
|
3682
|
+
actorDispatcher,
|
|
3683
|
+
inboxListeners: new ActivityListenerSet(),
|
|
3684
|
+
inboxErrorHandler: void 0,
|
|
3685
|
+
onNotFound: (_request) => new Response("Not found", { status: 404 }),
|
|
3686
|
+
signatureTimeWindow: false,
|
|
3687
|
+
skipSignatureVerification: true,
|
|
3688
|
+
queue: {
|
|
3689
|
+
enqueue(message, _options) {
|
|
3690
|
+
queuedMessages.push(message);
|
|
3691
|
+
return Promise.resolve();
|
|
3692
|
+
},
|
|
3693
|
+
listen(_handler, _options) {
|
|
3694
|
+
return Promise.resolve();
|
|
3695
|
+
}
|
|
3696
|
+
},
|
|
3697
|
+
meterProvider
|
|
3698
|
+
})).status, 202);
|
|
3699
|
+
assertEquals(queuedMessages.length, 1);
|
|
3700
|
+
const enqueued = recorder.getMeasurements("fedify.queue.task.enqueued");
|
|
3701
|
+
assertEquals(enqueued.length, 1);
|
|
3702
|
+
assertEquals(enqueued[0].type, "counter");
|
|
3703
|
+
assertEquals(enqueued[0].attributes["fedify.queue.role"], "inbox");
|
|
3704
|
+
assertEquals(enqueued[0].attributes["fedify.queue.task.attempt"], 0);
|
|
3705
|
+
assertEquals(enqueued[0].attributes["activitypub.activity.type"], "https://www.w3.org/ns/activitystreams#Create");
|
|
3706
|
+
assertEquals(enqueued[0].attributes["fedify.queue.backend"], void 0);
|
|
1995
3707
|
});
|
|
1996
3708
|
test("handleInbox() records unverified HTTP signature details", async () => {
|
|
1997
3709
|
const [tracerProvider, exporter] = createTestTracerProvider();
|
|
3710
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
1998
3711
|
const kv = new MemoryKvStore();
|
|
1999
3712
|
const federation = createFederation({
|
|
2000
3713
|
kv,
|
|
3714
|
+
meterProvider,
|
|
2001
3715
|
tracerProvider
|
|
2002
3716
|
});
|
|
2003
3717
|
const keyId = new URL("https://gone.example/users/someone#main-key");
|
|
@@ -2062,6 +3776,7 @@ test("handleInbox() records unverified HTTP signature details", async () => {
|
|
|
2062
3776
|
onNotFound: (_request) => new Response("Not found", { status: 404 }),
|
|
2063
3777
|
signatureTimeWindow: false,
|
|
2064
3778
|
skipSignatureVerification: false,
|
|
3779
|
+
meterProvider,
|
|
2065
3780
|
tracerProvider
|
|
2066
3781
|
})).status, 202);
|
|
2067
3782
|
const verifySpans = exporter.getSpans("http_signatures.verify");
|
|
@@ -2074,6 +3789,12 @@ test("handleInbox() records unverified HTTP signature details", async () => {
|
|
|
2074
3789
|
assert(event.attributes != null);
|
|
2075
3790
|
assertEquals(event.attributes["http_signatures.failure_reason"], "keyFetchError");
|
|
2076
3791
|
assertEquals(event.attributes["http_signatures.key_fetch_status"], 410);
|
|
3792
|
+
const failures = recorder.getMeasurements("activitypub.signature.verification_failure");
|
|
3793
|
+
assertEquals(failures.length, 1);
|
|
3794
|
+
assertEquals(failures[0].type, "counter");
|
|
3795
|
+
assertEquals(failures[0].value, 1);
|
|
3796
|
+
assertEquals(failures[0].attributes["activitypub.remote.host"], "gone.example");
|
|
3797
|
+
assertEquals(failures[0].attributes["activitypub.verification.failure_reason"], "keyFetchError");
|
|
2077
3798
|
});
|
|
2078
3799
|
test("handleInbox() challenge policy enabled + unsigned request", async () => {
|
|
2079
3800
|
const activity = new Create({
|
|
@@ -2476,6 +4197,7 @@ test("handleInbox() nonce consumption on valid signed request", async () => {
|
|
|
2476
4197
|
]), void 0, "Nonce must be consumed after use");
|
|
2477
4198
|
});
|
|
2478
4199
|
test("handleInbox() nonce replay prevention", async () => {
|
|
4200
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
2479
4201
|
const activity = new Create({
|
|
2480
4202
|
id: new URL("https://example.com/activities/nonce-3"),
|
|
2481
4203
|
actor: new URL("https://example.com/person2"),
|
|
@@ -2496,7 +4218,10 @@ test("handleInbox() nonce replay prevention", async () => {
|
|
|
2496
4218
|
rfc9421: { nonce }
|
|
2497
4219
|
});
|
|
2498
4220
|
const context = createRequestContext({
|
|
2499
|
-
federation: createFederation({
|
|
4221
|
+
federation: createFederation({
|
|
4222
|
+
kv: new MemoryKvStore(),
|
|
4223
|
+
meterProvider
|
|
4224
|
+
}),
|
|
2500
4225
|
request: signedRequest,
|
|
2501
4226
|
url: new URL(signedRequest.url),
|
|
2502
4227
|
data: void 0,
|
|
@@ -2526,6 +4251,7 @@ test("handleInbox() nonce replay prevention", async () => {
|
|
|
2526
4251
|
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
2527
4252
|
signatureTimeWindow: { minutes: 5 },
|
|
2528
4253
|
skipSignatureVerification: false,
|
|
4254
|
+
meterProvider,
|
|
2529
4255
|
inboxChallengePolicy: {
|
|
2530
4256
|
enabled: true,
|
|
2531
4257
|
requestNonce: true,
|
|
@@ -2540,6 +4266,11 @@ test("handleInbox() nonce replay prevention", async () => {
|
|
|
2540
4266
|
assert(parsed[0].parameters.nonce != null, "Fresh challenge must include a new nonce");
|
|
2541
4267
|
assert(parsed[0].parameters.nonce !== nonce, "Fresh nonce must differ from the replayed one");
|
|
2542
4268
|
assertEquals(response.headers.get("Cache-Control"), "no-store", "Challenge response must have Cache-Control: no-store");
|
|
4269
|
+
const failures = recorder.getMeasurements("activitypub.signature.verification_failure");
|
|
4270
|
+
assertEquals(failures.length, 1);
|
|
4271
|
+
assertEquals(failures[0].value, 1);
|
|
4272
|
+
assertEquals(failures[0].attributes["activitypub.remote.host"], "example.com");
|
|
4273
|
+
assertEquals(failures[0].attributes["activitypub.verification.failure_reason"], "invalidNonce");
|
|
2543
4274
|
});
|
|
2544
4275
|
test("handleInbox() nonce bypass: valid sig without nonce + invalid sig with nonce", async () => {
|
|
2545
4276
|
const activity = new Create({
|
|
@@ -2619,6 +4350,7 @@ test("handleInbox() nonce bypass: valid sig without nonce + invalid sig with non
|
|
|
2619
4350
|
]), true, "Nonce must not be consumed when it comes from a non-verified signature");
|
|
2620
4351
|
});
|
|
2621
4352
|
test("handleInbox() actor/key mismatch does not consume nonce", async () => {
|
|
4353
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
2622
4354
|
const maliciousActivity = new Create({
|
|
2623
4355
|
id: new URL("https://attacker.example.com/activities/mismatch-nonce-1"),
|
|
2624
4356
|
actor: new URL("https://victim.example.com/users/alice"),
|
|
@@ -2644,7 +4376,10 @@ test("handleInbox() actor/key mismatch does not consume nonce", async () => {
|
|
|
2644
4376
|
rfc9421: { nonce }
|
|
2645
4377
|
});
|
|
2646
4378
|
const context = createRequestContext({
|
|
2647
|
-
federation: createFederation({
|
|
4379
|
+
federation: createFederation({
|
|
4380
|
+
kv: new MemoryKvStore(),
|
|
4381
|
+
meterProvider
|
|
4382
|
+
}),
|
|
2648
4383
|
request: maliciousRequest,
|
|
2649
4384
|
url: new URL(maliciousRequest.url),
|
|
2650
4385
|
data: void 0,
|
|
@@ -2674,6 +4409,7 @@ test("handleInbox() actor/key mismatch does not consume nonce", async () => {
|
|
|
2674
4409
|
onNotFound: () => new Response("Not found", { status: 404 }),
|
|
2675
4410
|
signatureTimeWindow: { minutes: 5 },
|
|
2676
4411
|
skipSignatureVerification: false,
|
|
4412
|
+
meterProvider,
|
|
2677
4413
|
inboxChallengePolicy: {
|
|
2678
4414
|
enabled: true,
|
|
2679
4415
|
requestNonce: true,
|
|
@@ -2687,6 +4423,11 @@ test("handleInbox() actor/key mismatch does not consume nonce", async () => {
|
|
|
2687
4423
|
"acceptSignatureNonce",
|
|
2688
4424
|
nonce
|
|
2689
4425
|
]), true, "Nonce must not be consumed when actor/key ownership check fails");
|
|
4426
|
+
const failures = recorder.getMeasurements("activitypub.signature.verification_failure");
|
|
4427
|
+
assertEquals(failures.length, 1);
|
|
4428
|
+
assertEquals(failures[0].value, 1);
|
|
4429
|
+
assertEquals(failures[0].attributes["activitypub.remote.host"], "example.com");
|
|
4430
|
+
assertEquals(failures[0].attributes["activitypub.verification.failure_reason"], "actorKeyMismatch");
|
|
2690
4431
|
});
|
|
2691
4432
|
test("handleInbox() challenge policy enabled + unverifiedActivityHandler returns undefined", async () => {
|
|
2692
4433
|
const activity = new Create({
|