@fedify/fedify 2.4.0-pr.936.41 → 2.5.0-dev.2271

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 (244) hide show
  1. package/README.md +50 -39
  2. package/dist/{assert-CJ1KXcvv.mjs → assert-D1Eafmd_.mjs} +1 -1
  3. package/dist/{assert_equals-BV7oLMxm.mjs → assert_equals-Z6IcEbWU.mjs} +2 -28
  4. package/dist/assert_exists-cffSpJ7X.mjs +28 -0
  5. package/dist/assert_false-CPdRnc3E.mjs +23 -0
  6. package/dist/assert_greater_or_equal-BlafOF4-.mjs +31 -0
  7. package/dist/{assert_instance_of-Q9KTBnrf.mjs → assert_instance_of-mrXWWNY-.mjs} +1 -1
  8. package/dist/{assert_not_equals-EPJPoxuf.mjs → assert_not_equals-CpPK2t89.mjs} +2 -1
  9. package/dist/assert_rejects-CTptdYck.mjs +39 -0
  10. package/dist/{assert_strict_equals-Ec20AUwa.mjs → assert_strict_equals-CYvBmkpC.mjs} +2 -1
  11. package/dist/{assert_throws-NiFfqbnH.mjs → assert_throws-BYITOvPO.mjs} +2 -1
  12. package/dist/assertion_error-B-cx0fFA.mjs +31 -0
  13. package/dist/audience-CHQ-FlLD.mjs +95 -0
  14. package/dist/{builder-DonuxRl_.mjs → builder-Bda6LK7A.mjs} +169 -14
  15. package/dist/{circuit-breaker-CBGyz5F7.mjs → circuit-breaker-XJ53md4S.mjs} +224 -17
  16. package/dist/circuit-breaker-test-utils-DW6YC_Pe.mjs +14 -0
  17. package/dist/{client-CjF5IYQS.mjs → client-CEnNz8O7.mjs} +144 -8
  18. package/dist/{codec-BBga-FWI.mjs → codec-C6rTrpIp.mjs} +26 -3
  19. package/dist/{collection-BIhlldqK.mjs → collection-CnnP1UAv.mjs} +1 -1
  20. package/dist/compat/mod.cjs +1 -1
  21. package/dist/compat/mod.d.cts +1 -1
  22. package/dist/compat/mod.d.ts +1 -1
  23. package/dist/compat/mod.js +1 -1
  24. package/dist/compat/outgoing-jsonld.test.mjs +71 -3
  25. package/dist/compat/public-audience.test.mjs +14 -5
  26. package/dist/compat/secured-document.test.d.mts +2 -0
  27. package/dist/compat/secured-document.test.mjs +58 -0
  28. package/dist/compat/transformers.test.mjs +5 -5
  29. package/dist/compound-proof-B-9mXxFW.mjs +989 -0
  30. package/dist/{context-Zii1f5xS.d.ts → context-7NyiqIcg.d.ts} +1600 -25
  31. package/dist/{context-BJXxPElm.mjs → context-8Tu9kea0.mjs} +21 -63
  32. package/dist/{context-lxX6I7as.d.cts → context-tsYPJ2Ew.d.cts} +1600 -25
  33. package/dist/{deno-BHzKn9Ig.mjs → deno-oR5aJKpU.mjs} +1 -1
  34. package/dist/dist-DJQM4u7i.mjs +189 -0
  35. package/dist/docloader-DBoYbvYW.mjs +87 -0
  36. package/dist/{esm-yYQfPhIY.mjs → esm-BHXXx4Wx.mjs} +13 -6
  37. package/dist/federation/audience.test.d.mts +2 -0
  38. package/dist/federation/audience.test.mjs +130 -0
  39. package/dist/federation/builder.test.mjs +54 -5
  40. package/dist/federation/circuit-breaker.test.mjs +838 -7
  41. package/dist/federation/collection.test.mjs +3 -3
  42. package/dist/federation/compound-fanout.test.d.mts +2 -0
  43. package/dist/federation/compound-fanout.test.mjs +228 -0
  44. package/dist/federation/handler-compound-proof.test.d.mts +2 -0
  45. package/dist/federation/handler-compound-proof.test.mjs +384 -0
  46. package/dist/federation/handler.test.mjs +385 -15
  47. package/dist/federation/hashlink-media.test.d.mts +2 -0
  48. package/dist/federation/hashlink-media.test.mjs +422 -0
  49. package/dist/federation/idempotency.test.mjs +6 -6
  50. package/dist/federation/inbox-report.test.d.mts +2 -0
  51. package/dist/federation/inbox-report.test.mjs +475 -0
  52. package/dist/federation/inbox.test.mjs +3 -3
  53. package/dist/federation/keycache.test.mjs +215 -14
  54. package/dist/federation/kv.test.mjs +3 -3
  55. package/dist/federation/metrics.test.mjs +5 -5
  56. package/dist/federation/middleware.test.mjs +967 -35
  57. package/dist/federation/mod.cjs +18 -261
  58. package/dist/federation/mod.d.cts +5 -5
  59. package/dist/federation/mod.d.ts +5 -5
  60. package/dist/federation/mod.js +279 -1
  61. package/dist/federation/mq.test.mjs +123 -47
  62. package/dist/federation/negotiation.test.mjs +4 -4
  63. package/dist/federation/outgoing-proof.test.d.mts +2 -0
  64. package/dist/federation/outgoing-proof.test.mjs +931 -0
  65. package/dist/federation/portable-actor.test.d.mts +2 -0
  66. package/dist/federation/portable-actor.test.mjs +496 -0
  67. package/dist/federation/portable-chain.test.d.mts +2 -0
  68. package/dist/federation/portable-chain.test.mjs +370 -0
  69. package/dist/federation/portable-collection.test.d.mts +2 -0
  70. package/dist/federation/portable-collection.test.mjs +832 -0
  71. package/dist/federation/portable-delivery.test.d.mts +2 -0
  72. package/dist/federation/portable-delivery.test.mjs +746 -0
  73. package/dist/federation/portable-gateway-keys.test.d.mts +2 -0
  74. package/dist/federation/portable-gateway-keys.test.mjs +686 -0
  75. package/dist/federation/portable-inbox.test.d.mts +2 -0
  76. package/dist/federation/portable-inbox.test.mjs +1535 -0
  77. package/dist/federation/portable-parse-uri.test.d.mts +2 -0
  78. package/dist/federation/portable-parse-uri.test.mjs +348 -0
  79. package/dist/federation/portable-route-activity.test.d.mts +2 -0
  80. package/dist/federation/portable-route-activity.test.mjs +259 -0
  81. package/dist/federation/portable.test.d.mts +2 -0
  82. package/dist/federation/portable.test.mjs +1072 -0
  83. package/dist/federation/retry.test.mjs +4 -3
  84. package/dist/federation/router.test.mjs +9 -159
  85. package/dist/federation/send.test.mjs +245 -27
  86. package/dist/federation/tasks/codec.test.mjs +4 -4
  87. package/dist/federation/tasks/enqueue.test.mjs +42 -4
  88. package/dist/federation/tasks/tasks.test.mjs +7 -6
  89. package/dist/federation/temporal.test.mjs +4 -4
  90. package/dist/federation/webfinger.test.mjs +275 -12
  91. package/dist/{middleware-FrSMAYEx.cjs → federation-CkZiAcWY.cjs} +8640 -3722
  92. package/dist/http-B5JT115n.cjs +10630 -0
  93. package/dist/http-DydWCB7R.js +6527 -0
  94. package/dist/{http-CPVMUgsW.mjs → http-siciWkmx.mjs} +397 -168
  95. package/dist/key-BspPiXqn.mjs +951 -0
  96. package/dist/key-C1nv7_jD.d.ts +1125 -0
  97. package/dist/key-DYxuyd-s.d.cts +1125 -0
  98. package/dist/key-owner-evidence-APB6NpSk.mjs +108 -0
  99. package/dist/keycache-Da8A8mZa.mjs +200 -0
  100. package/dist/{kv-D6hNiMTK.d.ts → kv-B2mzK5tl.d.cts} +3 -2
  101. package/dist/{kv-gJ8LYbxX.d.cts → kv-B2mzK5tl.d.ts} +3 -2
  102. package/dist/kv-DBRWVx3Q.mjs +104 -0
  103. package/dist/{kv-cache-KyGqJqSp.mjs → kv-cache-BF1hnW65.mjs} +1 -1
  104. package/dist/{kv-cache-5bnX3-Jq.js → kv-cache-CTQr1tbn.js} +54 -16
  105. package/dist/{kv-cache-DQ29C4XL.cjs → kv-cache-DCac1nEU.cjs} +55 -19
  106. package/dist/{ld-Cj9F_5AP.mjs → ld-CzaP-hJl.mjs} +44 -12
  107. package/dist/map-local-context-conflict-DOYEEJ3u.mjs +204 -0
  108. package/dist/map-local-create-note-B0SeXJms.mjs +214 -0
  109. package/dist/{metrics-DMk-nTxI.mjs → metrics-CYg9yxBL.mjs} +3 -2
  110. package/dist/metrics-DVKYY6K1.d.cts +36 -0
  111. package/dist/metrics-DoRtI0qU.d.ts +36 -0
  112. package/dist/{middleware-Y-kpsp6S.js → middleware-CsDDXKuL.js} +5493 -1103
  113. package/dist/{middleware-CvwTUgnP.mjs → middleware-D7_LO-Vs.mjs} +3157 -272
  114. package/dist/{middleware-CEctxBN6.mjs → middleware-DWjrfpcz.mjs} +1 -1
  115. package/dist/{mod-gOlPDuT8.d.cts → mod-BojP2Bw3.d.cts} +1 -1
  116. package/dist/mod-CavhAgLw.d.cts +320 -0
  117. package/dist/mod-CtpF2BU5.d.ts +320 -0
  118. package/dist/{mod-BtEaIEcD.d.cts → mod-DEZXwV6J.d.ts} +10 -4
  119. package/dist/{mod-UMmveiO0.d.ts → mod-Dw-nPxQh.d.cts} +10 -4
  120. package/dist/{mod-DLc2Ot6f.d.ts → mod-Xj5P4s4d.d.ts} +1 -1
  121. package/dist/mod.cjs +40 -39
  122. package/dist/mod.d.cts +10 -11
  123. package/dist/mod.d.ts +10 -11
  124. package/dist/mod.js +9 -9
  125. package/dist/{mq-llHKivST.mjs → mq-Dm1Wmtm-.mjs} +6 -0
  126. package/dist/nodeinfo/client.test.mjs +139 -4
  127. package/dist/nodeinfo/handler.test.mjs +5 -5
  128. package/dist/nodeinfo/mod.cjs +1 -1
  129. package/dist/nodeinfo/mod.d.cts +1 -1
  130. package/dist/nodeinfo/mod.d.ts +1 -1
  131. package/dist/nodeinfo/mod.js +1 -1
  132. package/dist/nodeinfo/types.test.mjs +4 -4
  133. package/dist/otel/exporter.test.mjs +3 -3
  134. package/dist/otel/mod.cjs +0 -1
  135. package/dist/otel/mod.d.cts +1 -1
  136. package/dist/otel/mod.d.ts +1 -1
  137. package/dist/{outgoing-jsonld-URuqsICz.mjs → outgoing-jsonld-4IJqPYos.mjs} +12 -10
  138. package/dist/{owner-DEvZuyOE.d.cts → owner-BF0L9cMt.d.cts} +11 -0
  139. package/dist/{owner-CnngXDNJ.d.ts → owner-BSzr5-I8.d.ts} +11 -0
  140. package/dist/owner-DYgfTFHy.mjs +267 -0
  141. package/dist/portable-collection-BpsVsRov.js +670 -0
  142. package/dist/portable-collection-C74P1vtC.mjs +418 -0
  143. package/dist/portable-collection-DZd2bm7M.cjs +686 -0
  144. package/dist/portable-key-BIMLrAP4.mjs +549 -0
  145. package/dist/portable-key-id-Ca30-hOa.mjs +248 -0
  146. package/dist/proof-Dxy6ByqI.mjs +1299 -0
  147. package/dist/{public-audience-Cr5MsnMq.mjs → public-audience--klBIOk1.mjs} +15 -10
  148. package/dist/router-u5O5KCQr.mjs +156 -0
  149. package/dist/runtime/mod.cjs +1 -1
  150. package/dist/runtime/mod.js +1 -1
  151. package/dist/secured-document-CxzZZ9nL.mjs +35 -0
  152. package/dist/send-C4dDpAxm.mjs +495 -0
  153. package/dist/sig/accept.test.mjs +1 -1
  154. package/dist/sig/compound-proof-discovery.test.d.mts +2 -0
  155. package/dist/sig/compound-proof-discovery.test.mjs +331 -0
  156. package/dist/sig/compound-proof-producer.test.d.mts +2 -0
  157. package/dist/sig/compound-proof-producer.test.mjs +107 -0
  158. package/dist/sig/compound-proof-verification.test.d.mts +2 -0
  159. package/dist/sig/compound-proof-verification.test.mjs +833 -0
  160. package/dist/sig/compound-proof.test.d.mts +2 -0
  161. package/dist/sig/compound-proof.test.mjs +411 -0
  162. package/dist/sig/http.test.mjs +702 -31
  163. package/dist/sig/key-owner-evidence.test.d.mts +2 -0
  164. package/dist/sig/key-owner-evidence.test.mjs +100 -0
  165. package/dist/sig/key.test.mjs +220 -8
  166. package/dist/sig/ld.test.mjs +65 -7
  167. package/dist/sig/mitra-receive-interop.test.d.mts +2 -0
  168. package/dist/sig/mitra-receive-interop.test.mjs +260 -0
  169. package/dist/sig/mitra-send-interop.test.d.mts +2 -0
  170. package/dist/sig/mitra-send-interop.test.mjs +461 -0
  171. package/dist/sig/mod.cjs +18 -16
  172. package/dist/sig/mod.d.cts +4 -4
  173. package/dist/sig/mod.d.ts +4 -4
  174. package/dist/sig/mod.js +3 -3
  175. package/dist/sig/owner.test.mjs +108 -9
  176. package/dist/sig/portable-collection.test.d.mts +2 -0
  177. package/dist/sig/portable-collection.test.mjs +697 -0
  178. package/dist/sig/portable-interop.test.d.mts +2 -0
  179. package/dist/sig/portable-interop.test.mjs +156 -0
  180. package/dist/sig/portable-key-id.test.d.mts +2 -0
  181. package/dist/sig/portable-key-id.test.mjs +67 -0
  182. package/dist/sig/portable-key.test.d.mts +2 -0
  183. package/dist/sig/portable-key.test.mjs +1439 -0
  184. package/dist/sig/proof.test.mjs +1753 -16
  185. package/dist/sig/tootik-request-interop.test.d.mts +2 -0
  186. package/dist/sig/tootik-request-interop.test.mjs +190 -0
  187. package/dist/sig/tootik-send-interop.test.d.mts +2 -0
  188. package/dist/sig/tootik-send-interop.test.mjs +331 -0
  189. package/dist/sig/verification.test.d.mts +2 -0
  190. package/dist/sig/verification.test.mjs +310 -0
  191. package/dist/std__assert-DvMDRPes.mjs +8 -0
  192. package/dist/{testing-CO7ftO8A.mjs → tasks-IG3pHfvZ.mjs} +2 -2
  193. package/dist/{temporal-fsXO5IvD.mjs → temporal-D0KJI4NF.mjs} +3 -3
  194. package/dist/testing/context.test.d.mts +2 -0
  195. package/dist/testing/context.test.mjs +19 -0
  196. package/dist/testing/kv.test.d.mts +2 -0
  197. package/dist/testing/kv.test.mjs +101 -0
  198. package/dist/testing/mod.d.mts +1617 -48
  199. package/dist/testing/mod.mjs +6 -3
  200. package/dist/testing-t8xDeJZ5.mjs +8 -0
  201. package/dist/{transformers-DGwl-hD1.cjs → transformers-BalCrSsr.cjs} +0 -1
  202. package/dist/{types-DC1pNgJA.js → types-_S2_MeLF.js} +144 -8
  203. package/dist/{types-CPym83zc.cjs → types-poIao40f.cjs} +160 -7
  204. package/dist/utils/docloader.test.mjs +530 -8
  205. package/dist/utils/kv-cache.test.mjs +2 -2
  206. package/dist/utils/mod.cjs +1 -1
  207. package/dist/utils/mod.d.cts +1 -1
  208. package/dist/utils/mod.d.ts +1 -1
  209. package/dist/utils/mod.js +1 -1
  210. package/dist/verification-C3954hQh.mjs +5 -0
  211. package/dist/verification-Dpfcw8ml.mjs +105 -0
  212. package/dist/vocab/cjs.test.mjs +1 -1
  213. package/package.json +8 -8
  214. package/dist/assert_rejects-EgstcQy5.mjs +0 -82
  215. package/dist/docloader-e1khNUKy.mjs +0 -49
  216. package/dist/http-CsmQ-BQ4.js +0 -2483
  217. package/dist/http-VyDTd4G3.d.cts +0 -553
  218. package/dist/http-hlIjmewS.cjs +0 -2694
  219. package/dist/http-lf8Hsd91.d.ts +0 -553
  220. package/dist/key-DfPsX8hv.mjs +0 -453
  221. package/dist/keycache-_DHqz5z_.mjs +0 -97
  222. package/dist/metrics-CoUJLQhZ.d.cts +0 -276
  223. package/dist/metrics-Dc4jR-IC.d.ts +0 -276
  224. package/dist/mod-B0hW12_O.d.cts +0 -307
  225. package/dist/mod-COIAjwRS.d.ts +0 -307
  226. package/dist/owner-Br9YYzn_.mjs +0 -120
  227. package/dist/proof-B4ngoaNQ.mjs +0 -316
  228. package/dist/proof-B6E0XUXJ.js +0 -1415
  229. package/dist/proof-NN4xCQGL.cjs +0 -1562
  230. package/dist/send-CxHKUNzv.mjs +0 -239
  231. package/dist/std__assert-B2NnY9bq.mjs +0 -79
  232. package/dist/temporal-polyfill-BCdTHexd.cjs +0 -3565
  233. /package/dist/{accept-8PiolDP7.mjs → accept-CPVqki78.mjs} +0 -0
  234. /package/dist/{activity-listener-BtTSxZWA.mjs → activity-listener-C-BSbKI7.mjs} +0 -0
  235. /package/dist/{client-CAM_bQXx.d.cts → client-D-XP_Ccb.d.cts} +0 -0
  236. /package/dist/{client-CSddvgWN.d.ts → client-D-XP_Ccb.d.ts} +0 -0
  237. /package/dist/{keys-Dbyd7bhl.mjs → keys-DIJNHW-Y.mjs} +0 -0
  238. /package/dist/{kv-CXBAok3k.mjs → kv-DCHkaJ9-.mjs} +0 -0
  239. /package/dist/{negotiation-RuwrPFqi.mjs → negotiation-CCocWT_N.mjs} +0 -0
  240. /package/dist/{retry-DgFqeS5k.mjs → retry-zX5xw5e0.mjs} +0 -0
  241. /package/dist/{chunk-D5IdezsZ.js → rolldown-runtime-BC-Eo5Mj.js} +0 -0
  242. /package/dist/{chunk-Czpeue4M.cjs → rolldown-runtime-Dmm3t92W.cjs} +0 -0
  243. /package/dist/{transformers-DXze6FUB.js → transformers-DAtXl_Kc.js} +0 -0
  244. /package/dist/{types-CrtdVuDP.mjs → types-DE_LV1JB.mjs} +0 -0
@@ -0,0 +1,931 @@
1
+ import { Temporal } from "temporal-polyfill";
2
+ import "urlpattern-polyfill";
3
+ globalThis.addEventListener = () => {};
4
+ import { t as esm_default } from "../esm-BHXXx4Wx.mjs";
5
+ import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
6
+ import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
7
+ import "../std__assert-DvMDRPes.mjs";
8
+ import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
9
+ import { t as assert } from "../assert-D1Eafmd_.mjs";
10
+ import { n as exportJwk } from "../key-BspPiXqn.mjs";
11
+ import { s as detachSignature } from "../ld-CzaP-hJl.mjs";
12
+ import { a as signObject } from "../proof-Dxy6ByqI.mjs";
13
+ import { i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
14
+ import { C as getEmbeddedFirstGateway, S as getCompactRootId, c as assertPortableActorActivity, n as FederationImpl, o as createFederation, w as warnCompatibleIdsInJson } from "../middleware-D7_LO-Vs.mjs";
15
+ import { o as verifyCompoundPortableObjectProofs } from "../compound-proof-B-9mXxFW.mjs";
16
+ import { configure, reset } from "@logtape/logtape";
17
+ import { Accept, Announce, Create, Follow, Note, Person } from "@fedify/vocab";
18
+ import { exportDidKey, parseIri } from "@fedify/vocab-runtime";
19
+ import { mockDocumentLoader, test } from "@fedify/fixture";
20
+ //#region src/federation/outgoing-proof.test.ts
21
+ const options = {
22
+ contextLoader: mockDocumentLoader,
23
+ documentLoader: mockDocumentLoader
24
+ };
25
+ const created = Temporal.Instant.from("2023-02-24T23:36:38Z");
26
+ const childContext = [
27
+ "https://www.w3.org/ns/activitystreams",
28
+ "https://w3id.org/security/data-integrity/v1",
29
+ { ex: "https://example.com/ns#" }
30
+ ];
31
+ const limits = {
32
+ maxDepth: 64,
33
+ maxMaps: 1e4,
34
+ maxProofs: 32,
35
+ maxBytes: 10 * 1024 * 1024
36
+ };
37
+ const recipient = {
38
+ id: new URL("https://example.com/users/bob"),
39
+ inboxId: new URL("https://example.com/inbox")
40
+ };
41
+ const rsaKey = {
42
+ keyId: rsaPublicKey2.id,
43
+ privateKey: rsaPrivateKey2
44
+ };
45
+ async function didKey(pair) {
46
+ const { privateKey, publicKey } = pair ?? await crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"]);
47
+ const did = await exportDidKey(publicKey);
48
+ return {
49
+ did,
50
+ keyId: new URL(`${did}#${did.substring(8)}`),
51
+ privateKey,
52
+ publicKey
53
+ };
54
+ }
55
+ function sender(key) {
56
+ return {
57
+ keyId: key.keyId,
58
+ privateKey: key.privateKey
59
+ };
60
+ }
61
+ async function signedChild(owner) {
62
+ return await signObject(new Note({
63
+ id: parseIri(`ap+ef61://${owner.did}/objects/${crypto.randomUUID()}`),
64
+ attribution: parseIri(`ap+ef61://${owner.did}/actor`),
65
+ content: "A portable note"
66
+ }), owner.privateKey, owner.keyId, {
67
+ ...options,
68
+ context: childContext,
69
+ created
70
+ });
71
+ }
72
+ function portableCreate(owner, object) {
73
+ return new Create({
74
+ id: parseIri(`ap+ef61://${owner.did}/activities/${crypto.randomUUID()}`),
75
+ actor: parseIri(`ap+ef61://${owner.did}/actor`),
76
+ object
77
+ });
78
+ }
79
+ function httpCreate(object) {
80
+ return new Create({
81
+ id: new URL(`https://example.com/activities/${crypto.randomUUID()}`),
82
+ actor: new URL("https://example.com/users/alice"),
83
+ object
84
+ });
85
+ }
86
+ function createQueue() {
87
+ const queued = [];
88
+ return {
89
+ queued,
90
+ queue: {
91
+ enqueue(message) {
92
+ queued.push(message);
93
+ return Promise.resolve();
94
+ },
95
+ listen() {
96
+ return Promise.resolve();
97
+ }
98
+ }
99
+ };
100
+ }
101
+ function createTestFederation(queue) {
102
+ const federation = new FederationImpl({
103
+ kv: new MemoryKvStore(),
104
+ queue,
105
+ manuallyStartQueue: true,
106
+ contextLoaderFactory: () => mockDocumentLoader,
107
+ documentLoaderFactory: () => mockDocumentLoader
108
+ });
109
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox");
110
+ return federation;
111
+ }
112
+ /**
113
+ * Captures what is POSTed to the recipient's inbox while `run` executes.
114
+ */
115
+ async function capture(run) {
116
+ const bodies = [];
117
+ const requests = [];
118
+ esm_default.spyGlobal();
119
+ try {
120
+ esm_default.post(recipient.inboxId.href, async (cl) => {
121
+ const body = await cl.request.text();
122
+ requests.push(new Request(cl.request.url, {
123
+ method: cl.request.method,
124
+ headers: cl.request.headers,
125
+ body
126
+ }));
127
+ bodies.push(JSON.parse(body));
128
+ return new Response(null, { status: 202 });
129
+ });
130
+ await run();
131
+ } finally {
132
+ esm_default.hardReset();
133
+ }
134
+ return {
135
+ bodies,
136
+ requests
137
+ };
138
+ }
139
+ function proofOf(document) {
140
+ const proof = document.proof;
141
+ assert(proof != null && typeof proof === "object" && !Array.isArray(proof), `expected exactly one direct proof, got ${JSON.stringify(proof)}`);
142
+ return proof;
143
+ }
144
+ async function assertCompoundVerifies(body) {
145
+ const result = await verifyCompoundPortableObjectProofs(detachSignature(body), limits, options);
146
+ assertEquals(result.status, "ok");
147
+ assert(result.status === "ok" && result.verified);
148
+ }
149
+ test("a portable activity with one Ed25519 key gets one direct proof", async () => {
150
+ const owner = await didKey();
151
+ const child = await signedChild(await didKey());
152
+ const ctx = createTestFederation().createContext(new URL("https://example.com/"));
153
+ const { bodies, requests } = await capture(() => ctx.sendActivity([rsaKey, sender(owner)], recipient, portableCreate(owner, child)));
154
+ assertEquals(bodies.length, 1);
155
+ assertEquals(proofOf(bodies[0]).verificationMethod, owner.keyId.href);
156
+ assert(requests[0].headers.has("Signature"));
157
+ assertEquals(bodies[0].signature, void 0);
158
+ await assertCompoundVerifies(bodies[0]);
159
+ });
160
+ test("a portable activity is signed only by the key matching its DID", async () => {
161
+ const owner = await didKey();
162
+ const other = await didKey();
163
+ const childOwner = await didKey();
164
+ const child = await signedChild(childOwner);
165
+ for (const keys of [[other, owner], [owner, other]]) {
166
+ const ctx = createTestFederation().createContext(new URL("https://example.com/"));
167
+ const { bodies, requests } = await capture(() => ctx.sendActivity([
168
+ rsaKey,
169
+ ...keys.map(sender),
170
+ sender(childOwner)
171
+ ], recipient, portableCreate(owner, child)));
172
+ assertEquals(bodies.length, 1);
173
+ assertEquals(proofOf(bodies[0]).verificationMethod, owner.keyId.href);
174
+ assert(requests[0].headers.has("Signature"));
175
+ const embedded = bodies[0].object;
176
+ assertEquals(embedded["@context"], childContext);
177
+ assertEquals(proofOf(embedded).verificationMethod, childOwner.keyId.href);
178
+ await assertCompoundVerifies(bodies[0]);
179
+ }
180
+ });
181
+ test("a portable activity without exactly one DID-matching key is rejected", async () => {
182
+ const owner = await didKey();
183
+ const child = await signedChild(await didKey());
184
+ const cases = [
185
+ [
186
+ "no key matches",
187
+ [sender(await didKey()), sender(await didKey())],
188
+ "none of its 2 Ed25519 keys"
189
+ ],
190
+ [
191
+ "a same-origin key ID is not a DID URL",
192
+ [{
193
+ keyId: parseIri(`ap+ef61://${owner.did}/actor#main-key`),
194
+ privateKey: owner.privateKey
195
+ }, sender(await didKey())],
196
+ "none of its 1 Ed25519 keys"
197
+ ],
198
+ [
199
+ "several keys match",
200
+ [sender(owner), {
201
+ keyId: new URL(`${owner.did}#second`),
202
+ privateKey: owner.privateKey
203
+ }],
204
+ "2 of its Ed25519 keys"
205
+ ]
206
+ ];
207
+ for (const [name, keys, message] of cases) {
208
+ const ctx = createTestFederation().createContext(new URL("https://example.com/"));
209
+ const { bodies } = await capture(() => assertRejects(() => ctx.sendActivity([rsaKey, ...keys], recipient, portableCreate(owner, child)), TypeError, message));
210
+ assertEquals(bodies.length, 0, name);
211
+ }
212
+ });
213
+ test("a non-portable activity embedding portable objects needs one key", async () => {
214
+ const child = await signedChild(await didKey());
215
+ const ctx = createTestFederation().createContext(new URL("https://example.com/"));
216
+ const keys = [sender(await didKey()), sender(await didKey())];
217
+ assertEquals((await capture(() => assertRejects(() => ctx.sendActivity([rsaKey, ...keys], recipient, httpCreate(child)), TypeError, "exactly one Ed25519 key"))).bodies.length, 0);
218
+ const key = {
219
+ keyId: ed25519PublicKey.id,
220
+ privateKey: ed25519PrivateKey
221
+ };
222
+ const accepted = await capture(() => ctx.sendActivity([rsaKey, key], recipient, httpCreate(child)));
223
+ assertEquals(accepted.bodies.length, 1);
224
+ assertEquals(proofOf(accepted.bodies[0]).verificationMethod, ed25519PublicKey.id.href);
225
+ });
226
+ test("a non-portable activity embedding compatible-ID objects needs one key", async () => {
227
+ const owner = await didKey();
228
+ const compatibleId = (path) => new URL(`https://gw.example/.well-known/apgateway/${owner.did}${path}`);
229
+ const child = await signObject(new Note({
230
+ id: compatibleId(`/objects/${crypto.randomUUID()}`),
231
+ attribution: compatibleId("/actor"),
232
+ content: "A portable note"
233
+ }), owner.privateKey, owner.keyId, {
234
+ ...options,
235
+ context: childContext,
236
+ created
237
+ });
238
+ const ctx = createTestFederation().createContext(new URL("https://example.com/"));
239
+ const keys = [sender(await didKey()), sender(await didKey())];
240
+ assertEquals((await capture(() => assertRejects(() => ctx.sendActivity([rsaKey, ...keys], recipient, httpCreate(child)), TypeError, "exactly one Ed25519 key"))).bodies.length, 0);
241
+ const key = {
242
+ keyId: ed25519PublicKey.id,
243
+ privateKey: ed25519PrivateKey
244
+ };
245
+ const accepted = await capture(() => ctx.sendActivity([rsaKey, key], recipient, httpCreate(child)));
246
+ assertEquals(accepted.bodies.length, 1);
247
+ await assertCompoundVerifies(accepted.bodies[0]);
248
+ });
249
+ test("an activity without portable objects is still signed by every key", async () => {
250
+ const ctx = createTestFederation().createContext(new URL("https://example.com/"));
251
+ const keys = [await didKey(), await didKey()];
252
+ const { bodies } = await capture(() => ctx.sendActivity([rsaKey, ...keys.map(sender)], recipient, httpCreate(new Note({ content: "Hello" }))));
253
+ assertEquals(bodies.length, 1);
254
+ const proofs = bodies[0].proof;
255
+ assert(Array.isArray(proofs));
256
+ assertEquals(proofs.map((proof) => proof.verificationMethod), keys.map((key) => key.keyId.href));
257
+ });
258
+ test("a pre-signed portable activity is not re-signed", async () => {
259
+ const owner = await didKey();
260
+ const activity = await signObject(portableCreate(owner, await signedChild(await didKey())), owner.privateKey, owner.keyId, {
261
+ ...options,
262
+ created
263
+ });
264
+ const ctx = createTestFederation().createContext(new URL("https://example.com/"));
265
+ const keys = [sender(await didKey()), sender(await didKey())];
266
+ const { bodies } = await capture(() => ctx.sendActivity([rsaKey, ...keys], recipient, activity));
267
+ assertEquals(bodies.length, 1);
268
+ assertEquals(proofOf(bodies[0]).verificationMethod, owner.keyId.href);
269
+ await assertCompoundVerifies(bodies[0]);
270
+ });
271
+ test("a portable activity carrying a proof set is not delivered", async () => {
272
+ const owner = await didKey();
273
+ const second = {
274
+ ...owner,
275
+ keyId: new URL(`${owner.did}#second`)
276
+ };
277
+ let activity = portableCreate(owner, await signedChild(owner));
278
+ for (const key of [owner, second]) activity = await signObject(activity, key.privateKey, key.keyId, {
279
+ ...options,
280
+ created
281
+ });
282
+ assertEquals((await capture(() => assertRejects(() => createTestFederation().createContext(new URL("https://example.com/")).sendActivity(rsaKey, recipient, activity), TypeError, "JSON Pointer \"/proof\""))).bodies.length, 0);
283
+ const outbox = createQueue();
284
+ await assertRejects(() => createTestFederation(outbox.queue).createContext(new URL("https://example.com/")).sendActivity(rsaKey, recipient, activity, { fanout: "skip" }), TypeError, "JSON Pointer \"/proof\"");
285
+ assertEquals(outbox.queued.length, 0);
286
+ const fanout = createQueue();
287
+ await assertRejects(() => createTestFederation(fanout.queue).createContext(new URL("https://example.com/")).sendActivity(rsaKey, recipient, activity, { fanout: "force" }), TypeError, "JSON Pointer \"/proof\"");
288
+ assertEquals(fanout.queued.length, 0);
289
+ });
290
+ test("a proof set on an embedded map is not delivered either", async () => {
291
+ const owner = await didKey();
292
+ let child = await signedChild(owner);
293
+ child = await signObject(child, owner.privateKey, new URL(`${owner.did}#b`), {
294
+ ...options,
295
+ created
296
+ });
297
+ const ctx = createTestFederation().createContext(new URL("https://example.com/"));
298
+ const { bodies } = await capture(() => assertRejects(() => ctx.sendActivity(rsaKey, recipient, httpCreate(child)), TypeError, "JSON Pointer \"/object/proof\""));
299
+ assertEquals(bodies.length, 0);
300
+ });
301
+ test("a received signed object rebuilt inside a portable activity is not delivered", async () => {
302
+ const followerKey = await didKey();
303
+ const signedFollow = await signObject(new Follow({
304
+ id: new URL("https://example.com/follows/1"),
305
+ actor: new URL("https://example.com/users/bob"),
306
+ object: parseIri("ap+ef61://did:key:z6Mkabc/actor")
307
+ }), followerKey.privateKey, new URL("https://example.com/users/bob#key"), {
308
+ ...options,
309
+ created
310
+ });
311
+ const follow = await Follow.fromJsonLd(await signedFollow.toJsonLd({
312
+ format: "compact",
313
+ ...options
314
+ }), options);
315
+ const owner = await didKey();
316
+ const accept = (object) => new Accept({
317
+ id: parseIri(`ap+ef61://${owner.did}/accepts/${crypto.randomUUID()}`),
318
+ actor: parseIri(`ap+ef61://${owner.did}/actor`),
319
+ object
320
+ });
321
+ const unsigned = accept(follow);
322
+ const presigned = await signObject(accept(follow), owner.privateKey, owner.keyId, {
323
+ ...options,
324
+ created
325
+ });
326
+ const cases = [[unsigned, [rsaKey, sender(owner)]], [presigned, [rsaKey]]];
327
+ for (const [activity, keys] of cases) {
328
+ assertEquals((await capture(() => assertRejects(() => createTestFederation().createContext(new URL("https://example.com/")).sendActivity(keys, recipient, activity), TypeError, "JSON Pointer \"/object\""))).bodies.length, 0);
329
+ const fanout = createQueue();
330
+ await assertRejects(() => createTestFederation(fanout.queue).createContext(new URL("https://example.com/")).sendActivity(keys, recipient, activity, { fanout: "force" }), TypeError, "JSON Pointer \"/object\"");
331
+ assertEquals(fanout.queued.length, 0);
332
+ }
333
+ const { bodies } = await capture(() => createTestFederation().createContext(new URL("https://example.com/")).sendActivity([rsaKey, sender(owner)], recipient, accept(follow.id)));
334
+ assertEquals(bodies.length, 1);
335
+ await assertCompoundVerifies(bodies[0]);
336
+ });
337
+ test("forced fanout selects the DID-matching key before enqueueing", async () => {
338
+ const owner = await didKey();
339
+ const childOwner = await didKey();
340
+ const child = await signedChild(childOwner);
341
+ const { queue, queued } = createQueue();
342
+ const federation = createTestFederation(queue);
343
+ const ctx = federation.createContext(new URL("https://example.com/"));
344
+ const { bodies } = await capture(async () => {
345
+ await ctx.sendActivity([
346
+ rsaKey,
347
+ sender(await didKey()),
348
+ sender(owner)
349
+ ], recipient, portableCreate(owner, child), { fanout: "force" });
350
+ assertEquals(queued.length, 1);
351
+ const message = queued[0];
352
+ assertEquals(message.type, "fanout");
353
+ assertEquals(message.keys.length, 3);
354
+ assertEquals(message.keys[0].keyId, rsaKey.keyId.href);
355
+ for (let i = 0; i < queued.length; i++) await federation.processQueuedTask(void 0, queued[i]);
356
+ });
357
+ assertEquals(bodies.length, 1);
358
+ assertEquals(proofOf(bodies[0]).verificationMethod, owner.keyId.href);
359
+ const embedded = bodies[0].object;
360
+ assertEquals(embedded["@context"], childContext);
361
+ assertEquals(proofOf(embedded).verificationMethod, childOwner.keyId.href);
362
+ await assertCompoundVerifies(bodies[0]);
363
+ });
364
+ test("the fanout worker selects one key for an unsigned portable activity", async () => {
365
+ const owner = await didKey();
366
+ const child = await signedChild(await didKey());
367
+ const federation = createTestFederation();
368
+ const message = async (activity) => ({
369
+ type: "fanout",
370
+ id: crypto.randomUUID(),
371
+ baseUrl: "https://example.com",
372
+ keys: await Promise.all([
373
+ rsaKey,
374
+ sender(await didKey()),
375
+ sender(owner)
376
+ ].map(async (key) => ({
377
+ keyId: key.keyId.href,
378
+ privateKey: await exportJwk(key.privateKey)
379
+ }))),
380
+ inboxes: { [recipient.inboxId.href]: {
381
+ actorIds: [recipient.id.href],
382
+ sharedInbox: false
383
+ } },
384
+ activity: await activity.toJsonLd({
385
+ format: "compact",
386
+ ...options
387
+ }),
388
+ activityId: activity.id.href,
389
+ activityType: "https://www.w3.org/ns/activitystreams#Create",
390
+ traceContext: {}
391
+ });
392
+ const byId = new Create({
393
+ id: parseIri(`ap+ef61://${owner.did}/activities/${crypto.randomUUID()}`),
394
+ actor: parseIri(`ap+ef61://${owner.did}/actor`),
395
+ object: child.id
396
+ });
397
+ const { bodies } = await capture(async () => federation.processQueuedTask(void 0, await message(byId)));
398
+ assertEquals(bodies.length, 1);
399
+ assertEquals(proofOf(bodies[0]).verificationMethod, owner.keyId.href);
400
+ await assertCompoundVerifies(bodies[0]);
401
+ const embedded = await message(portableCreate(owner, child));
402
+ assertEquals((await capture(() => assertRejects(() => federation.processQueuedTask(void 0, embedded), TypeError, "JSON Pointer \"/object\""))).bodies.length, 0);
403
+ });
404
+ test("actor key pairs follow the compound-proof key selection too", async () => {
405
+ const dispatched = [];
406
+ const federation = createTestFederation();
407
+ federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => new Person({
408
+ id: ctx.getActorUri(identifier),
409
+ inbox: ctx.getInboxUri(identifier)
410
+ })).setKeyPairsDispatcher(() => dispatched);
411
+ const ctx = federation.createContext(new URL("https://example.com/"));
412
+ const owner = await didKey();
413
+ const child = await signedChild(await didKey());
414
+ const other = await didKey();
415
+ dispatched.push({
416
+ privateKey: rsaPrivateKey2,
417
+ publicKey: rsaPublicKey2.publicKey
418
+ }, {
419
+ privateKey: owner.privateKey,
420
+ publicKey: owner.publicKey
421
+ }, {
422
+ privateKey: other.privateKey,
423
+ publicKey: other.publicKey
424
+ });
425
+ assertEquals((await capture(() => assertRejects(() => ctx.sendActivity({ identifier: "alice" }, recipient, portableCreate(owner, child)), TypeError, "none of its 2 Ed25519 keys"))).bodies.length, 0);
426
+ dispatched.splice(2);
427
+ const presigned = await signObject(portableCreate(owner, child), owner.privateKey, owner.keyId, {
428
+ ...options,
429
+ created
430
+ });
431
+ const kept = await capture(() => ctx.sendActivity({ identifier: "alice" }, recipient, presigned));
432
+ assertEquals(kept.bodies.length, 1);
433
+ assertEquals(proofOf(kept.bodies[0]).verificationMethod, owner.keyId.href);
434
+ const ordinary = await signObject(httpCreate(new Note({ content: "Hello" })), owner.privateKey, owner.keyId, {
435
+ ...options,
436
+ created
437
+ });
438
+ const appended = await capture(() => ctx.sendActivity({ identifier: "alice" }, recipient, ordinary));
439
+ assertEquals(appended.bodies.length, 1);
440
+ const proofs = appended.bodies[0].proof;
441
+ assert(Array.isArray(proofs));
442
+ assertEquals(proofs.map((proof) => proof.verificationMethod), [owner.keyId.href, "https://example.com/users/alice#multikey-2"]);
443
+ });
444
+ test("a Fedify inbox accepts a portable activity Fedify produced", async () => {
445
+ const owner = await didKey();
446
+ const child = await signedChild(await didKey());
447
+ const ctx = createTestFederation().createContext(new URL("https://example.com/"));
448
+ const other = await didKey();
449
+ const { requests } = await capture(() => ctx.sendActivity([sender(other), sender(owner)], recipient, portableCreate(owner, child)));
450
+ assertEquals(requests.length, 1);
451
+ let received = 0;
452
+ const receiving = createFederation({
453
+ kv: new MemoryKvStore(),
454
+ contextLoaderFactory: () => mockDocumentLoader,
455
+ documentLoaderFactory: () => mockDocumentLoader
456
+ });
457
+ receiving.setActorDispatcher("/users/{identifier}", () => null);
458
+ receiving.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
459
+ received++;
460
+ });
461
+ assertEquals([(await receiving.fetch(requests[0], { contextData: void 0 })).status, received], [202, 1]);
462
+ });
463
+ const gateway = "https://gw.example";
464
+ function compatibleId(did, path, origin = gateway) {
465
+ return new URL(`${origin}/.well-known/apgateway/${did}${path}`);
466
+ }
467
+ function compatibleCreate(owner, object, { id, actor } = {}) {
468
+ return new Create({
469
+ id: id ?? compatibleId(owner.did, `/activities/${crypto.randomUUID()}`),
470
+ actor: actor ?? compatibleId(owner.did, "/actor"),
471
+ object
472
+ });
473
+ }
474
+ test("a compatible-ID activity is signed only by the key matching its DID", async () => {
475
+ const owner = await didKey();
476
+ const childOwner = await didKey();
477
+ const child = await signedChild(childOwner);
478
+ const ctx = createTestFederation().createContext(new URL("https://example.com/"));
479
+ const { bodies, requests } = await capture(async () => ctx.sendActivity([
480
+ rsaKey,
481
+ sender(await didKey()),
482
+ sender(owner),
483
+ sender(childOwner)
484
+ ], recipient, compatibleCreate(owner, child)));
485
+ assertEquals(bodies.length, 1);
486
+ assertEquals(proofOf(bodies[0]).verificationMethod, owner.keyId.href);
487
+ assert(requests[0].headers.has("Signature"));
488
+ assertEquals(bodies[0].signature, void 0);
489
+ await assertCompoundVerifies(bodies[0]);
490
+ });
491
+ test("a compatible-ID actor's activity needs an ID of the actor's DID", async () => {
492
+ const owner = await didKey();
493
+ const other = await didKey();
494
+ const cases = [
495
+ [
496
+ "a compatible activity ID of another DID",
497
+ compatibleCreate(owner, void 0, { id: compatibleId(other.did, "/activities/1") }),
498
+ "with the same DID"
499
+ ],
500
+ [
501
+ "an ap: activity ID of another DID",
502
+ compatibleCreate(owner, void 0, { id: parseIri(`ap://${other.did}/activities/1`) }),
503
+ "with the same DID"
504
+ ],
505
+ [
506
+ "an ordinary activity ID",
507
+ compatibleCreate(owner, void 0, { id: new URL("https://gw.example/activities/1") }),
508
+ "with the same DID"
509
+ ],
510
+ [
511
+ "a malformed compatible activity ID",
512
+ compatibleCreate(owner, void 0, { id: new URL(compatibleId(owner.did, "/activities/1").href + "?@gateway=https%3A%2F%2Fgw.example") }),
513
+ "with the same DID"
514
+ ],
515
+ [
516
+ "a malformed compatible actor ID",
517
+ compatibleCreate(owner, void 0, { actor: new URL(compatibleId(owner.did, "/actor").href + "?@gateway=https%3A%2F%2Fgw.example") }),
518
+ "malformed FEP-ef61 portable ID"
519
+ ],
520
+ [
521
+ "compatible actors of different DIDs",
522
+ new Create({
523
+ id: compatibleId(owner.did, "/activities/1"),
524
+ actors: [compatibleId(owner.did, "/actor"), compatibleId(other.did, "/actor")]
525
+ }),
526
+ "different DIDs"
527
+ ]
528
+ ];
529
+ for (const [name, activity, message] of cases) {
530
+ const ctx = createTestFederation().createContext(new URL("https://example.com/"));
531
+ const { bodies } = await capture(() => assertRejects(() => ctx.sendActivity([rsaKey, sender(owner)], recipient, activity), TypeError, message));
532
+ assertEquals(bodies.length, 0, name);
533
+ }
534
+ });
535
+ test("a compatible-ID activity without exactly one DID-matching key is rejected", async () => {
536
+ const owner = await didKey();
537
+ const cases = [
538
+ [
539
+ "no key matches",
540
+ [sender(await didKey())],
541
+ "none of its 1 Ed25519 keys"
542
+ ],
543
+ [
544
+ "only a gateway key",
545
+ [{
546
+ keyId: compatibleId(owner.did, "/actor#main-key"),
547
+ privateKey: owner.privateKey
548
+ }],
549
+ "none of its 0 Ed25519 keys"
550
+ ],
551
+ [
552
+ "several keys match",
553
+ [sender(owner), {
554
+ keyId: new URL(`${owner.did}#second`),
555
+ privateKey: owner.privateKey
556
+ }],
557
+ "2 of its Ed25519 keys"
558
+ ]
559
+ ];
560
+ for (const [name, keys, message] of cases) {
561
+ const ctx = createTestFederation().createContext(new URL("https://example.com/"));
562
+ const { bodies } = await capture(() => assertRejects(() => ctx.sendActivity([rsaKey, ...keys], recipient, compatibleCreate(owner)), TypeError, message));
563
+ assertEquals(bodies.length, 0, name);
564
+ }
565
+ });
566
+ test("compatible and ap: IDs of the same DID can be mixed", async () => {
567
+ const owner = await didKey();
568
+ const activities = [
569
+ compatibleCreate(owner, void 0, { id: parseIri(`ap://${owner.did}/activities/${crypto.randomUUID()}`) }),
570
+ compatibleCreate(owner, void 0, { actor: parseIri(`ap+ef61://${owner.did}/actor`) }),
571
+ compatibleCreate(owner, void 0, { id: compatibleId(owner.did, "/activities/1", "https://other.example") })
572
+ ];
573
+ for (const activity of activities) {
574
+ const ctx = createTestFederation().createContext(new URL("https://example.com/"));
575
+ const { bodies } = await capture(async () => ctx.sendActivity([
576
+ rsaKey,
577
+ sender(await didKey()),
578
+ sender(owner)
579
+ ], recipient, activity));
580
+ assertEquals(bodies.length, 1, activity.id?.href);
581
+ assertEquals(proofOf(bodies[0]).verificationMethod, owner.keyId.href);
582
+ }
583
+ });
584
+ test("a pre-signed compatible-ID activity is not re-signed", async () => {
585
+ const owner = await didKey();
586
+ const activity = await signObject(compatibleCreate(owner, await signedChild(await didKey())), owner.privateKey, owner.keyId, {
587
+ ...options,
588
+ created
589
+ });
590
+ const ctx = createTestFederation().createContext(new URL("https://example.com/"));
591
+ const { bodies } = await capture(async () => ctx.sendActivity([
592
+ rsaKey,
593
+ sender(await didKey()),
594
+ sender(await didKey())
595
+ ], recipient, activity));
596
+ assertEquals(bodies.length, 1);
597
+ assertEquals(proofOf(bodies[0]).verificationMethod, owner.keyId.href);
598
+ await assertCompoundVerifies(bodies[0]);
599
+ });
600
+ test("forced fanout selects the DID-matching key for a compatible-ID activity", async () => {
601
+ const owner = await didKey();
602
+ const { queue, queued } = createQueue();
603
+ const federation = createTestFederation(queue);
604
+ const ctx = federation.createContext(new URL("https://example.com/"));
605
+ const { bodies } = await capture(async () => {
606
+ await ctx.sendActivity([
607
+ rsaKey,
608
+ sender(await didKey()),
609
+ sender(owner)
610
+ ], recipient, compatibleCreate(owner, await signedChild(await didKey())), { fanout: "force" });
611
+ assertEquals(queued.length, 1);
612
+ for (let i = 0; i < queued.length; i++) await federation.processQueuedTask(void 0, queued[i]);
613
+ });
614
+ assertEquals(bodies.length, 1);
615
+ assertEquals(proofOf(bodies[0]).verificationMethod, owner.keyId.href);
616
+ await assertCompoundVerifies(bodies[0]);
617
+ });
618
+ test("a Fedify inbox accepts an activity Fedify produced for a compatible-ID actor", async () => {
619
+ const owner = await didKey();
620
+ const child = await signedChild(await didKey());
621
+ const ctx = createTestFederation().createContext(new URL("https://example.com/"));
622
+ const { requests } = await capture(async () => ctx.sendActivity([sender(await didKey()), sender(owner)], recipient, compatibleCreate(owner, child)));
623
+ assertEquals(requests.length, 1);
624
+ let received = 0;
625
+ const receiving = createFederation({
626
+ kv: new MemoryKvStore(),
627
+ contextLoaderFactory: () => mockDocumentLoader,
628
+ documentLoaderFactory: () => mockDocumentLoader
629
+ });
630
+ receiving.setActorDispatcher("/users/{identifier}", () => null);
631
+ receiving.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
632
+ received++;
633
+ });
634
+ const body = await requests[0].text();
635
+ const deliver = (json) => receiving.fetch(new Request(requests[0].url, {
636
+ method: "POST",
637
+ headers: requests[0].headers,
638
+ body: json
639
+ }), { contextData: void 0 });
640
+ assertEquals([(await deliver(body)).status, received], [202, 1]);
641
+ const forged = JSON.parse(body);
642
+ forged.actor = compatibleId((await didKey()).did, "/actor").href;
643
+ assertEquals([(await deliver(JSON.stringify(forged))).status, received], [401, 1]);
644
+ });
645
+ async function captureLogs(run) {
646
+ const records = [];
647
+ await reset();
648
+ try {
649
+ await configure({
650
+ sinks: { buffer: (record) => records.push(record) },
651
+ filters: {},
652
+ loggers: [{
653
+ category: ["logtape", "meta"],
654
+ sinks: []
655
+ }, {
656
+ category: [],
657
+ sinks: ["buffer"],
658
+ lowestLevel: "warning"
659
+ }]
660
+ });
661
+ await run();
662
+ } finally {
663
+ await reset();
664
+ }
665
+ return records.filter((record) => record.category[0] === "fedify");
666
+ }
667
+ test("sendActivity warns for compatible IDs off the actor's first gateway", async () => {
668
+ const owner = await didKey();
669
+ const ownerId = parseIri(`ap://${owner.did}/users/alice`);
670
+ const federation = createTestFederation();
671
+ let actorCalls = 0;
672
+ federation.setActorDispatcher("/users/{identifier}", (_ctx, identifier) => {
673
+ actorCalls++;
674
+ return identifier === "alice" ? new Person({
675
+ id: ownerId,
676
+ gateways: [new URL("https://primary.example"), new URL("https://secondary.example")]
677
+ }) : null;
678
+ });
679
+ const ctx = federation.createContext(new Request("https://example.com/"), void 0);
680
+ const child = await signObject(new Note({
681
+ id: compatibleId(owner.did, "/notes/1", "https://secondary.example"),
682
+ attribution: ownerId,
683
+ content: "Hello"
684
+ }), owner.privateKey, owner.keyId, {
685
+ ...options,
686
+ context: childContext,
687
+ created
688
+ });
689
+ const activity = new Create({
690
+ id: compatibleId(owner.did, "/activities/1", "https://secondary.example"),
691
+ actor: ownerId,
692
+ object: child
693
+ });
694
+ const records = await captureLogs(async () => {
695
+ const { bodies } = await capture(() => ctx.sendActivity([rsaKey, sender(owner)], recipient, activity));
696
+ assertEquals(bodies.length, 1);
697
+ assertEquals(bodies[0].id, activity.id?.href);
698
+ });
699
+ assertEquals(records.filter((r) => r.properties.kind === "activity").length, 1);
700
+ assertEquals(records.filter((r) => r.properties.kind === "object").length, 1);
701
+ assertEquals(actorCalls, 1);
702
+ });
703
+ test("sendActivity skips first-gateway and portable IDs", async () => {
704
+ const owner = await didKey();
705
+ const ownerId = parseIri(`ap://${owner.did}/users/alice`);
706
+ const federation = createTestFederation();
707
+ federation.setActorDispatcher("/users/{identifier}", (_ctx, identifier) => identifier === "alice" ? new Person({
708
+ id: ownerId,
709
+ gateways: [new URL("https://primary.example")]
710
+ }) : null);
711
+ const ctx = federation.createContext(new Request("https://example.com/"), void 0);
712
+ for (const id of [compatibleId(owner.did, "/activities/first", "https://primary.example"), parseIri(`ap://${owner.did}/activities/portable`)]) assertEquals((await captureLogs(() => capture(() => ctx.sendActivity([rsaKey, sender(owner)], recipient, new Create({
713
+ id,
714
+ actor: ownerId
715
+ }))))).filter((r) => r.properties.kind != null), []);
716
+ const plain = federation.createContext(new URL("https://example.com/"));
717
+ assertEquals((await captureLogs(() => capture(() => plain.sendActivity([rsaKey, sender(owner)], recipient, new Create({
718
+ id: compatibleId(owner.did, "/activities/unknown-gateway", "https://secondary.example"),
719
+ actor: ownerId
720
+ }))))).filter((r) => r.properties.kind != null), []);
721
+ });
722
+ test("fanout warns once before enqueueing a compatible activity", async () => {
723
+ const owner = await didKey();
724
+ const ownerId = parseIri(`ap://${owner.did}/users/alice`);
725
+ const { queue, queued } = createQueue();
726
+ const federation = createTestFederation(queue);
727
+ federation.setActorDispatcher("/users/{identifier}", () => new Person({
728
+ id: ownerId,
729
+ gateways: [new URL("https://primary.example")]
730
+ }));
731
+ const ctx = federation.createContext(new Request("https://example.com/"), void 0);
732
+ assertEquals((await captureLogs(() => capture(async () => {
733
+ await ctx.sendActivity([rsaKey, sender(owner)], recipient, new Create({
734
+ id: compatibleId(owner.did, "/activities/fanout", "https://secondary.example"),
735
+ actor: ownerId
736
+ }), { fanout: "force" });
737
+ assertEquals(queued.length, 1);
738
+ for (let i = 0; i < queued.length; i++) await federation.processQueuedTask(void 0, queued[i]);
739
+ }))).filter((r) => r.properties.kind === "activity").length, 1);
740
+ });
741
+ test("an embedded actor supplies gateways to a plain send context", async () => {
742
+ const owner = await didKey();
743
+ const ownerId = parseIri(`ap://${owner.did}/users/alice`);
744
+ const ctx = new FederationImpl({
745
+ kv: new MemoryKvStore(),
746
+ contextLoaderFactory: () => mockDocumentLoader,
747
+ documentLoaderFactory: () => mockDocumentLoader,
748
+ activityTransformers: []
749
+ }).createContext(new URL("https://example.com/"));
750
+ const activity = await signObject(new Create({
751
+ id: compatibleId(owner.did, "/activities/embedded-actor", "https://secondary.example"),
752
+ actor: new Person({
753
+ id: ownerId,
754
+ gateways: [new URL("https://primary.example")]
755
+ })
756
+ }), owner.privateKey, owner.keyId, {
757
+ ...options,
758
+ created
759
+ });
760
+ assertEquals(getEmbeddedFirstGateway(await activity.toJsonLd({
761
+ format: "compact",
762
+ contextLoader: mockDocumentLoader
763
+ }), ownerId, owner.did)?.origin, "https://primary.example");
764
+ assertEquals((await captureLogs(() => capture(() => ctx.sendActivity([rsaKey, sender(owner)], recipient, activity)))).filter((r) => r.properties.kind === "activity").length, 1);
765
+ });
766
+ test("sendActivity checks a locally owned embedded object independently", async () => {
767
+ const alice = await didKey();
768
+ const bob = await didKey();
769
+ const aliceId = parseIri(`ap://${alice.did}/users/alice`);
770
+ const bobId = parseIri(`ap://${bob.did}/users/bob`);
771
+ const federation = createTestFederation();
772
+ federation.setActorDispatcher("/users/{identifier}", (_ctx, identifier) => identifier === "alice" ? new Person({
773
+ id: aliceId,
774
+ gateways: [new URL("https://primary.example")]
775
+ }) : identifier === "bob" ? new Person({
776
+ id: bobId,
777
+ gateways: [new URL("https://bob.example")]
778
+ }) : null);
779
+ const child = await signObject(new Note({
780
+ id: compatibleId(bob.did, "/notes/1", "https://secondary.example"),
781
+ attribution: bobId,
782
+ content: "Bob's note"
783
+ }), bob.privateKey, bob.keyId, {
784
+ ...options,
785
+ context: childContext,
786
+ created
787
+ });
788
+ const ctx = federation.createContext(new Request("https://example.com/"), void 0);
789
+ assertEquals((await captureLogs(() => capture(() => ctx.sendActivity([rsaKey, sender(alice)], recipient, new Announce({
790
+ id: parseIri(`ap://${alice.did}/activities/announce`),
791
+ actor: aliceId,
792
+ object: child
793
+ }))))).filter((r) => r.properties.kind === "object").map((r) => r.properties.id), [child.id?.href]);
794
+ });
795
+ test("sendActivity in an actor dispatcher does not re-enter it", async () => {
796
+ const owner = await didKey();
797
+ const ownerId = parseIri(`ap://${owner.did}/users/alice`);
798
+ const federation = createTestFederation();
799
+ let calls = 0;
800
+ federation.setActorDispatcher("/users/{identifier}", async (ctx) => {
801
+ calls++;
802
+ await ctx.sendActivity([rsaKey, sender(owner)], recipient, new Create({
803
+ id: compatibleId(owner.did, "/activities/from-dispatcher", "https://secondary.example"),
804
+ actor: ownerId
805
+ }));
806
+ return new Person({
807
+ id: ownerId,
808
+ gateways: [new URL("https://primary.example")]
809
+ });
810
+ });
811
+ await capture(async () => {
812
+ assertEquals((await federation.fetch(new Request("https://example.com/users/alice", { headers: { Accept: "application/activity+json" } }), { contextData: void 0 })).status, 200);
813
+ });
814
+ assertEquals(calls, 1);
815
+ });
816
+ test("sendActivity still delivers when the diagnostic actor lookup fails", async () => {
817
+ const owner = await didKey();
818
+ const ownerId = parseIri(`ap://${owner.did}/users/alice`);
819
+ const federation = createTestFederation();
820
+ federation.setActorDispatcher("/users/{identifier}", () => {
821
+ throw new Error("Actor storage unavailable");
822
+ });
823
+ const ctx = federation.createContext(new Request("https://example.com/"), void 0);
824
+ const id = compatibleId(owner.did, "/activities/lookup-fails", "https://secondary.example");
825
+ assertEquals((await captureLogs(async () => {
826
+ const { bodies } = await capture(() => ctx.sendActivity([rsaKey, sender(owner)], recipient, new Create({
827
+ id,
828
+ actor: ownerId
829
+ })));
830
+ assertEquals(bodies.length, 1);
831
+ assertEquals(bodies[0].id, id.href);
832
+ })).filter((r) => r.properties.kind != null), []);
833
+ });
834
+ test("warning checks later local actors when the first is missing", async () => {
835
+ const owner = await didKey();
836
+ const missing = parseIri(`ap://${owner.did}/users/missing`);
837
+ const alice = parseIri(`ap://${owner.did}/users/alice`);
838
+ const federation = createTestFederation();
839
+ federation.setActorDispatcher("/users/{identifier}", (_ctx, identifier) => identifier === "alice" ? new Person({
840
+ id: alice,
841
+ gateways: [new URL("https://primary.example")]
842
+ }) : null);
843
+ const ctx = federation.createContext(new Request("https://example.com/"), void 0);
844
+ assertEquals((await captureLogs(() => warnCompatibleIdsInJson({
845
+ id: compatibleId(owner.did, "/activities/multi", "https://secondary.example").href,
846
+ type: "Create",
847
+ actor: [missing.href, alice.href]
848
+ }, ctx))).filter((r) => r.properties.kind === "activity").length, 1);
849
+ });
850
+ test("warning ignores an embedded gateway key", async () => {
851
+ const owner = await didKey();
852
+ const actorId = parseIri(`ap://${owner.did}/users/alice`);
853
+ const records = await captureLogs(() => warnCompatibleIdsInJson({
854
+ id: compatibleId(owner.did, "/activities/1", "https://secondary.example").href,
855
+ type: "Create",
856
+ actor: {
857
+ id: actorId.href,
858
+ type: "Person",
859
+ gateways: ["https://primary.example"],
860
+ publicKey: [{
861
+ id: compatibleId(owner.did, "/users/alice/keys/1", "https://secondary.example").href,
862
+ type: ["CryptographicKey"]
863
+ }, {
864
+ id: compatibleId(owner.did, "/users/alice/keys/2", "https://secondary.example").href,
865
+ type: "sec:Key"
866
+ }]
867
+ }
868
+ }));
869
+ assertEquals(records.filter((r) => r.properties.kind === "activity").length, 1);
870
+ assertEquals(records.filter((r) => r.properties.kind === "object"), []);
871
+ });
872
+ test("warning resolves a signed child's local ownership alias", async () => {
873
+ const alice = await didKey();
874
+ const bob = await didKey();
875
+ const aliceId = parseIri(`ap://${alice.did}/users/alice`);
876
+ const bobId = parseIri(`ap://${bob.did}/users/bob`);
877
+ const federation = createTestFederation();
878
+ federation.setActorDispatcher("/users/{identifier}", (_ctx, identifier) => identifier === "bob" ? new Person({
879
+ id: bobId,
880
+ gateways: [new URL("https://primary.example")]
881
+ }) : null);
882
+ const ctx = federation.createContext(new Request("https://example.com/"), void 0);
883
+ assertEquals((await captureLogs(() => warnCompatibleIdsInJson({
884
+ id: parseIri(`ap://${alice.did}/activities/announce`).href,
885
+ type: "Announce",
886
+ actor: aliceId.href,
887
+ object: {
888
+ "@context": ["https://www.w3.org/ns/activitystreams", {
889
+ by: {
890
+ "@id": "as:attributedTo",
891
+ "@type": "@id"
892
+ },
893
+ identifier: "@id"
894
+ }],
895
+ identifier: compatibleId(bob.did, "/notes/1", "https://secondary.example").href,
896
+ type: "Note",
897
+ by: bobId.href
898
+ }
899
+ }, ctx))).filter((r) => r.properties.kind === "object").length, 1);
900
+ });
901
+ test("warning reads an inline alias of the serialized root ID", async () => {
902
+ const id = compatibleId((await didKey()).did, "/notes/1", "https://secondary.example");
903
+ assertEquals(getCompactRootId({
904
+ "@context": ["https://www.w3.org/ns/activitystreams", { identifier: "@id" }],
905
+ identifier: id.href,
906
+ type: "Note"
907
+ })?.href, id.href);
908
+ });
909
+ test("an activity on another gateway than its compatible-ID actor is warned about", async () => {
910
+ const owner = await didKey();
911
+ const warnings = (activity) => captureLogs(() => assertPortableActorActivity(activity)).then((records) => records.map((r) => r.properties.activityId));
912
+ const other = compatibleId(owner.did, "/activities/1", "https://other.example");
913
+ assertEquals(await warnings(compatibleCreate(owner, void 0, { id: other })), [other.href]);
914
+ assertEquals(await warnings(compatibleCreate(owner)), []);
915
+ assertEquals(await warnings(compatibleCreate(owner, void 0, { id: parseIri(`ap://${owner.did}/activities/1`) })), []);
916
+ assertEquals(await warnings(compatibleCreate(owner, void 0, {
917
+ id: other,
918
+ actor: parseIri(`ap://${owner.did}/actor`)
919
+ })), []);
920
+ });
921
+ test("an unsigned activity with a malformed compatible ID is rejected", async () => {
922
+ const owner = await didKey();
923
+ const ctx = createTestFederation().createContext(new URL("https://example.com/"));
924
+ const { bodies } = await capture(() => assertRejects(() => ctx.sendActivity([rsaKey, sender(owner)], recipient, new Create({
925
+ id: new URL(compatibleId(owner.did, "/activities/1").href + "?@gateway=https%3A%2F%2Fgw.example"),
926
+ actor: new URL("https://example.com/users/alice")
927
+ })), TypeError, "malformed FEP-ef61 portable ID"));
928
+ assertEquals(bodies.length, 0);
929
+ });
930
+ //#endregion
931
+ export {};