@fedify/fedify 2.3.0-dev.994 → 2.3.0-pr.809.37

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.
Files changed (138) hide show
  1. package/README.md +16 -7
  2. package/dist/{assert-DikXweDx.mjs → assert-OguE97r2.mjs} +1 -1
  3. package/dist/{assert_instance_of-C4Ri6VuN.mjs → assert_instance_of-DBC5X09g.mjs} +1 -1
  4. package/dist/{assert_not_equals--wG9hV7u.mjs → assert_not_equals-DkVK8oqV.mjs} +1 -1
  5. package/dist/{assert_rejects-B-qJtC9Z.mjs → assert_rejects-DN60FHPX.mjs} +28 -3
  6. package/dist/{assert_strict_equals-Dmjbg-bA.mjs → assert_strict_equals-XEgZAlrj.mjs} +1 -1
  7. package/dist/{assert_throws-4NwKEy2q.mjs → assert_throws-BOkhLGYc.mjs} +1 -1
  8. package/dist/{builder-yAlpiIqP.mjs → builder-Dl5Sdmc9.mjs} +96 -42
  9. package/dist/{chunk-nlSIicah.js → chunk-CRNNMoPX.js} +2 -2
  10. package/dist/circuit-breaker-CSWsyoef.mjs +337 -0
  11. package/dist/{client-z-8dc-e1.d.cts → client-CAM_bQXx.d.cts} +1 -0
  12. package/dist/{client-AtlibPOU.d.ts → client-CSddvgWN.d.ts} +1 -2
  13. package/dist/compat/mod.d.cts +2 -1
  14. package/dist/compat/mod.d.ts +2 -3
  15. package/dist/compat/mod.js +3 -3
  16. package/dist/compat/outgoing-jsonld.test.mjs +4 -4
  17. package/dist/compat/public-audience.test.mjs +4 -4
  18. package/dist/compat/transformers.test.mjs +5 -5
  19. package/dist/{context-BzH2-ajs.d.ts → context-BBVLF7lx.d.cts} +342 -269
  20. package/dist/{context-DJGagtNd.d.cts → context-BU6jSQdo.d.ts} +344 -266
  21. package/dist/{context-Dk_tacqz.mjs → context-DVoTs_wM.mjs} +64 -5
  22. package/dist/{deno-XBVlKnOX.mjs → deno-BID9qTPk.mjs} +1 -1
  23. package/dist/{docloader-DPp-X6gk.mjs → docloader-BYFuVwwi.mjs} +4 -4
  24. package/dist/{esm-DVILvP5e.mjs → esm-vrlUxr60.mjs} +4 -7
  25. package/dist/federation/builder.test.mjs +163 -11
  26. package/dist/federation/circuit-breaker.test.d.mts +2 -0
  27. package/dist/federation/circuit-breaker.test.mjs +446 -0
  28. package/dist/federation/collection.test.mjs +3 -3
  29. package/dist/federation/handler.test.mjs +1770 -29
  30. package/dist/federation/idempotency.test.mjs +5 -5
  31. package/dist/federation/inbox.test.mjs +4 -4
  32. package/dist/federation/keycache.test.mjs +5 -5
  33. package/dist/federation/kv.test.mjs +2 -2
  34. package/dist/federation/metrics.test.d.mts +2 -0
  35. package/dist/federation/metrics.test.mjs +634 -0
  36. package/dist/federation/middleware.test.mjs +4036 -146
  37. package/dist/federation/mod.cjs +195 -14
  38. package/dist/federation/mod.d.cts +6 -4
  39. package/dist/federation/mod.d.ts +6 -6
  40. package/dist/federation/mod.js +192 -14
  41. package/dist/federation/mq.test.mjs +176 -15
  42. package/dist/federation/negotiation.test.mjs +4 -4
  43. package/dist/federation/retry.test.mjs +3 -3
  44. package/dist/federation/router.test.mjs +190 -9
  45. package/dist/federation/send.test.mjs +153 -15
  46. package/dist/federation/temporal.test.d.mts +2 -0
  47. package/dist/federation/temporal.test.mjs +71 -0
  48. package/dist/federation/webfinger.test.mjs +150 -5
  49. package/dist/{http-B4rrzxtg.js → http-B47Jg9iX.js} +994 -64
  50. package/dist/{http-CSlLA54v.mjs → http-CO59u1Yk.mjs} +146 -47
  51. package/dist/{http-BrGqJDXL.cjs → http-CjZwUPiT.cjs} +1099 -61
  52. package/dist/{http-aQzN9Ayi.d.ts → http-VyDTd4G3.d.cts} +15 -3
  53. package/dist/{http-CrGuipxe.d.cts → http-lf8Hsd91.d.ts} +15 -1
  54. package/dist/{key-Bzx--sHD.mjs → key-Dqdlo3mG.mjs} +43 -18
  55. package/dist/{kv-CbLNp3zQ.d.cts → kv-D6hNiMTK.d.ts} +1 -0
  56. package/dist/{kv-cache-DI-Sk7-I.cjs → kv-cache-BQ1-zDKX.cjs} +20 -3
  57. package/dist/{kv-cache-EMIqoIuB.js → kv-cache-Dh2ZH9Gr.js} +20 -3
  58. package/dist/{kv-cache-U__xU4qR.mjs → kv-cache-DhLkLv0P.mjs} +21 -3
  59. package/dist/{kv-GFYnFoOl.d.ts → kv-gJ8LYbxX.d.cts} +1 -3
  60. package/dist/ld-JbCByP5P.mjs +626 -0
  61. package/dist/metrics-ColY3QKj.mjs +815 -0
  62. package/dist/{middleware-zjZ6AFCW.mjs → middleware-Buqzuiaf.mjs} +1 -1
  63. package/dist/{middleware-An4DjES4.cjs → middleware-CWQ3Yi07.cjs} +2318 -623
  64. package/dist/{middleware-K1g6bEkV.mjs → middleware-Cg4EyWsq.mjs} +1639 -373
  65. package/dist/{middleware-nidH7VZH.js → middleware-iAJY2aU-.js} +2260 -556
  66. package/dist/{mod-CR8soWa9.d.ts → mod-B0hW12_O.d.cts} +26 -4
  67. package/dist/mod-C0F6kvgS.d.cts +182 -0
  68. package/dist/{mod-Cr3f-ACa.d.cts → mod-COIAjwRS.d.ts} +26 -2
  69. package/dist/{mod-CMEbIaNh.d.cts → mod-DFvNJcNb.d.ts} +56 -4
  70. package/dist/mod-vPYVoa5n.d.ts +182 -0
  71. package/dist/{mod-CLgIXe9w.d.ts → mod-yvIXFAEi.d.cts} +56 -6
  72. package/dist/mod.cjs +9 -6
  73. package/dist/mod.d.cts +12 -10
  74. package/dist/mod.d.ts +12 -12
  75. package/dist/mod.js +11 -11
  76. package/dist/mq-D-nlpY04.d.ts +208 -0
  77. package/dist/mq-D8uSFzxe.d.cts +208 -0
  78. package/dist/{negotiation-SQvQgUqe.mjs → negotiation-DDstyBvc.mjs} +29 -0
  79. package/dist/nodeinfo/client.test.mjs +4 -4
  80. package/dist/nodeinfo/handler.test.mjs +4 -4
  81. package/dist/nodeinfo/mod.d.cts +2 -1
  82. package/dist/nodeinfo/mod.d.ts +2 -3
  83. package/dist/nodeinfo/mod.js +3 -3
  84. package/dist/nodeinfo/types.test.mjs +3 -3
  85. package/dist/otel/exporter.test.mjs +28 -25
  86. package/dist/otel/mod.cjs +6 -5
  87. package/dist/otel/mod.d.cts +5 -3
  88. package/dist/otel/mod.d.ts +5 -5
  89. package/dist/otel/mod.js +8 -7
  90. package/dist/{outgoing-jsonld-CNmZLixq.mjs → outgoing-jsonld-L_DbOaFe.mjs} +2 -2
  91. package/dist/{owner-C-tqfvxn.mjs → owner-B4LgOmzV.mjs} +2 -2
  92. package/dist/{owner-CptqhsOy.d.cts → owner-CnngXDNJ.d.ts} +2 -1
  93. package/dist/{owner-74ARJ5TL.d.ts → owner-DEvZuyOE.d.cts} +2 -3
  94. package/dist/{proof-BlmRPzrK.mjs → proof-BoSGOq9q.mjs} +26 -8
  95. package/dist/{proof-Bbbwf8_x.js → proof-CzhZuawt.js} +378 -15
  96. package/dist/{proof-BW89QMVB.cjs → proof-DvMI-3vq.cjs} +432 -15
  97. package/dist/runtime/mod.d.cts +1 -0
  98. package/dist/runtime/mod.d.ts +1 -2
  99. package/dist/runtime/mod.js +3 -3
  100. package/dist/{send-05pJLcb6.mjs → send-ByK1Q7DK.mjs} +72 -26
  101. package/dist/sig/http.test.mjs +356 -33
  102. package/dist/sig/key.test.mjs +103 -6
  103. package/dist/sig/ld.test.mjs +696 -7
  104. package/dist/sig/mod.cjs +2 -2
  105. package/dist/sig/mod.d.cts +4 -3
  106. package/dist/sig/mod.d.ts +4 -5
  107. package/dist/sig/mod.js +4 -4
  108. package/dist/sig/owner.test.mjs +6 -6
  109. package/dist/sig/proof.test.mjs +169 -8
  110. package/dist/{std__assert-CRDpx_HF.mjs → std__assert-BBjXFNOb.mjs} +5 -30
  111. package/dist/temporal-LpjyAjKb.mjs +95 -0
  112. package/dist/testing/mod.d.mts +110 -10
  113. package/dist/testing/mod.mjs +1 -2
  114. package/dist/{transformers-ve6e2xcg.js → transformers-BGMIq1cs.js} +2 -2
  115. package/dist/{types-hvL8ElAs.js → types-CAY3OdLq.js} +2 -2
  116. package/dist/utils/docloader.test.mjs +6 -6
  117. package/dist/utils/kv-cache.test.mjs +67 -2
  118. package/dist/utils/mod.cjs +1 -1
  119. package/dist/utils/mod.d.cts +2 -1
  120. package/dist/utils/mod.d.ts +2 -3
  121. package/dist/utils/mod.js +3 -3
  122. package/dist/vocab/cjs.test.mjs +1 -1
  123. package/dist/vocab/mod.d.cts +1 -0
  124. package/dist/vocab/mod.d.ts +1 -2
  125. package/dist/vocab/mod.js +2 -2
  126. package/package.json +16 -16
  127. package/dist/ld-DR78beiv.mjs +0 -279
  128. package/dist/middleware-BFBaR0mF.cjs +0 -4
  129. package/dist/mod-2d12ffz3.d.ts +0 -64
  130. package/dist/mod-D35TRn09.d.cts +0 -62
  131. package/dist/router-CrMLXoOr.mjs +0 -114
  132. package/dist/{activity-listener-ell7W1s9.mjs → activity-listener-tztVvlNb.mjs} +0 -0
  133. package/dist/{assert_equals-Ew3jOFa3.mjs → assert_equals-C-ZRDbaf.mjs} +0 -0
  134. package/dist/{client-D_1QpnWt.mjs → client-ByXmQhYD.mjs} +1 -1
  135. package/dist/{collection-D-HqUuA2.mjs → collection-Cc3DVAhE.mjs} +0 -0
  136. package/dist/{keycache-EGATflN-.mjs → keycache-BeU0LCII.mjs} +0 -0
  137. package/dist/{public-audience-DYFHzm_c.mjs → public-audience-Cvbr2Gzt.mjs} +1 -1
  138. /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-Dk_tacqz.mjs";
5
- import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
6
- import "../std__assert-CRDpx_HF.mjs";
7
- import { t as assertInstanceOf } from "../assert_instance_of-C4Ri6VuN.mjs";
8
- import { t as assert } from "../assert-DikXweDx.mjs";
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-CSlLA54v.mjs";
11
+ import { s as signRequest } from "../http-CO59u1Yk.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-JbCByP5P.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-K1g6bEkV.mjs";
14
- import { t as ActivityListenerSet } from "../activity-listener-ell7W1s9.mjs";
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-Cg4EyWsq.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
- assertEquals(onNotFoundCalled, null);
1027
- assertEquals(response.status, 401);
1028
- onNotFoundCalled = null;
1029
- const signedRequest = await signRequest(unsignedRequest.clone(), rsaPrivateKey3, rsaPublicKey3.id);
1030
- const signedContext = createRequestContext({
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: signedRequest,
1033
- url: new URL(signedRequest.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
- response = await handleInbox(signedRequest, {
2230
+ await assertRejects(() => handleInbox(syntaxErrorHttpSignedRequest, {
1038
2231
  recipient: null,
1039
- context: signedContext,
2232
+ context: syntaxErrorHttpSignedContext,
1040
2233
  inboxContextFactory(_activity) {
1041
2234
  return createInboxContext({
1042
- ...unsignedContext,
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({ kv: new MemoryKvStore() }),
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({ kv: new MemoryKvStore() }),
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({