@fedify/fedify 2.3.1 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +11 -8
  2. package/dist/{builder-hivpmhlD.mjs → builder-Dx4EI2YS.mjs} +4 -2
  3. package/dist/{circuit-breaker-CSWsyoef.mjs → circuit-breaker-DLS_n1x5.mjs} +190 -7
  4. package/dist/circuit-breaker-test-utils-BVnv3T76.mjs +14 -0
  5. package/dist/{client-ByXmQhYD.mjs → client-D1KuWxjH.mjs} +63 -5
  6. package/dist/compat/mod.d.cts +1 -1
  7. package/dist/compat/mod.d.ts +1 -1
  8. package/dist/compat/transformers.test.mjs +1 -1
  9. package/dist/{context-BBVLF7lx.d.cts → context-Bjswu3eC.d.cts} +18 -1
  10. package/dist/{context-BU6jSQdo.d.ts → context-COxc_O-c.d.ts} +18 -1
  11. package/dist/{deno-C8RG0HM3.mjs → deno-B8lcQ4Ms.mjs} +1 -1
  12. package/dist/{docloader-YZA0m_3O.mjs → docloader-BMjxpyJD.mjs} +2 -2
  13. package/dist/federation/builder.test.mjs +13 -1
  14. package/dist/federation/circuit-breaker.test.mjs +622 -3
  15. package/dist/federation/handler.test.mjs +3 -3
  16. package/dist/federation/idempotency.test.mjs +2 -2
  17. package/dist/federation/metrics.test.mjs +1 -1
  18. package/dist/federation/middleware.test.mjs +16 -8
  19. package/dist/federation/mod.cjs +1 -1
  20. package/dist/federation/mod.d.cts +2 -2
  21. package/dist/federation/mod.d.ts +2 -2
  22. package/dist/federation/mod.js +1 -1
  23. package/dist/federation/send.test.mjs +3 -3
  24. package/dist/federation/temporal.test.mjs +1 -1
  25. package/dist/federation/webfinger.test.mjs +69 -7
  26. package/dist/{http-BajqV0pH.cjs → http-CpdQD2ke.cjs} +1 -1
  27. package/dist/{http-DPOMa1DD.js → http-DVZxcZEr.js} +1 -1
  28. package/dist/{http-PCt0862M.mjs → http-DlSQMJsb.mjs} +3 -3
  29. package/dist/{key-BKYwmbur.mjs → key-BqT9YrM3.mjs} +2 -2
  30. package/dist/{kv-cache-IjRceERu.js → kv-cache-B52kTPj4.js} +1 -1
  31. package/dist/{kv-cache-CVG3m2_-.cjs → kv-cache-B8gm4cVB.cjs} +1 -1
  32. package/dist/{kv-cache-Ceg0c1wh.mjs → kv-cache-BD6BgCTF.mjs} +1 -1
  33. package/dist/{ld-Dcw2skfn.mjs → ld-hwRDfZqq.mjs} +3 -3
  34. package/dist/{metrics-Pue1x4ta.mjs → metrics-DxJzdAgZ.mjs} +1 -1
  35. package/dist/{middleware-By8lqc18.js → middleware-B2rDMvhv.js} +231 -29
  36. package/dist/{middleware-WU_5fbGS.mjs → middleware-CubmqVku.mjs} +49 -32
  37. package/dist/{middleware-BiXEAbn2.cjs → middleware-D2F1jSer.cjs} +231 -29
  38. package/dist/{middleware-CyxqoeW9.mjs → middleware-DxCKBFFT.mjs} +1 -1
  39. package/dist/{mod-C0F6kvgS.d.cts → mod-0bIqGrKQ.d.cts} +1 -1
  40. package/dist/{mod-vPYVoa5n.d.ts → mod-3H9Gym5G.d.ts} +1 -1
  41. package/dist/mod.cjs +5 -5
  42. package/dist/mod.d.cts +2 -2
  43. package/dist/mod.d.ts +2 -2
  44. package/dist/mod.js +5 -5
  45. package/dist/nodeinfo/client.test.mjs +61 -1
  46. package/dist/nodeinfo/handler.test.mjs +1 -1
  47. package/dist/nodeinfo/mod.cjs +1 -1
  48. package/dist/nodeinfo/mod.js +1 -1
  49. package/dist/{owner-pVJ0Twi7.mjs → owner-Btzxz2vj.mjs} +2 -2
  50. package/dist/{proof-HzavUYxN.cjs → proof-BdnNZ9OM.cjs} +1 -1
  51. package/dist/{proof-DA8Kbchf.js → proof-C_kcg7vF.js} +1 -1
  52. package/dist/{proof-pXYODmNV.mjs → proof-D2Z0Wa9X.mjs} +3 -3
  53. package/dist/{send-BfAp60BJ.mjs → send-BljvjEOa.mjs} +3 -3
  54. package/dist/sig/http.test.mjs +2 -2
  55. package/dist/sig/key.test.mjs +1 -1
  56. package/dist/sig/ld.test.mjs +2 -2
  57. package/dist/sig/mod.cjs +2 -2
  58. package/dist/sig/mod.js +2 -2
  59. package/dist/sig/owner.test.mjs +1 -1
  60. package/dist/sig/proof.test.mjs +1 -1
  61. package/dist/{temporal-HZHBbbk4.mjs → temporal-Dg8Hb3lz.mjs} +1 -1
  62. package/dist/{types-CAY3OdLq.js → types-DTONwNqL.js} +63 -5
  63. package/dist/{types-KC4QAoxe.cjs → types-m8u9L86X.cjs} +62 -4
  64. package/dist/utils/docloader.test.mjs +2 -2
  65. package/dist/utils/kv-cache.test.mjs +1 -1
  66. package/dist/utils/mod.cjs +1 -1
  67. package/dist/utils/mod.js +1 -1
  68. package/package.json +6 -6
  69. package/skills/fedify/SKILL.md +462 -0
@@ -2,10 +2,10 @@ import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
3
  import { t as __exportAll } from "./chunk-CRNNMoPX.js";
4
4
  import { r as getDefaultActivityTransformers } from "./transformers-BGMIq1cs.js";
5
- import { A as formatAcceptSignature, D as recordOutboxEnqueue, E as recordOutboxActivity, F as version, O as recordWebFingerHandle, P as name, S as recordCollectionTotalItems, T as recordInboxActivity, a as verifyRequestDetailed, b as recordCollectionPageItems, d as validateCryptoKey, f as getDurationMs, g as isAbortError, h as instrumentDocumentLoader, i as verifyRequest, k as registerQueueDepthGauge, m as getRemoteHost, n as parseRfc9421SignatureInput, o as exportJwk, p as getFederationMetrics, t as doubleKnock, u as importJwk, v as recordCircuitBreakerStateChange, w as recordFanoutRecipients, x as recordCollectionRequest, y as recordCollectionDispatchDuration } from "./http-DPOMa1DD.js";
6
- import { _ as hasSignatureLike, b as signJsonLd, c as getKeyOwner, f as compactJsonLd, g as hasSignature, h as getNormalizationContextLoader, i as verifyObject, l as InvalidContextReferenceError, m as detachSignature, n as hasProofLike, o as normalizeOutgoingActivityJsonLd, r as signObject, s as doesActorOwnKey, u as assertSafeJsonLd, v as isClearlyMalformedContextReference, w as wrapContextLoaderForJsonLd, x as verifyCompactJsonLd, y as isInvalidUrlTypeError } from "./proof-DA8Kbchf.js";
7
- import { n as getNodeInfo, t as nodeInfoToJson } from "./types-CAY3OdLq.js";
8
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-IjRceERu.js";
5
+ import { A as formatAcceptSignature, D as recordOutboxEnqueue, E as recordOutboxActivity, F as version, O as recordWebFingerHandle, P as name, S as recordCollectionTotalItems, T as recordInboxActivity, a as verifyRequestDetailed, b as recordCollectionPageItems, d as validateCryptoKey, f as getDurationMs, g as isAbortError, h as instrumentDocumentLoader, i as verifyRequest, k as registerQueueDepthGauge, m as getRemoteHost, n as parseRfc9421SignatureInput, o as exportJwk, p as getFederationMetrics, t as doubleKnock, u as importJwk, v as recordCircuitBreakerStateChange, w as recordFanoutRecipients, x as recordCollectionRequest, y as recordCollectionDispatchDuration } from "./http-DVZxcZEr.js";
6
+ import { _ as hasSignatureLike, b as signJsonLd, c as getKeyOwner, f as compactJsonLd, g as hasSignature, h as getNormalizationContextLoader, i as verifyObject, l as InvalidContextReferenceError, m as detachSignature, n as hasProofLike, o as normalizeOutgoingActivityJsonLd, r as signObject, s as doesActorOwnKey, u as assertSafeJsonLd, v as isClearlyMalformedContextReference, w as wrapContextLoaderForJsonLd, x as verifyCompactJsonLd, y as isInvalidUrlTypeError } from "./proof-C_kcg7vF.js";
7
+ import { n as getNodeInfo, t as nodeInfoToJson } from "./types-DTONwNqL.js";
8
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-B52kTPj4.js";
9
9
  import { getLogger, withContext } from "@logtape/logtape";
10
10
  import { Router, RouterError, assertPath } from "@fedify/uri-template";
11
11
  import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
@@ -137,6 +137,8 @@ var FederationBuilderImpl = class {
137
137
  f.webFingerLinksDispatcher = this.webFingerLinksDispatcher;
138
138
  f.objectCallbacks = { ...this.objectCallbacks };
139
139
  f.objectTypeIds = { ...this.objectTypeIds };
140
+ f.collectionCallbacks = { ...this.collectionCallbacks };
141
+ f.collectionTypeIds = { ...this.collectionTypeIds };
140
142
  f.inboxPath = this.inboxPath;
141
143
  f.outboxPath = this.outboxPath;
142
144
  f.inboxCallbacks = this.inboxCallbacks == null ? void 0 : { ...this.inboxCallbacks };
@@ -680,6 +682,13 @@ function createFederationBuilder() {
680
682
  //#endregion
681
683
  //#region src/federation/circuit-breaker.ts
682
684
  const MAX_CUSTOM_FAILURE_HISTORY = 100;
685
+ const LEGACY_SWEEP_MARKER = ["__fedify_meta", "circuit_breaker_state_ttl_sweep_v1"];
686
+ const LEGACY_SWEEP_DELETING_MARKER = { __fedifyDeletingCircuitBreakerLegacyState: true };
687
+ const CIRCUIT_BREAKER_STATE_VERSION = 1;
688
+ const LEGACY_SWEEP_LOCK_TTL = Temporal.Duration.from({ minutes: 5 });
689
+ const LEGACY_SWEEP_RETRY_WINDOW = Temporal.Duration.from({ hours: 168 });
690
+ const LEGACY_SWEEP_WAIT_INTERVAL = 10;
691
+ const LEGACY_SWEEP_CAS_ATTEMPTS = 3;
683
692
  /**
684
693
  * Tracks reachability state for remote outbox delivery hosts.
685
694
  * @since 2.3.0
@@ -689,6 +698,8 @@ var CircuitBreaker = class {
689
698
  #prefix;
690
699
  #options;
691
700
  #now;
701
+ #legacySweep;
702
+ #legacySweepDone;
692
703
  #stateChangeObserver;
693
704
  constructor(options) {
694
705
  this.#kv = options.kv;
@@ -705,6 +716,7 @@ var CircuitBreaker = class {
705
716
  return now.until(this.#capHeldRetryAt(now, heldSince, now.add(delay)));
706
717
  }
707
718
  async beforeSend(remoteHost, message) {
719
+ this.#sweepLegacyStates();
708
720
  const heldSince = parseHeldSince(message.circuitHeldSince);
709
721
  const now = this.#now();
710
722
  if (heldSince != null && Temporal.Instant.compare(heldSince.add(this.#options.heldActivityTtl), now) <= 0) return {
@@ -786,6 +798,7 @@ var CircuitBreaker = class {
786
798
  throw new Error(`Failed to update circuit breaker state for ${remoteHost}`);
787
799
  }
788
800
  async recordSuccess(remoteHost) {
801
+ this.#sweepLegacyStates();
789
802
  for (let attempt = 0; attempt < 10; attempt++) {
790
803
  const oldState = await this.#get(remoteHost);
791
804
  if (oldState == null) return void 0;
@@ -806,6 +819,7 @@ var CircuitBreaker = class {
806
819
  return await this.recordSuccess(remoteHost);
807
820
  }
808
821
  async recordFailure(remoteHost) {
822
+ this.#sweepLegacyStates();
809
823
  const now = this.#now();
810
824
  for (let attempt = 0; attempt < 10; attempt++) {
811
825
  const oldState = await this.#get(remoteHost);
@@ -853,26 +867,122 @@ var CircuitBreaker = class {
853
867
  }
854
868
  }
855
869
  async getState(remoteHost) {
856
- return await this.#get(remoteHost);
870
+ this.#sweepLegacyStates();
871
+ return stripStoredCircuitBreakerState(await this.#get(remoteHost));
872
+ }
873
+ /**
874
+ * The currently running background legacy sweep, if any.
875
+ * @internal
876
+ */
877
+ get pendingSweep() {
878
+ return this.#legacySweep;
857
879
  }
858
880
  #key(remoteHost) {
859
881
  return [...this.#prefix, remoteHost];
860
882
  }
883
+ #legacySweepMarkerKey() {
884
+ return [...this.#prefix, ...LEGACY_SWEEP_MARKER];
885
+ }
886
+ #sweepLegacyStates() {
887
+ if (this.#kv.cas == null) return;
888
+ if (this.#options.stateTtl == null) return;
889
+ if (this.#legacySweep != null) return;
890
+ if (this.#isLegacySweepDone()) return;
891
+ this.#legacySweep = this.#sweepLegacyStatesImpl().catch((error) => {
892
+ getLogger([
893
+ "fedify",
894
+ "federation",
895
+ "circuit"
896
+ ]).warn("Failed to sweep legacy circuit breaker state:\n{error}", { error });
897
+ }).finally(() => {
898
+ this.#legacySweep = void 0;
899
+ });
900
+ }
901
+ async #sweepLegacyStatesImpl() {
902
+ const markerKey = this.#legacySweepMarkerKey();
903
+ const marker = await this.#acquireLegacySweep(markerKey);
904
+ if (marker === "done") return;
905
+ try {
906
+ for await (const { key, value } of this.#kv.list(this.#prefix)) {
907
+ if (key.length !== this.#prefix.length + 1) continue;
908
+ await this.#migrateLegacyState(key, value);
909
+ }
910
+ } catch (error) {
911
+ await this.#deleteIfUnchanged(markerKey, marker);
912
+ throw error;
913
+ }
914
+ const finishedMarker = this.#finishLegacySweepMarker(marker);
915
+ if (await this.#kv.cas(markerKey, marker, finishedMarker)) this.#rememberLegacySweepMarker(finishedMarker);
916
+ }
917
+ #finishLegacySweepMarker(marker) {
918
+ const retryUntil = getLegacySweepRetryUntil(marker);
919
+ if (retryUntil != null && Temporal.Instant.compare(this.#now(), Temporal.Instant.from(retryUntil)) < 0) return {
920
+ state: "done",
921
+ retryUntil
922
+ };
923
+ return { state: "final" };
924
+ }
925
+ async #migrateLegacyState(key, value) {
926
+ if (isCurrentCircuitBreakerState(value)) return;
927
+ const state = parseCircuitBreakerKvState(value);
928
+ if (state != null) {
929
+ await this.#kv.cas(key, value, markCircuitBreakerState(state), this.#setOptions());
930
+ return;
931
+ }
932
+ await this.#deleteIfUnchanged(key, value);
933
+ }
934
+ async #deleteIfUnchanged(key, value) {
935
+ if (await this.#kv.cas(key, value, LEGACY_SWEEP_DELETING_MARKER, { ttl: LEGACY_SWEEP_LOCK_TTL })) await this.#kv.delete(key);
936
+ }
937
+ async #acquireLegacySweep(markerKey) {
938
+ for (let attempt = 0; attempt < LEGACY_SWEEP_CAS_ATTEMPTS; attempt++) {
939
+ const marker = await this.#kv.get(markerKey);
940
+ const now = this.#now();
941
+ if (isLegacySweepActive(marker, now)) {
942
+ this.#rememberLegacySweepMarker(marker);
943
+ return "done";
944
+ }
945
+ const retryUntil = isLegacySweepRetryDue(marker, now) ? marker.retryUntil : getLegacySweepRetryUntil(marker) ?? now.add(LEGACY_SWEEP_RETRY_WINDOW).toString();
946
+ const sweeping = {
947
+ state: "sweeping",
948
+ started: now.toString(),
949
+ retryUntil
950
+ };
951
+ if (await this.#kv.cas(markerKey, marker ?? void 0, sweeping, { ttl: LEGACY_SWEEP_LOCK_TTL })) return sweeping;
952
+ await delay(LEGACY_SWEEP_WAIT_INTERVAL);
953
+ }
954
+ return "done";
955
+ }
956
+ #isLegacySweepDone() {
957
+ if (this.#legacySweepDone === "final") return true;
958
+ if (this.#legacySweepDone == null) return false;
959
+ if (Temporal.Instant.compare(this.#now(), this.#legacySweepDone) < 0) return true;
960
+ this.#legacySweepDone = void 0;
961
+ return false;
962
+ }
963
+ #rememberLegacySweepMarker(marker) {
964
+ const doneUntil = getLegacySweepDoneUntil(marker, this.#now());
965
+ if (doneUntil != null) this.#legacySweepDone = doneUntil;
966
+ }
861
967
  #capHeldRetryAt(now, heldSince, retryAt) {
862
968
  const expiresAt = (heldSince ?? now).add(this.#options.heldActivityTtl);
863
969
  return Temporal.Instant.compare(expiresAt, retryAt) < 0 ? expiresAt : retryAt;
864
970
  }
865
971
  async #get(remoteHost) {
866
- return parseCircuitBreakerKvState(await this.#kv.get(this.#key(remoteHost)));
972
+ return parseStoredCircuitBreakerKvState(await this.#kv.get(this.#key(remoteHost)));
867
973
  }
868
974
  async #replace(remoteHost, oldState, newState) {
869
975
  const key = this.#key(remoteHost);
976
+ const storedState = newState == null ? void 0 : markCircuitBreakerState(newState);
870
977
  if (this.#kv.cas == null) {
871
- if (newState == null) await this.#kv.delete(key);
872
- else await this.#kv.set(key, newState);
978
+ if (storedState == null) await this.#kv.delete(key);
979
+ else await this.#kv.set(key, storedState, this.#setOptions());
873
980
  return true;
874
981
  }
875
- return await this.#kv.cas(key, oldState, newState);
982
+ return await this.#kv.cas(key, oldState, storedState, storedState == null ? void 0 : this.#setOptions());
983
+ }
984
+ #setOptions() {
985
+ return this.#options.stateTtl == null ? void 0 : { ttl: this.#options.stateTtl };
876
986
  }
877
987
  async #notifyStateChange(remoteHost, previousState, newState) {
878
988
  try {
@@ -912,18 +1022,25 @@ var CircuitBreaker = class {
912
1022
  * @param options The public circuit breaker options supplied to Fedify.
913
1023
  * @returns The normalized failure predicate, failure pruning function,
914
1024
  * duration values, and optional callbacks with defaults applied.
915
- * @throws {RangeError} If any configured duration is not positive.
1025
+ * @throws {RangeError} If any configured duration is not positive, or if
1026
+ * `stateTtl` is shorter than `recoveryDelay`.
916
1027
  * @throws {TypeError} If `failureThreshold` is not a positive integer.
917
1028
  */
918
1029
  function normalizeCircuitBreakerOptions(options) {
919
1030
  const recoveryDelay = toInstantDuration(options.recoveryDelay ?? { minutes: 30 });
920
1031
  const heldActivityTtl = toInstantDuration(options.heldActivityTtl ?? { hours: 168 });
921
1032
  const releaseInterval = toInstantDuration(options.releaseInterval ?? { seconds: 1 });
1033
+ const configuredStateTtl = options.stateTtl == null ? void 0 : toInstantDuration(options.stateTtl);
922
1034
  assertPositiveDuration(recoveryDelay, "recoveryDelay");
923
1035
  assertPositiveDuration(heldActivityTtl, "heldActivityTtl");
924
1036
  assertPositiveDuration(releaseInterval, "releaseInterval");
1037
+ if (configuredStateTtl != null) {
1038
+ assertPositiveDuration(configuredStateTtl, "stateTtl");
1039
+ if (Temporal.Duration.compare(configuredStateTtl, recoveryDelay) < 0) throw new RangeError("stateTtl must be at least recoveryDelay.");
1040
+ }
925
1041
  let failure;
926
1042
  let pruneFailures;
1043
+ let stateTtl;
927
1044
  if (options.failure == null) {
928
1045
  const failureThreshold = options.failureThreshold ?? 5;
929
1046
  if (!Number.isInteger(failureThreshold) || failureThreshold <= 0) throw new TypeError("failureThreshold must be a positive integer.");
@@ -939,9 +1056,11 @@ function normalizeCircuitBreakerOptions(options) {
939
1056
  const last = timestamps[timestamps.length - 1];
940
1057
  return Temporal.Duration.compare(first.until(last), failureWindow) <= 0;
941
1058
  };
1059
+ stateTtl = configuredStateTtl ?? maxDuration(recoveryDelay.add(failureWindow), recoveryDelay.add(heldActivityTtl));
942
1060
  } else {
943
1061
  failure = options.failure;
944
1062
  pruneFailures = (timestamps) => timestamps.slice(-MAX_CUSTOM_FAILURE_HISTORY);
1063
+ stateTtl = configuredStateTtl;
945
1064
  }
946
1065
  return {
947
1066
  failure,
@@ -949,10 +1068,72 @@ function normalizeCircuitBreakerOptions(options) {
949
1068
  recoveryDelay,
950
1069
  heldActivityTtl,
951
1070
  releaseInterval,
1071
+ stateTtl,
952
1072
  onStateChange: options.onStateChange,
953
1073
  onActivityDrop: options.onActivityDrop
954
1074
  };
955
1075
  }
1076
+ function maxDuration(duration, ...durations) {
1077
+ return durations.reduce((max, candidate) => Temporal.Duration.compare(candidate, max) > 0 ? candidate : max, duration);
1078
+ }
1079
+ function isLegacySweepDone(value, now) {
1080
+ return getLegacySweepDoneUntil(value, now) != null;
1081
+ }
1082
+ function getLegacySweepDoneUntil(value, now) {
1083
+ if (typeof value !== "object" || value == null || !("state" in value)) return;
1084
+ if (value.state === "final") return "final";
1085
+ if (value.state === "done" && "retryUntil" in value && typeof value.retryUntil === "string") try {
1086
+ const retryUntil = Temporal.Instant.from(value.retryUntil);
1087
+ return Temporal.Instant.compare(now, retryUntil) < 0 ? retryUntil : void 0;
1088
+ } catch {
1089
+ return;
1090
+ }
1091
+ }
1092
+ function isLegacySweepRetryDue(value, now) {
1093
+ if (typeof value !== "object" || value == null || !("state" in value) || value.state !== "done" || !("retryUntil" in value) || typeof value.retryUntil !== "string") return false;
1094
+ try {
1095
+ return Temporal.Instant.compare(now, Temporal.Instant.from(value.retryUntil)) >= 0;
1096
+ } catch {
1097
+ return false;
1098
+ }
1099
+ }
1100
+ function isLegacySweepActive(value, now) {
1101
+ return isLegacySweepDone(value, now) || isLegacySweepInProgress(value, now);
1102
+ }
1103
+ function isLegacySweepInProgress(value, now) {
1104
+ if (typeof value !== "object" || value == null || !("state" in value) || value.state !== "sweeping" || !("started" in value) || typeof value.started !== "string") return false;
1105
+ try {
1106
+ return Temporal.Instant.compare(now, Temporal.Instant.from(value.started).add(LEGACY_SWEEP_LOCK_TTL)) < 0;
1107
+ } catch {
1108
+ return false;
1109
+ }
1110
+ }
1111
+ function getLegacySweepRetryUntil(value) {
1112
+ if (typeof value !== "object" || value == null || !("retryUntil" in value) || typeof value.retryUntil !== "string") return;
1113
+ try {
1114
+ Temporal.Instant.from(value.retryUntil);
1115
+ return value.retryUntil;
1116
+ } catch {
1117
+ return;
1118
+ }
1119
+ }
1120
+ function isCurrentCircuitBreakerState(value) {
1121
+ return typeof value === "object" && value != null && "__fedifyCircuitBreakerStateVersion" in value && value.__fedifyCircuitBreakerStateVersion === CIRCUIT_BREAKER_STATE_VERSION;
1122
+ }
1123
+ function markCircuitBreakerState(state) {
1124
+ return {
1125
+ ...state,
1126
+ __fedifyCircuitBreakerStateVersion: CIRCUIT_BREAKER_STATE_VERSION
1127
+ };
1128
+ }
1129
+ function stripStoredCircuitBreakerState(state) {
1130
+ if (state == null) return void 0;
1131
+ const { __fedifyCircuitBreakerStateVersion: _, ...publicState } = state;
1132
+ return publicState;
1133
+ }
1134
+ function delay(milliseconds) {
1135
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
1136
+ }
956
1137
  function toInstantDuration(duration) {
957
1138
  const parsed = Temporal.Duration.from(duration);
958
1139
  return Temporal.Duration.from({ milliseconds: Math.trunc(parsed.total({
@@ -987,6 +1168,9 @@ function parseHeldSince(value) {
987
1168
  * valid instant strings, or `undefined` when the stored value is malformed.
988
1169
  */
989
1170
  function parseCircuitBreakerKvState(value) {
1171
+ return stripStoredCircuitBreakerState(parseStoredCircuitBreakerKvState(value));
1172
+ }
1173
+ function parseStoredCircuitBreakerKvState(value) {
990
1174
  const isInstantString = (v) => {
991
1175
  if (typeof v !== "string") return false;
992
1176
  try {
@@ -1006,7 +1190,8 @@ function parseCircuitBreakerKvState(value) {
1006
1190
  state: record.state,
1007
1191
  failures: record.failures,
1008
1192
  ...record.opened == null ? {} : { opened: record.opened },
1009
- ...record.halfOpened == null ? {} : { halfOpened: record.halfOpened }
1193
+ ...record.halfOpened == null ? {} : { halfOpened: record.halfOpened },
1194
+ ...record.__fedifyCircuitBreakerStateVersion === CIRCUIT_BREAKER_STATE_VERSION ? { __fedifyCircuitBreakerStateVersion: CIRCUIT_BREAKER_STATE_VERSION } : {}
1010
1195
  };
1011
1196
  }
1012
1197
  //#endregion
@@ -3558,6 +3743,24 @@ const logger = getLogger([
3558
3743
  "webfinger",
3559
3744
  "server"
3560
3745
  ]);
3746
+ function getWebFingerSubjectAndAliases({ resourceUrl, actorUri, preferredUsername, acctUsername, host, contextHost }) {
3747
+ const aliases = [];
3748
+ let subject = resourceUrl.href;
3749
+ if (resourceUrl.protocol != "acct:" && preferredUsername != null) {
3750
+ aliases.push(`acct:${preferredUsername}@${host ?? contextHost}`);
3751
+ if (host != null && host !== contextHost) aliases.push(`acct:${preferredUsername}@${contextHost}`);
3752
+ }
3753
+ if (resourceUrl.href !== actorUri.href) aliases.push(actorUri.href);
3754
+ if (resourceUrl.protocol === "acct:" && host != null && host !== contextHost && !resourceUrl.href.endsWith(`@${host}`)) {
3755
+ subject = `acct:${preferredUsername ?? acctUsername}@${host}`;
3756
+ aliases.push(resourceUrl.href);
3757
+ if (preferredUsername != null && preferredUsername !== "" && preferredUsername !== acctUsername) aliases.push(`acct:${preferredUsername}@${contextHost}`);
3758
+ }
3759
+ return {
3760
+ subject,
3761
+ aliases
3762
+ };
3763
+ }
3561
3764
  /**
3562
3765
  * Handles a WebFinger request. You would not typically call this function
3563
3766
  * directly, but instead use {@link Federation.fetch} method.
@@ -3662,6 +3865,7 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
3662
3865
  return identifier;
3663
3866
  }
3664
3867
  let identifier = null;
3868
+ let acctUsername = null;
3665
3869
  const uriParsed = context.parseUri(resourceUrl);
3666
3870
  if (uriParsed?.type != "actor") {
3667
3871
  const match = /^acct:([^@]+)@([^@]+)$/.exec(resource);
@@ -3675,8 +3879,9 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
3675
3879
  const normalizedHost = portMatch == null ? domainToASCII(match[2].toLowerCase()) : domainToASCII(match[2].substring(0, portMatch.index).toLowerCase()) + portMatch[0];
3676
3880
  if (normalizedHost != context.url.host && normalizedHost != host) return await onNotFound(request);
3677
3881
  else {
3678
- identifier = await mapUsernameToIdentifier(match[1]);
3679
- resourceUrl = new URL(`acct:${match[1]}@${normalizedHost}`);
3882
+ acctUsername = match[1];
3883
+ identifier = await mapUsernameToIdentifier(acctUsername);
3884
+ resourceUrl = new URL(`acct:${acctUsername}@${normalizedHost}`);
3680
3885
  }
3681
3886
  }
3682
3887
  } else identifier = uriParsed.identifier;
@@ -3690,9 +3895,10 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
3690
3895
  status: 410,
3691
3896
  headers: { "Access-Control-Allow-Origin": "*" }
3692
3897
  });
3898
+ const actorUri = context.getActorUri(identifier);
3693
3899
  const links = [{
3694
3900
  rel: "self",
3695
- href: context.getActorUri(identifier).href,
3901
+ href: actorUri.href,
3696
3902
  type: "application/activity+json"
3697
3903
  }];
3698
3904
  for (const url of actor.urls) if (url instanceof Link && url.href != null) links.push({
@@ -3716,23 +3922,19 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
3716
3922
  const customLinks = await webFingerLinksDispatcher(context, resourceUrl);
3717
3923
  if (customLinks != null) for (const link of customLinks) links.push(link);
3718
3924
  }
3719
- const aliases = [];
3720
- const preferredUsername = actor.preferredUsername;
3721
- if (resourceUrl.protocol != "acct:" && preferredUsername != null) {
3722
- aliases.push(`acct:${preferredUsername}@${host ?? context.url.host}`);
3723
- if (host != null && host !== context.url.host) aliases.push(`acct:${preferredUsername}@${context.url.host}`);
3724
- }
3725
- if (resourceUrl.href !== context.getActorUri(identifier).href) aliases.push(context.getActorUri(identifier).href);
3726
- if (resourceUrl.protocol === "acct:" && host != null && host !== context.url.host && !resourceUrl.href.endsWith(`@${host}`)) {
3727
- const username = resourceUrl.href.replace(/^acct:/, "").replace(/@.*$/, "");
3728
- aliases.push(`acct:${username}@${host}`);
3729
- }
3730
- const jrd = {
3731
- subject: resourceUrl.href,
3925
+ const { subject, aliases } = getWebFingerSubjectAndAliases({
3926
+ resourceUrl,
3927
+ actorUri,
3928
+ preferredUsername: actor.preferredUsername,
3929
+ acctUsername,
3930
+ host,
3931
+ contextHost: context.url.host
3932
+ });
3933
+ return new Response(JSON.stringify({
3934
+ subject,
3732
3935
  aliases,
3733
3936
  links
3734
- };
3735
- return new Response(JSON.stringify(jrd), { headers: {
3937
+ }), { headers: {
3736
3938
  "Content-Type": "application/jrd+json",
3737
3939
  "Access-Control-Allow-Origin": "*"
3738
3940
  } });
@@ -1,26 +1,26 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-C8RG0HM3.mjs";
5
- import { a as instrumentDocumentLoader, b as recordWebFingerHandle, c as recordCircuitBreakerStateChange, d as recordCollectionRequest, f as recordCollectionTotalItems, g as recordInboxActivity, h as recordFanoutRecipients, i as getRemoteHost, l as recordCollectionDispatchDuration, n as getDurationMs, o as isAbortError, r as getFederationMetrics, u as recordCollectionPageItems, v as recordOutboxActivity, x as registerQueueDepthGauge, y as recordOutboxEnqueue } from "./metrics-Pue1x4ta.mjs";
4
+ import { n as version, t as name } from "./deno-B8lcQ4Ms.mjs";
5
+ import { a as instrumentDocumentLoader, b as recordWebFingerHandle, c as recordCircuitBreakerStateChange, d as recordCollectionRequest, f as recordCollectionTotalItems, g as recordInboxActivity, h as recordFanoutRecipients, i as getRemoteHost, l as recordCollectionDispatchDuration, n as getDurationMs, o as isAbortError, r as getFederationMetrics, u as recordCollectionPageItems, v as recordOutboxActivity, x as registerQueueDepthGauge, y as recordOutboxEnqueue } from "./metrics-DxJzdAgZ.mjs";
6
6
  import { t as formatAcceptSignature } from "./accept-CPkZzmGN.mjs";
7
- import { a as importJwk, o as validateCryptoKey, t as exportJwk } from "./key-BKYwmbur.mjs";
8
- import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-PCt0862M.mjs";
9
- import { t as getAuthenticatedDocumentLoader } from "./docloader-YZA0m_3O.mjs";
10
- import { n as kvCache } from "./kv-cache-Ceg0c1wh.mjs";
11
- import { _ as wrapContextLoaderForJsonLd, a as compactJsonLd, c as getNormalizationContextLoader, d as isClearlyMalformedContextReference, f as isInvalidUrlTypeError, l as hasSignature, m as verifyCompactJsonLd, p as signJsonLd, r as assertSafeJsonLd, s as detachSignature, t as InvalidContextReferenceError, u as hasSignatureLike } from "./ld-Dcw2skfn.mjs";
12
- import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-pVJ0Twi7.mjs";
7
+ import { a as importJwk, o as validateCryptoKey, t as exportJwk } from "./key-BqT9YrM3.mjs";
8
+ import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-DlSQMJsb.mjs";
9
+ import { t as getAuthenticatedDocumentLoader } from "./docloader-BMjxpyJD.mjs";
10
+ import { n as kvCache } from "./kv-cache-BD6BgCTF.mjs";
11
+ import { _ as wrapContextLoaderForJsonLd, a as compactJsonLd, c as getNormalizationContextLoader, d as isClearlyMalformedContextReference, f as isInvalidUrlTypeError, l as hasSignature, m as verifyCompactJsonLd, p as signJsonLd, r as assertSafeJsonLd, s as detachSignature, t as InvalidContextReferenceError, u as hasSignatureLike } from "./ld-hwRDfZqq.mjs";
12
+ import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-Btzxz2vj.mjs";
13
13
  import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-L_DbOaFe.mjs";
14
- import { i as verifyObject, n as hasProofLike, r as signObject } from "./proof-pXYODmNV.mjs";
15
- import { t as getNodeInfo } from "./client-ByXmQhYD.mjs";
14
+ import { i as verifyObject, n as hasProofLike, r as signObject } from "./proof-D2Z0Wa9X.mjs";
15
+ import { t as getNodeInfo } from "./client-D1KuWxjH.mjs";
16
16
  import { t as nodeInfoToJson } from "./types-J53Kw7so.mjs";
17
- import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-BfAp60BJ.mjs";
18
- import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-hivpmhlD.mjs";
19
- import { t as CircuitBreaker } from "./circuit-breaker-CSWsyoef.mjs";
17
+ import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-BljvjEOa.mjs";
18
+ import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-Dx4EI2YS.mjs";
19
+ import { t as CircuitBreaker } from "./circuit-breaker-DLS_n1x5.mjs";
20
20
  import { t as buildCollectionSynchronizationHeader } from "./collection-Cc3DVAhE.mjs";
21
21
  import { t as KvKeyCache } from "./keycache-BeU0LCII.mjs";
22
22
  import { t as acceptsJsonLd } from "./negotiation-DDstyBvc.mjs";
23
- import { t as hasMalformedKnownTemporalLiteral } from "./temporal-HZHBbbk4.mjs";
23
+ import { t as hasMalformedKnownTemporalLiteral } from "./temporal-Dg8Hb3lz.mjs";
24
24
  import { t as createExponentialBackoffPolicy } from "./retry-CXg_MBI-.mjs";
25
25
  import { getLogger, withContext } from "@logtape/logtape";
26
26
  import { RouterError } from "@fedify/uri-template";
@@ -2099,6 +2099,24 @@ const logger = getLogger([
2099
2099
  "webfinger",
2100
2100
  "server"
2101
2101
  ]);
2102
+ function getWebFingerSubjectAndAliases({ resourceUrl, actorUri, preferredUsername, acctUsername, host, contextHost }) {
2103
+ const aliases = [];
2104
+ let subject = resourceUrl.href;
2105
+ if (resourceUrl.protocol != "acct:" && preferredUsername != null) {
2106
+ aliases.push(`acct:${preferredUsername}@${host ?? contextHost}`);
2107
+ if (host != null && host !== contextHost) aliases.push(`acct:${preferredUsername}@${contextHost}`);
2108
+ }
2109
+ if (resourceUrl.href !== actorUri.href) aliases.push(actorUri.href);
2110
+ if (resourceUrl.protocol === "acct:" && host != null && host !== contextHost && !resourceUrl.href.endsWith(`@${host}`)) {
2111
+ subject = `acct:${preferredUsername ?? acctUsername}@${host}`;
2112
+ aliases.push(resourceUrl.href);
2113
+ if (preferredUsername != null && preferredUsername !== "" && preferredUsername !== acctUsername) aliases.push(`acct:${preferredUsername}@${contextHost}`);
2114
+ }
2115
+ return {
2116
+ subject,
2117
+ aliases
2118
+ };
2119
+ }
2102
2120
  /**
2103
2121
  * Handles a WebFinger request. You would not typically call this function
2104
2122
  * directly, but instead use {@link Federation.fetch} method.
@@ -2203,6 +2221,7 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
2203
2221
  return identifier;
2204
2222
  }
2205
2223
  let identifier = null;
2224
+ let acctUsername = null;
2206
2225
  const uriParsed = context.parseUri(resourceUrl);
2207
2226
  if (uriParsed?.type != "actor") {
2208
2227
  const match = /^acct:([^@]+)@([^@]+)$/.exec(resource);
@@ -2216,8 +2235,9 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
2216
2235
  const normalizedHost = portMatch == null ? domainToASCII(match[2].toLowerCase()) : domainToASCII(match[2].substring(0, portMatch.index).toLowerCase()) + portMatch[0];
2217
2236
  if (normalizedHost != context.url.host && normalizedHost != host) return await onNotFound(request);
2218
2237
  else {
2219
- identifier = await mapUsernameToIdentifier(match[1]);
2220
- resourceUrl = new URL(`acct:${match[1]}@${normalizedHost}`);
2238
+ acctUsername = match[1];
2239
+ identifier = await mapUsernameToIdentifier(acctUsername);
2240
+ resourceUrl = new URL(`acct:${acctUsername}@${normalizedHost}`);
2221
2241
  }
2222
2242
  }
2223
2243
  } else identifier = uriParsed.identifier;
@@ -2231,9 +2251,10 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
2231
2251
  status: 410,
2232
2252
  headers: { "Access-Control-Allow-Origin": "*" }
2233
2253
  });
2254
+ const actorUri = context.getActorUri(identifier);
2234
2255
  const links = [{
2235
2256
  rel: "self",
2236
- href: context.getActorUri(identifier).href,
2257
+ href: actorUri.href,
2237
2258
  type: "application/activity+json"
2238
2259
  }];
2239
2260
  for (const url of actor.urls) if (url instanceof Link && url.href != null) links.push({
@@ -2257,23 +2278,19 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
2257
2278
  const customLinks = await webFingerLinksDispatcher(context, resourceUrl);
2258
2279
  if (customLinks != null) for (const link of customLinks) links.push(link);
2259
2280
  }
2260
- const aliases = [];
2261
- const preferredUsername = actor.preferredUsername;
2262
- if (resourceUrl.protocol != "acct:" && preferredUsername != null) {
2263
- aliases.push(`acct:${preferredUsername}@${host ?? context.url.host}`);
2264
- if (host != null && host !== context.url.host) aliases.push(`acct:${preferredUsername}@${context.url.host}`);
2265
- }
2266
- if (resourceUrl.href !== context.getActorUri(identifier).href) aliases.push(context.getActorUri(identifier).href);
2267
- if (resourceUrl.protocol === "acct:" && host != null && host !== context.url.host && !resourceUrl.href.endsWith(`@${host}`)) {
2268
- const username = resourceUrl.href.replace(/^acct:/, "").replace(/@.*$/, "");
2269
- aliases.push(`acct:${username}@${host}`);
2270
- }
2271
- const jrd = {
2272
- subject: resourceUrl.href,
2281
+ const { subject, aliases } = getWebFingerSubjectAndAliases({
2282
+ resourceUrl,
2283
+ actorUri,
2284
+ preferredUsername: actor.preferredUsername,
2285
+ acctUsername,
2286
+ host,
2287
+ contextHost: context.url.host
2288
+ });
2289
+ return new Response(JSON.stringify({
2290
+ subject,
2273
2291
  aliases,
2274
2292
  links
2275
- };
2276
- return new Response(JSON.stringify(jrd), { headers: {
2293
+ }), { headers: {
2277
2294
  "Content-Type": "application/jrd+json",
2278
2295
  "Access-Control-Allow-Origin": "*"
2279
2296
  } });