@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,108 @@
1
+ import { Temporal } from "temporal-polyfill";
2
+ import "urlpattern-polyfill";
3
+ globalThis.addEventListener = () => {};
4
+ import { a as getRawCanonicalPortableId, c as isPortableActorDocument, n as getCanonicalPortableId } from "./portable-key-id-Ca30-hOa.mjs";
5
+ import { CryptographicKey, Object as Object$1 } from "@fedify/vocab";
6
+ //#region src/sig/key-owner-evidence.ts
7
+ /**
8
+ * How long a key of an [FEP-ef61] portable actor is trusted after its actor's
9
+ * signed document vouched for it, however long the proof of the document is
10
+ * valid. The actor can drop the key or the gateway from its document at any
11
+ * time, so the key is looked up again at least this often.
12
+ *
13
+ * [FEP-ef61]: https://w3id.org/fep/ef61
14
+ * @internal
15
+ */
16
+ const MAX_PORTABLE_KEY_TTL = Temporal.Duration.from({ hours: 1 });
17
+ const candidates = /* @__PURE__ */ new WeakMap();
18
+ const verified = /* @__PURE__ */ new WeakMap();
19
+ /**
20
+ * Attaches to a key that was just resolved the evidence of the portable actor
21
+ * that owns it. The evidence is not used until the key verifies
22
+ * a signature; see {@link promoteKeyOwnerEvidence}.
23
+ *
24
+ * Nothing is attached unless the document, the key's owner, and the actor
25
+ * the key ID belongs to agree on the actor's canonical portable ID.
26
+ * @param key The key, whose owner the document vouched for.
27
+ * @param document The expanded root node of the actor's verified document.
28
+ * @param expires When the evidence must not be used anymore.
29
+ * @returns Whether the evidence was attached.
30
+ * @internal
31
+ */
32
+ function attachKeyOwnerEvidence(key, document, expires) {
33
+ if (!(key instanceof CryptographicKey) || key.id == null || key.ownerId == null || key.publicKey == null || document == null || typeof document !== "object" || Array.isArray(document)) return false;
34
+ const rawId = document["@id"];
35
+ if (typeof rawId !== "string") return false;
36
+ const keyBase = new URL(key.id.href);
37
+ keyBase.hash = "";
38
+ const ownerId = getCanonicalPortableId(key.ownerId);
39
+ if (ownerId == null || getRawCanonicalPortableId(rawId) !== ownerId || getCanonicalPortableId(keyBase) !== ownerId) return false;
40
+ candidates.set(key, {
41
+ keyId: key.id.href,
42
+ keyOwner: key.ownerId.href,
43
+ publicKey: key.publicKey,
44
+ ownerId,
45
+ document: structuredClone(document),
46
+ expires
47
+ });
48
+ return true;
49
+ }
50
+ /**
51
+ * Makes the evidence attached to a key usable, as the key has just verified
52
+ * an HTTP Signature. Nothing happens if the key has no evidence.
53
+ * @param key The key that verified a signature.
54
+ * @internal
55
+ */
56
+ function promoteKeyOwnerEvidence(key) {
57
+ const evidence = candidates.get(key);
58
+ if (evidence == null) return;
59
+ candidates.delete(key);
60
+ verified.set(key, evidence);
61
+ }
62
+ /**
63
+ * Gets the evidence of the portable actor that owns a key HTTP Signature
64
+ * verification returned.
65
+ * @param key The key.
66
+ * @param now The current time.
67
+ * @returns The evidence, or `undefined` if the key was not returned by HTTP
68
+ * Signature verification, if it has no evidence, if the evidence
69
+ * expired, or if the key has been changed since.
70
+ * @internal
71
+ */
72
+ function getVerifiedKeyOwnerEvidence(key, now = Temporal.Now.instant()) {
73
+ const evidence = verified.get(key);
74
+ if (evidence == null) return void 0;
75
+ if (Temporal.Instant.compare(now, evidence.expires) >= 0) {
76
+ verified.delete(key);
77
+ return;
78
+ }
79
+ if (key.id?.href !== evidence.keyId || key.ownerId?.href !== evidence.keyOwner || key.publicKey !== evidence.publicKey) return;
80
+ return evidence;
81
+ }
82
+ /**
83
+ * Parses the actor from the evidence, i.e., from the expansion of its
84
+ * document whose proof was verified. Every call returns a new object.
85
+ * @param evidence The evidence.
86
+ * @param options Options for parsing the document.
87
+ * @returns The actor, or `null` if the document is not the actor that
88
+ * the evidence is about, or the evidence expired meanwhile.
89
+ * @internal
90
+ */
91
+ async function parseKeyOwnerEvidence(evidence, options = {}) {
92
+ const refuse = (url) => Promise.reject(/* @__PURE__ */ new Error(`Refusing to fetch ${url}.`));
93
+ let actor;
94
+ try {
95
+ actor = await Object$1.fromJsonLd(structuredClone(evidence.document), {
96
+ documentLoader: refuse,
97
+ contextLoader: options.contextLoader,
98
+ tracerProvider: options.tracerProvider
99
+ });
100
+ } catch (error) {
101
+ if (!(error instanceof TypeError)) throw error;
102
+ return null;
103
+ }
104
+ if (!isPortableActorDocument(actor) || getCanonicalPortableId(actor.id) !== evidence.ownerId || Temporal.Instant.compare(Temporal.Now.instant(), evidence.expires) >= 0) return null;
105
+ return actor;
106
+ }
107
+ //#endregion
108
+ export { promoteKeyOwnerEvidence as a, parseKeyOwnerEvidence as i, attachKeyOwnerEvidence as n, getVerifiedKeyOwnerEvidence as r, MAX_PORTABLE_KEY_TTL as t };
@@ -0,0 +1,200 @@
1
+ import { Temporal } from "temporal-polyfill";
2
+ import "urlpattern-polyfill";
3
+ globalThis.addEventListener = () => {};
4
+ import { t as MAX_PORTABLE_KEY_TTL } from "./key-owner-evidence-APB6NpSk.mjs";
5
+ import { CryptographicKey, Multikey } from "@fedify/vocab";
6
+ //#region src/federation/keycache.ts
7
+ const KEY_CACHE_GENERATION = "2";
8
+ const COMPATIBLE_KEY_SEGMENT = "__compatible";
9
+ const MAX_OWNER_BYTES = 32 * 1024;
10
+ function isCompatibleKeyEntry(value) {
11
+ return value != null && typeof value === "object" && "key" in value && "expires" in value && typeof value.expires === "number";
12
+ }
13
+ function isJsonObject(value) {
14
+ return value != null && typeof value === "object" && !Array.isArray(value);
15
+ }
16
+ var KvKeyCache = class {
17
+ kv;
18
+ prefix;
19
+ options;
20
+ unavailableKeyTtl;
21
+ keyTtl;
22
+ nullKeys;
23
+ constructor(kv, prefix, options = {}) {
24
+ this.kv = kv;
25
+ this.prefix = prefix;
26
+ this.options = options;
27
+ this.unavailableKeyTtl = options.unavailableKeyTtl ?? Temporal.Duration.from({ minutes: 10 });
28
+ this.keyTtl = options.keyTtl ?? Temporal.Duration.from({ days: 30 });
29
+ this.nullKeys = /* @__PURE__ */ new Map();
30
+ }
31
+ #getFetchErrorKey(keyId) {
32
+ return [
33
+ ...this.prefix,
34
+ "__fetchError",
35
+ keyId.href
36
+ ];
37
+ }
38
+ #entryKey(keyId) {
39
+ return [
40
+ ...this.prefix,
41
+ KEY_CACHE_GENERATION,
42
+ keyId.href
43
+ ];
44
+ }
45
+ async get(keyId) {
46
+ const negativeExpiration = this.nullKeys.get(keyId.href);
47
+ if (negativeExpiration != null) {
48
+ if (Temporal.Now.instant().until(negativeExpiration).sign >= 0) return null;
49
+ this.nullKeys.delete(keyId.href);
50
+ }
51
+ const serialized = await this.kv.get(this.#entryKey(keyId));
52
+ if (serialized === void 0) return void 0;
53
+ if (serialized === null) {
54
+ this.nullKeys.set(keyId.href, Temporal.Now.instant().add(this.unavailableKeyTtl));
55
+ return null;
56
+ }
57
+ try {
58
+ return await CryptographicKey.fromJsonLd(serialized, this.options);
59
+ } catch {
60
+ try {
61
+ return await Multikey.fromJsonLd(serialized, this.options);
62
+ } catch {
63
+ await this.kv.delete(this.#entryKey(keyId));
64
+ return;
65
+ }
66
+ }
67
+ }
68
+ async set(keyId, key) {
69
+ if (key == null) {
70
+ this.nullKeys.set(keyId.href, Temporal.Now.instant().add(this.unavailableKeyTtl));
71
+ await this.kv.set(this.#entryKey(keyId), null, { ttl: this.unavailableKeyTtl });
72
+ return;
73
+ }
74
+ this.nullKeys.delete(keyId.href);
75
+ const serialized = await key.toJsonLd(this.options);
76
+ await this.kv.set(this.#entryKey(keyId), serialized, { ttl: this.keyTtl });
77
+ }
78
+ /**
79
+ * Returns the namespace of keys at FEP-ef61 compatible identifiers looked
80
+ * up for the given purpose. Its entries carry the time they expire, which
81
+ * is checked when they are read, so that an entry is never used after
82
+ * the proof that vouched for its key expired, however late the underlying
83
+ * store evicts it.
84
+ * @param scope The purpose of the lookups.
85
+ * @returns The namespace.
86
+ * @internal
87
+ */
88
+ compatibleKeyScope(scope) {
89
+ const now = this.options.now ?? (() => Temporal.Now.instant());
90
+ const entryKey = (keyId) => [
91
+ ...this.prefix,
92
+ COMPATIBLE_KEY_SEGMENT,
93
+ scope,
94
+ keyId.href
95
+ ];
96
+ const isExpired = (entry) => entry.expires <= now().epochMilliseconds;
97
+ const keyTtl = Math.min(this.keyTtl.total("millisecond"), MAX_PORTABLE_KEY_TTL.total("millisecond"));
98
+ const unavailableKeyTtl = this.unavailableKeyTtl.total("millisecond");
99
+ const getEntry = async (keyId) => {
100
+ const entry = await this.kv.get(entryKey(keyId));
101
+ if (entry === void 0) return void 0;
102
+ if (!isCompatibleKeyEntry(entry) || isExpired(entry)) {
103
+ await this.kv.delete(entryKey(keyId));
104
+ return;
105
+ }
106
+ if (entry.key === null) return null;
107
+ let expires;
108
+ try {
109
+ expires = Temporal.Instant.fromEpochMilliseconds(entry.expires);
110
+ } catch (error) {
111
+ if (!(error instanceof RangeError)) throw error;
112
+ await this.kv.delete(entryKey(keyId));
113
+ return;
114
+ }
115
+ let key;
116
+ try {
117
+ key = await CryptographicKey.fromJsonLd(entry.key, this.options);
118
+ } catch {
119
+ try {
120
+ key = await Multikey.fromJsonLd(entry.key, this.options);
121
+ } catch {
122
+ await this.kv.delete(entryKey(keyId));
123
+ return;
124
+ }
125
+ }
126
+ if (isExpired(entry)) return void 0;
127
+ return {
128
+ key,
129
+ ...isJsonObject(entry.owner) ? { owner: entry.owner } : {},
130
+ expires
131
+ };
132
+ };
133
+ return {
134
+ get: async (keyId) => {
135
+ const entry = await getEntry(keyId);
136
+ return entry == null ? entry : entry.key;
137
+ },
138
+ getEntry,
139
+ set: async (keyId, key, options) => {
140
+ const serialized = key == null ? null : await key.toJsonLd(this.options);
141
+ const current = now();
142
+ let expires = current.epochMilliseconds + (key == null ? unavailableKeyTtl : keyTtl);
143
+ if (options?.expires != null) expires = Math.min(expires, options.expires.epochMilliseconds);
144
+ const ttl = expires - current.epochMilliseconds;
145
+ if (ttl <= 0) {
146
+ await this.kv.delete(entryKey(keyId));
147
+ return;
148
+ }
149
+ const owner = key == null || options?.owner == null || new TextEncoder().encode(JSON.stringify(options.owner)).length > MAX_OWNER_BYTES ? void 0 : options.owner;
150
+ await this.kv.set(entryKey(keyId), {
151
+ key: serialized,
152
+ expires,
153
+ ...owner == null ? {} : { owner }
154
+ }, { ttl: Temporal.Duration.from({ milliseconds: ttl }) });
155
+ return key == null ? void 0 : Temporal.Instant.fromEpochMilliseconds(expires);
156
+ },
157
+ delete: async (keyId) => {
158
+ await this.kv.delete(entryKey(keyId));
159
+ }
160
+ };
161
+ }
162
+ async getFetchError(keyId) {
163
+ const cached = await this.kv.get(this.#getFetchErrorKey(keyId));
164
+ if (cached == null || typeof cached !== "object") return void 0;
165
+ if ("status" in cached && typeof cached.status === "number" && "statusText" in cached && typeof cached.statusText === "string" && "headers" in cached && Array.isArray(cached.headers) && "body" in cached && typeof cached.body === "string") return {
166
+ status: cached.status,
167
+ response: new Response(cached.body, {
168
+ status: cached.status,
169
+ statusText: cached.statusText,
170
+ headers: cached.headers
171
+ })
172
+ };
173
+ else if ("errorName" in cached && typeof cached.errorName === "string" && "errorMessage" in cached && typeof cached.errorMessage === "string") {
174
+ const error = new Error(cached.errorMessage);
175
+ error.name = cached.errorName;
176
+ return { error };
177
+ }
178
+ }
179
+ async setFetchError(keyId, error) {
180
+ if (error == null) {
181
+ await this.kv.delete(this.#getFetchErrorKey(keyId));
182
+ return;
183
+ }
184
+ if ("status" in error) {
185
+ await this.kv.set(this.#getFetchErrorKey(keyId), {
186
+ status: error.status,
187
+ statusText: error.response.statusText,
188
+ headers: Array.from(error.response.headers.entries()),
189
+ body: await error.response.clone().text()
190
+ }, { ttl: this.unavailableKeyTtl });
191
+ return;
192
+ }
193
+ await this.kv.set(this.#getFetchErrorKey(keyId), {
194
+ errorName: error.error.name,
195
+ errorMessage: error.error.message
196
+ }, { ttl: this.unavailableKeyTtl });
197
+ }
198
+ };
199
+ //#endregion
200
+ export { KvKeyCache as t };
@@ -41,14 +41,15 @@ interface KvStore {
41
41
  /**
42
42
  * Gets the value for the given key.
43
43
  * @param key The key to get the value for.
44
- * @returns The value for the key, or `undefined` if the key does not exist.
44
+ * @returns The value for the key, including `null` if stored, or `undefined`
45
+ * if the key does not exist.
45
46
  * @template T The type of the value to get.
46
47
  */
47
48
  get<T = unknown>(key: KvKey): Promise<T | undefined>;
48
49
  /**
49
50
  * Sets the value for the given key.
50
51
  * @param key The key to set the value for.
51
- * @param value The value to set.
52
+ * @param value The value to set. `null` is a storable value.
52
53
  * @param options Additional options for setting the value.
53
54
  */
54
55
  set(key: KvKey, value: unknown, options?: KvStoreSetOptions): Promise<void>;
@@ -41,14 +41,15 @@ interface KvStore {
41
41
  /**
42
42
  * Gets the value for the given key.
43
43
  * @param key The key to get the value for.
44
- * @returns The value for the key, or `undefined` if the key does not exist.
44
+ * @returns The value for the key, including `null` if stored, or `undefined`
45
+ * if the key does not exist.
45
46
  * @template T The type of the value to get.
46
47
  */
47
48
  get<T = unknown>(key: KvKey): Promise<T | undefined>;
48
49
  /**
49
50
  * Sets the value for the given key.
50
51
  * @param key The key to set the value for.
51
- * @param value The value to set.
52
+ * @param value The value to set. `null` is a storable value.
52
53
  * @param options Additional options for setting the value.
53
54
  */
54
55
  set(key: KvKey, value: unknown, options?: KvStoreSetOptions): Promise<void>;
@@ -0,0 +1,104 @@
1
+ import { Temporal } from "temporal-polyfill";
2
+ import "urlpattern-polyfill";
3
+ globalThis.addEventListener = () => {};
4
+ import { t as MemoryKvStore } from "./kv-DCHkaJ9-.mjs";
5
+ //#region src/testing/kv.ts
6
+ /**
7
+ * A {@link KvStore} decorator whose entry expiry is driven by a virtual clock
8
+ * instead of wall-clock time.
9
+ *
10
+ * {@link MemoryKvStore} decides expiry by comparing timestamps rather than by
11
+ * scheduling timers, so a test that wants to observe a TTL elapsing would
12
+ * otherwise have to sleep for longer than the TTL and hope the runner keeps
13
+ * up. That makes such tests both slow and load-sensitive.
14
+ *
15
+ * This decorator keeps the TTLs itself and never passes them to the store it
16
+ * wraps, so nothing expires until {@link advance} moves the clock. Cache
17
+ * hits, expiry, and repopulation therefore all become deterministic, without
18
+ * patching any global and without a test-only entry point in the library
19
+ * itself. The clock starts at the Unix epoch and only ever moves forward.
20
+ */
21
+ var ManualClockKvStore = class {
22
+ #inner;
23
+ #expirations = /* @__PURE__ */ new Map();
24
+ #now = Temporal.Instant.fromEpochMilliseconds(0);
25
+ /**
26
+ * Present only when the wrapped store has its own `cas()`. Callers such as
27
+ * the task deduplication planner test `kv.cas != null` to decide whether a
28
+ * conditional write is available, so the decorator must not advertise a
29
+ * capability the wrapped store lacks.
30
+ */
31
+ cas;
32
+ constructor(inner = new MemoryKvStore()) {
33
+ this.#inner = inner;
34
+ const innerCas = inner.cas;
35
+ if (innerCas != null) this.cas = async (key, expectedValue, newValue, options) => {
36
+ await this.#evictIfExpired(key);
37
+ const swapped = await innerCas.call(inner, key, expectedValue, newValue);
38
+ if (swapped) this.#recordTtl(key, options);
39
+ return swapped;
40
+ };
41
+ }
42
+ /**
43
+ * The current reading of the virtual clock.
44
+ */
45
+ get now() {
46
+ return this.#now;
47
+ }
48
+ /**
49
+ * Moves the virtual clock forward. Any entry whose TTL has elapsed by the
50
+ * new time reads as missing from this point on, exactly as it would once a
51
+ * real `KvStore` had let it expire.
52
+ */
53
+ advance(duration) {
54
+ this.#now = this.#now.add(Temporal.Duration.from(duration).round({ largestUnit: "hour" }));
55
+ }
56
+ #encodeKey(key) {
57
+ return JSON.stringify(key);
58
+ }
59
+ /**
60
+ * Drops the entry if its TTL has elapsed. Mirrors {@link MemoryKvStore},
61
+ * which treats an entry as live up to and including its expiration instant.
62
+ */
63
+ async #evictIfExpired(key) {
64
+ const encodedKey = this.#encodeKey(key);
65
+ const expiration = this.#expirations.get(encodedKey);
66
+ if (expiration == null) return;
67
+ if (this.#now.until(expiration).sign >= 0) return;
68
+ this.#expirations.delete(encodedKey);
69
+ await this.#inner.delete(key);
70
+ }
71
+ async #evictAllExpired() {
72
+ for (const [encodedKey, expiration] of [...this.#expirations]) {
73
+ if (this.#now.until(expiration).sign >= 0) continue;
74
+ this.#expirations.delete(encodedKey);
75
+ await this.#inner.delete(JSON.parse(encodedKey));
76
+ }
77
+ }
78
+ #recordTtl(key, options) {
79
+ const encodedKey = this.#encodeKey(key);
80
+ if (options?.ttl == null) {
81
+ this.#expirations.delete(encodedKey);
82
+ return;
83
+ }
84
+ this.#expirations.set(encodedKey, this.#now.add(options.ttl.round({ largestUnit: "hour" })));
85
+ }
86
+ async get(key) {
87
+ await this.#evictIfExpired(key);
88
+ return await this.#inner.get(key);
89
+ }
90
+ async set(key, value, options) {
91
+ this.#recordTtl(key, options);
92
+ await this.#inner.set(key, value);
93
+ }
94
+ async delete(key) {
95
+ this.#expirations.delete(this.#encodeKey(key));
96
+ await this.#inner.delete(key);
97
+ }
98
+ async *list(prefix) {
99
+ await this.#evictAllExpired();
100
+ yield* this.#inner.list(prefix);
101
+ }
102
+ };
103
+ //#endregion
104
+ export { ManualClockKvStore as t };
@@ -1,7 +1,7 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { p as recordDocumentCache } from "./metrics-DMk-nTxI.mjs";
4
+ import { p as recordDocumentCache } from "./metrics-CYg9yxBL.mjs";
5
5
  import { getLogger } from "@logtape/logtape";
6
6
  import { preloadedContexts } from "@fedify/vocab-runtime";
7
7
  //#region src/utils/kv-cache.ts
@@ -1,47 +1,85 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
- import { C as recordDocumentCache, d as validateCryptoKey, t as doubleKnock } from "./http-CsmQ-BQ4.js";
3
+ import { G as validateCryptoKey, ot as recordDocumentCache, t as doubleKnock } from "./http-DydWCB7R.js";
4
4
  import { getLogger } from "@logtape/logtape";
5
+ import { FetchError, UrlError, createActivityPubRequest, getRemoteDocument, logRequest, preloadedContexts, resolveDocumentLoaderTimeout, validatePublicUrl, withDocumentLoaderTimeout } from "@fedify/vocab-runtime";
5
6
  import { curry } from "es-toolkit";
6
- import { UrlError, createActivityPubRequest, getRemoteDocument, logRequest, preloadedContexts, validatePublicUrl } from "@fedify/vocab-runtime";
7
7
  //#region src/utils/docloader.ts
8
8
  const logger$1 = getLogger([
9
9
  "fedify",
10
10
  "utils",
11
11
  "docloader"
12
12
  ]);
13
+ const DEFAULT_MAX_REDIRECTION = 20;
13
14
  /**
14
15
  * Gets an authenticated {@link DocumentLoader} for the given identity.
15
16
  * Note that an authenticated document loader intentionally does not cache
16
17
  * the fetched documents.
18
+ * At most 20 HTTP redirects and alternate document links are followed in total
19
+ * per call. Revisiting a URL within that chain throws a {@link FetchError}.
20
+ * Each call, including its double-knocking retries, times out after
21
+ * 10 seconds by default; see {@link DocumentLoaderFactoryOptions.timeout}.
17
22
  * @param identity The identity to get the document loader for.
18
23
  * The actor's key pair.
19
24
  * @param options The options for the document loader.
20
25
  * @returns The authenticated document loader.
21
26
  * @throws {TypeError} If the key is invalid or unsupported.
27
+ * @throws {RangeError} If the `timeout` option is invalid.
22
28
  * @since 0.4.0
23
29
  */
24
- function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedirection, userAgent, specDeterminer, tracerProvider } = {}) {
30
+ function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedirection, userAgent, specDeterminer, timeout, tracerProvider } = {}) {
25
31
  validateCryptoKey(identity.privateKey);
26
- async function load(url, options) {
27
- if (!allowPrivateAddress) try {
28
- await validatePublicUrl(url);
29
- } catch (error) {
30
- if (error instanceof UrlError) logger$1.error("Disallowed private URL: {url}", {
31
- url,
32
- error
33
- });
34
- throw error;
32
+ const resolvedTimeout = resolveDocumentLoaderTimeout(timeout);
33
+ async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
34
+ options?.signal?.throwIfAborted();
35
+ let currentUrl = new URL(url).href;
36
+ await validateUrl(currentUrl, options);
37
+ visited.add(currentUrl);
38
+ const originalRequest = createActivityPubRequest(currentUrl, { userAgent });
39
+ function follow(nextUrl) {
40
+ options?.signal?.throwIfAborted();
41
+ if (redirected >= DEFAULT_MAX_REDIRECTION) throw new FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
42
+ if (visited.has(nextUrl)) throw new FetchError(currentUrl, `Redirect loop detected: ${nextUrl}`);
43
+ redirected++;
35
44
  }
36
- return getRemoteDocument(url, await doubleKnock(createActivityPubRequest(url, { userAgent }), identity, {
45
+ const response = await doubleKnock(originalRequest, identity, {
37
46
  maxRedirection,
38
47
  specDeterminer,
39
48
  log: curry(logRequest)(logger$1),
40
49
  tracerProvider,
41
- signal: options?.signal
42
- }), load);
50
+ signal: options?.signal,
51
+ validateRedirect: async (nextUrl) => {
52
+ follow(nextUrl);
53
+ await validateUrl(nextUrl, options);
54
+ visited.add(nextUrl);
55
+ currentUrl = nextUrl;
56
+ }
57
+ });
58
+ options?.signal?.throwIfAborted();
59
+ return getRemoteDocument(currentUrl, response, (alternateUrl) => {
60
+ follow(alternateUrl);
61
+ return load(alternateUrl, options, redirected, visited);
62
+ }, options);
63
+ }
64
+ async function validateUrl(url, options) {
65
+ if (!allowPrivateAddress) {
66
+ try {
67
+ await validatePublicUrl(url);
68
+ } catch (error) {
69
+ if (error instanceof UrlError) if (error.reason === "dns") logger$1.debug("DNS lookup failed for {url}", {
70
+ url,
71
+ error
72
+ });
73
+ else logger$1[options?.suppressError ? "warn" : "error"]("Disallowed private URL: {url}", {
74
+ url,
75
+ error
76
+ });
77
+ throw error;
78
+ }
79
+ options?.signal?.throwIfAborted();
80
+ }
43
81
  }
44
- return load;
82
+ return withDocumentLoaderTimeout((url, options) => load(url, options), resolvedTimeout);
45
83
  }
46
84
  //#endregion
47
85
  //#region src/utils/kv-cache.ts
@@ -1,52 +1,88 @@
1
1
  const { URLPattern } = require("urlpattern-polyfill");
2
- require("./chunk-Czpeue4M.cjs");
3
- const require_http = require("./http-hlIjmewS.cjs");
4
- const require_temporal_polyfill = require("./temporal-polyfill-BCdTHexd.cjs");
2
+ const require_http = require("./http-B5JT115n.cjs");
5
3
  let _logtape_logtape = require("@logtape/logtape");
6
- let es_toolkit = require("es-toolkit");
7
4
  let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
5
+ let es_toolkit = require("es-toolkit");
8
6
  //#region src/utils/docloader.ts
9
7
  const logger$1 = (0, _logtape_logtape.getLogger)([
10
8
  "fedify",
11
9
  "utils",
12
10
  "docloader"
13
11
  ]);
12
+ const DEFAULT_MAX_REDIRECTION = 20;
14
13
  /**
15
14
  * Gets an authenticated {@link DocumentLoader} for the given identity.
16
15
  * Note that an authenticated document loader intentionally does not cache
17
16
  * the fetched documents.
17
+ * At most 20 HTTP redirects and alternate document links are followed in total
18
+ * per call. Revisiting a URL within that chain throws a {@link FetchError}.
19
+ * Each call, including its double-knocking retries, times out after
20
+ * 10 seconds by default; see {@link DocumentLoaderFactoryOptions.timeout}.
18
21
  * @param identity The identity to get the document loader for.
19
22
  * The actor's key pair.
20
23
  * @param options The options for the document loader.
21
24
  * @returns The authenticated document loader.
22
25
  * @throws {TypeError} If the key is invalid or unsupported.
26
+ * @throws {RangeError} If the `timeout` option is invalid.
23
27
  * @since 0.4.0
24
28
  */
25
- function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedirection, userAgent, specDeterminer, tracerProvider } = {}) {
29
+ function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedirection, userAgent, specDeterminer, timeout, tracerProvider } = {}) {
26
30
  require_http.validateCryptoKey(identity.privateKey);
27
- async function load(url, options) {
28
- if (!allowPrivateAddress) try {
29
- await (0, _fedify_vocab_runtime.validatePublicUrl)(url);
30
- } catch (error) {
31
- if (error instanceof _fedify_vocab_runtime.UrlError) logger$1.error("Disallowed private URL: {url}", {
32
- url,
33
- error
34
- });
35
- throw error;
31
+ const resolvedTimeout = (0, _fedify_vocab_runtime.resolveDocumentLoaderTimeout)(timeout);
32
+ async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
33
+ options?.signal?.throwIfAborted();
34
+ let currentUrl = new URL(url).href;
35
+ await validateUrl(currentUrl, options);
36
+ visited.add(currentUrl);
37
+ const originalRequest = (0, _fedify_vocab_runtime.createActivityPubRequest)(currentUrl, { userAgent });
38
+ function follow(nextUrl) {
39
+ options?.signal?.throwIfAborted();
40
+ if (redirected >= DEFAULT_MAX_REDIRECTION) throw new _fedify_vocab_runtime.FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
41
+ if (visited.has(nextUrl)) throw new _fedify_vocab_runtime.FetchError(currentUrl, `Redirect loop detected: ${nextUrl}`);
42
+ redirected++;
36
43
  }
37
- return (0, _fedify_vocab_runtime.getRemoteDocument)(url, await require_http.doubleKnock((0, _fedify_vocab_runtime.createActivityPubRequest)(url, { userAgent }), identity, {
44
+ const response = await require_http.doubleKnock(originalRequest, identity, {
38
45
  maxRedirection,
39
46
  specDeterminer,
40
47
  log: (0, es_toolkit.curry)(_fedify_vocab_runtime.logRequest)(logger$1),
41
48
  tracerProvider,
42
- signal: options?.signal
43
- }), load);
49
+ signal: options?.signal,
50
+ validateRedirect: async (nextUrl) => {
51
+ follow(nextUrl);
52
+ await validateUrl(nextUrl, options);
53
+ visited.add(nextUrl);
54
+ currentUrl = nextUrl;
55
+ }
56
+ });
57
+ options?.signal?.throwIfAborted();
58
+ return (0, _fedify_vocab_runtime.getRemoteDocument)(currentUrl, response, (alternateUrl) => {
59
+ follow(alternateUrl);
60
+ return load(alternateUrl, options, redirected, visited);
61
+ }, options);
62
+ }
63
+ async function validateUrl(url, options) {
64
+ if (!allowPrivateAddress) {
65
+ try {
66
+ await (0, _fedify_vocab_runtime.validatePublicUrl)(url);
67
+ } catch (error) {
68
+ if (error instanceof _fedify_vocab_runtime.UrlError) if (error.reason === "dns") logger$1.debug("DNS lookup failed for {url}", {
69
+ url,
70
+ error
71
+ });
72
+ else logger$1[options?.suppressError ? "warn" : "error"]("Disallowed private URL: {url}", {
73
+ url,
74
+ error
75
+ });
76
+ throw error;
77
+ }
78
+ options?.signal?.throwIfAborted();
79
+ }
44
80
  }
45
- return load;
81
+ return (0, _fedify_vocab_runtime.withDocumentLoaderTimeout)((url, options) => load(url, options), resolvedTimeout);
46
82
  }
47
83
  //#endregion
48
84
  //#region src/utils/kv-cache.ts
49
- const Temporal = require_temporal_polyfill.Temporal;
85
+ const Temporal = require_http.Temporal;
50
86
  const logger = (0, _logtape_logtape.getLogger)([
51
87
  "fedify",
52
88
  "utils",