@fedify/fedify 2.4.0-dev.2071 → 2.4.0-dev.2073

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 (83) hide show
  1. package/dist/{builder-DVQ0yhKJ.mjs → builder-CVHpPAjK.mjs} +2 -2
  2. package/dist/compat/mod.d.cts +1 -1
  3. package/dist/compat/mod.d.ts +1 -1
  4. package/dist/compat/transformers.test.mjs +1 -1
  5. package/dist/{compound-proof-KmXDfWFp.mjs → compound-proof-B5Skp-vE.mjs} +1 -1
  6. package/dist/{context-DsB9kF6q.d.ts → context-BSywYspi.d.ts} +109 -1
  7. package/dist/{context-DijrgHDe.d.cts → context-CZjZYXFC.d.cts} +109 -1
  8. package/dist/{context-DG5QeQ8W.mjs → context-DlZhxdFT.mjs} +2 -1
  9. package/dist/{deno-B-NtYZVW.mjs → deno-LYTbBhYV.mjs} +1 -1
  10. package/dist/{docloader-C6yOVoaF.mjs → docloader-C0SWQylk.mjs} +2 -2
  11. package/dist/federation/builder.test.mjs +1 -1
  12. package/dist/federation/compound-fanout.test.mjs +3 -3
  13. package/dist/federation/handler-compound-proof.test.mjs +4 -4
  14. package/dist/federation/handler.test.mjs +6 -6
  15. package/dist/federation/hashlink-media.test.mjs +2 -2
  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 +8 -8
  19. package/dist/federation/mod.cjs +1 -1
  20. package/dist/federation/mod.d.cts +3 -3
  21. package/dist/federation/mod.d.ts +3 -3
  22. package/dist/federation/mod.js +1 -1
  23. package/dist/federation/outgoing-proof.test.mjs +5 -5
  24. package/dist/federation/portable-gateway-keys.test.mjs +4 -4
  25. package/dist/federation/portable-inbox.test.d.mts +2 -0
  26. package/dist/federation/portable-inbox.test.mjs +546 -0
  27. package/dist/federation/portable.test.mjs +3 -3
  28. package/dist/federation/send.test.mjs +3 -3
  29. package/dist/federation/tasks/enqueue.test.mjs +2 -2
  30. package/dist/federation/tasks/tasks.test.mjs +3 -3
  31. package/dist/federation/temporal.test.mjs +1 -1
  32. package/dist/federation/webfinger.test.mjs +2 -2
  33. package/dist/{federation-CUiHc7dP.cjs → federation-B1aeTKKN.cjs} +468 -88
  34. package/dist/{http-DKWi8RjI.mjs → http-C7qHlv-b.mjs} +4 -4
  35. package/dist/{http-CuLWIJdf.cjs → http-DGk8XI38.cjs} +1 -1
  36. package/dist/{http-BoWBwC0g.js → http-vfTpzGEq.js} +1 -1
  37. package/dist/{key-lfcx_NaE.mjs → key-BYXnTGro.mjs} +2 -2
  38. package/dist/{kv-cache-BdnmnAVc.js → kv-cache-DCJuwdnB.js} +1 -1
  39. package/dist/{kv-cache-d8EjO29d.mjs → kv-cache-DEH5jyND.mjs} +1 -1
  40. package/dist/{kv-cache-DE8kSNQU.cjs → kv-cache-DvqkKNIj.cjs} +1 -1
  41. package/dist/{ld-jyjdrWjG.mjs → ld-DT3e2hOv.mjs} +3 -3
  42. package/dist/{metrics-B5Rei-HC.mjs → metrics-DvlvYpq_.mjs} +1 -1
  43. package/dist/{middleware-CdVnCDXD.mjs → middleware-BJcez_E3.mjs} +1 -1
  44. package/dist/{middleware-CAObohey.js → middleware-CCbyUF4c.js} +417 -38
  45. package/dist/{middleware-CPx4ZWwt.mjs → middleware-CpyCNOW_.mjs} +423 -48
  46. package/dist/{mod-DMkflQdF.d.ts → mod-B2h2KY0q.d.ts} +1 -1
  47. package/dist/{mod-DTAaBwiY.d.cts → mod-JzkaEXsi.d.cts} +1 -1
  48. package/dist/mod.cjs +5 -5
  49. package/dist/mod.d.cts +4 -4
  50. package/dist/mod.d.ts +4 -4
  51. package/dist/mod.js +5 -5
  52. package/dist/nodeinfo/handler.test.mjs +2 -2
  53. package/dist/{owner-ByqNQVaa.mjs → owner-CDsVeZI7.mjs} +3 -3
  54. package/dist/{owner-BXufwAzm.cjs → owner-DCIH7Iq0.cjs} +1 -1
  55. package/dist/{owner-1TFakcTF.js → owner-DfGz0cXm.js} +1 -1
  56. package/dist/{portable-key-Cw_ow5vn.mjs → portable-key-DCyKGCQd.mjs} +2 -2
  57. package/dist/{proof-B6gwdhN5.mjs → proof-CxsVG6vM.mjs} +4 -4
  58. package/dist/{send-DsK7xevO.mjs → send-BZJ7NsVV.mjs} +8 -4
  59. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  60. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  61. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  62. package/dist/sig/compound-proof.test.mjs +2 -2
  63. package/dist/sig/http.test.mjs +2 -2
  64. package/dist/sig/key.test.mjs +1 -1
  65. package/dist/sig/ld.test.mjs +2 -2
  66. package/dist/sig/mod.cjs +3 -3
  67. package/dist/sig/mod.js +3 -3
  68. package/dist/sig/owner.test.mjs +1 -1
  69. package/dist/sig/portable-collection.test.mjs +2 -2
  70. package/dist/sig/portable-key.test.mjs +3 -3
  71. package/dist/sig/proof.test.mjs +3 -3
  72. package/dist/{sig-TTn9wYrE.cjs → sig-C6TnADze.cjs} +2 -2
  73. package/dist/{sig-BR_Z6mpY.js → sig-Cs9_ecdh.js} +2 -2
  74. package/dist/{temporal-Bw5jtrBY.mjs → temporal-CLzSfIOV.mjs} +1 -1
  75. package/dist/testing/kv.test.mjs +1 -1
  76. package/dist/testing/mod.d.mts +108 -0
  77. package/dist/testing/mod.mjs +2 -2
  78. package/dist/{testing-By1YYsIK.mjs → testing-CO_cEvQc.mjs} +1 -1
  79. package/dist/utils/docloader.test.mjs +2 -2
  80. package/dist/utils/kv-cache.test.mjs +1 -1
  81. package/dist/utils/mod.cjs +1 -1
  82. package/dist/utils/mod.js +1 -1
  83. package/package.json +6 -6
@@ -1,10 +1,10 @@
1
1
  const { URLPattern } = require("urlpattern-polyfill");
2
2
  const require_rolldown_runtime = require("./rolldown-runtime-Dmm3t92W.cjs");
3
3
  const require_transformers = require("./transformers-BalCrSsr.cjs");
4
- const require_http = require("./http-CuLWIJdf.cjs");
4
+ const require_http = require("./http-DGk8XI38.cjs");
5
5
  const require_types = require("./types-Be2zbyeG.cjs");
6
- const require_owner = require("./owner-BXufwAzm.cjs");
7
- const require_kv_cache = require("./kv-cache-DE8kSNQU.cjs");
6
+ const require_owner = require("./owner-DCIH7Iq0.cjs");
7
+ const require_kv_cache = require("./kv-cache-DvqkKNIj.cjs");
8
8
  let _logtape_logtape = require("@logtape/logtape");
9
9
  let _fedify_uri_template = require("@fedify/uri-template");
10
10
  let _fedify_vocab = require("@fedify/vocab");
@@ -735,13 +735,13 @@ function createFederationBuilder() {
735
735
  }
736
736
  //#endregion
737
737
  //#region src/federation/circuit-breaker.ts
738
- const Temporal$10 = require_http.Temporal;
738
+ const Temporal$11 = require_http.Temporal;
739
739
  const LEGACY_SWEEP_MARKER = ["__fedify_meta", "circuit_breaker_state_ttl_sweep_v2"];
740
740
  const LEGACY_SWEEP_DELETING_MARKER = { __fedifyDeletingCircuitBreakerLegacyState: true };
741
741
  const CIRCUIT_BREAKER_STATE_VERSION = 1;
742
- const LEGACY_SWEEP_LOCK_TTL = Temporal$10.Duration.from({ minutes: 5 });
743
- const LEGACY_SWEEP_RENEW_INTERVAL = Temporal$10.Duration.from({ minutes: 1 });
744
- const LEGACY_SWEEP_RETRY_WINDOW = Temporal$10.Duration.from({ hours: 168 });
742
+ const LEGACY_SWEEP_LOCK_TTL = Temporal$11.Duration.from({ minutes: 5 });
743
+ const LEGACY_SWEEP_RENEW_INTERVAL = Temporal$11.Duration.from({ minutes: 1 });
744
+ const LEGACY_SWEEP_RETRY_WINDOW = Temporal$11.Duration.from({ hours: 168 });
745
745
  const LEGACY_SWEEP_WAIT_INTERVAL = 10;
746
746
  const LEGACY_SWEEP_CAS_ATTEMPTS = 3;
747
747
  /**
@@ -760,7 +760,7 @@ var CircuitBreaker = class {
760
760
  this.#kv = options.kv;
761
761
  this.#prefix = options.prefix;
762
762
  this.#options = normalizeCircuitBreakerOptions(options.options ?? {});
763
- this.#now = options.now ?? (() => Temporal$10.Now.instant());
763
+ this.#now = options.now ?? (() => Temporal$11.Now.instant());
764
764
  this.#stateChangeObserver = options.stateChangeObserver;
765
765
  }
766
766
  get options() {
@@ -774,7 +774,7 @@ var CircuitBreaker = class {
774
774
  this.#sweepLegacyStates();
775
775
  const heldSince = parseHeldSince(message.circuitHeldSince);
776
776
  const now = this.#now();
777
- if (heldSince != null && Temporal$10.Instant.compare(heldSince.add(this.#options.heldActivityTtl), now) <= 0) return {
777
+ if (heldSince != null && Temporal$11.Instant.compare(heldSince.add(this.#options.heldActivityTtl), now) <= 0) return {
778
778
  type: "drop",
779
779
  heldSince
780
780
  };
@@ -786,12 +786,12 @@ var CircuitBreaker = class {
786
786
  probe: false
787
787
  };
788
788
  if (oldState.state === "half-open") {
789
- const halfOpened = oldState.halfOpened == null ? void 0 : Temporal$10.Instant.from(oldState.halfOpened);
789
+ const halfOpened = oldState.halfOpened == null ? void 0 : Temporal$11.Instant.from(oldState.halfOpened);
790
790
  if (halfOpened != null) {
791
791
  const staleAt = halfOpened.add(this.#options.recoveryDelay);
792
- if (Temporal$10.Instant.compare(now, staleAt) < 0) {
792
+ if (Temporal$11.Instant.compare(now, staleAt) < 0) {
793
793
  const releaseAt = now.add(this.#options.releaseInterval);
794
- const retryAt = Temporal$10.Instant.compare(releaseAt, staleAt) < 0 ? releaseAt : staleAt;
794
+ const retryAt = Temporal$11.Instant.compare(releaseAt, staleAt) < 0 ? releaseAt : staleAt;
795
795
  const cappedRetryAt = this.#capHeldRetryAt(now, heldSince, retryAt);
796
796
  return {
797
797
  type: "hold",
@@ -813,8 +813,8 @@ var CircuitBreaker = class {
813
813
  lastConflictingState = "half-open";
814
814
  continue;
815
815
  }
816
- const probeAt = (oldState.opened == null ? now : Temporal$10.Instant.from(oldState.opened)).add(this.#options.recoveryDelay);
817
- if (Temporal$10.Instant.compare(now, probeAt) < 0) {
816
+ const probeAt = (oldState.opened == null ? now : Temporal$11.Instant.from(oldState.opened)).add(this.#options.recoveryDelay);
817
+ if (Temporal$11.Instant.compare(now, probeAt) < 0) {
818
818
  const retryAt = this.#capHeldRetryAt(now, heldSince, probeAt);
819
819
  return {
820
820
  type: "hold",
@@ -879,7 +879,7 @@ var CircuitBreaker = class {
879
879
  for (let attempt = 0; attempt < 10; attempt++) {
880
880
  const oldState = await this.#get(remoteHost);
881
881
  if (oldState?.state === "open") return void 0;
882
- const oldFailures = oldState?.failures.map(Temporal$10.Instant.from) ?? [];
882
+ const oldFailures = oldState?.failures.map(Temporal$11.Instant.from) ?? [];
883
883
  const failures = this.#options.pruneFailures([...oldFailures, now], now);
884
884
  let newState;
885
885
  let transition;
@@ -963,7 +963,7 @@ var CircuitBreaker = class {
963
963
  for await (const { key, value } of this.#kv.list(this.#prefix)) {
964
964
  if (key.length !== this.#prefix.length + 1) continue;
965
965
  await this.#migrateLegacyState(key, value);
966
- if (Temporal$10.Instant.compare(this.#now(), leaseRenewedAt.add(LEGACY_SWEEP_RENEW_INTERVAL)) >= 0) {
966
+ if (Temporal$11.Instant.compare(this.#now(), leaseRenewedAt.add(LEGACY_SWEEP_RENEW_INTERVAL)) >= 0) {
967
967
  const renewed = await this.#renewLegacySweep(markerKey, marker);
968
968
  if (renewed == null) return;
969
969
  marker = renewed;
@@ -987,7 +987,7 @@ var CircuitBreaker = class {
987
987
  }
988
988
  #finishLegacySweepMarker(marker) {
989
989
  const retryUntil = getLegacySweepRetryUntil(marker);
990
- if (retryUntil != null && Temporal$10.Instant.compare(this.#now(), Temporal$10.Instant.from(retryUntil)) < 0) return {
990
+ if (retryUntil != null && Temporal$11.Instant.compare(this.#now(), Temporal$11.Instant.from(retryUntil)) < 0) return {
991
991
  state: "done",
992
992
  retryUntil
993
993
  };
@@ -1030,7 +1030,7 @@ var CircuitBreaker = class {
1030
1030
  #isLegacySweepDone() {
1031
1031
  if (this.#legacySweepDone === "final") return true;
1032
1032
  if (this.#legacySweepDone == null) return false;
1033
- if (Temporal$10.Instant.compare(this.#now(), this.#legacySweepDone) < 0) return true;
1033
+ if (Temporal$11.Instant.compare(this.#now(), this.#legacySweepDone) < 0) return true;
1034
1034
  this.#legacySweepDone = void 0;
1035
1035
  return false;
1036
1036
  }
@@ -1040,7 +1040,7 @@ var CircuitBreaker = class {
1040
1040
  }
1041
1041
  #capHeldRetryAt(now, heldSince, retryAt) {
1042
1042
  const expiresAt = (heldSince ?? now).add(this.#options.heldActivityTtl);
1043
- return Temporal$10.Instant.compare(expiresAt, retryAt) < 0 ? expiresAt : retryAt;
1043
+ return Temporal$11.Instant.compare(expiresAt, retryAt) < 0 ? expiresAt : retryAt;
1044
1044
  }
1045
1045
  async #get(remoteHost) {
1046
1046
  return parseStoredCircuitBreakerKvState(await this.#kv.get(this.#key(remoteHost)));
@@ -1110,7 +1110,7 @@ function normalizeCircuitBreakerOptions(options) {
1110
1110
  assertPositiveDuration(releaseInterval, "releaseInterval");
1111
1111
  if (configuredStateTtl != null) {
1112
1112
  assertPositiveDuration(configuredStateTtl, "stateTtl");
1113
- if (Temporal$10.Duration.compare(configuredStateTtl, recoveryDelay) < 0) throw new RangeError("stateTtl must be at least recoveryDelay.");
1113
+ if (Temporal$11.Duration.compare(configuredStateTtl, recoveryDelay) < 0) throw new RangeError("stateTtl must be at least recoveryDelay.");
1114
1114
  }
1115
1115
  let failure;
1116
1116
  let pruneFailures;
@@ -1122,13 +1122,13 @@ function normalizeCircuitBreakerOptions(options) {
1122
1122
  assertPositiveDuration(failureWindow, "failureWindow");
1123
1123
  pruneFailures = (timestamps, now) => {
1124
1124
  const earliest = now.subtract(failureWindow);
1125
- return timestamps.filter((timestamp) => Temporal$10.Instant.compare(timestamp, earliest) >= 0).slice(-failureThreshold);
1125
+ return timestamps.filter((timestamp) => Temporal$11.Instant.compare(timestamp, earliest) >= 0).slice(-failureThreshold);
1126
1126
  };
1127
1127
  failure = (timestamps) => {
1128
1128
  if (timestamps.length < failureThreshold) return false;
1129
1129
  const first = timestamps[timestamps.length - failureThreshold];
1130
1130
  const last = timestamps[timestamps.length - 1];
1131
- return Temporal$10.Duration.compare(first.until(last), failureWindow) <= 0;
1131
+ return Temporal$11.Duration.compare(first.until(last), failureWindow) <= 0;
1132
1132
  };
1133
1133
  stateTtl = configuredStateTtl ?? maxDuration(recoveryDelay.add(failureWindow), recoveryDelay.add(heldActivityTtl));
1134
1134
  } else {
@@ -1148,7 +1148,7 @@ function normalizeCircuitBreakerOptions(options) {
1148
1148
  };
1149
1149
  }
1150
1150
  function maxDuration(duration, ...durations) {
1151
- return durations.reduce((max, candidate) => Temporal$10.Duration.compare(candidate, max) > 0 ? candidate : max, duration);
1151
+ return durations.reduce((max, candidate) => Temporal$11.Duration.compare(candidate, max) > 0 ? candidate : max, duration);
1152
1152
  }
1153
1153
  function isLegacySweepDone(value, now) {
1154
1154
  return getLegacySweepDoneUntil(value, now) != null;
@@ -1157,8 +1157,8 @@ function getLegacySweepDoneUntil(value, now) {
1157
1157
  if (typeof value !== "object" || value == null || !("state" in value)) return;
1158
1158
  if (value.state === "final") return "final";
1159
1159
  if (value.state === "done" && "retryUntil" in value && typeof value.retryUntil === "string") try {
1160
- const retryUntil = Temporal$10.Instant.from(value.retryUntil);
1161
- return Temporal$10.Instant.compare(now, retryUntil) < 0 ? retryUntil : void 0;
1160
+ const retryUntil = Temporal$11.Instant.from(value.retryUntil);
1161
+ return Temporal$11.Instant.compare(now, retryUntil) < 0 ? retryUntil : void 0;
1162
1162
  } catch {
1163
1163
  return;
1164
1164
  }
@@ -1166,7 +1166,7 @@ function getLegacySweepDoneUntil(value, now) {
1166
1166
  function isLegacySweepRetryDue(value, now) {
1167
1167
  if (typeof value !== "object" || value == null || !("state" in value) || value.state !== "done" || !("retryUntil" in value) || typeof value.retryUntil !== "string") return false;
1168
1168
  try {
1169
- return Temporal$10.Instant.compare(now, Temporal$10.Instant.from(value.retryUntil)) >= 0;
1169
+ return Temporal$11.Instant.compare(now, Temporal$11.Instant.from(value.retryUntil)) >= 0;
1170
1170
  } catch {
1171
1171
  return false;
1172
1172
  }
@@ -1177,7 +1177,7 @@ function isLegacySweepActive(value, now) {
1177
1177
  function isLegacySweepInProgress(value, now) {
1178
1178
  if (typeof value !== "object" || value == null || !("state" in value) || value.state !== "sweeping" || !("started" in value) || typeof value.started !== "string") return false;
1179
1179
  try {
1180
- return Temporal$10.Instant.compare(now, Temporal$10.Instant.from(value.started).add(LEGACY_SWEEP_LOCK_TTL)) < 0;
1180
+ return Temporal$11.Instant.compare(now, Temporal$11.Instant.from(value.started).add(LEGACY_SWEEP_LOCK_TTL)) < 0;
1181
1181
  } catch {
1182
1182
  return false;
1183
1183
  }
@@ -1185,7 +1185,7 @@ function isLegacySweepInProgress(value, now) {
1185
1185
  function getLegacySweepRetryUntil(value) {
1186
1186
  if (typeof value !== "object" || value == null || !("retryUntil" in value) || typeof value.retryUntil !== "string") return;
1187
1187
  try {
1188
- Temporal$10.Instant.from(value.retryUntil);
1188
+ Temporal$11.Instant.from(value.retryUntil);
1189
1189
  return value.retryUntil;
1190
1190
  } catch {
1191
1191
  return;
@@ -1209,19 +1209,19 @@ function delay(milliseconds) {
1209
1209
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
1210
1210
  }
1211
1211
  function toInstantDuration(duration) {
1212
- const parsed = Temporal$10.Duration.from(duration);
1213
- return Temporal$10.Duration.from({ milliseconds: Math.trunc(parsed.total({
1212
+ const parsed = Temporal$11.Duration.from(duration);
1213
+ return Temporal$11.Duration.from({ milliseconds: Math.trunc(parsed.total({
1214
1214
  unit: "millisecond",
1215
- relativeTo: Temporal$10.PlainDateTime.from("2026-01-01T00:00:00")
1215
+ relativeTo: Temporal$11.PlainDateTime.from("2026-01-01T00:00:00")
1216
1216
  })) });
1217
1217
  }
1218
1218
  function assertPositiveDuration(duration, name) {
1219
- if (Temporal$10.Duration.compare(duration, { seconds: 0 }) <= 0) throw new RangeError(`${name} must be a positive duration.`);
1219
+ if (Temporal$11.Duration.compare(duration, { seconds: 0 }) <= 0) throw new RangeError(`${name} must be a positive duration.`);
1220
1220
  }
1221
1221
  function parseHeldSince(value) {
1222
1222
  if (value == null) return void 0;
1223
1223
  try {
1224
- return Temporal$10.Instant.from(value);
1224
+ return Temporal$11.Instant.from(value);
1225
1225
  } catch (error) {
1226
1226
  (0, _logtape_logtape.getLogger)([
1227
1227
  "fedify",
@@ -1248,7 +1248,7 @@ function parseStoredCircuitBreakerKvState(value) {
1248
1248
  const isInstantString = (v) => {
1249
1249
  if (typeof v !== "string") return false;
1250
1250
  try {
1251
- Temporal$10.Instant.from(v);
1251
+ Temporal$11.Instant.from(v);
1252
1252
  return true;
1253
1253
  } catch {
1254
1254
  return false;
@@ -1849,7 +1849,7 @@ async function verifyCompoundPortableObjectProofs(json, limits, options = {}) {
1849
1849
  }
1850
1850
  //#endregion
1851
1851
  //#region src/federation/inbox.ts
1852
- const Temporal$9 = require_http.Temporal;
1852
+ const Temporal$10 = require_http.Temporal;
1853
1853
  async function routeActivity({ context: ctx, json, originalJson, normalizedActivity, ldSignatureVerified, activity, recipient, inboxListeners, inboxContextFactory, listenerInboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, meterProvider, tracerProvider, idempotencyStrategy }) {
1854
1854
  const logger = (0, _logtape_logtape.getLogger)([
1855
1855
  "fedify",
@@ -1996,7 +1996,7 @@ async function routeActivity({ context: ctx, json, originalJson, normalizedActiv
1996
1996
  return "error";
1997
1997
  }
1998
1998
  require_http.recordInboxActivity(meterProvider, "processed", (0, _fedify_vocab.getTypeId)(activity).href);
1999
- if (cacheKey != null) await kv.set(cacheKey, true, { ttl: Temporal$9.Duration.from({ days: 1 }) });
1999
+ if (cacheKey != null) await kv.set(cacheKey, true, { ttl: Temporal$10.Duration.from({ days: 1 }) });
2000
2000
  logger.info("Activity {activityId} has been processed.", {
2001
2001
  activityId: activity.id?.href,
2002
2002
  activity: json,
@@ -2008,7 +2008,7 @@ async function routeActivity({ context: ctx, json, originalJson, normalizedActiv
2008
2008
  }
2009
2009
  //#endregion
2010
2010
  //#region src/federation/keycache.ts
2011
- const Temporal$8 = require_http.Temporal;
2011
+ const Temporal$9 = require_http.Temporal;
2012
2012
  const KEY_CACHE_GENERATION = "2";
2013
2013
  var KvKeyCache = class {
2014
2014
  kv;
@@ -2021,8 +2021,8 @@ var KvKeyCache = class {
2021
2021
  this.kv = kv;
2022
2022
  this.prefix = prefix;
2023
2023
  this.options = options;
2024
- this.unavailableKeyTtl = options.unavailableKeyTtl ?? Temporal$8.Duration.from({ minutes: 10 });
2025
- this.keyTtl = options.keyTtl ?? Temporal$8.Duration.from({ days: 30 });
2024
+ this.unavailableKeyTtl = options.unavailableKeyTtl ?? Temporal$9.Duration.from({ minutes: 10 });
2025
+ this.keyTtl = options.keyTtl ?? Temporal$9.Duration.from({ days: 30 });
2026
2026
  this.nullKeys = /* @__PURE__ */ new Map();
2027
2027
  }
2028
2028
  #getFetchErrorKey(keyId) {
@@ -2042,13 +2042,13 @@ var KvKeyCache = class {
2042
2042
  async get(keyId) {
2043
2043
  const negativeExpiration = this.nullKeys.get(keyId.href);
2044
2044
  if (negativeExpiration != null) {
2045
- if (Temporal$8.Now.instant().until(negativeExpiration).sign >= 0) return null;
2045
+ if (Temporal$9.Now.instant().until(negativeExpiration).sign >= 0) return null;
2046
2046
  this.nullKeys.delete(keyId.href);
2047
2047
  }
2048
2048
  const serialized = await this.kv.get(this.#entryKey(keyId));
2049
2049
  if (serialized === void 0) return void 0;
2050
2050
  if (serialized === null) {
2051
- this.nullKeys.set(keyId.href, Temporal$8.Now.instant().add(this.unavailableKeyTtl));
2051
+ this.nullKeys.set(keyId.href, Temporal$9.Now.instant().add(this.unavailableKeyTtl));
2052
2052
  return null;
2053
2053
  }
2054
2054
  try {
@@ -2064,7 +2064,7 @@ var KvKeyCache = class {
2064
2064
  }
2065
2065
  async set(keyId, key) {
2066
2066
  if (key == null) {
2067
- this.nullKeys.set(keyId.href, Temporal$8.Now.instant().add(this.unavailableKeyTtl));
2067
+ this.nullKeys.set(keyId.href, Temporal$9.Now.instant().add(this.unavailableKeyTtl));
2068
2068
  await this.kv.set(this.#entryKey(keyId), null, { ttl: this.unavailableKeyTtl });
2069
2069
  return;
2070
2070
  }
@@ -2320,7 +2320,7 @@ function parseHashlinkGatewayRequest(url) {
2320
2320
  }
2321
2321
  //#endregion
2322
2322
  //#region src/federation/temporal.ts
2323
- const Temporal$7 = require_http.Temporal;
2323
+ const Temporal$8 = require_http.Temporal;
2324
2324
  function isPlainObject$1(value) {
2325
2325
  return typeof value === "object" && value != null && !Array.isArray(value);
2326
2326
  }
@@ -2330,7 +2330,7 @@ function normalizeDateTimeLiteral(value) {
2330
2330
  function isMalformedDateTimeLiteral(value) {
2331
2331
  if (typeof value !== "string") return false;
2332
2332
  try {
2333
- Temporal$7.Instant.from(normalizeDateTimeLiteral(value));
2333
+ Temporal$8.Instant.from(normalizeDateTimeLiteral(value));
2334
2334
  return false;
2335
2335
  } catch {
2336
2336
  return true;
@@ -2339,7 +2339,7 @@ function isMalformedDateTimeLiteral(value) {
2339
2339
  function isMalformedDurationLiteral(value) {
2340
2340
  if (typeof value !== "string") return false;
2341
2341
  try {
2342
- Temporal$7.Duration.from(value);
2342
+ Temporal$8.Duration.from(value);
2343
2343
  return false;
2344
2344
  } catch {
2345
2345
  return true;
@@ -2410,7 +2410,7 @@ async function hasMalformedKnownTemporalLiteral(value, contextLoader) {
2410
2410
  }
2411
2411
  //#endregion
2412
2412
  //#region src/federation/handler.ts
2413
- const Temporal$6 = require_http.Temporal;
2413
+ const Temporal$7 = require_http.Temporal;
2414
2414
  const rawInboxContextFactorySymbol = Symbol("fedify.rawInboxContextFactory");
2415
2415
  const INBOX_COMPOUND_PROOF_LIMITS = {
2416
2416
  maxDepth: 64,
@@ -2638,13 +2638,13 @@ function recordCollectionMetrics(meterProvider, base, result, options = {}) {
2638
2638
  * @param parameters The parameters for handling the collection.
2639
2639
  * @returns A promise that resolves to an HTTP response.
2640
2640
  */
2641
- async function handleCollection(request, { name: name$1, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
2642
- const spanName = name$1.trim().replace(/\s+/g, "_");
2641
+ async function handleCollection(request, { name: name$2, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
2642
+ const spanName = name$2.trim().replace(/\s+/g, "_");
2643
2643
  tracerProvider = tracerProvider ?? _opentelemetry_api.trace.getTracerProvider();
2644
2644
  const tracer = tracerProvider.getTracer(require_http.name, require_http.version);
2645
2645
  const cursor = new URL(request.url).searchParams.get("cursor");
2646
2646
  const metricBase = {
2647
- kind: getCollectionMetricKind(name$1),
2647
+ kind: getCollectionMetricKind(name$2),
2648
2648
  page: cursor != null,
2649
2649
  dispatcher: "built_in"
2650
2650
  };
@@ -2685,7 +2685,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
2685
2685
  span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR });
2686
2686
  return await onNotFound(request);
2687
2687
  }
2688
- const items = filterCollectionItems(page.items, name$1, filterPredicate);
2688
+ const items = filterCollectionItems(page.items, name$2, filterPredicate);
2689
2689
  itemCount = items.length;
2690
2690
  span.setAttribute("fedify.collection.items", itemCount);
2691
2691
  return items;
@@ -2725,7 +2725,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
2725
2725
  } else {
2726
2726
  const uri = new URL(baseUri);
2727
2727
  uri.searchParams.set("cursor", cursor);
2728
- const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$1}`, {
2728
+ const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$2}`, {
2729
2729
  kind: _opentelemetry_api.SpanKind.SERVER,
2730
2730
  attributes: {
2731
2731
  "activitypub.collection.id": uri.href,
@@ -2741,7 +2741,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
2741
2741
  span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR });
2742
2742
  return await onNotFound(request);
2743
2743
  }
2744
- const items = filterCollectionItems(page.items, name$1, filterPredicate);
2744
+ const items = filterCollectionItems(page.items, name$2, filterPredicate);
2745
2745
  itemCount = items.length;
2746
2746
  span.setAttribute("fedify.collection.items", itemCount);
2747
2747
  return {
@@ -3247,7 +3247,7 @@ async function handleInbox(request, options) {
3247
3247
  * @returns A promise that resolves to an HTTP response.
3248
3248
  */
3249
3249
  async function handleInboxInternal(request, parameters, span) {
3250
- const { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, publicKeyTtl, queue, actorDispatcher, inboxListeners, inboxErrorHandler, unverifiedActivityHandler, onNotFound, signatureTimeWindow, skipSignatureVerification, inboxChallengePolicy, meterProvider, tracerProvider } = parameters;
3250
+ const { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, publicKeyTtl, queue, actorDispatcher, inboxListeners, inboxErrorHandler, unverifiedActivityHandler, onNotFound, signatureTimeWindow, skipSignatureVerification, inboxChallengePolicy, meterProvider, tracerProvider, portableInbox } = parameters;
3251
3251
  const logger = (0, _logtape_logtape.getLogger)([
3252
3252
  "fedify",
3253
3253
  "federation",
@@ -3260,7 +3260,7 @@ async function handleInboxInternal(request, parameters, span) {
3260
3260
  message: "Actor dispatcher is not set."
3261
3261
  });
3262
3262
  return await onNotFound(request);
3263
- } else if (recipient != null) {
3263
+ } else if (recipient != null && portableInbox == null) {
3264
3264
  const actor = await actorDispatcher(ctx, recipient);
3265
3265
  if (actor == null || actor instanceof _fedify_vocab.Tombstone) {
3266
3266
  logger.error("Actor {recipient} not found.", { recipient });
@@ -3722,6 +3722,19 @@ async function handleInboxInternal(request, parameters, span) {
3722
3722
  tracerProvider,
3723
3723
  idempotencyStrategy: parameters.idempotencyStrategy
3724
3724
  });
3725
+ if (portableInbox != null && activity.id != null && PORTABLE_INBOX_FORWARDABLE_RESULTS.has(routeResult)) if (skipSignatureVerification || !(ldSigVerified || proofVerified)) logger.debug("Not forwarding activity {activityId} to the other gateways of the portable inbox {inbox}, as it is not authenticated by its own proof.", {
3726
+ activityId: activity.id.href,
3727
+ inbox: portableInbox.recipient.canonicalInboxId
3728
+ });
3729
+ else try {
3730
+ await portableInbox.forward(json, activity.id, (0, _fedify_vocab.getTypeId)(activity).href);
3731
+ } catch (error) {
3732
+ logger.error("Failed to forward activity {activityId} to the other gateways of the portable inbox {inbox}:\n{error}", {
3733
+ activityId: activity.id.href,
3734
+ inbox: portableInbox.recipient.canonicalInboxId,
3735
+ error
3736
+ });
3737
+ }
3725
3738
  if (routeResult === "alreadyProcessed") return new Response(`Activity <${activity.id}> has already been processed.`, {
3726
3739
  status: 202,
3727
3740
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -3747,6 +3760,12 @@ async function handleInboxInternal(request, parameters, span) {
3747
3760
  headers: { "Content-Type": "text/plain; charset=utf-8" }
3748
3761
  });
3749
3762
  }
3763
+ const PORTABLE_INBOX_FORWARDABLE_RESULTS = /* @__PURE__ */ new Set([
3764
+ "success",
3765
+ "enqueued",
3766
+ "alreadyProcessed",
3767
+ "unsupportedActivity"
3768
+ ]);
3750
3769
  /**
3751
3770
  * Handles a custom collection request.
3752
3771
  * @template TItem The type of items in the collection.
@@ -3869,8 +3888,8 @@ var CustomCollectionHandler = class {
3869
3888
  * @param CollectionPage The CollectionPage constructor.
3870
3889
  * @param filterPredicate Optional filter predicate for items.
3871
3890
  */
3872
- constructor(name$2, values, context, callbacks, tracerProvider = _opentelemetry_api.trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate) {
3873
- this.name = name$2;
3891
+ constructor(name$1, values, context, callbacks, tracerProvider = _opentelemetry_api.trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate) {
3892
+ this.name = name$1;
3874
3893
  this.values = values;
3875
3894
  this.context = context;
3876
3895
  this.callbacks = callbacks;
@@ -4340,7 +4359,7 @@ async function buildAcceptSignatureHeader(policy, kv, noncePrefix) {
4340
4359
  }
4341
4360
  async function setKey(kv, key, policy) {
4342
4361
  const seconds = policy.nonceTtlSeconds ?? 300;
4343
- const ttl = Temporal$6.Duration.from({ seconds });
4362
+ const ttl = Temporal$7.Duration.from({ seconds });
4344
4363
  await kv.set(key, true, { ttl });
4345
4364
  }
4346
4365
  const DEF_COMPONENTS = [
@@ -4365,7 +4384,7 @@ function isPortableGatewayKey(key) {
4365
4384
  }
4366
4385
  //#endregion
4367
4386
  //#region src/federation/kv.ts
4368
- const Temporal$5 = require_http.Temporal;
4387
+ const Temporal$6 = require_http.Temporal;
4369
4388
  /**
4370
4389
  * A key–value store that stores values in memory.
4371
4390
  * Do not use this in production as it does not persist values.
@@ -4385,7 +4404,7 @@ var MemoryKvStore = class {
4385
4404
  const entry = this.#values[encodedKey];
4386
4405
  if (entry == null) return Promise.resolve(void 0);
4387
4406
  const [value, expiration] = entry;
4388
- if (expiration != null && Temporal$5.Now.instant().until(expiration).sign < 0) {
4407
+ if (expiration != null && Temporal$6.Now.instant().until(expiration).sign < 0) {
4389
4408
  delete this.#values[encodedKey];
4390
4409
  return Promise.resolve(void 0);
4391
4410
  }
@@ -4396,7 +4415,7 @@ var MemoryKvStore = class {
4396
4415
  */
4397
4416
  set(key, value, options) {
4398
4417
  const encodedKey = this.#encodeKey(key);
4399
- const expiration = options?.ttl == null ? null : Temporal$5.Now.instant().add(options.ttl.round({ largestUnit: "hour" }));
4418
+ const expiration = options?.ttl == null ? null : Temporal$6.Now.instant().add(options.ttl.round({ largestUnit: "hour" }));
4400
4419
  this.#values[encodedKey] = [value, expiration];
4401
4420
  return Promise.resolve();
4402
4421
  }
@@ -4418,13 +4437,13 @@ var MemoryKvStore = class {
4418
4437
  if (entry == null) currentValue = void 0;
4419
4438
  else {
4420
4439
  const [value, expiration] = entry;
4421
- if (expiration != null && Temporal$5.Now.instant().until(expiration).sign < 0) {
4440
+ if (expiration != null && Temporal$6.Now.instant().until(expiration).sign < 0) {
4422
4441
  delete this.#values[encodedKey];
4423
4442
  currentValue = void 0;
4424
4443
  } else currentValue = value;
4425
4444
  }
4426
4445
  if (!(0, es_toolkit.isEqual)(currentValue, expectedValue)) return Promise.resolve(false);
4427
- const expiration = options?.ttl == null ? null : Temporal$5.Now.instant().add(options.ttl.round({ largestUnit: "hour" }));
4446
+ const expiration = options?.ttl == null ? null : Temporal$6.Now.instant().add(options.ttl.round({ largestUnit: "hour" }));
4428
4447
  this.#values[encodedKey] = [newValue, expiration];
4429
4448
  return Promise.resolve(true);
4430
4449
  }
@@ -4432,7 +4451,7 @@ var MemoryKvStore = class {
4432
4451
  * {@inheritDoc KvStore.list}
4433
4452
  */
4434
4453
  async *list(prefix) {
4435
- const now = Temporal$5.Now.instant();
4454
+ const now = Temporal$6.Now.instant();
4436
4455
  for (const [encodedKey, entry] of Object.entries(this.#values)) {
4437
4456
  const key = JSON.parse(encodedKey);
4438
4457
  if (prefix != null) {
@@ -4626,7 +4645,11 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
4626
4645
  rsaKey = key;
4627
4646
  break;
4628
4647
  }
4629
- if (rsaKey == null) logger.warn("No supported key found to sign the request to {inbox}. The request will be sent without a signature. In order to sign the request, at least one RSASSA-PKCS1-v1_5 key must be provided.", {
4648
+ if (rsaKey == null && keys.length < 1) logger.debug("Sending the activity {activityId} to {inbox} without a signature.", {
4649
+ activityId,
4650
+ inbox: inbox.href
4651
+ });
4652
+ else if (rsaKey == null) logger.warn("No supported key found to sign the request to {inbox}. The request will be sent without a signature. In order to sign the request, at least one RSASSA-PKCS1-v1_5 key must be provided.", {
4630
4653
  inbox: inbox.href,
4631
4654
  keys: keys.map((pair) => ({
4632
4655
  keyId: pair.keyId.href,
@@ -4968,6 +4991,252 @@ function serializeDataPointType(dataPointType) {
4968
4991
  }
4969
4992
  }
4970
4993
  //#endregion
4994
+ //#region src/federation/portable-inbox.ts
4995
+ const Temporal$5 = require_http.Temporal;
4996
+ const MAX_TIMEOUT_MS = 2 ** 31 - 1;
4997
+ /**
4998
+ * Fills in the defaults of {@link PortableInboxForwardingOptions} and
4999
+ * validates them.
5000
+ * @param options The options.
5001
+ * @returns The resolved options.
5002
+ * @throws {RangeError} If `maxTargets` is not a non-negative integer, if `ttl`
5003
+ * is not positive, if `deadline` is negative or longer
5004
+ * than 2,147,483,647 milliseconds (about 24.8 days), or
5005
+ * if either duration has calendar units, i.e., weeks,
5006
+ * months, or years, whose length depends on the date.
5007
+ */
5008
+ function resolvePortableInboxForwardingOptions(options = {}) {
5009
+ const maxTargets = options.maxTargets ?? 10;
5010
+ if (!Number.isInteger(maxTargets) || maxTargets < 0) throw new RangeError("portableInboxForwarding.maxTargets must be a non-negative integer.");
5011
+ const ttl = Temporal$5.Duration.from(options.ttl ?? { days: 30 });
5012
+ if (ttl.total("millisecond") <= 0) throw new RangeError("portableInboxForwarding.ttl must be positive.");
5013
+ const deadline = Temporal$5.Duration.from(options.deadline ?? { seconds: 10 });
5014
+ const deadlineMs = deadline.total("millisecond");
5015
+ if (deadlineMs < 0 || deadlineMs > MAX_TIMEOUT_MS) throw new RangeError(`portableInboxForwarding.deadline must be between 0 and ${MAX_TIMEOUT_MS} milliseconds.`);
5016
+ return {
5017
+ maxTargets,
5018
+ ttl,
5019
+ deadline
5020
+ };
5021
+ }
5022
+ /**
5023
+ * Decides whether this server accepts a delivery to a portable inbox on behalf
5024
+ * of the given actor, which the actor dispatcher returned for the identifier
5025
+ * in the requested inbox path.
5026
+ *
5027
+ * The actor document comes from the application, so it is trusted as is and
5028
+ * its proof is not verified here.
5029
+ * @param actor The actor the actor dispatcher returned.
5030
+ * @param options The requested DID, the canonical form of the requested inbox
5031
+ * ID, and the origin of this server.
5032
+ * @returns The resolution.
5033
+ */
5034
+ function resolvePortableInboxRecipient(actor, { authority, canonicalInboxId, localOrigin }) {
5035
+ if (actor == null) return {
5036
+ status: "rejected",
5037
+ reason: "notFound"
5038
+ };
5039
+ const id = actor.id;
5040
+ if (id == null || id.protocol !== "ap:" && id.protocol !== "ap+ef61:") return {
5041
+ status: "rejected",
5042
+ reason: "notPortable"
5043
+ };
5044
+ if (!isSameAuthority(id, authority)) return {
5045
+ status: "rejected",
5046
+ reason: "authorityMismatch"
5047
+ };
5048
+ const inboxId = actor.inboxId;
5049
+ if (inboxId == null || canonicalize(inboxId) !== canonicalInboxId) return {
5050
+ status: "rejected",
5051
+ reason: "inboxMismatch"
5052
+ };
5053
+ const gateways = actor.gateways;
5054
+ if (!gateways.some((g) => (0, _fedify_vocab_runtime.isGatewayUrl)(g) && g.origin === localOrigin)) return {
5055
+ status: "rejected",
5056
+ reason: "notGateway"
5057
+ };
5058
+ return {
5059
+ status: "accepted",
5060
+ recipient: {
5061
+ inboxId,
5062
+ canonicalInboxId,
5063
+ gateways
5064
+ }
5065
+ };
5066
+ }
5067
+ function isSameAuthority(id, authority) {
5068
+ try {
5069
+ return (0, _fedify_vocab_runtime.getFe34Origin)(id) === authority;
5070
+ } catch (error) {
5071
+ if (error instanceof TypeError) return false;
5072
+ throw error;
5073
+ }
5074
+ }
5075
+ function canonicalize(iri) {
5076
+ try {
5077
+ return (0, _fedify_vocab_runtime.canonicalizePortableUri)(typeof iri === "string" ? iri : (0, _fedify_vocab_runtime.formatIri)(iri));
5078
+ } catch (error) {
5079
+ if (error instanceof TypeError) return null;
5080
+ throw error;
5081
+ }
5082
+ }
5083
+ /**
5084
+ * Forwards an activity received in a portable inbox to the inboxes of the same
5085
+ * portable actor on its other gateways, as FEP-ef61 recommends.
5086
+ *
5087
+ * FEP-ef61 forbids forwarding an activity from an inbox more than once, so
5088
+ * each target gateway is claimed with an atomic compare-and-swap before the
5089
+ * activity is handed off, and a claim is never released, even if the hand-off
5090
+ * fails, since a failed hand-off does not prove that nothing was delivered.
5091
+ * Forwarding is therefore best effort. Without {@link KvStore.cas}, nothing is
5092
+ * forwarded.
5093
+ *
5094
+ * Forwarded requests are not signed with HTTP Signatures; the receiving
5095
+ * gateways authenticate the activity by its own proof.
5096
+ * @param parameters The parameters.
5097
+ * @returns The target inbox URLs that the activity was handed off to.
5098
+ */
5099
+ async function forwardPortableInboxActivity(parameters) {
5100
+ const logger = (0, _logtape_logtape.getLogger)([
5101
+ "fedify",
5102
+ "federation",
5103
+ "inbox"
5104
+ ]);
5105
+ const { recipient, activityId, excludedOrigins, kv, kvPrefix, outboxQueue } = parameters;
5106
+ const { maxTargets, ttl } = parameters.options ?? resolvePortableInboxForwardingOptions();
5107
+ if (kv.cas == null || maxTargets < 1) return [];
5108
+ const canonicalActivityId = canonicalize(activityId) ?? activityId.href;
5109
+ const targets = getForwardingTargets(recipient, excludedOrigins);
5110
+ if (targets.length > maxTargets) {
5111
+ logger.warn("The portable actor that owns the inbox {inbox} has more than {max} other gateways; the activity {activityId} is forwarded only to the first {max} of them.", {
5112
+ inbox: recipient.canonicalInboxId,
5113
+ activityId: activityId.href,
5114
+ max: maxTargets
5115
+ });
5116
+ targets.length = maxTargets;
5117
+ }
5118
+ const claimed = [];
5119
+ for (const { gateway, inbox } of targets) {
5120
+ const key = [
5121
+ ...kvPrefix,
5122
+ recipient.canonicalInboxId,
5123
+ canonicalActivityId,
5124
+ gateway
5125
+ ];
5126
+ if (await kv.cas(key, void 0, true, { ttl })) claimed.push(inbox);
5127
+ }
5128
+ if (claimed.length < 1) return [];
5129
+ logger.debug("Forwarding activity {activityId} from the portable inbox {inbox} to other gateways:\n{targets}", {
5130
+ activityId: activityId.href,
5131
+ inbox: recipient.canonicalInboxId,
5132
+ targets: claimed.map((t) => t.href)
5133
+ });
5134
+ if (outboxQueue == null) await forwardImmediately(parameters, claimed);
5135
+ else await enqueueForwarding(parameters, outboxQueue, claimed);
5136
+ return claimed;
5137
+ }
5138
+ function getForwardingTargets(recipient, excludedOrigins) {
5139
+ const logger = (0, _logtape_logtape.getLogger)([
5140
+ "fedify",
5141
+ "federation",
5142
+ "inbox"
5143
+ ]);
5144
+ const seen = new Set(excludedOrigins);
5145
+ const targets = [];
5146
+ for (const gateway of recipient.gateways) {
5147
+ if (!(0, _fedify_vocab_runtime.isGatewayUrl)(gateway) || seen.has(gateway.origin)) continue;
5148
+ seen.add(gateway.origin);
5149
+ let inbox;
5150
+ try {
5151
+ inbox = (0, _fedify_vocab_runtime.toCompatibleEf61Id)(recipient.inboxId, gateway.origin);
5152
+ } catch (error) {
5153
+ if (!(error instanceof TypeError)) throw error;
5154
+ logger.warn("Cannot forward to the gateway {gateway}, as the portable inbox {inbox} cannot be represented as its compatible identifier.", {
5155
+ gateway: gateway.origin,
5156
+ inbox: recipient.canonicalInboxId
5157
+ });
5158
+ continue;
5159
+ }
5160
+ targets.push({
5161
+ gateway: gateway.origin,
5162
+ inbox
5163
+ });
5164
+ }
5165
+ return targets;
5166
+ }
5167
+ async function forwardImmediately({ activity, activityId, activityType, allowPrivateAddress, options, meterProvider, tracerProvider }, inboxes) {
5168
+ const logger = (0, _logtape_logtape.getLogger)([
5169
+ "fedify",
5170
+ "federation",
5171
+ "inbox"
5172
+ ]);
5173
+ const deadline = (options ?? resolvePortableInboxForwardingOptions()).deadline.total("millisecond");
5174
+ const sends = inboxes.map((inbox) => sendActivity({
5175
+ activity,
5176
+ activityId: activityId.href,
5177
+ activityType,
5178
+ keys: [],
5179
+ inbox,
5180
+ allowPrivateAddress,
5181
+ meterProvider,
5182
+ tracerProvider
5183
+ }).catch((error) => {
5184
+ logger.error("Failed to forward activity {activityId} to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
5185
+ activityId: activityId.href,
5186
+ inbox: inbox.href,
5187
+ error
5188
+ });
5189
+ }));
5190
+ let timer;
5191
+ const timedOut = await Promise.race([Promise.all(sends).then(() => false), new Promise((resolve) => {
5192
+ timer = setTimeout(() => resolve(true), deadline);
5193
+ })]);
5194
+ clearTimeout(timer);
5195
+ if (timedOut) logger.warn("Forwarding activity {activityId} to other gateways did not finish in {deadline} ms; responding to the delivery without waiting for it.", {
5196
+ activityId: activityId.href,
5197
+ deadline
5198
+ });
5199
+ }
5200
+ async function enqueueForwarding({ activity, activityId, activityType, baseUrl, startQueue, meterProvider }, outboxQueue, inboxes) {
5201
+ const logger = (0, _logtape_logtape.getLogger)([
5202
+ "fedify",
5203
+ "federation",
5204
+ "inbox"
5205
+ ]);
5206
+ startQueue?.();
5207
+ const started = (/* @__PURE__ */ new Date()).toISOString();
5208
+ const traceContext = {};
5209
+ _opentelemetry_api.propagation.inject(_opentelemetry_api.context.active(), traceContext);
5210
+ await Promise.all(inboxes.map(async (inbox) => {
5211
+ const message = {
5212
+ type: "outbox",
5213
+ id: crypto.randomUUID(),
5214
+ baseUrl,
5215
+ keys: [],
5216
+ activity,
5217
+ activityId: activityId.href,
5218
+ activityType,
5219
+ inbox: inbox.href,
5220
+ sharedInbox: false,
5221
+ started,
5222
+ attempt: 0,
5223
+ headers: {},
5224
+ traceContext
5225
+ };
5226
+ try {
5227
+ await outboxQueue.enqueue(message);
5228
+ } catch (error) {
5229
+ logger.error("Failed to enqueue activity {activityId} to forward to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
5230
+ activityId: activityId.href,
5231
+ inbox: inbox.href,
5232
+ error
5233
+ });
5234
+ return;
5235
+ }
5236
+ require_http.recordOutboxEnqueue(meterProvider, outboxQueue, message);
5237
+ }));
5238
+ }
5239
+ //#endregion
4971
5240
  //#region src/federation/outgoing-proof.ts
4972
5241
  function isPortableUrl(url) {
4973
5242
  return url.protocol === "ap:" || url.protocol === "ap+ef61:";
@@ -5870,6 +6139,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5870
6139
  kvPrefixes;
5871
6140
  publicKeyTtl;
5872
6141
  httpMessageSignaturesSpecTtl;
6142
+ portableInboxForwarding;
5873
6143
  inboxQueue;
5874
6144
  outboxQueue;
5875
6145
  fanoutQueue;
@@ -5902,6 +6172,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5902
6172
  benchmarkMetricReader;
5903
6173
  benchmarkTriggerOptions;
5904
6174
  #mediaUploaderNoAuthWarned = false;
6175
+ #portableInboxForwardingWarned = false;
5905
6176
  #queueDepthGaugeSourceId = `fedify-${(++nextQueueDepthGaugeSourceId).toString(36)}`;
5906
6177
  #queueDepthGaugeEntries = [];
5907
6178
  #queueDepthGaugeMeterProvider;
@@ -5933,10 +6204,12 @@ var FederationImpl = class extends FederationBuilderImpl {
5933
6204
  acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"],
5934
6205
  circuitBreaker: ["_fedify", "circuit"],
5935
6206
  taskDeduplication: ["_fedify", "taskDeduplication"],
6207
+ portableInboxForwarding: ["_fedify", "portableInboxForwarding"],
5936
6208
  ...options.kvPrefixes ?? {}
5937
6209
  };
5938
6210
  this.publicKeyTtl = Temporal$2.Duration.from(options.publicKeyTtl ?? { days: 30 });
5939
6211
  this.httpMessageSignaturesSpecTtl = Temporal$2.Duration.from(options.httpMessageSignaturesSpecTtl ?? { days: 90 });
6212
+ this.portableInboxForwarding = resolvePortableInboxForwardingOptions(options.portableInboxForwarding);
5940
6213
  if (options.queue == null) {
5941
6214
  this.inboxQueue = void 0;
5942
6215
  this.outboxQueue = void 0;
@@ -6606,8 +6879,8 @@ var FederationImpl = class extends FederationBuilderImpl {
6606
6879
  ]);
6607
6880
  const baseUrl = new URL(message.baseUrl);
6608
6881
  let context = this.#createContext(baseUrl, ctxData);
6609
- if (message.identifier != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: await context.getDocumentLoader({ identifier: message.identifier }) });
6610
- else if (this.sharedInboxKeyDispatcher != null) {
6882
+ if (message.identifier != null && this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: await context.getDocumentLoader({ identifier: message.identifier }) });
6883
+ else if (message.identifier == null && this.sharedInboxKeyDispatcher != null) {
6611
6884
  const identity = await this.sharedInboxKeyDispatcher(context);
6612
6885
  if (identity != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
6613
6886
  }
@@ -7200,6 +7473,15 @@ var FederationImpl = class extends FederationBuilderImpl {
7200
7473
  span,
7201
7474
  metricState
7202
7475
  });
7476
+ if (request.method === "POST") {
7477
+ const portable = parsePortableGatewayRequest(url);
7478
+ if (portable != null) return await this.#fetchPortableInbox(request, portable, {
7479
+ onNotFound,
7480
+ contextData,
7481
+ span,
7482
+ metricState
7483
+ });
7484
+ }
7203
7485
  const portable = request.method === "GET" || request.method === "HEAD" ? parsePortableGatewayRequest(url) : null;
7204
7486
  if (portable != null) return await discardBodyForHead(request, await this.#fetchPortableObject(request, portable, {
7205
7487
  onNotFound,
@@ -7351,35 +7633,17 @@ var FederationImpl = class extends FederationBuilderImpl {
7351
7633
  onNotFound
7352
7634
  });
7353
7635
  context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier: route.values.identifier }) });
7354
- case "sharedInbox": {
7636
+ case "sharedInbox":
7355
7637
  if (routeName !== "inbox" && this.sharedInboxKeyDispatcher != null) {
7356
7638
  const identity = await this.sharedInboxKeyDispatcher(context);
7357
7639
  if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
7358
7640
  }
7359
- if (!this.manuallyStartQueue) this._startQueueInternal(contextData);
7360
- const inboxContextFactory = context.toInboxContext.bind(context);
7361
- inboxContextFactory[rawInboxContextFactorySymbol] = context.toInboxContext.bind(context);
7362
- return await handleInbox(request, {
7641
+ return await this.#handleInbox(request, {
7363
7642
  recipient: route.values.identifier ?? null,
7364
7643
  context,
7365
- inboxContextFactory,
7366
- kv: this.kv,
7367
- kvPrefixes: this.kvPrefixes,
7368
- publicKeyTtl: this.publicKeyTtl,
7369
- queue: this.inboxQueue,
7370
- actorDispatcher: this.actorCallbacks?.dispatcher,
7371
- inboxListeners: this.inboxListeners,
7372
- inboxErrorHandler: this.inboxErrorHandler,
7373
- unverifiedActivityHandler: this.unverifiedActivityHandler,
7374
- onNotFound,
7375
- signatureTimeWindow: this.signatureTimeWindow,
7376
- skipSignatureVerification: this.skipSignatureVerification,
7377
- inboxChallengePolicy: this.inboxChallengePolicy,
7378
- meterProvider: this.meterProvider,
7379
- tracerProvider: this.tracerProvider,
7380
- idempotencyStrategy: this.idempotencyStrategy
7644
+ contextData,
7645
+ onNotFound
7381
7646
  });
7382
- }
7383
7647
  case "following": return await handleCollection(request, {
7384
7648
  name: "following",
7385
7649
  identifier: route.values.identifier,
@@ -7532,6 +7796,113 @@ var FederationImpl = class extends FederationBuilderImpl {
7532
7796
  onNotFound
7533
7797
  });
7534
7798
  }
7799
+ async #handleInbox(request, { recipient, context, contextData, onNotFound, portableInbox }) {
7800
+ if (!this.manuallyStartQueue) this._startQueueInternal(contextData);
7801
+ const inboxContextFactory = context.toInboxContext.bind(context);
7802
+ inboxContextFactory[rawInboxContextFactorySymbol] = context.toInboxContext.bind(context);
7803
+ return await handleInbox(request, {
7804
+ recipient,
7805
+ context,
7806
+ inboxContextFactory,
7807
+ kv: this.kv,
7808
+ kvPrefixes: this.kvPrefixes,
7809
+ publicKeyTtl: this.publicKeyTtl,
7810
+ queue: this.inboxQueue,
7811
+ actorDispatcher: this.actorCallbacks?.dispatcher,
7812
+ inboxListeners: this.inboxListeners,
7813
+ inboxErrorHandler: this.inboxErrorHandler,
7814
+ unverifiedActivityHandler: this.unverifiedActivityHandler,
7815
+ onNotFound,
7816
+ signatureTimeWindow: this.signatureTimeWindow,
7817
+ skipSignatureVerification: this.skipSignatureVerification,
7818
+ inboxChallengePolicy: this.inboxChallengePolicy,
7819
+ meterProvider: this.meterProvider,
7820
+ tracerProvider: this.tracerProvider,
7821
+ idempotencyStrategy: this.idempotencyStrategy,
7822
+ portableInbox
7823
+ });
7824
+ }
7825
+ async #fetchPortableInbox(request, portable, { onNotFound, contextData, span, metricState }) {
7826
+ const logger = (0, _logtape_logtape.getLogger)([
7827
+ "fedify",
7828
+ "federation",
7829
+ "inbox"
7830
+ ]);
7831
+ if (portable.type === "malformed") {
7832
+ metricState.endpoint = "not_found";
7833
+ logger.debug("Malformed FEP-ef61 gateway inbox request {url}: {error}", {
7834
+ url: request.url,
7835
+ error: portable.error
7836
+ });
7837
+ return new Response("Malformed portable inbox ID.", {
7838
+ status: 400,
7839
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
7840
+ });
7841
+ }
7842
+ const route = this.router.route(portable.path);
7843
+ if (route == null || route.name !== "inbox") {
7844
+ metricState.endpoint = "not_found";
7845
+ return await onNotFound(request);
7846
+ }
7847
+ metricState.routeTemplate = PORTABLE_GATEWAY_ROUTE_PREFIX + route.template;
7848
+ metricState.endpoint = "inbox";
7849
+ span.updateName(`${request.method} ${metricState.routeTemplate}`);
7850
+ const identifier = route.values.identifier;
7851
+ let context = this.#createContext(request, contextData);
7852
+ const actor = this.actorCallbacks?.dispatcher == null ? null : await this.actorCallbacks.dispatcher(context, identifier);
7853
+ const localOrigin = context.canonicalOrigin;
7854
+ const resolution = resolvePortableInboxRecipient(actor == null || actor instanceof _fedify_vocab.Tombstone ? null : actor, {
7855
+ authority: portable.portableRequest.authority,
7856
+ canonicalInboxId: portable.canonicalId,
7857
+ localOrigin
7858
+ });
7859
+ if (resolution.status === "rejected") {
7860
+ logger.debug("Not accepting a delivery to the portable inbox {inbox} on behalf of the actor {identifier}: {reason}.", {
7861
+ inbox: portable.canonicalId,
7862
+ identifier,
7863
+ reason: resolution.reason
7864
+ });
7865
+ return await onNotFound(request);
7866
+ }
7867
+ const { recipient } = resolution;
7868
+ if (this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier }) });
7869
+ const excludedOrigins = [localOrigin, new URL(request.url).origin];
7870
+ return await this.#handleInbox(request, {
7871
+ recipient: identifier,
7872
+ context,
7873
+ contextData,
7874
+ onNotFound,
7875
+ portableInbox: {
7876
+ recipient,
7877
+ forward: async (activity, activityId, activityType) => {
7878
+ if (this.portableInboxForwarding.maxTargets < 1) return;
7879
+ if (this.kv.cas == null) {
7880
+ if (!this.#portableInboxForwardingWarned) {
7881
+ this.#portableInboxForwardingWarned = true;
7882
+ logger.warn("Activities delivered to FEP-ef61 portable inboxes are not forwarded to the other gateways, as the key–value store does not support compare-and-swap (KvStore.cas()), which is needed to forward each activity at most once.");
7883
+ }
7884
+ return;
7885
+ }
7886
+ await forwardPortableInboxActivity({
7887
+ recipient,
7888
+ activity,
7889
+ activityId,
7890
+ activityType,
7891
+ excludedOrigins,
7892
+ baseUrl: context.origin,
7893
+ kv: this.kv,
7894
+ kvPrefix: this.kvPrefixes.portableInboxForwarding,
7895
+ outboxQueue: this.outboxQueue,
7896
+ startQueue: this.manuallyStartQueue ? void 0 : () => this._startQueueInternal(contextData),
7897
+ allowPrivateAddress: this.allowPrivateAddress,
7898
+ options: this.portableInboxForwarding,
7899
+ meterProvider: this.meterProvider,
7900
+ tracerProvider: this.tracerProvider
7901
+ });
7902
+ }
7903
+ }
7904
+ });
7905
+ }
7535
7906
  async #fetchHashlinkMedia(request, media, { onNotFound, contextData, span, metricState }) {
7536
7907
  metricState.endpoint = "hashlink_media";
7537
7908
  metricState.routeTemplate = HASHLINK_MEDIA_ROUTE_TEMPLATE;
@@ -7762,6 +8133,12 @@ var ContextImpl = class ContextImpl {
7762
8133
  if (path == null) throw new _fedify_uri_template.RouterError("No inbox path registered.");
7763
8134
  return new URL(path, this.canonicalOrigin);
7764
8135
  }
8136
+ getPortableInboxUri(identifier, authority) {
8137
+ const path = this.federation.router.build("inbox", { identifier });
8138
+ if (path == null) throw new _fedify_uri_template.RouterError("No inbox path registered.");
8139
+ if (authority == null) throw new TypeError("The authority of a portable ID is required outside an FEP-ef61 gateway request.");
8140
+ return buildPortableUri(authority, path);
8141
+ }
7765
8142
  getFollowingUri(identifier) {
7766
8143
  const path = this.federation.router.build("following", { identifier });
7767
8144
  if (path == null) throw new _fedify_uri_template.RouterError("No following collection path registered.");
@@ -8364,6 +8741,9 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
8364
8741
  getPortableObjectUri(cls, values, authority) {
8365
8742
  return super.getPortableObjectUri(cls, values, authority ?? this.portableRequest?.authority);
8366
8743
  }
8744
+ getPortableInboxUri(identifier, authority) {
8745
+ return super.getPortableInboxUri(identifier, authority ?? this.portableRequest?.authority);
8746
+ }
8367
8747
  async getActor(identifier, options) {
8368
8748
  if (this.federation.actorCallbacks == null || this.federation.actorCallbacks.dispatcher == null) throw new Error("No actor dispatcher registered.");
8369
8749
  if (this.#invokedFromActorDispatcher != null) (0, _logtape_logtape.getLogger)([