@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,951 @@
1
+ import { Temporal } from "temporal-polyfill";
2
+ import "urlpattern-polyfill";
3
+ globalThis.addEventListener = () => {};
4
+ import { c as isPortableActorDocument, d as isPortableUri, l as isPortableId, m as parseKeyIdString, s as isCompatibleKeyId } from "./portable-key-id-Ca30-hOa.mjs";
5
+ import { a as verificationObservation } from "./verification-Dpfcw8ml.mjs";
6
+ import { n as version, t as name } from "./deno-oR5aJKpU.mjs";
7
+ import { _ as recordKeyLookup, n as getDurationMs, t as classifyFetchError } from "./metrics-CYg9yxBL.mjs";
8
+ import { n as attachKeyOwnerEvidence, t as MAX_PORTABLE_KEY_TTL } from "./key-owner-evidence-APB6NpSk.mjs";
9
+ import { getLogger } from "@logtape/logtape";
10
+ import { CryptographicKey, Multikey, Object as Object$1, isActor } from "@fedify/vocab";
11
+ import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
12
+ import { FetchError, getDocumentLoader, parseDidKeyVerificationMethod } from "@fedify/vocab-runtime";
13
+ //#region src/sig/key.ts
14
+ /**
15
+ * Checks if the given key is valid and supported. No-op if the key is valid,
16
+ * otherwise throws an error.
17
+ * @param key The key to check.
18
+ * @param type Which type of key to check. If not specified, the key can be
19
+ * either public or private.
20
+ * @throws {TypeError} If the key is invalid or unsupported.
21
+ */
22
+ function validateCryptoKey(key, type) {
23
+ if (type != null && key.type !== type) throw new TypeError(`The key is not a ${type} key.`);
24
+ if (!key.extractable) throw new TypeError("The key is not extractable.");
25
+ if (key.algorithm.name !== "RSASSA-PKCS1-v1_5" && key.algorithm.name !== "Ed25519") throw new TypeError("Currently only RSASSA-PKCS1-v1_5 and Ed25519 keys are supported. More algorithms will be added in the future!");
26
+ if (key.algorithm.name === "RSASSA-PKCS1-v1_5") {
27
+ if (key.algorithm.hash.name !== "SHA-256") throw new TypeError("For compatibility with the existing Fediverse software (e.g., Mastodon), hash algorithm for RSASSA-PKCS1-v1_5 keys must be SHA-256.");
28
+ }
29
+ }
30
+ /**
31
+ * Generates a key pair which is appropriate for Fedify.
32
+ * @param algorithm The algorithm to use. Currently only RSASSA-PKCS1-v1_5 and
33
+ * Ed25519 are supported.
34
+ * @returns The generated key pair.
35
+ * @throws {TypeError} If the algorithm is unsupported.
36
+ */
37
+ function generateCryptoKeyPair(algorithm) {
38
+ if (algorithm == null) getLogger([
39
+ "fedify",
40
+ "sig",
41
+ "key"
42
+ ]).warn("No algorithm specified. Using RSASSA-PKCS1-v1_5 by default, but it is recommended to specify the algorithm explicitly as the parameter will be required in the future.");
43
+ if (algorithm == null || algorithm === "RSASSA-PKCS1-v1_5") return crypto.subtle.generateKey({
44
+ name: "RSASSA-PKCS1-v1_5",
45
+ modulusLength: 4096,
46
+ publicExponent: new Uint8Array([
47
+ 1,
48
+ 0,
49
+ 1
50
+ ]),
51
+ hash: "SHA-256"
52
+ }, true, ["sign", "verify"]);
53
+ else if (algorithm === "Ed25519") return crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"]);
54
+ throw new TypeError("Unsupported algorithm: " + algorithm);
55
+ }
56
+ /**
57
+ * Exports a key in JWK format.
58
+ * @param key The key to export. Either public or private key.
59
+ * @returns The exported key in JWK format. The key is suitable for
60
+ * serialization and storage.
61
+ * @throws {TypeError} If the key is invalid or unsupported.
62
+ */
63
+ async function exportJwk(key) {
64
+ validateCryptoKey(key);
65
+ const jwk = await crypto.subtle.exportKey("jwk", key);
66
+ if (jwk.crv === "Ed25519") jwk.alg = "Ed25519";
67
+ return jwk;
68
+ }
69
+ /**
70
+ * Imports a key from JWK format.
71
+ * @param jwk The key in JWK format.
72
+ * @param type Which type of key to import, either `"public"` or `"private"`.
73
+ * @returns The imported key.
74
+ * @throws {TypeError} If the key is invalid or unsupported.
75
+ */
76
+ async function importJwk(jwk, type) {
77
+ let key;
78
+ if (jwk.kty === "RSA" && jwk.alg === "RS256") key = await crypto.subtle.importKey("jwk", jwk, {
79
+ name: "RSASSA-PKCS1-v1_5",
80
+ hash: "SHA-256"
81
+ }, true, type === "public" ? ["verify"] : ["sign"]);
82
+ else if (jwk.kty === "OKP" && jwk.crv === "Ed25519") {
83
+ if (navigator?.userAgent === "Cloudflare-Workers") {
84
+ jwk = { ...jwk };
85
+ delete jwk.alg;
86
+ }
87
+ key = await crypto.subtle.importKey("jwk", jwk, "Ed25519", true, type === "public" ? ["verify"] : ["sign"]);
88
+ } else throw new TypeError("Unsupported JWK format.");
89
+ validateCryptoKey(key, type);
90
+ return key;
91
+ }
92
+ /**
93
+ * Fetches the document that the given actor URI dereferences to, and returns
94
+ * the actor only if the host that served the document is authoritative for
95
+ * the id the document claims.
96
+ *
97
+ * Only the origin that serves an actor id can speak for it. Without that
98
+ * rule any host could serve a document describing somebody else's actor—and
99
+ * listing its own keys as that actor's.
100
+ *
101
+ * @param actorId The URI of the actor to fetch.
102
+ * @param options Options for fetching the document.
103
+ * @returns The actor, or `null` if the document cannot be fetched, is not an
104
+ * actor, or belongs to another origin.
105
+ * @internal
106
+ */
107
+ async function fetchActorDocument(actorId, options = {}) {
108
+ const logger = getLogger([
109
+ "fedify",
110
+ "sig",
111
+ "key"
112
+ ]);
113
+ if (isPortableId(actorId)) {
114
+ logger.debug("The actor {actorId} is a portable actor, which is not authenticated by the web origin that serves it.", { actorId: actorId.href });
115
+ return null;
116
+ }
117
+ const documentLoader = options.documentLoader ?? getDocumentLoader();
118
+ const contextLoader = options.contextLoader ?? getDocumentLoader();
119
+ const { tracerProvider } = options;
120
+ let document;
121
+ let documentUrl = actorId;
122
+ try {
123
+ const remoteDocument = await documentLoader(actorId.href);
124
+ document = remoteDocument.document;
125
+ documentUrl = new URL(remoteDocument.documentUrl ?? "", actorId);
126
+ } catch (error) {
127
+ logger.debug("Failed to fetch the actor {actorId}: {error}", {
128
+ actorId: actorId.href,
129
+ error
130
+ });
131
+ return null;
132
+ }
133
+ let object;
134
+ try {
135
+ object = await Object$1.fromJsonLd(document, {
136
+ documentLoader,
137
+ contextLoader,
138
+ tracerProvider,
139
+ baseUrl: documentUrl
140
+ });
141
+ } catch (error) {
142
+ if (!(error instanceof TypeError)) throw error;
143
+ logger.debug("The document served at {documentUrl} is not a valid object: {error}", {
144
+ documentUrl: documentUrl.href,
145
+ error
146
+ });
147
+ return null;
148
+ }
149
+ if (!isActor(object)) return null;
150
+ if (object.id != null && isPortableId(object.id)) {
151
+ logger.debug("The document served at {documentUrl} claims to be the portable actor {actorId}, which is not authenticated by the web origin that serves it.", {
152
+ documentUrl: documentUrl.href,
153
+ actorId: object.id?.href
154
+ });
155
+ return null;
156
+ }
157
+ if (object.id == null || object.id.origin !== documentUrl.origin) {
158
+ logger.debug("The document served at {documentUrl} claims to be the actor {actorId}, which belongs to another origin; refusing to treat it as that actor's document.", {
159
+ documentUrl: documentUrl.href,
160
+ actorId: object.id?.href
161
+ });
162
+ return null;
163
+ }
164
+ return object;
165
+ }
166
+ /**
167
+ * Resolves the owner that the given key claims, and returns it only if the
168
+ * claim holds.
169
+ *
170
+ * The `owner` of a {@link CryptographicKey}, like the `controller` of
171
+ * a {@link Multikey}, proves nothing on its own: the key document and the
172
+ * claim inside it are served by the same host, so anyone able to serve a key
173
+ * document can name any actor in the world as its owner. The claim becomes
174
+ * meaningful only once the named actor's own document is fetched and turns
175
+ * out to link back to the key. This function performs that mutual-link
176
+ * check, which is the only sound way to learn a fetched key's owner.
177
+ *
178
+ * The claimed owner is always dereferenced, never read out of the key
179
+ * document: an owner embedded there is written by whoever wrote the key.
180
+ *
181
+ * @param key The key whose ownership claim is to be verified. It must carry
182
+ * an `id`, as that is what the owner has to link back to.
183
+ * @param options Options for fetching the claimed owner.
184
+ * @returns The verified owner, or `null` if the key claims no owner, the
185
+ * owner cannot be fetched, or the owner does not link back to
186
+ * the key.
187
+ * @internal
188
+ */
189
+ async function verifyKeyOwnership(key, options = {}) {
190
+ const logger = getLogger([
191
+ "fedify",
192
+ "sig",
193
+ "key"
194
+ ]);
195
+ const keyId = key.id;
196
+ if (keyId == null) return null;
197
+ const claimedOwnerId = key instanceof CryptographicKey ? key.ownerId : key.controllerId;
198
+ if (claimedOwnerId == null) return null;
199
+ const owner = await fetchActorDocument(claimedOwnerId, options);
200
+ if (owner == null) {
201
+ logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims could not be resolved.", {
202
+ keyId: keyId.href,
203
+ claimedOwnerId: claimedOwnerId.href
204
+ });
205
+ return null;
206
+ }
207
+ const linkedKeyIds = key instanceof CryptographicKey ? owner.publicKeyIds : owner.assertionMethodIds;
208
+ for (const linkedKeyId of linkedKeyIds) if (linkedKeyId.href === keyId.href) return owner;
209
+ logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims does not list the key as its own.", {
210
+ keyId: keyId.href,
211
+ claimedOwnerId: claimedOwnerId.href
212
+ });
213
+ return null;
214
+ }
215
+ /**
216
+ * Wraps a key cache so that lookups through the wrapper miss, while
217
+ * whatever they resolve is still written to the cache. It is used to retry
218
+ * a verification that failed with a cached key with a freshly fetched one.
219
+ * @param keyCache The key cache to wrap.
220
+ * @returns The wrapped key cache.
221
+ * @internal
222
+ */
223
+ function bypassKeyCacheReads(keyCache) {
224
+ const cache = keyCache;
225
+ const bypassed = {
226
+ get: () => Promise.resolve(void 0),
227
+ set: async (keyId, key) => await cache?.set(keyId, key),
228
+ getFetchError: () => Promise.resolve(void 0),
229
+ setFetchError: async (keyId, error) => await cache?.setFetchError?.(keyId, error)
230
+ };
231
+ if (cache?.compatibleKeyScope != null) bypassed.compatibleKeyScope = (scope) => {
232
+ const scoped = cache.compatibleKeyScope(scope);
233
+ return {
234
+ get: () => Promise.resolve(void 0),
235
+ getEntry: () => Promise.resolve(void 0),
236
+ set: async (keyId, key, options) => await scoped.set(keyId, key, options),
237
+ delete: async (keyId) => await scoped.delete(keyId)
238
+ };
239
+ };
240
+ return bypassed;
241
+ }
242
+ /**
243
+ * Parses a key ID given as a string. Unlike `new URL()`, it accepts `ap:`
244
+ * and `ap+ef61:` URIs, and refuses a portable or compatible key ID whose
245
+ * identity URL parsing would change, e.g., by resolving dot segments.
246
+ * @throws {TypeError} If the key ID is invalid.
247
+ */
248
+ function parseKeyIdOrThrow(keyId) {
249
+ const parsed = parseKeyIdString(keyId);
250
+ if (parsed == null) throw new TypeError(`Invalid key ID: ${keyId}`);
251
+ return parsed;
252
+ }
253
+ async function withFetchKeySpan(keyId, tracerProvider, fetcher) {
254
+ tracerProvider ??= trace.getTracerProvider();
255
+ return await tracerProvider.getTracer(name, version).startActiveSpan("activitypub.fetch_key", {
256
+ kind: SpanKind.CLIENT,
257
+ attributes: {
258
+ "http.method": "GET",
259
+ "url.full": keyId.href,
260
+ "url.scheme": keyId.protocol.replace(/:$/, ""),
261
+ "url.domain": keyId.hostname,
262
+ "url.path": keyId.pathname,
263
+ "url.query": keyId.search.replace(/^\?/, ""),
264
+ "url.fragment": keyId.hash.replace(/^#/, "")
265
+ }
266
+ }, async (span) => {
267
+ try {
268
+ const result = await fetcher();
269
+ span.setAttribute("activitypub.actor.key.cached", result.cached);
270
+ return result;
271
+ } catch (e) {
272
+ span.setStatus({
273
+ code: SpanStatusCode.ERROR,
274
+ message: String(e)
275
+ });
276
+ throw e;
277
+ } finally {
278
+ span.end();
279
+ }
280
+ });
281
+ }
282
+ /**
283
+ * Fetches a {@link CryptographicKey} or {@link Multikey} from the given URL.
284
+ * If the given URL contains an {@link Actor} object, it tries to find
285
+ * the corresponding key in the `publicKey` or `assertionMethod` property. *
286
+ * Gateway keys of [FEP-ef61] portable actors, i.e., keys whose IDs are
287
+ * compatible identifiers that dereference to portable actor documents, are
288
+ * not resolved by this function, as they only authenticate HTTP requests;
289
+ * use `verifyRequest()` or `getKeyOwner()` for them instead. Nor are keys
290
+ * of portable actors whose IDs are `ap:` or `ap+ef61:` URIs, for the same
291
+ * reason. Keys whose IDs are compatible identifiers or `ap:` or `ap+ef61:`
292
+ * URIs are never read from or written to the key cache under their IDs,
293
+ * except for failures to fetch them.
294
+ *
295
+ * [FEP-ef61]: https://w3id.org/fep/ef61
296
+ * @template T The type of the key to fetch. Either {@link CryptographicKey}
297
+ * or {@link Multikey}.
298
+ * @param keyId The URL of the key.
299
+ * @param cls The class of the key to fetch. Either {@link CryptographicKey}
300
+ * or {@link Multikey}.
301
+ * @param options Options for fetching the key. See {@link FetchKeyOptions}.
302
+ * @returns The fetched key or `null` if the key is not found.
303
+ * @since 1.3.0
304
+ */
305
+ function fetchKey(keyId, cls, options = {}) {
306
+ keyId = typeof keyId === "string" ? parseKeyIdOrThrow(keyId) : keyId;
307
+ return withFetchKeySpan(keyId, options.tracerProvider, () => fetchKeyInternal(keyId, cls, options));
308
+ }
309
+ /**
310
+ * Fetches a {@link CryptographicKey} or {@link Multikey} from the given URL,
311
+ * preserving transport-level fetch failures for callers that need to inspect
312
+ * why the key could not be loaded. *
313
+ * Gateway keys of [FEP-ef61] portable actors, i.e., keys whose IDs are
314
+ * compatible identifiers that dereference to portable actor documents, are
315
+ * not resolved by this function, as they only authenticate HTTP requests;
316
+ * use `verifyRequest()` or `getKeyOwner()` for them instead. Nor are keys
317
+ * of portable actors whose IDs are `ap:` or `ap+ef61:` URIs, for the same
318
+ * reason. Keys whose IDs are compatible identifiers or `ap:` or `ap+ef61:`
319
+ * URIs are never read from or written to the key cache under their IDs,
320
+ * except for failures to fetch them.
321
+ *
322
+ * [FEP-ef61]: https://w3id.org/fep/ef61
323
+ *
324
+ * @template T The type of the key to fetch. Either {@link CryptographicKey}
325
+ * or {@link Multikey}.
326
+ * @param keyId The URL of the key.
327
+ * @param cls The class of the key to fetch. Either {@link CryptographicKey}
328
+ * or {@link Multikey}.
329
+ * @param options Options for fetching the key.
330
+ * @returns The fetched key, or detailed fetch failure information.
331
+ * @since 2.1.0
332
+ */
333
+ async function fetchKeyDetailed(keyId, cls, options = {}) {
334
+ const cacheKey = typeof keyId === "string" ? parseKeyIdOrThrow(keyId) : keyId;
335
+ return await withFetchKeySpan(cacheKey, options.tracerProvider, async () => {
336
+ return await fetchKeyWithResult(cacheKey, cls, options, async (cacheKey, keyId, keyCache, logger) => {
337
+ const fetchError = await keyCache?.getFetchError?.(cacheKey);
338
+ if (fetchError != null) {
339
+ logger.debug("Entry {keyId} found in cache with preserved fetch failure details.", { keyId });
340
+ return {
341
+ key: null,
342
+ cached: true,
343
+ fetchError
344
+ };
345
+ }
346
+ logger.debug("Entry {keyId} found in cache, but no fetch failure details are available.", { keyId });
347
+ return {
348
+ key: null,
349
+ cached: true
350
+ };
351
+ }, async (error, cacheKey, keyId, keyCache, logger) => {
352
+ logger.debug("Failed to fetch key {keyId}.", {
353
+ keyId,
354
+ error
355
+ });
356
+ await keyCache?.set(cacheKey, null);
357
+ if (error instanceof FetchError && error.response != null) {
358
+ const fetchError = {
359
+ status: error.response.status,
360
+ response: error.response.clone()
361
+ };
362
+ await keyCache?.setFetchError?.(cacheKey, fetchError);
363
+ return {
364
+ key: null,
365
+ cached: false,
366
+ fetchError
367
+ };
368
+ }
369
+ const fetchError = { error: error instanceof Error ? error : new Error(String(error)) };
370
+ await keyCache?.setFetchError?.(cacheKey, fetchError);
371
+ return {
372
+ key: null,
373
+ cached: false,
374
+ fetchError
375
+ };
376
+ });
377
+ });
378
+ }
379
+ async function getCachedFetchKey(cacheKey, keyId, cls, keyCache, logger) {
380
+ if (keyCache == null) return null;
381
+ const cachedKey = await keyCache.get(cacheKey);
382
+ const hit = checkCachedKeyHit(cachedKey, keyId, cls, logger);
383
+ if (hit != null) return hit;
384
+ if (cachedKey === null) {
385
+ logger.debug("Entry {keyId} found in cache, but it is unavailable.", { keyId });
386
+ return {
387
+ key: null,
388
+ cached: true
389
+ };
390
+ }
391
+ return null;
392
+ }
393
+ function checkCachedKeyHit(cachedKey, keyId, cls, logger) {
394
+ if (cachedKey instanceof cls && cachedKey.publicKey != null) {
395
+ logger.debug("Key {keyId} found in cache.", { keyId });
396
+ return {
397
+ key: cachedKey,
398
+ cached: true
399
+ };
400
+ }
401
+ return null;
402
+ }
403
+ /**
404
+ * Looks up a key at a compatible identifier, or at an `ap:` or `ap+ef61:`
405
+ * URI, in the cache namespace of a purpose, like {@link getCachedFetchKey}.
406
+ * A key that the verified document of its portable actor was cached with
407
+ * gets the actor's evidence attached from the same entry; see
408
+ * {@link attachKeyOwnerEvidence}.
409
+ */
410
+ async function getCachedCompatibleKey(cacheKey, keyId, cls, scopedCache, logger) {
411
+ if (scopedCache?.getEntry == null) return await getCachedFetchKey(cacheKey, keyId, cls, scopedCache, logger);
412
+ const entry = await scopedCache.getEntry(cacheKey);
413
+ if (entry === void 0) return null;
414
+ if (entry === null) {
415
+ logger.debug("Entry {keyId} found in cache, but it is unavailable.", { keyId });
416
+ return {
417
+ key: null,
418
+ cached: true
419
+ };
420
+ }
421
+ const hit = checkCachedKeyHit(entry.key, keyId, cls, logger);
422
+ if (hit?.key instanceof CryptographicKey && entry.owner != null) attachKeyOwnerEvidence(hit.key, entry.owner, entry.expires);
423
+ return hit;
424
+ }
425
+ /**
426
+ * Attaches the evidence of the portable actor whose document has just
427
+ * vouched for a key to the key. The evidence expires with the cache entry
428
+ * of the key, so that it is never trusted longer than the key is cached.
429
+ * Without a cache, it expires with the proof of the document, but no later
430
+ * than {@link MAX_PORTABLE_KEY_TTL}, as a cache entry would.
431
+ * @param resolution The resolution of the key.
432
+ * @param scopedCache The cache namespace the key was stored in, if any.
433
+ * @param cached When the cache entry expires, if the cache stored one.
434
+ */
435
+ function attachResolvedKeyOwnerEvidence(resolution, scopedCache, cached) {
436
+ let expires;
437
+ if (scopedCache != null) {
438
+ if (cached == null) return;
439
+ expires = cached;
440
+ } else {
441
+ expires = Temporal.Now.instant().add(MAX_PORTABLE_KEY_TTL);
442
+ if (resolution.expires != null && Temporal.Instant.compare(resolution.expires, expires) < 0) expires = resolution.expires;
443
+ }
444
+ attachKeyOwnerEvidence(resolution.key, resolution.document, expires);
445
+ }
446
+ async function clearFetchErrorMetadata(keyId, keyCache) {
447
+ await keyCache?.setFetchError?.(keyId, null);
448
+ }
449
+ /**
450
+ * Returns the identifier a fragment-bearing DID URL is a fragment of, or
451
+ * `null` if the URL is not a DID URL. `did:web:example.com#key` is thereby
452
+ * traced back to `did:web:example.com`, the DID that resolving the key went
453
+ * through in the first place.
454
+ */
455
+ function stripFragment(keyId) {
456
+ if (keyId.protocol !== "did:") return null;
457
+ const did = new URL(keyId.href);
458
+ did.hash = "";
459
+ return did;
460
+ }
461
+ function isDidKeyUrl(keyId) {
462
+ return keyId.protocol === "did:" && keyId.pathname.startsWith("key:");
463
+ }
464
+ async function doRawKeysMatch(left, right) {
465
+ const [leftRaw, rightRaw] = await Promise.all([crypto.subtle.exportKey("raw", left), crypto.subtle.exportKey("raw", right)]);
466
+ if (leftRaw.byteLength !== rightRaw.byteLength) return false;
467
+ const leftBytes = new Uint8Array(leftRaw);
468
+ const rightBytes = new Uint8Array(rightRaw);
469
+ return leftBytes.every((byte, index) => byte === rightBytes[index]);
470
+ }
471
+ async function isCachedDidKeyValid(key, verificationMethod) {
472
+ try {
473
+ return key.id?.href === verificationMethod.id.href && key.controllerId?.href === verificationMethod.controller.href && await doRawKeysMatch(key.publicKey, verificationMethod.publicKey);
474
+ } catch {
475
+ return false;
476
+ }
477
+ }
478
+ async function resolveDidKey(cacheKey, cls, keyCache, logger) {
479
+ if (!isDidKeyUrl(cacheKey)) return null;
480
+ const keyId = cacheKey.href;
481
+ if (!(cls === Multikey)) {
482
+ logger.debug("Failed to resolve did:key {keyId}; did:key verification methods are only supported as Multikey values.", { keyId });
483
+ return {
484
+ key: null,
485
+ cached: false
486
+ };
487
+ }
488
+ let verificationMethod;
489
+ try {
490
+ verificationMethod = await parseDidKeyVerificationMethod(cacheKey);
491
+ } catch (error) {
492
+ logger.debug("Failed to resolve did:key verification method {keyId}: {error}", {
493
+ keyId,
494
+ error
495
+ });
496
+ return {
497
+ key: null,
498
+ cached: false
499
+ };
500
+ }
501
+ const hit = checkCachedKeyHit(await keyCache?.get(cacheKey), keyId, cls, logger);
502
+ if (hit?.key instanceof Multikey && await isCachedDidKeyValid(hit.key, verificationMethod)) return hit;
503
+ const key = new Multikey({
504
+ id: verificationMethod.id,
505
+ controller: verificationMethod.controller,
506
+ publicKey: verificationMethod.publicKey
507
+ });
508
+ await keyCache?.set(cacheKey, key);
509
+ await clearFetchErrorMetadata(cacheKey, keyCache);
510
+ logger.debug("Resolved did:key verification method {keyId}.", { keyId });
511
+ return {
512
+ key,
513
+ cached: false
514
+ };
515
+ }
516
+ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider, keyIdBoundByCaller, portableKeyResolvers }, logger) {
517
+ let object;
518
+ try {
519
+ object = await Object$1.fromJsonLd(document, {
520
+ documentLoader,
521
+ contextLoader,
522
+ tracerProvider
523
+ });
524
+ } catch (e) {
525
+ if (!(e instanceof TypeError)) throw e;
526
+ try {
527
+ object = await cls.fromJsonLd(document, {
528
+ documentLoader,
529
+ contextLoader,
530
+ tracerProvider
531
+ });
532
+ } catch (e) {
533
+ if (e instanceof TypeError) {
534
+ logger.debug("Failed to verify; key {keyId} returned an invalid object.", { keyId });
535
+ await keyCache?.set(cacheKey, null);
536
+ await clearFetchErrorMetadata(cacheKey, keyCache);
537
+ return {
538
+ key: null,
539
+ cached: false
540
+ };
541
+ }
542
+ throw e;
543
+ }
544
+ }
545
+ if (isCompatibleKeyId(cacheKey) && isPortableActorDocument(object)) {
546
+ const scopedCache = keyCache;
547
+ if (portableKeyResolvers == null || cls !== CryptographicKey) {
548
+ logger.debug("Failed to verify; key {keyId} is a gateway key of the portable actor {actorId}, which is only accepted for HTTP Signatures.", {
549
+ keyId,
550
+ actorId: object.id?.href
551
+ });
552
+ await scopedCache?.set(cacheKey, null);
553
+ return {
554
+ key: null,
555
+ cached: false
556
+ };
557
+ }
558
+ const resolution = await portableKeyResolvers.gatewayKey(document, object, cacheKey);
559
+ if (resolution.type !== "verified") {
560
+ logger.debug("Failed to verify; the portable actor {actorId} does not vouch for its gateway key {keyId}.", {
561
+ keyId,
562
+ actorId: object.id?.href
563
+ });
564
+ await scopedCache?.set(cacheKey, null);
565
+ return {
566
+ key: null,
567
+ cached: false
568
+ };
569
+ }
570
+ attachResolvedKeyOwnerEvidence(resolution, scopedCache, await scopedCache?.set(cacheKey, resolution.key, {
571
+ expires: resolution.expires,
572
+ owner: resolution.document
573
+ }));
574
+ return {
575
+ key: resolution.key,
576
+ cached: false
577
+ };
578
+ }
579
+ let key = null;
580
+ let ownerDocument = null;
581
+ if (object instanceof cls && (object.id == null || object.id.href === keyId)) key = object.id == null ? object.clone({ id: cacheKey }) : object;
582
+ else if (isActor(object)) {
583
+ if (object.id == null || isPortableId(object.id) || object.id.origin !== documentUrl.origin) {
584
+ logger.debug("Failed to verify; the document served at {documentUrl} claims to be the actor {actorId}, which belongs to another origin.", {
585
+ keyId,
586
+ documentUrl: documentUrl.href,
587
+ actorId: object.id?.href
588
+ });
589
+ await keyCache?.set(cacheKey, null);
590
+ return {
591
+ key: null,
592
+ cached: false
593
+ };
594
+ }
595
+ ownerDocument = object;
596
+ const keys = cls === CryptographicKey ? object.getPublicKeys({
597
+ documentLoader,
598
+ contextLoader,
599
+ suppressError: true,
600
+ tracerProvider
601
+ }) : object.getAssertionMethods({
602
+ documentLoader,
603
+ contextLoader,
604
+ suppressError: true,
605
+ tracerProvider
606
+ });
607
+ let length = 0;
608
+ let lastKey = null;
609
+ try {
610
+ for await (const k of keys) {
611
+ length++;
612
+ lastKey = k;
613
+ if (k.id?.href === keyId) {
614
+ key = k;
615
+ break;
616
+ }
617
+ }
618
+ } catch (e) {
619
+ if (!(e instanceof TypeError)) throw e;
620
+ logger.debug("Failed to verify; a malformed key was encountered while iterating the keys of {keyId}; treating it as a missing key: {error}", {
621
+ keyId,
622
+ error: e
623
+ });
624
+ }
625
+ const keyIdUrl = new URL(keyId);
626
+ if (key == null && keyIdUrl.hash === "" && length === 1) key = lastKey;
627
+ if (key == null) {
628
+ logger.debug("Failed to verify; object {keyId} returned an {actorType}, but has no key matching {keyId}.", {
629
+ keyId,
630
+ actorType: object.constructor.name
631
+ });
632
+ await keyCache?.set(cacheKey, null);
633
+ await clearFetchErrorMetadata(cacheKey, keyCache);
634
+ return {
635
+ key: null,
636
+ cached: false
637
+ };
638
+ }
639
+ } else {
640
+ logger.debug("Failed to verify; key {keyId} returned an invalid object.", { keyId });
641
+ await keyCache?.set(cacheKey, null);
642
+ await clearFetchErrorMetadata(cacheKey, keyCache);
643
+ return {
644
+ key: null,
645
+ cached: false
646
+ };
647
+ }
648
+ if (key.publicKey == null) {
649
+ logger.debug("Failed to verify; key {keyId} has no publicKeyPem field.", { keyId });
650
+ await keyCache?.set(cacheKey, null);
651
+ await clearFetchErrorMetadata(cacheKey, keyCache);
652
+ return {
653
+ key: null,
654
+ cached: false
655
+ };
656
+ }
657
+ const claimedOwnerId = key instanceof CryptographicKey ? key.ownerId : key.controllerId;
658
+ if (!(keyIdBoundByCaller && claimedOwnerId != null && claimedOwnerId.href === stripFragment(cacheKey)?.href)) {
659
+ if (claimedOwnerId != null && isPortableId(claimedOwnerId)) {
660
+ logger.debug("Failed to verify; key {keyId} claims the portable actor {claimedOwnerId} as its owner, but is not its gateway key.", {
661
+ keyId,
662
+ claimedOwnerId: claimedOwnerId.href
663
+ });
664
+ await keyCache?.set(cacheKey, null);
665
+ await clearFetchErrorMetadata(cacheKey, keyCache);
666
+ return {
667
+ key: null,
668
+ cached: false
669
+ };
670
+ }
671
+ if (ownerDocument != null && claimedOwnerId == null) key = key instanceof CryptographicKey ? key.clone({ owner: ownerDocument.id }) : key.clone({ controller: ownerDocument.id });
672
+ else if (claimedOwnerId != null && claimedOwnerId.href !== ownerDocument?.id?.href) {
673
+ if (await verifyKeyOwnership(key, {
674
+ documentLoader,
675
+ contextLoader,
676
+ tracerProvider
677
+ }) == null) {
678
+ logger.debug("Failed to verify; the owner {claimedOwnerId} that key {keyId} claims does not list the key as its own.", {
679
+ keyId,
680
+ claimedOwnerId: claimedOwnerId.href
681
+ });
682
+ await keyCache?.set(cacheKey, null);
683
+ return {
684
+ key: null,
685
+ cached: false
686
+ };
687
+ }
688
+ }
689
+ }
690
+ if (keyCache != null) {
691
+ await keyCache.set(cacheKey, key);
692
+ logger.debug("Key {keyId} cached.", { keyId });
693
+ }
694
+ await clearFetchErrorMetadata(cacheKey, keyCache);
695
+ return {
696
+ key,
697
+ cached: false
698
+ };
699
+ }
700
+ /**
701
+ * Tells whether a key claims a portable actor as its owner. Such a key can
702
+ * only be a gateway key, which is never cached under its ID, so a cached one
703
+ * was cached by an older version that trusted web origins, and is fetched
704
+ * again.
705
+ */
706
+ function claimsPortableOwner(key) {
707
+ const ownerId = key instanceof CryptographicKey ? key.ownerId : key?.controllerId;
708
+ return ownerId != null && isPortableId(ownerId);
709
+ }
710
+ async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, onFetchError) {
711
+ const start = performance.now();
712
+ let outcome = { result: "error" };
713
+ try {
714
+ const logger = getLogger([
715
+ "fedify",
716
+ "sig",
717
+ "key"
718
+ ]);
719
+ const keyId = cacheKey.href;
720
+ const keyCache = options.keyCache;
721
+ if (isPortableUri(cacheKey)) {
722
+ const result = await fetchPortableActorKey(cacheKey, cls, options, onCachedUnavailable, onFetchError, logger);
723
+ outcome = result.outcome;
724
+ return result.result;
725
+ }
726
+ if (isCompatibleKeyId(cacheKey)) {
727
+ const result = await fetchCompatibleKey(cacheKey, cls, options, onCachedUnavailable, onFetchError, logger);
728
+ outcome = result.outcome;
729
+ return result.result;
730
+ }
731
+ const didKey = await resolveDidKey(cacheKey, cls, keyCache, logger);
732
+ if (didKey != null) {
733
+ outcome = { result: didKey.key == null ? "invalid" : didKey.cached ? "hit" : "fetched" };
734
+ return didKey;
735
+ }
736
+ const cached = await getCachedFetchKey(cacheKey, keyId, cls, keyCache, logger);
737
+ if (cached?.key === null && cached.cached) {
738
+ const cachedUnavailable = await onCachedUnavailable(cacheKey, keyId, keyCache, logger);
739
+ outcome = { result: "hit" };
740
+ return cachedUnavailable;
741
+ }
742
+ if (cached != null && !claimsPortableOwner(cached.key)) {
743
+ outcome = { result: "hit" };
744
+ return cached;
745
+ }
746
+ logger.debug("Fetching key {keyId} to verify signature...", { keyId });
747
+ let document;
748
+ let documentUrl = cacheKey;
749
+ try {
750
+ const remoteDocument = await (options.documentLoader ?? getDocumentLoader())(keyId);
751
+ document = remoteDocument.document;
752
+ documentUrl = new URL(remoteDocument.documentUrl ?? "", cacheKey);
753
+ } catch (error) {
754
+ const classified = classifyFetchError(error);
755
+ const errored = await onFetchError(error, cacheKey, keyId, keyCache, logger);
756
+ outcome = classified;
757
+ return errored;
758
+ }
759
+ const resolved = await resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, options, logger);
760
+ outcome = { result: resolved.key != null ? "fetched" : "invalid" };
761
+ return resolved;
762
+ } finally {
763
+ recordKeyLookup(options.meterProvider, {
764
+ durationMs: getDurationMs(start),
765
+ result: outcome.result,
766
+ remoteUrl: cacheKey,
767
+ cacheEnabled: options.keyCache != null,
768
+ statusCode: outcome.statusCode
769
+ });
770
+ }
771
+ }
772
+ /**
773
+ * Tells which purpose a lookup of a key at a compatible identifier serves.
774
+ */
775
+ function getCompatibleKeyScope(cls, options) {
776
+ if (cls !== CryptographicKey) return "multikey";
777
+ return options.portableKeyResolvers == null ? "cryptographicKey" : "httpSignature";
778
+ }
779
+ /**
780
+ * Looks up a key at an [FEP-ef61] compatible identifier. Such a key may be
781
+ * a gateway key of a portable actor, which is accepted for some purposes but
782
+ * not for others, so everything resolved from the document at the key ID is
783
+ * cached in a namespace of the lookup's purpose, and one purpose never reuses
784
+ * what another resolved, nor what another rejected. A failure to fetch
785
+ * the document fails every purpose alike, so it is cached in the shared
786
+ * namespace, as for any other key.
787
+ *
788
+ * [FEP-ef61]: https://w3id.org/fep/ef61
789
+ */
790
+ async function fetchCompatibleKey(cacheKey, cls, options, onCachedUnavailable, onFetchError, logger) {
791
+ const keyId = cacheKey.href;
792
+ const sharedCache = options.keyCache;
793
+ const scopedCache = sharedCache?.compatibleKeyScope?.(getCompatibleKeyScope(cls, options));
794
+ const cached = await getCachedCompatibleKey(cacheKey, keyId, cls, scopedCache, logger);
795
+ if (cached != null) return {
796
+ result: cached,
797
+ outcome: { result: "hit" }
798
+ };
799
+ if (await sharedCache?.get(cacheKey) === null) {
800
+ logger.debug("Entry {keyId} found in cache, but it could not be fetched.", { keyId });
801
+ return {
802
+ result: await onCachedUnavailable(cacheKey, keyId, sharedCache, logger),
803
+ outcome: { result: "hit" }
804
+ };
805
+ }
806
+ logger.debug("Fetching key {keyId} to verify signature...", { keyId });
807
+ let document;
808
+ let documentUrl;
809
+ try {
810
+ const remoteDocument = await (options.documentLoader ?? getDocumentLoader())(keyId);
811
+ document = remoteDocument.document;
812
+ documentUrl = new URL(remoteDocument.documentUrl ?? "", cacheKey);
813
+ } catch (error) {
814
+ const outcome = classifyFetchError(error);
815
+ const result = await onFetchError(error, cacheKey, keyId, sharedCache, logger);
816
+ await scopedCache?.delete(cacheKey);
817
+ return {
818
+ result,
819
+ outcome
820
+ };
821
+ }
822
+ const resolved = await resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, {
823
+ ...options,
824
+ keyCache: scopedCache
825
+ }, logger);
826
+ return {
827
+ result: resolved,
828
+ outcome: { result: resolved.key != null ? "fetched" : "invalid" }
829
+ };
830
+ }
831
+ /**
832
+ * Looks up a key of an [FEP-ef61] portable actor at an `ap:` or `ap+ef61:`
833
+ * key ID. Such a key is accepted only for HTTP Signatures, and is rejected
834
+ * for any other purpose without being fetched. Like keys at compatible
835
+ * identifiers, what it resolves to is cached in the namespace of the lookup's
836
+ * purpose, and a failure to fetch the actor's document from every gateway is
837
+ * cached in the shared namespace. A failure that is neither, e.g., one
838
+ * gateway being unreachable while another served an invalid document, is not
839
+ * cached at all.
840
+ *
841
+ * [FEP-ef61]: https://w3id.org/fep/ef61
842
+ */
843
+ async function fetchPortableActorKey(cacheKey, cls, options, onCachedUnavailable, onFetchError, logger) {
844
+ const keyId = cacheKey.href;
845
+ const resolvers = options.portableKeyResolvers;
846
+ if (resolvers == null || cls !== CryptographicKey) {
847
+ logger.debug("Failed to verify; key {keyId} is a key of a portable actor, which is only accepted for HTTP Signatures.", { keyId });
848
+ return {
849
+ result: {
850
+ key: null,
851
+ cached: false
852
+ },
853
+ outcome: { result: "invalid" }
854
+ };
855
+ }
856
+ const sharedCache = options.keyCache;
857
+ const scopedCache = sharedCache?.compatibleKeyScope?.("httpSignature");
858
+ const cached = await getCachedCompatibleKey(cacheKey, keyId, cls, scopedCache, logger);
859
+ if (cached != null) return {
860
+ result: cached,
861
+ outcome: { result: "hit" }
862
+ };
863
+ if (await sharedCache?.get(cacheKey) === null) {
864
+ logger.debug("Entry {keyId} found in cache, but it could not be fetched.", { keyId });
865
+ return {
866
+ result: await onCachedUnavailable(cacheKey, keyId, sharedCache, logger),
867
+ outcome: { result: "hit" }
868
+ };
869
+ }
870
+ logger.debug("Resolving key {keyId} to verify signature...", { keyId });
871
+ const resolution = await resolvers.actorKey(cacheKey);
872
+ if (resolution.type === "verified") {
873
+ attachResolvedKeyOwnerEvidence(resolution, scopedCache, await scopedCache?.set(cacheKey, resolution.key, {
874
+ expires: resolution.expires,
875
+ owner: resolution.document
876
+ }));
877
+ return {
878
+ result: {
879
+ key: resolution.key,
880
+ cached: false
881
+ },
882
+ outcome: { result: "fetched" }
883
+ };
884
+ }
885
+ if (resolution.type === "rejected") {
886
+ await scopedCache?.set(cacheKey, null);
887
+ return {
888
+ result: {
889
+ key: null,
890
+ cached: false
891
+ },
892
+ outcome: { result: "invalid" }
893
+ };
894
+ }
895
+ await scopedCache?.delete(cacheKey);
896
+ return {
897
+ result: await onFetchError(resolution.error, cacheKey, keyId, resolution.cacheable ? sharedCache : void 0, logger),
898
+ outcome: resolution.cacheable ? classifyFetchError(resolution.error) : { result: "error" }
899
+ };
900
+ }
901
+ async function fetchKeyInternal(keyId, cls, options = {}) {
902
+ return await fetchKeyWithResult(typeof keyId === "string" ? parseKeyIdOrThrow(keyId) : keyId, cls, options, async (_cacheKey, _keyId, _keyCache, _logger) => {
903
+ const check = options[verificationObservation]?.check;
904
+ if (check != null) try {
905
+ const result = await _keyCache?.getFetchError?.(_cacheKey);
906
+ if (result != null) check.reason = {
907
+ type: "keyFetchError",
908
+ keyId: new URL(_cacheKey.href),
909
+ result
910
+ };
911
+ } catch {}
912
+ return {
913
+ key: null,
914
+ cached: true
915
+ };
916
+ }, async (error, cacheKey, keyId, keyCache, logger) => {
917
+ const check = options[verificationObservation]?.check;
918
+ if (check != null) {
919
+ let result;
920
+ try {
921
+ result = error instanceof FetchError && error.response != null ? {
922
+ status: error.response.status,
923
+ response: error.response.clone()
924
+ } : { error: error instanceof Error ? error : new Error(String(error)) };
925
+ } catch {
926
+ result = { error: error instanceof Error ? error : /* @__PURE__ */ new Error() };
927
+ }
928
+ check.reason = {
929
+ type: "keyFetchError",
930
+ keyId: new URL(cacheKey.href),
931
+ result
932
+ };
933
+ }
934
+ logger.debug("Failed to fetch key {keyId}.", {
935
+ keyId,
936
+ error
937
+ });
938
+ await keyCache?.set(cacheKey, null);
939
+ if (error instanceof FetchError && error.response != null) await keyCache?.setFetchError?.(cacheKey, {
940
+ status: error.response.status,
941
+ response: error.response.clone()
942
+ });
943
+ else await keyCache?.setFetchError?.(cacheKey, { error: error instanceof Error ? error : new Error(String(error)) });
944
+ return {
945
+ key: null,
946
+ cached: false
947
+ };
948
+ });
949
+ }
950
+ //#endregion
951
+ export { fetchKeyDetailed as a, validateCryptoKey as c, fetchKey as i, verifyKeyOwnership as l, exportJwk as n, generateCryptoKeyPair as o, fetchActorDocument as r, importJwk as s, bypassKeyCacheReads as t };