@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,832 @@
1
+ import "temporal-polyfill";
2
+ import "urlpattern-polyfill";
3
+ globalThis.addEventListener = () => {};
4
+ import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
5
+ import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
6
+ import { t as assertFalse } from "../assert_false-CPdRnc3E.mjs";
7
+ import { t as assertThrows } from "../assert_throws-BYITOvPO.mjs";
8
+ import "../std__assert-DvMDRPes.mjs";
9
+ import { t as assert } from "../assert-D1Eafmd_.mjs";
10
+ import { a as signObject, c as verifyPortableObjectProof } from "../proof-Dxy6ByqI.mjs";
11
+ import { n as ed25519PrivateKey, r as ed25519PublicKey } from "../keys-DIJNHW-Y.mjs";
12
+ import { b as PORTABLE_OBJECT_CONTENT_TYPE, o as createFederation } from "../middleware-D7_LO-Vs.mjs";
13
+ import { t as verifyPortableObject } from "../portable-collection-C74P1vtC.mjs";
14
+ import { c as verifyServedPortableObjects } from "../compound-proof-B-9mXxFW.mjs";
15
+ import { n as RouterError } from "../router-u5O5KCQr.mjs";
16
+ import { configure, reset } from "@logtape/logtape";
17
+ import { Create, Note, Object as Object$1, OrderedCollection, PUBLIC_COLLECTION, Person, Tombstone, lookupObject, traverseCollection } from "@fedify/vocab";
18
+ import { FetchError, exportDidKey, formatIri, parseIri, toCompatibleEf61Id } from "@fedify/vocab-runtime";
19
+ import { mockDocumentLoader, test } from "@fedify/fixture";
20
+ //#region src/federation/portable-collection.test.ts
21
+ const did = await exportDidKey(ed25519PublicKey.publicKey);
22
+ const keyId = new URL(`${did}#${did.slice(8)}`);
23
+ const otherKeyPair = await crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"]);
24
+ const otherDid = await exportDidKey(otherKeyPair.publicKey);
25
+ const otherKeyId = new URL(`${otherDid}#${otherDid.slice(8)}`);
26
+ const ORIGIN = "https://example.com";
27
+ const ACCEPT = "application/activity+json";
28
+ function gatewayUrl(path, authority = did) {
29
+ return `${ORIGIN}/.well-known/apgateway/${authority}${path}`;
30
+ }
31
+ function portableId(path, authority = did) {
32
+ return `ap+ef61://${authority}${path}`;
33
+ }
34
+ async function fetchJson(federation, url, init = {}) {
35
+ const response = await federation.fetch(new Request(url, {
36
+ ...init,
37
+ headers: {
38
+ Accept: ACCEPT,
39
+ ...init.headers
40
+ }
41
+ }), { contextData: void 0 });
42
+ const body = await response.text();
43
+ const json = response.headers.get("Content-Type")?.includes("json");
44
+ return [response, body === "" || !json ? null : JSON.parse(body)];
45
+ }
46
+ async function sign(object, privateKey = ed25519PrivateKey, key = keyId) {
47
+ return await signObject(object, privateKey, key, { contextLoader: mockDocumentLoader });
48
+ }
49
+ /**
50
+ * The DIDs of the actors that this server hosts, which the actor dispatcher
51
+ * looks up by identifier, like an application would in its database.
52
+ */
53
+ const users = {
54
+ alice: did,
55
+ bob: did,
56
+ carol: otherDid,
57
+ compatible: did,
58
+ mismatch: did,
59
+ "no-outbox": did,
60
+ https: did,
61
+ tombstone: did,
62
+ "request-did": did
63
+ };
64
+ const NOTES_PER_PAGE = 2;
65
+ const PAGES = [
66
+ "0",
67
+ "1",
68
+ "2"
69
+ ];
70
+ function getNoteValues(identifier, cursor, index) {
71
+ return { id: `${identifier}-${cursor}-${index}` };
72
+ }
73
+ function createTestFederation(options = {}) {
74
+ const calls = options.calls ?? [];
75
+ const federation = createFederation({
76
+ kv: new MemoryKvStore(),
77
+ documentLoaderFactory: () => mockDocumentLoader,
78
+ contextLoaderFactory: () => mockDocumentLoader
79
+ });
80
+ const noteCache = /* @__PURE__ */ new Map();
81
+ const getNote = async (ctx, id, authority) => {
82
+ const key = `${authority} ${id}`;
83
+ let note = noteCache.get(key);
84
+ if (note == null) {
85
+ note = await sign(new Note({
86
+ id: ctx.getPortableObjectUri(Note, { id }, authority),
87
+ to: PUBLIC_COLLECTION,
88
+ content: `Note ${id}`
89
+ }), authority === did ? ed25519PrivateKey : otherKeyPair.privateKey, authority === did ? keyId : otherKeyId);
90
+ noteCache.set(key, note);
91
+ }
92
+ return note;
93
+ };
94
+ federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
95
+ calls.push(`actor ${identifier}`);
96
+ const userDid = users[identifier];
97
+ if (userDid == null) return null;
98
+ switch (identifier) {
99
+ case "https": return new Person({
100
+ id: ctx.getActorUri(identifier),
101
+ outbox: ctx.getOutboxUri(identifier)
102
+ });
103
+ case "tombstone": return new Tombstone({ id: ctx.getPortableActorUri(identifier, userDid) });
104
+ case "request-did": {
105
+ const authority = ctx.portableRequest?.authority;
106
+ return new Person({
107
+ id: ctx.getPortableActorUri(identifier, authority ?? userDid),
108
+ outbox: ctx.getPortableOutboxUri(identifier, authority ?? userDid),
109
+ gateways: [new URL(ORIGIN)]
110
+ });
111
+ }
112
+ case "compatible": {
113
+ const compatible = (id) => toCompatibleEf61Id(id, ORIGIN);
114
+ return new Person({
115
+ id: compatible(ctx.getPortableActorUri(identifier, userDid)),
116
+ outbox: compatible(ctx.getPortableOutboxUri(identifier, userDid)),
117
+ gateways: [new URL(ORIGIN), new URL("https://other.example")]
118
+ });
119
+ }
120
+ }
121
+ return await sign(new Person({
122
+ id: ctx.getPortableActorUri(identifier, userDid),
123
+ preferredUsername: identifier,
124
+ inbox: ctx.getPortableInboxUri(identifier, userDid),
125
+ outbox: identifier === "no-outbox" ? null : identifier === "mismatch" ? ctx.getPortableOutboxUri("alice", userDid) : ctx.getPortableOutboxUri(identifier, userDid),
126
+ followers: ctx.getPortableFollowersUri(identifier, userDid),
127
+ featured: ctx.getPortableFeaturedUri(identifier, userDid),
128
+ gateways: [new URL(ORIGIN)]
129
+ }), userDid === did ? ed25519PrivateKey : otherKeyPair.privateKey, userDid === did ? keyId : otherKeyId);
130
+ });
131
+ const createCache = /* @__PURE__ */ new Map();
132
+ const getCreate = async (ctx, id, authority) => {
133
+ const key = `${authority} ${id}`;
134
+ let create = createCache.get(key);
135
+ if (create == null) {
136
+ create = await sign(new Create({
137
+ id: ctx.getPortableObjectUri(Create, { id }, authority),
138
+ actor: ctx.getPortableActorUri("alice", authority),
139
+ to: PUBLIC_COLLECTION,
140
+ object: await getNote(ctx, id, authority)
141
+ }), authority === did ? ed25519PrivateKey : otherKeyPair.privateKey, authority === did ? keyId : otherKeyId);
142
+ createCache.set(key, create);
143
+ }
144
+ return create;
145
+ };
146
+ federation.setObjectDispatcher(Note, "/notes/{id}", (ctx, values) => ctx.portableRequest == null ? null : getNote(ctx, values.id, ctx.portableRequest.authority));
147
+ federation.setObjectDispatcher(Create, "/creates/{id}", (ctx, values) => ctx.portableRequest == null ? null : getCreate(ctx, values.id, ctx.portableRequest.authority));
148
+ const outbox = federation.setOutboxDispatcher("/users/{identifier}/outbox", async (ctx, identifier, cursor) => {
149
+ calls.push(`outbox ${identifier} ${cursor}`);
150
+ if (cursor == null) return null;
151
+ const authority = users[identifier];
152
+ const items = [];
153
+ for (let i = 0; i < NOTES_PER_PAGE; i++) items.push(await getCreate(ctx, getNoteValues(identifier, cursor, i).id, authority));
154
+ const index = PAGES.indexOf(cursor);
155
+ return {
156
+ items,
157
+ prevCursor: index > 0 ? PAGES[index - 1] : null,
158
+ nextCursor: index < PAGES.length - 1 ? PAGES[index + 1] : null
159
+ };
160
+ }).setFirstCursor((_ctx, identifier) => {
161
+ calls.push(`firstCursor ${identifier}`);
162
+ return PAGES[0];
163
+ }).setLastCursor(() => PAGES[PAGES.length - 1]).setCounter((_ctx, identifier) => {
164
+ calls.push(`counter ${identifier}`);
165
+ return PAGES.length * NOTES_PER_PAGE;
166
+ });
167
+ if (options.authorize) outbox.authorize((ctx, identifier) => {
168
+ calls.push(`authorize ${identifier} ${ctx.portableRequest?.authority ?? ""}`);
169
+ return ctx.request.headers.get("X-Allowed") === "yes";
170
+ });
171
+ federation.setFollowersDispatcher("/users/{identifier}/followers", (_ctx, _identifier) => ({ items: [{
172
+ id: new URL("https://a.example/users/1"),
173
+ inboxId: null
174
+ }, {
175
+ id: new URL("https://b.example/users/2"),
176
+ inboxId: null
177
+ }] }));
178
+ federation.setFeaturedDispatcher("/users/{identifier}/featured", () => ({ items: [] }));
179
+ if (options.inboxDispatcher ?? true) federation.setInboxDispatcher("/users/{identifier}/inbox", () => ({ items: [] }));
180
+ federation.setInboxListeners("/users/{identifier}/inbox");
181
+ federation.setOrderedCollectionDispatcher("bookmarks", Note, "/users/{identifier}/bookmarks", async (ctx, values, cursor) => {
182
+ calls.push(`bookmarks ${values.identifier} ${cursor}`);
183
+ const authority = users[values.identifier];
184
+ const tag = ctx.url.searchParams.get("tag") ?? "all";
185
+ return {
186
+ items: [await getNote(ctx, `${tag}-${cursor ?? "all"}`, authority)],
187
+ nextCursor: cursor === "0" ? "1" : null,
188
+ prevCursor: cursor === "1" ? "0" : null
189
+ };
190
+ }).setFirstCursor(() => "0").mapPortableOwner((_ctx, values) => values.identifier === "nobody" ? null : values.identifier);
191
+ federation.setCollectionDispatcher("drafts", Note, "/users/{identifier}/drafts", () => ({ items: [] }));
192
+ federation.setCollectionDispatcher("embedded", Object$1, "/embedded/{kind}", async (ctx, values) => {
193
+ const note = await getNote(ctx, values.kind, did);
194
+ switch (values.kind) {
195
+ case "signed": return { items: [note] };
196
+ case "other-did": return { items: [await getNote(ctx, "other", otherDid)] };
197
+ case "unsigned": return { items: [new Note({
198
+ id: ctx.getPortableObjectUri(Note, { id: "u" }, did),
199
+ content: "Unsigned"
200
+ })] };
201
+ case "tampered": return { items: [note.clone({ content: "Tampered" })] };
202
+ case "ordinary": return { items: [new Note({
203
+ id: new URL("https://example.com/notes/ordinary"),
204
+ content: "Ordinary"
205
+ })] };
206
+ case "nested": return { items: [await sign(new Create({
207
+ id: ctx.getPortableObjectUri(Note, { id: "create" }, did),
208
+ actor: ctx.getPortableActorUri("alice", did),
209
+ object: new Note({
210
+ id: ctx.getPortableObjectUri(Note, { id: "inner" }, did),
211
+ content: "Unsigned inner"
212
+ })
213
+ }))] };
214
+ case "replies": return { items: [await sign(new Note({
215
+ id: ctx.getPortableObjectUri(Note, { id: "replies" }, did),
216
+ content: "Replies",
217
+ replies: new OrderedCollection({
218
+ id: new URL(ctx.getPortableObjectUri(Note, { id: "replies" }, did).href + "/replies"),
219
+ totalItems: 0
220
+ })
221
+ }))] };
222
+ }
223
+ return null;
224
+ }).mapPortableOwner(() => "alice");
225
+ return federation;
226
+ }
227
+ test("Federation.fetch() serves portable outboxes", async (t) => {
228
+ const federation = createTestFederation();
229
+ await t.step("collection", async () => {
230
+ const [response, json] = await fetchJson(federation, gatewayUrl("/users/alice/outbox"));
231
+ assertEquals(response.status, 200);
232
+ assertEquals(response.headers.get("Content-Type"), PORTABLE_OBJECT_CONTENT_TYPE);
233
+ assertEquals(json?.id, portableId("/users/alice/outbox"));
234
+ assertEquals(json?.type, "OrderedCollection");
235
+ assertEquals(json?.attributedTo, portableId("/users/alice"));
236
+ assertEquals(json?.totalItems, PAGES.length * NOTES_PER_PAGE);
237
+ assertEquals(json?.first, portableId("/users/alice/outbox?cursor=0"));
238
+ assertEquals(json?.last, portableId("/users/alice/outbox?cursor=2"));
239
+ });
240
+ await t.step("pages", async () => {
241
+ const [response, json] = await fetchJson(federation, gatewayUrl("/users/alice/outbox?cursor=1"));
242
+ assertEquals(response.status, 200);
243
+ assertEquals(json?.id, portableId("/users/alice/outbox?cursor=1"));
244
+ assertEquals(json?.type, "OrderedCollectionPage");
245
+ assertEquals(json?.partOf, portableId("/users/alice/outbox"));
246
+ assertEquals(json?.attributedTo, portableId("/users/alice"));
247
+ assertEquals(json?.prev, portableId("/users/alice/outbox?cursor=0"));
248
+ assertEquals(json?.next, portableId("/users/alice/outbox?cursor=2"));
249
+ const items = json?.orderedItems;
250
+ assertEquals(items.map((item) => item.id), [0, 1].map((i) => portableId(`/creates/${getNoteValues("alice", "1", i).id}`)));
251
+ assert(items.every((item) => item.proof != null));
252
+ assert(items.every((item) => item["@context"] != null));
253
+ });
254
+ await t.step("HEAD", async () => {
255
+ const response = await federation.fetch(new Request(gatewayUrl("/users/alice/outbox?cursor=0"), {
256
+ method: "HEAD",
257
+ headers: { Accept: ACCEPT }
258
+ }), { contextData: void 0 });
259
+ assertEquals(response.status, 200);
260
+ assertEquals(await response.text(), "");
261
+ });
262
+ await t.step("location hints and other parameters", async () => {
263
+ const [response, json] = await fetchJson(federation, gatewayUrl("/users/alice/outbox?cursor=1&@gateway=https%3A%2F%2Fexample.com"));
264
+ assertEquals(response.status, 200);
265
+ assertEquals(json?.id, portableId("/users/alice/outbox?cursor=1"));
266
+ assertEquals(json?.partOf, portableId("/users/alice/outbox"));
267
+ });
268
+ await t.step("actors sharing a DID", async () => {
269
+ const [response, json] = await fetchJson(federation, gatewayUrl("/users/bob/outbox"));
270
+ assertEquals(response.status, 200);
271
+ assertEquals(json?.id, portableId("/users/bob/outbox"));
272
+ assertEquals(json?.attributedTo, portableId("/users/bob"));
273
+ });
274
+ await t.step("another DID", async () => {
275
+ const [response, json] = await fetchJson(federation, gatewayUrl("/users/carol/outbox", otherDid));
276
+ assertEquals(response.status, 200);
277
+ assertEquals(json?.id, portableId("/users/carol/outbox", otherDid));
278
+ });
279
+ await t.step("compatible identifiers", async () => {
280
+ const [response, json] = await fetchJson(federation, gatewayUrl("/users/compatible/outbox"));
281
+ assertEquals(response.status, 200);
282
+ assertEquals(json?.id, gatewayUrl("/users/compatible/outbox"));
283
+ assertEquals(json?.attributedTo, gatewayUrl("/users/compatible"));
284
+ assertEquals(json?.first, gatewayUrl("/users/compatible/outbox?cursor=0"));
285
+ });
286
+ await t.step("not acceptable", async () => {
287
+ assertEquals((await federation.fetch(new Request(gatewayUrl("/users/alice/outbox"), { headers: { Accept: "text/html" } }), { contextData: void 0 })).status, 406);
288
+ });
289
+ });
290
+ test("Federation.fetch() does not serve collections of other DIDs", async (t) => {
291
+ const calls = [];
292
+ const federation = createTestFederation({ calls });
293
+ const cases = {
294
+ "a DID that is not the actor's": gatewayUrl("/users/alice/outbox", otherDid),
295
+ "an actor that is not portable": gatewayUrl("/users/https/outbox"),
296
+ "an actor whose outbox is another": gatewayUrl("/users/mismatch/outbox"),
297
+ "an actor without an outbox": gatewayUrl("/users/no-outbox/outbox"),
298
+ "a deleted actor": gatewayUrl("/users/tombstone/outbox"),
299
+ "an unknown actor": gatewayUrl("/users/unknown/outbox"),
300
+ "an actor dispatcher taking the DID from the request": gatewayUrl("/users/request-did/outbox", otherDid),
301
+ "an unregistered collection": gatewayUrl("/users/alice/liked"),
302
+ "a custom collection without an owner mapper": gatewayUrl("/users/alice/drafts"),
303
+ "a custom collection whose owner mapper returns null": gatewayUrl("/users/nobody/bookmarks"),
304
+ "a custom collection of another DID": gatewayUrl("/users/alice/bookmarks", otherDid)
305
+ };
306
+ for (const [name, url] of Object.entries(cases)) await t.step(name, async () => {
307
+ calls.length = 0;
308
+ const [response] = await fetchJson(federation, url);
309
+ assertEquals(response.status, 404);
310
+ assertFalse(calls.some((call) => call.startsWith("outbox ")));
311
+ assertFalse(calls.some((call) => call.startsWith("bookmarks ")));
312
+ });
313
+ await t.step("malformed DIDs", async () => {
314
+ const [response] = await fetchJson(federation, gatewayUrl("/users/alice/outbox", "did:key:u" + did.slice(9)));
315
+ assertEquals(response.status, 400);
316
+ });
317
+ });
318
+ test("Federation.fetch() serves portable inbox collections only with an inbox dispatcher", async () => {
319
+ const [response, json] = await fetchJson(createTestFederation(), gatewayUrl("/users/alice/inbox"));
320
+ assertEquals(response.status, 200);
321
+ assertEquals(json?.id, portableId("/users/alice/inbox"));
322
+ const [response2] = await fetchJson(createTestFederation({ inboxDispatcher: false }), gatewayUrl("/users/alice/inbox"));
323
+ assertEquals(response2.status, 404);
324
+ });
325
+ test("Federation.fetch() filters portable followers collections", async () => {
326
+ const federation = createTestFederation();
327
+ const [response, json] = await fetchJson(federation, gatewayUrl("/users/alice/followers?base-url=" + encodeURIComponent("https://a.example/path")));
328
+ assertEquals(response.status, 200);
329
+ assertEquals(json?.id, portableId("/users/alice/followers?base-url=" + encodeURIComponent("https://a.example/")));
330
+ assertEquals(json?.orderedItems, ["https://a.example/users/1"]);
331
+ const collectionId = json?.id;
332
+ const [pageResponse, page] = await fetchJson(federation, gatewayUrl("/users/alice/followers?base-url=" + encodeURIComponent("https://a.example/path") + "&cursor=0"));
333
+ assertEquals(pageResponse.status, 200);
334
+ assertEquals(page?.id, `${collectionId}&cursor=0`);
335
+ assertEquals(page?.partOf, collectionId);
336
+ const [invalidResponse, invalidPage] = await fetchJson(federation, gatewayUrl("/users/alice/followers?base-url=invalid&cursor=0"));
337
+ assertEquals(invalidResponse.status, 200);
338
+ assertEquals(invalidPage?.id, portableId("/users/alice/followers?cursor=0"));
339
+ assertEquals(invalidPage?.partOf, portableId("/users/alice/followers"));
340
+ });
341
+ test("Federation.fetch() authorizes portable collection requests", async (t) => {
342
+ const calls = [];
343
+ const federation = createTestFederation({
344
+ authorize: true,
345
+ calls
346
+ });
347
+ await t.step("denied", async () => {
348
+ calls.length = 0;
349
+ const [response] = await fetchJson(federation, gatewayUrl("/users/alice/outbox"));
350
+ assertEquals(response.status, 401);
351
+ assertEquals(calls, ["actor alice", `authorize alice ${did}`]);
352
+ });
353
+ await t.step("allowed", async () => {
354
+ calls.length = 0;
355
+ const [response] = await fetchJson(federation, gatewayUrl("/users/alice/outbox?cursor=0"), { headers: { "X-Allowed": "yes" } });
356
+ assertEquals(response.status, 200);
357
+ assertEquals(calls.filter((call) => call.startsWith("authorize ")), [`authorize alice ${did}`]);
358
+ });
359
+ await t.step("a DID that is not the actor's", async () => {
360
+ calls.length = 0;
361
+ const [response] = await fetchJson(federation, gatewayUrl("/users/alice/outbox", otherDid), { headers: { "X-Allowed": "yes" } });
362
+ assertEquals(response.status, 404);
363
+ assertEquals(calls, ["actor alice"]);
364
+ });
365
+ });
366
+ test("Federation.fetch() serves portable custom collections", async (t) => {
367
+ const federation = createTestFederation();
368
+ await t.step("collection", async () => {
369
+ const [response, json] = await fetchJson(federation, gatewayUrl("/users/alice/bookmarks?tag=fedify"));
370
+ assertEquals(response.status, 200);
371
+ assertEquals(json?.id, portableId("/users/alice/bookmarks?tag=fedify"));
372
+ assertEquals(json?.attributedTo, portableId("/users/alice"));
373
+ assertEquals(json?.first, portableId("/users/alice/bookmarks?tag=fedify&cursor=0"));
374
+ });
375
+ await t.step("pages", async () => {
376
+ const [response, json] = await fetchJson(federation, gatewayUrl("/users/alice/bookmarks?tag=fedify&cursor=0"));
377
+ assertEquals(response.status, 200);
378
+ assertEquals(json?.id, portableId("/users/alice/bookmarks?tag=fedify&cursor=0"));
379
+ assertEquals(json?.partOf, portableId("/users/alice/bookmarks?tag=fedify"));
380
+ assertEquals(json?.next, portableId("/users/alice/bookmarks?tag=fedify&cursor=1"));
381
+ assertEquals((json?.orderedItems)[0].id, portableId("/notes/fedify-0"));
382
+ });
383
+ await t.step("compatible identifiers", async () => {
384
+ const [response, json] = await fetchJson(federation, gatewayUrl("/users/compatible/bookmarks"));
385
+ assertEquals(response.status, 200);
386
+ assertEquals(json?.id, gatewayUrl("/users/compatible/bookmarks"));
387
+ assertEquals(json?.attributedTo, gatewayUrl("/users/compatible"));
388
+ });
389
+ });
390
+ test("Federation.fetch() applies the proof policy to embedded portable objects", async (t) => {
391
+ const federation = createTestFederation();
392
+ for (const [kind, status] of Object.entries({
393
+ signed: 200,
394
+ "other-did": 200,
395
+ ordinary: 200,
396
+ replies: 200,
397
+ unsigned: 500,
398
+ tampered: 500,
399
+ nested: 500
400
+ })) await t.step(kind, async () => {
401
+ const [response] = await fetchJson(federation, gatewayUrl(`/embedded/${kind}`));
402
+ assertEquals(response.status, status);
403
+ });
404
+ await t.step("HEAD", async () => {
405
+ const response = await federation.fetch(new Request(gatewayUrl("/embedded/unsigned"), {
406
+ method: "HEAD",
407
+ headers: { Accept: ACCEPT }
408
+ }), { contextData: void 0 });
409
+ assertEquals(response.status, 500);
410
+ assertEquals(await response.text(), "");
411
+ });
412
+ });
413
+ test("Context builds portable collection IDs", async (t) => {
414
+ const ctx = createTestFederation().createContext(new URL(ORIGIN), void 0);
415
+ const helpers = {
416
+ getPortableOutboxUri: [(authority) => ctx.getPortableOutboxUri("alice", authority), "/users/alice/outbox"],
417
+ getPortableFollowersUri: [(authority) => ctx.getPortableFollowersUri("alice", authority), "/users/alice/followers"],
418
+ getPortableFeaturedUri: [(authority) => ctx.getPortableFeaturedUri("alice", authority), "/users/alice/featured"],
419
+ getPortableCollectionUri: [(authority) => ctx.getPortableCollectionUri("bookmarks", { identifier: "alice" }, authority), "/users/alice/bookmarks"]
420
+ };
421
+ for (const [name, [helper, path]] of Object.entries(helpers)) await t.step(name, () => {
422
+ assertEquals(formatIri(helper(did)), portableId(path));
423
+ assertThrows(() => helper(void 0), TypeError);
424
+ assertThrows(() => helper("https://example.com"), TypeError);
425
+ });
426
+ await t.step("unregistered collections", () => {
427
+ assertThrows(() => ctx.getPortableLikedUri("alice", did), RouterError);
428
+ assertThrows(() => ctx.getPortableFollowingUri("alice", did), RouterError);
429
+ assertThrows(() => ctx.getPortableFeaturedTagsUri("alice", did), RouterError);
430
+ assertThrows(() => ctx.getPortableCollectionUri("unknown", {}, did), RouterError);
431
+ });
432
+ await t.step("default authority of RequestContext", async () => {
433
+ const federation = createFederation({
434
+ kv: new MemoryKvStore(),
435
+ documentLoaderFactory: () => mockDocumentLoader,
436
+ contextLoaderFactory: () => mockDocumentLoader
437
+ });
438
+ const built = [];
439
+ federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => new Person({
440
+ id: ctx.getPortableActorUri(identifier, did),
441
+ outbox: ctx.getPortableOutboxUri(identifier, did)
442
+ }));
443
+ federation.setOutboxDispatcher("/users/{identifier}/outbox", (ctx, identifier) => {
444
+ built.push(formatIri(ctx.getPortableOutboxUri(identifier)));
445
+ return { items: [] };
446
+ });
447
+ const [response] = await fetchJson(federation, gatewayUrl("/users/alice/outbox"));
448
+ assertEquals(response.status, 200);
449
+ assertEquals(built, [portableId("/users/alice/outbox")]);
450
+ const requestContext = federation.createContext(new Request(`${ORIGIN}/`), void 0);
451
+ assertThrows(() => requestContext.getPortableOutboxUri("alice"), TypeError);
452
+ });
453
+ });
454
+ /**
455
+ * Captures Fedify's warnings logged while `run` executes.
456
+ */
457
+ async function captureWarnings(run) {
458
+ const records = [];
459
+ await reset();
460
+ try {
461
+ await configure({
462
+ sinks: { buffer: (record) => records.push(record) },
463
+ filters: {},
464
+ loggers: [{
465
+ category: ["logtape", "meta"],
466
+ sinks: []
467
+ }, {
468
+ category: [],
469
+ sinks: ["buffer"],
470
+ lowestLevel: "warning"
471
+ }]
472
+ });
473
+ await run();
474
+ } finally {
475
+ await reset();
476
+ }
477
+ return records.filter((record) => record.category[0] === "fedify");
478
+ }
479
+ test("Actor dispatchers warn about mismatching portable collections", async () => {
480
+ const federation = createTestFederation();
481
+ const mismatches = (await captureWarnings(async () => {
482
+ for (const identifier of [
483
+ "alice",
484
+ "mismatch",
485
+ "https"
486
+ ]) await fetchJson(federation, `${ORIGIN}/users/${identifier}`);
487
+ })).filter((record) => String(record.rawMessage).includes("does not match the portable ID"));
488
+ assertEquals(mismatches.map((record) => record.properties.property), ["outbox"]);
489
+ assertEquals(mismatches[0].properties.value, ctxOutbox("alice"));
490
+ });
491
+ function ctxOutbox(identifier) {
492
+ return new URL(`ap+ef61://${encodeURIComponent(did)}/users/${identifier}/outbox`).href;
493
+ }
494
+ test("Fedify traverses portable collections that Fedify serves", async () => {
495
+ const federation = createTestFederation();
496
+ const loader = (origin) => async (url) => {
497
+ if (!/^https?:/.test(url)) throw new Error(`Not found: ${url}`);
498
+ const target = new URL(url);
499
+ const request = new URL(target.pathname + target.search, ORIGIN);
500
+ const response = await federation.fetch(new Request(request, { headers: { Accept: ACCEPT } }), { contextData: void 0 });
501
+ if (!response.ok) throw new FetchError(url, `HTTP ${response.status}`, response);
502
+ return {
503
+ contextUrl: null,
504
+ documentUrl: new URL(target.pathname + target.search, origin).href,
505
+ document: await response.json()
506
+ };
507
+ };
508
+ const options = {
509
+ documentLoader: loader(ORIGIN),
510
+ contextLoader: mockDocumentLoader,
511
+ gateways: [ORIGIN],
512
+ verifyPortableObject
513
+ };
514
+ const actorUri = (gateway) => `${portableId("/users/alice")}?@gateway=${encodeURIComponent(gateway)}`;
515
+ const actor = await lookupObject(actorUri(ORIGIN), options);
516
+ assert(actor instanceof Person);
517
+ const outbox = await actor.getOutbox(options);
518
+ assert(outbox instanceof OrderedCollection);
519
+ assertEquals((await Array.fromAsync(traverseCollection(outbox, options))).map((item) => item instanceof Create ? formatIri(item.id) : null), PAGES.flatMap((cursor) => [0, 1].map((i) => portableId(`/creates/${getNoteValues("alice", cursor, i).id}`))));
520
+ assertEquals(await actor.getFeatured(options), null);
521
+ const evil = "https://evil.example";
522
+ const evilOptions = {
523
+ ...options,
524
+ documentLoader: loader(evil),
525
+ gateways: [evil]
526
+ };
527
+ const actor2 = await lookupObject(actorUri(evil), evilOptions);
528
+ assert(actor2 instanceof Person);
529
+ assertEquals(await actor2.getOutbox(evilOptions), null);
530
+ });
531
+ test("Fedify traverses portable collections through their owners' gateways", async () => {
532
+ const federation = createTestFederation();
533
+ const requests = [];
534
+ const documentLoader = async (url) => {
535
+ requests.push(url);
536
+ if (!/^https?:/.test(url)) throw new Error(`Not found: ${url}`);
537
+ const target = new URL(url);
538
+ const response = await federation.fetch(new Request(new URL(target.pathname + target.search, ORIGIN), { headers: { Accept: ACCEPT } }), { contextData: void 0 });
539
+ if (!response.ok) throw new FetchError(url, `HTTP ${response.status}`, response);
540
+ return {
541
+ contextUrl: null,
542
+ documentUrl: url,
543
+ document: await response.json()
544
+ };
545
+ };
546
+ const options = {
547
+ documentLoader,
548
+ contextLoader: mockDocumentLoader,
549
+ verifyPortableObject
550
+ };
551
+ const actor = await lookupObject(portableId("/users/alice"), {
552
+ ...options,
553
+ gateways: [ORIGIN]
554
+ });
555
+ assert(actor instanceof Person);
556
+ const outbox = await actor.getOutbox(options);
557
+ assert(outbox instanceof OrderedCollection);
558
+ assertEquals((await Array.fromAsync(traverseCollection(outbox, options))).length, PAGES.length * NOTES_PER_PAGE);
559
+ assert(requests.every((url) => url.startsWith(`${ORIGIN}/`)));
560
+ });
561
+ test("Fedify accepts empty portable collections that Fedify serves", async () => {
562
+ const federation = createTestFederation();
563
+ const [response, json] = await fetchJson(federation, gatewayUrl("/users/alice/inbox"));
564
+ assertEquals(response.status, 200);
565
+ assertEquals(json?.totalItems, 0);
566
+ assertEquals(await verifyPortableObjectProof(json, { contextLoader: mockDocumentLoader }), {
567
+ verified: false,
568
+ reason: { type: "unsecuredCollection" }
569
+ });
570
+ const documentLoader = async (url) => {
571
+ const target = new URL(url);
572
+ const response = await federation.fetch(new Request(new URL(target.pathname + target.search, ORIGIN), { headers: { Accept: ACCEPT } }), { contextData: void 0 });
573
+ if (!response.ok) throw new FetchError(url, `HTTP ${response.status}`, response);
574
+ return {
575
+ contextUrl: null,
576
+ documentUrl: url,
577
+ document: await response.json()
578
+ };
579
+ };
580
+ const options = {
581
+ documentLoader,
582
+ contextLoader: mockDocumentLoader,
583
+ gateways: [ORIGIN],
584
+ verifyPortableObject
585
+ };
586
+ const actor = await lookupObject(portableId("/users/alice"), options);
587
+ assert(actor instanceof Person);
588
+ const inbox = await actor.getInbox(options);
589
+ assert(inbox instanceof OrderedCollection);
590
+ assertEquals(inbox.totalItems, 0);
591
+ assertEquals(await Array.fromAsync(traverseCollection(inbox, options)), []);
592
+ });
593
+ test("verifyServedPortableObjects() classifies unsigned maps under their effective contexts", async () => {
594
+ const AS = "https://www.w3.org/ns/activitystreams";
595
+ const collection = (member) => ({
596
+ "@context": AS,
597
+ id: portableId("/collection"),
598
+ type: "OrderedCollection",
599
+ orderedItems: [member]
600
+ });
601
+ assertEquals(await verifyServedPortableObjects(collection({
602
+ id: portableId("/replies"),
603
+ totalItems: 0
604
+ })), { verified: true });
605
+ assertEquals(await verifyServedPortableObjects(collection({ id: portableId("/notes/1") })), { verified: true });
606
+ assertEquals(await verifyServedPortableObjects(collection({
607
+ id: portableId("/notes/1"),
608
+ content: "Hi"
609
+ })), {
610
+ verified: false,
611
+ path: "/orderedItems/0",
612
+ id: portableId("/notes/1"),
613
+ reason: "missingProof"
614
+ });
615
+ assertEquals(await verifyServedPortableObjects(collection({
616
+ "@context": { totalItems: `${AS}#content` },
617
+ type: "Note",
618
+ tag: {
619
+ id: portableId("/notes/2"),
620
+ totalItems: "Hi"
621
+ }
622
+ })), {
623
+ verified: false,
624
+ path: "/orderedItems/0/tag",
625
+ id: portableId("/notes/2"),
626
+ reason: "missingProof"
627
+ });
628
+ assertEquals(await verifyServedPortableObjects(collection({
629
+ "@context": { tag: {
630
+ "@id": `${AS}#tag`,
631
+ "@context": {}
632
+ } },
633
+ type: "Note",
634
+ tag: {
635
+ id: portableId("/replies"),
636
+ totalItems: 0
637
+ }
638
+ })), {
639
+ verified: false,
640
+ path: "/orderedItems/0",
641
+ id: "",
642
+ reason: "unsupportedContext"
643
+ });
644
+ });
645
+ test("verifyServedPortableObjects() verifies embedded maps as they are served", async () => {
646
+ const AS = "https://www.w3.org/ns/activitystreams";
647
+ const signed = await (await sign(new Note({
648
+ id: parseIri(portableId("/notes/signed")),
649
+ content: "Hi"
650
+ }))).toJsonLd({
651
+ format: "compact",
652
+ contextLoader: mockDocumentLoader
653
+ });
654
+ const collection = (member) => ({
655
+ "@context": AS,
656
+ id: portableId("/collection"),
657
+ type: "OrderedCollection",
658
+ orderedItems: [member]
659
+ });
660
+ assertEquals(await verifyServedPortableObjects(collection(signed), { contextLoader: mockDocumentLoader }), { verified: true });
661
+ const context = signed["@context"];
662
+ assertEquals((await verifyServedPortableObjects(collection({
663
+ ...signed,
664
+ "@context": [...Array.isArray(context) ? context : [context], { content: `${AS}#summary` }]
665
+ }), { contextLoader: mockDocumentLoader })).verified, false);
666
+ assertEquals(await verifyServedPortableObjects(collection({
667
+ "@context": { uri: "@id" },
668
+ uri: portableId("/notes/aliased"),
669
+ type: "Note",
670
+ content: "Unsigned"
671
+ })), {
672
+ verified: false,
673
+ path: "",
674
+ id: portableId("/notes/aliased"),
675
+ reason: "hiddenPortableObject"
676
+ });
677
+ });
678
+ test("verifyServedPortableObjects() finds portable objects hidden by contexts", async () => {
679
+ const AS = "https://www.w3.org/ns/activitystreams";
680
+ const collection = (member) => ({
681
+ "@context": AS,
682
+ id: portableId("/collection"),
683
+ type: "OrderedCollection",
684
+ orderedItems: [member]
685
+ });
686
+ assertEquals(await verifyServedPortableObjects(collection({
687
+ "@context": { uri: "id" },
688
+ uri: portableId("/notes/aliased"),
689
+ type: "Note",
690
+ content: "Unsigned"
691
+ })), {
692
+ verified: false,
693
+ path: "",
694
+ id: portableId("/notes/aliased"),
695
+ reason: "hiddenPortableObject"
696
+ });
697
+ for (const decoy of [{
698
+ id: portableId("/notes/decoy"),
699
+ totalItems: 0
700
+ }, null]) {
701
+ const id = decoy == null ? portableId("/collection") : decoy.id;
702
+ const document = collection({
703
+ "@context": { uri: "id" },
704
+ uri: id,
705
+ type: "Note",
706
+ content: "Unsigned"
707
+ });
708
+ if (decoy != null) document.orderedItems.push(decoy);
709
+ assertEquals(await verifyServedPortableObjects(document), {
710
+ verified: false,
711
+ path: "",
712
+ id,
713
+ reason: "hiddenPortableObject"
714
+ });
715
+ }
716
+ assertEquals(await verifyServedPortableObjects(collection({
717
+ "@context": { proof: `${AS}#tag` },
718
+ id: "https://example.com/notes/ordinary",
719
+ type: "Note",
720
+ proof: {
721
+ id: portableId("/notes/hidden"),
722
+ type: "Note",
723
+ content: "Unsigned"
724
+ }
725
+ })), {
726
+ verified: false,
727
+ path: "",
728
+ id: portableId("/notes/hidden"),
729
+ reason: "hiddenPortableObject"
730
+ });
731
+ });
732
+ test("verifyServedPortableObjects() leaves non-portable remote objects alone", async () => {
733
+ const AS = "https://www.w3.org/ns/activitystreams";
734
+ const collection = (member) => ({
735
+ "@context": AS,
736
+ id: portableId("/collection"),
737
+ type: "OrderedCollection",
738
+ orderedItems: [member]
739
+ });
740
+ assertEquals(await verifyServedPortableObjects(collection({
741
+ "@context": [AS, {
742
+ toot: "http://joinmastodon.org/ns#",
743
+ focalPoint: {
744
+ "@container": "@list",
745
+ "@id": "toot:focalPoint"
746
+ }
747
+ }],
748
+ id: "https://mastodon.example/users/bob/statuses/1",
749
+ type: "Note",
750
+ content: `<p>AP: see ${gatewayUrl("/users/alice")}, ap://${did}/users/alice, and did:plc:abcdefghijklmnop</p>`,
751
+ url: gatewayUrl("/users/alice")
752
+ })), { verified: true });
753
+ assertEquals(await verifyServedPortableObjects(collection({
754
+ "@context": [AS, "https://evil.example/context.jsonld"],
755
+ id: "https://evil.example/activities/1",
756
+ type: "Create",
757
+ object: {
758
+ id: "x:notes/1",
759
+ type: "Note",
760
+ content: "Forged"
761
+ }
762
+ })), {
763
+ verified: false,
764
+ path: "/orderedItems/0",
765
+ id: "https://evil.example/activities/1",
766
+ reason: "unsupportedContext"
767
+ });
768
+ assertEquals(await verifyServedPortableObjects(collection({
769
+ "@context": [AS, { x: {
770
+ "@id": `ap+ef61://${did}/`,
771
+ "@prefix": true
772
+ } }],
773
+ id: "https://example.com/objects/2",
774
+ type: "Note",
775
+ tag: {
776
+ id: "x:notes/1",
777
+ type: "Note"
778
+ }
779
+ })), {
780
+ verified: false,
781
+ path: "",
782
+ id: portableId("/notes/1"),
783
+ reason: "hiddenPortableObject"
784
+ });
785
+ assertEquals(await verifyServedPortableObjects(collection({
786
+ "@context": [AS, {
787
+ toot: "http://joinmastodon.org/ns#",
788
+ id: "toot:notId",
789
+ focalPoint: {
790
+ "@container": "@list",
791
+ "@id": "toot:focalPoint"
792
+ }
793
+ }],
794
+ id: "https://example.com/objects/3",
795
+ type: "Note",
796
+ tag: {
797
+ id: portableId("/notes/3"),
798
+ type: "Note"
799
+ }
800
+ })), {
801
+ verified: false,
802
+ path: "/orderedItems/0",
803
+ id: "https://example.com/objects/3",
804
+ reason: "unsupportedContext"
805
+ });
806
+ });
807
+ test("Federation.fetch() authorizes portable custom collection requests", async () => {
808
+ const federation = createFederation({
809
+ kv: new MemoryKvStore(),
810
+ documentLoaderFactory: () => mockDocumentLoader,
811
+ contextLoaderFactory: () => mockDocumentLoader
812
+ });
813
+ const calls = [];
814
+ federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => new Person({ id: ctx.getPortableActorUri(identifier, did) }));
815
+ federation.setCollectionDispatcher("bookmarks", Note, "/users/{identifier}/bookmarks", (_ctx, values) => {
816
+ calls.push(`dispatch ${values.identifier}`);
817
+ return { items: [] };
818
+ }).mapPortableOwner((_ctx, values) => values.identifier).authorize((ctx, values) => {
819
+ calls.push(`authorize ${values.identifier} ${ctx.portableRequest?.authority}`);
820
+ return ctx.request.headers.get("X-Allowed") === "yes";
821
+ });
822
+ const [denied] = await fetchJson(federation, gatewayUrl("/users/alice/bookmarks"));
823
+ assertEquals(denied.status, 401);
824
+ assertEquals(calls, [`authorize alice ${did}`]);
825
+ calls.length = 0;
826
+ const [allowed, json] = await fetchJson(federation, gatewayUrl("/users/alice/bookmarks"), { headers: { "X-Allowed": "yes" } });
827
+ assertEquals(allowed.status, 200);
828
+ assertEquals(json?.id, portableId("/users/alice/bookmarks"));
829
+ assertEquals(calls, [`authorize alice ${did}`, "dispatch alice"]);
830
+ });
831
+ //#endregion
832
+ export {};