@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
|
@@ -3,7 +3,14 @@ import "urlpattern-polyfill";
|
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
4
|
import { getLogger } from "@logtape/logtape";
|
|
5
5
|
//#region src/federation/circuit-breaker.ts
|
|
6
|
-
const
|
|
6
|
+
const LEGACY_SWEEP_MARKER = ["__fedify_meta", "circuit_breaker_state_ttl_sweep_v2"];
|
|
7
|
+
const LEGACY_SWEEP_DELETING_MARKER = { __fedifyDeletingCircuitBreakerLegacyState: true };
|
|
8
|
+
const CIRCUIT_BREAKER_STATE_VERSION = 1;
|
|
9
|
+
const LEGACY_SWEEP_LOCK_TTL = Temporal.Duration.from({ minutes: 5 });
|
|
10
|
+
const LEGACY_SWEEP_RENEW_INTERVAL = Temporal.Duration.from({ minutes: 1 });
|
|
11
|
+
const LEGACY_SWEEP_RETRY_WINDOW = Temporal.Duration.from({ hours: 168 });
|
|
12
|
+
const LEGACY_SWEEP_WAIT_INTERVAL = 10;
|
|
13
|
+
const LEGACY_SWEEP_CAS_ATTEMPTS = 3;
|
|
7
14
|
/**
|
|
8
15
|
* Tracks reachability state for remote outbox delivery hosts.
|
|
9
16
|
* @since 2.3.0
|
|
@@ -13,6 +20,8 @@ var CircuitBreaker = class {
|
|
|
13
20
|
#prefix;
|
|
14
21
|
#options;
|
|
15
22
|
#now;
|
|
23
|
+
#legacySweep;
|
|
24
|
+
#legacySweepDone;
|
|
16
25
|
#stateChangeObserver;
|
|
17
26
|
constructor(options) {
|
|
18
27
|
this.#kv = options.kv;
|
|
@@ -29,6 +38,7 @@ var CircuitBreaker = class {
|
|
|
29
38
|
return now.until(this.#capHeldRetryAt(now, heldSince, now.add(delay)));
|
|
30
39
|
}
|
|
31
40
|
async beforeSend(remoteHost, message) {
|
|
41
|
+
this.#sweepLegacyStates();
|
|
32
42
|
const heldSince = parseHeldSince(message.circuitHeldSince);
|
|
33
43
|
const now = this.#now();
|
|
34
44
|
if (heldSince != null && Temporal.Instant.compare(heldSince.add(this.#options.heldActivityTtl), now) <= 0) return {
|
|
@@ -37,7 +47,7 @@ var CircuitBreaker = class {
|
|
|
37
47
|
};
|
|
38
48
|
let lastConflictingState;
|
|
39
49
|
for (let attempt = 0; attempt < 10; attempt++) {
|
|
40
|
-
const oldState = await this.#get(remoteHost);
|
|
50
|
+
const { state: oldState, raw: oldValue } = await this.#get(remoteHost);
|
|
41
51
|
if (oldState == null || oldState.state === "closed") return {
|
|
42
52
|
type: "send",
|
|
43
53
|
probe: false
|
|
@@ -63,7 +73,7 @@ var CircuitBreaker = class {
|
|
|
63
73
|
state: "half-open",
|
|
64
74
|
halfOpened: now.toString()
|
|
65
75
|
};
|
|
66
|
-
if (await this.#replace(remoteHost,
|
|
76
|
+
if (await this.#replace(remoteHost, oldValue, newState)) return {
|
|
67
77
|
type: "send",
|
|
68
78
|
probe: true
|
|
69
79
|
};
|
|
@@ -85,7 +95,7 @@ var CircuitBreaker = class {
|
|
|
85
95
|
state: "half-open",
|
|
86
96
|
halfOpened: now.toString()
|
|
87
97
|
};
|
|
88
|
-
if (await this.#replace(remoteHost,
|
|
98
|
+
if (await this.#replace(remoteHost, oldValue, newState)) {
|
|
89
99
|
await this.#notifyStateChange(remoteHost, "open", "half-open");
|
|
90
100
|
return {
|
|
91
101
|
type: "send",
|
|
@@ -110,10 +120,11 @@ var CircuitBreaker = class {
|
|
|
110
120
|
throw new Error(`Failed to update circuit breaker state for ${remoteHost}`);
|
|
111
121
|
}
|
|
112
122
|
async recordSuccess(remoteHost) {
|
|
123
|
+
this.#sweepLegacyStates();
|
|
113
124
|
for (let attempt = 0; attempt < 10; attempt++) {
|
|
114
|
-
const oldState = await this.#get(remoteHost);
|
|
125
|
+
const { state: oldState, raw: oldValue } = await this.#get(remoteHost);
|
|
115
126
|
if (oldState == null) return void 0;
|
|
116
|
-
if (await this.#replace(remoteHost,
|
|
127
|
+
if (await this.#replace(remoteHost, oldValue, void 0)) {
|
|
117
128
|
if (oldState.state !== "closed") {
|
|
118
129
|
await this.#notifyStateChange(remoteHost, oldState.state, "closed");
|
|
119
130
|
return {
|
|
@@ -130,9 +141,10 @@ var CircuitBreaker = class {
|
|
|
130
141
|
return await this.recordSuccess(remoteHost);
|
|
131
142
|
}
|
|
132
143
|
async recordFailure(remoteHost) {
|
|
144
|
+
this.#sweepLegacyStates();
|
|
133
145
|
const now = this.#now();
|
|
134
146
|
for (let attempt = 0; attempt < 10; attempt++) {
|
|
135
|
-
const oldState = await this.#get(remoteHost);
|
|
147
|
+
const { state: oldState, raw: oldValue } = await this.#get(remoteHost);
|
|
136
148
|
if (oldState?.state === "open") return void 0;
|
|
137
149
|
const oldFailures = oldState?.failures.map(Temporal.Instant.from) ?? [];
|
|
138
150
|
const failures = this.#options.pruneFailures([...oldFailures, now], now);
|
|
@@ -149,7 +161,7 @@ var CircuitBreaker = class {
|
|
|
149
161
|
state: "closed",
|
|
150
162
|
failures: failures.map((t) => t.toString())
|
|
151
163
|
};
|
|
152
|
-
if (await this.#replace(remoteHost,
|
|
164
|
+
if (await this.#replace(remoteHost, oldValue, newState)) {
|
|
153
165
|
if (transition != null) {
|
|
154
166
|
await this.#notifyStateChange(remoteHost, transition[0], transition[1]);
|
|
155
167
|
return {
|
|
@@ -177,26 +189,146 @@ var CircuitBreaker = class {
|
|
|
177
189
|
}
|
|
178
190
|
}
|
|
179
191
|
async getState(remoteHost) {
|
|
180
|
-
|
|
192
|
+
this.#sweepLegacyStates();
|
|
193
|
+
return stripStoredCircuitBreakerState((await this.#get(remoteHost)).state);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* The currently running background legacy sweep, if any.
|
|
197
|
+
* @internal
|
|
198
|
+
*/
|
|
199
|
+
get pendingSweep() {
|
|
200
|
+
return this.#legacySweep;
|
|
181
201
|
}
|
|
182
202
|
#key(remoteHost) {
|
|
183
203
|
return [...this.#prefix, remoteHost];
|
|
184
204
|
}
|
|
205
|
+
#legacySweepMarkerKey() {
|
|
206
|
+
return [...this.#prefix, ...LEGACY_SWEEP_MARKER];
|
|
207
|
+
}
|
|
208
|
+
#sweepLegacyStates() {
|
|
209
|
+
if (this.#kv.cas == null) return;
|
|
210
|
+
if (this.#options.stateTtl == null) return;
|
|
211
|
+
if (this.#legacySweep != null) return;
|
|
212
|
+
if (this.#isLegacySweepDone()) return;
|
|
213
|
+
this.#legacySweep = this.#sweepLegacyStatesImpl().catch((error) => {
|
|
214
|
+
getLogger([
|
|
215
|
+
"fedify",
|
|
216
|
+
"federation",
|
|
217
|
+
"circuit"
|
|
218
|
+
]).warn("Failed to sweep legacy circuit breaker state:\n{error}", { error });
|
|
219
|
+
}).finally(() => {
|
|
220
|
+
this.#legacySweep = void 0;
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
async #sweepLegacyStatesImpl() {
|
|
224
|
+
const markerKey = this.#legacySweepMarkerKey();
|
|
225
|
+
const acquired = await this.#acquireLegacySweep(markerKey);
|
|
226
|
+
if (acquired === "done") return;
|
|
227
|
+
let marker = acquired;
|
|
228
|
+
let leaseRenewedAt = this.#now();
|
|
229
|
+
try {
|
|
230
|
+
for await (const { key, value } of this.#kv.list(this.#prefix)) {
|
|
231
|
+
if (key.length !== this.#prefix.length + 1) continue;
|
|
232
|
+
await this.#migrateLegacyState(key, value);
|
|
233
|
+
if (Temporal.Instant.compare(this.#now(), leaseRenewedAt.add(LEGACY_SWEEP_RENEW_INTERVAL)) >= 0) {
|
|
234
|
+
const renewed = await this.#renewLegacySweep(markerKey, marker);
|
|
235
|
+
if (renewed == null) return;
|
|
236
|
+
marker = renewed;
|
|
237
|
+
leaseRenewedAt = this.#now();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
} catch (error) {
|
|
241
|
+
await this.#deleteIfUnchanged(markerKey, marker);
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
const finishedMarker = this.#finishLegacySweepMarker(marker);
|
|
245
|
+
if (await this.#kv.cas(markerKey, marker, finishedMarker)) this.#rememberLegacySweepMarker(finishedMarker);
|
|
246
|
+
}
|
|
247
|
+
async #renewLegacySweep(markerKey, marker) {
|
|
248
|
+
const renewed = {
|
|
249
|
+
state: "sweeping",
|
|
250
|
+
started: this.#now().toString(),
|
|
251
|
+
retryUntil: getLegacySweepRetryUntil(marker) ?? this.#now().add(LEGACY_SWEEP_RETRY_WINDOW).toString()
|
|
252
|
+
};
|
|
253
|
+
if (await this.#kv.cas(markerKey, marker, renewed, { ttl: LEGACY_SWEEP_LOCK_TTL })) return renewed;
|
|
254
|
+
}
|
|
255
|
+
#finishLegacySweepMarker(marker) {
|
|
256
|
+
const retryUntil = getLegacySweepRetryUntil(marker);
|
|
257
|
+
if (retryUntil != null && Temporal.Instant.compare(this.#now(), Temporal.Instant.from(retryUntil)) < 0) return {
|
|
258
|
+
state: "done",
|
|
259
|
+
retryUntil
|
|
260
|
+
};
|
|
261
|
+
return { state: "final" };
|
|
262
|
+
}
|
|
263
|
+
async #migrateLegacyState(key, value) {
|
|
264
|
+
if (isCurrentCircuitBreakerState(value)) {
|
|
265
|
+
await this.#kv.cas(key, value, value, this.#setOptions());
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const state = parseCircuitBreakerKvState(value);
|
|
269
|
+
if (state != null) {
|
|
270
|
+
await this.#kv.cas(key, value, markCircuitBreakerState(state), this.#setOptions());
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
await this.#deleteIfUnchanged(key, value);
|
|
274
|
+
}
|
|
275
|
+
async #deleteIfUnchanged(key, value) {
|
|
276
|
+
if (await this.#kv.cas(key, value, LEGACY_SWEEP_DELETING_MARKER, { ttl: LEGACY_SWEEP_LOCK_TTL })) await this.#kv.delete(key);
|
|
277
|
+
}
|
|
278
|
+
async #acquireLegacySweep(markerKey) {
|
|
279
|
+
for (let attempt = 0; attempt < LEGACY_SWEEP_CAS_ATTEMPTS; attempt++) {
|
|
280
|
+
const marker = await this.#kv.get(markerKey);
|
|
281
|
+
const now = this.#now();
|
|
282
|
+
if (isLegacySweepActive(marker, now)) {
|
|
283
|
+
this.#rememberLegacySweepMarker(marker);
|
|
284
|
+
return "done";
|
|
285
|
+
}
|
|
286
|
+
const retryUntil = isLegacySweepRetryDue(marker, now) ? marker.retryUntil : getLegacySweepRetryUntil(marker) ?? now.add(LEGACY_SWEEP_RETRY_WINDOW).toString();
|
|
287
|
+
const sweeping = {
|
|
288
|
+
state: "sweeping",
|
|
289
|
+
started: now.toString(),
|
|
290
|
+
retryUntil
|
|
291
|
+
};
|
|
292
|
+
if (await this.#kv.cas(markerKey, marker ?? void 0, sweeping, { ttl: LEGACY_SWEEP_LOCK_TTL })) return sweeping;
|
|
293
|
+
await delay(LEGACY_SWEEP_WAIT_INTERVAL);
|
|
294
|
+
}
|
|
295
|
+
return "done";
|
|
296
|
+
}
|
|
297
|
+
#isLegacySweepDone() {
|
|
298
|
+
if (this.#legacySweepDone === "final") return true;
|
|
299
|
+
if (this.#legacySweepDone == null) return false;
|
|
300
|
+
if (Temporal.Instant.compare(this.#now(), this.#legacySweepDone) < 0) return true;
|
|
301
|
+
this.#legacySweepDone = void 0;
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
#rememberLegacySweepMarker(marker) {
|
|
305
|
+
const doneUntil = getLegacySweepDoneUntil(marker, this.#now());
|
|
306
|
+
if (doneUntil != null) this.#legacySweepDone = doneUntil;
|
|
307
|
+
}
|
|
185
308
|
#capHeldRetryAt(now, heldSince, retryAt) {
|
|
186
309
|
const expiresAt = (heldSince ?? now).add(this.#options.heldActivityTtl);
|
|
187
310
|
return Temporal.Instant.compare(expiresAt, retryAt) < 0 ? expiresAt : retryAt;
|
|
188
311
|
}
|
|
189
312
|
async #get(remoteHost) {
|
|
190
|
-
|
|
313
|
+
const value = await this.#kv.get(this.#key(remoteHost));
|
|
314
|
+
const state = parseStoredCircuitBreakerKvState(value);
|
|
315
|
+
return {
|
|
316
|
+
state,
|
|
317
|
+
raw: state == null ? void 0 : value
|
|
318
|
+
};
|
|
191
319
|
}
|
|
192
|
-
async #replace(remoteHost,
|
|
320
|
+
async #replace(remoteHost, oldValue, newState) {
|
|
193
321
|
const key = this.#key(remoteHost);
|
|
322
|
+
const storedState = newState == null ? void 0 : markCircuitBreakerState(newState);
|
|
194
323
|
if (this.#kv.cas == null) {
|
|
195
|
-
if (
|
|
196
|
-
else await this.#kv.set(key,
|
|
324
|
+
if (storedState == null) await this.#kv.delete(key);
|
|
325
|
+
else await this.#kv.set(key, storedState, this.#setOptions());
|
|
197
326
|
return true;
|
|
198
327
|
}
|
|
199
|
-
return await this.#kv.cas(key,
|
|
328
|
+
return await this.#kv.cas(key, oldValue, storedState, storedState == null ? void 0 : this.#setOptions());
|
|
329
|
+
}
|
|
330
|
+
#setOptions() {
|
|
331
|
+
return this.#options.stateTtl == null ? void 0 : { ttl: this.#options.stateTtl };
|
|
200
332
|
}
|
|
201
333
|
async #notifyStateChange(remoteHost, previousState, newState) {
|
|
202
334
|
try {
|
|
@@ -236,18 +368,25 @@ var CircuitBreaker = class {
|
|
|
236
368
|
* @param options The public circuit breaker options supplied to Fedify.
|
|
237
369
|
* @returns The normalized failure predicate, failure pruning function,
|
|
238
370
|
* duration values, and optional callbacks with defaults applied.
|
|
239
|
-
* @throws {RangeError} If any configured duration is not positive
|
|
371
|
+
* @throws {RangeError} If any configured duration is not positive, or if
|
|
372
|
+
* `stateTtl` is shorter than `recoveryDelay`.
|
|
240
373
|
* @throws {TypeError} If `failureThreshold` is not a positive integer.
|
|
241
374
|
*/
|
|
242
375
|
function normalizeCircuitBreakerOptions(options) {
|
|
243
376
|
const recoveryDelay = toInstantDuration(options.recoveryDelay ?? { minutes: 30 });
|
|
244
377
|
const heldActivityTtl = toInstantDuration(options.heldActivityTtl ?? { hours: 168 });
|
|
245
378
|
const releaseInterval = toInstantDuration(options.releaseInterval ?? { seconds: 1 });
|
|
379
|
+
const configuredStateTtl = options.stateTtl == null ? void 0 : toInstantDuration(options.stateTtl);
|
|
246
380
|
assertPositiveDuration(recoveryDelay, "recoveryDelay");
|
|
247
381
|
assertPositiveDuration(heldActivityTtl, "heldActivityTtl");
|
|
248
382
|
assertPositiveDuration(releaseInterval, "releaseInterval");
|
|
383
|
+
if (configuredStateTtl != null) {
|
|
384
|
+
assertPositiveDuration(configuredStateTtl, "stateTtl");
|
|
385
|
+
if (Temporal.Duration.compare(configuredStateTtl, recoveryDelay) < 0) throw new RangeError("stateTtl must be at least recoveryDelay.");
|
|
386
|
+
}
|
|
249
387
|
let failure;
|
|
250
388
|
let pruneFailures;
|
|
389
|
+
let stateTtl;
|
|
251
390
|
if (options.failure == null) {
|
|
252
391
|
const failureThreshold = options.failureThreshold ?? 5;
|
|
253
392
|
if (!Number.isInteger(failureThreshold) || failureThreshold <= 0) throw new TypeError("failureThreshold must be a positive integer.");
|
|
@@ -263,9 +402,11 @@ function normalizeCircuitBreakerOptions(options) {
|
|
|
263
402
|
const last = timestamps[timestamps.length - 1];
|
|
264
403
|
return Temporal.Duration.compare(first.until(last), failureWindow) <= 0;
|
|
265
404
|
};
|
|
405
|
+
stateTtl = configuredStateTtl ?? maxDuration(recoveryDelay.add(failureWindow), recoveryDelay.add(heldActivityTtl));
|
|
266
406
|
} else {
|
|
267
407
|
failure = options.failure;
|
|
268
|
-
pruneFailures = (timestamps) => timestamps.slice(-
|
|
408
|
+
pruneFailures = (timestamps) => timestamps.slice(-100);
|
|
409
|
+
stateTtl = configuredStateTtl ?? recoveryDelay.add(heldActivityTtl);
|
|
269
410
|
}
|
|
270
411
|
return {
|
|
271
412
|
failure,
|
|
@@ -273,10 +414,72 @@ function normalizeCircuitBreakerOptions(options) {
|
|
|
273
414
|
recoveryDelay,
|
|
274
415
|
heldActivityTtl,
|
|
275
416
|
releaseInterval,
|
|
417
|
+
stateTtl,
|
|
276
418
|
onStateChange: options.onStateChange,
|
|
277
419
|
onActivityDrop: options.onActivityDrop
|
|
278
420
|
};
|
|
279
421
|
}
|
|
422
|
+
function maxDuration(duration, ...durations) {
|
|
423
|
+
return durations.reduce((max, candidate) => Temporal.Duration.compare(candidate, max) > 0 ? candidate : max, duration);
|
|
424
|
+
}
|
|
425
|
+
function isLegacySweepDone(value, now) {
|
|
426
|
+
return getLegacySweepDoneUntil(value, now) != null;
|
|
427
|
+
}
|
|
428
|
+
function getLegacySweepDoneUntil(value, now) {
|
|
429
|
+
if (typeof value !== "object" || value == null || !("state" in value)) return;
|
|
430
|
+
if (value.state === "final") return "final";
|
|
431
|
+
if (value.state === "done" && "retryUntil" in value && typeof value.retryUntil === "string") try {
|
|
432
|
+
const retryUntil = Temporal.Instant.from(value.retryUntil);
|
|
433
|
+
return Temporal.Instant.compare(now, retryUntil) < 0 ? retryUntil : void 0;
|
|
434
|
+
} catch {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
function isLegacySweepRetryDue(value, now) {
|
|
439
|
+
if (typeof value !== "object" || value == null || !("state" in value) || value.state !== "done" || !("retryUntil" in value) || typeof value.retryUntil !== "string") return false;
|
|
440
|
+
try {
|
|
441
|
+
return Temporal.Instant.compare(now, Temporal.Instant.from(value.retryUntil)) >= 0;
|
|
442
|
+
} catch {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function isLegacySweepActive(value, now) {
|
|
447
|
+
return isLegacySweepDone(value, now) || isLegacySweepInProgress(value, now);
|
|
448
|
+
}
|
|
449
|
+
function isLegacySweepInProgress(value, now) {
|
|
450
|
+
if (typeof value !== "object" || value == null || !("state" in value) || value.state !== "sweeping" || !("started" in value) || typeof value.started !== "string") return false;
|
|
451
|
+
try {
|
|
452
|
+
return Temporal.Instant.compare(now, Temporal.Instant.from(value.started).add(LEGACY_SWEEP_LOCK_TTL)) < 0;
|
|
453
|
+
} catch {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
function getLegacySweepRetryUntil(value) {
|
|
458
|
+
if (typeof value !== "object" || value == null || !("retryUntil" in value) || typeof value.retryUntil !== "string") return;
|
|
459
|
+
try {
|
|
460
|
+
Temporal.Instant.from(value.retryUntil);
|
|
461
|
+
return value.retryUntil;
|
|
462
|
+
} catch {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function isCurrentCircuitBreakerState(value) {
|
|
467
|
+
return typeof value === "object" && value != null && "__fedifyCircuitBreakerStateVersion" in value && value.__fedifyCircuitBreakerStateVersion === CIRCUIT_BREAKER_STATE_VERSION;
|
|
468
|
+
}
|
|
469
|
+
function markCircuitBreakerState(state) {
|
|
470
|
+
return {
|
|
471
|
+
...state,
|
|
472
|
+
__fedifyCircuitBreakerStateVersion: CIRCUIT_BREAKER_STATE_VERSION
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
function stripStoredCircuitBreakerState(state) {
|
|
476
|
+
if (state == null) return void 0;
|
|
477
|
+
const { __fedifyCircuitBreakerStateVersion: _, ...publicState } = state;
|
|
478
|
+
return publicState;
|
|
479
|
+
}
|
|
480
|
+
function delay(milliseconds) {
|
|
481
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
482
|
+
}
|
|
280
483
|
function toInstantDuration(duration) {
|
|
281
484
|
const parsed = Temporal.Duration.from(duration);
|
|
282
485
|
return Temporal.Duration.from({ milliseconds: Math.trunc(parsed.total({
|
|
@@ -311,6 +514,9 @@ function parseHeldSince(value) {
|
|
|
311
514
|
* valid instant strings, or `undefined` when the stored value is malformed.
|
|
312
515
|
*/
|
|
313
516
|
function parseCircuitBreakerKvState(value) {
|
|
517
|
+
return stripStoredCircuitBreakerState(parseStoredCircuitBreakerKvState(value));
|
|
518
|
+
}
|
|
519
|
+
function parseStoredCircuitBreakerKvState(value) {
|
|
314
520
|
const isInstantString = (v) => {
|
|
315
521
|
if (typeof v !== "string") return false;
|
|
316
522
|
try {
|
|
@@ -330,7 +536,8 @@ function parseCircuitBreakerKvState(value) {
|
|
|
330
536
|
state: record.state,
|
|
331
537
|
failures: record.failures,
|
|
332
538
|
...record.opened == null ? {} : { opened: record.opened },
|
|
333
|
-
...record.halfOpened == null ? {} : { halfOpened: record.halfOpened }
|
|
539
|
+
...record.halfOpened == null ? {} : { halfOpened: record.halfOpened },
|
|
540
|
+
...record.__fedifyCircuitBreakerStateVersion === CIRCUIT_BREAKER_STATE_VERSION ? { __fedifyCircuitBreakerStateVersion: CIRCUIT_BREAKER_STATE_VERSION } : {}
|
|
334
541
|
};
|
|
335
542
|
}
|
|
336
543
|
//#endregion
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import "temporal-polyfill";
|
|
2
|
+
import "urlpattern-polyfill";
|
|
3
|
+
globalThis.addEventListener = () => {};
|
|
4
|
+
//#region src/federation/circuit-breaker-test-utils.ts
|
|
5
|
+
function markCircuitBreakerLegacySweepDone(kv) {
|
|
6
|
+
return kv.set([
|
|
7
|
+
"_fedify",
|
|
8
|
+
"circuit",
|
|
9
|
+
"__fedify_meta",
|
|
10
|
+
"circuit_breaker_state_ttl_sweep_v2"
|
|
11
|
+
], { state: "final" });
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { markCircuitBreakerLegacySweepDone as t };
|
|
@@ -2,22 +2,157 @@ import "temporal-polyfill";
|
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
4
|
import { getLogger } from "@logtape/logtape";
|
|
5
|
-
import { getUserAgent } from "@fedify/vocab-runtime";
|
|
5
|
+
import { FetchError, UrlError, getUserAgent, validatePublicUrl } from "@fedify/vocab-runtime";
|
|
6
|
+
//#region src/utils/body.ts
|
|
7
|
+
/** Decoded JSON body limit: 16 MiB. @internal */
|
|
8
|
+
const MAX_BODY_SIZE = 16 * 1024 * 1024;
|
|
9
|
+
/** A body exceeded the byte limit. @internal */
|
|
10
|
+
var BodyTooLargeError = class extends FetchError {
|
|
11
|
+
/** Creates an error for a body exceeding the given limit. */
|
|
12
|
+
constructor(url, maxBytes) {
|
|
13
|
+
super(url, `Body exceeds the limit of ${maxBytes} bytes`);
|
|
14
|
+
this.name = "BodyTooLargeError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
/** Validates a finite, positive byte limit. @internal */
|
|
18
|
+
function validateBodySizeLimit(maxBytes) {
|
|
19
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new RangeError("The body size limit must be a positive safe integer.");
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Reads UTF-8 text while limiting bytes received from the body stream.
|
|
23
|
+
* Fetch responses are already decompressed, so the stream count is authoritative.
|
|
24
|
+
* @param message The response or request to read.
|
|
25
|
+
* @param maxBytes The maximum number of decoded bytes to read.
|
|
26
|
+
* @param url The URL to include in errors and logs.
|
|
27
|
+
* @returns The decoded body text.
|
|
28
|
+
* @throws {BodyTooLargeError} If the body exceeds the limit.
|
|
29
|
+
* @internal
|
|
30
|
+
*/
|
|
31
|
+
async function readBoundedText(message, maxBytes, url) {
|
|
32
|
+
validateBodySizeLimit(maxBytes);
|
|
33
|
+
const reader = message.body?.getReader();
|
|
34
|
+
const tooLarge = () => {
|
|
35
|
+
getLogger([
|
|
36
|
+
"fedify",
|
|
37
|
+
"runtime",
|
|
38
|
+
"body"
|
|
39
|
+
]).warn("Body from {url} exceeds the limit of {maxBytes} bytes.", {
|
|
40
|
+
url: url.toString(),
|
|
41
|
+
maxBytes
|
|
42
|
+
});
|
|
43
|
+
throw new BodyTooLargeError(url, maxBytes);
|
|
44
|
+
};
|
|
45
|
+
try {
|
|
46
|
+
const length = message.headers.get("Content-Length");
|
|
47
|
+
const encoding = message.headers.get("Content-Encoding");
|
|
48
|
+
if ((encoding == null || encoding.toLowerCase() === "identity") && length != null && /^\d+$/.test(length) && Number(length) > maxBytes) tooLarge();
|
|
49
|
+
if (reader == null) return "";
|
|
50
|
+
let size = 0;
|
|
51
|
+
const textReader = new ReadableStream({ async pull(controller) {
|
|
52
|
+
const { done, value } = await reader.read();
|
|
53
|
+
if (done) {
|
|
54
|
+
controller.close();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
size += value.byteLength;
|
|
58
|
+
if (size > maxBytes) tooLarge();
|
|
59
|
+
controller.enqueue(value);
|
|
60
|
+
} }, { highWaterMark: 0 }).pipeThrough(new TextDecoderStream()).getReader();
|
|
61
|
+
try {
|
|
62
|
+
const chunks = [];
|
|
63
|
+
while (true) {
|
|
64
|
+
const { done, value } = await textReader.read();
|
|
65
|
+
if (done) break;
|
|
66
|
+
chunks.push(value);
|
|
67
|
+
}
|
|
68
|
+
return chunks.join("");
|
|
69
|
+
} finally {
|
|
70
|
+
textReader.releaseLock();
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (reader != null) reader.cancel(error).catch(() => {});
|
|
74
|
+
throw error;
|
|
75
|
+
} finally {
|
|
76
|
+
reader?.releaseLock();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
6
80
|
//#region src/nodeinfo/client.ts
|
|
7
81
|
const logger = getLogger([
|
|
8
82
|
"fedify",
|
|
9
83
|
"nodeinfo",
|
|
10
84
|
"client"
|
|
11
85
|
]);
|
|
86
|
+
const DEFAULT_MAX_REDIRECTION = 5;
|
|
87
|
+
/**
|
|
88
|
+
* Fetches a URL while guarding against SSRF. Before every request—including
|
|
89
|
+
* each redirect hop—the destination is validated with
|
|
90
|
+
* {@link validatePublicUrl}, so that loopback, link-local, and private
|
|
91
|
+
* addresses, as well as non-HTTP(S) schemes such as `data:`, are rejected.
|
|
92
|
+
* Redirects are followed manually with a cap, so that a public URL cannot
|
|
93
|
+
* bounce the request to an internal address.
|
|
94
|
+
* @param url The URL to fetch.
|
|
95
|
+
* @param headers The request headers.
|
|
96
|
+
* @returns The final response, or `null` if the URL is disallowed, its
|
|
97
|
+
* hostname cannot be resolved, or there are too many redirects.
|
|
98
|
+
*/
|
|
99
|
+
async function fetchPublicUrl(url, headers) {
|
|
100
|
+
let current = url instanceof URL ? url : new URL(url);
|
|
101
|
+
let redirected = 0;
|
|
102
|
+
while (true) {
|
|
103
|
+
try {
|
|
104
|
+
await validatePublicUrl(current.href);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
if (e instanceof UrlError) {
|
|
107
|
+
if (e.reason === "dns") logger.error("DNS lookup failed for {url}: {error}", {
|
|
108
|
+
url: current.href,
|
|
109
|
+
error: e
|
|
110
|
+
});
|
|
111
|
+
else logger.error("Refused to fetch a private or invalid URL {url}: {error}", {
|
|
112
|
+
url: current.href,
|
|
113
|
+
error: e
|
|
114
|
+
});
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
throw e;
|
|
118
|
+
}
|
|
119
|
+
const response = await fetch(current, {
|
|
120
|
+
headers,
|
|
121
|
+
redirect: "manual"
|
|
122
|
+
});
|
|
123
|
+
if (response.status >= 300 && response.status < 400 && response.headers.has("Location")) {
|
|
124
|
+
redirected++;
|
|
125
|
+
if (redirected >= DEFAULT_MAX_REDIRECTION) {
|
|
126
|
+
logger.error("Too many redirections ({redirections}) while fetching {url}.", {
|
|
127
|
+
redirections: redirected,
|
|
128
|
+
url: current.href
|
|
129
|
+
});
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const next = new URL(response.headers.get("Location"), response.url == null || response.url === "" ? current : response.url);
|
|
133
|
+
if (next.protocol !== current.protocol) {
|
|
134
|
+
logger.error("Refused a redirect to a different protocol ({protocol} to {redirectedProtocol}) while fetching NodeInfo.", {
|
|
135
|
+
protocol: current.protocol,
|
|
136
|
+
redirectedProtocol: next.protocol
|
|
137
|
+
});
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
current = next;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
return response;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
12
146
|
async function getNodeInfo(url, options = {}) {
|
|
13
147
|
try {
|
|
14
148
|
let nodeInfoUrl = url;
|
|
15
149
|
if (!options.direct) {
|
|
16
150
|
const wellKnownUrl = new URL("/.well-known/nodeinfo", url);
|
|
17
|
-
const wellKnownResponse = await
|
|
151
|
+
const wellKnownResponse = await fetchPublicUrl(wellKnownUrl, {
|
|
18
152
|
Accept: "application/json",
|
|
19
153
|
"User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
|
|
20
|
-
}
|
|
154
|
+
});
|
|
155
|
+
if (wellKnownResponse == null) return void 0;
|
|
21
156
|
if (!wellKnownResponse.ok) {
|
|
22
157
|
logger.error("Failed to fetch {url}: {status} {statusText}", {
|
|
23
158
|
url: wellKnownUrl.href,
|
|
@@ -26,7 +161,7 @@ async function getNodeInfo(url, options = {}) {
|
|
|
26
161
|
});
|
|
27
162
|
return;
|
|
28
163
|
}
|
|
29
|
-
const wellKnownRd = await wellKnownResponse
|
|
164
|
+
const wellKnownRd = JSON.parse(await readBoundedText(wellKnownResponse, MAX_BODY_SIZE, wellKnownUrl));
|
|
30
165
|
const link = wellKnownRd?.links?.find((link) => link != null && "rel" in link && (link.rel === "http://nodeinfo.diaspora.software/ns/schema/2.0" || link.rel === "http://nodeinfo.diaspora.software/ns/schema/2.1") && "href" in link && link.href != null);
|
|
31
166
|
if (link == null || link.href == null) {
|
|
32
167
|
logger.error("Failed to find a NodeInfo document link from {url}: {resourceDescriptor}", {
|
|
@@ -37,10 +172,11 @@ async function getNodeInfo(url, options = {}) {
|
|
|
37
172
|
}
|
|
38
173
|
nodeInfoUrl = link.href;
|
|
39
174
|
}
|
|
40
|
-
const response = await
|
|
175
|
+
const response = await fetchPublicUrl(nodeInfoUrl, {
|
|
41
176
|
Accept: "application/json",
|
|
42
177
|
"User-Agent": typeof options.userAgent === "string" ? options.userAgent : getUserAgent(options.userAgent)
|
|
43
|
-
}
|
|
178
|
+
});
|
|
179
|
+
if (response == null) return void 0;
|
|
44
180
|
if (!response.ok) {
|
|
45
181
|
logger.error("Failed to fetch NodeInfo document from {url}: {status} {statusText}", {
|
|
46
182
|
url: nodeInfoUrl.toString(),
|
|
@@ -49,7 +185,7 @@ async function getNodeInfo(url, options = {}) {
|
|
|
49
185
|
});
|
|
50
186
|
return;
|
|
51
187
|
}
|
|
52
|
-
const data = await response
|
|
188
|
+
const data = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, nodeInfoUrl));
|
|
53
189
|
if (options.parse === "none") return data;
|
|
54
190
|
return parseNodeInfo(data, { tryBestEffort: options.parse === "best-effort" }) ?? void 0;
|
|
55
191
|
} catch (error) {
|
|
@@ -253,4 +389,4 @@ function parseUsage(data, options = {}) {
|
|
|
253
389
|
};
|
|
254
390
|
}
|
|
255
391
|
//#endregion
|
|
256
|
-
export { parseProtocol as a, parseUsage as c, parseOutboundService as i, parseInboundService as n, parseServices as o, parseNodeInfo as r, parseSoftware as s, getNodeInfo as t };
|
|
392
|
+
export { parseProtocol as a, parseUsage as c, readBoundedText as d, parseOutboundService as i, BodyTooLargeError as l, parseInboundService as n, parseServices as o, parseNodeInfo as r, parseSoftware as s, getNodeInfo as t, MAX_BODY_SIZE as u };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import "temporal-polyfill";
|
|
1
|
+
import { Temporal } from "temporal-polyfill";
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
4
|
import { Link, Object as Object$1 } from "@fedify/vocab";
|
|
@@ -16,8 +16,14 @@ var TaskCodec = class TaskCodec {
|
|
|
16
16
|
this.options = options;
|
|
17
17
|
}
|
|
18
18
|
/** Serializes `data`, encoding any vocabulary object as its JSON-LD. */
|
|
19
|
-
serialize = (data) => stringifyAsync(data, {
|
|
20
|
-
|
|
19
|
+
serialize = (data) => stringifyAsync(data, {
|
|
20
|
+
Vocab: this.#stringifyVocab,
|
|
21
|
+
FedifyTemporal: stringifyTemporal
|
|
22
|
+
});
|
|
23
|
+
deserialize = async (raw) => await this.#revive(/* @__PURE__ */ new Map())(parse(raw, {
|
|
24
|
+
Vocab: VocabHolder.from,
|
|
25
|
+
FedifyTemporal: parseTemporal
|
|
26
|
+
}));
|
|
21
27
|
/** Validates `data` against `schema`, then serializes it. */
|
|
22
28
|
encode = async (schema, data) => this.serialize(await TaskCodec.validate(schema, data));
|
|
23
29
|
/** Deserializes `raw`, then validates the result against `schema`. */
|
|
@@ -83,6 +89,23 @@ var TaskCodec = class TaskCodec {
|
|
|
83
89
|
];
|
|
84
90
|
};
|
|
85
91
|
const isVocab = (value) => value instanceof Object$1 || value instanceof Link;
|
|
92
|
+
const temporalTypeNames = [
|
|
93
|
+
"Duration",
|
|
94
|
+
"Instant",
|
|
95
|
+
"PlainDate",
|
|
96
|
+
"PlainTime",
|
|
97
|
+
"PlainDateTime",
|
|
98
|
+
"PlainMonthDay",
|
|
99
|
+
"PlainYearMonth",
|
|
100
|
+
"ZonedDateTime"
|
|
101
|
+
];
|
|
102
|
+
function stringifyTemporal(value) {
|
|
103
|
+
for (const name of temporalTypeNames) if (value instanceof Temporal[name]) return [name, value.toString()];
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
function parseTemporal([name, value]) {
|
|
107
|
+
return Temporal[name].from(value);
|
|
108
|
+
}
|
|
86
109
|
const isPlainObject = (value) => value === null || typeof value !== "object" ? false : isObjectPrototype(globalThis.Object.getPrototypeOf(value));
|
|
87
110
|
const isObjectPrototype = (proto) => proto === null || proto === globalThis.Object.prototype;
|
|
88
111
|
const isInstanceOf = (cls) => (v) => v instanceof cls;
|
|
@@ -13,7 +13,7 @@ import { encodeHex } from "byte-encodings/hex";
|
|
|
13
13
|
async function digest(uris) {
|
|
14
14
|
const processed = /* @__PURE__ */ new Set();
|
|
15
15
|
const encoder = new TextEncoder();
|
|
16
|
-
const result = new Uint8Array(32);
|
|
16
|
+
const result = /* @__PURE__ */ new Uint8Array(32);
|
|
17
17
|
for (const uri of uris) {
|
|
18
18
|
const u = uri instanceof URL ? uri.href : uri;
|
|
19
19
|
if (processed.has(u)) continue;
|
package/dist/compat/mod.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const { URLPattern } = require("urlpattern-polyfill");
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
const require_transformers = require("../transformers-
|
|
3
|
+
const require_transformers = require("../transformers-BalCrSsr.cjs");
|
|
4
4
|
exports.actorDehydrator = require_transformers.actorDehydrator;
|
|
5
5
|
exports.autoIdAssigner = require_transformers.autoIdAssigner;
|
|
6
6
|
exports.getDefaultActivityTransformers = require_transformers.getDefaultActivityTransformers;
|
package/dist/compat/mod.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/// <reference lib="esnext.temporal" />
|
|
2
|
-
import {
|
|
2
|
+
import { en as ActivityTransformer, n as Context } from "../context-tsYPJ2Ew.cjs";
|
|
3
3
|
import { Activity } from "@fedify/vocab";
|
|
4
4
|
|
|
5
5
|
//#region src/compat/transformers.d.ts
|
package/dist/compat/mod.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/// <reference lib="esnext.temporal" />
|
|
2
|
-
import {
|
|
2
|
+
import { en as ActivityTransformer, n as Context } from "../context-7NyiqIcg.js";
|
|
3
3
|
import { Activity } from "@fedify/vocab";
|
|
4
4
|
|
|
5
5
|
//#region src/compat/transformers.d.ts
|
package/dist/compat/mod.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { Temporal } from "temporal-polyfill";
|
|
2
2
|
import { URLPattern } from "urlpattern-polyfill";
|
|
3
|
-
import { n as autoIdAssigner, r as getDefaultActivityTransformers, t as actorDehydrator } from "../transformers-
|
|
3
|
+
import { n as autoIdAssigner, r as getDefaultActivityTransformers, t as actorDehydrator } from "../transformers-DAtXl_Kc.js";
|
|
4
4
|
export { actorDehydrator, autoIdAssigner, getDefaultActivityTransformers };
|