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

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-i3jxVe9m.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-Cayy-ECj.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-Djkzd-G_.mjs} +1 -1
  10. package/dist/{docloader-C6yOVoaF.mjs → docloader-dA0RswUV.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 +138 -11
  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-CFd0Jhhv.cjs} +463 -82
  34. package/dist/{http-DKWi8RjI.mjs → http-CCJ97Jql.mjs} +4 -4
  35. package/dist/{http-CuLWIJdf.cjs → http-Dvy_IwRa.cjs} +1 -1
  36. package/dist/{http-BoWBwC0g.js → http-aqh3yFdM.js} +1 -1
  37. package/dist/{key-lfcx_NaE.mjs → key-IxTTC8Yf.mjs} +2 -2
  38. package/dist/{kv-cache-DE8kSNQU.cjs → kv-cache-BP69NV0P.cjs} +1 -1
  39. package/dist/{kv-cache-d8EjO29d.mjs → kv-cache-Bwwbh-xR.mjs} +1 -1
  40. package/dist/{kv-cache-BdnmnAVc.js → kv-cache-Y6SDRSX6.js} +1 -1
  41. package/dist/{ld-jyjdrWjG.mjs → ld-l0B9nREb.mjs} +3 -3
  42. package/dist/{metrics-B5Rei-HC.mjs → metrics-D1p6hv4z.mjs} +1 -1
  43. package/dist/{middleware-CdVnCDXD.mjs → middleware-BDjCUHMS.mjs} +1 -1
  44. package/dist/{middleware-CPx4ZWwt.mjs → middleware-BMgzqBtr.mjs} +418 -42
  45. package/dist/{middleware-CAObohey.js → middleware-SSfryVzY.js} +412 -32
  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-BXufwAzm.cjs → owner-BpOxL5PG.cjs} +1 -1
  54. package/dist/{owner-ByqNQVaa.mjs → owner-DGbYmXqU.mjs} +3 -3
  55. package/dist/{owner-1TFakcTF.js → owner-jH1z_YfD.js} +1 -1
  56. package/dist/{portable-key-Cw_ow5vn.mjs → portable-key-CCwJGSsU.mjs} +2 -2
  57. package/dist/{proof-B6gwdhN5.mjs → proof-CVL7JnZd.mjs} +4 -4
  58. package/dist/{send-DsK7xevO.mjs → send-Ejy1jOz9.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-BR_Z6mpY.js → sig-Dpy1B1Ad.js} +2 -2
  73. package/dist/{sig-TTn9wYrE.cjs → sig-xoqy3ATq.cjs} +2 -2
  74. package/dist/{temporal-Bw5jtrBY.mjs → temporal-DNJ1cNpr.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 +7 -7
@@ -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-Dvy_IwRa.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-BpOxL5PG.cjs");
7
+ const require_kv_cache = require("./kv-cache-BP69NV0P.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,
@@ -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 });
@@ -3671,8 +3671,9 @@ async function handleInboxInternal(request, parameters, span) {
3671
3671
  });
3672
3672
  }
3673
3673
  if (!skipSignatureVerification) {
3674
- if (inspectCompoundPortableObjectApplicability(json, INBOX_COMPOUND_PROOF_LIMITS) !== "absent") {
3675
- const compoundProof = await verifyCompoundPortableObjectProofs(json, INBOX_COMPOUND_PROOF_LIMITS, {
3674
+ const compoundJson = Array.isArray(json) ? json : jsonWithoutSig;
3675
+ if (inspectCompoundPortableObjectApplicability(compoundJson, INBOX_COMPOUND_PROOF_LIMITS) !== "absent") {
3676
+ const compoundProof = await verifyCompoundPortableObjectProofs(compoundJson, INBOX_COMPOUND_PROOF_LIMITS, {
3676
3677
  documentLoader: ctx.documentLoader,
3677
3678
  keyCache,
3678
3679
  meterProvider,
@@ -3722,6 +3723,19 @@ async function handleInboxInternal(request, parameters, span) {
3722
3723
  tracerProvider,
3723
3724
  idempotencyStrategy: parameters.idempotencyStrategy
3724
3725
  });
3726
+ 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.", {
3727
+ activityId: activity.id.href,
3728
+ inbox: portableInbox.recipient.canonicalInboxId
3729
+ });
3730
+ else try {
3731
+ await portableInbox.forward(json, activity.id, (0, _fedify_vocab.getTypeId)(activity).href);
3732
+ } catch (error) {
3733
+ logger.error("Failed to forward activity {activityId} to the other gateways of the portable inbox {inbox}:\n{error}", {
3734
+ activityId: activity.id.href,
3735
+ inbox: portableInbox.recipient.canonicalInboxId,
3736
+ error
3737
+ });
3738
+ }
3725
3739
  if (routeResult === "alreadyProcessed") return new Response(`Activity <${activity.id}> has already been processed.`, {
3726
3740
  status: 202,
3727
3741
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -3747,6 +3761,12 @@ async function handleInboxInternal(request, parameters, span) {
3747
3761
  headers: { "Content-Type": "text/plain; charset=utf-8" }
3748
3762
  });
3749
3763
  }
3764
+ const PORTABLE_INBOX_FORWARDABLE_RESULTS = /* @__PURE__ */ new Set([
3765
+ "success",
3766
+ "enqueued",
3767
+ "alreadyProcessed",
3768
+ "unsupportedActivity"
3769
+ ]);
3750
3770
  /**
3751
3771
  * Handles a custom collection request.
3752
3772
  * @template TItem The type of items in the collection.
@@ -4340,7 +4360,7 @@ async function buildAcceptSignatureHeader(policy, kv, noncePrefix) {
4340
4360
  }
4341
4361
  async function setKey(kv, key, policy) {
4342
4362
  const seconds = policy.nonceTtlSeconds ?? 300;
4343
- const ttl = Temporal$6.Duration.from({ seconds });
4363
+ const ttl = Temporal$7.Duration.from({ seconds });
4344
4364
  await kv.set(key, true, { ttl });
4345
4365
  }
4346
4366
  const DEF_COMPONENTS = [
@@ -4365,7 +4385,7 @@ function isPortableGatewayKey(key) {
4365
4385
  }
4366
4386
  //#endregion
4367
4387
  //#region src/federation/kv.ts
4368
- const Temporal$5 = require_http.Temporal;
4388
+ const Temporal$6 = require_http.Temporal;
4369
4389
  /**
4370
4390
  * A key–value store that stores values in memory.
4371
4391
  * Do not use this in production as it does not persist values.
@@ -4385,7 +4405,7 @@ var MemoryKvStore = class {
4385
4405
  const entry = this.#values[encodedKey];
4386
4406
  if (entry == null) return Promise.resolve(void 0);
4387
4407
  const [value, expiration] = entry;
4388
- if (expiration != null && Temporal$5.Now.instant().until(expiration).sign < 0) {
4408
+ if (expiration != null && Temporal$6.Now.instant().until(expiration).sign < 0) {
4389
4409
  delete this.#values[encodedKey];
4390
4410
  return Promise.resolve(void 0);
4391
4411
  }
@@ -4396,7 +4416,7 @@ var MemoryKvStore = class {
4396
4416
  */
4397
4417
  set(key, value, options) {
4398
4418
  const encodedKey = this.#encodeKey(key);
4399
- const expiration = options?.ttl == null ? null : Temporal$5.Now.instant().add(options.ttl.round({ largestUnit: "hour" }));
4419
+ const expiration = options?.ttl == null ? null : Temporal$6.Now.instant().add(options.ttl.round({ largestUnit: "hour" }));
4400
4420
  this.#values[encodedKey] = [value, expiration];
4401
4421
  return Promise.resolve();
4402
4422
  }
@@ -4418,13 +4438,13 @@ var MemoryKvStore = class {
4418
4438
  if (entry == null) currentValue = void 0;
4419
4439
  else {
4420
4440
  const [value, expiration] = entry;
4421
- if (expiration != null && Temporal$5.Now.instant().until(expiration).sign < 0) {
4441
+ if (expiration != null && Temporal$6.Now.instant().until(expiration).sign < 0) {
4422
4442
  delete this.#values[encodedKey];
4423
4443
  currentValue = void 0;
4424
4444
  } else currentValue = value;
4425
4445
  }
4426
4446
  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" }));
4447
+ const expiration = options?.ttl == null ? null : Temporal$6.Now.instant().add(options.ttl.round({ largestUnit: "hour" }));
4428
4448
  this.#values[encodedKey] = [newValue, expiration];
4429
4449
  return Promise.resolve(true);
4430
4450
  }
@@ -4432,7 +4452,7 @@ var MemoryKvStore = class {
4432
4452
  * {@inheritDoc KvStore.list}
4433
4453
  */
4434
4454
  async *list(prefix) {
4435
- const now = Temporal$5.Now.instant();
4455
+ const now = Temporal$6.Now.instant();
4436
4456
  for (const [encodedKey, entry] of Object.entries(this.#values)) {
4437
4457
  const key = JSON.parse(encodedKey);
4438
4458
  if (prefix != null) {
@@ -4626,7 +4646,11 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
4626
4646
  rsaKey = key;
4627
4647
  break;
4628
4648
  }
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.", {
4649
+ if (rsaKey == null && keys.length < 1) logger.debug("Sending the activity {activityId} to {inbox} without a signature.", {
4650
+ activityId,
4651
+ inbox: inbox.href
4652
+ });
4653
+ 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
4654
  inbox: inbox.href,
4631
4655
  keys: keys.map((pair) => ({
4632
4656
  keyId: pair.keyId.href,
@@ -4968,6 +4992,252 @@ function serializeDataPointType(dataPointType) {
4968
4992
  }
4969
4993
  }
4970
4994
  //#endregion
4995
+ //#region src/federation/portable-inbox.ts
4996
+ const Temporal$5 = require_http.Temporal;
4997
+ const MAX_TIMEOUT_MS = 2 ** 31 - 1;
4998
+ /**
4999
+ * Fills in the defaults of {@link PortableInboxForwardingOptions} and
5000
+ * validates them.
5001
+ * @param options The options.
5002
+ * @returns The resolved options.
5003
+ * @throws {RangeError} If `maxTargets` is not a non-negative integer, if `ttl`
5004
+ * is not positive, if `deadline` is negative or longer
5005
+ * than 2,147,483,647 milliseconds (about 24.8 days), or
5006
+ * if either duration has calendar units, i.e., weeks,
5007
+ * months, or years, whose length depends on the date.
5008
+ */
5009
+ function resolvePortableInboxForwardingOptions(options = {}) {
5010
+ const maxTargets = options.maxTargets ?? 10;
5011
+ if (!Number.isInteger(maxTargets) || maxTargets < 0) throw new RangeError("portableInboxForwarding.maxTargets must be a non-negative integer.");
5012
+ const ttl = Temporal$5.Duration.from(options.ttl ?? { days: 30 });
5013
+ if (ttl.total("millisecond") <= 0) throw new RangeError("portableInboxForwarding.ttl must be positive.");
5014
+ const deadline = Temporal$5.Duration.from(options.deadline ?? { seconds: 10 });
5015
+ const deadlineMs = deadline.total("millisecond");
5016
+ if (deadlineMs < 0 || deadlineMs > MAX_TIMEOUT_MS) throw new RangeError(`portableInboxForwarding.deadline must be between 0 and ${MAX_TIMEOUT_MS} milliseconds.`);
5017
+ return {
5018
+ maxTargets,
5019
+ ttl,
5020
+ deadline
5021
+ };
5022
+ }
5023
+ /**
5024
+ * Decides whether this server accepts a delivery to a portable inbox on behalf
5025
+ * of the given actor, which the actor dispatcher returned for the identifier
5026
+ * in the requested inbox path.
5027
+ *
5028
+ * The actor document comes from the application, so it is trusted as is and
5029
+ * its proof is not verified here.
5030
+ * @param actor The actor the actor dispatcher returned.
5031
+ * @param options The requested DID, the canonical form of the requested inbox
5032
+ * ID, and the origin of this server.
5033
+ * @returns The resolution.
5034
+ */
5035
+ function resolvePortableInboxRecipient(actor, { authority, canonicalInboxId, localOrigin }) {
5036
+ if (actor == null) return {
5037
+ status: "rejected",
5038
+ reason: "notFound"
5039
+ };
5040
+ const id = actor.id;
5041
+ if (id == null || id.protocol !== "ap:" && id.protocol !== "ap+ef61:") return {
5042
+ status: "rejected",
5043
+ reason: "notPortable"
5044
+ };
5045
+ if (!isSameAuthority(id, authority)) return {
5046
+ status: "rejected",
5047
+ reason: "authorityMismatch"
5048
+ };
5049
+ const inboxId = actor.inboxId;
5050
+ if (inboxId == null || canonicalize(inboxId) !== canonicalInboxId) return {
5051
+ status: "rejected",
5052
+ reason: "inboxMismatch"
5053
+ };
5054
+ const gateways = actor.gateways;
5055
+ if (!gateways.some((g) => (0, _fedify_vocab_runtime.isGatewayUrl)(g) && g.origin === localOrigin)) return {
5056
+ status: "rejected",
5057
+ reason: "notGateway"
5058
+ };
5059
+ return {
5060
+ status: "accepted",
5061
+ recipient: {
5062
+ inboxId,
5063
+ canonicalInboxId,
5064
+ gateways
5065
+ }
5066
+ };
5067
+ }
5068
+ function isSameAuthority(id, authority) {
5069
+ try {
5070
+ return (0, _fedify_vocab_runtime.getFe34Origin)(id) === authority;
5071
+ } catch (error) {
5072
+ if (error instanceof TypeError) return false;
5073
+ throw error;
5074
+ }
5075
+ }
5076
+ function canonicalize(iri) {
5077
+ try {
5078
+ return (0, _fedify_vocab_runtime.canonicalizePortableUri)(typeof iri === "string" ? iri : (0, _fedify_vocab_runtime.formatIri)(iri));
5079
+ } catch (error) {
5080
+ if (error instanceof TypeError) return null;
5081
+ throw error;
5082
+ }
5083
+ }
5084
+ /**
5085
+ * Forwards an activity received in a portable inbox to the inboxes of the same
5086
+ * portable actor on its other gateways, as FEP-ef61 recommends.
5087
+ *
5088
+ * FEP-ef61 forbids forwarding an activity from an inbox more than once, so
5089
+ * each target gateway is claimed with an atomic compare-and-swap before the
5090
+ * activity is handed off, and a claim is never released, even if the hand-off
5091
+ * fails, since a failed hand-off does not prove that nothing was delivered.
5092
+ * Forwarding is therefore best effort. Without {@link KvStore.cas}, nothing is
5093
+ * forwarded.
5094
+ *
5095
+ * Forwarded requests are not signed with HTTP Signatures; the receiving
5096
+ * gateways authenticate the activity by its own proof.
5097
+ * @param parameters The parameters.
5098
+ * @returns The target inbox URLs that the activity was handed off to.
5099
+ */
5100
+ async function forwardPortableInboxActivity(parameters) {
5101
+ const logger = (0, _logtape_logtape.getLogger)([
5102
+ "fedify",
5103
+ "federation",
5104
+ "inbox"
5105
+ ]);
5106
+ const { recipient, activityId, excludedOrigins, kv, kvPrefix, outboxQueue } = parameters;
5107
+ const { maxTargets, ttl } = parameters.options ?? resolvePortableInboxForwardingOptions();
5108
+ if (kv.cas == null || maxTargets < 1) return [];
5109
+ const canonicalActivityId = canonicalize(activityId) ?? activityId.href;
5110
+ const targets = getForwardingTargets(recipient, excludedOrigins);
5111
+ if (targets.length > maxTargets) {
5112
+ 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.", {
5113
+ inbox: recipient.canonicalInboxId,
5114
+ activityId: activityId.href,
5115
+ max: maxTargets
5116
+ });
5117
+ targets.length = maxTargets;
5118
+ }
5119
+ const claimed = [];
5120
+ for (const { gateway, inbox } of targets) {
5121
+ const key = [
5122
+ ...kvPrefix,
5123
+ recipient.canonicalInboxId,
5124
+ canonicalActivityId,
5125
+ gateway
5126
+ ];
5127
+ if (await kv.cas(key, void 0, true, { ttl })) claimed.push(inbox);
5128
+ }
5129
+ if (claimed.length < 1) return [];
5130
+ logger.debug("Forwarding activity {activityId} from the portable inbox {inbox} to other gateways:\n{targets}", {
5131
+ activityId: activityId.href,
5132
+ inbox: recipient.canonicalInboxId,
5133
+ targets: claimed.map((t) => t.href)
5134
+ });
5135
+ if (outboxQueue == null) await forwardImmediately(parameters, claimed);
5136
+ else await enqueueForwarding(parameters, outboxQueue, claimed);
5137
+ return claimed;
5138
+ }
5139
+ function getForwardingTargets(recipient, excludedOrigins) {
5140
+ const logger = (0, _logtape_logtape.getLogger)([
5141
+ "fedify",
5142
+ "federation",
5143
+ "inbox"
5144
+ ]);
5145
+ const seen = new Set(excludedOrigins);
5146
+ const targets = [];
5147
+ for (const gateway of recipient.gateways) {
5148
+ if (!(0, _fedify_vocab_runtime.isGatewayUrl)(gateway) || seen.has(gateway.origin)) continue;
5149
+ seen.add(gateway.origin);
5150
+ let inbox;
5151
+ try {
5152
+ inbox = (0, _fedify_vocab_runtime.toCompatibleEf61Id)(recipient.inboxId, gateway.origin);
5153
+ } catch (error) {
5154
+ if (!(error instanceof TypeError)) throw error;
5155
+ logger.warn("Cannot forward to the gateway {gateway}, as the portable inbox {inbox} cannot be represented as its compatible identifier.", {
5156
+ gateway: gateway.origin,
5157
+ inbox: recipient.canonicalInboxId
5158
+ });
5159
+ continue;
5160
+ }
5161
+ targets.push({
5162
+ gateway: gateway.origin,
5163
+ inbox
5164
+ });
5165
+ }
5166
+ return targets;
5167
+ }
5168
+ async function forwardImmediately({ activity, activityId, activityType, allowPrivateAddress, options, meterProvider, tracerProvider }, inboxes) {
5169
+ const logger = (0, _logtape_logtape.getLogger)([
5170
+ "fedify",
5171
+ "federation",
5172
+ "inbox"
5173
+ ]);
5174
+ const deadline = (options ?? resolvePortableInboxForwardingOptions()).deadline.total("millisecond");
5175
+ const sends = inboxes.map((inbox) => sendActivity({
5176
+ activity,
5177
+ activityId: activityId.href,
5178
+ activityType,
5179
+ keys: [],
5180
+ inbox,
5181
+ allowPrivateAddress,
5182
+ meterProvider,
5183
+ tracerProvider
5184
+ }).catch((error) => {
5185
+ logger.error("Failed to forward activity {activityId} to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
5186
+ activityId: activityId.href,
5187
+ inbox: inbox.href,
5188
+ error
5189
+ });
5190
+ }));
5191
+ let timer;
5192
+ const timedOut = await Promise.race([Promise.all(sends).then(() => false), new Promise((resolve) => {
5193
+ timer = setTimeout(() => resolve(true), deadline);
5194
+ })]);
5195
+ clearTimeout(timer);
5196
+ if (timedOut) logger.warn("Forwarding activity {activityId} to other gateways did not finish in {deadline} ms; responding to the delivery without waiting for it.", {
5197
+ activityId: activityId.href,
5198
+ deadline
5199
+ });
5200
+ }
5201
+ async function enqueueForwarding({ activity, activityId, activityType, baseUrl, startQueue, meterProvider }, outboxQueue, inboxes) {
5202
+ const logger = (0, _logtape_logtape.getLogger)([
5203
+ "fedify",
5204
+ "federation",
5205
+ "inbox"
5206
+ ]);
5207
+ startQueue?.();
5208
+ const started = (/* @__PURE__ */ new Date()).toISOString();
5209
+ const traceContext = {};
5210
+ _opentelemetry_api.propagation.inject(_opentelemetry_api.context.active(), traceContext);
5211
+ await Promise.all(inboxes.map(async (inbox) => {
5212
+ const message = {
5213
+ type: "outbox",
5214
+ id: crypto.randomUUID(),
5215
+ baseUrl,
5216
+ keys: [],
5217
+ activity,
5218
+ activityId: activityId.href,
5219
+ activityType,
5220
+ inbox: inbox.href,
5221
+ sharedInbox: false,
5222
+ started,
5223
+ attempt: 0,
5224
+ headers: {},
5225
+ traceContext
5226
+ };
5227
+ try {
5228
+ await outboxQueue.enqueue(message);
5229
+ } catch (error) {
5230
+ logger.error("Failed to enqueue activity {activityId} to forward to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
5231
+ activityId: activityId.href,
5232
+ inbox: inbox.href,
5233
+ error
5234
+ });
5235
+ return;
5236
+ }
5237
+ require_http.recordOutboxEnqueue(meterProvider, outboxQueue, message);
5238
+ }));
5239
+ }
5240
+ //#endregion
4971
5241
  //#region src/federation/outgoing-proof.ts
4972
5242
  function isPortableUrl(url) {
4973
5243
  return url.protocol === "ap:" || url.protocol === "ap+ef61:";
@@ -5870,6 +6140,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5870
6140
  kvPrefixes;
5871
6141
  publicKeyTtl;
5872
6142
  httpMessageSignaturesSpecTtl;
6143
+ portableInboxForwarding;
5873
6144
  inboxQueue;
5874
6145
  outboxQueue;
5875
6146
  fanoutQueue;
@@ -5902,6 +6173,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5902
6173
  benchmarkMetricReader;
5903
6174
  benchmarkTriggerOptions;
5904
6175
  #mediaUploaderNoAuthWarned = false;
6176
+ #portableInboxForwardingWarned = false;
5905
6177
  #queueDepthGaugeSourceId = `fedify-${(++nextQueueDepthGaugeSourceId).toString(36)}`;
5906
6178
  #queueDepthGaugeEntries = [];
5907
6179
  #queueDepthGaugeMeterProvider;
@@ -5933,10 +6205,12 @@ var FederationImpl = class extends FederationBuilderImpl {
5933
6205
  acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"],
5934
6206
  circuitBreaker: ["_fedify", "circuit"],
5935
6207
  taskDeduplication: ["_fedify", "taskDeduplication"],
6208
+ portableInboxForwarding: ["_fedify", "portableInboxForwarding"],
5936
6209
  ...options.kvPrefixes ?? {}
5937
6210
  };
5938
6211
  this.publicKeyTtl = Temporal$2.Duration.from(options.publicKeyTtl ?? { days: 30 });
5939
6212
  this.httpMessageSignaturesSpecTtl = Temporal$2.Duration.from(options.httpMessageSignaturesSpecTtl ?? { days: 90 });
6213
+ this.portableInboxForwarding = resolvePortableInboxForwardingOptions(options.portableInboxForwarding);
5940
6214
  if (options.queue == null) {
5941
6215
  this.inboxQueue = void 0;
5942
6216
  this.outboxQueue = void 0;
@@ -6606,8 +6880,8 @@ var FederationImpl = class extends FederationBuilderImpl {
6606
6880
  ]);
6607
6881
  const baseUrl = new URL(message.baseUrl);
6608
6882
  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) {
6883
+ if (message.identifier != null && this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: await context.getDocumentLoader({ identifier: message.identifier }) });
6884
+ else if (message.identifier == null && this.sharedInboxKeyDispatcher != null) {
6611
6885
  const identity = await this.sharedInboxKeyDispatcher(context);
6612
6886
  if (identity != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
6613
6887
  }
@@ -7200,6 +7474,15 @@ var FederationImpl = class extends FederationBuilderImpl {
7200
7474
  span,
7201
7475
  metricState
7202
7476
  });
7477
+ if (request.method === "POST") {
7478
+ const portable = parsePortableGatewayRequest(url);
7479
+ if (portable != null) return await this.#fetchPortableInbox(request, portable, {
7480
+ onNotFound,
7481
+ contextData,
7482
+ span,
7483
+ metricState
7484
+ });
7485
+ }
7203
7486
  const portable = request.method === "GET" || request.method === "HEAD" ? parsePortableGatewayRequest(url) : null;
7204
7487
  if (portable != null) return await discardBodyForHead(request, await this.#fetchPortableObject(request, portable, {
7205
7488
  onNotFound,
@@ -7351,35 +7634,17 @@ var FederationImpl = class extends FederationBuilderImpl {
7351
7634
  onNotFound
7352
7635
  });
7353
7636
  context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier: route.values.identifier }) });
7354
- case "sharedInbox": {
7637
+ case "sharedInbox":
7355
7638
  if (routeName !== "inbox" && this.sharedInboxKeyDispatcher != null) {
7356
7639
  const identity = await this.sharedInboxKeyDispatcher(context);
7357
7640
  if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
7358
7641
  }
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, {
7642
+ return await this.#handleInbox(request, {
7363
7643
  recipient: route.values.identifier ?? null,
7364
7644
  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
7645
+ contextData,
7646
+ onNotFound
7381
7647
  });
7382
- }
7383
7648
  case "following": return await handleCollection(request, {
7384
7649
  name: "following",
7385
7650
  identifier: route.values.identifier,
@@ -7532,6 +7797,113 @@ var FederationImpl = class extends FederationBuilderImpl {
7532
7797
  onNotFound
7533
7798
  });
7534
7799
  }
7800
+ async #handleInbox(request, { recipient, context, contextData, onNotFound, portableInbox }) {
7801
+ if (!this.manuallyStartQueue) this._startQueueInternal(contextData);
7802
+ const inboxContextFactory = context.toInboxContext.bind(context);
7803
+ inboxContextFactory[rawInboxContextFactorySymbol] = context.toInboxContext.bind(context);
7804
+ return await handleInbox(request, {
7805
+ recipient,
7806
+ context,
7807
+ inboxContextFactory,
7808
+ kv: this.kv,
7809
+ kvPrefixes: this.kvPrefixes,
7810
+ publicKeyTtl: this.publicKeyTtl,
7811
+ queue: this.inboxQueue,
7812
+ actorDispatcher: this.actorCallbacks?.dispatcher,
7813
+ inboxListeners: this.inboxListeners,
7814
+ inboxErrorHandler: this.inboxErrorHandler,
7815
+ unverifiedActivityHandler: this.unverifiedActivityHandler,
7816
+ onNotFound,
7817
+ signatureTimeWindow: this.signatureTimeWindow,
7818
+ skipSignatureVerification: this.skipSignatureVerification,
7819
+ inboxChallengePolicy: this.inboxChallengePolicy,
7820
+ meterProvider: this.meterProvider,
7821
+ tracerProvider: this.tracerProvider,
7822
+ idempotencyStrategy: this.idempotencyStrategy,
7823
+ portableInbox
7824
+ });
7825
+ }
7826
+ async #fetchPortableInbox(request, portable, { onNotFound, contextData, span, metricState }) {
7827
+ const logger = (0, _logtape_logtape.getLogger)([
7828
+ "fedify",
7829
+ "federation",
7830
+ "inbox"
7831
+ ]);
7832
+ if (portable.type === "malformed") {
7833
+ metricState.endpoint = "not_found";
7834
+ logger.debug("Malformed FEP-ef61 gateway inbox request {url}: {error}", {
7835
+ url: request.url,
7836
+ error: portable.error
7837
+ });
7838
+ return new Response("Malformed portable inbox ID.", {
7839
+ status: 400,
7840
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
7841
+ });
7842
+ }
7843
+ const route = this.router.route(portable.path);
7844
+ if (route == null || route.name !== "inbox") {
7845
+ metricState.endpoint = "not_found";
7846
+ return await onNotFound(request);
7847
+ }
7848
+ metricState.routeTemplate = PORTABLE_GATEWAY_ROUTE_PREFIX + route.template;
7849
+ metricState.endpoint = "inbox";
7850
+ span.updateName(`${request.method} ${metricState.routeTemplate}`);
7851
+ const identifier = route.values.identifier;
7852
+ let context = this.#createContext(request, contextData);
7853
+ const actor = this.actorCallbacks?.dispatcher == null ? null : await this.actorCallbacks.dispatcher(context, identifier);
7854
+ const localOrigin = context.canonicalOrigin;
7855
+ const resolution = resolvePortableInboxRecipient(actor == null || actor instanceof _fedify_vocab.Tombstone ? null : actor, {
7856
+ authority: portable.portableRequest.authority,
7857
+ canonicalInboxId: portable.canonicalId,
7858
+ localOrigin
7859
+ });
7860
+ if (resolution.status === "rejected") {
7861
+ logger.debug("Not accepting a delivery to the portable inbox {inbox} on behalf of the actor {identifier}: {reason}.", {
7862
+ inbox: portable.canonicalId,
7863
+ identifier,
7864
+ reason: resolution.reason
7865
+ });
7866
+ return await onNotFound(request);
7867
+ }
7868
+ const { recipient } = resolution;
7869
+ if (this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier }) });
7870
+ const excludedOrigins = [localOrigin, new URL(request.url).origin];
7871
+ return await this.#handleInbox(request, {
7872
+ recipient: identifier,
7873
+ context,
7874
+ contextData,
7875
+ onNotFound,
7876
+ portableInbox: {
7877
+ recipient,
7878
+ forward: async (activity, activityId, activityType) => {
7879
+ if (this.portableInboxForwarding.maxTargets < 1) return;
7880
+ if (this.kv.cas == null) {
7881
+ if (!this.#portableInboxForwardingWarned) {
7882
+ this.#portableInboxForwardingWarned = true;
7883
+ 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.");
7884
+ }
7885
+ return;
7886
+ }
7887
+ await forwardPortableInboxActivity({
7888
+ recipient,
7889
+ activity,
7890
+ activityId,
7891
+ activityType,
7892
+ excludedOrigins,
7893
+ baseUrl: context.origin,
7894
+ kv: this.kv,
7895
+ kvPrefix: this.kvPrefixes.portableInboxForwarding,
7896
+ outboxQueue: this.outboxQueue,
7897
+ startQueue: this.manuallyStartQueue ? void 0 : () => this._startQueueInternal(contextData),
7898
+ allowPrivateAddress: this.allowPrivateAddress,
7899
+ options: this.portableInboxForwarding,
7900
+ meterProvider: this.meterProvider,
7901
+ tracerProvider: this.tracerProvider
7902
+ });
7903
+ }
7904
+ }
7905
+ });
7906
+ }
7535
7907
  async #fetchHashlinkMedia(request, media, { onNotFound, contextData, span, metricState }) {
7536
7908
  metricState.endpoint = "hashlink_media";
7537
7909
  metricState.routeTemplate = HASHLINK_MEDIA_ROUTE_TEMPLATE;
@@ -7762,6 +8134,12 @@ var ContextImpl = class ContextImpl {
7762
8134
  if (path == null) throw new _fedify_uri_template.RouterError("No inbox path registered.");
7763
8135
  return new URL(path, this.canonicalOrigin);
7764
8136
  }
8137
+ getPortableInboxUri(identifier, authority) {
8138
+ const path = this.federation.router.build("inbox", { identifier });
8139
+ if (path == null) throw new _fedify_uri_template.RouterError("No inbox path registered.");
8140
+ if (authority == null) throw new TypeError("The authority of a portable ID is required outside an FEP-ef61 gateway request.");
8141
+ return buildPortableUri(authority, path);
8142
+ }
7765
8143
  getFollowingUri(identifier) {
7766
8144
  const path = this.federation.router.build("following", { identifier });
7767
8145
  if (path == null) throw new _fedify_uri_template.RouterError("No following collection path registered.");
@@ -8364,6 +8742,9 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
8364
8742
  getPortableObjectUri(cls, values, authority) {
8365
8743
  return super.getPortableObjectUri(cls, values, authority ?? this.portableRequest?.authority);
8366
8744
  }
8745
+ getPortableInboxUri(identifier, authority) {
8746
+ return super.getPortableInboxUri(identifier, authority ?? this.portableRequest?.authority);
8747
+ }
8367
8748
  async getActor(identifier, options) {
8368
8749
  if (this.federation.actorCallbacks == null || this.federation.actorCallbacks.dispatcher == null) throw new Error("No actor dispatcher registered.");
8369
8750
  if (this.#invokedFromActorDispatcher != null) (0, _logtape_logtape.getLogger)([