@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,746 @@
1
+ import { Temporal } from "temporal-polyfill";
2
+ import "urlpattern-polyfill";
3
+ globalThis.addEventListener = () => {};
4
+ import { t as esm_default } from "../esm-BHXXx4Wx.mjs";
5
+ import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
6
+ import { i as rsaPrivateKey2, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
7
+ import { n as FederationImpl, r as InboxContextImpl } from "../middleware-D7_LO-Vs.mjs";
8
+ import { n as extractInboxes, t as SendActivityError } from "../send-C4dDpAxm.mjs";
9
+ import { Create, Endpoints, Note, Person } from "@fedify/vocab";
10
+ import { parseIri } from "@fedify/vocab-runtime";
11
+ import { mockDocumentLoader, test } from "@fedify/fixture";
12
+ import { deepStrictEqual, ok, rejects, strictEqual } from "node:assert/strict";
13
+ //#region src/federation/portable-delivery.test.ts
14
+ const DID = "did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2";
15
+ const OTHER_DID = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
16
+ const PORTABLE_INBOX = `ap+ef61://${DID}/actor/inbox`;
17
+ const ACTOR_ID = `ap+ef61://${encodeURIComponent(DID)}/actor`;
18
+ const inboxOn = (gateway, did = DID) => `${gateway}/.well-known/apgateway/${did}/actor/inbox`;
19
+ function portableActor(options = {}) {
20
+ const did = options.did ?? DID;
21
+ return new Person({
22
+ id: parseIri(options.id ?? `ap://${did}/actor`),
23
+ inbox: parseIri(options.inbox ?? `ap://${did}/actor/inbox`),
24
+ gateways: (options.gateways ?? []).map((g) => new URL(g)),
25
+ endpoints: options.sharedInbox == null ? null : new Endpoints({ sharedInbox: new URL(options.sharedInbox) })
26
+ });
27
+ }
28
+ test("extractInboxes() resolves portable inboxes", async (t) => {
29
+ await t.step("through the recipient's gateways", () => {
30
+ deepStrictEqual(extractInboxes({ recipients: [portableActor({ gateways: ["https://gw1.example", "https://gw2.example"] })] }), { [inboxOn("https://gw1.example")]: {
31
+ actorIds: /* @__PURE__ */ new Set([ACTOR_ID]),
32
+ sharedInbox: false,
33
+ portableInbox: PORTABLE_INBOX,
34
+ gatewayInboxes: [inboxOn("https://gw1.example"), inboxOn("https://gw2.example")]
35
+ } });
36
+ });
37
+ await t.step("through @gateway hints of the inbox", () => {
38
+ const inboxes = extractInboxes({ recipients: [portableActor({ inbox: `ap://${DID}/actor/inbox?@gateway=https%3A%2F%2Fhint1.example&@gateway=https%3A%2F%2Fhint2.example` })] });
39
+ deepStrictEqual(Object.keys(inboxes), [inboxOn("https://hint1.example")]);
40
+ deepStrictEqual(inboxes[inboxOn("https://hint1.example")].gatewayInboxes, [inboxOn("https://hint1.example"), inboxOn("https://hint2.example")]);
41
+ });
42
+ await t.step("preferring gateways to hints", () => {
43
+ deepStrictEqual(extractInboxes({ recipients: [portableActor({
44
+ inbox: `ap://${DID}/actor/inbox?@gateway=https%3A%2F%2Fhint.example`,
45
+ gateways: ["https://gw.example"]
46
+ })] })[inboxOn("https://gw.example")].gatewayInboxes, [inboxOn("https://gw.example")]);
47
+ });
48
+ await t.step("skipping a recipient without gateways", () => {
49
+ deepStrictEqual(extractInboxes({ recipients: [portableActor()] }), {});
50
+ });
51
+ await t.step("skipping invalid gateways", () => {
52
+ const inboxes = extractInboxes({ recipients: [{
53
+ id: parseIri(`ap://${DID}/actor`),
54
+ inboxId: parseIri(`ap://${DID}/actor/inbox`),
55
+ gateways: [new URL("https://bad.example/path"), new URL("https://gw.example")]
56
+ }] });
57
+ deepStrictEqual(Object.keys(inboxes), [inboxOn("https://gw.example")]);
58
+ });
59
+ await t.step("capping the number of gateways", () => {
60
+ strictEqual(extractInboxes({ recipients: [portableActor({ gateways: Array.from({ length: 8 }, (_, i) => `https://gw${i}.example`) })] })[inboxOn("https://gw0.example")].gatewayInboxes?.length, 5);
61
+ });
62
+ await t.step("merging spellings of the same portable inbox", () => {
63
+ const inboxes = extractInboxes({ recipients: [portableActor({ gateways: ["https://gw1.example"] }), portableActor({
64
+ id: `ap+ef61://${encodeURIComponent(DID)}/actor`,
65
+ inbox: `ap+ef61://${encodeURIComponent(DID)}/actor/inbox`,
66
+ gateways: ["https://gw2.example", "https://gw1.example"]
67
+ })] });
68
+ deepStrictEqual(Object.keys(inboxes), [inboxOn("https://gw1.example")]);
69
+ deepStrictEqual(inboxes[inboxOn("https://gw1.example")].gatewayInboxes, [inboxOn("https://gw1.example"), inboxOn("https://gw2.example")]);
70
+ });
71
+ await t.step("merging with a compatible identifier inbox", () => {
72
+ const compatible = {
73
+ id: new URL(`https://gw1.example/.well-known/apgateway/${DID}/actor`),
74
+ inboxId: new URL(inboxOn("https://gw1.example"))
75
+ };
76
+ const portable = portableActor({ gateways: ["https://gw1.example", "https://gw2.example"] });
77
+ for (const recipients of [[compatible, portable], [portable, compatible]]) deepStrictEqual(extractInboxes({ recipients }), { [inboxOn("https://gw1.example")]: {
78
+ actorIds: new Set(recipients.map((r) => r.id.href)),
79
+ sharedInbox: false,
80
+ portableInbox: PORTABLE_INBOX,
81
+ gatewayInboxes: [inboxOn("https://gw1.example"), inboxOn("https://gw2.example")]
82
+ } });
83
+ });
84
+ await t.step("merging with an inbox on a later gateway", () => {
85
+ const compatible = {
86
+ id: new URL(`https://gw2.example/.well-known/apgateway/${DID}/actor`),
87
+ inboxId: new URL(inboxOn("https://gw2.example"))
88
+ };
89
+ const portable = portableActor({ gateways: ["https://gw1.example", "https://gw2.example"] });
90
+ for (const recipients of [[compatible, portable], [portable, compatible]]) {
91
+ const inboxes = extractInboxes({ recipients });
92
+ const keys = Object.keys(inboxes);
93
+ strictEqual(keys.length, 1);
94
+ const entry = inboxes[keys[0]];
95
+ strictEqual(entry.gatewayInboxes?.[0], keys[0]);
96
+ deepStrictEqual(new Set(entry.gatewayInboxes), /* @__PURE__ */ new Set([inboxOn("https://gw1.example"), inboxOn("https://gw2.example")]));
97
+ strictEqual(entry.portableInbox, PORTABLE_INBOX);
98
+ strictEqual(entry.actorIds.size, 2);
99
+ }
100
+ });
101
+ await t.step("merging inboxes on several gateways", () => {
102
+ const onGateway = (gateway) => ({
103
+ id: new URL(`${gateway}/.well-known/apgateway/${DID}/actor`),
104
+ inboxId: new URL(inboxOn(gateway))
105
+ });
106
+ const portable = portableActor({ gateways: ["https://gw1.example", "https://gw2.example"] });
107
+ const gw1 = onGateway("https://gw1.example");
108
+ const gw2 = onGateway("https://gw2.example");
109
+ for (const recipients of [
110
+ [
111
+ gw1,
112
+ gw2,
113
+ portable
114
+ ],
115
+ [
116
+ gw2,
117
+ gw1,
118
+ portable
119
+ ],
120
+ [
121
+ gw1,
122
+ portable,
123
+ gw2
124
+ ],
125
+ [
126
+ portable,
127
+ gw2,
128
+ gw1
129
+ ]
130
+ ]) {
131
+ const inboxes = extractInboxes({ recipients });
132
+ const keys = Object.keys(inboxes);
133
+ strictEqual(keys.length, 1);
134
+ const entry = inboxes[keys[0]];
135
+ strictEqual(entry.gatewayInboxes?.[0], keys[0]);
136
+ deepStrictEqual(new Set(entry.gatewayInboxes), /* @__PURE__ */ new Set([inboxOn("https://gw1.example"), inboxOn("https://gw2.example")]));
137
+ strictEqual(entry.portableInbox, PORTABLE_INBOX);
138
+ strictEqual(entry.actorIds.size, 3);
139
+ }
140
+ });
141
+ await t.step("merging with a shared inbox of the same URL", () => {
142
+ const shared = {
143
+ id: new URL("https://other.example/users/bob"),
144
+ inboxId: new URL("https://other.example/users/bob/inbox"),
145
+ endpoints: { sharedInbox: new URL(inboxOn("https://gw1.example")) }
146
+ };
147
+ const portable = portableActor({ gateways: ["https://gw1.example"] });
148
+ for (const recipients of [[shared, portable], [portable, shared]]) {
149
+ const inboxes = extractInboxes({
150
+ recipients,
151
+ preferSharedInbox: true
152
+ });
153
+ strictEqual(inboxes[inboxOn("https://gw1.example")].sharedInbox, false);
154
+ strictEqual(inboxes[inboxOn("https://gw1.example")].portableInbox, PORTABLE_INBOX);
155
+ strictEqual(inboxes[inboxOn("https://gw1.example")].actorIds.size, 2);
156
+ }
157
+ });
158
+ await t.step("not preferring the shared inbox of a portable inbox", () => {
159
+ const inboxes = extractInboxes({
160
+ recipients: [portableActor({
161
+ gateways: ["https://gw.example"],
162
+ sharedInbox: "https://gw.example/inbox"
163
+ })],
164
+ preferSharedInbox: true
165
+ });
166
+ deepStrictEqual(Object.keys(inboxes), [inboxOn("https://gw.example")]);
167
+ strictEqual(inboxes[inboxOn("https://gw.example")].sharedInbox, false);
168
+ });
169
+ await t.step("excluding gateways by excludeBaseUris", () => {
170
+ const recipients = [portableActor({ gateways: ["https://gw1.example", "https://gw2.example"] })];
171
+ let inboxes = extractInboxes({
172
+ recipients,
173
+ excludeBaseUris: [new URL("https://gw1.example/")]
174
+ });
175
+ deepStrictEqual(inboxes[inboxOn("https://gw2.example")].gatewayInboxes, [inboxOn("https://gw2.example")]);
176
+ inboxes = extractInboxes({
177
+ recipients,
178
+ excludeBaseUris: [new URL("https://gw1.example/"), new URL("https://gw2.example/foo")]
179
+ });
180
+ deepStrictEqual(inboxes, {});
181
+ });
182
+ await t.step("keeping ordinary inboxes unchanged", () => {
183
+ deepStrictEqual(extractInboxes({ recipients: [new Person({
184
+ id: new URL("https://example.com/alice"),
185
+ inbox: new URL("https://example.com/alice/inbox")
186
+ })] }), { "https://example.com/alice/inbox": {
187
+ actorIds: /* @__PURE__ */ new Set(["https://example.com/alice"]),
188
+ sharedInbox: false
189
+ } });
190
+ });
191
+ });
192
+ function createQueue(options = {}) {
193
+ const queued = [];
194
+ return {
195
+ queue: {
196
+ nativeRetrial: options.nativeRetrial,
197
+ enqueue(message, options) {
198
+ queued.push({
199
+ message: JSON.parse(JSON.stringify(message)),
200
+ options
201
+ });
202
+ return Promise.resolve();
203
+ },
204
+ listen() {
205
+ return Promise.resolve();
206
+ }
207
+ },
208
+ queued
209
+ };
210
+ }
211
+ const sender = {
212
+ keyId: rsaPublicKey2.id,
213
+ privateKey: rsaPrivateKey2
214
+ };
215
+ function createActivity(id = "https://example.com/activities/1") {
216
+ return new Create({
217
+ id: new URL(id),
218
+ actor: new URL("https://example.com/person2"),
219
+ object: new Note({
220
+ id: new URL(`${id}/note`),
221
+ content: "Hello"
222
+ })
223
+ });
224
+ }
225
+ test("Context.sendActivity() to a portable inbox", async (t) => {
226
+ esm_default.spyGlobal();
227
+ await t.step("immediately", async () => {
228
+ esm_default.hardReset();
229
+ esm_default.spyGlobal();
230
+ const requests = [];
231
+ esm_default.post("begin:https://gw1.example/", (call) => {
232
+ requests.push(call.url);
233
+ return 202;
234
+ });
235
+ await new FederationImpl({
236
+ kv: new MemoryKvStore(),
237
+ allowPrivateAddress: true
238
+ }).createContext(new URL("https://example.com/")).sendActivity(sender, portableActor({ gateways: ["https://gw1.example"] }), createActivity(), { immediate: true });
239
+ deepStrictEqual(requests, [inboxOn("https://gw1.example")]);
240
+ });
241
+ await t.step("immediately, failing over to the next gateway", async () => {
242
+ esm_default.hardReset();
243
+ esm_default.spyGlobal();
244
+ const requests = [];
245
+ esm_default.post("begin:https://gw1.example/", (call) => {
246
+ requests.push(call.url);
247
+ return 404;
248
+ });
249
+ esm_default.post("begin:https://gw2.example/", (call) => {
250
+ requests.push(call.url);
251
+ return 202;
252
+ });
253
+ await new FederationImpl({
254
+ kv: new MemoryKvStore(),
255
+ allowPrivateAddress: true
256
+ }).createContext(new URL("https://example.com/")).sendActivity(sender, portableActor({ gateways: ["https://gw1.example", "https://gw2.example"] }), createActivity(), { immediate: true });
257
+ deepStrictEqual([...new Set(requests)], [inboxOn("https://gw1.example"), inboxOn("https://gw2.example")]);
258
+ });
259
+ await t.step("immediately, failing on every gateway", async () => {
260
+ esm_default.hardReset();
261
+ esm_default.spyGlobal();
262
+ esm_default.post("begin:https://gw1.example/", 500);
263
+ esm_default.post("begin:https://gw2.example/", 404);
264
+ const ctx = new FederationImpl({
265
+ kv: new MemoryKvStore(),
266
+ allowPrivateAddress: true
267
+ }).createContext(new URL("https://example.com/"));
268
+ await rejects(() => ctx.sendActivity(sender, portableActor({ gateways: ["https://gw1.example", "https://gw2.example"] }), createActivity(), { immediate: true }), (error) => error instanceof SendActivityError && error.statusCode === 404 && error.inbox.href === inboxOn("https://gw2.example"));
269
+ });
270
+ await t.step("rejecting private gateways", async () => {
271
+ esm_default.hardReset();
272
+ esm_default.spyGlobal();
273
+ let requests = 0;
274
+ esm_default.post("begin:http://127.0.0.1/", () => {
275
+ requests++;
276
+ return 202;
277
+ });
278
+ const ctx = new FederationImpl({ kv: new MemoryKvStore() }).createContext(new URL("https://example.com/"));
279
+ await rejects(() => ctx.sendActivity(sender, portableActor({ gateways: ["http://127.0.0.1"] }), createActivity(), { immediate: true }));
280
+ strictEqual(requests, 0);
281
+ });
282
+ await t.step("through the queue", async () => {
283
+ esm_default.hardReset();
284
+ esm_default.spyGlobal();
285
+ const requests = [];
286
+ esm_default.post("begin:https://gw1.example/", (call) => {
287
+ requests.push(call.url);
288
+ return 404;
289
+ });
290
+ esm_default.post("begin:https://gw2.example/", (call) => {
291
+ requests.push(call.url);
292
+ return 202;
293
+ });
294
+ const { queue, queued } = createQueue();
295
+ const federation = new FederationImpl({
296
+ kv: new MemoryKvStore(),
297
+ queue,
298
+ manuallyStartQueue: true,
299
+ allowPrivateAddress: true
300
+ });
301
+ await federation.createContext(new URL("https://example.com/")).sendActivity(sender, [portableActor({ gateways: ["https://gw1.example", "https://gw2.example"] }), portableActor({
302
+ did: OTHER_DID,
303
+ gateways: ["https://gw1.example"]
304
+ })], createActivity(), {
305
+ orderingKey: "key",
306
+ fanout: "skip"
307
+ });
308
+ strictEqual(queued.length, 2);
309
+ const messages = queued.map((q) => q.message);
310
+ deepStrictEqual(messages.map((m) => m.inbox), [inboxOn("https://gw1.example"), inboxOn("https://gw1.example", OTHER_DID)]);
311
+ deepStrictEqual(messages[0].gatewayInboxes, [inboxOn("https://gw1.example"), inboxOn("https://gw2.example")]);
312
+ strictEqual(messages[0].portableInbox, PORTABLE_INBOX);
313
+ deepStrictEqual(queued.map((q) => q.options?.orderingKey), [`key\n${DID}`, `key\n${OTHER_DID}`]);
314
+ queued.length = 0;
315
+ await federation.processQueuedTask(void 0, messages[0]);
316
+ deepStrictEqual([...new Set(requests)], [inboxOn("https://gw1.example"), inboxOn("https://gw2.example")]);
317
+ strictEqual(queued.length, 0);
318
+ });
319
+ await t.step("through the fanout queue", async () => {
320
+ esm_default.hardReset();
321
+ esm_default.spyGlobal();
322
+ const { queue, queued } = createQueue();
323
+ const federation = new FederationImpl({
324
+ kv: new MemoryKvStore(),
325
+ queue,
326
+ manuallyStartQueue: true,
327
+ allowPrivateAddress: true
328
+ });
329
+ await federation.createContext(new URL("https://example.com/")).sendActivity(sender, portableActor({ gateways: ["https://gw1.example", "https://gw2.example"] }), createActivity(), { fanout: "force" });
330
+ strictEqual(queued.length, 1);
331
+ const fanout = queued[0].message;
332
+ deepStrictEqual(fanout.inboxes, { [inboxOn("https://gw1.example")]: {
333
+ actorIds: [ACTOR_ID],
334
+ sharedInbox: false,
335
+ portableInbox: PORTABLE_INBOX,
336
+ gatewayInboxes: [inboxOn("https://gw1.example"), inboxOn("https://gw2.example")]
337
+ } });
338
+ queued.length = 0;
339
+ await federation.processQueuedTask(void 0, fanout);
340
+ strictEqual(queued.length, 1);
341
+ const outbox = queued[0].message;
342
+ strictEqual(outbox.inbox, inboxOn("https://gw1.example"));
343
+ deepStrictEqual(outbox.gatewayInboxes, [inboxOn("https://gw1.example"), inboxOn("https://gw2.example")]);
344
+ });
345
+ esm_default.hardReset();
346
+ });
347
+ test("InboxContext.forwardActivity() to a portable inbox", async (t) => {
348
+ esm_default.spyGlobal();
349
+ const activity = {
350
+ "@context": "https://www.w3.org/ns/activitystreams",
351
+ type: "Create",
352
+ id: "https://example.com/activities/1",
353
+ actor: "https://example.com/person2"
354
+ };
355
+ const recipient = portableActor({ gateways: ["https://gw1.example", "https://gw2.example"] });
356
+ function createInboxContext(federation) {
357
+ return new InboxContextImpl(null, activity, activity.id, "https://www.w3.org/ns/activitystreams#Create", {
358
+ data: void 0,
359
+ federation,
360
+ url: new URL("https://example.com/"),
361
+ documentLoader: mockDocumentLoader,
362
+ contextLoader: mockDocumentLoader
363
+ });
364
+ }
365
+ await t.step("immediately", async () => {
366
+ esm_default.hardReset();
367
+ esm_default.spyGlobal();
368
+ const requests = [];
369
+ esm_default.post("begin:https://gw1.example/", (call) => {
370
+ requests.push(call.url);
371
+ return 500;
372
+ });
373
+ esm_default.post("begin:https://gw2.example/", (call) => {
374
+ requests.push(call.url);
375
+ return 202;
376
+ });
377
+ await createInboxContext(new FederationImpl({
378
+ kv: new MemoryKvStore(),
379
+ allowPrivateAddress: true
380
+ })).forwardActivity(sender, recipient, {
381
+ immediate: true,
382
+ skipIfUnsigned: false
383
+ });
384
+ deepStrictEqual([...new Set(requests)], [inboxOn("https://gw1.example"), inboxOn("https://gw2.example")]);
385
+ });
386
+ await t.step("through the queue", async () => {
387
+ esm_default.hardReset();
388
+ esm_default.spyGlobal();
389
+ const { queue, queued } = createQueue();
390
+ await createInboxContext(new FederationImpl({
391
+ kv: new MemoryKvStore(),
392
+ queue,
393
+ manuallyStartQueue: true,
394
+ allowPrivateAddress: true
395
+ })).forwardActivity(sender, recipient, {
396
+ orderingKey: "key",
397
+ skipIfUnsigned: false
398
+ });
399
+ strictEqual(queued.length, 1);
400
+ const message = queued[0].message;
401
+ strictEqual(message.inbox, inboxOn("https://gw1.example"));
402
+ strictEqual(message.portableInbox, PORTABLE_INBOX);
403
+ deepStrictEqual(message.gatewayInboxes, [inboxOn("https://gw1.example"), inboxOn("https://gw2.example")]);
404
+ strictEqual(queued[0].options?.orderingKey, `key\n${DID}`);
405
+ });
406
+ esm_default.hardReset();
407
+ });
408
+ test("processQueuedTask() with portable inbox gateways", async (t) => {
409
+ esm_default.spyGlobal();
410
+ function createMessage(gateways, overrides = {}) {
411
+ const gatewayInboxes = gateways.map((g) => inboxOn(g));
412
+ return {
413
+ type: "outbox",
414
+ id: crypto.randomUUID(),
415
+ baseUrl: "https://example.com",
416
+ keys: [],
417
+ activity: {
418
+ "@context": "https://www.w3.org/ns/activitystreams",
419
+ type: "Create",
420
+ id: "https://example.com/activities/1",
421
+ actor: "https://example.com/person2",
422
+ object: {
423
+ type: "Note",
424
+ content: "test"
425
+ }
426
+ },
427
+ activityId: "https://example.com/activities/1",
428
+ activityType: "https://www.w3.org/ns/activitystreams#Create",
429
+ inbox: gatewayInboxes[0],
430
+ sharedInbox: false,
431
+ actorIds: [`ap+ef61://${DID}/actor`],
432
+ started: (/* @__PURE__ */ new Date()).toISOString(),
433
+ attempt: 0,
434
+ headers: {},
435
+ orderingKey: `key\n${DID}`,
436
+ portableInbox: PORTABLE_INBOX,
437
+ gatewayInboxes,
438
+ traceContext: {},
439
+ ...overrides
440
+ };
441
+ }
442
+ function setup(options = {}, queueOptions = {}) {
443
+ const { queue, queued } = createQueue(queueOptions);
444
+ const permanentFailures = [];
445
+ const federation = new FederationImpl({
446
+ kv: new MemoryKvStore(),
447
+ queue,
448
+ manuallyStartQueue: true,
449
+ allowPrivateAddress: true,
450
+ ...options
451
+ });
452
+ federation.setOutboxPermanentFailureHandler((_ctx, values) => {
453
+ permanentFailures.push({
454
+ inbox: values.inbox,
455
+ statusCode: values.statusCode
456
+ });
457
+ });
458
+ return {
459
+ federation,
460
+ queued,
461
+ permanentFailures
462
+ };
463
+ }
464
+ function mockGateways(responses) {
465
+ esm_default.hardReset();
466
+ esm_default.spyGlobal();
467
+ const requests = [];
468
+ for (const [gateway, response] of Object.entries(responses)) esm_default.post(`begin:${gateway}/`, (call) => {
469
+ const { origin } = new URL(call.url);
470
+ if (requests.at(-1) !== origin) requests.push(origin);
471
+ return response instanceof Response ? response.clone() : response;
472
+ });
473
+ return requests;
474
+ }
475
+ await t.step("fails over from a transport error", async () => {
476
+ esm_default.hardReset();
477
+ esm_default.spyGlobal();
478
+ const requests = [];
479
+ esm_default.post("begin:https://gw1.example/", () => {
480
+ requests.push("https://gw1.example");
481
+ throw new TypeError("network error");
482
+ });
483
+ esm_default.post("begin:https://gw2.example/", () => {
484
+ requests.push("https://gw2.example");
485
+ return 202;
486
+ });
487
+ const { federation, queued } = setup();
488
+ await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"]));
489
+ deepStrictEqual(requests, ["https://gw1.example", "https://gw2.example"]);
490
+ strictEqual(queued.length, 0);
491
+ });
492
+ await t.step("retries the remaining gateways as one attempt", async () => {
493
+ const requests = mockGateways({
494
+ "https://gw1.example": 500,
495
+ "https://gw2.example": 404,
496
+ "https://gw3.example": 502
497
+ });
498
+ const { federation, queued, permanentFailures } = setup();
499
+ await federation.processQueuedTask(void 0, createMessage([
500
+ "https://gw1.example",
501
+ "https://gw2.example",
502
+ "https://gw3.example"
503
+ ]));
504
+ deepStrictEqual(requests, [
505
+ "https://gw1.example",
506
+ "https://gw2.example",
507
+ "https://gw3.example"
508
+ ]);
509
+ strictEqual(permanentFailures.length, 0);
510
+ strictEqual(queued.length, 1);
511
+ const retry = queued[0].message;
512
+ strictEqual(retry.attempt, 1);
513
+ strictEqual(retry.inbox, inboxOn("https://gw1.example"));
514
+ deepStrictEqual(retry.gatewayInboxes, [inboxOn("https://gw1.example"), inboxOn("https://gw3.example")]);
515
+ strictEqual(queued[0].options?.orderingKey, `key\n${DID}`);
516
+ ok(queued[0].options?.delay != null);
517
+ });
518
+ await t.step("reports a permanent failure once", async () => {
519
+ mockGateways({
520
+ "https://gw1.example": 410,
521
+ "https://gw2.example": 404
522
+ });
523
+ const { federation, queued, permanentFailures } = setup();
524
+ await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"]));
525
+ strictEqual(queued.length, 0);
526
+ deepStrictEqual(permanentFailures, [{
527
+ inbox: new URL(inboxOn("https://gw2.example")),
528
+ statusCode: 404
529
+ }]);
530
+ });
531
+ await t.step("respects custom permanent failure statuses", async () => {
532
+ mockGateways({
533
+ "https://gw1.example": 404,
534
+ "https://gw2.example": 500
535
+ });
536
+ const { federation, queued, permanentFailures } = setup({ permanentFailureStatusCodes: [500] });
537
+ await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"]));
538
+ strictEqual(permanentFailures.length, 0);
539
+ deepStrictEqual(queued[0].message.gatewayInboxes, [inboxOn("https://gw1.example")]);
540
+ });
541
+ await t.step("gives up when the retry policy does", async () => {
542
+ mockGateways({
543
+ "https://gw1.example": 500,
544
+ "https://gw2.example": 500
545
+ });
546
+ const { federation, queued, permanentFailures } = setup({ outboxRetryPolicy: () => null });
547
+ await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"]));
548
+ strictEqual(queued.length, 0);
549
+ strictEqual(permanentFailures.length, 0);
550
+ });
551
+ await t.step("throws with a natively retrying queue", async () => {
552
+ mockGateways({
553
+ "https://gw1.example": 500,
554
+ "https://gw2.example": 502
555
+ });
556
+ const { federation, queued } = setup({}, { nativeRetrial: true });
557
+ await rejects(() => federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"])), (error) => error instanceof SendActivityError && error.statusCode === 502);
558
+ strictEqual(queued.length, 0);
559
+ });
560
+ await t.step("excludes permanent failures from native retries", async () => {
561
+ mockGateways({
562
+ "https://gw1.example": 404,
563
+ "https://gw2.example": 500
564
+ });
565
+ const { federation, queued } = setup({}, { nativeRetrial: true });
566
+ await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"]));
567
+ strictEqual(queued.length, 1);
568
+ const retry = queued[0].message;
569
+ strictEqual(retry.attempt, 1);
570
+ deepStrictEqual(retry.gatewayInboxes, [inboxOn("https://gw2.example")]);
571
+ });
572
+ await t.step("drops a held delivery deferred by Retry-After", async () => {
573
+ const requests = mockGateways({
574
+ "https://gw1.example": 202,
575
+ "https://gw2.example": 202
576
+ });
577
+ const { federation, queued } = setup({ circuitBreaker: { heldActivityTtl: { hours: 1 } } });
578
+ const later = Temporal.Now.instant().add({ hours: 9 }).toString();
579
+ await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"], {
580
+ circuitHeld: true,
581
+ circuitHeldSince: Temporal.Now.instant().subtract({ hours: 1 }).toString(),
582
+ gatewayNotBefore: {
583
+ [inboxOn("https://gw1.example")]: later,
584
+ [inboxOn("https://gw2.example")]: later
585
+ }
586
+ }));
587
+ deepStrictEqual(requests, []);
588
+ strictEqual(queued.length, 0);
589
+ });
590
+ await t.step("retries when the first gateway is due", async () => {
591
+ mockGateways({
592
+ "https://gw1.example": new Response(null, {
593
+ status: 429,
594
+ headers: { "Retry-After": "60" }
595
+ }),
596
+ "https://gw2.example": new Response(null, {
597
+ status: 503,
598
+ headers: { "Retry-After": "3600" }
599
+ })
600
+ });
601
+ const { federation, queued } = setup();
602
+ await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"]));
603
+ strictEqual(queued.length, 1);
604
+ const retry = queued[0].message;
605
+ strictEqual(retry.attempt, 1);
606
+ deepStrictEqual(Object.keys(retry.gatewayNotBefore ?? {}), [inboxOn("https://gw1.example"), inboxOn("https://gw2.example")]);
607
+ strictEqual(Math.round(queued[0].options.delay.total("second")), 60);
608
+ });
609
+ await t.step("does not wait for Retry-After of every gateway", async () => {
610
+ mockGateways({
611
+ "https://gw1.example": new Response(null, {
612
+ status: 429,
613
+ headers: { "Retry-After": "3600" }
614
+ }),
615
+ "https://gw2.example": 500
616
+ });
617
+ const { federation, queued } = setup({ outboxRetryPolicy: () => Temporal.Duration.from({ seconds: 30 }) });
618
+ await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"]));
619
+ strictEqual(queued.length, 1);
620
+ const retry = queued[0].message;
621
+ deepStrictEqual(Object.keys(retry.gatewayNotBefore ?? {}), [inboxOn("https://gw1.example")]);
622
+ strictEqual(Math.round(queued[0].options.delay.total("second")), 30);
623
+ });
624
+ await t.step("ignores a Retry-After too long to honor", async () => {
625
+ const requests = mockGateways({
626
+ "https://gw1.example": new Response(null, {
627
+ status: 429,
628
+ headers: { "Retry-After": "9000000000000" }
629
+ }),
630
+ "https://gw2.example": 202
631
+ });
632
+ const { federation, queued } = setup();
633
+ await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"]));
634
+ deepStrictEqual(requests, ["https://gw1.example", "https://gw2.example"]);
635
+ strictEqual(queued.length, 0);
636
+ });
637
+ await t.step("skips gateways that are not to be tried yet", async () => {
638
+ const requests = mockGateways({
639
+ "https://gw1.example": 202,
640
+ "https://gw2.example": 202
641
+ });
642
+ const { federation, queued } = setup();
643
+ await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"], { gatewayNotBefore: { [inboxOn("https://gw1.example")]: Temporal.Now.instant().add({ hours: 1 }).toString() } }));
644
+ deepStrictEqual(requests, ["https://gw2.example"]);
645
+ strictEqual(queued.length, 0);
646
+ });
647
+ await t.step("holds when every gateway is held", async () => {
648
+ const requests = mockGateways({
649
+ "https://gw1.example": 202,
650
+ "https://gw2.example": 202
651
+ });
652
+ const { federation, queued } = setup();
653
+ const inAnHour = Temporal.Now.instant().add({ hours: 1 }).toString();
654
+ const inAMinute = Temporal.Now.instant().add({ minutes: 1 }).toString();
655
+ await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"], { gatewayNotBefore: {
656
+ [inboxOn("https://gw1.example")]: inAnHour,
657
+ [inboxOn("https://gw2.example")]: inAMinute
658
+ } }));
659
+ deepStrictEqual(requests, []);
660
+ strictEqual(queued.length, 1);
661
+ const held = queued[0].message;
662
+ strictEqual(held.attempt, 0);
663
+ strictEqual(held.circuitHeld, void 0);
664
+ ok(queued[0].options.delay.total("second") <= 60);
665
+ ok(queued[0].options.delay.total("second") > 50);
666
+ });
667
+ await t.step("skips a gateway whose circuit is open", async () => {
668
+ const requests = mockGateways({
669
+ "https://gw1.example": 500,
670
+ "https://gw2.example": 202
671
+ });
672
+ const { federation, queued } = setup({ circuitBreaker: {
673
+ failureThreshold: 1,
674
+ recoveryDelay: { hours: 1 }
675
+ } });
676
+ await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"]));
677
+ deepStrictEqual(requests, ["https://gw1.example", "https://gw2.example"]);
678
+ strictEqual(queued.length, 0);
679
+ requests.length = 0;
680
+ await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"]));
681
+ deepStrictEqual(requests, ["https://gw2.example"]);
682
+ strictEqual(queued.length, 0);
683
+ });
684
+ await t.step("holds a permanent failure and an open circuit", async () => {
685
+ const requests = mockGateways({
686
+ "https://gw1.example": 500,
687
+ "https://gw2.example": 404
688
+ });
689
+ const { federation, queued, permanentFailures } = setup({ circuitBreaker: {
690
+ failureThreshold: 1,
691
+ recoveryDelay: { hours: 1 }
692
+ } });
693
+ await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"]));
694
+ deepStrictEqual(requests, ["https://gw1.example", "https://gw2.example"]);
695
+ strictEqual(permanentFailures.length, 0);
696
+ strictEqual(queued.length, 1);
697
+ const held = queued[0].message;
698
+ strictEqual(held.attempt, 0);
699
+ strictEqual(held.circuitHeld, true);
700
+ ok(held.circuitHeldSince != null);
701
+ strictEqual(held.inbox, inboxOn("https://gw1.example"));
702
+ deepStrictEqual(held.gatewayInboxes, [inboxOn("https://gw1.example")]);
703
+ strictEqual(Math.round(queued[0].options.delay.total("minute")), 60);
704
+ });
705
+ await t.step("preserves when the delivery was first held", async () => {
706
+ mockGateways({
707
+ "https://gw1.example": 500,
708
+ "https://gw2.example": 500
709
+ });
710
+ const { federation, queued } = setup({ circuitBreaker: { failureThreshold: 10 } });
711
+ const heldSince = Temporal.Now.instant().subtract({ hours: 1 }).toString();
712
+ await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"], {
713
+ circuitHeld: true,
714
+ circuitHeldSince: heldSince
715
+ }));
716
+ strictEqual(queued.length, 1);
717
+ const retry = queued[0].message;
718
+ strictEqual(retry.attempt, 1);
719
+ strictEqual(retry.circuitHeld, void 0);
720
+ strictEqual(retry.circuitHeldSince, heldSince);
721
+ });
722
+ await t.step("drops a delivery held for too long", async () => {
723
+ const requests = mockGateways({
724
+ "https://gw1.example": 202,
725
+ "https://gw2.example": 202
726
+ });
727
+ const { federation, queued } = setup({ circuitBreaker: { heldActivityTtl: { hours: 1 } } });
728
+ await federation.processQueuedTask(void 0, createMessage(["https://gw1.example", "https://gw2.example"], {
729
+ circuitHeld: true,
730
+ circuitHeldSince: Temporal.Now.instant().subtract({ hours: 2 }).toString()
731
+ }));
732
+ deepStrictEqual(requests, []);
733
+ strictEqual(queued.length, 0);
734
+ });
735
+ await t.step("falls back to the ordinary path for one gateway", async () => {
736
+ const requests = mockGateways({ "https://gw1.example": 404 });
737
+ const { federation, queued, permanentFailures } = setup();
738
+ await federation.processQueuedTask(void 0, createMessage(["https://gw1.example"]));
739
+ deepStrictEqual(requests, ["https://gw1.example"]);
740
+ strictEqual(queued.length, 0);
741
+ strictEqual(permanentFailures.length, 1);
742
+ });
743
+ esm_default.hardReset();
744
+ });
745
+ //#endregion
746
+ export {};