@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.
- package/README.md +50 -39
- package/dist/{assert-CJ1KXcvv.mjs → assert-D1Eafmd_.mjs} +1 -1
- package/dist/{assert_equals-BV7oLMxm.mjs → assert_equals-Z6IcEbWU.mjs} +2 -28
- package/dist/assert_exists-cffSpJ7X.mjs +28 -0
- package/dist/assert_false-CPdRnc3E.mjs +23 -0
- package/dist/assert_greater_or_equal-BlafOF4-.mjs +31 -0
- package/dist/{assert_instance_of-Q9KTBnrf.mjs → assert_instance_of-mrXWWNY-.mjs} +1 -1
- package/dist/{assert_not_equals-EPJPoxuf.mjs → assert_not_equals-CpPK2t89.mjs} +2 -1
- package/dist/assert_rejects-CTptdYck.mjs +39 -0
- package/dist/{assert_strict_equals-Ec20AUwa.mjs → assert_strict_equals-CYvBmkpC.mjs} +2 -1
- package/dist/{assert_throws-NiFfqbnH.mjs → assert_throws-BYITOvPO.mjs} +2 -1
- package/dist/assertion_error-B-cx0fFA.mjs +31 -0
- package/dist/audience-CHQ-FlLD.mjs +95 -0
- package/dist/{builder-DonuxRl_.mjs → builder-Bda6LK7A.mjs} +169 -14
- package/dist/{circuit-breaker-CBGyz5F7.mjs → circuit-breaker-XJ53md4S.mjs} +224 -17
- package/dist/circuit-breaker-test-utils-DW6YC_Pe.mjs +14 -0
- package/dist/{client-CjF5IYQS.mjs → client-CEnNz8O7.mjs} +144 -8
- package/dist/{codec-BBga-FWI.mjs → codec-C6rTrpIp.mjs} +26 -3
- package/dist/{collection-BIhlldqK.mjs → collection-CnnP1UAv.mjs} +1 -1
- package/dist/compat/mod.cjs +1 -1
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/mod.js +1 -1
- package/dist/compat/outgoing-jsonld.test.mjs +71 -3
- package/dist/compat/public-audience.test.mjs +14 -5
- package/dist/compat/secured-document.test.d.mts +2 -0
- package/dist/compat/secured-document.test.mjs +58 -0
- package/dist/compat/transformers.test.mjs +5 -5
- package/dist/compound-proof-B-9mXxFW.mjs +989 -0
- package/dist/{context-Zii1f5xS.d.ts → context-7NyiqIcg.d.ts} +1600 -25
- package/dist/{context-BJXxPElm.mjs → context-8Tu9kea0.mjs} +21 -63
- package/dist/{context-lxX6I7as.d.cts → context-tsYPJ2Ew.d.cts} +1600 -25
- package/dist/{deno-BHzKn9Ig.mjs → deno-oR5aJKpU.mjs} +1 -1
- package/dist/dist-DJQM4u7i.mjs +189 -0
- package/dist/docloader-DBoYbvYW.mjs +87 -0
- package/dist/{esm-yYQfPhIY.mjs → esm-BHXXx4Wx.mjs} +13 -6
- package/dist/federation/audience.test.d.mts +2 -0
- package/dist/federation/audience.test.mjs +130 -0
- package/dist/federation/builder.test.mjs +54 -5
- package/dist/federation/circuit-breaker.test.mjs +838 -7
- package/dist/federation/collection.test.mjs +3 -3
- package/dist/federation/compound-fanout.test.d.mts +2 -0
- package/dist/federation/compound-fanout.test.mjs +228 -0
- package/dist/federation/handler-compound-proof.test.d.mts +2 -0
- package/dist/federation/handler-compound-proof.test.mjs +384 -0
- package/dist/federation/handler.test.mjs +385 -15
- package/dist/federation/hashlink-media.test.d.mts +2 -0
- package/dist/federation/hashlink-media.test.mjs +422 -0
- package/dist/federation/idempotency.test.mjs +6 -6
- package/dist/federation/inbox-report.test.d.mts +2 -0
- package/dist/federation/inbox-report.test.mjs +475 -0
- package/dist/federation/inbox.test.mjs +3 -3
- package/dist/federation/keycache.test.mjs +215 -14
- package/dist/federation/kv.test.mjs +3 -3
- package/dist/federation/metrics.test.mjs +5 -5
- package/dist/federation/middleware.test.mjs +967 -35
- package/dist/federation/mod.cjs +18 -261
- package/dist/federation/mod.d.cts +5 -5
- package/dist/federation/mod.d.ts +5 -5
- package/dist/federation/mod.js +279 -1
- package/dist/federation/mq.test.mjs +123 -47
- package/dist/federation/negotiation.test.mjs +4 -4
- package/dist/federation/outgoing-proof.test.d.mts +2 -0
- package/dist/federation/outgoing-proof.test.mjs +931 -0
- package/dist/federation/portable-actor.test.d.mts +2 -0
- package/dist/federation/portable-actor.test.mjs +496 -0
- package/dist/federation/portable-chain.test.d.mts +2 -0
- package/dist/federation/portable-chain.test.mjs +370 -0
- package/dist/federation/portable-collection.test.d.mts +2 -0
- package/dist/federation/portable-collection.test.mjs +832 -0
- package/dist/federation/portable-delivery.test.d.mts +2 -0
- package/dist/federation/portable-delivery.test.mjs +746 -0
- package/dist/federation/portable-gateway-keys.test.d.mts +2 -0
- package/dist/federation/portable-gateway-keys.test.mjs +686 -0
- package/dist/federation/portable-inbox.test.d.mts +2 -0
- package/dist/federation/portable-inbox.test.mjs +1535 -0
- package/dist/federation/portable-parse-uri.test.d.mts +2 -0
- package/dist/federation/portable-parse-uri.test.mjs +348 -0
- package/dist/federation/portable-route-activity.test.d.mts +2 -0
- package/dist/federation/portable-route-activity.test.mjs +259 -0
- package/dist/federation/portable.test.d.mts +2 -0
- package/dist/federation/portable.test.mjs +1072 -0
- package/dist/federation/retry.test.mjs +4 -3
- package/dist/federation/router.test.mjs +9 -159
- package/dist/federation/send.test.mjs +245 -27
- package/dist/federation/tasks/codec.test.mjs +4 -4
- package/dist/federation/tasks/enqueue.test.mjs +42 -4
- package/dist/federation/tasks/tasks.test.mjs +7 -6
- package/dist/federation/temporal.test.mjs +4 -4
- package/dist/federation/webfinger.test.mjs +275 -12
- package/dist/{middleware-FrSMAYEx.cjs → federation-CkZiAcWY.cjs} +8640 -3722
- package/dist/http-B5JT115n.cjs +10630 -0
- package/dist/http-DydWCB7R.js +6527 -0
- package/dist/{http-CPVMUgsW.mjs → http-siciWkmx.mjs} +397 -168
- package/dist/key-BspPiXqn.mjs +951 -0
- package/dist/key-C1nv7_jD.d.ts +1125 -0
- package/dist/key-DYxuyd-s.d.cts +1125 -0
- package/dist/key-owner-evidence-APB6NpSk.mjs +108 -0
- package/dist/keycache-Da8A8mZa.mjs +200 -0
- package/dist/{kv-D6hNiMTK.d.ts → kv-B2mzK5tl.d.cts} +3 -2
- package/dist/{kv-gJ8LYbxX.d.cts → kv-B2mzK5tl.d.ts} +3 -2
- package/dist/kv-DBRWVx3Q.mjs +104 -0
- package/dist/{kv-cache-KyGqJqSp.mjs → kv-cache-BF1hnW65.mjs} +1 -1
- package/dist/{kv-cache-5bnX3-Jq.js → kv-cache-CTQr1tbn.js} +54 -16
- package/dist/{kv-cache-DQ29C4XL.cjs → kv-cache-DCac1nEU.cjs} +55 -19
- package/dist/{ld-Cj9F_5AP.mjs → ld-CzaP-hJl.mjs} +44 -12
- package/dist/map-local-context-conflict-DOYEEJ3u.mjs +204 -0
- package/dist/map-local-create-note-B0SeXJms.mjs +214 -0
- package/dist/{metrics-DMk-nTxI.mjs → metrics-CYg9yxBL.mjs} +3 -2
- package/dist/metrics-DVKYY6K1.d.cts +36 -0
- package/dist/metrics-DoRtI0qU.d.ts +36 -0
- package/dist/{middleware-Y-kpsp6S.js → middleware-CsDDXKuL.js} +5493 -1103
- package/dist/{middleware-CvwTUgnP.mjs → middleware-D7_LO-Vs.mjs} +3157 -272
- package/dist/{middleware-CEctxBN6.mjs → middleware-DWjrfpcz.mjs} +1 -1
- package/dist/{mod-gOlPDuT8.d.cts → mod-BojP2Bw3.d.cts} +1 -1
- package/dist/mod-CavhAgLw.d.cts +320 -0
- package/dist/mod-CtpF2BU5.d.ts +320 -0
- package/dist/{mod-BtEaIEcD.d.cts → mod-DEZXwV6J.d.ts} +10 -4
- package/dist/{mod-UMmveiO0.d.ts → mod-Dw-nPxQh.d.cts} +10 -4
- package/dist/{mod-DLc2Ot6f.d.ts → mod-Xj5P4s4d.d.ts} +1 -1
- package/dist/mod.cjs +40 -39
- package/dist/mod.d.cts +10 -11
- package/dist/mod.d.ts +10 -11
- package/dist/mod.js +9 -9
- package/dist/{mq-llHKivST.mjs → mq-Dm1Wmtm-.mjs} +6 -0
- package/dist/nodeinfo/client.test.mjs +139 -4
- package/dist/nodeinfo/handler.test.mjs +5 -5
- package/dist/nodeinfo/mod.cjs +1 -1
- package/dist/nodeinfo/mod.d.cts +1 -1
- package/dist/nodeinfo/mod.d.ts +1 -1
- package/dist/nodeinfo/mod.js +1 -1
- package/dist/nodeinfo/types.test.mjs +4 -4
- package/dist/otel/exporter.test.mjs +3 -3
- package/dist/otel/mod.cjs +0 -1
- package/dist/otel/mod.d.cts +1 -1
- package/dist/otel/mod.d.ts +1 -1
- package/dist/{outgoing-jsonld-URuqsICz.mjs → outgoing-jsonld-4IJqPYos.mjs} +12 -10
- package/dist/{owner-DEvZuyOE.d.cts → owner-BF0L9cMt.d.cts} +11 -0
- package/dist/{owner-CnngXDNJ.d.ts → owner-BSzr5-I8.d.ts} +11 -0
- package/dist/owner-DYgfTFHy.mjs +267 -0
- package/dist/portable-collection-BpsVsRov.js +670 -0
- package/dist/portable-collection-C74P1vtC.mjs +418 -0
- package/dist/portable-collection-DZd2bm7M.cjs +686 -0
- package/dist/portable-key-BIMLrAP4.mjs +549 -0
- package/dist/portable-key-id-Ca30-hOa.mjs +248 -0
- package/dist/proof-Dxy6ByqI.mjs +1299 -0
- package/dist/{public-audience-Cr5MsnMq.mjs → public-audience--klBIOk1.mjs} +15 -10
- package/dist/router-u5O5KCQr.mjs +156 -0
- package/dist/runtime/mod.cjs +1 -1
- package/dist/runtime/mod.js +1 -1
- package/dist/secured-document-CxzZZ9nL.mjs +35 -0
- package/dist/send-C4dDpAxm.mjs +495 -0
- package/dist/sig/accept.test.mjs +1 -1
- package/dist/sig/compound-proof-discovery.test.d.mts +2 -0
- package/dist/sig/compound-proof-discovery.test.mjs +331 -0
- package/dist/sig/compound-proof-producer.test.d.mts +2 -0
- package/dist/sig/compound-proof-producer.test.mjs +107 -0
- package/dist/sig/compound-proof-verification.test.d.mts +2 -0
- package/dist/sig/compound-proof-verification.test.mjs +833 -0
- package/dist/sig/compound-proof.test.d.mts +2 -0
- package/dist/sig/compound-proof.test.mjs +411 -0
- package/dist/sig/http.test.mjs +702 -31
- package/dist/sig/key-owner-evidence.test.d.mts +2 -0
- package/dist/sig/key-owner-evidence.test.mjs +100 -0
- package/dist/sig/key.test.mjs +220 -8
- package/dist/sig/ld.test.mjs +65 -7
- package/dist/sig/mitra-receive-interop.test.d.mts +2 -0
- package/dist/sig/mitra-receive-interop.test.mjs +260 -0
- package/dist/sig/mitra-send-interop.test.d.mts +2 -0
- package/dist/sig/mitra-send-interop.test.mjs +461 -0
- package/dist/sig/mod.cjs +18 -16
- package/dist/sig/mod.d.cts +4 -4
- package/dist/sig/mod.d.ts +4 -4
- package/dist/sig/mod.js +3 -3
- package/dist/sig/owner.test.mjs +108 -9
- package/dist/sig/portable-collection.test.d.mts +2 -0
- package/dist/sig/portable-collection.test.mjs +697 -0
- package/dist/sig/portable-interop.test.d.mts +2 -0
- package/dist/sig/portable-interop.test.mjs +156 -0
- package/dist/sig/portable-key-id.test.d.mts +2 -0
- package/dist/sig/portable-key-id.test.mjs +67 -0
- package/dist/sig/portable-key.test.d.mts +2 -0
- package/dist/sig/portable-key.test.mjs +1439 -0
- package/dist/sig/proof.test.mjs +1753 -16
- package/dist/sig/tootik-request-interop.test.d.mts +2 -0
- package/dist/sig/tootik-request-interop.test.mjs +190 -0
- package/dist/sig/tootik-send-interop.test.d.mts +2 -0
- package/dist/sig/tootik-send-interop.test.mjs +331 -0
- package/dist/sig/verification.test.d.mts +2 -0
- package/dist/sig/verification.test.mjs +310 -0
- package/dist/std__assert-DvMDRPes.mjs +8 -0
- package/dist/{testing-CO7ftO8A.mjs → tasks-IG3pHfvZ.mjs} +2 -2
- package/dist/{temporal-fsXO5IvD.mjs → temporal-D0KJI4NF.mjs} +3 -3
- package/dist/testing/context.test.d.mts +2 -0
- package/dist/testing/context.test.mjs +19 -0
- package/dist/testing/kv.test.d.mts +2 -0
- package/dist/testing/kv.test.mjs +101 -0
- package/dist/testing/mod.d.mts +1617 -48
- package/dist/testing/mod.mjs +6 -3
- package/dist/testing-t8xDeJZ5.mjs +8 -0
- package/dist/{transformers-DGwl-hD1.cjs → transformers-BalCrSsr.cjs} +0 -1
- package/dist/{types-DC1pNgJA.js → types-_S2_MeLF.js} +144 -8
- package/dist/{types-CPym83zc.cjs → types-poIao40f.cjs} +160 -7
- package/dist/utils/docloader.test.mjs +530 -8
- package/dist/utils/kv-cache.test.mjs +2 -2
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.d.cts +1 -1
- package/dist/utils/mod.d.ts +1 -1
- package/dist/utils/mod.js +1 -1
- package/dist/verification-C3954hQh.mjs +5 -0
- package/dist/verification-Dpfcw8ml.mjs +105 -0
- package/dist/vocab/cjs.test.mjs +1 -1
- package/package.json +8 -8
- package/dist/assert_rejects-EgstcQy5.mjs +0 -82
- package/dist/docloader-e1khNUKy.mjs +0 -49
- package/dist/http-CsmQ-BQ4.js +0 -2483
- package/dist/http-VyDTd4G3.d.cts +0 -553
- package/dist/http-hlIjmewS.cjs +0 -2694
- package/dist/http-lf8Hsd91.d.ts +0 -553
- package/dist/key-DfPsX8hv.mjs +0 -453
- package/dist/keycache-_DHqz5z_.mjs +0 -97
- package/dist/metrics-CoUJLQhZ.d.cts +0 -276
- package/dist/metrics-Dc4jR-IC.d.ts +0 -276
- package/dist/mod-B0hW12_O.d.cts +0 -307
- package/dist/mod-COIAjwRS.d.ts +0 -307
- package/dist/owner-Br9YYzn_.mjs +0 -120
- package/dist/proof-B4ngoaNQ.mjs +0 -316
- package/dist/proof-B6E0XUXJ.js +0 -1415
- package/dist/proof-NN4xCQGL.cjs +0 -1562
- package/dist/send-CxHKUNzv.mjs +0 -239
- package/dist/std__assert-B2NnY9bq.mjs +0 -79
- package/dist/temporal-polyfill-BCdTHexd.cjs +0 -3565
- /package/dist/{accept-8PiolDP7.mjs → accept-CPVqki78.mjs} +0 -0
- /package/dist/{activity-listener-BtTSxZWA.mjs → activity-listener-C-BSbKI7.mjs} +0 -0
- /package/dist/{client-CAM_bQXx.d.cts → client-D-XP_Ccb.d.cts} +0 -0
- /package/dist/{client-CSddvgWN.d.ts → client-D-XP_Ccb.d.ts} +0 -0
- /package/dist/{keys-Dbyd7bhl.mjs → keys-DIJNHW-Y.mjs} +0 -0
- /package/dist/{kv-CXBAok3k.mjs → kv-DCHkaJ9-.mjs} +0 -0
- /package/dist/{negotiation-RuwrPFqi.mjs → negotiation-CCocWT_N.mjs} +0 -0
- /package/dist/{retry-DgFqeS5k.mjs → retry-zX5xw5e0.mjs} +0 -0
- /package/dist/{chunk-D5IdezsZ.js → rolldown-runtime-BC-Eo5Mj.js} +0 -0
- /package/dist/{chunk-Czpeue4M.cjs → rolldown-runtime-Dmm3t92W.cjs} +0 -0
- /package/dist/{transformers-DXze6FUB.js → transformers-DAtXl_Kc.js} +0 -0
- /package/dist/{types-CrtdVuDP.mjs → types-DE_LV1JB.mjs} +0 -0
|
@@ -1,43 +1,48 @@
|
|
|
1
1
|
import { Temporal } from "temporal-polyfill";
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
|
-
import { t as esm_default } from "../esm-
|
|
5
|
-
import { t as MemoryKvStore } from "../kv-
|
|
6
|
-
import {
|
|
7
|
-
import { t as
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
import { t as
|
|
11
|
-
import { t as
|
|
12
|
-
import { t as
|
|
13
|
-
import { t as
|
|
14
|
-
import { t as
|
|
15
|
-
import
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { t as
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import { t as
|
|
24
|
-
import { t as
|
|
4
|
+
import { t as esm_default } from "../esm-BHXXx4Wx.mjs";
|
|
5
|
+
import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
|
|
6
|
+
import { t as ManualClockKvStore } from "../kv-DBRWVx3Q.mjs";
|
|
7
|
+
import { a as numberSchema, r as envelopeSchema, t as MockQueue } from "../tasks-IG3pHfvZ.mjs";
|
|
8
|
+
import "../testing-t8xDeJZ5.mjs";
|
|
9
|
+
import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
|
|
10
|
+
import { t as assertExists } from "../assert_exists-cffSpJ7X.mjs";
|
|
11
|
+
import { t as assertFalse } from "../assert_false-CPdRnc3E.mjs";
|
|
12
|
+
import { t as assertInstanceOf } from "../assert_instance_of-mrXWWNY-.mjs";
|
|
13
|
+
import { t as assertThrows } from "../assert_throws-BYITOvPO.mjs";
|
|
14
|
+
import { t as assertNotEquals } from "../assert_not_equals-CpPK2t89.mjs";
|
|
15
|
+
import "../std__assert-DvMDRPes.mjs";
|
|
16
|
+
import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
|
|
17
|
+
import { t as assertStrictEquals } from "../assert_strict_equals-CYvBmkpC.mjs";
|
|
18
|
+
import { t as assert } from "../assert-D1Eafmd_.mjs";
|
|
19
|
+
import { g as recordInboxActivity } from "../metrics-CYg9yxBL.mjs";
|
|
20
|
+
import { a as compactJsonLd, h as verifyJsonLd, p as signJsonLd, s as detachSignature } from "../ld-CzaP-hJl.mjs";
|
|
21
|
+
import { a as signObject, s as verifyObject } from "../proof-Dxy6ByqI.mjs";
|
|
22
|
+
import { f as verifyRequest, l as signRequest } from "../http-siciWkmx.mjs";
|
|
23
|
+
import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DIJNHW-Y.mjs";
|
|
24
|
+
import { t as getAuthenticatedDocumentLoader } from "../docloader-DBoYbvYW.mjs";
|
|
25
|
+
import { T as handleBenchmarkTrigger, i as KvSpecDeterminer, n as FederationImpl, o as createFederation, r as InboxContextImpl, t as ContextImpl } from "../middleware-D7_LO-Vs.mjs";
|
|
26
|
+
import { t as doesActorOwnKey } from "../owner-DYgfTFHy.mjs";
|
|
27
|
+
import { t as CircuitBreaker } from "../circuit-breaker-XJ53md4S.mjs";
|
|
28
|
+
import { t as TaskCodec } from "../codec-C6rTrpIp.mjs";
|
|
29
|
+
import { t as markCircuitBreakerLegacySweepDone } from "../circuit-breaker-test-utils-DW6YC_Pe.mjs";
|
|
25
30
|
import { configure, reset } from "@logtape/logtape";
|
|
26
31
|
import { RouterError } from "@fedify/uri-template";
|
|
27
32
|
import * as vocab from "@fedify/vocab";
|
|
28
33
|
import { Create, Note, Offer, Person, getTypeId, lookupObject } from "@fedify/vocab";
|
|
29
34
|
import { SpanStatusCode, metrics } from "@opentelemetry/api";
|
|
35
|
+
import { FetchError, UrlError, getDocumentLoader } from "@fedify/vocab-runtime";
|
|
30
36
|
import { createTestMeterProvider, createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
|
|
31
|
-
import
|
|
32
|
-
import { deepStrictEqual, ok, rejects, strictEqual } from "node:assert/strict";
|
|
37
|
+
import dns from "node:dns/promises";
|
|
38
|
+
import { deepStrictEqual, ok, rejects, strictEqual, throws } from "node:assert/strict";
|
|
33
39
|
import serialize from "json-canon";
|
|
34
40
|
import { DataPointType, MeterProvider as MeterProvider$1, MetricReader } from "@opentelemetry/sdk-metrics";
|
|
35
41
|
//#region ../fixture/src/fixtures/example.com/create.json
|
|
36
|
-
var id$2 = "https://example.com/create";
|
|
37
42
|
var create_default = {
|
|
38
43
|
"@context": "https://www.w3.org/ns/activitystreams",
|
|
39
44
|
type: "Create",
|
|
40
|
-
id:
|
|
45
|
+
id: "https://example.com/create",
|
|
41
46
|
actor: "https://example.com/person"
|
|
42
47
|
};
|
|
43
48
|
//#endregion
|
|
@@ -1171,6 +1176,90 @@ test({
|
|
|
1171
1176
|
esm_default.hardReset();
|
|
1172
1177
|
}
|
|
1173
1178
|
});
|
|
1179
|
+
test("Federation.fetch() [rfc9421] multiple POST signatures", async (t) => {
|
|
1180
|
+
for (const [name, validSecond, maxHttpSignatures] of [
|
|
1181
|
+
[
|
|
1182
|
+
"valid second signature",
|
|
1183
|
+
true,
|
|
1184
|
+
void 0
|
|
1185
|
+
],
|
|
1186
|
+
[
|
|
1187
|
+
"both signatures invalid",
|
|
1188
|
+
false,
|
|
1189
|
+
void 0
|
|
1190
|
+
],
|
|
1191
|
+
[
|
|
1192
|
+
"valid second signature beyond maxHttpSignatures",
|
|
1193
|
+
true,
|
|
1194
|
+
1
|
|
1195
|
+
]
|
|
1196
|
+
]) await t.step(name, async () => {
|
|
1197
|
+
const inbox = [];
|
|
1198
|
+
const federation = createFederation({
|
|
1199
|
+
kv: new MemoryKvStore(),
|
|
1200
|
+
documentLoaderFactory: () => mockDocumentLoader,
|
|
1201
|
+
contextLoaderFactory: () => mockDocumentLoader,
|
|
1202
|
+
maxHttpSignatures
|
|
1203
|
+
});
|
|
1204
|
+
federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => new vocab.Person({ id: ctx.getActorUri(identifier) }));
|
|
1205
|
+
federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(vocab.Create, (_ctx, activity) => {
|
|
1206
|
+
inbox.push(activity.id.href);
|
|
1207
|
+
});
|
|
1208
|
+
const signed = await signRequest(new Request("https://example.com/inbox", {
|
|
1209
|
+
method: "POST",
|
|
1210
|
+
headers: { "Content-Type": "application/activity+json" },
|
|
1211
|
+
body: JSON.stringify(create_default)
|
|
1212
|
+
}), rsaPrivateKey2, rsaPublicKey2.id, { spec: "rfc9421" });
|
|
1213
|
+
const input = signed.headers.get("Signature-Input");
|
|
1214
|
+
const signature = signed.headers.get("Signature");
|
|
1215
|
+
signed.headers.set("Signature-Input", `${input}, ${input.replace(/^sig1=/, "sig2=")}`);
|
|
1216
|
+
signed.headers.set("Signature", `sig1=:AAAAAA==:, ${validSecond ? signature.replace(/^sig1=/, "sig2=") : "sig2=:AAAAAA==:"}`);
|
|
1217
|
+
const response = await federation.fetch(signed, { contextData: void 0 });
|
|
1218
|
+
const accepted = validSecond && maxHttpSignatures == null;
|
|
1219
|
+
assertEquals(response.status, accepted ? 202 : 401);
|
|
1220
|
+
assertEquals(inbox, accepted ? [create_default.id] : []);
|
|
1221
|
+
});
|
|
1222
|
+
});
|
|
1223
|
+
test("FederationOptions.maxHttpSignatures", async (t) => {
|
|
1224
|
+
const kv = new MemoryKvStore();
|
|
1225
|
+
await t.step("defaults to three", () => {
|
|
1226
|
+
const federation = createFederation({ kv });
|
|
1227
|
+
assertInstanceOf(federation, FederationImpl);
|
|
1228
|
+
assertEquals(federation.maxHttpSignatures, 3);
|
|
1229
|
+
});
|
|
1230
|
+
await t.step("rejects invalid values", () => {
|
|
1231
|
+
for (const maxHttpSignatures of [
|
|
1232
|
+
0,
|
|
1233
|
+
-1,
|
|
1234
|
+
1.5,
|
|
1235
|
+
NaN,
|
|
1236
|
+
2 ** 53,
|
|
1237
|
+
null
|
|
1238
|
+
]) assertThrows(() => createFederation({
|
|
1239
|
+
kv,
|
|
1240
|
+
maxHttpSignatures
|
|
1241
|
+
}), RangeError);
|
|
1242
|
+
const federation = createFederation({
|
|
1243
|
+
kv,
|
|
1244
|
+
maxHttpSignatures: Infinity
|
|
1245
|
+
});
|
|
1246
|
+
assertInstanceOf(federation, FederationImpl);
|
|
1247
|
+
assertEquals(federation.maxHttpSignatures, Infinity);
|
|
1248
|
+
});
|
|
1249
|
+
await t.step("applies to RequestContext.getSignedKey()", async () => {
|
|
1250
|
+
let request = new Request("https://example.com/", { headers: { "Accept": "application/ld+json" } });
|
|
1251
|
+
for (const [label, keyId] of [["sig1", new URL("https://example.com/missing-key")], ["sig2", rsaPublicKey2.id]]) request = await signRequest(request, rsaPrivateKey2, keyId, {
|
|
1252
|
+
spec: "rfc9421",
|
|
1253
|
+
rfc9421: { label }
|
|
1254
|
+
});
|
|
1255
|
+
for (const maxHttpSignatures of [void 0, 1]) assertEquals(await createFederation({
|
|
1256
|
+
kv,
|
|
1257
|
+
documentLoaderFactory: () => mockDocumentLoader,
|
|
1258
|
+
contextLoaderFactory: () => mockDocumentLoader,
|
|
1259
|
+
maxHttpSignatures
|
|
1260
|
+
}).createContext(request, void 0).getSignedKey(), maxHttpSignatures == null ? rsaPublicKey2 : null);
|
|
1261
|
+
});
|
|
1262
|
+
});
|
|
1174
1263
|
test("Federation.fetch()", async (t) => {
|
|
1175
1264
|
esm_default.spyGlobal();
|
|
1176
1265
|
esm_default.get("https://example.com/key2", {
|
|
@@ -1269,7 +1358,7 @@ test("Federation.fetch()", async (t) => {
|
|
|
1269
1358
|
}), rsaPrivateKey2, rsaPublicKey2.id);
|
|
1270
1359
|
assertEquals((await federation.fetch(request, { contextData: void 0 })).status, 202);
|
|
1271
1360
|
assertEquals(inbox.length, 1, "Expected one item in the inbox, json");
|
|
1272
|
-
assertEquals(inbox[0], id
|
|
1361
|
+
assertEquals(inbox[0], create_default.id);
|
|
1273
1362
|
});
|
|
1274
1363
|
await t.step("GET with application/json", async () => {
|
|
1275
1364
|
const { federation, dispatches } = createTestContext();
|
|
@@ -1292,7 +1381,7 @@ test("Federation.fetch()", async (t) => {
|
|
|
1292
1381
|
}), rsaPrivateKey2, rsaPublicKey2.id);
|
|
1293
1382
|
assertEquals((await federation.fetch(request, { contextData: void 0 })).status, 202);
|
|
1294
1383
|
assertEquals(inbox.length, 1, "Expected one inbox activity, ld+json");
|
|
1295
|
-
assertEquals(inbox[0], id
|
|
1384
|
+
assertEquals(inbox[0], create_default.id);
|
|
1296
1385
|
});
|
|
1297
1386
|
await t.step("GET with application/ld+json", async () => {
|
|
1298
1387
|
const { federation, dispatches } = createTestContext();
|
|
@@ -1316,7 +1405,7 @@ test("Federation.fetch()", async (t) => {
|
|
|
1316
1405
|
}), rsaPrivateKey2, rsaPublicKey2.id);
|
|
1317
1406
|
assertEquals((await federation.fetch(request, { contextData: void 0 })).status, 202);
|
|
1318
1407
|
assertEquals(inbox.length, 1);
|
|
1319
|
-
assertEquals(inbox[0], id
|
|
1408
|
+
assertEquals(inbox[0], create_default.id);
|
|
1320
1409
|
});
|
|
1321
1410
|
await t.step("GET with application/activity+json", async () => {
|
|
1322
1411
|
const { federation, dispatches } = createTestContext();
|
|
@@ -2044,7 +2133,7 @@ test("Federation.setOutboxListeners()", async (t) => {
|
|
|
2044
2133
|
}
|
|
2045
2134
|
}), { contextData: void 0 });
|
|
2046
2135
|
assertEquals([response.status, await response.text()], [202, ""]);
|
|
2047
|
-
assertEquals(received, [`john:${id
|
|
2136
|
+
assertEquals(received, [`john:${create_default.id}`]);
|
|
2048
2137
|
response = await federation.fetch(new Request("https://example.com/users/no-one/outbox", {
|
|
2049
2138
|
method: "POST",
|
|
2050
2139
|
body: JSON.stringify(create_default),
|
|
@@ -2106,7 +2195,7 @@ test("Federation.setOutboxListeners()", async (t) => {
|
|
|
2106
2195
|
}
|
|
2107
2196
|
}), { contextData: void 0 });
|
|
2108
2197
|
assertEquals([response.status, await response.text()], [202, ""]);
|
|
2109
|
-
assertEquals(received, [`john:${id
|
|
2198
|
+
assertEquals(received, [`john:${create_default.id}`]);
|
|
2110
2199
|
});
|
|
2111
2200
|
await t.step("warns when listener omits delivery", async () => {
|
|
2112
2201
|
await withLogtapeLock(async () => {
|
|
@@ -2161,7 +2250,7 @@ test("Federation.setOutboxListeners()", async (t) => {
|
|
|
2161
2250
|
const records = [];
|
|
2162
2251
|
await reset();
|
|
2163
2252
|
esm_default.spyGlobal();
|
|
2164
|
-
esm_default.post("https://
|
|
2253
|
+
esm_default.post("https://example.com/inbox", {
|
|
2165
2254
|
status: 202,
|
|
2166
2255
|
body: "Accepted"
|
|
2167
2256
|
});
|
|
@@ -2187,7 +2276,7 @@ test("Federation.setOutboxListeners()", async (t) => {
|
|
|
2187
2276
|
federation.setOutboxListeners("/users/{identifier}/outbox").on(vocab.Activity, async (ctx, activity) => {
|
|
2188
2277
|
await ctx.sendActivity({ identifier: ctx.identifier }, new vocab.Person({
|
|
2189
2278
|
id: new URL("https://remote.example/users/alice"),
|
|
2190
|
-
inbox: new URL("https://
|
|
2279
|
+
inbox: new URL("https://example.com/inbox")
|
|
2191
2280
|
}), activity);
|
|
2192
2281
|
}).authorize((ctx, identifier) => {
|
|
2193
2282
|
return identifier === "john" && ctx.request.headers.get("authorization") === "Bearer token";
|
|
@@ -2263,7 +2352,7 @@ test("Federation.setOutboxListeners()", async (t) => {
|
|
|
2263
2352
|
let ldsVerified = false;
|
|
2264
2353
|
await reset();
|
|
2265
2354
|
esm_default.spyGlobal();
|
|
2266
|
-
esm_default.post("https://
|
|
2355
|
+
esm_default.post("https://example.com/inbox", async (cl) => {
|
|
2267
2356
|
const verifyOptions = {
|
|
2268
2357
|
documentLoader: mockDocumentLoader,
|
|
2269
2358
|
contextLoader: mockDocumentLoader
|
|
@@ -2296,7 +2385,7 @@ test("Federation.setOutboxListeners()", async (t) => {
|
|
|
2296
2385
|
keyId: rsaPublicKey2.id
|
|
2297
2386
|
}], {
|
|
2298
2387
|
id: new URL("https://remote.example/users/alice"),
|
|
2299
|
-
inboxId: new URL("https://
|
|
2388
|
+
inboxId: new URL("https://example.com/inbox")
|
|
2300
2389
|
}, { skipIfUnsigned: true });
|
|
2301
2390
|
}).authorize((ctx, identifier) => {
|
|
2302
2391
|
return identifier === "person2" && ctx.request.headers.get("authorization") === "Bearer token";
|
|
@@ -3106,6 +3195,7 @@ test("FederationImpl.processQueuedTask()", async (t) => {
|
|
|
3106
3195
|
const kv = new MemoryKvStore();
|
|
3107
3196
|
const queuedMessages = [];
|
|
3108
3197
|
const federation = new FederationImpl({
|
|
3198
|
+
allowPrivateAddress: true,
|
|
3109
3199
|
kv,
|
|
3110
3200
|
queue: {
|
|
3111
3201
|
nativeRetrial: true,
|
|
@@ -3168,6 +3258,7 @@ test("FederationImpl.processQueuedTask()", async (t) => {
|
|
|
3168
3258
|
const kv = new MemoryKvStore();
|
|
3169
3259
|
let queuedMessages = [];
|
|
3170
3260
|
const federation = new FederationImpl({
|
|
3261
|
+
allowPrivateAddress: true,
|
|
3171
3262
|
kv,
|
|
3172
3263
|
queue: {
|
|
3173
3264
|
enqueue(message, _options) {
|
|
@@ -3238,6 +3329,7 @@ test("FederationImpl.processQueuedTask()", async (t) => {
|
|
|
3238
3329
|
const kv = new MemoryKvStore();
|
|
3239
3330
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
3240
3331
|
await new FederationImpl({
|
|
3332
|
+
allowPrivateAddress: true,
|
|
3241
3333
|
kv,
|
|
3242
3334
|
meterProvider,
|
|
3243
3335
|
queue: {
|
|
@@ -3280,6 +3372,7 @@ test("FederationImpl.processQueuedTask()", async (t) => {
|
|
|
3280
3372
|
const kv = new MemoryKvStore();
|
|
3281
3373
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
3282
3374
|
await new FederationImpl({
|
|
3375
|
+
allowPrivateAddress: true,
|
|
3283
3376
|
kv,
|
|
3284
3377
|
meterProvider,
|
|
3285
3378
|
queue: {
|
|
@@ -4225,6 +4318,51 @@ test("FederationImpl.processQueuedTask()", async (t) => {
|
|
|
4225
4318
|
assertEquals(errorCount, 1);
|
|
4226
4319
|
assertEquals(queuedMessages, []);
|
|
4227
4320
|
});
|
|
4321
|
+
await t.step("malformed FEP-ef61 gateways are permanent queued inbox parse errors", async () => {
|
|
4322
|
+
const queuedMessages = [];
|
|
4323
|
+
const queue = {
|
|
4324
|
+
enqueue(message, _options) {
|
|
4325
|
+
queuedMessages.push(message);
|
|
4326
|
+
return Promise.resolve();
|
|
4327
|
+
},
|
|
4328
|
+
listen(_handler, _options) {
|
|
4329
|
+
return Promise.resolve();
|
|
4330
|
+
}
|
|
4331
|
+
};
|
|
4332
|
+
const kv = new MemoryKvStore();
|
|
4333
|
+
let errorCount = 0;
|
|
4334
|
+
const federation = new FederationImpl({
|
|
4335
|
+
kv,
|
|
4336
|
+
queue
|
|
4337
|
+
});
|
|
4338
|
+
federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
|
|
4339
|
+
throw new Error("listener should not run");
|
|
4340
|
+
}).onError(() => {
|
|
4341
|
+
errorCount++;
|
|
4342
|
+
});
|
|
4343
|
+
await federation.processQueuedTask(void 0, {
|
|
4344
|
+
type: "inbox",
|
|
4345
|
+
id: crypto.randomUUID(),
|
|
4346
|
+
baseUrl: "https://example.com",
|
|
4347
|
+
activity: {
|
|
4348
|
+
"@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/fep/ef61"],
|
|
4349
|
+
id: "https://remote.example/activities/invalid-gateway",
|
|
4350
|
+
type: "Create",
|
|
4351
|
+
actor: "https://remote.example/users/alice",
|
|
4352
|
+
object: {
|
|
4353
|
+
id: "https://remote.example/users/bob",
|
|
4354
|
+
type: "Person",
|
|
4355
|
+
gateways: ["https://gw.example/path"]
|
|
4356
|
+
}
|
|
4357
|
+
},
|
|
4358
|
+
started: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4359
|
+
attempt: 0,
|
|
4360
|
+
identifier: null,
|
|
4361
|
+
traceContext: {}
|
|
4362
|
+
});
|
|
4363
|
+
assertEquals(errorCount, 1);
|
|
4364
|
+
assertEquals(queuedMessages, []);
|
|
4365
|
+
});
|
|
4228
4366
|
await t.step("legacy raw LDS inbox messages with network-path context ids retry", async () => {
|
|
4229
4367
|
const queue = {
|
|
4230
4368
|
enqueue(message, _options) {
|
|
@@ -4935,6 +5073,157 @@ test("FederationImpl.processQueuedTask()", async (t) => {
|
|
|
4935
5073
|
assertEquals(queuedMessages, []);
|
|
4936
5074
|
});
|
|
4937
5075
|
});
|
|
5076
|
+
test({
|
|
5077
|
+
name: "FederationImpl.processQueuedTask() accounts for destination validation failures",
|
|
5078
|
+
ignore: "Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted",
|
|
5079
|
+
async fn(t) {
|
|
5080
|
+
for (const result of [
|
|
5081
|
+
"throws",
|
|
5082
|
+
"empty",
|
|
5083
|
+
"private"
|
|
5084
|
+
]) for (const failureThreshold of [1, 5]) for (const redirected of [false, true]) await t.step(`${result}, threshold: ${failureThreshold}, redirected: ${redirected}`, async () => {
|
|
5085
|
+
const destination = "https://delivery.invalid/inbox";
|
|
5086
|
+
const publicInbox = "https://8.8.8.8/inbox";
|
|
5087
|
+
const queuedMessages = [];
|
|
5088
|
+
const delays = [];
|
|
5089
|
+
const errors = [];
|
|
5090
|
+
const activities = [];
|
|
5091
|
+
const attempts = [];
|
|
5092
|
+
const delay = Temporal.Duration.from({ seconds: 5 });
|
|
5093
|
+
const kv = new MemoryKvStore();
|
|
5094
|
+
await markCircuitBreakerLegacySweepDone(kv);
|
|
5095
|
+
const [meterProvider, recorder] = createTestMeterProvider();
|
|
5096
|
+
const recoveryDelay = Temporal.Duration.from({ minutes: 30 });
|
|
5097
|
+
const federation = new FederationImpl({
|
|
5098
|
+
kv,
|
|
5099
|
+
meterProvider,
|
|
5100
|
+
circuitBreaker: {
|
|
5101
|
+
failureThreshold,
|
|
5102
|
+
recoveryDelay
|
|
5103
|
+
},
|
|
5104
|
+
queue: {
|
|
5105
|
+
enqueue(message, options) {
|
|
5106
|
+
queuedMessages.push(message);
|
|
5107
|
+
delays.push(options?.delay);
|
|
5108
|
+
return Promise.resolve();
|
|
5109
|
+
},
|
|
5110
|
+
listen() {
|
|
5111
|
+
return Promise.resolve();
|
|
5112
|
+
}
|
|
5113
|
+
},
|
|
5114
|
+
documentLoaderFactory: () => mockDocumentLoader,
|
|
5115
|
+
contextLoaderFactory: () => mockDocumentLoader,
|
|
5116
|
+
onOutboxError(error, activity) {
|
|
5117
|
+
errors.push(error);
|
|
5118
|
+
activities.push(activity);
|
|
5119
|
+
},
|
|
5120
|
+
outboxRetryPolicy(options) {
|
|
5121
|
+
attempts.push(options.attempts);
|
|
5122
|
+
return delay;
|
|
5123
|
+
}
|
|
5124
|
+
});
|
|
5125
|
+
const message = {
|
|
5126
|
+
type: "outbox",
|
|
5127
|
+
id: crypto.randomUUID(),
|
|
5128
|
+
baseUrl: "https://example.com",
|
|
5129
|
+
keys: [],
|
|
5130
|
+
activity: {
|
|
5131
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
5132
|
+
type: "Create",
|
|
5133
|
+
id: "https://example.com/activities/dns-failure",
|
|
5134
|
+
actor: "https://example.com/users/alice",
|
|
5135
|
+
object: {
|
|
5136
|
+
type: "Note",
|
|
5137
|
+
content: "test"
|
|
5138
|
+
}
|
|
5139
|
+
},
|
|
5140
|
+
activityType: "https://www.w3.org/ns/activitystreams#Create",
|
|
5141
|
+
inbox: redirected ? publicInbox : destination,
|
|
5142
|
+
sharedInbox: false,
|
|
5143
|
+
started: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5144
|
+
attempt: 0,
|
|
5145
|
+
headers: {},
|
|
5146
|
+
traceContext: {}
|
|
5147
|
+
};
|
|
5148
|
+
const originalLookup = dns.lookup;
|
|
5149
|
+
const resolverError = /* @__PURE__ */ new Error("Resolver unavailable");
|
|
5150
|
+
const lookups = [];
|
|
5151
|
+
dns.lookup = ((hostname) => {
|
|
5152
|
+
lookups.push(hostname);
|
|
5153
|
+
assertEquals(hostname, "delivery.invalid");
|
|
5154
|
+
if (result === "throws") return Promise.reject(resolverError);
|
|
5155
|
+
return Promise.resolve(result === "empty" ? [] : [{
|
|
5156
|
+
address: "127.0.0.1",
|
|
5157
|
+
family: 4
|
|
5158
|
+
}]);
|
|
5159
|
+
});
|
|
5160
|
+
try {
|
|
5161
|
+
esm_default.mockGlobal().catch(202);
|
|
5162
|
+
if (redirected) esm_default.route(publicInbox, {
|
|
5163
|
+
status: 307,
|
|
5164
|
+
headers: { Location: destination }
|
|
5165
|
+
});
|
|
5166
|
+
await federation.processQueuedTask(void 0, message);
|
|
5167
|
+
assertEquals(errors.length, 1);
|
|
5168
|
+
const error = errors[0];
|
|
5169
|
+
if (result === "private") {
|
|
5170
|
+
assertInstanceOf(error, UrlError);
|
|
5171
|
+
assertEquals(error.reason, "disallowed");
|
|
5172
|
+
} else {
|
|
5173
|
+
assertInstanceOf(error, FetchError);
|
|
5174
|
+
assertEquals(error.url.href, destination);
|
|
5175
|
+
assertInstanceOf(error.cause, UrlError);
|
|
5176
|
+
assertEquals(error.cause.reason, "dns");
|
|
5177
|
+
assertStrictEquals(error.cause.cause, result === "throws" ? resolverError : void 0);
|
|
5178
|
+
}
|
|
5179
|
+
assertEquals(activities.length, 1);
|
|
5180
|
+
assertInstanceOf(activities[0], Create);
|
|
5181
|
+
assertEquals(activities[0].id?.href, "https://example.com/activities/dns-failure");
|
|
5182
|
+
assertEquals(attempts, [0]);
|
|
5183
|
+
const host = new URL(message.inbox).host;
|
|
5184
|
+
const state = await kv.get([
|
|
5185
|
+
"_fedify",
|
|
5186
|
+
"circuit",
|
|
5187
|
+
host
|
|
5188
|
+
]);
|
|
5189
|
+
if (result === "private") assertEquals(state, void 0);
|
|
5190
|
+
else {
|
|
5191
|
+
assertExists(state);
|
|
5192
|
+
assertEquals(state.failures.length, 1);
|
|
5193
|
+
assertEquals(state.state, failureThreshold === 1 ? "open" : "closed");
|
|
5194
|
+
}
|
|
5195
|
+
if (result !== "private" && failureThreshold === 1) {
|
|
5196
|
+
assertEquals(queuedMessages.length, 1);
|
|
5197
|
+
const held = queuedMessages[0];
|
|
5198
|
+
assertEquals(held.attempt, 0);
|
|
5199
|
+
assertEquals(held.circuitHeld, true);
|
|
5200
|
+
assertExists(held.circuitHeldSince);
|
|
5201
|
+
assertEquals(delays, [recoveryDelay]);
|
|
5202
|
+
} else {
|
|
5203
|
+
assertEquals(queuedMessages, [{
|
|
5204
|
+
...message,
|
|
5205
|
+
attempt: 1
|
|
5206
|
+
}]);
|
|
5207
|
+
assertEquals(delays, [delay]);
|
|
5208
|
+
}
|
|
5209
|
+
const expectedCount = result === "private" && !redirected ? 0 : 1;
|
|
5210
|
+
const sent = recorder.getMeasurements("activitypub.delivery.sent");
|
|
5211
|
+
assertEquals(sent.length, expectedCount);
|
|
5212
|
+
if (expectedCount === 1) {
|
|
5213
|
+
assertEquals(sent[0].value, 1);
|
|
5214
|
+
assertEquals(sent[0].attributes["activitypub.delivery.success"], false);
|
|
5215
|
+
assertEquals(sent[0].attributes["activitypub.remote.host"], host);
|
|
5216
|
+
}
|
|
5217
|
+
assertEquals(lookups, ["delivery.invalid"]);
|
|
5218
|
+
assertEquals(esm_default.callHistory.calls(destination).length, 0);
|
|
5219
|
+
assertEquals(esm_default.callHistory.calls().length, redirected ? 1 : 0);
|
|
5220
|
+
} finally {
|
|
5221
|
+
dns.lookup = originalLookup;
|
|
5222
|
+
esm_default.hardReset();
|
|
5223
|
+
}
|
|
5224
|
+
});
|
|
5225
|
+
}
|
|
5226
|
+
});
|
|
4938
5227
|
test("FederationImpl.processQueuedTask() permanent failure", async (t) => {
|
|
4939
5228
|
esm_default.spyGlobal();
|
|
4940
5229
|
esm_default.post("https://gone.example/inbox", {
|
|
@@ -4968,6 +5257,7 @@ test("FederationImpl.processQueuedTask() permanent failure", async (t) => {
|
|
|
4968
5257
|
return Promise.resolve();
|
|
4969
5258
|
}
|
|
4970
5259
|
},
|
|
5260
|
+
allowPrivateAddress: true,
|
|
4971
5261
|
...options.permanentFailureStatusCodes ? { permanentFailureStatusCodes: options.permanentFailureStatusCodes } : {},
|
|
4972
5262
|
...options.meterProvider ? { meterProvider: options.meterProvider } : {},
|
|
4973
5263
|
...options.tracerProvider ? { tracerProvider: options.tracerProvider } : {}
|
|
@@ -5166,6 +5456,7 @@ test("FederationImpl.processQueuedTask() circuit breaker", async (t) => {
|
|
|
5166
5456
|
return Promise.resolve();
|
|
5167
5457
|
}
|
|
5168
5458
|
},
|
|
5459
|
+
allowPrivateAddress: true,
|
|
5169
5460
|
circuitBreaker: options,
|
|
5170
5461
|
...federationOptions
|
|
5171
5462
|
});
|
|
@@ -5316,6 +5607,7 @@ test("FederationImpl.processQueuedTask() circuit breaker", async (t) => {
|
|
|
5316
5607
|
body: ""
|
|
5317
5608
|
});
|
|
5318
5609
|
const { federation, queued, kv } = setup({ failureThreshold: 1 });
|
|
5610
|
+
await markCircuitBreakerLegacySweepDone(kv);
|
|
5319
5611
|
await kv.set([
|
|
5320
5612
|
"_fedify",
|
|
5321
5613
|
"circuit",
|
|
@@ -5355,6 +5647,7 @@ test("FederationImpl.processQueuedTask() circuit breaker", async (t) => {
|
|
|
5355
5647
|
const { federation, queued, kv } = setup({ failureThreshold: 1 }, { outboxRetryPolicy: () => Temporal.Duration.from({ seconds: 3 }) });
|
|
5356
5648
|
kv.cas = () => Promise.resolve(false);
|
|
5357
5649
|
await federation.processQueuedTask(void 0, createOutboxMessage("https://failure-bookkeeping.example/inbox"));
|
|
5650
|
+
await federation.circuitBreaker?.pendingSweep;
|
|
5358
5651
|
assertEquals(queued.length, 1);
|
|
5359
5652
|
const retry = queued[0].message;
|
|
5360
5653
|
assertEquals(retry.attempt, 1);
|
|
@@ -5449,6 +5742,7 @@ test("FederationImpl.processQueuedTask() circuit breaker", async (t) => {
|
|
|
5449
5742
|
body: "server error"
|
|
5450
5743
|
});
|
|
5451
5744
|
const { federation, queued, kv } = setup({ failureThreshold: 1 }, { permanentFailureStatusCodes: [500] });
|
|
5745
|
+
await markCircuitBreakerLegacySweepDone(kv);
|
|
5452
5746
|
await kv.set([
|
|
5453
5747
|
"_fedify",
|
|
5454
5748
|
"circuit",
|
|
@@ -5681,6 +5975,7 @@ test("FederationImpl.processQueuedTask() circuit breaker", async (t) => {
|
|
|
5681
5975
|
failureThreshold: 1,
|
|
5682
5976
|
releaseInterval: { seconds: 1 }
|
|
5683
5977
|
}, { permanentFailureStatusCodes: [500] });
|
|
5978
|
+
await markCircuitBreakerLegacySweepDone(kv);
|
|
5684
5979
|
await kv.set([
|
|
5685
5980
|
"_fedify",
|
|
5686
5981
|
"circuit",
|
|
@@ -5715,6 +6010,7 @@ test("FederationImpl.processQueuedTask() circuit breaker", async (t) => {
|
|
|
5715
6010
|
failureThreshold: 1,
|
|
5716
6011
|
releaseInterval: { seconds: 1 }
|
|
5717
6012
|
});
|
|
6013
|
+
await markCircuitBreakerLegacySweepDone(kv);
|
|
5718
6014
|
await kv.set([
|
|
5719
6015
|
"_fedify",
|
|
5720
6016
|
"circuit",
|
|
@@ -5806,6 +6102,7 @@ test("FederationImpl.processQueuedTask() circuit breaker", async (t) => {
|
|
|
5806
6102
|
releaseInterval: { minutes: 1 }
|
|
5807
6103
|
}
|
|
5808
6104
|
});
|
|
6105
|
+
await markCircuitBreakerLegacySweepDone(kv);
|
|
5809
6106
|
await kv.set([
|
|
5810
6107
|
"_fedify",
|
|
5811
6108
|
"circuit",
|
|
@@ -5848,6 +6145,7 @@ test("FederationImpl.processQueuedTask() circuit breaker", async (t) => {
|
|
|
5848
6145
|
recoveryDelay: { seconds: 1 }
|
|
5849
6146
|
}
|
|
5850
6147
|
});
|
|
6148
|
+
await markCircuitBreakerLegacySweepDone(kv);
|
|
5851
6149
|
await kv.set([
|
|
5852
6150
|
"_fedify",
|
|
5853
6151
|
"circuit",
|
|
@@ -5936,6 +6234,7 @@ test("FederationImpl.processQueuedTask() circuit breaker", async (t) => {
|
|
|
5936
6234
|
}
|
|
5937
6235
|
}
|
|
5938
6236
|
});
|
|
6237
|
+
await markCircuitBreakerLegacySweepDone(kv);
|
|
5939
6238
|
await kv.set([
|
|
5940
6239
|
"_fedify",
|
|
5941
6240
|
"circuit",
|
|
@@ -6171,7 +6470,8 @@ test("FederationImpl.processQueuedTask() queue task metrics", async (t) => {
|
|
|
6171
6470
|
listen(_handler, _options) {
|
|
6172
6471
|
return Promise.resolve();
|
|
6173
6472
|
}
|
|
6174
|
-
}
|
|
6473
|
+
},
|
|
6474
|
+
allowPrivateAddress: true
|
|
6175
6475
|
});
|
|
6176
6476
|
federation.setInboxListeners("/users/{identifier}/inbox", "/inbox");
|
|
6177
6477
|
esm_default.spyGlobal();
|
|
@@ -7096,6 +7396,81 @@ test({
|
|
|
7096
7396
|
assertEquals(rejected[1].attributes["activitypub.activity.type"], "https://www.w3.org/ns/activitystreams#Offer");
|
|
7097
7397
|
}
|
|
7098
7398
|
});
|
|
7399
|
+
for (const queued of [false, true]) test(`ContextImpl.routeActivity() routes the fetched document (queued: ${queued})`, async () => {
|
|
7400
|
+
const id = new URL("https://example.com/verified-create");
|
|
7401
|
+
const genuine = new Create({
|
|
7402
|
+
id,
|
|
7403
|
+
actor: new URL("https://example.com/person"),
|
|
7404
|
+
object: new vocab.Note({ content: "Genuine content" }),
|
|
7405
|
+
to: new URL("https://example.com/recipient")
|
|
7406
|
+
});
|
|
7407
|
+
const genuineJson = await genuine.toJsonLd({ contextLoader: mockDocumentLoader });
|
|
7408
|
+
const forged = new Create({
|
|
7409
|
+
id,
|
|
7410
|
+
actor: new URL("https://victim.example/actor"),
|
|
7411
|
+
object: new vocab.Note({ content: "Forged content" }),
|
|
7412
|
+
to: new URL("https://attacker.example/recipient")
|
|
7413
|
+
});
|
|
7414
|
+
const messages = [];
|
|
7415
|
+
const queue = {
|
|
7416
|
+
enqueue(message) {
|
|
7417
|
+
messages.push(message);
|
|
7418
|
+
return Promise.resolve();
|
|
7419
|
+
},
|
|
7420
|
+
async listen() {}
|
|
7421
|
+
};
|
|
7422
|
+
const federation = createFederation({
|
|
7423
|
+
kv: new MemoryKvStore(),
|
|
7424
|
+
documentLoaderFactory: () => async (url, options) => url === id.href ? {
|
|
7425
|
+
document: genuineJson,
|
|
7426
|
+
documentUrl: id.href,
|
|
7427
|
+
contextUrl: null
|
|
7428
|
+
} : await mockDocumentLoader(url, options),
|
|
7429
|
+
contextLoaderFactory: () => mockDocumentLoader,
|
|
7430
|
+
queue: {
|
|
7431
|
+
inbox: queued ? queue : void 0,
|
|
7432
|
+
outbox: queue
|
|
7433
|
+
},
|
|
7434
|
+
manuallyStartQueue: true
|
|
7435
|
+
});
|
|
7436
|
+
const received = [];
|
|
7437
|
+
const contextDocuments = [];
|
|
7438
|
+
federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, async (ctx, activity) => {
|
|
7439
|
+
received.push(await activity.toJsonLd({ contextLoader: mockDocumentLoader }));
|
|
7440
|
+
assertInstanceOf(ctx, InboxContextImpl);
|
|
7441
|
+
contextDocuments.push(ctx.activity);
|
|
7442
|
+
await ctx.forwardActivity({
|
|
7443
|
+
privateKey: ed25519PrivateKey,
|
|
7444
|
+
keyId: ed25519Multikey.id
|
|
7445
|
+
}, {
|
|
7446
|
+
id: new URL("https://example.com/recipient"),
|
|
7447
|
+
inboxId: new URL("https://example.com/inbox")
|
|
7448
|
+
});
|
|
7449
|
+
});
|
|
7450
|
+
const ctx = federation.createContext(new URL("https://local.example/"));
|
|
7451
|
+
async function processInbox() {
|
|
7452
|
+
const inboxMessages = messages.filter((message) => message.type === "inbox");
|
|
7453
|
+
for (const message of inboxMessages) await federation.processQueuedTask(void 0, message);
|
|
7454
|
+
}
|
|
7455
|
+
assert(await ctx.routeActivity(null, forged));
|
|
7456
|
+
if (queued) {
|
|
7457
|
+
assertEquals(received, []);
|
|
7458
|
+
assertEquals(messages.length, 1);
|
|
7459
|
+
assert(messages[0].type === "inbox");
|
|
7460
|
+
assertEquals(messages[0].activity, genuineJson);
|
|
7461
|
+
await processInbox();
|
|
7462
|
+
}
|
|
7463
|
+
assertEquals(received, [genuineJson]);
|
|
7464
|
+
assertEquals(contextDocuments, [genuineJson]);
|
|
7465
|
+
const forwarded = messages.filter((message) => message.type === "outbox");
|
|
7466
|
+
assertEquals(forwarded.length, 1);
|
|
7467
|
+
assertEquals(forwarded[0].activity, genuineJson);
|
|
7468
|
+
assert(await ctx.routeActivity(null, genuine));
|
|
7469
|
+
if (queued) await processInbox();
|
|
7470
|
+
assertEquals(received, [genuineJson]);
|
|
7471
|
+
assertEquals(contextDocuments, [genuineJson]);
|
|
7472
|
+
assertEquals(messages.filter((message) => message.type === "outbox").length, 1);
|
|
7473
|
+
});
|
|
7099
7474
|
test("ContextImpl.routeActivity() marks queued signed activities as non-LDS", async () => {
|
|
7100
7475
|
let queuedMessage = null;
|
|
7101
7476
|
const federation = new FederationImpl({
|
|
@@ -7155,6 +7530,47 @@ test("ContextImpl.getCollectionUri()", () => {
|
|
|
7155
7530
|
assertThrows(() => ctx.getCollectionUri(Symbol(notReg), values));
|
|
7156
7531
|
assertThrows(() => ctx.getCollectionUri(Symbol.for(notReg), values));
|
|
7157
7532
|
});
|
|
7533
|
+
test("symbol-named custom collections are served and parsed", async () => {
|
|
7534
|
+
const federation = createFederation({ kv: new MemoryKvStore() });
|
|
7535
|
+
const names = [Symbol("bookmarks"), Symbol("bookmarks")];
|
|
7536
|
+
const paths = ["bookmarks", "ordered-bookmarks"];
|
|
7537
|
+
const calls = [];
|
|
7538
|
+
federation.setCollectionDispatcher(names[0], vocab.Object, "/users/{identifier}/bookmarks", (_ctx, _values, cursor) => {
|
|
7539
|
+
calls.push([0, cursor]);
|
|
7540
|
+
return { items: [] };
|
|
7541
|
+
});
|
|
7542
|
+
federation.setOrderedCollectionDispatcher(names[1], vocab.Object, "/users/{identifier}/ordered-bookmarks", (_ctx, _values, cursor) => {
|
|
7543
|
+
calls.push([1, cursor]);
|
|
7544
|
+
return { items: [] };
|
|
7545
|
+
});
|
|
7546
|
+
const ctx = federation.createContext(new URL("https://example.com/"));
|
|
7547
|
+
for (const [index, name] of names.entries()) {
|
|
7548
|
+
const uri = ctx.getCollectionUri(name, { identifier: "alice" });
|
|
7549
|
+
assertEquals(uri.pathname, `/users/alice/${paths[index]}`);
|
|
7550
|
+
const parsed = ctx.parseUri(uri);
|
|
7551
|
+
assertEquals(parsed?.type, index === 0 ? "collection" : "orderedCollection");
|
|
7552
|
+
if (parsed?.type !== "collection" && parsed?.type !== "orderedCollection") throw new Error("Expected a custom collection URI");
|
|
7553
|
+
assertStrictEquals(parsed.name, name);
|
|
7554
|
+
assertStrictEquals(parsed.class, vocab.Object);
|
|
7555
|
+
assertEquals(parsed.typeId, vocab.Object.typeId);
|
|
7556
|
+
assertEquals(parsed.values, { identifier: "alice" });
|
|
7557
|
+
for (const cursor of [null, "next"]) {
|
|
7558
|
+
const pageUri = new URL(uri);
|
|
7559
|
+
if (cursor != null) pageUri.searchParams.set("cursor", cursor);
|
|
7560
|
+
const response = await federation.fetch(new Request(pageUri, { headers: { accept: "application/activity+json" } }), { contextData: void 0 });
|
|
7561
|
+
assertEquals(response.status, 200);
|
|
7562
|
+
const body = await response.json();
|
|
7563
|
+
assert(body !== null && typeof body === "object" && "type" in body);
|
|
7564
|
+
assertEquals(body.type, `${index === 0 ? "Collection" : "OrderedCollection"}${cursor == null ? "" : "Page"}`);
|
|
7565
|
+
}
|
|
7566
|
+
}
|
|
7567
|
+
assertEquals(calls, [
|
|
7568
|
+
[0, null],
|
|
7569
|
+
[0, "next"],
|
|
7570
|
+
[1, null],
|
|
7571
|
+
[1, "next"]
|
|
7572
|
+
]);
|
|
7573
|
+
});
|
|
7158
7574
|
test("InboxContextImpl.forwardActivity()", async (t) => {
|
|
7159
7575
|
esm_default.spyGlobal();
|
|
7160
7576
|
let verified = null;
|
|
@@ -7348,6 +7764,193 @@ test("KvSpecDeterminer", async (t) => {
|
|
|
7348
7764
|
spec = await determiner.determineSpec("example.com");
|
|
7349
7765
|
assertEquals(spec, "rfc9421");
|
|
7350
7766
|
});
|
|
7767
|
+
await t.step("should default specTtl to 90 days", () => {
|
|
7768
|
+
assertEquals(new KvSpecDeterminer(new MemoryKvStore(), ["test", "spec"]).specTtl.total("day"), 90);
|
|
7769
|
+
});
|
|
7770
|
+
await t.step("should accept a configurable specTtl as a 4th positional argument", () => {
|
|
7771
|
+
assertEquals(new KvSpecDeterminer(new MemoryKvStore(), ["test", "spec"], "rfc9421", { specTtl: Temporal.Duration.from({ days: 7 }) }).specTtl.total("day"), 7);
|
|
7772
|
+
});
|
|
7773
|
+
await t.step("should expire, relearn, and remember the spec again", async () => {
|
|
7774
|
+
const kv = new ManualClockKvStore();
|
|
7775
|
+
const determiner = new KvSpecDeterminer(kv, ["test", "spec"], "rfc9421", { specTtl: Temporal.Duration.from({ days: 90 }) });
|
|
7776
|
+
await determiner.rememberSpec("example.com", "draft-cavage-http-signatures-12");
|
|
7777
|
+
assertEquals(await determiner.determineSpec("example.com"), "draft-cavage-http-signatures-12");
|
|
7778
|
+
kv.advance({ days: 91 });
|
|
7779
|
+
assertEquals(await determiner.determineSpec("example.com"), "rfc9421");
|
|
7780
|
+
await determiner.rememberSpec("example.com", "draft-cavage-http-signatures-12");
|
|
7781
|
+
assertEquals(await determiner.determineSpec("example.com"), "draft-cavage-http-signatures-12");
|
|
7782
|
+
});
|
|
7783
|
+
});
|
|
7784
|
+
/**
|
|
7785
|
+
* A `KvStore` that records the TTL every write was made with, so tests can
|
|
7786
|
+
* assert on TTLs even after the entries themselves have expired.
|
|
7787
|
+
*/
|
|
7788
|
+
var TtlRecordingKvStore = class {
|
|
7789
|
+
inner = new ManualClockKvStore();
|
|
7790
|
+
writes = [];
|
|
7791
|
+
/** Moves the wrapped store's virtual clock forward. */
|
|
7792
|
+
advance(duration) {
|
|
7793
|
+
this.inner.advance(duration);
|
|
7794
|
+
}
|
|
7795
|
+
get(key) {
|
|
7796
|
+
return this.inner.get(key);
|
|
7797
|
+
}
|
|
7798
|
+
set(key, value, options) {
|
|
7799
|
+
this.writes.push({
|
|
7800
|
+
key,
|
|
7801
|
+
ttl: options?.ttl
|
|
7802
|
+
});
|
|
7803
|
+
return this.inner.set(key, value, options);
|
|
7804
|
+
}
|
|
7805
|
+
delete(key) {
|
|
7806
|
+
return this.inner.delete(key);
|
|
7807
|
+
}
|
|
7808
|
+
list(prefix) {
|
|
7809
|
+
return this.inner.list(prefix);
|
|
7810
|
+
}
|
|
7811
|
+
/** The TTL of the most recent write to `key`, or `undefined` if never set. */
|
|
7812
|
+
lastTtl(key) {
|
|
7813
|
+
const matches = this.writes.filter((w) => w.key.length === key.length && w.key.every((p, i) => p === key[i]));
|
|
7814
|
+
return matches.length < 1 ? void 0 : matches[matches.length - 1].ttl;
|
|
7815
|
+
}
|
|
7816
|
+
};
|
|
7817
|
+
test("createFederation() defaults the cache TTLs and lets them be overridden", () => {
|
|
7818
|
+
const defaults = new FederationImpl({ kv: new MemoryKvStore() });
|
|
7819
|
+
assertEquals(defaults.publicKeyTtl.total("day"), 30);
|
|
7820
|
+
assertEquals(defaults.httpMessageSignaturesSpecTtl.total("day"), 90);
|
|
7821
|
+
const overridden = new FederationImpl({
|
|
7822
|
+
kv: new MemoryKvStore(),
|
|
7823
|
+
publicKeyTtl: { days: 1 },
|
|
7824
|
+
httpMessageSignaturesSpecTtl: { hours: 12 }
|
|
7825
|
+
});
|
|
7826
|
+
assertEquals(overridden.publicKeyTtl.total("day"), 1);
|
|
7827
|
+
assertEquals(overridden.httpMessageSignaturesSpecTtl.total("hour"), 12);
|
|
7828
|
+
});
|
|
7829
|
+
test("createFederation() applies httpMessageSignaturesSpecTtl to remembered specs", async () => {
|
|
7830
|
+
esm_default.spyGlobal();
|
|
7831
|
+
try {
|
|
7832
|
+
const attempts = [];
|
|
7833
|
+
esm_default.post("https://example.com/inbox", async (cl) => {
|
|
7834
|
+
const request = cl.request.clone();
|
|
7835
|
+
const spec = request.headers.has("Signature-Input") ? "rfc9421" : "draft-cavage-http-signatures-12";
|
|
7836
|
+
attempts.push(spec);
|
|
7837
|
+
if (spec === "rfc9421") return new Response(null, { status: 401 });
|
|
7838
|
+
const key = await verifyRequest(request, {
|
|
7839
|
+
documentLoader: mockDocumentLoader,
|
|
7840
|
+
contextLoader: mockDocumentLoader
|
|
7841
|
+
});
|
|
7842
|
+
return new Response(null, { status: key == null ? 401 : 202 });
|
|
7843
|
+
});
|
|
7844
|
+
const kv = new TtlRecordingKvStore();
|
|
7845
|
+
const ctx = createFederation({
|
|
7846
|
+
kv,
|
|
7847
|
+
documentLoaderFactory: () => mockDocumentLoader,
|
|
7848
|
+
contextLoaderFactory: () => mockDocumentLoader,
|
|
7849
|
+
httpMessageSignaturesSpecTtl: { days: 14 }
|
|
7850
|
+
}).createContext(new URL("https://example.com/"), void 0);
|
|
7851
|
+
const specKey = [
|
|
7852
|
+
"_fedify",
|
|
7853
|
+
"httpMessageSignaturesSpec",
|
|
7854
|
+
"https://example.com"
|
|
7855
|
+
];
|
|
7856
|
+
const send = () => ctx.sendActivity([{
|
|
7857
|
+
privateKey: rsaPrivateKey2,
|
|
7858
|
+
keyId: rsaPublicKey2.id
|
|
7859
|
+
}], {
|
|
7860
|
+
id: new URL("https://example.com/recipient"),
|
|
7861
|
+
inboxId: new URL("https://example.com/inbox")
|
|
7862
|
+
}, new vocab.Create({
|
|
7863
|
+
id: new URL(`https://example.com/activities/${crypto.randomUUID()}`),
|
|
7864
|
+
actor: new URL("https://example.com/person")
|
|
7865
|
+
}));
|
|
7866
|
+
await send();
|
|
7867
|
+
assertEquals(attempts, ["rfc9421", "draft-cavage-http-signatures-12"]);
|
|
7868
|
+
assertEquals(await kv.get(specKey), "draft-cavage-http-signatures-12");
|
|
7869
|
+
assertEquals(kv.lastTtl(specKey)?.total("day"), 14);
|
|
7870
|
+
attempts.length = 0;
|
|
7871
|
+
await send();
|
|
7872
|
+
assertEquals(attempts, ["draft-cavage-http-signatures-12"]);
|
|
7873
|
+
kv.advance({ days: 15 });
|
|
7874
|
+
assertEquals(await kv.get(specKey), void 0);
|
|
7875
|
+
attempts.length = 0;
|
|
7876
|
+
await send();
|
|
7877
|
+
assertEquals(attempts, ["rfc9421", "draft-cavage-http-signatures-12"]);
|
|
7878
|
+
assertEquals(await kv.get(specKey), "draft-cavage-http-signatures-12");
|
|
7879
|
+
assertEquals(kv.lastTtl(specKey)?.total("day"), 14);
|
|
7880
|
+
} finally {
|
|
7881
|
+
esm_default.hardReset();
|
|
7882
|
+
}
|
|
7883
|
+
});
|
|
7884
|
+
test("createFederation() applies publicKeyTtl to cached public keys", async () => {
|
|
7885
|
+
esm_default.spyGlobal();
|
|
7886
|
+
try {
|
|
7887
|
+
let keyFetches = 0;
|
|
7888
|
+
esm_default.get("begin:https://example.com/person2", () => {
|
|
7889
|
+
keyFetches++;
|
|
7890
|
+
return {
|
|
7891
|
+
headers: { "Content-Type": "application/activity+json" },
|
|
7892
|
+
body: person2_default
|
|
7893
|
+
};
|
|
7894
|
+
});
|
|
7895
|
+
const keyId = "https://example.com/person2#key3";
|
|
7896
|
+
const kv = new TtlRecordingKvStore();
|
|
7897
|
+
const federation = createFederation({
|
|
7898
|
+
kv,
|
|
7899
|
+
documentLoaderFactory: () => mockDocumentLoader,
|
|
7900
|
+
contextLoaderFactory: () => mockDocumentLoader,
|
|
7901
|
+
publicKeyTtl: { days: 7 }
|
|
7902
|
+
});
|
|
7903
|
+
const inbox = [];
|
|
7904
|
+
federation.setActorDispatcher("/users/{identifier}", (_, identifier) => identifier === "john" ? new vocab.Person({}) : null).setKeyPairsDispatcher(() => [{
|
|
7905
|
+
privateKey: rsaPrivateKey2,
|
|
7906
|
+
publicKey: rsaPublicKey2.publicKey
|
|
7907
|
+
}]);
|
|
7908
|
+
federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(vocab.Create, (_ctx, create) => {
|
|
7909
|
+
inbox.push(create);
|
|
7910
|
+
});
|
|
7911
|
+
const deliver = async () => {
|
|
7912
|
+
const activity = new vocab.Create({
|
|
7913
|
+
id: new URL(`https://example.com/activities/${crypto.randomUUID()}`),
|
|
7914
|
+
actor: new URL("https://example.com/person2")
|
|
7915
|
+
});
|
|
7916
|
+
let request = new Request("https://example.com/users/john/inbox", {
|
|
7917
|
+
method: "POST",
|
|
7918
|
+
headers: {
|
|
7919
|
+
"Content-Type": "application/activity+json",
|
|
7920
|
+
accept: "application/ld+json"
|
|
7921
|
+
},
|
|
7922
|
+
body: JSON.stringify(await activity.toJsonLd({ contextLoader: mockDocumentLoader }))
|
|
7923
|
+
});
|
|
7924
|
+
request = await signRequest(request, rsaPrivateKey3, new URL(keyId));
|
|
7925
|
+
return await federation.fetch(request, { contextData: void 0 });
|
|
7926
|
+
};
|
|
7927
|
+
const publicKeyKey = [
|
|
7928
|
+
"_fedify",
|
|
7929
|
+
"publicKey",
|
|
7930
|
+
"2",
|
|
7931
|
+
keyId
|
|
7932
|
+
];
|
|
7933
|
+
assertEquals((await deliver()).status, 202);
|
|
7934
|
+
assertEquals(inbox.length, 1);
|
|
7935
|
+
assert(keyFetches > 0);
|
|
7936
|
+
assert(await kv.get(publicKeyKey) != null);
|
|
7937
|
+
assertEquals(kv.lastTtl(publicKeyKey)?.total("day"), 7);
|
|
7938
|
+
const keyCacheWrites = () => kv.writes.filter((w) => w.key.length === publicKeyKey.length && w.key.every((part, i) => part === publicKeyKey[i])).length;
|
|
7939
|
+
const warmWrites = keyCacheWrites();
|
|
7940
|
+
assertEquals((await deliver()).status, 202);
|
|
7941
|
+
assertEquals(inbox.length, 2);
|
|
7942
|
+
assertEquals(keyCacheWrites(), warmWrites);
|
|
7943
|
+
kv.advance({ days: 8 });
|
|
7944
|
+
assertEquals(await kv.get(publicKeyKey), void 0);
|
|
7945
|
+
keyFetches = 0;
|
|
7946
|
+
assertEquals((await deliver()).status, 202);
|
|
7947
|
+
assertEquals(inbox.length, 3);
|
|
7948
|
+
assert(keyFetches > 0);
|
|
7949
|
+
assert(await kv.get(publicKeyKey) != null);
|
|
7950
|
+
assertEquals(kv.lastTtl(publicKeyKey)?.total("day"), 7);
|
|
7951
|
+
} finally {
|
|
7952
|
+
esm_default.hardReset();
|
|
7953
|
+
}
|
|
7351
7954
|
});
|
|
7352
7955
|
test("createFederation() instruments documentLoader with activitypub.document.fetch", async () => {
|
|
7353
7956
|
const [meterProvider, recorder] = createTestMeterProvider();
|
|
@@ -7589,5 +8192,334 @@ test("ContextImpl.enqueueTaskMany()", async (t) => {
|
|
|
7589
8192
|
strictEqual(queue.enqueued.length, 0);
|
|
7590
8193
|
});
|
|
7591
8194
|
});
|
|
8195
|
+
test("ContextImpl.sendActivity() honors the private-address policy", async (t) => {
|
|
8196
|
+
for (const allowPrivateAddress of [false, true]) await t.step(`allowPrivateAddress: ${allowPrivateAddress}`, async () => {
|
|
8197
|
+
const inbox = "http://127.0.0.1/inbox";
|
|
8198
|
+
esm_default.mockGlobal().post(inbox, 202);
|
|
8199
|
+
try {
|
|
8200
|
+
const ctx = createFederation({
|
|
8201
|
+
kv: new MemoryKvStore(),
|
|
8202
|
+
allowPrivateAddress
|
|
8203
|
+
}).createContext(new URL("https://example.com/"));
|
|
8204
|
+
const send = () => ctx.sendActivity({
|
|
8205
|
+
privateKey: ed25519PrivateKey,
|
|
8206
|
+
keyId: ed25519Multikey.id
|
|
8207
|
+
}, new Person({
|
|
8208
|
+
id: new URL("https://example.com/recipient"),
|
|
8209
|
+
inbox: new URL(inbox)
|
|
8210
|
+
}), new Create({
|
|
8211
|
+
id: new URL("https://example.com/activity"),
|
|
8212
|
+
actor: new URL("https://example.com/person")
|
|
8213
|
+
}), { immediate: true });
|
|
8214
|
+
if (allowPrivateAddress) {
|
|
8215
|
+
await send();
|
|
8216
|
+
assertEquals(esm_default.callHistory.calls(inbox).length, 1);
|
|
8217
|
+
} else {
|
|
8218
|
+
await assertRejects(send, UrlError);
|
|
8219
|
+
assertEquals(esm_default.callHistory.calls(inbox).length, 0);
|
|
8220
|
+
}
|
|
8221
|
+
} finally {
|
|
8222
|
+
esm_default.hardReset();
|
|
8223
|
+
}
|
|
8224
|
+
});
|
|
8225
|
+
});
|
|
8226
|
+
test("Federation.fetch() bounds inbox bodies before dispatch", async () => {
|
|
8227
|
+
let dispatched = false;
|
|
8228
|
+
const json = JSON.stringify({
|
|
8229
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
8230
|
+
id: "https://example.com/activities/bounded",
|
|
8231
|
+
type: "Create",
|
|
8232
|
+
actor: "https://example.com/actor"
|
|
8233
|
+
});
|
|
8234
|
+
const federation = createFederation({
|
|
8235
|
+
kv: new MemoryKvStore(),
|
|
8236
|
+
skipSignatureVerification: true,
|
|
8237
|
+
contextLoaderFactory: () => mockDocumentLoader
|
|
8238
|
+
});
|
|
8239
|
+
federation.setActorDispatcher("/actors/{identifier}", () => new Person({ id: new URL("https://example.com/actor") }));
|
|
8240
|
+
federation.setInboxListeners("/actors/{identifier}/inbox", "/inbox").on(Create, () => {
|
|
8241
|
+
dispatched = true;
|
|
8242
|
+
});
|
|
8243
|
+
for (const contentLength of [
|
|
8244
|
+
void 0,
|
|
8245
|
+
"1",
|
|
8246
|
+
String(16777217)
|
|
8247
|
+
]) {
|
|
8248
|
+
assertEquals((await federation.fetch(new Request("https://example.com/inbox", {
|
|
8249
|
+
method: "POST",
|
|
8250
|
+
headers: contentLength == null ? {} : { "Content-Length": contentLength },
|
|
8251
|
+
body: json.padEnd(16777217)
|
|
8252
|
+
}), { contextData: void 0 })).status, 413);
|
|
8253
|
+
assertFalse(dispatched);
|
|
8254
|
+
}
|
|
8255
|
+
let canceled = false;
|
|
8256
|
+
let pulls = 0;
|
|
8257
|
+
const body = new ReadableStream({
|
|
8258
|
+
pull(controller) {
|
|
8259
|
+
pulls++;
|
|
8260
|
+
controller.enqueue(new Uint8Array(64 * 1024));
|
|
8261
|
+
},
|
|
8262
|
+
cancel() {
|
|
8263
|
+
canceled = true;
|
|
8264
|
+
}
|
|
8265
|
+
}, { highWaterMark: 0 });
|
|
8266
|
+
const request = new Request("https://example.com/inbox", {
|
|
8267
|
+
method: "POST",
|
|
8268
|
+
body,
|
|
8269
|
+
duplex: "half"
|
|
8270
|
+
});
|
|
8271
|
+
assertEquals((await federation.fetch(request, { contextData: void 0 })).status, 413);
|
|
8272
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
8273
|
+
assert(canceled);
|
|
8274
|
+
assert(pulls <= 260);
|
|
8275
|
+
assertFalse(dispatched);
|
|
8276
|
+
assertEquals((await federation.fetch(new Request("https://example.com/inbox", {
|
|
8277
|
+
method: "POST",
|
|
8278
|
+
body: json
|
|
8279
|
+
}), { contextData: void 0 })).status, 202);
|
|
8280
|
+
assert(dispatched);
|
|
8281
|
+
});
|
|
8282
|
+
function getRawMessage(record) {
|
|
8283
|
+
return typeof record.rawMessage === "string" ? record.rawMessage : record.rawMessage.join("");
|
|
8284
|
+
}
|
|
8285
|
+
test("Federation.fetch() serves tombstones of objects", async (t) => {
|
|
8286
|
+
const deleted = Temporal.Instant.from("2024-01-15T00:00:00Z");
|
|
8287
|
+
const federation = createFederation({
|
|
8288
|
+
kv: new MemoryKvStore(),
|
|
8289
|
+
documentLoaderFactory: () => mockDocumentLoader,
|
|
8290
|
+
contextLoaderFactory: () => mockDocumentLoader
|
|
8291
|
+
});
|
|
8292
|
+
federation.setObjectDispatcher(vocab.Note, "/users/{identifier}/notes/{id}", (ctx, values) => {
|
|
8293
|
+
if (values.id === "missing") return null;
|
|
8294
|
+
if (values.id === "deleted") return new vocab.Tombstone({
|
|
8295
|
+
id: ctx.getObjectUri(vocab.Note, values),
|
|
8296
|
+
formerType: vocab.Note,
|
|
8297
|
+
deleted
|
|
8298
|
+
});
|
|
8299
|
+
return new vocab.Note({
|
|
8300
|
+
id: ctx.getObjectUri(vocab.Note, values),
|
|
8301
|
+
content: "Hello"
|
|
8302
|
+
});
|
|
8303
|
+
});
|
|
8304
|
+
federation.setObjectDispatcher(vocab.Tombstone, "/tombstones/{id}", (ctx, values) => new vocab.Tombstone({ id: ctx.getObjectUri(vocab.Tombstone, values) }));
|
|
8305
|
+
federation.setObjectDispatcher(vocab.Object, "/objects/{id}", (ctx, values) => values.id === "deleted" ? new vocab.Tombstone({ id: ctx.getObjectUri(vocab.Object, values) }) : new vocab.Note({ id: ctx.getObjectUri(vocab.Object, values) }));
|
|
8306
|
+
function fetch(path, method = "GET") {
|
|
8307
|
+
return federation.fetch(new Request(`https://example.com${path}`, {
|
|
8308
|
+
method,
|
|
8309
|
+
headers: { Accept: "application/activity+json" }
|
|
8310
|
+
}), { contextData: void 0 });
|
|
8311
|
+
}
|
|
8312
|
+
await t.step("GET responds with 410 Gone and the tombstone", async () => {
|
|
8313
|
+
const response = await fetch("/users/john/notes/deleted");
|
|
8314
|
+
assertEquals(response.status, 410);
|
|
8315
|
+
assertEquals(response.headers.get("Content-Type"), "application/activity+json");
|
|
8316
|
+
const body = await response.json();
|
|
8317
|
+
assertEquals(body.id, "https://example.com/users/john/notes/deleted");
|
|
8318
|
+
assertEquals(body.type, "Tombstone");
|
|
8319
|
+
assertEquals(body.formerType, "as:Note");
|
|
8320
|
+
assertEquals(body.deleted, "2024-01-15T00:00:00Z");
|
|
8321
|
+
});
|
|
8322
|
+
await t.step("HEAD responds with 410 Gone without a body", async () => {
|
|
8323
|
+
const response = await fetch("/users/john/notes/deleted", "HEAD");
|
|
8324
|
+
assertEquals(response.status, 410);
|
|
8325
|
+
assertEquals(response.body, null);
|
|
8326
|
+
});
|
|
8327
|
+
await t.step("objects and null are served as before", async () => {
|
|
8328
|
+
const response = await fetch("/users/john/notes/123");
|
|
8329
|
+
assertEquals(response.status, 200);
|
|
8330
|
+
assertEquals((await response.json()).type, "Note");
|
|
8331
|
+
assertEquals((await fetch("/users/john/notes/missing")).status, 404);
|
|
8332
|
+
});
|
|
8333
|
+
await t.step("Tombstone and Object dispatchers respond with 410", async () => {
|
|
8334
|
+
assertEquals((await fetch("/tombstones/1")).status, 410);
|
|
8335
|
+
assertEquals((await fetch("/objects/deleted")).status, 410);
|
|
8336
|
+
assertEquals((await fetch("/objects/live")).status, 200);
|
|
8337
|
+
});
|
|
8338
|
+
await t.step("RequestContext.getObject()", async () => {
|
|
8339
|
+
const ctx = federation.createContext(new Request("https://example.com/"), void 0);
|
|
8340
|
+
const values = {
|
|
8341
|
+
identifier: "john",
|
|
8342
|
+
id: "deleted"
|
|
8343
|
+
};
|
|
8344
|
+
assertEquals(await ctx.getObject(vocab.Note, values), null);
|
|
8345
|
+
assertEquals(await ctx.getObject(vocab.Note, values, { tombstone: "suppress" }), null);
|
|
8346
|
+
assertEquals(await ctx.getObject(vocab.Note, values, {}), null);
|
|
8347
|
+
const tombstone = await ctx.getObject(vocab.Note, values, { tombstone: "passthrough" });
|
|
8348
|
+
assertInstanceOf(tombstone, vocab.Tombstone);
|
|
8349
|
+
assertEquals(tombstone.id, new URL("https://example.com/users/john/notes/deleted"));
|
|
8350
|
+
assertEquals(tombstone.deleted, deleted);
|
|
8351
|
+
assertInstanceOf(await ctx.getObject(vocab.Note, values, { tombstone: "passthrough" }), vocab.Tombstone);
|
|
8352
|
+
assertInstanceOf(await ctx.getObject(vocab.Note, {
|
|
8353
|
+
identifier: "john",
|
|
8354
|
+
id: "123"
|
|
8355
|
+
}), vocab.Note);
|
|
8356
|
+
assertEquals(await ctx.getObject(vocab.Note, {
|
|
8357
|
+
identifier: "john",
|
|
8358
|
+
id: "missing"
|
|
8359
|
+
}, { tombstone: "passthrough" }), null);
|
|
8360
|
+
assertInstanceOf(await ctx.getObject(vocab.Tombstone, { id: "1" }), vocab.Tombstone);
|
|
8361
|
+
assertInstanceOf(await ctx.getObject(vocab.Object, { id: "deleted" }, { tombstone: "suppress" }), vocab.Tombstone);
|
|
8362
|
+
});
|
|
8363
|
+
await t.step("authorization predicates can get the tombstone", async () => {
|
|
8364
|
+
await withLogtapeLock(async () => {
|
|
8365
|
+
const records = [];
|
|
8366
|
+
await reset();
|
|
8367
|
+
try {
|
|
8368
|
+
await configure({
|
|
8369
|
+
sinks: { buffer(record) {
|
|
8370
|
+
records.push(record);
|
|
8371
|
+
} },
|
|
8372
|
+
filters: {},
|
|
8373
|
+
loggers: [{
|
|
8374
|
+
category: ["fedify", "federation"],
|
|
8375
|
+
sinks: ["buffer"]
|
|
8376
|
+
}, {
|
|
8377
|
+
category: ["logtape", "meta"],
|
|
8378
|
+
sinks: []
|
|
8379
|
+
}]
|
|
8380
|
+
});
|
|
8381
|
+
const federation = createFederation({
|
|
8382
|
+
kv: new MemoryKvStore(),
|
|
8383
|
+
documentLoaderFactory: () => mockDocumentLoader,
|
|
8384
|
+
contextLoaderFactory: () => mockDocumentLoader
|
|
8385
|
+
});
|
|
8386
|
+
let dispatched = 0;
|
|
8387
|
+
federation.setObjectDispatcher(vocab.Note, "/notes/{id}", (ctx, values) => {
|
|
8388
|
+
dispatched++;
|
|
8389
|
+
return new vocab.Tombstone({ id: ctx.getObjectUri(vocab.Note, values) });
|
|
8390
|
+
}).authorize(async (ctx, values) => {
|
|
8391
|
+
return await ctx.getObject(vocab.Note, values, { tombstone: "passthrough" }) instanceof vocab.Tombstone;
|
|
8392
|
+
});
|
|
8393
|
+
assertEquals((await federation.fetch(new Request("https://example.com/notes/1", { headers: { Accept: "application/activity+json" } }), { contextData: void 0 })).status, 410);
|
|
8394
|
+
assertEquals(dispatched, 2);
|
|
8395
|
+
assert(records.some((record) => getRawMessage(record).startsWith("RequestContext.getObject(") && getRawMessage(record).includes("may cause an infinite loop")));
|
|
8396
|
+
} finally {
|
|
8397
|
+
await reset();
|
|
8398
|
+
}
|
|
8399
|
+
});
|
|
8400
|
+
});
|
|
8401
|
+
await t.step("warns about the id of a tombstone", async () => {
|
|
8402
|
+
await withLogtapeLock(async () => {
|
|
8403
|
+
const records = [];
|
|
8404
|
+
await reset();
|
|
8405
|
+
try {
|
|
8406
|
+
await configure({
|
|
8407
|
+
sinks: { buffer(record) {
|
|
8408
|
+
records.push(record);
|
|
8409
|
+
} },
|
|
8410
|
+
filters: {},
|
|
8411
|
+
loggers: [{
|
|
8412
|
+
category: [
|
|
8413
|
+
"fedify",
|
|
8414
|
+
"federation",
|
|
8415
|
+
"object"
|
|
8416
|
+
],
|
|
8417
|
+
sinks: ["buffer"]
|
|
8418
|
+
}, {
|
|
8419
|
+
category: ["logtape", "meta"],
|
|
8420
|
+
sinks: []
|
|
8421
|
+
}]
|
|
8422
|
+
});
|
|
8423
|
+
const federation = createFederation({
|
|
8424
|
+
kv: new MemoryKvStore(),
|
|
8425
|
+
documentLoaderFactory: () => mockDocumentLoader,
|
|
8426
|
+
contextLoaderFactory: () => mockDocumentLoader
|
|
8427
|
+
});
|
|
8428
|
+
const ids = {
|
|
8429
|
+
"matching": new URL("https://example.com/notes/matching"),
|
|
8430
|
+
"missing": null,
|
|
8431
|
+
"mismatched": new URL("https://example.com/other/mismatched"),
|
|
8432
|
+
"ap": new URL("ap://did%3Akey%3Az6MkTest/notes/ap"),
|
|
8433
|
+
"ap-ef61": new URL("ap+ef61://did%3Akey%3Az6MkTest/notes/ap-ef61")
|
|
8434
|
+
};
|
|
8435
|
+
federation.setObjectDispatcher(vocab.Note, "/notes/{id}", (_ctx, { id }) => id === "live" ? new vocab.Note({ id: new URL("https://example.com/other/live") }) : new vocab.Tombstone({ id: ids[id] }));
|
|
8436
|
+
const warnings = async (id) => {
|
|
8437
|
+
records.length = 0;
|
|
8438
|
+
assertEquals((await federation.fetch(new Request(`https://example.com/notes/${id}`, { headers: { Accept: "application/activity+json" } }), { contextData: void 0 })).status, id === "live" ? 200 : 410);
|
|
8439
|
+
return records.filter((r) => r.level === "warning");
|
|
8440
|
+
};
|
|
8441
|
+
assertEquals(await warnings("matching"), []);
|
|
8442
|
+
const missing = await warnings("missing");
|
|
8443
|
+
assertEquals(missing.length, 1);
|
|
8444
|
+
assert(getRawMessage(missing[0]).includes("without an id property"));
|
|
8445
|
+
const mismatched = await warnings("mismatched");
|
|
8446
|
+
assertEquals(mismatched.length, 1);
|
|
8447
|
+
assert(getRawMessage(mismatched[0]).includes("does not match"));
|
|
8448
|
+
assertEquals(mismatched[0].properties.tombstoneId, "https://example.com/other/mismatched");
|
|
8449
|
+
assertEquals(mismatched[0].properties.objectUri, "https://example.com/notes/mismatched");
|
|
8450
|
+
assertEquals(await warnings("ap"), []);
|
|
8451
|
+
assertEquals(await warnings("ap-ef61"), []);
|
|
8452
|
+
assertEquals(await warnings("live"), []);
|
|
8453
|
+
} finally {
|
|
8454
|
+
await reset();
|
|
8455
|
+
}
|
|
8456
|
+
});
|
|
8457
|
+
});
|
|
8458
|
+
});
|
|
8459
|
+
test("createFederation() applies documentLoaderTimeout to built-in loaders", async (t) => {
|
|
8460
|
+
esm_default.mockGlobal();
|
|
8461
|
+
let requests = 0;
|
|
8462
|
+
esm_default.get("begin:https://slow.example/", () => {
|
|
8463
|
+
requests++;
|
|
8464
|
+
return new Promise(() => {});
|
|
8465
|
+
});
|
|
8466
|
+
const isTimeout = (error) => {
|
|
8467
|
+
ok(error instanceof FetchError, String(error));
|
|
8468
|
+
strictEqual(error.response, void 0);
|
|
8469
|
+
ok(error.cause instanceof DOMException);
|
|
8470
|
+
strictEqual(error.cause.name, "TimeoutError");
|
|
8471
|
+
ok(error.message.endsWith("Timed out after 100 ms"), error.message);
|
|
8472
|
+
return true;
|
|
8473
|
+
};
|
|
8474
|
+
try {
|
|
8475
|
+
await t.step("built-in loaders", async () => {
|
|
8476
|
+
const ctx = createFederation({
|
|
8477
|
+
kv: new MemoryKvStore(),
|
|
8478
|
+
allowPrivateAddress: true,
|
|
8479
|
+
documentLoaderTimeout: { milliseconds: 100 }
|
|
8480
|
+
}).createContext(new URL("https://example.com/"), void 0);
|
|
8481
|
+
await rejects(ctx.documentLoader("https://slow.example/object"), isTimeout);
|
|
8482
|
+
await rejects(ctx.contextLoader("https://slow.example/context"), isTimeout);
|
|
8483
|
+
await rejects(ctx.getDocumentLoader({
|
|
8484
|
+
keyId: new URL("https://example.com/key2"),
|
|
8485
|
+
privateKey: rsaPrivateKey2
|
|
8486
|
+
})("https://slow.example/private"), isTimeout);
|
|
8487
|
+
strictEqual(requests, 3);
|
|
8488
|
+
});
|
|
8489
|
+
await t.step("per-call override and null", async () => {
|
|
8490
|
+
const federation = createFederation({
|
|
8491
|
+
kv: new MemoryKvStore(),
|
|
8492
|
+
allowPrivateAddress: true,
|
|
8493
|
+
documentLoaderTimeout: null
|
|
8494
|
+
});
|
|
8495
|
+
await rejects(federation.documentLoaderFactory({ timeout: 100 })("https://slow.example/object"), isTimeout);
|
|
8496
|
+
await rejects(federation.authenticatedDocumentLoaderFactory({
|
|
8497
|
+
keyId: new URL("https://example.com/key2"),
|
|
8498
|
+
privateKey: rsaPrivateKey2
|
|
8499
|
+
}, { timeout: 100 })("https://slow.example/private"), isTimeout);
|
|
8500
|
+
const controller = new AbortController();
|
|
8501
|
+
const reason = /* @__PURE__ */ new Error("Canceled by the caller");
|
|
8502
|
+
setTimeout(() => controller.abort(reason), 150);
|
|
8503
|
+
await rejects(federation.documentLoaderFactory()("https://slow.example/object", { signal: controller.signal }), (error) => error === reason || error instanceof Error && error.name === "AbortError");
|
|
8504
|
+
});
|
|
8505
|
+
} finally {
|
|
8506
|
+
esm_default.hardReset();
|
|
8507
|
+
}
|
|
8508
|
+
});
|
|
8509
|
+
test("createFederation() validates documentLoaderTimeout", () => {
|
|
8510
|
+
for (const documentLoaderTimeout of [
|
|
8511
|
+
{ milliseconds: 0 },
|
|
8512
|
+
{ seconds: -1 },
|
|
8513
|
+
{ days: 25 },
|
|
8514
|
+
{ months: 1 }
|
|
8515
|
+
]) throws(() => createFederation({
|
|
8516
|
+
kv: new MemoryKvStore(),
|
|
8517
|
+
documentLoaderTimeout
|
|
8518
|
+
}), RangeError);
|
|
8519
|
+
createFederation({
|
|
8520
|
+
kv: new MemoryKvStore(),
|
|
8521
|
+
documentLoaderTimeout: Temporal.Duration.from({ seconds: 30 })
|
|
8522
|
+
});
|
|
8523
|
+
});
|
|
7592
8524
|
//#endregion
|
|
7593
8525
|
export {};
|