@fedify/fedify 2.4.0-dev.2065 → 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.
- package/dist/{builder-CMkrnYbY.mjs → builder-CVHpPAjK.mjs} +6 -2
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/transformers.test.mjs +1 -1
- package/dist/{compound-proof-Dj3DCsoj.mjs → compound-proof-B5Skp-vE.mjs} +1 -1
- package/dist/{context-D_gg4M0n.d.ts → context-BSywYspi.d.ts} +153 -2
- package/dist/{context-b36TpPoG.d.cts → context-CZjZYXFC.d.cts} +153 -2
- package/dist/{context-DG5QeQ8W.mjs → context-DlZhxdFT.mjs} +2 -1
- package/dist/{deno-BuRFI7wk.mjs → deno-LYTbBhYV.mjs} +1 -1
- package/dist/{docloader-_aSNLA90.mjs → docloader-C0SWQylk.mjs} +2 -2
- package/dist/federation/builder.test.mjs +1 -1
- package/dist/federation/compound-fanout.test.mjs +4 -4
- package/dist/federation/handler-compound-proof.test.mjs +4 -4
- package/dist/federation/handler.test.mjs +6 -6
- package/dist/federation/hashlink-media.test.mjs +2 -2
- package/dist/federation/idempotency.test.mjs +2 -2
- package/dist/federation/metrics.test.mjs +1 -1
- package/dist/federation/middleware.test.mjs +9 -9
- package/dist/federation/mod.cjs +1 -1
- package/dist/federation/mod.d.cts +3 -3
- package/dist/federation/mod.d.ts +3 -3
- package/dist/federation/mod.js +1 -1
- package/dist/federation/outgoing-proof.test.mjs +6 -6
- package/dist/federation/portable-gateway-keys.test.d.mts +2 -0
- package/dist/federation/portable-gateway-keys.test.mjs +363 -0
- package/dist/federation/portable-inbox.test.d.mts +2 -0
- package/dist/federation/portable-inbox.test.mjs +546 -0
- package/dist/federation/portable.test.mjs +3 -3
- package/dist/federation/send.test.mjs +3 -3
- package/dist/federation/tasks/enqueue.test.mjs +2 -2
- package/dist/federation/tasks/tasks.test.mjs +3 -3
- package/dist/federation/temporal.test.mjs +1 -1
- package/dist/federation/webfinger.test.mjs +2 -2
- package/dist/{federation-Coi_bg12.cjs → federation-B1aeTKKN.cjs} +631 -145
- package/dist/{http-f1qaBtOh.mjs → http-C7qHlv-b.mjs} +20 -5
- package/dist/{http-BGShIsd6.cjs → http-DGk8XI38.cjs} +2684 -48
- package/dist/{http-65PYi9jY.d.ts → http-n2v6DPxu.d.ts} +52 -2
- package/dist/http-vfTpzGEq.js +5087 -0
- package/dist/{http-CP-7OdSq.d.cts → http-xlG-jRdB.d.cts} +52 -2
- package/dist/{key-BxHsA1s9.mjs → key-BYXnTGro.mjs} +147 -7
- package/dist/{kv-cache-Cgj-kpQS.js → kv-cache-DCJuwdnB.js} +1 -1
- package/dist/{kv-cache-C9IY-FMN.mjs → kv-cache-DEH5jyND.mjs} +1 -1
- package/dist/{kv-cache-CY6pDNoS.cjs → kv-cache-DvqkKNIj.cjs} +1 -1
- package/dist/{ld-CqJq5erX.mjs → ld-DT3e2hOv.mjs} +3 -3
- package/dist/{metrics-DOqknlZd.mjs → metrics-DvlvYpq_.mjs} +1 -1
- package/dist/{middleware-VOCZN3SO.mjs → middleware-BJcez_E3.mjs} +1 -1
- package/dist/{middleware-1h_LgwXC.js → middleware-CCbyUF4c.js} +547 -62
- package/dist/{middleware-s-AZzx4r.mjs → middleware-CpyCNOW_.mjs} +549 -72
- package/dist/{mod-BhMvsgdF.d.ts → mod-B2h2KY0q.d.ts} +1 -1
- package/dist/{mod-Dhm7SqyE.d.ts → mod-C9WSpZWm.d.ts} +1 -1
- package/dist/{mod-C0BYc0OU.d.ts → mod-CVZrXSdm.d.ts} +1 -1
- package/dist/{mod-pLbeNwcC.d.cts → mod-HDSvS-ww.d.cts} +1 -1
- package/dist/{mod-Bfenkg_c.d.cts → mod-JzkaEXsi.d.cts} +1 -1
- package/dist/{mod-CyiSjRkk.d.cts → mod-xGFCHqq_.d.cts} +1 -1
- package/dist/mod.cjs +18 -18
- package/dist/mod.d.cts +7 -7
- package/dist/mod.d.ts +7 -7
- package/dist/mod.js +5 -5
- package/dist/nodeinfo/handler.test.mjs +2 -2
- package/dist/{owner-D_F22lfM.mjs → owner-CDsVeZI7.mjs} +45 -2
- package/dist/owner-DCIH7Iq0.cjs +225 -0
- package/dist/owner-DfGz0cXm.js +215 -0
- package/dist/portable-key-DCyKGCQd.mjs +194 -0
- package/dist/{proof-B03JXhrk.mjs → proof-CxsVG6vM.mjs} +5 -5
- package/dist/{send-NNkfKtVN.mjs → send-BZJ7NsVV.mjs} +8 -4
- package/dist/sig/compound-proof-discovery.test.mjs +1 -1
- package/dist/sig/compound-proof-producer.test.mjs +1 -1
- package/dist/sig/compound-proof-verification.test.mjs +2 -2
- package/dist/sig/compound-proof.test.mjs +3 -3
- package/dist/sig/http.test.mjs +2 -2
- package/dist/sig/key.test.mjs +1 -1
- package/dist/sig/ld.test.mjs +2 -2
- package/dist/sig/mod.cjs +16 -16
- package/dist/sig/mod.d.cts +2 -2
- package/dist/sig/mod.d.ts +2 -2
- package/dist/sig/mod.js +3 -3
- package/dist/sig/owner.test.mjs +1 -1
- package/dist/sig/portable-collection.test.mjs +3 -3
- package/dist/sig/portable-key.test.d.mts +2 -0
- package/dist/sig/portable-key.test.mjs +275 -0
- package/dist/sig/proof.test.mjs +4 -4
- package/dist/{sig-B1E2uyhQ.cjs → sig-C6TnADze.cjs} +4 -4
- package/dist/{sig-Bb20Pyrd.js → sig-Cs9_ecdh.js} +2 -2
- package/dist/{temporal-VVVSuruq.mjs → temporal-CLzSfIOV.mjs} +1 -1
- package/dist/testing/kv.test.mjs +1 -1
- package/dist/testing/mod.d.mts +151 -0
- package/dist/testing/mod.mjs +2 -2
- package/dist/{testing-By1YYsIK.mjs → testing-CO_cEvQc.mjs} +1 -1
- package/dist/utils/docloader.test.mjs +2 -2
- package/dist/utils/kv-cache.test.mjs +1 -1
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.d.cts +1 -1
- package/dist/utils/mod.d.ts +1 -1
- package/dist/utils/mod.js +1 -1
- package/package.json +6 -6
- package/dist/http-z5n7chWw.js +0 -2647
- package/dist/owner-BLBWXCNt.js +0 -2276
- package/dist/owner-D8ZpRusc.cjs +0 -2446
|
@@ -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-
|
|
4
|
+
const require_http = require("./http-DGk8XI38.cjs");
|
|
5
5
|
const require_types = require("./types-Be2zbyeG.cjs");
|
|
6
|
-
const require_owner = require("./owner-
|
|
7
|
-
const require_kv_cache = require("./kv-cache-
|
|
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");
|
|
@@ -281,6 +281,10 @@ var FederationBuilderImpl = class {
|
|
|
281
281
|
callbacks.aliasMapper = mapper;
|
|
282
282
|
return setters;
|
|
283
283
|
},
|
|
284
|
+
mapPortableActorId(mapper) {
|
|
285
|
+
callbacks.portableActorIdMapper = mapper;
|
|
286
|
+
return setters;
|
|
287
|
+
},
|
|
284
288
|
mapActorAlias: (path, identifier) => {
|
|
285
289
|
if (identifier === "") throw new _fedify_uri_template.RouterError("Identifier cannot be empty.");
|
|
286
290
|
if (this.router.has(`actorAlias:${identifier}`)) throw new _fedify_uri_template.RouterError(`Actor alias for "${identifier}" already set.`);
|
|
@@ -731,13 +735,13 @@ function createFederationBuilder() {
|
|
|
731
735
|
}
|
|
732
736
|
//#endregion
|
|
733
737
|
//#region src/federation/circuit-breaker.ts
|
|
734
|
-
const Temporal$
|
|
738
|
+
const Temporal$11 = require_http.Temporal;
|
|
735
739
|
const LEGACY_SWEEP_MARKER = ["__fedify_meta", "circuit_breaker_state_ttl_sweep_v2"];
|
|
736
740
|
const LEGACY_SWEEP_DELETING_MARKER = { __fedifyDeletingCircuitBreakerLegacyState: true };
|
|
737
741
|
const CIRCUIT_BREAKER_STATE_VERSION = 1;
|
|
738
|
-
const LEGACY_SWEEP_LOCK_TTL = Temporal$
|
|
739
|
-
const LEGACY_SWEEP_RENEW_INTERVAL = Temporal$
|
|
740
|
-
const LEGACY_SWEEP_RETRY_WINDOW = Temporal$
|
|
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 });
|
|
741
745
|
const LEGACY_SWEEP_WAIT_INTERVAL = 10;
|
|
742
746
|
const LEGACY_SWEEP_CAS_ATTEMPTS = 3;
|
|
743
747
|
/**
|
|
@@ -756,7 +760,7 @@ var CircuitBreaker = class {
|
|
|
756
760
|
this.#kv = options.kv;
|
|
757
761
|
this.#prefix = options.prefix;
|
|
758
762
|
this.#options = normalizeCircuitBreakerOptions(options.options ?? {});
|
|
759
|
-
this.#now = options.now ?? (() => Temporal$
|
|
763
|
+
this.#now = options.now ?? (() => Temporal$11.Now.instant());
|
|
760
764
|
this.#stateChangeObserver = options.stateChangeObserver;
|
|
761
765
|
}
|
|
762
766
|
get options() {
|
|
@@ -770,7 +774,7 @@ var CircuitBreaker = class {
|
|
|
770
774
|
this.#sweepLegacyStates();
|
|
771
775
|
const heldSince = parseHeldSince(message.circuitHeldSince);
|
|
772
776
|
const now = this.#now();
|
|
773
|
-
if (heldSince != null && Temporal$
|
|
777
|
+
if (heldSince != null && Temporal$11.Instant.compare(heldSince.add(this.#options.heldActivityTtl), now) <= 0) return {
|
|
774
778
|
type: "drop",
|
|
775
779
|
heldSince
|
|
776
780
|
};
|
|
@@ -782,12 +786,12 @@ var CircuitBreaker = class {
|
|
|
782
786
|
probe: false
|
|
783
787
|
};
|
|
784
788
|
if (oldState.state === "half-open") {
|
|
785
|
-
const halfOpened = oldState.halfOpened == null ? void 0 : Temporal$
|
|
789
|
+
const halfOpened = oldState.halfOpened == null ? void 0 : Temporal$11.Instant.from(oldState.halfOpened);
|
|
786
790
|
if (halfOpened != null) {
|
|
787
791
|
const staleAt = halfOpened.add(this.#options.recoveryDelay);
|
|
788
|
-
if (Temporal$
|
|
792
|
+
if (Temporal$11.Instant.compare(now, staleAt) < 0) {
|
|
789
793
|
const releaseAt = now.add(this.#options.releaseInterval);
|
|
790
|
-
const retryAt = Temporal$
|
|
794
|
+
const retryAt = Temporal$11.Instant.compare(releaseAt, staleAt) < 0 ? releaseAt : staleAt;
|
|
791
795
|
const cappedRetryAt = this.#capHeldRetryAt(now, heldSince, retryAt);
|
|
792
796
|
return {
|
|
793
797
|
type: "hold",
|
|
@@ -809,8 +813,8 @@ var CircuitBreaker = class {
|
|
|
809
813
|
lastConflictingState = "half-open";
|
|
810
814
|
continue;
|
|
811
815
|
}
|
|
812
|
-
const probeAt = (oldState.opened == null ? now : Temporal$
|
|
813
|
-
if (Temporal$
|
|
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) {
|
|
814
818
|
const retryAt = this.#capHeldRetryAt(now, heldSince, probeAt);
|
|
815
819
|
return {
|
|
816
820
|
type: "hold",
|
|
@@ -875,7 +879,7 @@ var CircuitBreaker = class {
|
|
|
875
879
|
for (let attempt = 0; attempt < 10; attempt++) {
|
|
876
880
|
const oldState = await this.#get(remoteHost);
|
|
877
881
|
if (oldState?.state === "open") return void 0;
|
|
878
|
-
const oldFailures = oldState?.failures.map(Temporal$
|
|
882
|
+
const oldFailures = oldState?.failures.map(Temporal$11.Instant.from) ?? [];
|
|
879
883
|
const failures = this.#options.pruneFailures([...oldFailures, now], now);
|
|
880
884
|
let newState;
|
|
881
885
|
let transition;
|
|
@@ -959,7 +963,7 @@ var CircuitBreaker = class {
|
|
|
959
963
|
for await (const { key, value } of this.#kv.list(this.#prefix)) {
|
|
960
964
|
if (key.length !== this.#prefix.length + 1) continue;
|
|
961
965
|
await this.#migrateLegacyState(key, value);
|
|
962
|
-
if (Temporal$
|
|
966
|
+
if (Temporal$11.Instant.compare(this.#now(), leaseRenewedAt.add(LEGACY_SWEEP_RENEW_INTERVAL)) >= 0) {
|
|
963
967
|
const renewed = await this.#renewLegacySweep(markerKey, marker);
|
|
964
968
|
if (renewed == null) return;
|
|
965
969
|
marker = renewed;
|
|
@@ -983,7 +987,7 @@ var CircuitBreaker = class {
|
|
|
983
987
|
}
|
|
984
988
|
#finishLegacySweepMarker(marker) {
|
|
985
989
|
const retryUntil = getLegacySweepRetryUntil(marker);
|
|
986
|
-
if (retryUntil != null && Temporal$
|
|
990
|
+
if (retryUntil != null && Temporal$11.Instant.compare(this.#now(), Temporal$11.Instant.from(retryUntil)) < 0) return {
|
|
987
991
|
state: "done",
|
|
988
992
|
retryUntil
|
|
989
993
|
};
|
|
@@ -1026,7 +1030,7 @@ var CircuitBreaker = class {
|
|
|
1026
1030
|
#isLegacySweepDone() {
|
|
1027
1031
|
if (this.#legacySweepDone === "final") return true;
|
|
1028
1032
|
if (this.#legacySweepDone == null) return false;
|
|
1029
|
-
if (Temporal$
|
|
1033
|
+
if (Temporal$11.Instant.compare(this.#now(), this.#legacySweepDone) < 0) return true;
|
|
1030
1034
|
this.#legacySweepDone = void 0;
|
|
1031
1035
|
return false;
|
|
1032
1036
|
}
|
|
@@ -1036,7 +1040,7 @@ var CircuitBreaker = class {
|
|
|
1036
1040
|
}
|
|
1037
1041
|
#capHeldRetryAt(now, heldSince, retryAt) {
|
|
1038
1042
|
const expiresAt = (heldSince ?? now).add(this.#options.heldActivityTtl);
|
|
1039
|
-
return Temporal$
|
|
1043
|
+
return Temporal$11.Instant.compare(expiresAt, retryAt) < 0 ? expiresAt : retryAt;
|
|
1040
1044
|
}
|
|
1041
1045
|
async #get(remoteHost) {
|
|
1042
1046
|
return parseStoredCircuitBreakerKvState(await this.#kv.get(this.#key(remoteHost)));
|
|
@@ -1106,7 +1110,7 @@ function normalizeCircuitBreakerOptions(options) {
|
|
|
1106
1110
|
assertPositiveDuration(releaseInterval, "releaseInterval");
|
|
1107
1111
|
if (configuredStateTtl != null) {
|
|
1108
1112
|
assertPositiveDuration(configuredStateTtl, "stateTtl");
|
|
1109
|
-
if (Temporal$
|
|
1113
|
+
if (Temporal$11.Duration.compare(configuredStateTtl, recoveryDelay) < 0) throw new RangeError("stateTtl must be at least recoveryDelay.");
|
|
1110
1114
|
}
|
|
1111
1115
|
let failure;
|
|
1112
1116
|
let pruneFailures;
|
|
@@ -1118,13 +1122,13 @@ function normalizeCircuitBreakerOptions(options) {
|
|
|
1118
1122
|
assertPositiveDuration(failureWindow, "failureWindow");
|
|
1119
1123
|
pruneFailures = (timestamps, now) => {
|
|
1120
1124
|
const earliest = now.subtract(failureWindow);
|
|
1121
|
-
return timestamps.filter((timestamp) => Temporal$
|
|
1125
|
+
return timestamps.filter((timestamp) => Temporal$11.Instant.compare(timestamp, earliest) >= 0).slice(-failureThreshold);
|
|
1122
1126
|
};
|
|
1123
1127
|
failure = (timestamps) => {
|
|
1124
1128
|
if (timestamps.length < failureThreshold) return false;
|
|
1125
1129
|
const first = timestamps[timestamps.length - failureThreshold];
|
|
1126
1130
|
const last = timestamps[timestamps.length - 1];
|
|
1127
|
-
return Temporal$
|
|
1131
|
+
return Temporal$11.Duration.compare(first.until(last), failureWindow) <= 0;
|
|
1128
1132
|
};
|
|
1129
1133
|
stateTtl = configuredStateTtl ?? maxDuration(recoveryDelay.add(failureWindow), recoveryDelay.add(heldActivityTtl));
|
|
1130
1134
|
} else {
|
|
@@ -1144,7 +1148,7 @@ function normalizeCircuitBreakerOptions(options) {
|
|
|
1144
1148
|
};
|
|
1145
1149
|
}
|
|
1146
1150
|
function maxDuration(duration, ...durations) {
|
|
1147
|
-
return durations.reduce((max, candidate) => Temporal$
|
|
1151
|
+
return durations.reduce((max, candidate) => Temporal$11.Duration.compare(candidate, max) > 0 ? candidate : max, duration);
|
|
1148
1152
|
}
|
|
1149
1153
|
function isLegacySweepDone(value, now) {
|
|
1150
1154
|
return getLegacySweepDoneUntil(value, now) != null;
|
|
@@ -1153,8 +1157,8 @@ function getLegacySweepDoneUntil(value, now) {
|
|
|
1153
1157
|
if (typeof value !== "object" || value == null || !("state" in value)) return;
|
|
1154
1158
|
if (value.state === "final") return "final";
|
|
1155
1159
|
if (value.state === "done" && "retryUntil" in value && typeof value.retryUntil === "string") try {
|
|
1156
|
-
const retryUntil = Temporal$
|
|
1157
|
-
return Temporal$
|
|
1160
|
+
const retryUntil = Temporal$11.Instant.from(value.retryUntil);
|
|
1161
|
+
return Temporal$11.Instant.compare(now, retryUntil) < 0 ? retryUntil : void 0;
|
|
1158
1162
|
} catch {
|
|
1159
1163
|
return;
|
|
1160
1164
|
}
|
|
@@ -1162,7 +1166,7 @@ function getLegacySweepDoneUntil(value, now) {
|
|
|
1162
1166
|
function isLegacySweepRetryDue(value, now) {
|
|
1163
1167
|
if (typeof value !== "object" || value == null || !("state" in value) || value.state !== "done" || !("retryUntil" in value) || typeof value.retryUntil !== "string") return false;
|
|
1164
1168
|
try {
|
|
1165
|
-
return Temporal$
|
|
1169
|
+
return Temporal$11.Instant.compare(now, Temporal$11.Instant.from(value.retryUntil)) >= 0;
|
|
1166
1170
|
} catch {
|
|
1167
1171
|
return false;
|
|
1168
1172
|
}
|
|
@@ -1173,7 +1177,7 @@ function isLegacySweepActive(value, now) {
|
|
|
1173
1177
|
function isLegacySweepInProgress(value, now) {
|
|
1174
1178
|
if (typeof value !== "object" || value == null || !("state" in value) || value.state !== "sweeping" || !("started" in value) || typeof value.started !== "string") return false;
|
|
1175
1179
|
try {
|
|
1176
|
-
return Temporal$
|
|
1180
|
+
return Temporal$11.Instant.compare(now, Temporal$11.Instant.from(value.started).add(LEGACY_SWEEP_LOCK_TTL)) < 0;
|
|
1177
1181
|
} catch {
|
|
1178
1182
|
return false;
|
|
1179
1183
|
}
|
|
@@ -1181,7 +1185,7 @@ function isLegacySweepInProgress(value, now) {
|
|
|
1181
1185
|
function getLegacySweepRetryUntil(value) {
|
|
1182
1186
|
if (typeof value !== "object" || value == null || !("retryUntil" in value) || typeof value.retryUntil !== "string") return;
|
|
1183
1187
|
try {
|
|
1184
|
-
Temporal$
|
|
1188
|
+
Temporal$11.Instant.from(value.retryUntil);
|
|
1185
1189
|
return value.retryUntil;
|
|
1186
1190
|
} catch {
|
|
1187
1191
|
return;
|
|
@@ -1205,19 +1209,19 @@ function delay(milliseconds) {
|
|
|
1205
1209
|
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
1206
1210
|
}
|
|
1207
1211
|
function toInstantDuration(duration) {
|
|
1208
|
-
const parsed = Temporal$
|
|
1209
|
-
return Temporal$
|
|
1212
|
+
const parsed = Temporal$11.Duration.from(duration);
|
|
1213
|
+
return Temporal$11.Duration.from({ milliseconds: Math.trunc(parsed.total({
|
|
1210
1214
|
unit: "millisecond",
|
|
1211
|
-
relativeTo: Temporal$
|
|
1215
|
+
relativeTo: Temporal$11.PlainDateTime.from("2026-01-01T00:00:00")
|
|
1212
1216
|
})) });
|
|
1213
1217
|
}
|
|
1214
1218
|
function assertPositiveDuration(duration, name) {
|
|
1215
|
-
if (Temporal$
|
|
1219
|
+
if (Temporal$11.Duration.compare(duration, { seconds: 0 }) <= 0) throw new RangeError(`${name} must be a positive duration.`);
|
|
1216
1220
|
}
|
|
1217
1221
|
function parseHeldSince(value) {
|
|
1218
1222
|
if (value == null) return void 0;
|
|
1219
1223
|
try {
|
|
1220
|
-
return Temporal$
|
|
1224
|
+
return Temporal$11.Instant.from(value);
|
|
1221
1225
|
} catch (error) {
|
|
1222
1226
|
(0, _logtape_logtape.getLogger)([
|
|
1223
1227
|
"fedify",
|
|
@@ -1244,7 +1248,7 @@ function parseStoredCircuitBreakerKvState(value) {
|
|
|
1244
1248
|
const isInstantString = (v) => {
|
|
1245
1249
|
if (typeof v !== "string") return false;
|
|
1246
1250
|
try {
|
|
1247
|
-
Temporal$
|
|
1251
|
+
Temporal$11.Instant.from(v);
|
|
1248
1252
|
return true;
|
|
1249
1253
|
} catch {
|
|
1250
1254
|
return false;
|
|
@@ -1777,7 +1781,7 @@ async function verifyDiscoveredProofDocuments(discovered, options) {
|
|
|
1777
1781
|
verified: false,
|
|
1778
1782
|
reason: { type: "missingContext" }
|
|
1779
1783
|
});
|
|
1780
|
-
const key = await
|
|
1784
|
+
const key = await require_http.verifyMapLocalProof(document.securedDocument, options);
|
|
1781
1785
|
return key == null ? Object.freeze({
|
|
1782
1786
|
...metadata,
|
|
1783
1787
|
verified: false,
|
|
@@ -1815,7 +1819,7 @@ async function verifyCompoundPortableObjectProofs(json, limits, options = {}) {
|
|
|
1815
1819
|
const proof = proofByPath.get(object.path);
|
|
1816
1820
|
const key = proof?.verified === true ? proof.key : null;
|
|
1817
1821
|
try {
|
|
1818
|
-
const policy = await
|
|
1822
|
+
const policy = await require_http.verifyPortableObjectProofPolicy(object.document, key, options);
|
|
1819
1823
|
return policy.verified ? Object.freeze({
|
|
1820
1824
|
...metadata,
|
|
1821
1825
|
id: policy.objectId,
|
|
@@ -1845,7 +1849,7 @@ async function verifyCompoundPortableObjectProofs(json, limits, options = {}) {
|
|
|
1845
1849
|
}
|
|
1846
1850
|
//#endregion
|
|
1847
1851
|
//#region src/federation/inbox.ts
|
|
1848
|
-
const Temporal$
|
|
1852
|
+
const Temporal$10 = require_http.Temporal;
|
|
1849
1853
|
async function routeActivity({ context: ctx, json, originalJson, normalizedActivity, ldSignatureVerified, activity, recipient, inboxListeners, inboxContextFactory, listenerInboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, meterProvider, tracerProvider, idempotencyStrategy }) {
|
|
1850
1854
|
const logger = (0, _logtape_logtape.getLogger)([
|
|
1851
1855
|
"fedify",
|
|
@@ -1992,7 +1996,7 @@ async function routeActivity({ context: ctx, json, originalJson, normalizedActiv
|
|
|
1992
1996
|
return "error";
|
|
1993
1997
|
}
|
|
1994
1998
|
require_http.recordInboxActivity(meterProvider, "processed", (0, _fedify_vocab.getTypeId)(activity).href);
|
|
1995
|
-
if (cacheKey != null) await kv.set(cacheKey, true, { ttl: Temporal$
|
|
1999
|
+
if (cacheKey != null) await kv.set(cacheKey, true, { ttl: Temporal$10.Duration.from({ days: 1 }) });
|
|
1996
2000
|
logger.info("Activity {activityId} has been processed.", {
|
|
1997
2001
|
activityId: activity.id?.href,
|
|
1998
2002
|
activity: json,
|
|
@@ -2004,7 +2008,7 @@ async function routeActivity({ context: ctx, json, originalJson, normalizedActiv
|
|
|
2004
2008
|
}
|
|
2005
2009
|
//#endregion
|
|
2006
2010
|
//#region src/federation/keycache.ts
|
|
2007
|
-
const Temporal$
|
|
2011
|
+
const Temporal$9 = require_http.Temporal;
|
|
2008
2012
|
const KEY_CACHE_GENERATION = "2";
|
|
2009
2013
|
var KvKeyCache = class {
|
|
2010
2014
|
kv;
|
|
@@ -2017,8 +2021,8 @@ var KvKeyCache = class {
|
|
|
2017
2021
|
this.kv = kv;
|
|
2018
2022
|
this.prefix = prefix;
|
|
2019
2023
|
this.options = options;
|
|
2020
|
-
this.unavailableKeyTtl = options.unavailableKeyTtl ?? Temporal$
|
|
2021
|
-
this.keyTtl = options.keyTtl ?? Temporal$
|
|
2024
|
+
this.unavailableKeyTtl = options.unavailableKeyTtl ?? Temporal$9.Duration.from({ minutes: 10 });
|
|
2025
|
+
this.keyTtl = options.keyTtl ?? Temporal$9.Duration.from({ days: 30 });
|
|
2022
2026
|
this.nullKeys = /* @__PURE__ */ new Map();
|
|
2023
2027
|
}
|
|
2024
2028
|
#getFetchErrorKey(keyId) {
|
|
@@ -2038,13 +2042,13 @@ var KvKeyCache = class {
|
|
|
2038
2042
|
async get(keyId) {
|
|
2039
2043
|
const negativeExpiration = this.nullKeys.get(keyId.href);
|
|
2040
2044
|
if (negativeExpiration != null) {
|
|
2041
|
-
if (Temporal$
|
|
2045
|
+
if (Temporal$9.Now.instant().until(negativeExpiration).sign >= 0) return null;
|
|
2042
2046
|
this.nullKeys.delete(keyId.href);
|
|
2043
2047
|
}
|
|
2044
2048
|
const serialized = await this.kv.get(this.#entryKey(keyId));
|
|
2045
2049
|
if (serialized === void 0) return void 0;
|
|
2046
2050
|
if (serialized === null) {
|
|
2047
|
-
this.nullKeys.set(keyId.href, Temporal$
|
|
2051
|
+
this.nullKeys.set(keyId.href, Temporal$9.Now.instant().add(this.unavailableKeyTtl));
|
|
2048
2052
|
return null;
|
|
2049
2053
|
}
|
|
2050
2054
|
try {
|
|
@@ -2060,7 +2064,7 @@ var KvKeyCache = class {
|
|
|
2060
2064
|
}
|
|
2061
2065
|
async set(keyId, key) {
|
|
2062
2066
|
if (key == null) {
|
|
2063
|
-
this.nullKeys.set(keyId.href, Temporal$
|
|
2067
|
+
this.nullKeys.set(keyId.href, Temporal$9.Now.instant().add(this.unavailableKeyTtl));
|
|
2064
2068
|
await this.kv.set(this.#entryKey(keyId), null, { ttl: this.unavailableKeyTtl });
|
|
2065
2069
|
return;
|
|
2066
2070
|
}
|
|
@@ -2316,7 +2320,7 @@ function parseHashlinkGatewayRequest(url) {
|
|
|
2316
2320
|
}
|
|
2317
2321
|
//#endregion
|
|
2318
2322
|
//#region src/federation/temporal.ts
|
|
2319
|
-
const Temporal$
|
|
2323
|
+
const Temporal$8 = require_http.Temporal;
|
|
2320
2324
|
function isPlainObject$1(value) {
|
|
2321
2325
|
return typeof value === "object" && value != null && !Array.isArray(value);
|
|
2322
2326
|
}
|
|
@@ -2326,7 +2330,7 @@ function normalizeDateTimeLiteral(value) {
|
|
|
2326
2330
|
function isMalformedDateTimeLiteral(value) {
|
|
2327
2331
|
if (typeof value !== "string") return false;
|
|
2328
2332
|
try {
|
|
2329
|
-
Temporal$
|
|
2333
|
+
Temporal$8.Instant.from(normalizeDateTimeLiteral(value));
|
|
2330
2334
|
return false;
|
|
2331
2335
|
} catch {
|
|
2332
2336
|
return true;
|
|
@@ -2335,7 +2339,7 @@ function isMalformedDateTimeLiteral(value) {
|
|
|
2335
2339
|
function isMalformedDurationLiteral(value) {
|
|
2336
2340
|
if (typeof value !== "string") return false;
|
|
2337
2341
|
try {
|
|
2338
|
-
Temporal$
|
|
2342
|
+
Temporal$8.Duration.from(value);
|
|
2339
2343
|
return false;
|
|
2340
2344
|
} catch {
|
|
2341
2345
|
return true;
|
|
@@ -2397,7 +2401,7 @@ function hasMalformedKnownTemporalLiteralInternal(value, visited) {
|
|
|
2397
2401
|
async function hasMalformedKnownTemporalLiteral(value, contextLoader) {
|
|
2398
2402
|
try {
|
|
2399
2403
|
return hasMalformedKnownTemporalLiteralInternal(await _fedify_vocab_runtime_jsonld.default.expand(value, {
|
|
2400
|
-
documentLoader:
|
|
2404
|
+
documentLoader: require_http.getNormalizationContextLoader(contextLoader),
|
|
2401
2405
|
keepFreeFloatingNodes: true
|
|
2402
2406
|
}), /* @__PURE__ */ new Set());
|
|
2403
2407
|
} catch {
|
|
@@ -2406,7 +2410,7 @@ async function hasMalformedKnownTemporalLiteral(value, contextLoader) {
|
|
|
2406
2410
|
}
|
|
2407
2411
|
//#endregion
|
|
2408
2412
|
//#region src/federation/handler.ts
|
|
2409
|
-
const Temporal$
|
|
2413
|
+
const Temporal$7 = require_http.Temporal;
|
|
2410
2414
|
const rawInboxContextFactorySymbol = Symbol("fedify.rawInboxContextFactory");
|
|
2411
2415
|
const INBOX_COMPOUND_PROOF_LIMITS = {
|
|
2412
2416
|
maxDepth: 64,
|
|
@@ -2421,15 +2425,15 @@ function isPermanentRemoteContextError$1(error) {
|
|
|
2421
2425
|
if (!(error instanceof Error) || error.name !== "jsonld.InvalidUrl") return false;
|
|
2422
2426
|
const details = error.details;
|
|
2423
2427
|
if (details?.code === "invalid remote context") return true;
|
|
2424
|
-
return isRemoteContextLoadingFailure$1(error) && typeof details?.url === "string" && !URL.canParse(details.url) &&
|
|
2428
|
+
return isRemoteContextLoadingFailure$1(error) && typeof details?.url === "string" && !URL.canParse(details.url) && require_http.isClearlyMalformedContextReference(details.url);
|
|
2425
2429
|
}
|
|
2426
2430
|
function isInvalidJsonLdError(error) {
|
|
2427
2431
|
if (!(error instanceof Error)) return false;
|
|
2428
2432
|
const name = error.name;
|
|
2429
|
-
return name === "UnsafeJsonLdError" || error instanceof
|
|
2433
|
+
return name === "UnsafeJsonLdError" || error instanceof require_http.InvalidContextReferenceError || isPermanentRemoteContextError$1(error) || name === "jsonld.SyntaxError" && !isRemoteContextLoadingFailure$1(error);
|
|
2430
2434
|
}
|
|
2431
2435
|
function isValidationTypeError(error) {
|
|
2432
|
-
return error instanceof TypeError && (/^(Invalid JSON-LD:|Invalid type:|Unexpected type:|Invalid @id:)/.test(error.message) ||
|
|
2436
|
+
return error instanceof TypeError && (/^(Invalid JSON-LD:|Invalid type:|Unexpected type:|Invalid @id:)/.test(error.message) || require_http.isInvalidUrlTypeError(error));
|
|
2433
2437
|
}
|
|
2434
2438
|
function isPermanentActivityParseError(error) {
|
|
2435
2439
|
return isInvalidJsonLdError(error) || isValidationTypeError(error);
|
|
@@ -2535,7 +2539,7 @@ async function handlePortableObject(request, { values, context, objectDispatcher
|
|
|
2535
2539
|
try {
|
|
2536
2540
|
jsonLd = await object.toJsonLd(context);
|
|
2537
2541
|
servedId = await getJsonLdRootId(jsonLd, context.contextLoader);
|
|
2538
|
-
result = await
|
|
2542
|
+
result = await require_http.verifyPortableObjectProof(jsonLd, {
|
|
2539
2543
|
contextLoader: context.contextLoader,
|
|
2540
2544
|
documentLoader: context.documentLoader,
|
|
2541
2545
|
tracerProvider: context.tracerProvider,
|
|
@@ -2576,7 +2580,7 @@ function isRequestedPortableObject(object, canonicalId) {
|
|
|
2576
2580
|
}
|
|
2577
2581
|
async function getJsonLdRootId(jsonLd, contextLoader) {
|
|
2578
2582
|
const expanded = await _fedify_vocab_runtime_jsonld.default.expand(jsonLd, {
|
|
2579
|
-
documentLoader:
|
|
2583
|
+
documentLoader: require_http.getNormalizationContextLoader(contextLoader),
|
|
2580
2584
|
keepFreeFloatingNodes: true
|
|
2581
2585
|
});
|
|
2582
2586
|
if (expanded.length !== 1) return null;
|
|
@@ -3243,7 +3247,7 @@ async function handleInbox(request, options) {
|
|
|
3243
3247
|
* @returns A promise that resolves to an HTTP response.
|
|
3244
3248
|
*/
|
|
3245
3249
|
async function handleInboxInternal(request, parameters, span) {
|
|
3246
|
-
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;
|
|
3247
3251
|
const logger = (0, _logtape_logtape.getLogger)([
|
|
3248
3252
|
"fedify",
|
|
3249
3253
|
"federation",
|
|
@@ -3256,7 +3260,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
3256
3260
|
message: "Actor dispatcher is not set."
|
|
3257
3261
|
});
|
|
3258
3262
|
return await onNotFound(request);
|
|
3259
|
-
} else if (recipient != null) {
|
|
3263
|
+
} else if (recipient != null && portableInbox == null) {
|
|
3260
3264
|
const actor = await actorDispatcher(ctx, recipient);
|
|
3261
3265
|
if (actor == null || actor instanceof _fedify_vocab.Tombstone) {
|
|
3262
3266
|
logger.error("Actor {recipient} not found.", { recipient });
|
|
@@ -3328,8 +3332,8 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
3328
3332
|
tracerProvider,
|
|
3329
3333
|
keyTtl: publicKeyTtl
|
|
3330
3334
|
});
|
|
3331
|
-
const jsonWithoutSig =
|
|
3332
|
-
const hasLdSignature =
|
|
3335
|
+
const jsonWithoutSig = require_http.detachSignature(json);
|
|
3336
|
+
const hasLdSignature = require_http.hasSignature(json);
|
|
3333
3337
|
const canAttemptAlternateAuthAfterLdSignatureFailure = skipSignatureVerification || hasHttpSignatureHeaders(request) || hasObjectIntegrityProof(jsonWithoutSig);
|
|
3334
3338
|
let deferredLdSignatureError = void 0;
|
|
3335
3339
|
const respondInvalidActivity = async (error) => {
|
|
@@ -3361,7 +3365,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
3361
3365
|
let ldSigVerified = false;
|
|
3362
3366
|
if (hasLdSignature) {
|
|
3363
3367
|
try {
|
|
3364
|
-
compactedJson = await
|
|
3368
|
+
compactedJson = await require_http.compactJsonLd(json, ctx.contextLoader);
|
|
3365
3369
|
} catch (error) {
|
|
3366
3370
|
if (isInvalidJsonLdError(error)) {
|
|
3367
3371
|
logger.error("Failed to parse JSON-LD:\n{error}", {
|
|
@@ -3381,9 +3385,9 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
3381
3385
|
});
|
|
3382
3386
|
}
|
|
3383
3387
|
if (compactedJson !== json) {
|
|
3384
|
-
compactedJsonWithoutSig =
|
|
3388
|
+
compactedJsonWithoutSig = require_http.detachSignature(compactedJson);
|
|
3385
3389
|
try {
|
|
3386
|
-
ldSigVerified = await
|
|
3390
|
+
ldSigVerified = await require_http.verifyCompactJsonLd(compactedJson, {
|
|
3387
3391
|
contextLoader: ctx.contextLoader,
|
|
3388
3392
|
documentLoader: ctx.documentLoader,
|
|
3389
3393
|
keyCache,
|
|
@@ -3405,7 +3409,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
3405
3409
|
if (!canAttemptAlternateAuthAfterLdSignatureFailure) throw error;
|
|
3406
3410
|
if (!skipSignatureVerification) try {
|
|
3407
3411
|
await _fedify_vocab.Object.fromJsonLd(compactedJson, {
|
|
3408
|
-
contextLoader:
|
|
3412
|
+
contextLoader: require_http.getNormalizationContextLoader(ctx.contextLoader),
|
|
3409
3413
|
documentLoader: ctx.documentLoader,
|
|
3410
3414
|
tracerProvider
|
|
3411
3415
|
});
|
|
@@ -3429,6 +3433,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
3429
3433
|
}
|
|
3430
3434
|
let activity = null;
|
|
3431
3435
|
let activityVerified = false;
|
|
3436
|
+
let proofVerified = false;
|
|
3432
3437
|
if (ldSigVerified) {
|
|
3433
3438
|
logger.debug("Linked Data Signatures are verified.", {
|
|
3434
3439
|
recipient,
|
|
@@ -3437,7 +3442,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
3437
3442
|
try {
|
|
3438
3443
|
activity = await _fedify_vocab.Activity.fromJsonLd(compactedJsonWithoutSig, {
|
|
3439
3444
|
...ctx,
|
|
3440
|
-
contextLoader:
|
|
3445
|
+
contextLoader: require_http.getNormalizationContextLoader(ctx.contextLoader)
|
|
3441
3446
|
});
|
|
3442
3447
|
} catch (error) {
|
|
3443
3448
|
if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig, ctx.contextLoader)) return await respondInvalidActivity(error);
|
|
@@ -3445,14 +3450,29 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
3445
3450
|
return await respondInvalidActivity(error);
|
|
3446
3451
|
}
|
|
3447
3452
|
activityVerified = true;
|
|
3453
|
+
if (!skipSignatureVerification && require_http.hasPortableActor(activity)) try {
|
|
3454
|
+
proofVerified = await require_http.verifyObject(_fedify_vocab.Activity, jsonWithoutSig, {
|
|
3455
|
+
contextLoader: require_http.wrapContextLoaderForJsonLd(ctx.contextLoader),
|
|
3456
|
+
documentLoader: ctx.documentLoader,
|
|
3457
|
+
keyCache,
|
|
3458
|
+
meterProvider,
|
|
3459
|
+
tracerProvider
|
|
3460
|
+
}) != null;
|
|
3461
|
+
} catch (error) {
|
|
3462
|
+
if (!isPermanentActivityParseError(error)) throw error;
|
|
3463
|
+
logger.debug("Failed to verify the Object Integrity Proofs of the portable actor's activity:\n{error}", {
|
|
3464
|
+
recipient,
|
|
3465
|
+
error
|
|
3466
|
+
});
|
|
3467
|
+
}
|
|
3448
3468
|
} else {
|
|
3449
3469
|
logger.debug("Linked Data Signatures are not verified.", {
|
|
3450
3470
|
recipient,
|
|
3451
3471
|
json
|
|
3452
3472
|
});
|
|
3453
3473
|
try {
|
|
3454
|
-
activity = await
|
|
3455
|
-
contextLoader:
|
|
3474
|
+
activity = await require_http.verifyObject(_fedify_vocab.Activity, jsonWithoutSig, {
|
|
3475
|
+
contextLoader: require_http.wrapContextLoaderForJsonLd(ctx.contextLoader),
|
|
3456
3476
|
documentLoader: ctx.documentLoader,
|
|
3457
3477
|
keyCache,
|
|
3458
3478
|
meterProvider,
|
|
@@ -3501,6 +3521,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
3501
3521
|
activity: json
|
|
3502
3522
|
});
|
|
3503
3523
|
activityVerified = true;
|
|
3524
|
+
proofVerified = true;
|
|
3504
3525
|
}
|
|
3505
3526
|
}
|
|
3506
3527
|
let httpSigKey = null;
|
|
@@ -3597,7 +3618,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
3597
3618
|
try {
|
|
3598
3619
|
activity = await _fedify_vocab.Activity.fromJsonLd(jsonWithoutSig, {
|
|
3599
3620
|
...ctx,
|
|
3600
|
-
contextLoader:
|
|
3621
|
+
contextLoader: require_http.wrapContextLoaderForJsonLd(ctx.contextLoader)
|
|
3601
3622
|
});
|
|
3602
3623
|
} catch (error) {
|
|
3603
3624
|
if (!isPermanentActivityParseError(error)) throw error;
|
|
@@ -3613,6 +3634,24 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
3613
3634
|
"http_signatures.verified": httpSigKey != null,
|
|
3614
3635
|
"http_signatures.key_id": httpSigKey?.id?.href ?? ""
|
|
3615
3636
|
});
|
|
3637
|
+
if (!skipSignatureVerification && !proofVerified && (require_http.hasPortableActor(activity) || isPortableGatewayKey(httpSigKey))) {
|
|
3638
|
+
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
3639
|
+
logger.error("The activity {activityId} of the portable actor {actorId} is not authenticated by a valid Object Integrity Proof.", {
|
|
3640
|
+
activity: json,
|
|
3641
|
+
recipient,
|
|
3642
|
+
activityId: activity.id?.href,
|
|
3643
|
+
actorId: activity.actorId?.href,
|
|
3644
|
+
keyId: httpSigKey?.id?.href
|
|
3645
|
+
});
|
|
3646
|
+
span.setStatus({
|
|
3647
|
+
code: _opentelemetry_api.SpanStatusCode.ERROR,
|
|
3648
|
+
message: `The activity of the portable actor (${activity.actorId?.href}) is not authenticated by a valid Object Integrity Proof.`
|
|
3649
|
+
});
|
|
3650
|
+
return new Response("Activities of portable actors must have valid Object Integrity Proofs.", {
|
|
3651
|
+
status: 401,
|
|
3652
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
3653
|
+
});
|
|
3654
|
+
}
|
|
3616
3655
|
if (httpSigKey != null && !await require_owner.doesActorOwnKey(activity, httpSigKey, ctx)) {
|
|
3617
3656
|
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
3618
3657
|
require_http.getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("actorKeyMismatch", httpSigKey.id == null ? void 0 : require_http.getRemoteHost(httpSigKey.id));
|
|
@@ -3683,6 +3722,19 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
3683
3722
|
tracerProvider,
|
|
3684
3723
|
idempotencyStrategy: parameters.idempotencyStrategy
|
|
3685
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
|
+
}
|
|
3686
3738
|
if (routeResult === "alreadyProcessed") return new Response(`Activity <${activity.id}> has already been processed.`, {
|
|
3687
3739
|
status: 202,
|
|
3688
3740
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
@@ -3708,6 +3760,12 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
3708
3760
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
3709
3761
|
});
|
|
3710
3762
|
}
|
|
3763
|
+
const PORTABLE_INBOX_FORWARDABLE_RESULTS = /* @__PURE__ */ new Set([
|
|
3764
|
+
"success",
|
|
3765
|
+
"enqueued",
|
|
3766
|
+
"alreadyProcessed",
|
|
3767
|
+
"unsupportedActivity"
|
|
3768
|
+
]);
|
|
3711
3769
|
/**
|
|
3712
3770
|
* Handles a custom collection request.
|
|
3713
3771
|
* @template TItem The type of items in the collection.
|
|
@@ -4301,7 +4359,7 @@ async function buildAcceptSignatureHeader(policy, kv, noncePrefix) {
|
|
|
4301
4359
|
}
|
|
4302
4360
|
async function setKey(kv, key, policy) {
|
|
4303
4361
|
const seconds = policy.nonceTtlSeconds ?? 300;
|
|
4304
|
-
const ttl = Temporal$
|
|
4362
|
+
const ttl = Temporal$7.Duration.from({ seconds });
|
|
4305
4363
|
await kv.set(key, true, { ttl });
|
|
4306
4364
|
}
|
|
4307
4365
|
const DEF_COMPONENTS = [
|
|
@@ -4315,9 +4373,18 @@ const MIN_COMPONENTS = [
|
|
|
4315
4373
|
"@target-uri",
|
|
4316
4374
|
"@authority"
|
|
4317
4375
|
];
|
|
4376
|
+
/**
|
|
4377
|
+
* Checks whether an HTTP Signature key is a gateway key of an FEP-ef61
|
|
4378
|
+
* portable actor, i.e., its ID is a compatible identifier and it is owned by
|
|
4379
|
+
* a portable actor, which only the portable actor's signed document can
|
|
4380
|
+
* establish.
|
|
4381
|
+
*/
|
|
4382
|
+
function isPortableGatewayKey(key) {
|
|
4383
|
+
return key?.id != null && require_http.isCompatibleKeyId(key.id) && key.ownerId != null && require_http.isPortableUri(key.ownerId);
|
|
4384
|
+
}
|
|
4318
4385
|
//#endregion
|
|
4319
4386
|
//#region src/federation/kv.ts
|
|
4320
|
-
const Temporal$
|
|
4387
|
+
const Temporal$6 = require_http.Temporal;
|
|
4321
4388
|
/**
|
|
4322
4389
|
* A key–value store that stores values in memory.
|
|
4323
4390
|
* Do not use this in production as it does not persist values.
|
|
@@ -4337,7 +4404,7 @@ var MemoryKvStore = class {
|
|
|
4337
4404
|
const entry = this.#values[encodedKey];
|
|
4338
4405
|
if (entry == null) return Promise.resolve(void 0);
|
|
4339
4406
|
const [value, expiration] = entry;
|
|
4340
|
-
if (expiration != null && Temporal$
|
|
4407
|
+
if (expiration != null && Temporal$6.Now.instant().until(expiration).sign < 0) {
|
|
4341
4408
|
delete this.#values[encodedKey];
|
|
4342
4409
|
return Promise.resolve(void 0);
|
|
4343
4410
|
}
|
|
@@ -4348,7 +4415,7 @@ var MemoryKvStore = class {
|
|
|
4348
4415
|
*/
|
|
4349
4416
|
set(key, value, options) {
|
|
4350
4417
|
const encodedKey = this.#encodeKey(key);
|
|
4351
|
-
const expiration = options?.ttl == null ? null : Temporal$
|
|
4418
|
+
const expiration = options?.ttl == null ? null : Temporal$6.Now.instant().add(options.ttl.round({ largestUnit: "hour" }));
|
|
4352
4419
|
this.#values[encodedKey] = [value, expiration];
|
|
4353
4420
|
return Promise.resolve();
|
|
4354
4421
|
}
|
|
@@ -4370,13 +4437,13 @@ var MemoryKvStore = class {
|
|
|
4370
4437
|
if (entry == null) currentValue = void 0;
|
|
4371
4438
|
else {
|
|
4372
4439
|
const [value, expiration] = entry;
|
|
4373
|
-
if (expiration != null && Temporal$
|
|
4440
|
+
if (expiration != null && Temporal$6.Now.instant().until(expiration).sign < 0) {
|
|
4374
4441
|
delete this.#values[encodedKey];
|
|
4375
4442
|
currentValue = void 0;
|
|
4376
4443
|
} else currentValue = value;
|
|
4377
4444
|
}
|
|
4378
4445
|
if (!(0, es_toolkit.isEqual)(currentValue, expectedValue)) return Promise.resolve(false);
|
|
4379
|
-
const expiration = options?.ttl == null ? null : Temporal$
|
|
4446
|
+
const expiration = options?.ttl == null ? null : Temporal$6.Now.instant().add(options.ttl.round({ largestUnit: "hour" }));
|
|
4380
4447
|
this.#values[encodedKey] = [newValue, expiration];
|
|
4381
4448
|
return Promise.resolve(true);
|
|
4382
4449
|
}
|
|
@@ -4384,7 +4451,7 @@ var MemoryKvStore = class {
|
|
|
4384
4451
|
* {@inheritDoc KvStore.list}
|
|
4385
4452
|
*/
|
|
4386
4453
|
async *list(prefix) {
|
|
4387
|
-
const now = Temporal$
|
|
4454
|
+
const now = Temporal$6.Now.instant();
|
|
4388
4455
|
for (const [encodedKey, entry] of Object.entries(this.#values)) {
|
|
4389
4456
|
const key = JSON.parse(encodedKey);
|
|
4390
4457
|
if (prefix != null) {
|
|
@@ -4578,7 +4645,11 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
|
|
|
4578
4645
|
rsaKey = key;
|
|
4579
4646
|
break;
|
|
4580
4647
|
}
|
|
4581
|
-
if (rsaKey == null) logger.
|
|
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.", {
|
|
4582
4653
|
inbox: inbox.href,
|
|
4583
4654
|
keys: keys.map((pair) => ({
|
|
4584
4655
|
keyId: pair.keyId.href,
|
|
@@ -4920,6 +4991,252 @@ function serializeDataPointType(dataPointType) {
|
|
|
4920
4991
|
}
|
|
4921
4992
|
}
|
|
4922
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
|
|
4923
5240
|
//#region src/federation/outgoing-proof.ts
|
|
4924
5241
|
function isPortableUrl(url) {
|
|
4925
5242
|
return url.protocol === "ap:" || url.protocol === "ap+ef61:";
|
|
@@ -4928,39 +5245,50 @@ function formatKeyIds(candidates) {
|
|
|
4928
5245
|
return candidates.map((c) => c.verificationMethod.href).join(", ");
|
|
4929
5246
|
}
|
|
4930
5247
|
/**
|
|
5248
|
+
* Rejects an activity of a portable actor whose ID cannot be authenticated by
|
|
5249
|
+
* the actor's DID: FEP-ef61 requires the activity to be a portable object
|
|
5250
|
+
* signed by that DID, so its ID has to be a portable ID with the same DID.
|
|
5251
|
+
* @throws {TypeError} If the activity is performed by a portable actor, but
|
|
5252
|
+
* its ID is not a portable ID of the actor's DID, or its
|
|
5253
|
+
* portable actors do not share a DID.
|
|
5254
|
+
* @internal
|
|
5255
|
+
*/
|
|
5256
|
+
function assertPortableActorActivity(activity) {
|
|
5257
|
+
const portableActors = activity.actorIds.filter(isPortableUrl);
|
|
5258
|
+
if (portableActors.length < 1) return;
|
|
5259
|
+
const did = (0, _fedify_vocab_runtime.getFe34Origin)(portableActors[0]);
|
|
5260
|
+
if (portableActors.some((actorId) => (0, _fedify_vocab_runtime.getFe34Origin)(actorId) !== did)) throw new TypeError(`The activity ${activity.id?.href} has portable actors with different DIDs (${portableActors.map((a) => a.href).join(", ")}); a portable activity is signed by a single DID.`);
|
|
5261
|
+
if (activity.id == null || !isPortableUrl(activity.id) || (0, _fedify_vocab_runtime.getFe34Origin)(activity.id) !== did) throw new TypeError(`The activity ${activity.id?.href} is performed by the portable actor ${portableActors[0].href}, so its ID has to be an ap: or ap+ef61: URI with the same DID (${did}), as FEP-ef61 requires it to carry an Object Integrity Proof made by that DID.`);
|
|
5262
|
+
}
|
|
5263
|
+
/**
|
|
4931
5264
|
* Creates the Object Integrity Proofs Fedify attaches to an outgoing activity.
|
|
4932
5265
|
*
|
|
5266
|
+
* Keys whose IDs are FEP-ef61 compatible identifiers are gateway keys, which
|
|
5267
|
+
* never sign proofs. A portable activity, i.e., one with an `ap:` or
|
|
5268
|
+
* `ap+ef61:` ID, that carries no proof is signed only by the one Ed25519 key
|
|
5269
|
+
* whose verification method is a DID URL for its DID, even if it is the only
|
|
5270
|
+
* key; if no key or more than one key qualifies, this function throws.
|
|
5271
|
+
*
|
|
4933
5272
|
* Outside the map-local compound-proof profile, i.e., when the activity
|
|
4934
5273
|
* embeds no portable object, every Ed25519 key signs the activity, as it
|
|
4935
5274
|
* always has. Inside the profile, which accepts exactly one direct proof per
|
|
4936
5275
|
* map, Fedify creates at most one proof:
|
|
4937
5276
|
*
|
|
4938
5277
|
* - An activity that already carries a proof is kept as is.
|
|
4939
|
-
* - A single Ed25519 key signs
|
|
4940
|
-
* - With several Ed25519 keys, a portable activity is
|
|
4941
|
-
*
|
|
4942
|
-
* proof the FEP-ef61 policy can accept. If no key or more than one key
|
|
4943
|
-
* qualifies, or the activity is not portable itself, the choice is
|
|
4944
|
-
* ambiguous and this function throws.
|
|
5278
|
+
* - A single Ed25519 key signs a non-portable activity.
|
|
5279
|
+
* - With several Ed25519 keys, a non-portable activity is ambiguous, and
|
|
5280
|
+
* this function throws.
|
|
4945
5281
|
*
|
|
4946
|
-
* @throws {TypeError} If a single proof key cannot be chosen
|
|
5282
|
+
* @throws {TypeError} If a single proof key cannot be chosen, or the activity
|
|
5283
|
+
* is performed by a portable actor but is not a portable
|
|
5284
|
+
* activity of the same DID.
|
|
4947
5285
|
* @internal
|
|
4948
5286
|
*/
|
|
4949
5287
|
async function signOutgoingActivity(activity, candidates, options) {
|
|
4950
5288
|
const { contextLoader, tracerProvider } = options;
|
|
4951
|
-
|
|
4952
|
-
for await (const _ of activity.getProofs({ contextLoader })) {
|
|
4953
|
-
hasProof = true;
|
|
4954
|
-
break;
|
|
4955
|
-
}
|
|
4956
|
-
const keys = candidates.filter((c) => c.privateKey.algorithm.name === "Ed25519");
|
|
4957
|
-
if (keys.length < 1 || hasProof && !options.appendToExistingProofs) return {
|
|
4958
|
-
activity,
|
|
4959
|
-
hasProof,
|
|
4960
|
-
proofCreated: false
|
|
4961
|
-
};
|
|
5289
|
+
assertPortableActorActivity(activity);
|
|
4962
5290
|
const sign = async (selected) => {
|
|
4963
|
-
for (const { verificationMethod, privateKey } of selected) activity = await
|
|
5291
|
+
for (const { verificationMethod, privateKey } of selected) activity = await require_http.signObject(activity, privateKey, verificationMethod, {
|
|
4964
5292
|
contextLoader,
|
|
4965
5293
|
tracerProvider
|
|
4966
5294
|
});
|
|
@@ -4970,6 +5298,18 @@ async function signOutgoingActivity(activity, candidates, options) {
|
|
|
4970
5298
|
proofCreated: true
|
|
4971
5299
|
};
|
|
4972
5300
|
};
|
|
5301
|
+
let hasProof = false;
|
|
5302
|
+
for await (const _ of activity.getProofs({ contextLoader })) {
|
|
5303
|
+
hasProof = true;
|
|
5304
|
+
break;
|
|
5305
|
+
}
|
|
5306
|
+
const keys = candidates.filter((c) => c.privateKey.algorithm.name === "Ed25519" && !require_http.isCompatibleKeyId(c.verificationMethod));
|
|
5307
|
+
if (!hasProof && activity.id != null && isPortableUrl(activity.id)) return await sign(selectPortableActivityKey(activity, activity.id, keys));
|
|
5308
|
+
if (keys.length < 1 || hasProof && !options.appendToExistingProofs) return {
|
|
5309
|
+
activity,
|
|
5310
|
+
hasProof,
|
|
5311
|
+
proofCreated: false
|
|
5312
|
+
};
|
|
4973
5313
|
if (keys.length < 2 && !hasProof) return await sign(keys);
|
|
4974
5314
|
if (!containsCompoundPortableObject(await activity.toJsonLd({
|
|
4975
5315
|
format: "compact",
|
|
@@ -4989,15 +5329,26 @@ async function signOutgoingActivity(activity, candidates, options) {
|
|
|
4989
5329
|
proofCreated: false
|
|
4990
5330
|
};
|
|
4991
5331
|
}
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
5332
|
+
throw new TypeError(`Cannot choose an Object Integrity Proof key for the activity ${activityId}: it embeds portable objects, so it falls under the map-local compound-proof profile, which accepts exactly one direct proof per map, but ${keys.length} Ed25519 keys were supplied (${formatKeyIds(keys)}). Send it with explicit sender keys that contain exactly one Ed25519 key (RSA keys for HTTP Signatures may stay), or sign it with signObject() before sending it.`);
|
|
5333
|
+
}
|
|
5334
|
+
/**
|
|
5335
|
+
* Chooses the one Ed25519 key that can sign a portable activity: the key
|
|
5336
|
+
* whose verification method is a DID URL for the activity's DID.
|
|
5337
|
+
* @throws {TypeError} If no key or more than one key qualifies.
|
|
5338
|
+
*/
|
|
5339
|
+
function selectPortableActivityKey(activity, activityId, keys) {
|
|
5340
|
+
const activityDid = (0, _fedify_vocab_runtime.getFe34Origin)(activityId);
|
|
5341
|
+
const eligible = keys.filter((c) => c.verificationMethod.protocol === "did:" && (0, _fedify_vocab_runtime.haveSameFe34Origin)(activityId, c.verificationMethod));
|
|
5342
|
+
if (eligible.length !== 1) throw new TypeError(`Cannot choose an Object Integrity Proof key for the portable activity ${activityId.href}: ` + (eligible.length < 1 ? `none of its ${keys.length} Ed25519 keys (${formatKeyIds(keys)}) ` : `${eligible.length} of its Ed25519 keys (${formatKeyIds(eligible)}) `) + `${eligible.length < 1 ? "is" : "are"} a DID URL verification method for ${activityDid}. FEP-ef61 requires a portable activity's proof to be made by its DID, and the map-local compound-proof profile accepts exactly one direct proof per map. Gateway keys, whose IDs are compatible identifiers, never sign proofs. Supply exactly one Ed25519 key whose key ID is a DID URL for ${activityDid}, or sign the activity with signObject() before sending it.`);
|
|
5343
|
+
(0, _logtape_logtape.getLogger)([
|
|
5344
|
+
"fedify",
|
|
5345
|
+
"federation",
|
|
5346
|
+
"outbox"
|
|
5347
|
+
]).debug("The portable activity {activityId} is signed only by {keyId}, the one Ed25519 key whose verification method matches its DID.", {
|
|
5348
|
+
activityId: activity.id?.href,
|
|
4998
5349
|
keyId: eligible[0].verificationMethod.href
|
|
4999
5350
|
});
|
|
5000
|
-
return
|
|
5351
|
+
return eligible;
|
|
5001
5352
|
}
|
|
5002
5353
|
/**
|
|
5003
5354
|
* Rejects an outgoing activity whose proofs Fedify's own inbox would reject
|
|
@@ -5758,10 +6109,10 @@ function isPermanentRemoteContextError(error) {
|
|
|
5758
6109
|
if (!(error instanceof Error) || error.name !== "jsonld.InvalidUrl") return false;
|
|
5759
6110
|
const details = error.details;
|
|
5760
6111
|
if (details?.code === "invalid remote context") return true;
|
|
5761
|
-
return isRemoteContextLoadingFailure(error) && typeof details?.url === "string" && !URL.canParse(details.url) &&
|
|
6112
|
+
return isRemoteContextLoadingFailure(error) && typeof details?.url === "string" && !URL.canParse(details.url) && require_http.isClearlyMalformedContextReference(details.url);
|
|
5762
6113
|
}
|
|
5763
6114
|
function isPermanentInboxParseError(error) {
|
|
5764
|
-
return error instanceof Error && (error.name === "UnsafeJsonLdError" || error instanceof
|
|
6115
|
+
return error instanceof Error && (error.name === "UnsafeJsonLdError" || error instanceof require_http.InvalidContextReferenceError || isPermanentRemoteContextError(error) || error.name === "jsonld.SyntaxError" && !isRemoteContextLoadingFailure(error)) || error instanceof TypeError && (/^(Invalid JSON-LD:|Invalid type:|Unexpected type:|Invalid @id:)/.test(error.message) || require_http.isInvalidUrlTypeError(error));
|
|
5765
6116
|
}
|
|
5766
6117
|
function hasLinkedDataSignatureJsonLdProcessingError(error) {
|
|
5767
6118
|
if (error.message.startsWith("Maximum deep iterations exceeded")) return true;
|
|
@@ -5788,6 +6139,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
5788
6139
|
kvPrefixes;
|
|
5789
6140
|
publicKeyTtl;
|
|
5790
6141
|
httpMessageSignaturesSpecTtl;
|
|
6142
|
+
portableInboxForwarding;
|
|
5791
6143
|
inboxQueue;
|
|
5792
6144
|
outboxQueue;
|
|
5793
6145
|
fanoutQueue;
|
|
@@ -5820,6 +6172,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
5820
6172
|
benchmarkMetricReader;
|
|
5821
6173
|
benchmarkTriggerOptions;
|
|
5822
6174
|
#mediaUploaderNoAuthWarned = false;
|
|
6175
|
+
#portableInboxForwardingWarned = false;
|
|
5823
6176
|
#queueDepthGaugeSourceId = `fedify-${(++nextQueueDepthGaugeSourceId).toString(36)}`;
|
|
5824
6177
|
#queueDepthGaugeEntries = [];
|
|
5825
6178
|
#queueDepthGaugeMeterProvider;
|
|
@@ -5851,10 +6204,12 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
5851
6204
|
acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"],
|
|
5852
6205
|
circuitBreaker: ["_fedify", "circuit"],
|
|
5853
6206
|
taskDeduplication: ["_fedify", "taskDeduplication"],
|
|
6207
|
+
portableInboxForwarding: ["_fedify", "portableInboxForwarding"],
|
|
5854
6208
|
...options.kvPrefixes ?? {}
|
|
5855
6209
|
};
|
|
5856
6210
|
this.publicKeyTtl = Temporal$2.Duration.from(options.publicKeyTtl ?? { days: 30 });
|
|
5857
6211
|
this.httpMessageSignaturesSpecTtl = Temporal$2.Duration.from(options.httpMessageSignaturesSpecTtl ?? { days: 90 });
|
|
6212
|
+
this.portableInboxForwarding = resolvePortableInboxForwardingOptions(options.portableInboxForwarding);
|
|
5858
6213
|
if (options.queue == null) {
|
|
5859
6214
|
this.inboxQueue = void 0;
|
|
5860
6215
|
this.outboxQueue = void 0;
|
|
@@ -6524,8 +6879,8 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
6524
6879
|
]);
|
|
6525
6880
|
const baseUrl = new URL(message.baseUrl);
|
|
6526
6881
|
let context = this.#createContext(baseUrl, ctxData);
|
|
6527
|
-
if (message.identifier != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: await context.getDocumentLoader({ identifier: message.identifier }) });
|
|
6528
|
-
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) {
|
|
6529
6884
|
const identity = await this.sharedInboxKeyDispatcher(context);
|
|
6530
6885
|
if (identity != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
|
|
6531
6886
|
}
|
|
@@ -6614,22 +6969,22 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
6614
6969
|
let parseInput = void 0;
|
|
6615
6970
|
let parseContextLoader = context.contextLoader;
|
|
6616
6971
|
try {
|
|
6617
|
-
const hasSignatureField =
|
|
6972
|
+
const hasSignatureField = require_http.hasSignature(message.activity);
|
|
6618
6973
|
const shouldParseFromNormalizedSignedPayload = message.ldSignatureVerified === true || message.normalizedActivity != null || message.ldSignatureVerified == null && hasSignatureField;
|
|
6619
6974
|
const parseContext = hasSignatureField ? {
|
|
6620
6975
|
...context,
|
|
6621
|
-
contextLoader:
|
|
6976
|
+
contextLoader: require_http.getNormalizationContextLoader(context.contextLoader)
|
|
6622
6977
|
} : {
|
|
6623
6978
|
...context,
|
|
6624
|
-
contextLoader:
|
|
6979
|
+
contextLoader: require_http.wrapContextLoaderForJsonLd(context.contextLoader)
|
|
6625
6980
|
};
|
|
6626
6981
|
parseContextLoader = parseContext.contextLoader;
|
|
6627
6982
|
let normalizedActivity;
|
|
6628
6983
|
if (shouldParseFromNormalizedSignedPayload) {
|
|
6629
|
-
normalizedActivity = message.normalizedActivity ?? await
|
|
6630
|
-
|
|
6984
|
+
normalizedActivity = message.normalizedActivity ?? await require_http.compactJsonLd(message.activity, context.contextLoader);
|
|
6985
|
+
require_http.assertSafeJsonLd(normalizedActivity);
|
|
6631
6986
|
}
|
|
6632
|
-
parseInput = shouldParseFromNormalizedSignedPayload ?
|
|
6987
|
+
parseInput = shouldParseFromNormalizedSignedPayload ? require_http.detachSignature(normalizedActivity) : hasSignatureField ? require_http.detachSignature(message.activity) : message.activity;
|
|
6633
6988
|
activity = await _fedify_vocab.Activity.fromJsonLd(parseInput, parseContext);
|
|
6634
6989
|
activityType = (0, _fedify_vocab.getTypeId)(activity).href;
|
|
6635
6990
|
span.setAttribute("activitypub.activity.type", activityType);
|
|
@@ -6884,9 +7239,10 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
6884
7239
|
const contextLoader = this.contextLoaderFactory(this.#getLoaderOptions(ctx.origin));
|
|
6885
7240
|
const activityId = activity.id.href;
|
|
6886
7241
|
let rsaKey = null;
|
|
7242
|
+
const ldSignable = !require_http.hasPortableActor(activity);
|
|
6887
7243
|
for (const { keyId, privateKey } of keys) {
|
|
6888
7244
|
require_http.validateCryptoKey(privateKey, "private");
|
|
6889
|
-
if (rsaKey == null && privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") rsaKey = {
|
|
7245
|
+
if (ldSignable && rsaKey == null && privateKey.algorithm.name === "RSASSA-PKCS1-v1_5" && !require_http.isCompatibleKeyId(keyId)) rsaKey = {
|
|
6890
7246
|
keyId,
|
|
6891
7247
|
privateKey
|
|
6892
7248
|
};
|
|
@@ -6905,9 +7261,10 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
6905
7261
|
format: "compact",
|
|
6906
7262
|
contextLoader
|
|
6907
7263
|
});
|
|
6908
|
-
if (proofCreated || !hasProof || options.normalizeExistingProofs) jsonLd = await
|
|
7264
|
+
if (proofCreated || !hasProof || options.normalizeExistingProofs) jsonLd = await require_http.normalizeOutgoingActivityJsonLd(jsonLd, contextLoader, { preserveNestedSecuredDocuments: true });
|
|
6909
7265
|
assertSupportedCompoundProofShape(jsonLd, activityId);
|
|
6910
|
-
if (rsaKey == null) logger.
|
|
7266
|
+
if (rsaKey == null && !ldSignable) logger.debug("The activity {activityId} is performed by a portable actor, so it is sent without a Linked Data signature.", { activityId });
|
|
7267
|
+
else if (rsaKey == null) logger.warn("No supported key found to create a Linked Data signature for the activity {activityId}. The activity will be sent without a Linked Data signature. In order to create a Linked Data signature, at least one RSASSA-PKCS1-v1_5 key must be provided.", {
|
|
6911
7268
|
activityId,
|
|
6912
7269
|
keys: keys.map((pair) => ({
|
|
6913
7270
|
keyId: pair.keyId.href,
|
|
@@ -6915,7 +7272,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
6915
7272
|
}))
|
|
6916
7273
|
});
|
|
6917
7274
|
else try {
|
|
6918
|
-
jsonLd = await
|
|
7275
|
+
jsonLd = await require_http.signJsonLd(jsonLd, rsaKey.privateKey, rsaKey.keyId, {
|
|
6919
7276
|
contextLoader,
|
|
6920
7277
|
tracerProvider: this.tracerProvider
|
|
6921
7278
|
});
|
|
@@ -7116,6 +7473,15 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
7116
7473
|
span,
|
|
7117
7474
|
metricState
|
|
7118
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
|
+
}
|
|
7119
7485
|
const portable = request.method === "GET" || request.method === "HEAD" ? parsePortableGatewayRequest(url) : null;
|
|
7120
7486
|
if (portable != null) return await discardBodyForHead(request, await this.#fetchPortableObject(request, portable, {
|
|
7121
7487
|
onNotFound,
|
|
@@ -7267,35 +7633,17 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
7267
7633
|
onNotFound
|
|
7268
7634
|
});
|
|
7269
7635
|
context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier: route.values.identifier }) });
|
|
7270
|
-
case "sharedInbox":
|
|
7636
|
+
case "sharedInbox":
|
|
7271
7637
|
if (routeName !== "inbox" && this.sharedInboxKeyDispatcher != null) {
|
|
7272
7638
|
const identity = await this.sharedInboxKeyDispatcher(context);
|
|
7273
7639
|
if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
|
|
7274
7640
|
}
|
|
7275
|
-
|
|
7276
|
-
const inboxContextFactory = context.toInboxContext.bind(context);
|
|
7277
|
-
inboxContextFactory[rawInboxContextFactorySymbol] = context.toInboxContext.bind(context);
|
|
7278
|
-
return await handleInbox(request, {
|
|
7641
|
+
return await this.#handleInbox(request, {
|
|
7279
7642
|
recipient: route.values.identifier ?? null,
|
|
7280
7643
|
context,
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
kvPrefixes: this.kvPrefixes,
|
|
7284
|
-
publicKeyTtl: this.publicKeyTtl,
|
|
7285
|
-
queue: this.inboxQueue,
|
|
7286
|
-
actorDispatcher: this.actorCallbacks?.dispatcher,
|
|
7287
|
-
inboxListeners: this.inboxListeners,
|
|
7288
|
-
inboxErrorHandler: this.inboxErrorHandler,
|
|
7289
|
-
unverifiedActivityHandler: this.unverifiedActivityHandler,
|
|
7290
|
-
onNotFound,
|
|
7291
|
-
signatureTimeWindow: this.signatureTimeWindow,
|
|
7292
|
-
skipSignatureVerification: this.skipSignatureVerification,
|
|
7293
|
-
inboxChallengePolicy: this.inboxChallengePolicy,
|
|
7294
|
-
meterProvider: this.meterProvider,
|
|
7295
|
-
tracerProvider: this.tracerProvider,
|
|
7296
|
-
idempotencyStrategy: this.idempotencyStrategy
|
|
7644
|
+
contextData,
|
|
7645
|
+
onNotFound
|
|
7297
7646
|
});
|
|
7298
|
-
}
|
|
7299
7647
|
case "following": return await handleCollection(request, {
|
|
7300
7648
|
name: "following",
|
|
7301
7649
|
identifier: route.values.identifier,
|
|
@@ -7448,6 +7796,113 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
7448
7796
|
onNotFound
|
|
7449
7797
|
});
|
|
7450
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
|
+
}
|
|
7451
7906
|
async #fetchHashlinkMedia(request, media, { onNotFound, contextData, span, metricState }) {
|
|
7452
7907
|
metricState.endpoint = "hashlink_media";
|
|
7453
7908
|
metricState.routeTemplate = HASHLINK_MEDIA_ROUTE_TEMPLATE;
|
|
@@ -7678,6 +8133,12 @@ var ContextImpl = class ContextImpl {
|
|
|
7678
8133
|
if (path == null) throw new _fedify_uri_template.RouterError("No inbox path registered.");
|
|
7679
8134
|
return new URL(path, this.canonicalOrigin);
|
|
7680
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
|
+
}
|
|
7681
8142
|
getFollowingUri(identifier) {
|
|
7682
8143
|
const path = this.federation.router.build("following", { identifier });
|
|
7683
8144
|
if (path == null) throw new _fedify_uri_template.RouterError("No following collection path registered.");
|
|
@@ -7783,14 +8244,15 @@ var ContextImpl = class ContextImpl {
|
|
|
7783
8244
|
getActorKeyPairsIdentifier: identifier,
|
|
7784
8245
|
actorKeyPairsDispatcherIdentifier: this.invokedFromActorKeyPairsDispatcher.identifier
|
|
7785
8246
|
});
|
|
8247
|
+
const portableActor = await this.getPortableActorKeyOwner(identifier);
|
|
7786
8248
|
let keyPairs;
|
|
7787
8249
|
try {
|
|
7788
|
-
keyPairs = await this.getKeyPairsFromIdentifier(identifier);
|
|
8250
|
+
keyPairs = await this.getKeyPairsFromIdentifier(identifier, portableActor);
|
|
7789
8251
|
} catch (_) {
|
|
7790
8252
|
logger.warn("No actor key pairs dispatcher registered.");
|
|
7791
8253
|
return [];
|
|
7792
8254
|
}
|
|
7793
|
-
const owner = this.getActorUri(identifier);
|
|
8255
|
+
const owner = portableActor?.id ?? this.getActorUri(identifier);
|
|
7794
8256
|
const result = [];
|
|
7795
8257
|
let i = 1;
|
|
7796
8258
|
for (const keyPair of keyPairs) {
|
|
@@ -7802,7 +8264,7 @@ var ContextImpl = class ContextImpl {
|
|
|
7802
8264
|
publicKey: keyPair.publicKey
|
|
7803
8265
|
}),
|
|
7804
8266
|
multikey: new _fedify_vocab.Multikey({
|
|
7805
|
-
id: new URL(`#multikey-${i}`, owner),
|
|
8267
|
+
id: portableActor == null ? new URL(`#multikey-${i}`, owner) : keyPair.keyId,
|
|
7806
8268
|
controller: owner,
|
|
7807
8269
|
publicKey: keyPair.publicKey
|
|
7808
8270
|
})
|
|
@@ -7812,7 +8274,26 @@ var ContextImpl = class ContextImpl {
|
|
|
7812
8274
|
}
|
|
7813
8275
|
return result;
|
|
7814
8276
|
}
|
|
7815
|
-
|
|
8277
|
+
/**
|
|
8278
|
+
* Resolves the portable actor that the actor with the given identifier is,
|
|
8279
|
+
* through the portable actor ID mapper, along with the base of its gateway
|
|
8280
|
+
* key IDs on this server.
|
|
8281
|
+
* @param identifier The actor's identifier.
|
|
8282
|
+
* @returns The portable actor's ID and key ID base, or `null` if the actor
|
|
8283
|
+
* is not portable.
|
|
8284
|
+
* @throws {TypeError} If the mapper returns an invalid portable actor ID.
|
|
8285
|
+
*/
|
|
8286
|
+
async getPortableActorKeyOwner(identifier) {
|
|
8287
|
+
const mapper = this.federation.actorCallbacks?.portableActorIdMapper;
|
|
8288
|
+
if (mapper == null) return null;
|
|
8289
|
+
const id = await mapper(this, identifier);
|
|
8290
|
+
if (id == null) return null;
|
|
8291
|
+
return {
|
|
8292
|
+
id,
|
|
8293
|
+
keyBase: require_http.getGatewayKeyBase(id, this.canonicalOrigin)
|
|
8294
|
+
};
|
|
8295
|
+
}
|
|
8296
|
+
async getKeyPairsFromIdentifier(identifier, portableActor) {
|
|
7816
8297
|
const logger = (0, _logtape_logtape.getLogger)([
|
|
7817
8298
|
"fedify",
|
|
7818
8299
|
"federation",
|
|
@@ -7829,6 +8310,8 @@ var ContextImpl = class ContextImpl {
|
|
|
7829
8310
|
}
|
|
7830
8311
|
throw error;
|
|
7831
8312
|
}
|
|
8313
|
+
if (portableActor === void 0) portableActor = await this.getPortableActorKeyOwner(identifier);
|
|
8314
|
+
const keyBase = portableActor?.keyBase ?? actorUri;
|
|
7832
8315
|
const keyPairs = await this.federation.actorCallbacks?.keyPairsDispatcher(new ContextImpl({
|
|
7833
8316
|
...this,
|
|
7834
8317
|
invokedFromActorKeyPairsDispatcher: { identifier }
|
|
@@ -7839,7 +8322,7 @@ var ContextImpl = class ContextImpl {
|
|
|
7839
8322
|
for (const keyPair of keyPairs) {
|
|
7840
8323
|
result.push({
|
|
7841
8324
|
...keyPair,
|
|
7842
|
-
keyId: new URL(i == 0 ? `#main-key` : `#key-${i + 1}`,
|
|
8325
|
+
keyId: new URL(i == 0 ? `#main-key` : `#key-${i + 1}`, keyBase)
|
|
7843
8326
|
});
|
|
7844
8327
|
i++;
|
|
7845
8328
|
}
|
|
@@ -7885,7 +8368,7 @@ var ContextImpl = class ContextImpl {
|
|
|
7885
8368
|
userAgent: options.userAgent ?? this.federation.userAgent,
|
|
7886
8369
|
tracerProvider: options.tracerProvider ?? this.tracerProvider,
|
|
7887
8370
|
meterProvider: options.meterProvider ?? this.meterProvider,
|
|
7888
|
-
verifyPortableObject: options.verifyPortableObject ??
|
|
8371
|
+
verifyPortableObject: options.verifyPortableObject ?? require_http.verifyPortableObjectProof,
|
|
7889
8372
|
allowPrivateAddress: this.federation.allowPrivateAddress
|
|
7890
8373
|
});
|
|
7891
8374
|
}
|
|
@@ -8148,7 +8631,7 @@ var ContextImpl = class ContextImpl {
|
|
|
8148
8631
|
tracerProvider: this.tracerProvider,
|
|
8149
8632
|
keyTtl: this.federation.publicKeyTtl
|
|
8150
8633
|
});
|
|
8151
|
-
if (await
|
|
8634
|
+
if (await require_http.verifyObject(_fedify_vocab.Activity, json, {
|
|
8152
8635
|
contextLoader,
|
|
8153
8636
|
documentLoader: options.documentLoader ?? this.documentLoader,
|
|
8154
8637
|
meterProvider: this.meterProvider,
|
|
@@ -8258,6 +8741,9 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
|
|
|
8258
8741
|
getPortableObjectUri(cls, values, authority) {
|
|
8259
8742
|
return super.getPortableObjectUri(cls, values, authority ?? this.portableRequest?.authority);
|
|
8260
8743
|
}
|
|
8744
|
+
getPortableInboxUri(identifier, authority) {
|
|
8745
|
+
return super.getPortableInboxUri(identifier, authority ?? this.portableRequest?.authority);
|
|
8746
|
+
}
|
|
8261
8747
|
async getActor(identifier, options) {
|
|
8262
8748
|
if (this.federation.actorCallbacks == null || this.federation.actorCallbacks.dispatcher == null) throw new Error("No actor dispatcher registered.");
|
|
8263
8749
|
if (this.#invokedFromActorDispatcher != null) (0, _logtape_logtape.getLogger)([
|
|
@@ -8381,8 +8867,8 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
|
|
|
8381
8867
|
if (forwarder.length < 1) throw new Error("The forwarder's key pairs are empty.");
|
|
8382
8868
|
keys = forwarder;
|
|
8383
8869
|
} else keys = [forwarder];
|
|
8384
|
-
if (!
|
|
8385
|
-
if (!
|
|
8870
|
+
if (!require_http.hasSignatureLike(ctx.activity)) {
|
|
8871
|
+
if (!require_http.hasProofLike(ctx.activity)) {
|
|
8386
8872
|
if (options?.skipIfUnsigned) return false;
|
|
8387
8873
|
logger.warn("The activity {activityId} is not signed; even if it is forwarded to other servers as is, it may not be accepted by them due to the lack of a signature/proof.", {
|
|
8388
8874
|
activityId: ctx.activityId,
|