@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,1439 @@
1
+ import { Temporal } from "temporal-polyfill";
2
+ import "urlpattern-polyfill";
3
+ globalThis.addEventListener = () => {};
4
+ import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
5
+ import { a as fetchKeyDetailed, i as fetchKey } from "../key-BspPiXqn.mjs";
6
+ import { h as verifyJsonLd, p as signJsonLd } from "../ld-CzaP-hJl.mjs";
7
+ import { d as verifyProof, r as createProof } from "../proof-Dxy6ByqI.mjs";
8
+ import { n as resolvePortableActorKey } from "../portable-key-BIMLrAP4.mjs";
9
+ import { f as verifyRequest, l as signRequest, p as verifyRequestDetailed } from "../http-siciWkmx.mjs";
10
+ import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
11
+ import { n as getKeyOwner, t as doesActorOwnKey } from "../owner-DYgfTFHy.mjs";
12
+ import { t as KvKeyCache } from "../keycache-Da8A8mZa.mjs";
13
+ import { Create, CryptographicKey, Multikey, Note, Person } from "@fedify/vocab";
14
+ import { FetchError, encodeMultibase, exportDidKey, exportSpki, parseIri, preloadedContexts } from "@fedify/vocab-runtime";
15
+ import { test } from "@fedify/fixture";
16
+ import { deepStrictEqual, ok, strictEqual } from "node:assert/strict";
17
+ import serialize from "json-canon";
18
+ //#region src/sig/portable-key.test.ts
19
+ const did = await exportDidKey(ed25519PublicKey.publicKey);
20
+ const didKeyId = `${did}#${did.slice(8)}`;
21
+ const otherKeyPair = await crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"]);
22
+ const otherDid = await exportDidKey(otherKeyPair.publicKey);
23
+ const actorId = `ap+ef61://${did}/actor`;
24
+ const gw1 = "https://gw1.example";
25
+ const gw2 = "https://gw2.example";
26
+ function compatibleId(gateway, id = actorId) {
27
+ return `${gateway}/.well-known/apgateway/${id.replace(/^ap\+ef61:\/\//, "")}`;
28
+ }
29
+ const keyId = new URL(`${compatibleId(gw1)}#main-key`);
30
+ const gatewayPublicKey = rsaPublicKey2.publicKey;
31
+ const contextLoader = (url) => {
32
+ const document = preloadedContexts[url];
33
+ if (document == null) return Promise.reject(/* @__PURE__ */ new Error(`No context: ${url}`));
34
+ return Promise.resolve({
35
+ contextUrl: null,
36
+ documentUrl: url,
37
+ document
38
+ });
39
+ };
40
+ function createLoader(responses) {
41
+ const fetched = [];
42
+ const loader = (url) => {
43
+ fetched.push(url);
44
+ const document = responses[url.replace(/#.*$/, "")];
45
+ if (document == null) return Promise.reject(new FetchError(url, "HTTP 404", new Response(null, { status: 404 })));
46
+ return Promise.resolve({
47
+ contextUrl: null,
48
+ documentUrl: url,
49
+ document: structuredClone(document)
50
+ });
51
+ };
52
+ return Object.assign(loader, { fetched });
53
+ }
54
+ async function sign(document, privateKey = ed25519PrivateKey, verificationMethod = didKeyId, proofOptions = {}) {
55
+ const proofConfig = {
56
+ "@context": document["@context"],
57
+ type: "DataIntegrityProof",
58
+ cryptosuite: "eddsa-jcs-2022",
59
+ verificationMethod,
60
+ proofPurpose: "assertionMethod",
61
+ created: "2023-02-24T23:36:38Z",
62
+ ...proofOptions
63
+ };
64
+ const encoder = new TextEncoder();
65
+ const proofDigest = await crypto.subtle.digest("SHA-256", encoder.encode(serialize(proofConfig)));
66
+ const messageDigest = await crypto.subtle.digest("SHA-256", encoder.encode(serialize(document)));
67
+ const digest = /* @__PURE__ */ new Uint8Array(64);
68
+ digest.set(new Uint8Array(proofDigest), 0);
69
+ digest.set(new Uint8Array(messageDigest), 32);
70
+ const signature = await crypto.subtle.sign("Ed25519", privateKey, digest);
71
+ return {
72
+ ...document,
73
+ proof: {
74
+ ...proofConfig,
75
+ proofValue: new TextDecoder().decode(encodeMultibase("base58btc", new Uint8Array(signature)))
76
+ }
77
+ };
78
+ }
79
+ async function actorJson(options = {}) {
80
+ const id = parseIri(options.id ?? actorId);
81
+ const key = options.key ?? keyId;
82
+ const json = await new Person({
83
+ id,
84
+ preferredUsername: "alice",
85
+ inbox: parseIri(`${options.id ?? actorId}/inbox`),
86
+ gateways: (options.gateways ?? [gw1, gw2]).map((g) => new URL(g)),
87
+ assertionMethods: options.assertionMethod === false ? [] : [new Multikey({
88
+ id: key,
89
+ controller: id,
90
+ publicKey: gatewayPublicKey
91
+ })],
92
+ publicKeys: options.publicKey === null ? [] : [new CryptographicKey({
93
+ id: key,
94
+ owner: id,
95
+ publicKey: options.publicKey ?? gatewayPublicKey
96
+ })]
97
+ }).toJsonLd({
98
+ format: "compact",
99
+ contextLoader
100
+ });
101
+ const context = json["@context"];
102
+ if (!context.includes("https://w3id.org/security/data-integrity/v1")) json["@context"] = [...context, "https://w3id.org/security/data-integrity/v1"];
103
+ return json;
104
+ }
105
+ async function signedRequest(spec, key = keyId) {
106
+ return await signRequest(new Request("https://recipient.example/inbox", {
107
+ method: "POST",
108
+ body: "{}",
109
+ headers: { "Content-Type": "application/activity+json" }
110
+ }), rsaPrivateKey2, key, { spec });
111
+ }
112
+ test("verifyRequest() accepts gateway keys vouched for by portable actors", async () => {
113
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await actorJson()) });
114
+ for (const spec of ["draft-cavage-http-signatures-12", "rfc9421"]) {
115
+ const key = await verifyRequest(await signedRequest(spec), {
116
+ documentLoader,
117
+ contextLoader,
118
+ spec
119
+ });
120
+ ok(key != null, spec);
121
+ strictEqual(key.id?.href, keyId.href);
122
+ strictEqual(key.ownerId?.href, parseIri(actorId).href);
123
+ }
124
+ });
125
+ test("verifyRequest() accepts gateway keys only in assertionMethod", async () => {
126
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await actorJson({ publicKey: null })) });
127
+ strictEqual((await verifyRequest(await signedRequest(), {
128
+ documentLoader,
129
+ contextLoader
130
+ }))?.id?.href, keyId.href);
131
+ });
132
+ test("verifyRequest() rejects gateway keys that portable actors do not vouch for", async () => {
133
+ const unsigned = await actorJson();
134
+ const cases = {
135
+ "an unsigned actor document": unsigned,
136
+ "an actor document signed by another DID": await sign(unsigned, otherKeyPair.privateKey, `${otherDid}#${otherDid.slice(8)}`),
137
+ "an unlisted gateway": await sign(await actorJson({ gateways: [gw2] })),
138
+ "a key missing from both assertionMethod and publicKey": await sign(await actorJson({
139
+ assertionMethod: false,
140
+ publicKey: null
141
+ })),
142
+ "a key referred to by URL in assertionMethod, embedded in publicKey": await sign({
143
+ ...await actorJson({ assertionMethod: false }),
144
+ assertionMethod: [keyId.href]
145
+ }),
146
+ "a key only in publicKey without an owner": await sign(withoutPublicKeyOwner(await actorJson({ assertionMethod: false }))),
147
+ "two keys with the same ID in publicKey": await sign(await duplicatePublicKey(await actorJson({ assertionMethod: false }))),
148
+ "a key referred to by URL": await sign({
149
+ ...await actorJson({
150
+ publicKey: null,
151
+ assertionMethod: false
152
+ }),
153
+ assertionMethod: [keyId.href]
154
+ }),
155
+ "a publicKey entry with other key material": await sign(await actorJson({ publicKey: rsaPublicKey3.publicKey })),
156
+ "a key ID for another actor": await sign(await actorJson({ key: new URL(`${compatibleId(gw1, `${actorId}/other`)}#main-key`) }))
157
+ };
158
+ for (const [name, document] of Object.entries(cases)) {
159
+ const documentLoader = createLoader({ [compatibleId(gw1)]: document });
160
+ strictEqual(await verifyRequest(await signedRequest(), {
161
+ documentLoader,
162
+ contextLoader
163
+ }), null, name);
164
+ }
165
+ const otherActorKey = new URL(`${compatibleId(gw1, `${actorId}/other`)}#main-key`);
166
+ const documentLoader = createLoader({ [compatibleId(gw1, `${actorId}/other`)]: await sign(await actorJson()) });
167
+ strictEqual(await verifyRequest(await signedRequest(void 0, otherActorKey), {
168
+ documentLoader,
169
+ contextLoader
170
+ }), null);
171
+ });
172
+ function withoutPublicKeyOwner(json) {
173
+ const { owner: _, ...publicKey } = json.publicKey;
174
+ return {
175
+ ...json,
176
+ publicKey
177
+ };
178
+ }
179
+ async function duplicatePublicKey(json) {
180
+ const publicKey = json.publicKey;
181
+ return {
182
+ ...json,
183
+ publicKey: [publicKey, {
184
+ ...publicKey,
185
+ publicKeyPem: await exportSpki(rsaPublicKey3.publicKey)
186
+ }]
187
+ };
188
+ }
189
+ test("verifyRequest() accepts gateway keys referred to by URL in publicKey", async () => {
190
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign({
191
+ ...await actorJson({ publicKey: null }),
192
+ publicKey: keyId.href
193
+ }) });
194
+ strictEqual((await verifyRequest(await signedRequest(), {
195
+ documentLoader,
196
+ contextLoader
197
+ }))?.id?.href, keyId.href);
198
+ });
199
+ test("verifyRequest() accepts gateway keys only in publicKey", async () => {
200
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await actorJson({ assertionMethod: false })) });
201
+ const key = await verifyRequest(await signedRequest(), {
202
+ documentLoader,
203
+ contextLoader
204
+ });
205
+ strictEqual(key?.id?.href, keyId.href);
206
+ strictEqual(key?.ownerId?.href, parseIri(actorId).href);
207
+ });
208
+ test("verifyRequest() resolves gateway keys of compatible-ID actors", async () => {
209
+ const id = compatibleId(gw1);
210
+ const signed = await sign(await actorJson({
211
+ id,
212
+ gateways: [gw1]
213
+ }));
214
+ let key = await verifyRequest(await signedRequest(), {
215
+ documentLoader: createLoader({ [id]: signed }),
216
+ contextLoader
217
+ });
218
+ strictEqual(key?.id?.href, keyId.href);
219
+ strictEqual(key?.ownerId?.href, id);
220
+ key = await verifyRequest(await signedRequest(), {
221
+ documentLoader: createLoader({ [id]: await sign(await actorJson({
222
+ id,
223
+ gateways: [gw1],
224
+ assertionMethod: false
225
+ })) }),
226
+ contextLoader
227
+ });
228
+ strictEqual(key?.id?.href, keyId.href);
229
+ const evil = "https://evil.example";
230
+ const evilId = compatibleId(evil);
231
+ const evilKeyId = new URL(`${evilId}#main-key`);
232
+ const unsigned = await actorJson({
233
+ id: evilId,
234
+ gateways: [evil],
235
+ key: evilKeyId
236
+ });
237
+ const documentLoader = createLoader({ [evilId]: unsigned });
238
+ strictEqual(await verifyRequest(await signedRequest(void 0, evilKeyId), {
239
+ documentLoader,
240
+ contextLoader
241
+ }), null);
242
+ strictEqual(await getKeyOwner(evilKeyId, {
243
+ documentLoader,
244
+ contextLoader
245
+ }), null);
246
+ strictEqual(await verifyRequest(await signedRequest(), {
247
+ documentLoader: createLoader({ [id]: await sign(await actorJson({
248
+ id,
249
+ gateways: [gw1]
250
+ }), otherKeyPair.privateKey, `${otherDid}#${otherDid.slice(8)}`) }),
251
+ contextLoader
252
+ }), null);
253
+ });
254
+ test("keys at ordinary URLs cannot belong to portable actors", async () => {
255
+ const evil = "https://evil.example";
256
+ const evilActorId = compatibleId(evil);
257
+ const evilKeyId = new URL(`${evil}/keys/1`);
258
+ const options = {
259
+ documentLoader: createLoader({
260
+ [evilKeyId.href]: {
261
+ "@context": "https://w3id.org/security/v1",
262
+ id: evilKeyId.href,
263
+ type: "Key",
264
+ owner: evilActorId,
265
+ publicKeyPem: await exportSpki(gatewayPublicKey)
266
+ },
267
+ [evilActorId]: await actorJson({
268
+ id: evilActorId,
269
+ gateways: [evil],
270
+ key: evilKeyId,
271
+ assertionMethod: false
272
+ })
273
+ }),
274
+ contextLoader
275
+ };
276
+ strictEqual(await verifyRequest(await signedRequest(void 0, evilKeyId), options), null);
277
+ strictEqual((await fetchKey(evilKeyId, CryptographicKey, options)).key, null);
278
+ strictEqual(await getKeyOwner(evilKeyId, options), null);
279
+ const key = new CryptographicKey({
280
+ id: evilKeyId,
281
+ owner: new URL(evilActorId),
282
+ publicKey: gatewayPublicKey
283
+ });
284
+ strictEqual(await getKeyOwner(key, options), null);
285
+ ok(!await doesActorOwnKey(new Create({
286
+ id: new URL(`${evil}/activities/1`),
287
+ actor: new URL(evilActorId)
288
+ }), key, options));
289
+ const actorKeyId = new URL(`${evil}/users/alice#main-key`);
290
+ for (const document of [await actorJson({
291
+ id: evilActorId,
292
+ gateways: [evil],
293
+ key: actorKeyId
294
+ }), withoutPublicKeyOwner(await actorJson({
295
+ id: evilActorId,
296
+ gateways: [evil],
297
+ key: actorKeyId,
298
+ assertionMethod: false
299
+ }))]) {
300
+ const actorOptions = {
301
+ documentLoader: createLoader({ [`${evil}/users/alice`]: document }),
302
+ contextLoader
303
+ };
304
+ strictEqual(await verifyRequest(await signedRequest(void 0, actorKeyId), actorOptions), null);
305
+ strictEqual((await fetchKey(actorKeyId, CryptographicKey, actorOptions)).key, null);
306
+ strictEqual(await getKeyOwner(actorKeyId, actorOptions), null);
307
+ }
308
+ const ownerKeyId = new URL(`${evil}/keys/2`);
309
+ const ownerOptions = {
310
+ documentLoader: createLoader({
311
+ [ownerKeyId.href]: {
312
+ "@context": "https://w3id.org/security/v1",
313
+ id: ownerKeyId.href,
314
+ type: "Key",
315
+ owner: `${evil}/users/bob`,
316
+ publicKeyPem: await exportSpki(gatewayPublicKey)
317
+ },
318
+ [`${evil}/users/bob`]: await actorJson({
319
+ id: evilActorId,
320
+ gateways: [evil],
321
+ key: ownerKeyId,
322
+ assertionMethod: false
323
+ })
324
+ }),
325
+ contextLoader
326
+ };
327
+ strictEqual(await getKeyOwner(ownerKeyId, ownerOptions), null);
328
+ strictEqual(await getKeyOwner(new CryptographicKey({
329
+ id: ownerKeyId,
330
+ owner: new URL(`${evil}/users/bob`),
331
+ publicKey: gatewayPublicKey
332
+ }), ownerOptions), null);
333
+ const keyCache = {
334
+ get: () => Promise.resolve(key),
335
+ set: () => Promise.resolve()
336
+ };
337
+ strictEqual((await fetchKey(evilKeyId, CryptographicKey, {
338
+ ...options,
339
+ keyCache
340
+ })).key, null);
341
+ });
342
+ const specs = ["draft-cavage-http-signatures-12", "rfc9421"];
343
+ function fetchKeyOf(cls, options) {
344
+ return cls === CryptographicKey ? fetchKey(keyId, CryptographicKey, options) : fetchKey(keyId, Multikey, options);
345
+ }
346
+ function fetchCount(loader, url = keyId) {
347
+ const document = url.href.replace(/#.*$/, "");
348
+ return loader.fetched.filter((u) => u.replace(/#.*$/, "") === document).length;
349
+ }
350
+ test("verifyRequest() caches failures to fetch compatible key IDs", async () => {
351
+ const unreachable = {
352
+ "404 Not Found": createLoader({}),
353
+ "a network error": (url) => Promise.reject(/* @__PURE__ */ new TypeError(`${url}`))
354
+ };
355
+ for (const spec of specs) for (const [name, inner] of Object.entries(unreachable)) {
356
+ const fetched = [];
357
+ const documentLoader = Object.assign((url) => {
358
+ fetched.push(url);
359
+ return inner(url);
360
+ }, { fetched });
361
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
362
+ const options = {
363
+ documentLoader,
364
+ contextLoader,
365
+ keyCache,
366
+ spec
367
+ };
368
+ for (let i = 0; i < 3; i++) {
369
+ const result = await verifyRequestDetailed(await signedRequest(spec), options);
370
+ ok(!result.verified, `${spec}, ${name}`);
371
+ strictEqual(result.reason.type, "keyFetchError", `${spec}, ${name}`);
372
+ }
373
+ strictEqual(fetchCount(documentLoader), 1, `${spec}, ${name}`);
374
+ for (const cls of [CryptographicKey, Multikey]) {
375
+ const result = await fetchKeyOf(cls, options);
376
+ strictEqual(result.key, null);
377
+ ok(result.cached, `${spec}, ${name}, ${cls.name}`);
378
+ }
379
+ strictEqual(fetchCount(documentLoader), 1, `${spec}, ${name}`);
380
+ }
381
+ });
382
+ test("verifyRequest() caches gateway keys only for HTTP Signatures", async () => {
383
+ for (const spec of specs) {
384
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await actorJson()) });
385
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
386
+ const options = {
387
+ documentLoader,
388
+ contextLoader,
389
+ keyCache,
390
+ spec
391
+ };
392
+ for (let i = 0; i < 3; i++) strictEqual((await verifyRequest(await signedRequest(spec), options))?.ownerId?.href, parseIri(actorId).href, spec);
393
+ strictEqual(fetchCount(documentLoader), 1, spec);
394
+ const document = {
395
+ "@context": "https://www.w3.org/ns/activitystreams",
396
+ type: "Create",
397
+ id: "https://gw1.example/activities/1",
398
+ actor: compatibleId(gw1)
399
+ };
400
+ const { signature } = await signJsonLd(document, rsaPrivateKey2, keyId, { contextLoader });
401
+ ok(!await verifyJsonLd({
402
+ ...document,
403
+ signature
404
+ }, options), spec);
405
+ const note = new Note({
406
+ id: new URL("https://gw1.example/notes/1"),
407
+ attribution: new URL(compatibleId(gw1)),
408
+ content: "Hello"
409
+ });
410
+ const proof = await createProof(note, ed25519PrivateKey, keyId, { contextLoader });
411
+ strictEqual(await verifyProof(await note.toJsonLd({
412
+ format: "compact",
413
+ contextLoader
414
+ }), proof, options), null, spec);
415
+ for (const cls of [CryptographicKey, Multikey]) strictEqual((await fetchKeyOf(cls, options)).key, null, spec);
416
+ const fetched = fetchCount(documentLoader);
417
+ ok(await verifyRequest(await signedRequest(spec), options) != null, spec);
418
+ strictEqual(fetchCount(documentLoader), fetched, spec);
419
+ }
420
+ });
421
+ test("verifyRequest() is not rejected by other purposes' negative entries", async () => {
422
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await actorJson()) });
423
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
424
+ const options = {
425
+ documentLoader,
426
+ contextLoader,
427
+ keyCache
428
+ };
429
+ for (const cls of [CryptographicKey, Multikey]) strictEqual((await fetchKeyOf(cls, options)).key, null);
430
+ ok(await verifyRequest(await signedRequest(), options) != null);
431
+ const fetched = fetchCount(documentLoader);
432
+ for (const cls of [CryptographicKey, Multikey]) {
433
+ const result = await fetchKeyOf(cls, options);
434
+ strictEqual(result.key, null);
435
+ ok(result.cached);
436
+ }
437
+ strictEqual(fetchCount(documentLoader), fetched);
438
+ });
439
+ test("fetchKey() does not share negative entries of compatible key IDs across key classes", async () => {
440
+ const ownerId = new URL("https://gw1.example/users/bob");
441
+ const multikey = new Multikey({
442
+ id: keyId,
443
+ controller: ownerId,
444
+ publicKey: ed25519PublicKey.publicKey
445
+ });
446
+ const documentLoader = createLoader({
447
+ [compatibleId(gw1)]: await multikey.toJsonLd({ contextLoader }),
448
+ [ownerId.href]: await new Person({
449
+ id: ownerId,
450
+ assertionMethods: [multikey]
451
+ }).toJsonLd({ contextLoader })
452
+ });
453
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
454
+ const options = {
455
+ documentLoader,
456
+ contextLoader,
457
+ keyCache
458
+ };
459
+ strictEqual((await fetchKey(keyId, CryptographicKey, options)).key, null);
460
+ strictEqual(await verifyRequest(await signedRequest(), options), null);
461
+ const result = await fetchKey(keyId, Multikey, options);
462
+ strictEqual(result.key?.id?.href, keyId.href);
463
+ ok(!result.cached);
464
+ const cached = await fetchKey(keyId, Multikey, options);
465
+ strictEqual(cached.key?.id?.href, keyId.href);
466
+ ok(cached.cached);
467
+ const other = await fetchKey(keyId, CryptographicKey, options);
468
+ strictEqual(other.key, null);
469
+ ok(other.cached);
470
+ });
471
+ test("verifyRequest() caches rejected portable actor documents", async () => {
472
+ for (const spec of specs) {
473
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await actorJson() });
474
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
475
+ const options = {
476
+ documentLoader,
477
+ contextLoader,
478
+ keyCache,
479
+ spec
480
+ };
481
+ for (let i = 0; i < 3; i++) {
482
+ const result = await verifyRequestDetailed(await signedRequest(spec), options);
483
+ ok(!result.verified, spec);
484
+ strictEqual(result.reason.type, "invalidSignature", spec);
485
+ }
486
+ strictEqual(fetchCount(documentLoader), 1, spec);
487
+ }
488
+ });
489
+ test("verifyRequest() refetches a cached gateway key that fails to verify", async () => {
490
+ for (const spec of specs) {
491
+ const responses = { [compatibleId(gw1)]: await sign(await actorJson()) };
492
+ const documentLoader = createLoader(responses);
493
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
494
+ const options = {
495
+ documentLoader,
496
+ contextLoader,
497
+ keyCache,
498
+ spec
499
+ };
500
+ ok(await verifyRequest(await signedRequest(spec), options) != null);
501
+ responses[compatibleId(gw1)] = await sign(await actorJson({
502
+ publicKey: rsaPublicKey3.publicKey,
503
+ assertionMethod: false
504
+ }));
505
+ const rotated = async () => await signRequest(new Request("https://recipient.example/inbox", {
506
+ method: "POST",
507
+ body: "{}",
508
+ headers: { "Content-Type": "application/activity+json" }
509
+ }), rsaPrivateKey3, keyId, { spec });
510
+ ok(await verifyRequest(await rotated(), options) != null, spec);
511
+ strictEqual(fetchCount(documentLoader), 2, spec);
512
+ ok(await verifyRequest(await rotated(), options) != null, spec);
513
+ strictEqual(fetchCount(documentLoader), 2, spec);
514
+ }
515
+ });
516
+ test("verifyRequest() drops a cached gateway key when its refetch fails", async () => {
517
+ for (const spec of specs) {
518
+ const responses = { [compatibleId(gw1)]: await sign(await actorJson()) };
519
+ const documentLoader = createLoader(responses);
520
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
521
+ const options = {
522
+ documentLoader,
523
+ contextLoader,
524
+ keyCache,
525
+ spec
526
+ };
527
+ ok(await verifyRequest(await signedRequest(spec), options) != null);
528
+ delete responses[compatibleId(gw1)];
529
+ const result = await verifyRequestDetailed(await signRequest(new Request("https://recipient.example/inbox", {
530
+ method: "POST",
531
+ body: "{}",
532
+ headers: { "Content-Type": "application/activity+json" }
533
+ }), rsaPrivateKey3, keyId, { spec }), options);
534
+ ok(!result.verified, spec);
535
+ strictEqual(result.reason.type, "keyFetchError", spec);
536
+ strictEqual(fetchCount(documentLoader), 2, spec);
537
+ const next = await verifyRequestDetailed(await signedRequest(spec), options);
538
+ ok(!next.verified, spec);
539
+ strictEqual(next.reason.type, "keyFetchError", spec);
540
+ strictEqual(fetchCount(documentLoader), 2, spec);
541
+ }
542
+ });
543
+ test("verifyRequest() does not trust a cached gateway key for longer than its proof", async () => {
544
+ let now = Temporal.Now.instant();
545
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"], { now: () => now });
546
+ const expires = now.add({ minutes: 30 });
547
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await actorJson(), ed25519PrivateKey, didKeyId, { expires: expires.toString() }) });
548
+ const options = {
549
+ documentLoader,
550
+ contextLoader,
551
+ keyCache
552
+ };
553
+ ok(await verifyRequest(await signedRequest(), options) != null);
554
+ now = expires.subtract({ seconds: 1 });
555
+ ok(await verifyRequest(await signedRequest(), options) != null);
556
+ strictEqual(fetchCount(documentLoader), 1);
557
+ now = expires;
558
+ ok(await verifyRequest(await signedRequest(), options) != null);
559
+ strictEqual(fetchCount(documentLoader), 2);
560
+ });
561
+ test("verifyRequest() looks up a cached gateway key again after an hour", async () => {
562
+ let now = Temporal.Now.instant();
563
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"], { now: () => now });
564
+ const responses = { [compatibleId(gw1)]: await sign(await actorJson()) };
565
+ const documentLoader = createLoader(responses);
566
+ const options = {
567
+ documentLoader,
568
+ contextLoader,
569
+ keyCache
570
+ };
571
+ const start = now;
572
+ ok(await verifyRequest(await signedRequest(), options) != null);
573
+ now = start.add({ minutes: 59 });
574
+ ok(await verifyRequest(await signedRequest(), options) != null);
575
+ strictEqual(fetchCount(documentLoader), 1);
576
+ responses[compatibleId(gw1)] = await sign(await actorJson({ gateways: [gw2] }));
577
+ now = start.add({ hours: 1 });
578
+ strictEqual(await verifyRequest(await signedRequest(), options), null);
579
+ strictEqual(fetchCount(documentLoader), 2);
580
+ });
581
+ test("verifyRequest() rejects and caches gateway keys of expired documents", async () => {
582
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await actorJson(), ed25519PrivateKey, didKeyId, { expires: "2000-01-01T00:00:00Z" }) });
583
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
584
+ const options = {
585
+ documentLoader,
586
+ contextLoader,
587
+ keyCache
588
+ };
589
+ strictEqual(await verifyRequest(await signedRequest(), options), null);
590
+ strictEqual(await verifyRequest(await signedRequest(), options), null);
591
+ strictEqual(fetchCount(documentLoader), 1);
592
+ });
593
+ test("verifyRequest() takes the expiration of a proof whatever its context", async () => {
594
+ let now = Temporal.Now.instant();
595
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"], { now: () => now });
596
+ const expires = now.add({ minutes: 30 });
597
+ const actor = await actorJson();
598
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign({
599
+ ...actor,
600
+ "@context": [...actor["@context"], { expires: "https://example.com/ns#notExpiration" }]
601
+ }, ed25519PrivateKey, didKeyId, { expires: expires.toString() }) });
602
+ const options = {
603
+ documentLoader,
604
+ contextLoader,
605
+ keyCache
606
+ };
607
+ ok(await verifyRequest(await signedRequest(), options) != null);
608
+ now = expires.subtract({ seconds: 1 });
609
+ ok(await verifyRequest(await signedRequest(), options) != null);
610
+ strictEqual(fetchCount(documentLoader), 1);
611
+ now = expires;
612
+ ok(await verifyRequest(await signedRequest(), options) != null);
613
+ strictEqual(fetchCount(documentLoader), 2);
614
+ });
615
+ test("a key cache without namespaces caches only fetch failures of compatible key IDs", async () => {
616
+ const calls = [];
617
+ const entries = /* @__PURE__ */ new Map();
618
+ const keyCache = {
619
+ get(id) {
620
+ calls.push(`get ${id.href}`);
621
+ return Promise.resolve(entries.get(id.href));
622
+ },
623
+ set(id, key) {
624
+ calls.push(`set ${id.href} ${key == null ? "null" : "key"}`);
625
+ entries.set(id.href, key);
626
+ return Promise.resolve();
627
+ }
628
+ };
629
+ const options = {
630
+ documentLoader: createLoader({ [compatibleId(gw1)]: await sign(await actorJson()) }),
631
+ contextLoader,
632
+ keyCache
633
+ };
634
+ ok(await verifyRequest(await signedRequest(), options) != null);
635
+ for (const cls of [CryptographicKey, Multikey]) strictEqual((await fetchKeyOf(cls, options)).key, null);
636
+ ok(await verifyRequest(await signedRequest(), options) != null);
637
+ deepStrictEqual(calls.filter((c) => c.startsWith("set ") && c.includes(keyId.origin)), []);
638
+ const unreachable = new URL(`${compatibleId(gw2)}#main-key`);
639
+ strictEqual((await fetchKey(unreachable, CryptographicKey, options)).key, null);
640
+ deepStrictEqual(calls.filter((c) => c.startsWith("set ") && c.includes(gw2)), [`set ${unreachable.href} null`]);
641
+ });
642
+ test("doesActorOwnKey() and getKeyOwner() resolve gateway keys to portable actors", async () => {
643
+ const options = {
644
+ documentLoader: createLoader({ [compatibleId(gw1)]: await sign(await actorJson()) }),
645
+ contextLoader
646
+ };
647
+ const key = await verifyRequest(await signedRequest(), options);
648
+ ok(key != null);
649
+ const activity = (actor) => new Create({
650
+ id: parseIri(`ap+ef61://${did}/activities/1`),
651
+ actor: parseIri(actor)
652
+ });
653
+ ok(await doesActorOwnKey(activity(actorId), key, options));
654
+ ok(await doesActorOwnKey(activity(compatibleId(gw2)), key, options));
655
+ ok(!await doesActorOwnKey(activity(`${actorId}/other`), key, options));
656
+ strictEqual((await getKeyOwner(keyId, options))?.id?.href, parseIri(actorId).href);
657
+ strictEqual((await getKeyOwner(key, options))?.id?.href, key.ownerId?.href);
658
+ const forged = key.clone({ publicKey: rsaPublicKey3.publicKey });
659
+ strictEqual(await getKeyOwner(forged, options), null);
660
+ ok(!await doesActorOwnKey(activity(actorId), forged, options));
661
+ });
662
+ test("doesActorOwnKey() and getKeyOwner() do not fall back for rejected gateway keys", async () => {
663
+ const options = {
664
+ documentLoader: createLoader({ [compatibleId(gw1)]: await actorJson() }),
665
+ contextLoader
666
+ };
667
+ const key = new CryptographicKey({
668
+ id: keyId,
669
+ owner: parseIri(actorId),
670
+ publicKey: gatewayPublicKey
671
+ });
672
+ ok(!await doesActorOwnKey(new Create({
673
+ id: parseIri(`ap+ef61://${did}/activities/1`),
674
+ actor: parseIri(actorId)
675
+ }), key, options));
676
+ strictEqual(await getKeyOwner(keyId, options), null);
677
+ strictEqual(await getKeyOwner(key, options), null);
678
+ });
679
+ test("verifyRequest() binds gateway keys to the actor whose document is signed", async () => {
680
+ const attackerDid = otherDid;
681
+ const attackerActorId = `ap+ef61://${attackerDid}/actor`;
682
+ const shiftingContext = "https://attacker.example/context";
683
+ let loads = 0;
684
+ const shiftingContextLoader = (url) => {
685
+ if (url !== shiftingContext) return contextLoader(url);
686
+ loads++;
687
+ const document = { "@context": loads <= 2 ? {
688
+ victimId: "@id",
689
+ attackerId: "https://attacker.example/ns#a"
690
+ } : {
691
+ victimId: "https://attacker.example/ns#v",
692
+ attackerId: "@id"
693
+ } };
694
+ return Promise.resolve({
695
+ contextUrl: null,
696
+ documentUrl: url,
697
+ document
698
+ });
699
+ };
700
+ const { id: _, ...victim } = await actorJson({ gateways: [gw1] });
701
+ const document = await sign({
702
+ ...victim,
703
+ "@context": [...victim["@context"], shiftingContext],
704
+ victimId: actorId,
705
+ attackerId: attackerActorId
706
+ }, otherKeyPair.privateKey, `${attackerDid}#${attackerDid.slice(8)}`);
707
+ const documentLoader = createLoader({ [compatibleId(gw1)]: document });
708
+ strictEqual(await verifyRequest(await signedRequest(), {
709
+ documentLoader,
710
+ contextLoader: shiftingContextLoader
711
+ }), null);
712
+ ok(loads > 2, "the shifting context was loaded for the proof");
713
+ loads = 0;
714
+ strictEqual(await getKeyOwner(keyId, {
715
+ documentLoader,
716
+ contextLoader: shiftingContextLoader
717
+ }), null);
718
+ });
719
+ test("verifyRequest() matches gateway keys listed under ap: URIs", async () => {
720
+ const portableKeyId = parseIri(`${actorId}#main-key`);
721
+ const cases = {
722
+ "both properties": await actorJson({ key: portableKeyId }),
723
+ "assertionMethod only": await actorJson({
724
+ key: portableKeyId,
725
+ publicKey: null
726
+ }),
727
+ "publicKey only": await actorJson({
728
+ key: portableKeyId,
729
+ assertionMethod: false
730
+ })
731
+ };
732
+ for (const [name, document] of Object.entries(cases)) {
733
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(document) });
734
+ for (const spec of specs) {
735
+ const key = await verifyRequest(await signedRequest(spec), {
736
+ documentLoader,
737
+ contextLoader,
738
+ spec
739
+ });
740
+ strictEqual(key?.id?.href, keyId.href, `${name}, ${spec}`);
741
+ strictEqual(key?.ownerId?.href, parseIri(actorId).href, name);
742
+ }
743
+ }
744
+ });
745
+ test("verifyRequest() rejects ambiguous or foreign aliases of gateway keys", async () => {
746
+ const portableKeyId = `${actorId}#main-key`;
747
+ const base = await actorJson();
748
+ const assertionMethod = base.assertionMethod;
749
+ const publicKey = base.publicKey;
750
+ const cases = {
751
+ "an exact entry and an ap: alias in assertionMethod": {
752
+ ...base,
753
+ assertionMethod: [assertionMethod, {
754
+ ...assertionMethod,
755
+ id: portableKeyId
756
+ }]
757
+ },
758
+ "a reference and an embedded ap: alias in assertionMethod": {
759
+ ...base,
760
+ assertionMethod: [keyId.href, {
761
+ ...assertionMethod,
762
+ id: portableKeyId
763
+ }]
764
+ },
765
+ "an exact entry and an ap: alias in publicKey": {
766
+ ...base,
767
+ publicKey: [publicKey, {
768
+ ...publicKey,
769
+ id: portableKeyId
770
+ }]
771
+ },
772
+ "a key of another gateway": await actorJson({ key: new URL(`${compatibleId(gw2)}#main-key`) }),
773
+ "an ap: key ID with dot segments": {
774
+ ...base,
775
+ assertionMethod: {
776
+ ...assertionMethod,
777
+ id: portableKeyId.replace("/actor#", "/x/../actor#")
778
+ },
779
+ publicKey: {
780
+ ...publicKey,
781
+ id: portableKeyId.replace("/actor#", "/x/../actor#")
782
+ }
783
+ },
784
+ "a compatible key ID with dot segments": {
785
+ ...base,
786
+ assertionMethod: {
787
+ ...assertionMethod,
788
+ id: keyId.href.replace("/actor#", "/x/../actor#")
789
+ },
790
+ publicKey: {
791
+ ...publicKey,
792
+ id: keyId.href.replace("/actor#", "/x/../actor#")
793
+ }
794
+ }
795
+ };
796
+ for (const [name, document] of Object.entries(cases)) {
797
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(document) });
798
+ strictEqual(await verifyRequest(await signedRequest(), {
799
+ documentLoader,
800
+ contextLoader
801
+ }), null, name);
802
+ }
803
+ });
804
+ test("verifyRequest() refuses key IDs whose identity URL parsing changes", async () => {
805
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await actorJson()) });
806
+ const request = await signedRequest("draft-cavage-http-signatures-12");
807
+ const dotted = keyId.href.replace("/actor#", "/x/../actor#");
808
+ const headers = new Headers(request.headers);
809
+ headers.set("Signature", headers.get("Signature").replace(keyId.href, dotted));
810
+ strictEqual(await verifyRequest(new Request(request, { headers }), {
811
+ documentLoader,
812
+ contextLoader
813
+ }), null);
814
+ strictEqual(documentLoader.fetched.length, 0);
815
+ const dottedPortable = `${actorId.replace("/actor", "/x/../actor")}#main-key`;
816
+ for (const id of [
817
+ dotted,
818
+ dottedPortable,
819
+ ` ${dotted}`,
820
+ ` ${dottedPortable}`,
821
+ ` ${parseIri(actorId).href}#main-key`,
822
+ dottedPortable.replace("ap+ef61:", "ap+ ef61:"),
823
+ dotted.replace("did:key:", "did:ke y:"),
824
+ `${gw1}/.well-known/apgateway/${did}/../../../users/alice#main-key`,
825
+ ` ap://not-a-did/actor#main-key`
826
+ ]) {
827
+ let error;
828
+ try {
829
+ await fetchKey(id, CryptographicKey, {
830
+ documentLoader,
831
+ contextLoader
832
+ });
833
+ } catch (e) {
834
+ error = e;
835
+ }
836
+ ok(error instanceof TypeError, id);
837
+ }
838
+ });
839
+ const apKeyId = parseIri(`${actorId}#main-key`);
840
+ function hinted(...gateways) {
841
+ const query = gateways.map((g) => `@gateway=${encodeURIComponent(g)}`).join("&");
842
+ return parseIri(`${actorId}${query === "" ? "" : `?${query}`}#main-key`);
843
+ }
844
+ async function apActorJson(options = {}) {
845
+ return await actorJson({
846
+ key: apKeyId,
847
+ ...options
848
+ });
849
+ }
850
+ test("verifyRequest() accepts keys of portable actors at ap: key IDs", async () => {
851
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await apActorJson()) });
852
+ const hintedKeyId = hinted(gw1);
853
+ for (const spec of specs) {
854
+ const key = await verifyRequest(await signedRequest(spec, hintedKeyId), {
855
+ documentLoader,
856
+ contextLoader,
857
+ spec
858
+ });
859
+ ok(key != null, spec);
860
+ strictEqual(key.id?.href, hintedKeyId.href, spec);
861
+ strictEqual(key.ownerId?.href, parseIri(actorId).href, spec);
862
+ }
863
+ const raw = `ap://${did}/actor?@gateway=${encodeURIComponent(gw1)}#main-key`;
864
+ for (const written of [raw, raw.replace(/^ap:/, "ap+ef61:")]) {
865
+ const request = await signedRequest("draft-cavage-http-signatures-12", hintedKeyId);
866
+ const headers = new Headers(request.headers);
867
+ headers.set("Signature", headers.get("Signature").replace(hintedKeyId.href, written));
868
+ strictEqual((await verifyRequest(new Request(request, { headers }), {
869
+ documentLoader,
870
+ contextLoader
871
+ }))?.ownerId?.href, parseIri(actorId).href, written);
872
+ }
873
+ deepStrictEqual([...new Set(documentLoader.fetched)], [compatibleId(gw1)]);
874
+ });
875
+ test("verifyRequest() asks the document loader for ap: key IDs without hints", async () => {
876
+ const documentLoader = createLoader({ [actorId]: await sign(await apActorJson()) });
877
+ strictEqual((await verifyRequest(await signedRequest(void 0, apKeyId), {
878
+ documentLoader,
879
+ contextLoader
880
+ }))?.ownerId?.href, parseIri(actorId).href);
881
+ deepStrictEqual(documentLoader.fetched, [actorId]);
882
+ const result = await verifyRequestDetailed(await signedRequest(void 0, apKeyId), {
883
+ documentLoader: createLoader({}),
884
+ contextLoader
885
+ });
886
+ ok(!result.verified);
887
+ strictEqual(result.reason.type, "keyFetchError");
888
+ });
889
+ test("verifyRequest() rejects keys at ap: key IDs that portable actors do not vouch for", async () => {
890
+ const unsigned = await apActorJson();
891
+ const ordinaryActorId = "https://gw1.example/users/alice";
892
+ const cases = {
893
+ "an unsigned actor document": unsigned,
894
+ "an actor document signed by another DID": await sign(unsigned, otherKeyPair.privateKey, `${otherDid}#${otherDid.slice(8)}`),
895
+ "a key missing from both assertionMethod and publicKey": await sign(await apActorJson({
896
+ assertionMethod: false,
897
+ publicKey: null
898
+ })),
899
+ "a key referred to by URL": await sign({
900
+ ...await apActorJson({
901
+ publicKey: null,
902
+ assertionMethod: false
903
+ }),
904
+ assertionMethod: [apKeyId.href]
905
+ }),
906
+ "a key only in publicKey without an owner": await sign(withoutPublicKeyOwner(await apActorJson({ assertionMethod: false }))),
907
+ "a publicKey entry with other key material": await sign(await apActorJson({ publicKey: rsaPublicKey3.publicKey })),
908
+ "a key only at a compatible identifier": await sign(await actorJson()),
909
+ "an actor without gateways": await sign(await apActorJson({ gateways: [] })),
910
+ "the document of another actor": await sign(await apActorJson({ id: `${actorId}/other` })),
911
+ "an ordinary actor": await actorJson({
912
+ id: ordinaryActorId,
913
+ key: apKeyId
914
+ }),
915
+ "a standalone key": {
916
+ "@context": "https://w3id.org/security/v1",
917
+ id: apKeyId.href,
918
+ type: "Key",
919
+ owner: actorId,
920
+ publicKeyPem: await exportSpki(gatewayPublicKey)
921
+ }
922
+ };
923
+ for (const [name, document] of Object.entries(cases)) {
924
+ const documentLoader = createLoader({ [compatibleId(gw1)]: document });
925
+ strictEqual(await verifyRequest(await signedRequest(void 0, hinted(gw1)), {
926
+ documentLoader,
927
+ contextLoader
928
+ }), null, name);
929
+ }
930
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await apActorJson()) });
931
+ strictEqual(await verifyRequest(await signedRequest(void 0, parseIri(`${actorId}?@gateway=${encodeURIComponent(gw1)}`)), {
932
+ documentLoader,
933
+ contextLoader
934
+ }), null);
935
+ strictEqual(documentLoader.fetched.length, 0);
936
+ });
937
+ test("verifyRequest() tries the gateways of ap: key IDs in order", async () => {
938
+ const signed = await sign(await apActorJson());
939
+ const cases = {
940
+ "an invalid document at the first gateway": {
941
+ [compatibleId(gw1)]: await apActorJson(),
942
+ [compatibleId(gw2)]: signed
943
+ },
944
+ "an unreachable first gateway": { [compatibleId(gw2)]: signed }
945
+ };
946
+ for (const [name, responses] of Object.entries(cases)) {
947
+ const documentLoader = createLoader(responses);
948
+ strictEqual((await verifyRequest(await signedRequest(void 0, hinted(gw1, gw2)), {
949
+ documentLoader,
950
+ contextLoader
951
+ }))?.ownerId?.href, parseIri(actorId).href, name);
952
+ deepStrictEqual(documentLoader.fetched, [compatibleId(gw1), compatibleId(gw2)], name);
953
+ }
954
+ const documentLoader = createLoader({
955
+ [compatibleId(gw1)]: signed,
956
+ [compatibleId(gw2)]: signed
957
+ });
958
+ ok(await verifyRequest(await signedRequest(void 0, hinted(gw1, gw2)), {
959
+ documentLoader,
960
+ contextLoader
961
+ }) != null);
962
+ deepStrictEqual(documentLoader.fetched, [compatibleId(gw1)]);
963
+ const gateways = Array.from({ length: 7 }, (_, i) => `https://gw${i + 10}.example`);
964
+ const many = createLoader({});
965
+ ok(!(await verifyRequestDetailed(await signedRequest(void 0, hinted(...gateways)), {
966
+ documentLoader: many,
967
+ contextLoader
968
+ })).verified);
969
+ deepStrictEqual(many.fetched, gateways.slice(0, 3).map((g) => compatibleId(g)));
970
+ for (const [index, expected] of [[2, true], [3, false]]) {
971
+ const documentLoader = createLoader({ [compatibleId(gateways[index])]: signed });
972
+ strictEqual(await verifyRequest(await signedRequest(void 0, hinted(...gateways)), {
973
+ documentLoader,
974
+ contextLoader
975
+ }) != null, expected, `gateway ${index + 1}`);
976
+ deepStrictEqual(documentLoader.fetched, gateways.slice(0, 3).map((g) => compatibleId(g)), `gateway ${index + 1}`);
977
+ }
978
+ const sparse = createLoader({ [compatibleId(gateways[2])]: signed });
979
+ ok(await verifyRequest(await signedRequest(void 0, hinted("not a gateway", gateways[0], gateways[0].toUpperCase(), "https://gw.example/path", gateways[1], gateways[2])), {
980
+ documentLoader: sparse,
981
+ contextLoader
982
+ }) != null);
983
+ deepStrictEqual(sparse.fetched, gateways.slice(0, 3).map((g) => compatibleId(g)));
984
+ });
985
+ function failingLoader(responses) {
986
+ const fetched = [];
987
+ const loader = (url) => {
988
+ fetched.push(url);
989
+ const response = responses[url.replace(/#.*$/, "")] ?? 404;
990
+ if (typeof response === "number") return Promise.reject(new FetchError(url, `HTTP ${response}`, new Response(null, { status: response })));
991
+ return Promise.resolve({
992
+ contextUrl: null,
993
+ documentUrl: url,
994
+ document: structuredClone(response)
995
+ });
996
+ };
997
+ return Object.assign(loader, { fetched });
998
+ }
999
+ test("verifyRequest() reports why keys at ap: key IDs could not be looked up", async () => {
1000
+ const hintedKeyId = hinted(gw1, gw2);
1001
+ const invalid = await apActorJson();
1002
+ const cases = [
1003
+ {
1004
+ name: "every gateway serves an invalid document",
1005
+ responses: {
1006
+ [compatibleId(gw1)]: invalid,
1007
+ [compatibleId(gw2)]: invalid
1008
+ },
1009
+ reason: "invalidSignature",
1010
+ cached: true
1011
+ },
1012
+ {
1013
+ name: "every gateway responds with 410 Gone",
1014
+ responses: {
1015
+ [compatibleId(gw1)]: 410,
1016
+ [compatibleId(gw2)]: 410
1017
+ },
1018
+ reason: "keyFetchError",
1019
+ status: 410,
1020
+ cached: true
1021
+ },
1022
+ {
1023
+ name: "the gateways respond with different statuses",
1024
+ responses: {
1025
+ [compatibleId(gw1)]: 404,
1026
+ [compatibleId(gw2)]: 503
1027
+ },
1028
+ reason: "keyFetchError",
1029
+ cached: true
1030
+ },
1031
+ {
1032
+ name: "one gateway serves an invalid document, the other is gone",
1033
+ responses: {
1034
+ [compatibleId(gw1)]: invalid,
1035
+ [compatibleId(gw2)]: 410
1036
+ },
1037
+ reason: "keyFetchError",
1038
+ cached: false
1039
+ }
1040
+ ];
1041
+ for (const { name, responses, reason, status, cached } of cases) for (const spec of specs) {
1042
+ const documentLoader = failingLoader(responses);
1043
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
1044
+ const options = {
1045
+ documentLoader,
1046
+ contextLoader,
1047
+ keyCache,
1048
+ spec
1049
+ };
1050
+ for (let i = 0; i < 2; i++) {
1051
+ const result = await verifyRequestDetailed(await signedRequest(spec, hintedKeyId), options);
1052
+ ok(!result.verified, `${name}, ${spec}`);
1053
+ strictEqual(result.reason.type, reason, `${name}, ${spec}`);
1054
+ if (result.reason.type === "keyFetchError") strictEqual("status" in result.reason.result ? result.reason.result.status : void 0, status, `${name}, ${spec}`);
1055
+ }
1056
+ strictEqual(documentLoader.fetched.length, cached ? 2 : 4, `${name}, ${spec}`);
1057
+ }
1058
+ });
1059
+ test("verifyRequest() accepts keys at ap: key IDs only for HTTP Signatures", async () => {
1060
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await apActorJson()) });
1061
+ const hintedKeyId = hinted(gw1);
1062
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
1063
+ const options = {
1064
+ documentLoader,
1065
+ contextLoader,
1066
+ keyCache
1067
+ };
1068
+ for (const cls of [CryptographicKey, Multikey]) strictEqual((cls === CryptographicKey ? await fetchKey(hintedKeyId, CryptographicKey, options) : await fetchKey(hintedKeyId, Multikey, options)).key, null);
1069
+ strictEqual(documentLoader.fetched.length, 0);
1070
+ for (let i = 0; i < 3; i++) {
1071
+ const key = await verifyRequest(await signedRequest(void 0, hintedKeyId), options);
1072
+ strictEqual(key?.id?.href, hintedKeyId.href);
1073
+ strictEqual(key?.ownerId?.href, parseIri(actorId).href);
1074
+ }
1075
+ strictEqual(documentLoader.fetched.length, 1);
1076
+ for (const cls of [CryptographicKey, Multikey]) strictEqual((cls === CryptographicKey ? await fetchKey(hintedKeyId, CryptographicKey, options) : await fetchKey(hintedKeyId, Multikey, options)).key, null);
1077
+ const document = {
1078
+ "@context": "https://www.w3.org/ns/activitystreams",
1079
+ type: "Create",
1080
+ id: "https://gw1.example/activities/1",
1081
+ actor: compatibleId(gw1)
1082
+ };
1083
+ const { signature } = await signJsonLd(document, rsaPrivateKey2, hintedKeyId, { contextLoader });
1084
+ ok(!await verifyJsonLd({
1085
+ ...document,
1086
+ signature
1087
+ }, options));
1088
+ strictEqual(documentLoader.fetched.length, 1);
1089
+ const legacyCache = {
1090
+ get: () => Promise.resolve(new CryptographicKey({
1091
+ id: hintedKeyId,
1092
+ owner: parseIri(actorId),
1093
+ publicKey: gatewayPublicKey
1094
+ })),
1095
+ set: () => Promise.resolve()
1096
+ };
1097
+ const empty = createLoader({});
1098
+ strictEqual(await verifyRequest(await signedRequest(void 0, hintedKeyId), {
1099
+ documentLoader: empty,
1100
+ contextLoader,
1101
+ keyCache: legacyCache
1102
+ }), null);
1103
+ strictEqual(empty.fetched.length, 1);
1104
+ });
1105
+ test("verifyRequest() refreshes a cached key at an ap: key ID that fails to verify", async () => {
1106
+ for (const spec of specs) {
1107
+ const responses = { [compatibleId(gw1)]: await sign(await apActorJson()) };
1108
+ const documentLoader = failingLoader(responses);
1109
+ const hintedKeyId = hinted(gw1, gw2);
1110
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
1111
+ const options = {
1112
+ documentLoader,
1113
+ contextLoader,
1114
+ keyCache,
1115
+ spec
1116
+ };
1117
+ ok(await verifyRequest(await signedRequest(spec, hintedKeyId), options) != null);
1118
+ strictEqual(documentLoader.fetched.length, 1, spec);
1119
+ responses[compatibleId(gw1)] = await apActorJson();
1120
+ responses[compatibleId(gw2)] = 503;
1121
+ ok(!(await verifyRequestDetailed(await signRequest(new Request("https://recipient.example/inbox", {
1122
+ method: "POST",
1123
+ body: "{}",
1124
+ headers: { "Content-Type": "application/activity+json" }
1125
+ }), rsaPrivateKey3, hintedKeyId, { spec }), options)).verified, spec);
1126
+ strictEqual(documentLoader.fetched.length, 3, spec);
1127
+ const next = await verifyRequestDetailed(await signedRequest(spec, hintedKeyId), options);
1128
+ ok(!next.verified, spec);
1129
+ strictEqual(next.reason.type, "keyFetchError", spec);
1130
+ strictEqual(documentLoader.fetched.length, 5, spec);
1131
+ }
1132
+ });
1133
+ test("doesActorOwnKey() and getKeyOwner() resolve keys at ap: key IDs to portable actors", async () => {
1134
+ const options = {
1135
+ documentLoader: createLoader({ [compatibleId(gw1)]: await sign(await apActorJson()) }),
1136
+ contextLoader
1137
+ };
1138
+ const hintedKeyId = hinted(gw1);
1139
+ const key = new CryptographicKey({
1140
+ id: hintedKeyId,
1141
+ owner: parseIri(actorId),
1142
+ publicKey: gatewayPublicKey
1143
+ });
1144
+ const activity = new Create({
1145
+ id: parseIri(`${actorId.replace("/actor", "")}/activities/1`),
1146
+ actor: parseIri(`ap://${did}/actor`)
1147
+ });
1148
+ ok(await doesActorOwnKey(activity, key, options));
1149
+ strictEqual((await getKeyOwner(hintedKeyId, options))?.id?.href, parseIri(actorId).href);
1150
+ strictEqual((await getKeyOwner(key, options))?.id?.href, parseIri(actorId).href);
1151
+ ok(!await doesActorOwnKey(new Create({
1152
+ id: parseIri(`ap+ef61://${otherDid}/activities/1`),
1153
+ actor: parseIri(`ap+ef61://${otherDid}/actor`)
1154
+ }), key, options));
1155
+ const other = key.clone({ publicKey: rsaPublicKey3.publicKey });
1156
+ ok(!await doesActorOwnKey(activity, other, options));
1157
+ strictEqual(await getKeyOwner(other, options), null);
1158
+ const unsigned = {
1159
+ documentLoader: createLoader({ [compatibleId(gw1)]: await apActorJson() }),
1160
+ contextLoader
1161
+ };
1162
+ ok(!await doesActorOwnKey(activity, key, unsigned));
1163
+ strictEqual(await getKeyOwner(hintedKeyId, unsigned), null);
1164
+ });
1165
+ test("verifyRequest() does not trust a cached key at an ap: key ID for longer than its proof", async () => {
1166
+ let now = Temporal.Now.instant();
1167
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"], { now: () => now });
1168
+ const expires = now.add({ minutes: 30 });
1169
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await apActorJson(), ed25519PrivateKey, didKeyId, { expires: expires.toString() }) });
1170
+ const options = {
1171
+ documentLoader,
1172
+ contextLoader,
1173
+ keyCache
1174
+ };
1175
+ const hintedKeyId = hinted(gw1);
1176
+ ok(await verifyRequest(await signedRequest(void 0, hintedKeyId), options) != null);
1177
+ now = expires.subtract({ seconds: 1 });
1178
+ ok(await verifyRequest(await signedRequest(void 0, hintedKeyId), options) != null);
1179
+ strictEqual(documentLoader.fetched.length, 1);
1180
+ now = expires;
1181
+ ok(await verifyRequest(await signedRequest(void 0, hintedKeyId), options) != null);
1182
+ strictEqual(documentLoader.fetched.length, 2);
1183
+ });
1184
+ test("verifyRequest() does not cache keys at ap: key IDs it could not process", async () => {
1185
+ let failures = 1;
1186
+ const flakyContextLoader = (url, options) => {
1187
+ if (failures > 0) {
1188
+ failures--;
1189
+ return Promise.reject(/* @__PURE__ */ new Error(`Temporarily failed to load ${url}`));
1190
+ }
1191
+ return contextLoader(url, options);
1192
+ };
1193
+ const documentLoader = createLoader({ [compatibleId(gw1)]: await sign(await apActorJson()) });
1194
+ const options = {
1195
+ documentLoader,
1196
+ contextLoader: flakyContextLoader,
1197
+ keyCache: new KvKeyCache(new MemoryKvStore(), ["pk"])
1198
+ };
1199
+ const hintedKeyId = hinted(gw1);
1200
+ const result = await verifyRequestDetailed(await signedRequest(void 0, hintedKeyId), options);
1201
+ ok(!result.verified);
1202
+ strictEqual(result.reason.type, "keyFetchError");
1203
+ if (result.reason.type === "keyFetchError") ok(!("status" in result.reason.result));
1204
+ strictEqual((await verifyRequest(await signedRequest(void 0, hintedKeyId), options))?.ownerId?.href, parseIri(actorId).href);
1205
+ strictEqual(documentLoader.fetched.length, 2);
1206
+ });
1207
+ /**
1208
+ * A loader that serves the given responses, and never responds for the URLs
1209
+ * in `hanging`. A hanging response is given up when its signal aborts,
1210
+ * unless `ignoreSignal` is set.
1211
+ */
1212
+ function controlledLoader(responses, hanging, { ignoreSignal = false, delays = {} } = {}) {
1213
+ const fetched = [];
1214
+ const signals = [];
1215
+ const respond = failingLoader(responses);
1216
+ const loader = async (url, options) => {
1217
+ fetched.push(url);
1218
+ signals.push(options?.signal);
1219
+ const signal = ignoreSignal ? void 0 : options?.signal;
1220
+ const delay = delays[url];
1221
+ if (delay != null) await new Promise((resolve) => setTimeout(resolve, delay));
1222
+ if (hanging.includes(url)) return await new Promise((_, reject) => {
1223
+ signal?.addEventListener("abort", () => reject(signal.reason));
1224
+ });
1225
+ return await respond(url);
1226
+ };
1227
+ return Object.assign(loader, {
1228
+ fetched,
1229
+ signals
1230
+ });
1231
+ }
1232
+ function isTimeoutError(error) {
1233
+ return error instanceof DOMException && error.name === "TimeoutError";
1234
+ }
1235
+ test("resolvePortableActorKey() gives up gateways that do not respond in time", async () => {
1236
+ const signed = await sign(await apActorJson());
1237
+ for (const ignoreSignal of [false, true]) {
1238
+ const documentLoader = controlledLoader({ [compatibleId(gw2)]: signed }, [compatibleId(gw1)], { ignoreSignal });
1239
+ const resolution = await resolvePortableActorKey(hinted(gw1, gw2), {
1240
+ documentLoader,
1241
+ contextLoader,
1242
+ lookupTimeout: 50
1243
+ });
1244
+ strictEqual(resolution.type, "unavailable", `ignoreSignal: ${ignoreSignal}`);
1245
+ if (resolution.type === "unavailable") strictEqual(resolution.cacheable, false);
1246
+ deepStrictEqual(documentLoader.fetched, [compatibleId(gw1)]);
1247
+ const [signal] = documentLoader.signals;
1248
+ ok(signal?.aborted);
1249
+ ok(isTimeoutError(signal.reason));
1250
+ }
1251
+ });
1252
+ test("resolvePortableActorKey() caches timeouts only as failures to fetch", async () => {
1253
+ const invalid = await apActorJson();
1254
+ const cases = [
1255
+ {
1256
+ name: "the only gateway times out",
1257
+ responses: {},
1258
+ hanging: [compatibleId(gw1)],
1259
+ hints: [gw1],
1260
+ cacheable: true
1261
+ },
1262
+ {
1263
+ name: "a gateway responds with 404, and the last one times out",
1264
+ responses: { [compatibleId(gw1)]: 404 },
1265
+ hanging: [compatibleId(gw2)],
1266
+ hints: [gw1, gw2],
1267
+ cacheable: true
1268
+ },
1269
+ {
1270
+ name: "a gateway serves an invalid document, and the last one times out",
1271
+ responses: { [compatibleId(gw1)]: invalid },
1272
+ hanging: [compatibleId(gw2)],
1273
+ hints: [gw1, gw2],
1274
+ cacheable: false
1275
+ },
1276
+ {
1277
+ name: "the document loader cannot resolve the ap: URI in time",
1278
+ responses: {},
1279
+ hanging: [actorId],
1280
+ hints: [],
1281
+ cacheable: true
1282
+ }
1283
+ ];
1284
+ for (const { name, responses, hanging, hints, cacheable } of cases) {
1285
+ const documentLoader = controlledLoader(responses, hanging);
1286
+ const resolution = await resolvePortableActorKey(hinted(...hints), {
1287
+ documentLoader,
1288
+ contextLoader,
1289
+ lookupTimeout: 50
1290
+ });
1291
+ strictEqual(resolution.type, "unavailable", name);
1292
+ if (resolution.type !== "unavailable") continue;
1293
+ strictEqual(resolution.cacheable, cacheable, name);
1294
+ ok(!(resolution.error instanceof FetchError), name);
1295
+ if (hanging.length === 1 && hints.length < 2) ok(isTimeoutError(resolution.error), name);
1296
+ }
1297
+ });
1298
+ test("resolvePortableActorKey() shares one timeout among all gateways", async () => {
1299
+ const documentLoader = controlledLoader({ [compatibleId(gw1)]: 404 }, [compatibleId(gw2)], { delays: { [compatibleId(gw1)]: 250 } });
1300
+ const started = Date.now();
1301
+ const resolution = await resolvePortableActorKey(hinted(gw1, gw2), {
1302
+ documentLoader,
1303
+ contextLoader,
1304
+ lookupTimeout: 300
1305
+ });
1306
+ const elapsed = Date.now() - started;
1307
+ ok(elapsed < 500, `took ${elapsed} ms`);
1308
+ strictEqual(resolution.type, "unavailable");
1309
+ deepStrictEqual(documentLoader.fetched, [compatibleId(gw1), compatibleId(gw2)]);
1310
+ ok(documentLoader.signals[1]?.aborted);
1311
+ });
1312
+ test("resolvePortableActorKey() bounds loading the contexts of actor documents", async () => {
1313
+ const documentLoader = controlledLoader({ [compatibleId(gw1)]: await sign(await apActorJson()) }, []);
1314
+ const hangingContextLoader = (_url, options) => new Promise((_, reject) => {
1315
+ options?.signal?.addEventListener("abort", () => reject(options.signal.reason));
1316
+ });
1317
+ const resolution = await resolvePortableActorKey(hinted(gw1, gw2), {
1318
+ documentLoader,
1319
+ contextLoader: hangingContextLoader,
1320
+ lookupTimeout: 50
1321
+ });
1322
+ strictEqual(resolution.type, "unavailable");
1323
+ if (resolution.type === "unavailable") strictEqual(resolution.cacheable, false);
1324
+ deepStrictEqual(documentLoader.fetched, [compatibleId(gw1)]);
1325
+ });
1326
+ test("resolvePortableActorKey() ignores what is resolved after the timeout", async () => {
1327
+ let verifiedLate = () => {};
1328
+ const late = new Promise((resolve) => verifiedLate = resolve);
1329
+ let lookupDone = () => {};
1330
+ const done = new Promise((resolve) => lookupDone = resolve);
1331
+ const slowKeyCache = {
1332
+ async get() {
1333
+ await done;
1334
+ },
1335
+ set() {
1336
+ verifiedLate();
1337
+ return Promise.resolve();
1338
+ }
1339
+ };
1340
+ const documentLoader = controlledLoader({ [compatibleId(gw1)]: await sign(await apActorJson()) }, []);
1341
+ const resolution = await resolvePortableActorKey(hinted(gw1), {
1342
+ documentLoader,
1343
+ contextLoader,
1344
+ keyCache: slowKeyCache,
1345
+ lookupTimeout: 300
1346
+ });
1347
+ lookupDone();
1348
+ strictEqual(resolution.type, "unavailable");
1349
+ if (resolution.type === "unavailable") strictEqual(resolution.cacheable, false);
1350
+ await late;
1351
+ const lateRejection = controlledLoader({}, [], { delays: { [compatibleId(gw1)]: 60 } });
1352
+ strictEqual((await resolvePortableActorKey(hinted(gw1), {
1353
+ documentLoader: lateRejection,
1354
+ contextLoader,
1355
+ lookupTimeout: 20
1356
+ })).type, "unavailable");
1357
+ await new Promise((resolve) => setTimeout(resolve, 80));
1358
+ });
1359
+ test("resolvePortableActorKey() validates the lookup timeout", async () => {
1360
+ const documentLoader = controlledLoader({}, []);
1361
+ for (const lookupTimeout of [
1362
+ -1,
1363
+ NaN,
1364
+ Infinity,
1365
+ 2 ** 31
1366
+ ]) {
1367
+ let error;
1368
+ try {
1369
+ await resolvePortableActorKey(hinted(gw1), {
1370
+ documentLoader,
1371
+ contextLoader,
1372
+ lookupTimeout
1373
+ });
1374
+ } catch (e) {
1375
+ error = e;
1376
+ }
1377
+ ok(error instanceof RangeError, String(lookupTimeout));
1378
+ }
1379
+ const resolution = await resolvePortableActorKey(hinted(gw1), {
1380
+ documentLoader,
1381
+ contextLoader,
1382
+ lookupTimeout: 0
1383
+ });
1384
+ strictEqual(resolution.type, "unavailable");
1385
+ if (resolution.type === "unavailable") strictEqual(resolution.cacheable, false);
1386
+ deepStrictEqual(documentLoader.fetched, []);
1387
+ });
1388
+ test("fetchKeyDetailed() caches keys at ap: key IDs that time out as fetch failures", async () => {
1389
+ const cases = [{
1390
+ responses: { [compatibleId(gw1)]: 404 },
1391
+ hanging: [compatibleId(gw2)],
1392
+ cached: true
1393
+ }, {
1394
+ responses: {},
1395
+ hanging: [compatibleId(gw1)],
1396
+ cached: false
1397
+ }];
1398
+ for (const { responses, hanging, cached } of cases) {
1399
+ const documentLoader = controlledLoader(responses, hanging);
1400
+ const keyCache = new KvKeyCache(new MemoryKvStore(), ["pk"]);
1401
+ const options = {
1402
+ documentLoader,
1403
+ contextLoader,
1404
+ keyCache,
1405
+ portableKeyResolvers: {
1406
+ gatewayKey: () => Promise.resolve({ type: "legacy" }),
1407
+ actorKey: (keyId) => resolvePortableActorKey(keyId, {
1408
+ documentLoader,
1409
+ contextLoader,
1410
+ lookupTimeout: 50
1411
+ })
1412
+ }
1413
+ };
1414
+ const hintedKeyId = hinted(gw1, gw2);
1415
+ for (let i = 0; i < 2; i++) {
1416
+ const result = await fetchKeyDetailed(hintedKeyId, CryptographicKey, options);
1417
+ strictEqual(result.key, null);
1418
+ strictEqual(result.cached, cached && i > 0, `${cached}, ${i}`);
1419
+ ok(result.fetchError == null || !("status" in result.fetchError), "a timeout has no HTTP status");
1420
+ }
1421
+ strictEqual(documentLoader.fetched.filter((url) => url === compatibleId(gw1)).length, cached ? 1 : 2);
1422
+ }
1423
+ });
1424
+ test("verifyRequestDetailed() reports keys at ap: key IDs that time out as fetch errors", async () => {
1425
+ const timeout = new DOMException("Timed out", "TimeoutError");
1426
+ const documentLoader = () => Promise.reject(timeout);
1427
+ for (const spec of specs) {
1428
+ const result = await verifyRequestDetailed(await signedRequest(spec, hinted(gw1)), {
1429
+ documentLoader,
1430
+ contextLoader,
1431
+ spec
1432
+ });
1433
+ ok(!result.verified, spec);
1434
+ strictEqual(result.reason.type, "keyFetchError", spec);
1435
+ if (result.reason.type === "keyFetchError") ok(!("status" in result.reason.result), spec);
1436
+ }
1437
+ });
1438
+ //#endregion
1439
+ export {};