@fedify/fedify 2.5.0-dev.2446 → 2.5.0-dev.2492
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/{activity-listener-C-BSbKI7.mjs → activity-listener-CELFAodc.mjs} +8 -4
- package/dist/{builder-Da8b9O6v.mjs → builder-DphK9v7f.mjs} +5 -5
- package/dist/{client-D-XP_Ccb.d.ts → client-3InDZbba.d.cts} +0 -1
- package/dist/{client-D-XP_Ccb.d.cts → client-3InDZbba.d.ts} +0 -1
- package/dist/{client-CEnNz8O7.mjs → client-5SIFzW3q.mjs} +113 -64
- 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-D1djO5fa.mjs → compound-proof-cYtvlTgF.mjs} +2 -2
- package/dist/{context-8Tu9kea0.mjs → context-COHd1Rsh.mjs} +2 -0
- package/dist/{context-BdB54V-A.d.ts → context-DQJE9YfH.d.ts} +332 -6
- package/dist/{context-CQ9H8vNW.d.cts → context-DSuL3YWx.d.cts} +332 -6
- package/dist/{deno-eNpfj4oR.mjs → deno-iOAA7iRN.mjs} +1 -1
- package/dist/docloader-DCZkiTeF.mjs +136 -0
- package/dist/{exporter-DThFfe7P.mjs → exporter-DVG68ozH.mjs} +36 -30
- package/dist/federation/builder.test.mjs +1 -1
- package/dist/federation/compound-fanout.test.mjs +6 -3
- package/dist/federation/handler-compound-proof.test.mjs +5 -5
- package/dist/federation/handler.test.mjs +22 -7
- package/dist/federation/hashlink-media.test.mjs +2 -2
- package/dist/federation/idempotency.test.mjs +2 -2
- package/dist/federation/inbox-actorless.test.d.mts +2 -0
- package/dist/federation/inbox-actorless.test.mjs +487 -0
- package/dist/federation/inbox-authentication.test.d.mts +2 -0
- package/dist/federation/inbox-authentication.test.mjs +116 -0
- package/dist/federation/inbox-report.test.mjs +5 -5
- package/dist/federation/inbox-view.test.d.mts +2 -0
- package/dist/federation/inbox-view.test.mjs +369 -0
- package/dist/federation/inbox.test.mjs +22 -12
- package/dist/federation/metrics.test.mjs +1 -1
- package/dist/federation/middleware.test.mjs +470 -35
- package/dist/federation/mod.cjs +2 -1
- package/dist/federation/mod.d.cts +4 -4
- package/dist/federation/mod.d.ts +4 -4
- package/dist/federation/mod.js +8 -2
- package/dist/federation/mq.test.mjs +36 -1
- package/dist/federation/outgoing-proof.test.mjs +9 -6
- package/dist/federation/portable-actor.test.mjs +3 -3
- package/dist/federation/portable-chain.test.mjs +3 -3
- package/dist/federation/portable-collection.test.mjs +4 -4
- package/dist/federation/portable-delivery.test.mjs +2 -2
- package/dist/federation/portable-gateway-keys.test.mjs +13 -7
- package/dist/federation/portable-inbox.test.mjs +4 -4
- package/dist/federation/portable-parse-uri.test.mjs +1 -1
- package/dist/federation/portable-route-activity.test.mjs +2 -2
- package/dist/federation/portable.test.mjs +3 -3
- package/dist/federation/send.test.mjs +502 -93
- package/dist/federation/tasks/enqueue.test.mjs +3 -3
- package/dist/federation/tasks/tasks.test.mjs +4 -4
- package/dist/federation/temporal.test.mjs +1 -1
- package/dist/federation/webfinger.test.mjs +2 -2
- package/dist/{federation-BVH7drJA.cjs → federation-D0AZw_aw.cjs} +1415 -236
- package/dist/{http-BUJ5SFh6.mjs → http-BoJCMnjY.mjs} +204 -106
- package/dist/{http-DlNxxW_5.js → http-ChXUU_3c.js} +319 -123
- package/dist/{http-BfW3wyQ4.cjs → http-kv1LZw-r.cjs} +339 -125
- package/dist/inbox-BjihTKLo.mjs +312 -0
- package/dist/inbox-authentication-DflPYlLL.mjs +171 -0
- package/dist/inbox-view-D-skypg-.mjs +385 -0
- package/dist/{key-nYGY4QB9.d.cts → key-DbLNT-vA.d.ts} +1 -2
- package/dist/{key-DhsBxxZN.d.ts → key-DlHT4R6k.d.cts} +1 -2
- package/dist/{kv-cache-D_m3A241.mjs → kv-cache-Cmsyke4d.mjs} +1 -1
- package/dist/{kv-cache-7015lbrx.js → kv-cache-DQcte7ES.js} +73 -28
- package/dist/{kv-cache-kkBB0vbI.cjs → kv-cache-Ddh5sMxH.cjs} +72 -27
- package/dist/ld-CWVpo_NM.d.cts +250 -0
- package/dist/{ld-CD2CsPxq.mjs → ld-CdvcOEXQ.mjs} +52 -9
- package/dist/ld-Inagww4R.d.ts +250 -0
- package/dist/{metrics-D3LHk6Z4.mjs → metrics-CpxvUrBz.mjs} +1 -1
- package/dist/{middleware-N9eM16XE.js → middleware-BHb5toqA.js} +1413 -247
- package/dist/{middleware-CCDXvXOG.mjs → middleware-BKs2DLUF.mjs} +1 -1
- package/dist/{middleware-CzoWvrdZ.mjs → middleware-CoBLrTKX.mjs} +533 -132
- package/dist/{mod-B0dGgQsg.d.cts → mod-6TQ4UQZb.d.cts} +1 -1
- package/dist/mod-B4kBw1-8.d.ts +154 -0
- package/dist/{mod-DkhfZC1u.d.ts → mod-B7j30IQf.d.ts} +2 -9
- package/dist/mod-C1nT74IG.d.cts +154 -0
- package/dist/{mod-D0bxB8jS.d.ts → mod-CIeBgsOC.d.ts} +1 -1
- package/dist/{mod-B5-wdzmJ.d.cts → mod-YvA4Rk8J.d.cts} +2 -9
- package/dist/mod.cjs +6 -5
- package/dist/mod.d.cts +9 -9
- package/dist/mod.d.ts +9 -9
- package/dist/mod.js +6 -6
- package/dist/{mq-Dm1Wmtm-.mjs → mq-eF96QxUT.mjs} +6 -0
- package/dist/nodeinfo/client.test.mjs +337 -19
- package/dist/nodeinfo/handler.test.mjs +2 -2
- package/dist/nodeinfo/mod.cjs +1 -1
- package/dist/nodeinfo/mod.d.cts +1 -1
- package/dist/nodeinfo/mod.d.ts +1 -1
- package/dist/nodeinfo/mod.js +1 -1
- package/dist/otel/exporter.test.mjs +257 -1
- package/dist/otel/mod.cjs +36 -30
- package/dist/otel/mod.d.cts +13 -0
- package/dist/otel/mod.d.ts +13 -0
- package/dist/otel/mod.js +36 -30
- package/dist/{owner-C5-FVX6t.mjs → owner-DTnVpUKA.mjs} +3 -3
- package/dist/{portable-collection-Cony3_LC.mjs → portable-collection-CJeYTL61.mjs} +2 -2
- package/dist/{portable-collection-C-Z1SKDy.cjs → portable-collection-DciwBIia.cjs} +1 -1
- package/dist/{portable-collection-CC2zNAje.js → portable-collection-TmtpzD4E.js} +1 -1
- package/dist/{portable-key-DNc_flH-.mjs → portable-key-DcYok4ki.mjs} +1 -1
- package/dist/{proof-Cryb1c70.mjs → proof-GH7Poos_.mjs} +98 -10
- package/dist/{send-AF25pWaJ.mjs → send-BgGJh6pv.mjs} +174 -76
- 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 +2 -2
- package/dist/sig/http.test.mjs +553 -2
- package/dist/sig/key.test.mjs +1 -1
- package/dist/sig/ld.test.mjs +1 -1
- package/dist/sig/mitra-receive-interop.test.mjs +4 -4
- package/dist/sig/mitra-send-interop.test.mjs +3 -3
- package/dist/sig/mod.cjs +2 -2
- package/dist/sig/mod.d.cts +3 -3
- package/dist/sig/mod.d.ts +3 -3
- package/dist/sig/mod.js +2 -2
- package/dist/sig/owner.test.mjs +1 -1
- package/dist/sig/portable-collection.test.mjs +1 -1
- package/dist/sig/portable-interop.test.mjs +3 -3
- package/dist/sig/portable-key.test.mjs +5 -5
- package/dist/sig/proof.test.mjs +45 -5
- package/dist/sig/tootik-request-interop.test.mjs +4 -4
- package/dist/sig/tootik-send-interop.test.mjs +3 -3
- package/dist/sig/verification.test.mjs +3 -3
- package/dist/{temporal-BsAeuUJj.mjs → temporal-gJINzcL4.mjs} +1 -1
- package/dist/testing/kv.test.mjs +1 -1
- package/dist/testing/mod.d.mts +288 -2
- package/dist/testing/mod.mjs +2 -2
- package/dist/{testing-t8xDeJZ5.mjs → testing-Dbr21vZk.mjs} +1 -1
- package/dist/{types-_S2_MeLF.js → types-DUjZb4i6.js} +112 -63
- package/dist/{types-poIao40f.cjs → types-hKCCjii0.cjs} +111 -62
- package/dist/utils/docloader.test.mjs +344 -36
- 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 +7 -7
- package/dist/docloader-B-0nqOB1.mjs +0 -91
- package/dist/inbox-CHvMaIFX.mjs +0 -188
- package/dist/mod-BRpER-B0.d.ts +0 -320
- package/dist/mod-xAfaLS4T.d.cts +0 -320
- package/dist/owner-BF0L9cMt.d.cts +0 -78
- package/dist/owner-BSzr5-I8.d.ts +0 -78
|
@@ -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-
|
|
5
|
-
const require_types = require("./types-
|
|
6
|
-
const require_portable_collection = require("./portable-collection-
|
|
7
|
-
const require_kv_cache = require("./kv-cache-
|
|
4
|
+
const require_http = require("./http-kv1LZw-r.cjs");
|
|
5
|
+
const require_types = require("./types-hKCCjii0.cjs");
|
|
6
|
+
const require_portable_collection = require("./portable-collection-DciwBIia.cjs");
|
|
7
|
+
const require_kv_cache = require("./kv-cache-Ddh5sMxH.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");
|
|
@@ -15,7 +15,10 @@ let byte_encodings_hex = require("byte-encodings/hex");
|
|
|
15
15
|
let _fedify_vocab_runtime_jsonld = require("@fedify/vocab-runtime/jsonld");
|
|
16
16
|
_fedify_vocab_runtime_jsonld = require_rolldown_runtime.__toESM(_fedify_vocab_runtime_jsonld, 1);
|
|
17
17
|
let es_toolkit = require("es-toolkit");
|
|
18
|
+
let json_canon = require("json-canon");
|
|
19
|
+
json_canon = require_rolldown_runtime.__toESM(json_canon, 1);
|
|
18
20
|
let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conventions");
|
|
21
|
+
let _fedify_vocab_runtime_internal = require("@fedify/vocab-runtime/internal");
|
|
19
22
|
let _fedify_webfinger = require("@fedify/webfinger");
|
|
20
23
|
let _opentelemetry_sdk_metrics = require("@opentelemetry/sdk-metrics");
|
|
21
24
|
let devalue = require("devalue");
|
|
@@ -32,9 +35,12 @@ var ActivityListenerSet = class {
|
|
|
32
35
|
clone.#listeners = new Map(this.#listeners);
|
|
33
36
|
return clone;
|
|
34
37
|
}
|
|
35
|
-
add(type, listener) {
|
|
38
|
+
add(type, listener, options = {}) {
|
|
36
39
|
if (this.#listeners.has(type)) throw new TypeError("Listener already set for this type.");
|
|
37
|
-
this.#listeners.set(type,
|
|
40
|
+
this.#listeners.set(type, {
|
|
41
|
+
listener,
|
|
42
|
+
options: { ...options }
|
|
43
|
+
});
|
|
38
44
|
}
|
|
39
45
|
dispatchWithClass(activity) {
|
|
40
46
|
let cls = activity.constructor;
|
|
@@ -44,10 +50,11 @@ var ActivityListenerSet = class {
|
|
|
44
50
|
cls = globalThis.Object.getPrototypeOf(cls);
|
|
45
51
|
}
|
|
46
52
|
if (cls == null) return null;
|
|
47
|
-
const listener = this.#listeners.get(cls);
|
|
53
|
+
const { listener, options } = this.#listeners.get(cls);
|
|
48
54
|
return {
|
|
49
55
|
class: cls,
|
|
50
|
-
listener
|
|
56
|
+
listener,
|
|
57
|
+
options
|
|
51
58
|
};
|
|
52
59
|
}
|
|
53
60
|
dispatch(activity) {
|
|
@@ -635,8 +642,8 @@ var FederationBuilderImpl = class {
|
|
|
635
642
|
}
|
|
636
643
|
const listeners = this.inboxListeners = new ActivityListenerSet();
|
|
637
644
|
const setters = {
|
|
638
|
-
on(type, listener) {
|
|
639
|
-
listeners.add(type, listener);
|
|
645
|
+
on(type, listener, options) {
|
|
646
|
+
listeners.add(type, listener, options);
|
|
640
647
|
return setters;
|
|
641
648
|
},
|
|
642
649
|
onError: (handler) => {
|
|
@@ -2805,6 +2812,172 @@ async function isInAudience(object, actor, options = {}) {
|
|
|
2805
2812
|
return false;
|
|
2806
2813
|
}
|
|
2807
2814
|
//#endregion
|
|
2815
|
+
//#region src/federation/inbox-authentication.ts
|
|
2816
|
+
/**
|
|
2817
|
+
* The evidence of an activity that has not been authenticated by Fedify.
|
|
2818
|
+
*/
|
|
2819
|
+
const EMPTY_EVIDENCE = Object.freeze({
|
|
2820
|
+
verified: false,
|
|
2821
|
+
requestSigners: Object.freeze([]),
|
|
2822
|
+
documentSigners: Object.freeze([]),
|
|
2823
|
+
originAuthenticated: Object.freeze([]),
|
|
2824
|
+
expandedActivity: Object.freeze([]),
|
|
2825
|
+
baseUrl: null
|
|
2826
|
+
});
|
|
2827
|
+
/**
|
|
2828
|
+
* Gets the evidence of an activity that has not been authenticated.
|
|
2829
|
+
* @internal
|
|
2830
|
+
*/
|
|
2831
|
+
function getEmptyAuthenticationEvidence() {
|
|
2832
|
+
return EMPTY_EVIDENCE;
|
|
2833
|
+
}
|
|
2834
|
+
/**
|
|
2835
|
+
* Gets the principals that an authorization decision should rely on.
|
|
2836
|
+
* @param evidence The evidence.
|
|
2837
|
+
* @param scope See {@link IsActorAuthenticatedOptions.scope}.
|
|
2838
|
+
* @returns The principals.
|
|
2839
|
+
* @internal
|
|
2840
|
+
*/
|
|
2841
|
+
function getEffectivePrincipals(evidence, scope = "effective") {
|
|
2842
|
+
if (scope === "document" || evidence.documentSigners.length > 0) return evidence.documentSigners;
|
|
2843
|
+
if (evidence.requestSigners.length > 0) return evidence.requestSigners;
|
|
2844
|
+
return evidence.originAuthenticated;
|
|
2845
|
+
}
|
|
2846
|
+
/**
|
|
2847
|
+
* Normalizes an actor ID for comparison: portable IDs and compatible
|
|
2848
|
+
* identifiers to their canonical portable ID, other IDs to their `href`.
|
|
2849
|
+
* @returns The normalized ID, or `null` for a malformed portable ID.
|
|
2850
|
+
*/
|
|
2851
|
+
function normalizeActorId(id) {
|
|
2852
|
+
return require_http.isPortableId(id) ? require_http.getCanonicalPortableId(id) : id.href;
|
|
2853
|
+
}
|
|
2854
|
+
/**
|
|
2855
|
+
* Checks whether the given principal authenticates the given actor.
|
|
2856
|
+
* @param principal The principal.
|
|
2857
|
+
* @param actorId The actor ID.
|
|
2858
|
+
* @returns `true` if the principal authenticates the actor.
|
|
2859
|
+
* @internal
|
|
2860
|
+
*/
|
|
2861
|
+
function principalAuthenticates(principal, actorId) {
|
|
2862
|
+
if (principal.kind === "portableAuthority") {
|
|
2863
|
+
if (!require_http.isPortableId(actorId)) return false;
|
|
2864
|
+
const did = require_http.getPortableDid(actorId);
|
|
2865
|
+
return did != null && did === require_http.getPortableDid(principal.id);
|
|
2866
|
+
}
|
|
2867
|
+
const expected = normalizeActorId(actorId);
|
|
2868
|
+
return expected != null && normalizeActorId(principal.id) === expected;
|
|
2869
|
+
}
|
|
2870
|
+
/**
|
|
2871
|
+
* Checks whether the given evidence authenticates the given actor.
|
|
2872
|
+
*
|
|
2873
|
+
* Actor IDs are compared exactly, except that [FEP-ef61] portable IDs and
|
|
2874
|
+
* compatible identifiers are compared by their canonical portable IDs, and
|
|
2875
|
+
* a portable authority (a DID) authenticates the portable actors of that DID.
|
|
2876
|
+
*
|
|
2877
|
+
* @example
|
|
2878
|
+
* ``` typescript
|
|
2879
|
+
* federation
|
|
2880
|
+
* .setInboxListeners("/users/{identifier}/inbox", "/inbox")
|
|
2881
|
+
* .on(Follow, async (ctx, follow) => {
|
|
2882
|
+
* if (follow.actorId == null) return;
|
|
2883
|
+
* // The actor is always authenticated for activities that declare one,
|
|
2884
|
+
* // but the same check works for any actor an activity names:
|
|
2885
|
+
* isActorAuthenticated(ctx.authentication, follow.actorId);
|
|
2886
|
+
* });
|
|
2887
|
+
* ```
|
|
2888
|
+
*
|
|
2889
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
2890
|
+
* @param evidence The authentication evidence, usually
|
|
2891
|
+
* {@link InboxContext.authentication}.
|
|
2892
|
+
* @param actorId The actor ID to check.
|
|
2893
|
+
* @param options Options.
|
|
2894
|
+
* @returns `true` if the actor is authenticated.
|
|
2895
|
+
* @since 2.5.0
|
|
2896
|
+
*/
|
|
2897
|
+
function isActorAuthenticated(evidence, actorId, options = {}) {
|
|
2898
|
+
return getEffectivePrincipals(evidence, options.scope).some((principal) => principalAuthenticates(principal, actorId));
|
|
2899
|
+
}
|
|
2900
|
+
function serializePrincipal(principal) {
|
|
2901
|
+
return {
|
|
2902
|
+
kind: principal.kind,
|
|
2903
|
+
id: principal.id.href,
|
|
2904
|
+
mechanism: principal.mechanism,
|
|
2905
|
+
keyId: principal.keyId?.href ?? null
|
|
2906
|
+
};
|
|
2907
|
+
}
|
|
2908
|
+
/**
|
|
2909
|
+
* Serializes the principals of the given evidence for a queued message.
|
|
2910
|
+
* @internal
|
|
2911
|
+
*/
|
|
2912
|
+
function serializeAuthentication(evidence) {
|
|
2913
|
+
return {
|
|
2914
|
+
verified: evidence.verified,
|
|
2915
|
+
requestSigners: evidence.requestSigners.map(serializePrincipal),
|
|
2916
|
+
documentSigners: evidence.documentSigners.map(serializePrincipal),
|
|
2917
|
+
originAuthenticated: evidence.originAuthenticated.map(serializePrincipal)
|
|
2918
|
+
};
|
|
2919
|
+
}
|
|
2920
|
+
const KINDS = /* @__PURE__ */ new Set(["actor", "portableAuthority"]);
|
|
2921
|
+
const MECHANISMS = /* @__PURE__ */ new Set([
|
|
2922
|
+
"httpSignatures",
|
|
2923
|
+
"linkedDataSignatures",
|
|
2924
|
+
"objectIntegrityProofs",
|
|
2925
|
+
"origin"
|
|
2926
|
+
]);
|
|
2927
|
+
function deserializePrincipals(value) {
|
|
2928
|
+
if (!Array.isArray(value)) return null;
|
|
2929
|
+
const principals = [];
|
|
2930
|
+
for (const item of value) {
|
|
2931
|
+
if (item == null || typeof item !== "object" || !KINDS.has(item.kind) || !MECHANISMS.has(item.mechanism) || typeof item.id !== "string" || !URL.canParse(item.id) || item.keyId != null && (typeof item.keyId !== "string" || !URL.canParse(item.keyId))) return null;
|
|
2932
|
+
principals.push(Object.freeze({
|
|
2933
|
+
kind: item.kind,
|
|
2934
|
+
id: new URL(item.id),
|
|
2935
|
+
mechanism: item.mechanism,
|
|
2936
|
+
keyId: item.keyId == null ? null : new URL(item.keyId)
|
|
2937
|
+
}));
|
|
2938
|
+
}
|
|
2939
|
+
return principals;
|
|
2940
|
+
}
|
|
2941
|
+
/**
|
|
2942
|
+
* Restores the evidence of a queued message.
|
|
2943
|
+
* @param value The serialized principals.
|
|
2944
|
+
* @param expandedActivity The expanded activity, restored from the queued
|
|
2945
|
+
* verified view.
|
|
2946
|
+
* @param baseUrl The base URL, restored from the queued verified view.
|
|
2947
|
+
* @returns The evidence, or `null` if the serialized value is malformed.
|
|
2948
|
+
* @internal
|
|
2949
|
+
*/
|
|
2950
|
+
function deserializeAuthentication(value, expandedActivity, baseUrl) {
|
|
2951
|
+
if (value == null || typeof value !== "object" || typeof value.verified !== "boolean") return null;
|
|
2952
|
+
const record = value;
|
|
2953
|
+
const requestSigners = deserializePrincipals(record.requestSigners);
|
|
2954
|
+
const documentSigners = deserializePrincipals(record.documentSigners);
|
|
2955
|
+
const originAuthenticated = deserializePrincipals(record.originAuthenticated);
|
|
2956
|
+
if (requestSigners == null || documentSigners == null || originAuthenticated == null) return null;
|
|
2957
|
+
return createAuthenticationEvidence({
|
|
2958
|
+
verified: record.verified,
|
|
2959
|
+
requestSigners,
|
|
2960
|
+
documentSigners,
|
|
2961
|
+
originAuthenticated,
|
|
2962
|
+
expandedActivity,
|
|
2963
|
+
baseUrl
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
/**
|
|
2967
|
+
* Creates frozen evidence.
|
|
2968
|
+
* @internal
|
|
2969
|
+
*/
|
|
2970
|
+
function createAuthenticationEvidence(evidence) {
|
|
2971
|
+
return Object.freeze({
|
|
2972
|
+
verified: evidence.verified,
|
|
2973
|
+
requestSigners: Object.freeze([...evidence.requestSigners]),
|
|
2974
|
+
documentSigners: Object.freeze([...evidence.documentSigners]),
|
|
2975
|
+
originAuthenticated: Object.freeze([...evidence.originAuthenticated]),
|
|
2976
|
+
expandedActivity: evidence.expandedActivity,
|
|
2977
|
+
baseUrl: evidence.baseUrl
|
|
2978
|
+
});
|
|
2979
|
+
}
|
|
2980
|
+
//#endregion
|
|
2808
2981
|
//#region src/federation/inbox.ts
|
|
2809
2982
|
const Temporal$10 = require_http.Temporal;
|
|
2810
2983
|
/**
|
|
@@ -2827,13 +3000,135 @@ function actorsHaveSameOrigin(activity) {
|
|
|
2827
3000
|
const origin = require_http.getAuthenticationOrigin(activity.id);
|
|
2828
3001
|
return origin != null && activity.actorIds.every((actor) => require_http.getAuthenticationOrigin(actor) === origin);
|
|
2829
3002
|
}
|
|
2830
|
-
|
|
3003
|
+
/**
|
|
3004
|
+
* The headroom that the size check of a queued inbox message leaves for the
|
|
3005
|
+
* fields that grow when Fedify retries the message, e.g., its attempt count.
|
|
3006
|
+
*/
|
|
3007
|
+
const QUEUED_MESSAGE_HEADROOM = 64;
|
|
3008
|
+
/**
|
|
3009
|
+
* Computes the idempotency key of an incoming activity.
|
|
3010
|
+
* @returns The key, or `null` if the activity is not deduplicated.
|
|
3011
|
+
* @internal
|
|
3012
|
+
*/
|
|
3013
|
+
async function getInboxIdempotencyKey(strategy, inboxContext, activity, origin, recipient, prefix) {
|
|
3014
|
+
if (activity.id == null) return null;
|
|
3015
|
+
strategy ??= "per-inbox";
|
|
3016
|
+
let keyString;
|
|
3017
|
+
if (typeof strategy === "function") keyString = await strategy(inboxContext(), activity);
|
|
3018
|
+
else switch (strategy) {
|
|
3019
|
+
case "global":
|
|
3020
|
+
keyString = activity.id.href;
|
|
3021
|
+
break;
|
|
3022
|
+
case "per-origin":
|
|
3023
|
+
keyString = `${origin}\n${activity.id.href}`;
|
|
3024
|
+
break;
|
|
3025
|
+
case "per-inbox":
|
|
3026
|
+
keyString = `${origin}\n${activity.id.href}\n${recipient == null ? "sharedInbox" : `inbox\n${recipient}`}`;
|
|
3027
|
+
break;
|
|
3028
|
+
default: keyString = `${origin}\n${activity.id.href}`;
|
|
3029
|
+
}
|
|
3030
|
+
return keyString == null ? null : [...prefix, keyString];
|
|
3031
|
+
}
|
|
3032
|
+
/**
|
|
3033
|
+
* Checks whether an incoming activity has already been processed, by its
|
|
3034
|
+
* idempotency key, or by the key that queue workers of older versions of
|
|
3035
|
+
* Fedify recorded.
|
|
3036
|
+
* @internal
|
|
3037
|
+
*/
|
|
3038
|
+
async function isInboxActivityProcessed(kv, cacheKey, activity, origin, prefix) {
|
|
3039
|
+
if (cacheKey == null) return false;
|
|
3040
|
+
if (await kv.get(cacheKey) === true) return true;
|
|
3041
|
+
if (activity.id == null) return false;
|
|
3042
|
+
return await kv.get([
|
|
3043
|
+
...prefix,
|
|
3044
|
+
origin,
|
|
3045
|
+
activity.id.href
|
|
3046
|
+
]) === true;
|
|
3047
|
+
}
|
|
3048
|
+
/**
|
|
3049
|
+
* The bytes added to the UTF-8 JSON length of a message for each of its JSON
|
|
3050
|
+
* values, to bound the size of serialization formats other than JSON, such
|
|
3051
|
+
* as the structured serialization of Deno KV, which spends more bytes than
|
|
3052
|
+
* JSON on type tags and lengths of small values, e.g., empty arrays, and up
|
|
3053
|
+
* to nine bytes on a fractional number that JSON writes in three.
|
|
3054
|
+
*/
|
|
3055
|
+
const PER_VALUE_SERIALIZATION_OVERHEAD = 8;
|
|
3056
|
+
/**
|
|
3057
|
+
* Estimates, conservatively, how large a queued message is once a queue
|
|
3058
|
+
* backend serializes it: the UTF-8 length of its JSON serialization, plus
|
|
3059
|
+
* a fixed overhead for each JSON value in it, plus the bytes that encoding
|
|
3060
|
+
* strings beyond Latin-1 as UTF-16 adds.
|
|
3061
|
+
* @internal
|
|
3062
|
+
*/
|
|
3063
|
+
function estimateQueuedMessageSize(message) {
|
|
3064
|
+
const encoder = new TextEncoder();
|
|
3065
|
+
let values = 0;
|
|
3066
|
+
let stringExpansion = 0;
|
|
3067
|
+
const countString = (value) => {
|
|
3068
|
+
values++;
|
|
3069
|
+
if (!/[\u0100-\uffff]/.test(value)) return;
|
|
3070
|
+
const expansion = value.length * 2 - encoder.encode(value).byteLength;
|
|
3071
|
+
if (expansion > 0) stringExpansion += expansion;
|
|
3072
|
+
};
|
|
3073
|
+
const count = (value) => {
|
|
3074
|
+
if (typeof value === "string") {
|
|
3075
|
+
countString(value);
|
|
3076
|
+
return;
|
|
3077
|
+
}
|
|
3078
|
+
values++;
|
|
3079
|
+
if (Array.isArray(value)) for (const item of value) count(item);
|
|
3080
|
+
else if (value != null && typeof value === "object") for (const [key, item] of Object.entries(value)) {
|
|
3081
|
+
if (item === void 0) continue;
|
|
3082
|
+
countString(key);
|
|
3083
|
+
count(item);
|
|
3084
|
+
}
|
|
3085
|
+
};
|
|
3086
|
+
count(message);
|
|
3087
|
+
return encoder.encode(JSON.stringify(message)).byteLength + stringExpansion + values * PER_VALUE_SERIALIZATION_OVERHEAD;
|
|
3088
|
+
}
|
|
3089
|
+
/**
|
|
3090
|
+
* Checks whether an activity that declares no actor is authenticated: at
|
|
3091
|
+
* least one principal must be authenticated for it, and its ID, if any, must
|
|
3092
|
+
* have the same [FEP-fe34] origin as one of those principals.
|
|
3093
|
+
*
|
|
3094
|
+
* [FEP-fe34]: https://w3id.org/fep/fe34
|
|
3095
|
+
* @returns `"ok"` if it is authenticated, or why it is not.
|
|
3096
|
+
* @internal
|
|
3097
|
+
*/
|
|
3098
|
+
function checkActorlessActivity(activity, authentication) {
|
|
3099
|
+
const principals = getEffectivePrincipals(authentication);
|
|
3100
|
+
if (principals.length < 1) return {
|
|
3101
|
+
status: "noAuthenticatedPrincipal",
|
|
3102
|
+
principals
|
|
3103
|
+
};
|
|
3104
|
+
if (activity.id == null) return {
|
|
3105
|
+
status: "ok",
|
|
3106
|
+
principals
|
|
3107
|
+
};
|
|
3108
|
+
const origin = require_http.getAuthenticationOrigin(activity.id);
|
|
3109
|
+
if (origin == null || !principals.some((principal) => require_http.getAuthenticationOrigin(principal.id) === origin)) return {
|
|
3110
|
+
status: "activityOriginMismatch",
|
|
3111
|
+
principals
|
|
3112
|
+
};
|
|
3113
|
+
return {
|
|
3114
|
+
status: "ok",
|
|
3115
|
+
principals
|
|
3116
|
+
};
|
|
3117
|
+
}
|
|
3118
|
+
/**
|
|
3119
|
+
* Same as {@link checkActorlessActivity}, but returns a boolean.
|
|
3120
|
+
* @internal
|
|
3121
|
+
*/
|
|
3122
|
+
function isActorlessActivityAuthenticated(activity, authentication) {
|
|
3123
|
+
return checkActorlessActivity(activity, authentication).status === "ok";
|
|
3124
|
+
}
|
|
3125
|
+
async function routeActivity({ context: ctx, json, originalJson, normalizedActivity, ldSignatureVerified, authentication, verifiedView, replayable, actorless, activity, recipient, inboxListeners, inboxContextFactory, listenerInboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, meterProvider, tracerProvider, idempotencyStrategy }) {
|
|
2831
3126
|
const logger = (0, _logtape_logtape.getLogger)([
|
|
2832
3127
|
"fedify",
|
|
2833
3128
|
"federation",
|
|
2834
3129
|
"inbox"
|
|
2835
3130
|
]);
|
|
2836
|
-
if (activity.actorId == null) {
|
|
3131
|
+
if (activity.actorId == null && !actorless) {
|
|
2837
3132
|
logger.error("Missing actor.", { activity: json });
|
|
2838
3133
|
span.setStatus({
|
|
2839
3134
|
code: _opentelemetry_api.SpanStatusCode.ERROR,
|
|
@@ -2842,58 +3137,59 @@ async function routeActivity({ context: ctx, json, originalJson, normalizedActiv
|
|
|
2842
3137
|
require_http.recordInboxActivity(meterProvider, "rejected", (0, _fedify_vocab.getTypeId)(activity).href);
|
|
2843
3138
|
return "missingActor";
|
|
2844
3139
|
}
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
keyString = `${ctx.origin}\n${activity.id.href}\n${recipient == null ? "sharedInbox" : `inbox\n${recipient}`}`;
|
|
2860
|
-
break;
|
|
2861
|
-
default: keyString = `${ctx.origin}\n${activity.id.href}`;
|
|
2862
|
-
}
|
|
2863
|
-
if (keyString != null) cacheKey = [...kvPrefixes.activityIdempotence, keyString];
|
|
3140
|
+
const contextMetadata = { authentication };
|
|
3141
|
+
const cacheKey = await getInboxIdempotencyKey(idempotencyStrategy, () => inboxContextFactory(recipient, json, activity.id?.href, (0, _fedify_vocab.getTypeId)(activity).href, contextMetadata), activity, ctx.origin, recipient, kvPrefixes.activityIdempotence);
|
|
3142
|
+
if (await isInboxActivityProcessed(kv, cacheKey, activity, ctx.origin, kvPrefixes.activityIdempotence)) {
|
|
3143
|
+
logger.debug("Activity {activityId} has already been processed.", {
|
|
3144
|
+
activityId: activity.id?.href,
|
|
3145
|
+
activity: json,
|
|
3146
|
+
recipient
|
|
3147
|
+
});
|
|
3148
|
+
span.setStatus({
|
|
3149
|
+
code: _opentelemetry_api.SpanStatusCode.UNSET,
|
|
3150
|
+
message: `Activity ${activity.id?.href} has already been processed.`
|
|
3151
|
+
});
|
|
3152
|
+
require_http.recordInboxActivity(meterProvider, "rejected", (0, _fedify_vocab.getTypeId)(activity).href);
|
|
3153
|
+
return "alreadyProcessed";
|
|
2864
3154
|
}
|
|
2865
|
-
if (
|
|
2866
|
-
|
|
2867
|
-
|
|
3155
|
+
if (activity.actorId != null) span.setAttribute("activitypub.actor.id", activity.actorId.href);
|
|
3156
|
+
let message;
|
|
3157
|
+
if (queue != null) {
|
|
3158
|
+
const carrier = {};
|
|
3159
|
+
_opentelemetry_api.propagation.inject(_opentelemetry_api.context.active(), carrier);
|
|
3160
|
+
message = {
|
|
3161
|
+
type: "inbox",
|
|
3162
|
+
id: crypto.randomUUID(),
|
|
3163
|
+
baseUrl: ctx.origin,
|
|
3164
|
+
activity: originalJson ?? json,
|
|
3165
|
+
...normalizedActivity == null ? {} : { normalizedActivity },
|
|
3166
|
+
...ldSignatureVerified == null ? {} : { ldSignatureVerified },
|
|
3167
|
+
...verifiedView == null ? {} : { verifiedView },
|
|
3168
|
+
...authentication == null ? {} : { authentication: serializeAuthentication(authentication) },
|
|
3169
|
+
identifier: recipient,
|
|
3170
|
+
attempt: 0,
|
|
3171
|
+
started: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3172
|
+
traceContext: carrier
|
|
3173
|
+
};
|
|
3174
|
+
const maxMessageSize = queue.maxMessageSize;
|
|
3175
|
+
if (replayable === false) {
|
|
3176
|
+
logger.warn("The incoming activity {activityId} uses a JSON-LD context that cannot be kept for the queue; processing it immediately instead.", {
|
|
2868
3177
|
activityId: activity.id?.href,
|
|
2869
|
-
activity: json,
|
|
2870
3178
|
recipient
|
|
2871
3179
|
});
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
3180
|
+
message = void 0;
|
|
3181
|
+
} else if (maxMessageSize != null && estimateQueuedMessageSize(message) > maxMessageSize - QUEUED_MESSAGE_HEADROOM) {
|
|
3182
|
+
logger.warn("The incoming activity {activityId} is too large to enqueue; processing it immediately instead.", {
|
|
3183
|
+
activityId: activity.id?.href,
|
|
3184
|
+
recipient,
|
|
3185
|
+
maxMessageSize
|
|
2875
3186
|
});
|
|
2876
|
-
|
|
2877
|
-
return "alreadyProcessed";
|
|
3187
|
+
message = void 0;
|
|
2878
3188
|
}
|
|
2879
3189
|
}
|
|
2880
|
-
|
|
2881
|
-
if (queue != null) {
|
|
2882
|
-
const carrier = {};
|
|
2883
|
-
_opentelemetry_api.propagation.inject(_opentelemetry_api.context.active(), carrier);
|
|
3190
|
+
if (queue != null && message != null) {
|
|
2884
3191
|
try {
|
|
2885
|
-
await queue.enqueue(
|
|
2886
|
-
type: "inbox",
|
|
2887
|
-
id: crypto.randomUUID(),
|
|
2888
|
-
baseUrl: ctx.origin,
|
|
2889
|
-
activity: originalJson ?? json,
|
|
2890
|
-
...normalizedActivity == null ? {} : { normalizedActivity },
|
|
2891
|
-
...ldSignatureVerified == null ? {} : { ldSignatureVerified },
|
|
2892
|
-
identifier: recipient,
|
|
2893
|
-
attempt: 0,
|
|
2894
|
-
started: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2895
|
-
traceContext: carrier
|
|
2896
|
-
});
|
|
3192
|
+
await queue.enqueue(message);
|
|
2897
3193
|
} catch (error) {
|
|
2898
3194
|
logger.error("Failed to enqueue the incoming activity {activityId}:\n{error}", {
|
|
2899
3195
|
error,
|
|
@@ -2943,7 +3239,7 @@ async function routeActivity({ context: ctx, json, originalJson, normalizedActiv
|
|
|
2943
3239
|
const started = performance.now();
|
|
2944
3240
|
try {
|
|
2945
3241
|
const contextFactory = listenerInboxContextFactory ?? inboxContextFactory;
|
|
2946
|
-
await listener(contextFactory(recipient, contextFactory === inboxContextFactory ? json : originalJson ?? json, activity.id?.href, activityType), activity);
|
|
3242
|
+
await listener(contextFactory(recipient, contextFactory === inboxContextFactory ? json : originalJson ?? json, activity.id?.href, activityType, contextMetadata), activity);
|
|
2947
3243
|
} finally {
|
|
2948
3244
|
require_http.getFederationMetrics(meterProvider).recordInboxProcessingDuration(activityType, require_http.getDurationMs(started));
|
|
2949
3245
|
}
|
|
@@ -2984,6 +3280,382 @@ async function routeActivity({ context: ctx, json, originalJson, normalizedActiv
|
|
|
2984
3280
|
});
|
|
2985
3281
|
}
|
|
2986
3282
|
//#endregion
|
|
3283
|
+
//#region src/federation/inbox-view.ts
|
|
3284
|
+
/**
|
|
3285
|
+
* The interpretation of an incoming activity that Fedify authenticated, kept
|
|
3286
|
+
* so that listeners, authorization helpers, and queue workers observe the
|
|
3287
|
+
* same JSON-LD meaning that signature verification observed.
|
|
3288
|
+
*
|
|
3289
|
+
* Every JSON-LD processing step of one delivery goes through a request-scoped
|
|
3290
|
+
* *context snapshot*, which memoizes the remote context responses so that
|
|
3291
|
+
* a nondeterministic or mutable remote context cannot give verification and
|
|
3292
|
+
* dispatch different meanings. The snapshot is serialized into queued
|
|
3293
|
+
* messages, so that workers and retries replay the very same responses
|
|
3294
|
+
* without loading contexts again.
|
|
3295
|
+
* @module
|
|
3296
|
+
*/
|
|
3297
|
+
/**
|
|
3298
|
+
* Thrown when replaying an interpretation needs a context response that its
|
|
3299
|
+
* snapshot does not have, or has with a different content.
|
|
3300
|
+
* @internal
|
|
3301
|
+
*/
|
|
3302
|
+
var ContextSnapshotMissError = class extends Error {
|
|
3303
|
+
url;
|
|
3304
|
+
constructor(url, reason) {
|
|
3305
|
+
super(`Context ${url} cannot be replayed: ${reason}.`);
|
|
3306
|
+
this.name = "ContextSnapshotMissError";
|
|
3307
|
+
this.url = url;
|
|
3308
|
+
}
|
|
3309
|
+
};
|
|
3310
|
+
function normalizeUrl(url) {
|
|
3311
|
+
return URL.canParse(url) ? new URL(url).href : url;
|
|
3312
|
+
}
|
|
3313
|
+
/**
|
|
3314
|
+
* Creates the context snapshot of a delivery.
|
|
3315
|
+
* @param base The caller's context loader.
|
|
3316
|
+
* @returns The snapshot.
|
|
3317
|
+
* @internal
|
|
3318
|
+
*/
|
|
3319
|
+
function createContextSnapshot(base) {
|
|
3320
|
+
const underlying = {
|
|
3321
|
+
standard: require_http.wrapContextLoaderForJsonLd(base),
|
|
3322
|
+
normalization: require_http.getNormalizationContextLoader(base)
|
|
3323
|
+
};
|
|
3324
|
+
const cache = /* @__PURE__ */ new Map();
|
|
3325
|
+
const recorded = /* @__PURE__ */ new Map();
|
|
3326
|
+
const loaders = /* @__PURE__ */ new Map();
|
|
3327
|
+
let released = false;
|
|
3328
|
+
return {
|
|
3329
|
+
loader(policy) {
|
|
3330
|
+
let loader = loaders.get(policy);
|
|
3331
|
+
if (loader != null) return loader;
|
|
3332
|
+
const effective = underlying[policy];
|
|
3333
|
+
loader = require_http.markEffectiveContextLoader(async (url, options) => {
|
|
3334
|
+
if (released) return await effective(url, options);
|
|
3335
|
+
const normalizedUrl = normalizeUrl(url);
|
|
3336
|
+
const key = `${policy}\n${normalizedUrl}`;
|
|
3337
|
+
let promise = cache.get(key);
|
|
3338
|
+
if (promise == null) {
|
|
3339
|
+
promise = Promise.resolve(effective(url, options)).then((response) => {
|
|
3340
|
+
const cloned = normalizeResponse(structuredClone(response));
|
|
3341
|
+
recorded.set(key, {
|
|
3342
|
+
policy,
|
|
3343
|
+
url: normalizedUrl,
|
|
3344
|
+
response: structuredClone(cloned)
|
|
3345
|
+
});
|
|
3346
|
+
return cloned;
|
|
3347
|
+
});
|
|
3348
|
+
promise.catch(() => {});
|
|
3349
|
+
cache.set(key, promise);
|
|
3350
|
+
}
|
|
3351
|
+
return structuredClone(await promise);
|
|
3352
|
+
}, policy);
|
|
3353
|
+
loaders.set(policy, loader);
|
|
3354
|
+
return loader;
|
|
3355
|
+
},
|
|
3356
|
+
entries() {
|
|
3357
|
+
return [...recorded.values()];
|
|
3358
|
+
},
|
|
3359
|
+
release() {
|
|
3360
|
+
released = true;
|
|
3361
|
+
}
|
|
3362
|
+
};
|
|
3363
|
+
}
|
|
3364
|
+
function getBundledResponse(url) {
|
|
3365
|
+
if (!(url in _fedify_vocab_runtime.preloadedContexts)) return null;
|
|
3366
|
+
return {
|
|
3367
|
+
contextUrl: null,
|
|
3368
|
+
document: _fedify_vocab_runtime.preloadedContexts[url],
|
|
3369
|
+
documentUrl: url
|
|
3370
|
+
};
|
|
3371
|
+
}
|
|
3372
|
+
async function sha256(value) {
|
|
3373
|
+
return (0, byte_encodings_hex.encodeHex)(await crypto.subtle.digest("SHA-256", new TextEncoder().encode((0, json_canon.default)(value))));
|
|
3374
|
+
}
|
|
3375
|
+
function normalizeResponse(response) {
|
|
3376
|
+
return {
|
|
3377
|
+
contextUrl: response.contextUrl ?? null,
|
|
3378
|
+
document: response.document,
|
|
3379
|
+
documentUrl: response.documentUrl
|
|
3380
|
+
};
|
|
3381
|
+
}
|
|
3382
|
+
/**
|
|
3383
|
+
* Serializes the entries of a context snapshot for a queued message.
|
|
3384
|
+
* @param entries The entries.
|
|
3385
|
+
* @returns The serialized entries, which are plain JSON values.
|
|
3386
|
+
* @internal
|
|
3387
|
+
*/
|
|
3388
|
+
async function serializeContextSnapshot(entries) {
|
|
3389
|
+
const result = [];
|
|
3390
|
+
for (const entry of entries) {
|
|
3391
|
+
const response = normalizeResponse(entry.response);
|
|
3392
|
+
const hash = await sha256(response);
|
|
3393
|
+
const bundled = getBundledResponse(entry.url);
|
|
3394
|
+
if (bundled != null && await sha256(bundled) === hash) result.push({
|
|
3395
|
+
policy: entry.policy,
|
|
3396
|
+
url: entry.url,
|
|
3397
|
+
sha256: hash,
|
|
3398
|
+
bundled: true
|
|
3399
|
+
});
|
|
3400
|
+
else result.push({
|
|
3401
|
+
policy: entry.policy,
|
|
3402
|
+
url: entry.url,
|
|
3403
|
+
sha256: hash,
|
|
3404
|
+
response: structuredClone(response)
|
|
3405
|
+
});
|
|
3406
|
+
}
|
|
3407
|
+
return result;
|
|
3408
|
+
}
|
|
3409
|
+
function isRemoteDocument(value) {
|
|
3410
|
+
if (value == null || typeof value !== "object") return false;
|
|
3411
|
+
const record = value;
|
|
3412
|
+
return typeof record.documentUrl === "string" && "document" in record && (record.contextUrl == null || typeof record.contextUrl === "string");
|
|
3413
|
+
}
|
|
3414
|
+
/**
|
|
3415
|
+
* Restores the entries of a serialized context snapshot, verifying the
|
|
3416
|
+
* hashes of stored responses. Bundled entries are reconstructed from the
|
|
3417
|
+
* contexts this version of Fedify bundles.
|
|
3418
|
+
* @param entries The serialized entries.
|
|
3419
|
+
* @returns The entries.
|
|
3420
|
+
* @throws {ContextSnapshotMissError} If an entry is malformed, or its
|
|
3421
|
+
* content does not match its hash.
|
|
3422
|
+
* @internal
|
|
3423
|
+
*/
|
|
3424
|
+
async function deserializeContextSnapshot(entries) {
|
|
3425
|
+
if (!Array.isArray(entries)) throw new ContextSnapshotMissError("(snapshot)", "malformed snapshot");
|
|
3426
|
+
const result = [];
|
|
3427
|
+
for (const entry of entries) {
|
|
3428
|
+
if (entry == null || typeof entry !== "object" || entry.policy !== "standard" && entry.policy !== "normalization" || typeof entry.url !== "string" || typeof entry.sha256 !== "string") throw new ContextSnapshotMissError("(snapshot)", "malformed entry");
|
|
3429
|
+
let response;
|
|
3430
|
+
if (entry.bundled === true) {
|
|
3431
|
+
response = getBundledResponse(entry.url);
|
|
3432
|
+
if (response == null) throw new ContextSnapshotMissError(entry.url, "not bundled");
|
|
3433
|
+
} else if (isRemoteDocument(entry.response)) response = normalizeResponse(entry.response);
|
|
3434
|
+
else throw new ContextSnapshotMissError(entry.url, "malformed entry");
|
|
3435
|
+
if (entry.bundled !== true && await sha256(response) !== entry.sha256) throw new ContextSnapshotMissError(entry.url, "content mismatch");
|
|
3436
|
+
result.push({
|
|
3437
|
+
policy: entry.policy,
|
|
3438
|
+
url: entry.url,
|
|
3439
|
+
response: structuredClone(response)
|
|
3440
|
+
});
|
|
3441
|
+
}
|
|
3442
|
+
return result;
|
|
3443
|
+
}
|
|
3444
|
+
/**
|
|
3445
|
+
* Creates a context loader of a policy that serves only the responses of
|
|
3446
|
+
* a snapshot. It is marked as the effective loader of the policy.
|
|
3447
|
+
* @param entries The snapshot's entries.
|
|
3448
|
+
* @param policy The policy.
|
|
3449
|
+
* @returns The loader, which throws {@link ContextSnapshotMissError} for
|
|
3450
|
+
* anything else.
|
|
3451
|
+
* @internal
|
|
3452
|
+
*/
|
|
3453
|
+
function createSnapshotOnlyLoader(entries, policy) {
|
|
3454
|
+
const responses = /* @__PURE__ */ new Map();
|
|
3455
|
+
for (const entry of entries) if (entry.policy === policy) responses.set(entry.url, entry.response);
|
|
3456
|
+
return require_http.markEffectiveContextLoader(async (url) => {
|
|
3457
|
+
const response = responses.get(normalizeUrl(url));
|
|
3458
|
+
if (response == null) throw new ContextSnapshotMissError(url, "not in the snapshot");
|
|
3459
|
+
return structuredClone(response);
|
|
3460
|
+
}, policy);
|
|
3461
|
+
}
|
|
3462
|
+
/**
|
|
3463
|
+
* Creates a {@link ReleasableLoader}.
|
|
3464
|
+
* @param strict The loader to use until release.
|
|
3465
|
+
* @param fallback The loader to use after release.
|
|
3466
|
+
* @param policy The policy both loaders apply.
|
|
3467
|
+
* @returns The releasable loader.
|
|
3468
|
+
* @internal
|
|
3469
|
+
*/
|
|
3470
|
+
function createReleasableLoader(strict, fallback, policy) {
|
|
3471
|
+
let released = false;
|
|
3472
|
+
return {
|
|
3473
|
+
loader: require_http.markEffectiveContextLoader((url, options) => released ? fallback(url, options) : strict(url, options), policy),
|
|
3474
|
+
release() {
|
|
3475
|
+
released = true;
|
|
3476
|
+
}
|
|
3477
|
+
};
|
|
3478
|
+
}
|
|
3479
|
+
/**
|
|
3480
|
+
* Expands a JSON-LD document with the given context loader.
|
|
3481
|
+
* @param document The document.
|
|
3482
|
+
* @param contextLoader The context loader.
|
|
3483
|
+
* @returns The expanded document.
|
|
3484
|
+
* @internal
|
|
3485
|
+
*/
|
|
3486
|
+
async function expandDocument(document, contextLoader) {
|
|
3487
|
+
return await _fedify_vocab_runtime_jsonld.default.expand(document, { documentLoader: contextLoader });
|
|
3488
|
+
}
|
|
3489
|
+
/**
|
|
3490
|
+
* Computes the digest of an expanded document.
|
|
3491
|
+
* @param expanded The expanded document.
|
|
3492
|
+
* @returns The hex-encoded SHA-256 of its canonical JSON.
|
|
3493
|
+
* @internal
|
|
3494
|
+
*/
|
|
3495
|
+
function digestExpandedDocument(expanded) {
|
|
3496
|
+
return sha256(expanded);
|
|
3497
|
+
}
|
|
3498
|
+
/**
|
|
3499
|
+
* Deeply freezes a JSON value.
|
|
3500
|
+
* @param value The value.
|
|
3501
|
+
* @returns The same value, frozen.
|
|
3502
|
+
* @internal
|
|
3503
|
+
*/
|
|
3504
|
+
function deepFreeze(value) {
|
|
3505
|
+
if (value != null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
3506
|
+
Object.freeze(value);
|
|
3507
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
3508
|
+
}
|
|
3509
|
+
return value;
|
|
3510
|
+
}
|
|
3511
|
+
const ACTOR_IRI = "https://www.w3.org/ns/activitystreams#actor";
|
|
3512
|
+
/**
|
|
3513
|
+
* Gets the single root node of an expanded document.
|
|
3514
|
+
* @param expanded The expanded document.
|
|
3515
|
+
* @returns The root node, or `null` if there is not exactly one.
|
|
3516
|
+
* @internal
|
|
3517
|
+
*/
|
|
3518
|
+
function getRootNode(expanded) {
|
|
3519
|
+
if (expanded.length !== 1) return null;
|
|
3520
|
+
const node = expanded[0];
|
|
3521
|
+
if (node == null || typeof node !== "object" || Array.isArray(node)) return null;
|
|
3522
|
+
return node;
|
|
3523
|
+
}
|
|
3524
|
+
/**
|
|
3525
|
+
* Gets the values of a property of an expanded node, unwrapping a single
|
|
3526
|
+
* `@list` container the way generated vocabulary decoders do.
|
|
3527
|
+
* @param node The expanded node.
|
|
3528
|
+
* @param iri The property IRI.
|
|
3529
|
+
* @returns The values, or an empty array if the property is absent.
|
|
3530
|
+
* @internal
|
|
3531
|
+
*/
|
|
3532
|
+
function getPropertyValues(node, iri) {
|
|
3533
|
+
const values = node[iri];
|
|
3534
|
+
if (values == null) return [];
|
|
3535
|
+
if (!Array.isArray(values)) return [values];
|
|
3536
|
+
if (values.length === 1 && values[0] != null && typeof values[0] === "object" && "@list" in values[0]) {
|
|
3537
|
+
const list = values[0]["@list"];
|
|
3538
|
+
return Array.isArray(list) ? list : [list];
|
|
3539
|
+
}
|
|
3540
|
+
return values;
|
|
3541
|
+
}
|
|
3542
|
+
/**
|
|
3543
|
+
* Tells whether the root node of an expanded activity declares any `actor`
|
|
3544
|
+
* value at all, including blank nodes, anonymous or untyped nodes, and
|
|
3545
|
+
* literals, which ID accessors and typed parsing may drop.
|
|
3546
|
+
* @param expanded The expanded activity.
|
|
3547
|
+
* @returns `"absent"` if the root node declares no actor; `"present"`
|
|
3548
|
+
* otherwise, including when there is no single root node.
|
|
3549
|
+
* @internal
|
|
3550
|
+
*/
|
|
3551
|
+
function getActorPresence(expanded) {
|
|
3552
|
+
const root = getRootNode(expanded);
|
|
3553
|
+
if (root == null) return "present";
|
|
3554
|
+
return getPropertyValues(root, ACTOR_IRI).length < 1 ? "absent" : "present";
|
|
3555
|
+
}
|
|
3556
|
+
/**
|
|
3557
|
+
* Builds the serialized view of an authenticated activity. The document is
|
|
3558
|
+
* expanded through the snapshot, so that the snapshot holds every context
|
|
3559
|
+
* response the interpretation needs.
|
|
3560
|
+
* @param snapshot The delivery's context snapshot.
|
|
3561
|
+
* @param source Which document the activity was parsed from.
|
|
3562
|
+
* @returns The serialized view and the expanded activity.
|
|
3563
|
+
* @internal
|
|
3564
|
+
*/
|
|
3565
|
+
async function buildVerifiedActivityView(snapshot, source) {
|
|
3566
|
+
const loader = snapshot.loader(source.policy);
|
|
3567
|
+
const used = /* @__PURE__ */ new Set();
|
|
3568
|
+
const expanded = await expandDocument(source.document, require_http.markEffectiveContextLoader((url, options) => {
|
|
3569
|
+
used.add(normalizeUrl(url));
|
|
3570
|
+
return loader(url, options);
|
|
3571
|
+
}, source.policy));
|
|
3572
|
+
const entries = snapshot.entries().filter((entry) => entry.policy === source.policy && used.has(entry.url));
|
|
3573
|
+
const contexts = await serializeContextSnapshot(entries);
|
|
3574
|
+
const references = /* @__PURE__ */ new Set();
|
|
3575
|
+
collectContextReferences(source.document, source.baseUrl?.href, references);
|
|
3576
|
+
for (const entry of entries) {
|
|
3577
|
+
const { contextUrl, document } = entry.response;
|
|
3578
|
+
collectContextReferences(document, entry.url, references);
|
|
3579
|
+
if (contextUrl != null) collectContextReferences({ "@context": contextUrl }, entry.url, references);
|
|
3580
|
+
}
|
|
3581
|
+
return {
|
|
3582
|
+
replayable: [...references].every((url) => used.has(url)),
|
|
3583
|
+
view: {
|
|
3584
|
+
version: 1,
|
|
3585
|
+
policy: source.policy,
|
|
3586
|
+
source: source.source,
|
|
3587
|
+
baseUrl: source.baseUrl?.href ?? null,
|
|
3588
|
+
contexts,
|
|
3589
|
+
expandedDigest: await digestExpandedDocument(expanded)
|
|
3590
|
+
},
|
|
3591
|
+
expanded
|
|
3592
|
+
};
|
|
3593
|
+
}
|
|
3594
|
+
/**
|
|
3595
|
+
* Collects the remote context references of a JSON-LD document or context
|
|
3596
|
+
* document: every IRI given as a value of `@context` at any depth, including
|
|
3597
|
+
* scoped contexts of term definitions, and of `@import`.
|
|
3598
|
+
* @param value The document.
|
|
3599
|
+
* @param base The URL to resolve relative references against.
|
|
3600
|
+
* @param references The set to add the normalized references to.
|
|
3601
|
+
*/
|
|
3602
|
+
function collectContextReferences(value, base, references) {
|
|
3603
|
+
const add = (reference) => {
|
|
3604
|
+
let url = reference;
|
|
3605
|
+
if (base != null && URL.canParse(reference, base)) url = new URL(reference, base).href;
|
|
3606
|
+
references.add(normalizeUrl(url));
|
|
3607
|
+
};
|
|
3608
|
+
const collectContext = (context) => {
|
|
3609
|
+
if (typeof context === "string") add(context);
|
|
3610
|
+
else if (Array.isArray(context)) for (const item of context) collectContext(item);
|
|
3611
|
+
else if (context != null && typeof context === "object") {
|
|
3612
|
+
const imported = context["@import"];
|
|
3613
|
+
if (typeof imported === "string") add(imported);
|
|
3614
|
+
walk(context);
|
|
3615
|
+
}
|
|
3616
|
+
};
|
|
3617
|
+
const walk = (node) => {
|
|
3618
|
+
if (Array.isArray(node)) for (const item of node) walk(item);
|
|
3619
|
+
else if (node != null && typeof node === "object") for (const [key, item] of Object.entries(node)) if (key === "@context") collectContext(item);
|
|
3620
|
+
else walk(item);
|
|
3621
|
+
};
|
|
3622
|
+
walk(value);
|
|
3623
|
+
}
|
|
3624
|
+
/**
|
|
3625
|
+
* Validates the shape of a serialized view taken from a queued message.
|
|
3626
|
+
* @param value The value of the message's `verifiedView` field.
|
|
3627
|
+
* @returns The view, or `null` if it is malformed or of an unsupported
|
|
3628
|
+
* version.
|
|
3629
|
+
* @internal
|
|
3630
|
+
*/
|
|
3631
|
+
function parseSerializedVerifiedActivityView(value) {
|
|
3632
|
+
if (value == null || typeof value !== "object") return null;
|
|
3633
|
+
const view = value;
|
|
3634
|
+
if (view.version !== 1 || view.policy !== "standard" && view.policy !== "normalization" || view.source !== "activity" && view.source !== "normalizedActivity" || view.baseUrl !== null && (typeof view.baseUrl !== "string" || !URL.canParse(view.baseUrl)) || !Array.isArray(view.contexts) || typeof view.expandedDigest !== "string") return null;
|
|
3635
|
+
return view;
|
|
3636
|
+
}
|
|
3637
|
+
/**
|
|
3638
|
+
* Restores a view from a queued message without loading any context.
|
|
3639
|
+
* @param view The serialized view.
|
|
3640
|
+
* @param message The fields of the message the view refers to.
|
|
3641
|
+
* @returns The restored view.
|
|
3642
|
+
* @throws {ContextSnapshotMissError} If the view cannot be replayed exactly.
|
|
3643
|
+
* @internal
|
|
3644
|
+
*/
|
|
3645
|
+
async function restoreVerifiedActivityView(view, message) {
|
|
3646
|
+
const loader = createSnapshotOnlyLoader(await deserializeContextSnapshot(view.contexts), view.policy);
|
|
3647
|
+
if (view.source === "normalizedActivity" && message.normalizedActivity == null) throw new ContextSnapshotMissError("(normalizedActivity)", "the normalized activity is missing");
|
|
3648
|
+
const document = require_http.detachSignature(view.source === "activity" ? message.activity : message.normalizedActivity);
|
|
3649
|
+
const expanded = await expandDocument(document, loader);
|
|
3650
|
+
if (await digestExpandedDocument(expanded) !== view.expandedDigest) throw new ContextSnapshotMissError("(activity)", "the interpretation does not match the authenticated one");
|
|
3651
|
+
return {
|
|
3652
|
+
document,
|
|
3653
|
+
loader,
|
|
3654
|
+
expanded,
|
|
3655
|
+
baseUrl: view.baseUrl == null ? null : new URL(view.baseUrl)
|
|
3656
|
+
};
|
|
3657
|
+
}
|
|
3658
|
+
//#endregion
|
|
2987
3659
|
//#region src/federation/keycache.ts
|
|
2988
3660
|
const Temporal$9 = require_http.Temporal;
|
|
2989
3661
|
const KEY_CACHE_GENERATION = "3";
|
|
@@ -3648,9 +4320,10 @@ function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris }) {
|
|
|
3648
4320
|
*
|
|
3649
4321
|
* @param parameters The parameters for sending the activity.
|
|
3650
4322
|
* See also {@link SendActivityParameters}.
|
|
4323
|
+
* @param deps The dependencies, for tests.
|
|
3651
4324
|
* @throws {Error} If the activity fails to send.
|
|
3652
4325
|
*/
|
|
3653
|
-
function sendActivity(options) {
|
|
4326
|
+
function sendActivity(options, deps = {}) {
|
|
3654
4327
|
const tracerProvider = options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider();
|
|
3655
4328
|
return tracerProvider.getTracer(require_http.name, require_http.version).startActiveSpan("activitypub.send_activity", {
|
|
3656
4329
|
kind: _opentelemetry_api.SpanKind.CLIENT,
|
|
@@ -3666,7 +4339,7 @@ function sendActivity(options) {
|
|
|
3666
4339
|
}, () => sendActivityInternal({
|
|
3667
4340
|
...options,
|
|
3668
4341
|
tracerProvider
|
|
3669
|
-
}, span));
|
|
4342
|
+
}, span, deps));
|
|
3670
4343
|
} catch (e) {
|
|
3671
4344
|
span.setStatus({
|
|
3672
4345
|
code: _opentelemetry_api.SpanStatusCode.ERROR,
|
|
@@ -3751,13 +4424,14 @@ async function readLimitedResponseBody(response, maxBytes) {
|
|
|
3751
4424
|
totalBytes += value.length;
|
|
3752
4425
|
}
|
|
3753
4426
|
} finally {
|
|
4427
|
+
if (truncated) reader.cancel().catch(() => {});
|
|
3754
4428
|
reader.releaseLock();
|
|
3755
4429
|
}
|
|
3756
4430
|
let result = chunks.join("");
|
|
3757
4431
|
if (truncated) result += "… (truncated)";
|
|
3758
4432
|
return result;
|
|
3759
4433
|
}
|
|
3760
|
-
async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, allowPrivateAddress, headers, specDeterminer, meterProvider, tracerProvider }, span) {
|
|
4434
|
+
async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, allowPrivateAddress, headers, specDeterminer, meterProvider, tracerProvider }, span, deps) {
|
|
3761
4435
|
const logger = (0, _logtape_logtape.getLogger)([
|
|
3762
4436
|
"fedify",
|
|
3763
4437
|
"federation",
|
|
@@ -3766,9 +4440,11 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
|
|
|
3766
4440
|
const federationMetrics = require_http.getFederationMetrics(meterProvider);
|
|
3767
4441
|
const started = performance.now();
|
|
3768
4442
|
let deliverySuccess = false;
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
4443
|
+
const openUnvalidatedHop = deps.openHop ?? (allowPrivateAddress ? _fedify_vocab_runtime_internal.openDirectFetch : (url) => (0, _fedify_vocab_runtime_internal.openPublicFetch)(url));
|
|
4444
|
+
async function openHop(url) {
|
|
4445
|
+
let session;
|
|
4446
|
+
try {
|
|
4447
|
+
session = await openUnvalidatedHop(url);
|
|
3772
4448
|
} catch (error) {
|
|
3773
4449
|
if (error instanceof _fedify_vocab_runtime.UrlError && error.reason === "dns") {
|
|
3774
4450
|
logger.error("DNS resolution failed for URL: {url}", {
|
|
@@ -3785,82 +4461,90 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
|
|
|
3785
4461
|
});
|
|
3786
4462
|
throw error;
|
|
3787
4463
|
}
|
|
4464
|
+
return session;
|
|
3788
4465
|
}
|
|
4466
|
+
let initialSession;
|
|
3789
4467
|
try {
|
|
3790
|
-
await
|
|
4468
|
+
initialSession = await openHop(inbox.href);
|
|
3791
4469
|
} catch (error) {
|
|
3792
4470
|
if (error instanceof _fedify_vocab_runtime.FetchError) federationMetrics.recordDelivery(inbox, require_http.getDurationMs(started), false, activityType);
|
|
3793
4471
|
throw error;
|
|
3794
4472
|
}
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
}
|
|
3803
|
-
let rsaKey = null;
|
|
3804
|
-
for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
|
|
3805
|
-
rsaKey = key;
|
|
3806
|
-
break;
|
|
3807
|
-
}
|
|
3808
|
-
if (rsaKey == null && keys.length < 1) logger.debug("Sending the activity {activityId} to {inbox} without a signature.", {
|
|
3809
|
-
activityId,
|
|
3810
|
-
inbox: inbox.href
|
|
3811
|
-
});
|
|
3812
|
-
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.", {
|
|
3813
|
-
inbox: inbox.href,
|
|
3814
|
-
keys: keys.map((pair) => ({
|
|
3815
|
-
keyId: pair.keyId.href,
|
|
3816
|
-
privateKey: pair.privateKey
|
|
3817
|
-
}))
|
|
3818
|
-
});
|
|
3819
|
-
let response;
|
|
4473
|
+
let initialPending = true;
|
|
4474
|
+
const openDeliveryHop = (url) => {
|
|
4475
|
+
if (initialPending) {
|
|
4476
|
+
initialPending = false;
|
|
4477
|
+
return Promise.resolve(initialSession);
|
|
4478
|
+
}
|
|
4479
|
+
return openHop(url);
|
|
4480
|
+
};
|
|
3820
4481
|
try {
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
4482
|
+
headers = new Headers(headers);
|
|
4483
|
+
headers.set("Content-Type", "application/activity+json");
|
|
4484
|
+
const request = new Request(inbox, {
|
|
4485
|
+
method: "POST",
|
|
4486
|
+
headers,
|
|
4487
|
+
body: JSON.stringify(activity),
|
|
4488
|
+
redirect: "manual"
|
|
3825
4489
|
});
|
|
3826
|
-
|
|
3827
|
-
const
|
|
3828
|
-
|
|
4490
|
+
let rsaKey = null;
|
|
4491
|
+
for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
|
|
4492
|
+
rsaKey = key;
|
|
4493
|
+
break;
|
|
4494
|
+
}
|
|
4495
|
+
if (rsaKey == null && keys.length < 1) logger.debug("Sending the activity {activityId} to {inbox} without a signature.", {
|
|
3829
4496
|
activityId,
|
|
4497
|
+
inbox: inbox.href
|
|
4498
|
+
});
|
|
4499
|
+
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.", {
|
|
3830
4500
|
inbox: inbox.href,
|
|
3831
|
-
|
|
4501
|
+
keys: keys.map((pair) => ({
|
|
4502
|
+
keyId: pair.keyId.href,
|
|
4503
|
+
privateKey: pair.privateKey
|
|
4504
|
+
}))
|
|
3832
4505
|
});
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
}
|
|
3842
|
-
error = "";
|
|
3843
|
-
}
|
|
3844
|
-
logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
|
|
4506
|
+
let outcome;
|
|
4507
|
+
try {
|
|
4508
|
+
outcome = rsaKey == null ? await fetchWithValidatedRedirects(request, openDeliveryHop, (response) => consumeSendActivityResponse(response, inbox)) : await require_http.doubleKnockScoped(request, rsaKey, {
|
|
4509
|
+
tracerProvider,
|
|
4510
|
+
specDeterminer
|
|
4511
|
+
}, openDeliveryHop, (response) => consumeSendActivityResponse(response, inbox));
|
|
4512
|
+
} catch (error) {
|
|
4513
|
+
const failure = error instanceof _fedify_vocab_runtime.FetchError || error instanceof _fedify_vocab_runtime.UrlError ? error : createFetchError(inbox.href, error);
|
|
4514
|
+
logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
|
|
3845
4515
|
activityId,
|
|
3846
4516
|
inbox: inbox.href,
|
|
3847
|
-
|
|
3848
|
-
statusText: response.statusText,
|
|
3849
|
-
error
|
|
4517
|
+
error: failure
|
|
3850
4518
|
});
|
|
3851
|
-
|
|
4519
|
+
federationMetrics.recordDelivery(inbox, require_http.getDurationMs(started), false, activityType);
|
|
4520
|
+
throw failure;
|
|
4521
|
+
}
|
|
4522
|
+
try {
|
|
4523
|
+
if (!outcome.ok) {
|
|
4524
|
+
const { status, statusText, error, responseHeaders } = outcome;
|
|
4525
|
+
logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
|
|
4526
|
+
activityId,
|
|
4527
|
+
inbox: inbox.href,
|
|
4528
|
+
status,
|
|
4529
|
+
statusText,
|
|
4530
|
+
error
|
|
4531
|
+
});
|
|
4532
|
+
throw new SendActivityError(inbox, status, `Failed to send activity ${activityId} to ${inbox.href} (${status} ${statusText}):\n${error}`, error, responseHeaders);
|
|
4533
|
+
}
|
|
4534
|
+
deliverySuccess = true;
|
|
4535
|
+
const eventAttributes = {
|
|
4536
|
+
"activitypub.inbox.url": inbox.href,
|
|
4537
|
+
"activitypub.activity.id": activityId ?? ""
|
|
4538
|
+
};
|
|
4539
|
+
if (activityType != null) eventAttributes["activitypub.activity.type"] = activityType;
|
|
4540
|
+
const actorId = getActivityActorId(activity);
|
|
4541
|
+
if (actorId != null) eventAttributes["activitypub.actor.id"] = actorId;
|
|
4542
|
+
span.addEvent("activitypub.activity.sent", eventAttributes);
|
|
4543
|
+
} finally {
|
|
4544
|
+
federationMetrics.recordDelivery(inbox, require_http.getDurationMs(started), deliverySuccess, activityType);
|
|
3852
4545
|
}
|
|
3853
|
-
deliverySuccess = true;
|
|
3854
|
-
const eventAttributes = {
|
|
3855
|
-
"activitypub.inbox.url": inbox.href,
|
|
3856
|
-
"activitypub.activity.id": activityId ?? ""
|
|
3857
|
-
};
|
|
3858
|
-
if (activityType != null) eventAttributes["activitypub.activity.type"] = activityType;
|
|
3859
|
-
const actorId = getActivityActorId(activity);
|
|
3860
|
-
if (actorId != null) eventAttributes["activitypub.actor.id"] = actorId;
|
|
3861
|
-
span.addEvent("activitypub.activity.sent", eventAttributes);
|
|
3862
4546
|
} finally {
|
|
3863
|
-
|
|
4547
|
+
if (initialPending) await initialSession.close();
|
|
3864
4548
|
}
|
|
3865
4549
|
}
|
|
3866
4550
|
function createFetchError(url, cause) {
|
|
@@ -3868,10 +4552,75 @@ function createFetchError(url, cause) {
|
|
|
3868
4552
|
error.cause = cause;
|
|
3869
4553
|
return error;
|
|
3870
4554
|
}
|
|
3871
|
-
|
|
4555
|
+
/**
|
|
4556
|
+
* Starts cancelling the body of a response without waiting for it to settle.
|
|
4557
|
+
* The cancellation of a branch of a tee (e.g., a body cloned by a fetch mock)
|
|
4558
|
+
* settles only after the other branch is cancelled too, per the Streams
|
|
4559
|
+
* standard, so awaiting it could block forever. Failures are only logged.
|
|
4560
|
+
*/
|
|
4561
|
+
function cancelResponseBody(response, url) {
|
|
4562
|
+
response.body?.cancel().catch((error) => {
|
|
4563
|
+
(0, _logtape_logtape.getLogger)([
|
|
4564
|
+
"fedify",
|
|
4565
|
+
"federation",
|
|
4566
|
+
"outbox"
|
|
4567
|
+
]).debug("Failed to cancel the response body from {url}: {error}", {
|
|
4568
|
+
url,
|
|
4569
|
+
error
|
|
4570
|
+
});
|
|
4571
|
+
});
|
|
4572
|
+
}
|
|
4573
|
+
/**
|
|
4574
|
+
* Reads what is needed from the final response of a delivery while its
|
|
4575
|
+
* session is open: the body of a successful response is cancelled, and at
|
|
4576
|
+
* most 1 KiB of an error response body is read.
|
|
4577
|
+
*/
|
|
4578
|
+
async function consumeSendActivityResponse(response, inbox) {
|
|
4579
|
+
if (response.ok) {
|
|
4580
|
+
cancelResponseBody(response, response.url || inbox.href);
|
|
4581
|
+
return { ok: true };
|
|
4582
|
+
}
|
|
4583
|
+
let error;
|
|
4584
|
+
try {
|
|
4585
|
+
error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
|
|
4586
|
+
} catch (_) {
|
|
4587
|
+
error = "";
|
|
4588
|
+
}
|
|
4589
|
+
return {
|
|
4590
|
+
ok: false,
|
|
4591
|
+
status: response.status,
|
|
4592
|
+
statusText: response.statusText,
|
|
4593
|
+
error,
|
|
4594
|
+
responseHeaders: new Headers(response.headers)
|
|
4595
|
+
};
|
|
4596
|
+
}
|
|
4597
|
+
/**
|
|
4598
|
+
* Sends a request without signing it, following redirects manually like the
|
|
4599
|
+
* Fetch standard does. Every hop is opened with `openHop()`, which validates
|
|
4600
|
+
* the destination, after the previous hop is closed. The final response is
|
|
4601
|
+
* passed to `consume()`, and its session is closed after that.
|
|
4602
|
+
* This is exported only for tests; it is not exported from any entry point.
|
|
4603
|
+
* @param request The request to send.
|
|
4604
|
+
* @param openHop The function to open a session for each hop.
|
|
4605
|
+
* @param consume The function to consume the final response.
|
|
4606
|
+
* @returns The value returned by `consume`.
|
|
4607
|
+
*/
|
|
4608
|
+
async function fetchWithValidatedRedirects(request, openHop, consume) {
|
|
4609
|
+
const logger = (0, _logtape_logtape.getLogger)([
|
|
4610
|
+
"fedify",
|
|
4611
|
+
"federation",
|
|
4612
|
+
"outbox"
|
|
4613
|
+
]);
|
|
3872
4614
|
let body = await request.clone().text();
|
|
3873
4615
|
for (let redirects = 0;; redirects++) {
|
|
3874
|
-
const
|
|
4616
|
+
const session = await openHop(request.url);
|
|
4617
|
+
let response;
|
|
4618
|
+
try {
|
|
4619
|
+
response = await session.fetch(request, { redirect: "manual" });
|
|
4620
|
+
} catch (error) {
|
|
4621
|
+
await closeAfterError(session, logger);
|
|
4622
|
+
throw error;
|
|
4623
|
+
}
|
|
3875
4624
|
const location = response.headers.get("Location");
|
|
3876
4625
|
if (![
|
|
3877
4626
|
301,
|
|
@@ -3879,11 +4628,21 @@ async function fetchWithValidatedRedirects(request, validateUrl) {
|
|
|
3879
4628
|
303,
|
|
3880
4629
|
307,
|
|
3881
4630
|
308
|
|
3882
|
-
].includes(response.status) || location == null)
|
|
3883
|
-
|
|
4631
|
+
].includes(response.status) || location == null) try {
|
|
4632
|
+
return await consume(response);
|
|
4633
|
+
} catch (error) {
|
|
4634
|
+
logger.debug("Failed to consume the response from {url}; closing the session: {error}", {
|
|
4635
|
+
url: request.url,
|
|
4636
|
+
error
|
|
4637
|
+
});
|
|
4638
|
+
throw error;
|
|
4639
|
+
} finally {
|
|
4640
|
+
await session.close();
|
|
4641
|
+
}
|
|
4642
|
+
cancelResponseBody(response, request.url);
|
|
4643
|
+
await session.close();
|
|
3884
4644
|
if (redirects >= 20) throw new TypeError("Too many redirects");
|
|
3885
4645
|
const url = new URL(location, request.url);
|
|
3886
|
-
await validateUrl(url.href);
|
|
3887
4646
|
const headers = new Headers(request.headers);
|
|
3888
4647
|
if (url.origin !== new URL(request.url).origin) {
|
|
3889
4648
|
headers.delete("Authorization");
|
|
@@ -3911,6 +4670,16 @@ async function fetchWithValidatedRedirects(request, validateUrl) {
|
|
|
3911
4670
|
});
|
|
3912
4671
|
}
|
|
3913
4672
|
}
|
|
4673
|
+
async function closeAfterError(session, logger) {
|
|
4674
|
+
try {
|
|
4675
|
+
await session.close();
|
|
4676
|
+
} catch (error) {
|
|
4677
|
+
logger.debug("Failed to close the session for {url}: {error}", {
|
|
4678
|
+
url: session.url.href,
|
|
4679
|
+
error
|
|
4680
|
+
});
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
3914
4683
|
/**
|
|
3915
4684
|
* An error that is thrown when an activity fails to send to a remote inbox.
|
|
3916
4685
|
* It contains structured information about the failure, including the HTTP
|
|
@@ -5405,11 +6174,12 @@ async function handleInbox(request, options) {
|
|
|
5405
6174
|
}, async (span) => {
|
|
5406
6175
|
if (options.recipient != null) span.setAttribute("fedify.inbox.recipient", options.recipient);
|
|
5407
6176
|
const spanContext = span.spanContext();
|
|
6177
|
+
const snapshot = createContextSnapshot(options.context.contextLoader);
|
|
5408
6178
|
try {
|
|
5409
6179
|
return await (0, _logtape_logtape.withContext)({
|
|
5410
6180
|
traceId: spanContext.traceId,
|
|
5411
6181
|
spanId: spanContext.spanId
|
|
5412
|
-
}, () => handleInboxInternal(request, options, span));
|
|
6182
|
+
}, () => handleInboxInternal(request, options, span, snapshot));
|
|
5413
6183
|
} catch (e) {
|
|
5414
6184
|
options.observation.hasException = true;
|
|
5415
6185
|
span.setStatus({
|
|
@@ -5418,6 +6188,7 @@ async function handleInbox(request, options) {
|
|
|
5418
6188
|
});
|
|
5419
6189
|
throw e;
|
|
5420
6190
|
} finally {
|
|
6191
|
+
snapshot.release();
|
|
5421
6192
|
options.observation.project(span);
|
|
5422
6193
|
span.end();
|
|
5423
6194
|
}
|
|
@@ -5429,9 +6200,10 @@ async function handleInbox(request, options) {
|
|
|
5429
6200
|
* @param request The HTTP request.
|
|
5430
6201
|
* @param options The parameters for handling the inbox.
|
|
5431
6202
|
* @param span The OpenTelemetry span for tracing.
|
|
6203
|
+
* @param snapshot The context snapshot of the delivery.
|
|
5432
6204
|
* @returns A promise that resolves to an HTTP response.
|
|
5433
6205
|
*/
|
|
5434
|
-
async function handleInboxInternal(request, parameters, span) {
|
|
6206
|
+
async function handleInboxInternal(request, parameters, span, snapshot) {
|
|
5435
6207
|
const { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, publicKeyTtl, queue, actorDispatcher, inboxListeners, inboxErrorHandler, unverifiedActivityHandler, onNotFound, signatureTimeWindow, maxHttpSignatures, skipSignatureVerification, inboxChallengePolicy, meterProvider, tracerProvider, portableInbox } = parameters;
|
|
5436
6208
|
const observation = parameters.observation;
|
|
5437
6209
|
const signatureObservation = { [require_http.verificationObservation]: observation.verification };
|
|
@@ -5545,6 +6317,8 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5545
6317
|
tracerProvider,
|
|
5546
6318
|
keyTtl: publicKeyTtl
|
|
5547
6319
|
});
|
|
6320
|
+
const standardLoader = snapshot.loader("standard");
|
|
6321
|
+
const normalizationLoader = snapshot.loader("normalization");
|
|
5548
6322
|
const jsonWithoutSig = require_http.detachSignature(json);
|
|
5549
6323
|
const hasLdSignature = require_http.hasSignature(json);
|
|
5550
6324
|
const canAttemptAlternateAuthAfterLdSignatureFailure = skipSignatureVerification || hasHttpSignatureHeaders(request) || hasObjectIntegrityProof(jsonWithoutSig);
|
|
@@ -5580,9 +6354,10 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5580
6354
|
let compactedJson = json;
|
|
5581
6355
|
let compactedJsonWithoutSig = jsonWithoutSig;
|
|
5582
6356
|
let ldSigVerified = false;
|
|
6357
|
+
let ldSigKey = null;
|
|
5583
6358
|
if (hasLdSignature) {
|
|
5584
6359
|
try {
|
|
5585
|
-
compactedJson = await require_http.compactJsonLd(json,
|
|
6360
|
+
compactedJson = await require_http.compactJsonLd(json, normalizationLoader);
|
|
5586
6361
|
} catch (error) {
|
|
5587
6362
|
if (isInvalidJsonLdError(error)) {
|
|
5588
6363
|
logger.error("Failed to parse JSON-LD:\n{error}", {
|
|
@@ -5618,16 +6393,18 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5618
6393
|
if (compactedJson !== json) {
|
|
5619
6394
|
compactedJsonWithoutSig = require_http.detachSignature(compactedJson);
|
|
5620
6395
|
try {
|
|
5621
|
-
|
|
6396
|
+
const ldResult = await require_http.verifyCompactJsonLdDetailed(compactedJson, {
|
|
5622
6397
|
...signatureObservation,
|
|
5623
|
-
contextLoader:
|
|
6398
|
+
contextLoader: normalizationLoader,
|
|
5624
6399
|
documentLoader: ctx.documentLoader,
|
|
5625
6400
|
keyCache,
|
|
5626
6401
|
meterProvider,
|
|
5627
6402
|
tracerProvider
|
|
5628
6403
|
});
|
|
6404
|
+
ldSigVerified = ldResult.verified;
|
|
6405
|
+
ldSigKey = ldResult.key;
|
|
5629
6406
|
} catch (error) {
|
|
5630
|
-
if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig,
|
|
6407
|
+
if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig, normalizationLoader)) return await respondInvalidActivity(error);
|
|
5631
6408
|
if (isInvalidJsonLdError(error)) {
|
|
5632
6409
|
logger.error("Failed to parse JSON-LD:\n{error}", {
|
|
5633
6410
|
recipient,
|
|
@@ -5645,12 +6422,12 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5645
6422
|
if (!canAttemptAlternateAuthAfterLdSignatureFailure) throw error;
|
|
5646
6423
|
if (!skipSignatureVerification) try {
|
|
5647
6424
|
await _fedify_vocab.Object.fromJsonLd(compactedJson, {
|
|
5648
|
-
contextLoader:
|
|
6425
|
+
contextLoader: normalizationLoader,
|
|
5649
6426
|
documentLoader: ctx.documentLoader,
|
|
5650
6427
|
tracerProvider
|
|
5651
6428
|
});
|
|
5652
6429
|
} catch (parseError) {
|
|
5653
|
-
if (parseError instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig,
|
|
6430
|
+
if (parseError instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig, normalizationLoader)) return await respondInvalidActivity(parseError);
|
|
5654
6431
|
if (isInvalidJsonLdError(parseError)) {
|
|
5655
6432
|
logger.error("Failed to parse JSON-LD:\n{error}", {
|
|
5656
6433
|
recipient,
|
|
@@ -5674,6 +6451,33 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5674
6451
|
let activity = null;
|
|
5675
6452
|
let activityVerified = false;
|
|
5676
6453
|
let proofVerified = false;
|
|
6454
|
+
let documentSigners = [];
|
|
6455
|
+
let requestSigners = [];
|
|
6456
|
+
let viewSource;
|
|
6457
|
+
const rawViewSource = {
|
|
6458
|
+
policy: "standard",
|
|
6459
|
+
source: "activity",
|
|
6460
|
+
document: jsonWithoutSig,
|
|
6461
|
+
baseUrl: null
|
|
6462
|
+
};
|
|
6463
|
+
const isActorlessCandidate = async (candidate, source) => {
|
|
6464
|
+
if (skipSignatureVerification) return false;
|
|
6465
|
+
if (inboxListeners?.dispatchWithClass(candidate)?.options.allowActorless !== true) return false;
|
|
6466
|
+
try {
|
|
6467
|
+
return getActorPresence(await expandDocument(source.document, snapshot.loader(source.policy))) === "absent";
|
|
6468
|
+
} catch {
|
|
6469
|
+
return false;
|
|
6470
|
+
}
|
|
6471
|
+
};
|
|
6472
|
+
const verifyProofs = (contextLoader = standardLoader) => require_http.verifyObjectDetailed(_fedify_vocab.Activity, jsonWithoutSig, {
|
|
6473
|
+
...signatureObservation,
|
|
6474
|
+
contextLoader,
|
|
6475
|
+
documentLoader: ctx.documentLoader,
|
|
6476
|
+
keyCache,
|
|
6477
|
+
meterProvider,
|
|
6478
|
+
tracerProvider,
|
|
6479
|
+
verifyPortableObject: ctx.verifyPortableObject
|
|
6480
|
+
});
|
|
5677
6481
|
if (ldSigVerified) {
|
|
5678
6482
|
logger.debug("Linked Data Signatures are verified.", {
|
|
5679
6483
|
recipient,
|
|
@@ -5682,18 +6486,49 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5682
6486
|
try {
|
|
5683
6487
|
activity = observation.activity = await _fedify_vocab.Activity.fromJsonLd(compactedJsonWithoutSig, {
|
|
5684
6488
|
...ctx,
|
|
5685
|
-
contextLoader:
|
|
6489
|
+
contextLoader: normalizationLoader
|
|
5686
6490
|
});
|
|
5687
6491
|
} catch (error) {
|
|
5688
|
-
if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig,
|
|
6492
|
+
if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig, normalizationLoader)) return await respondInvalidActivity(error);
|
|
5689
6493
|
if (!isPermanentActivityParseError(error)) throw error;
|
|
5690
6494
|
return await respondInvalidActivity(error);
|
|
5691
6495
|
}
|
|
5692
6496
|
activityVerified = true;
|
|
6497
|
+
viewSource = {
|
|
6498
|
+
policy: "normalization",
|
|
6499
|
+
source: "normalizedActivity",
|
|
6500
|
+
document: compactedJsonWithoutSig,
|
|
6501
|
+
baseUrl: null
|
|
6502
|
+
};
|
|
6503
|
+
if (ldSigKey?.ownerId != null) documentSigners = [{
|
|
6504
|
+
kind: "actor",
|
|
6505
|
+
id: ldSigKey.ownerId,
|
|
6506
|
+
mechanism: "linkedDataSignatures",
|
|
6507
|
+
keyId: ldSigKey.id
|
|
6508
|
+
}];
|
|
6509
|
+
if (await isActorlessCandidate(activity, viewSource)) {
|
|
6510
|
+
let result = null;
|
|
6511
|
+
try {
|
|
6512
|
+
result = await verifyProofs();
|
|
6513
|
+
} catch (error) {
|
|
6514
|
+
if (!isPermanentActivityParseError(error)) throw error;
|
|
6515
|
+
logger.debug("Failed to verify the Object Integrity Proofs of the activity without an actor:\n{error}", {
|
|
6516
|
+
recipient,
|
|
6517
|
+
error
|
|
6518
|
+
});
|
|
6519
|
+
}
|
|
6520
|
+
if (result?.status === "unavailable") throw result.error;
|
|
6521
|
+
if (result?.status === "verified" && result.signers.length > 0) {
|
|
6522
|
+
activity = observation.activity = result.object;
|
|
6523
|
+
documentSigners = toProofPrincipals(result.signers);
|
|
6524
|
+
viewSource = rawViewSource;
|
|
6525
|
+
proofVerified = true;
|
|
6526
|
+
}
|
|
6527
|
+
}
|
|
5693
6528
|
if (!skipSignatureVerification && isPortableActivity(activity)) try {
|
|
5694
6529
|
proofVerified = await require_http.verifyObject(_fedify_vocab.Activity, jsonWithoutSig, {
|
|
5695
6530
|
...signatureObservation,
|
|
5696
|
-
contextLoader:
|
|
6531
|
+
contextLoader: standardLoader,
|
|
5697
6532
|
documentLoader: ctx.documentLoader,
|
|
5698
6533
|
keyCache,
|
|
5699
6534
|
meterProvider,
|
|
@@ -5711,18 +6546,11 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5711
6546
|
recipient,
|
|
5712
6547
|
json
|
|
5713
6548
|
});
|
|
6549
|
+
let proofResult;
|
|
5714
6550
|
try {
|
|
5715
|
-
|
|
5716
|
-
...signatureObservation,
|
|
5717
|
-
contextLoader: require_http.wrapContextLoaderForJsonLd(ctx.contextLoader),
|
|
5718
|
-
documentLoader: ctx.documentLoader,
|
|
5719
|
-
keyCache,
|
|
5720
|
-
meterProvider,
|
|
5721
|
-
tracerProvider,
|
|
5722
|
-
verifyPortableObject: ctx.verifyPortableObject
|
|
5723
|
-
});
|
|
6551
|
+
proofResult = await verifyProofs();
|
|
5724
6552
|
} catch (error) {
|
|
5725
|
-
if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(jsonWithoutSig,
|
|
6553
|
+
if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(jsonWithoutSig, standardLoader)) return await respondInvalidActivity(error);
|
|
5726
6554
|
if (deferredLdSignatureError != null) {
|
|
5727
6555
|
logger.debug("Object Integrity Proof fallback did not supersede a deferred Linked Data Signature failure:\n{error}", {
|
|
5728
6556
|
recipient,
|
|
@@ -5758,6 +6586,22 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5758
6586
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
5759
6587
|
});
|
|
5760
6588
|
}
|
|
6589
|
+
if (proofResult.status === "verified") if (proofResult.signers.length < 1 && await isActorlessCandidate(proofResult.object, rawViewSource)) activity = null;
|
|
6590
|
+
else {
|
|
6591
|
+
activity = proofResult.object;
|
|
6592
|
+
documentSigners = toProofPrincipals(proofResult.signers);
|
|
6593
|
+
viewSource = rawViewSource;
|
|
6594
|
+
}
|
|
6595
|
+
else if (proofResult.status === "unavailable") {
|
|
6596
|
+
let parsed = null;
|
|
6597
|
+
try {
|
|
6598
|
+
parsed = await _fedify_vocab.Activity.fromJsonLd(jsonWithoutSig, {
|
|
6599
|
+
...ctx,
|
|
6600
|
+
contextLoader: standardLoader
|
|
6601
|
+
});
|
|
6602
|
+
} catch {}
|
|
6603
|
+
if (parsed != null && await isActorlessCandidate(parsed, rawViewSource)) throw proofResult.error;
|
|
6604
|
+
}
|
|
5761
6605
|
if (activity == null) logger.debug("Object Integrity Proofs are not verified.", {
|
|
5762
6606
|
recipient,
|
|
5763
6607
|
activity: json
|
|
@@ -5887,14 +6731,28 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5887
6731
|
try {
|
|
5888
6732
|
activity = observation.activity = await _fedify_vocab.Activity.fromJsonLd(jsonWithoutSig, {
|
|
5889
6733
|
...ctx,
|
|
5890
|
-
contextLoader:
|
|
6734
|
+
contextLoader: standardLoader
|
|
5891
6735
|
});
|
|
5892
6736
|
} catch (error) {
|
|
5893
6737
|
if (!isPermanentActivityParseError(error)) throw error;
|
|
5894
6738
|
return await respondInvalidActivity(error);
|
|
5895
6739
|
}
|
|
6740
|
+
viewSource = rawViewSource;
|
|
5896
6741
|
}
|
|
5897
6742
|
observation.activity = activity;
|
|
6743
|
+
let view;
|
|
6744
|
+
try {
|
|
6745
|
+
view = await buildVerifiedActivityView(snapshot, viewSource ?? rawViewSource);
|
|
6746
|
+
} catch (error) {
|
|
6747
|
+
if (!isPermanentActivityParseError(error)) throw error;
|
|
6748
|
+
return await respondInvalidActivity(error);
|
|
6749
|
+
}
|
|
6750
|
+
const actorless = !skipSignatureVerification && getActorPresence(view.expanded) === "absent" && inboxListeners?.dispatchWithClass(activity)?.options.allowActorless === true;
|
|
6751
|
+
span.setAttribute("activitypub.activity.actorless", actorless);
|
|
6752
|
+
if (actorless && httpSigKey != null) {
|
|
6753
|
+
const principal = getHttpKeyPrincipal(httpSigKey);
|
|
6754
|
+
if (principal != null) requestSigners = [principal];
|
|
6755
|
+
}
|
|
5898
6756
|
observation.stage = "policy";
|
|
5899
6757
|
observation.result = {
|
|
5900
6758
|
disposition: "rejected",
|
|
@@ -5938,7 +6796,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5938
6796
|
const rejection = await require_http.observeAttempt(signatureObservation, "objectIntegrity", async (verification) => {
|
|
5939
6797
|
const rejection = await verifyPortableActivityId(activity.id, jsonWithoutSig, {
|
|
5940
6798
|
[require_http.verificationObservation]: verification,
|
|
5941
|
-
contextLoader:
|
|
6799
|
+
contextLoader: standardLoader,
|
|
5942
6800
|
documentLoader: ctx.documentLoader,
|
|
5943
6801
|
keyCache,
|
|
5944
6802
|
meterProvider,
|
|
@@ -5976,7 +6834,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5976
6834
|
});
|
|
5977
6835
|
}
|
|
5978
6836
|
}
|
|
5979
|
-
if (httpSigKey != null && !await require_portable_collection.doesActorOwnKey(activity, httpSigKey, ctx)) {
|
|
6837
|
+
if (httpSigKey != null && !actorless && !await require_portable_collection.doesActorOwnKey(activity, httpSigKey, ctx)) {
|
|
5980
6838
|
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
5981
6839
|
observation.authentication = {
|
|
5982
6840
|
status: "rejected",
|
|
@@ -6002,6 +6860,66 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
6002
6860
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
6003
6861
|
});
|
|
6004
6862
|
}
|
|
6863
|
+
if (httpSigKey != null && !actorless && activity.actorId != null) requestSigners = [{
|
|
6864
|
+
kind: "actor",
|
|
6865
|
+
id: activity.actorId,
|
|
6866
|
+
mechanism: "httpSignatures",
|
|
6867
|
+
keyId: httpSigKey.id
|
|
6868
|
+
}];
|
|
6869
|
+
if (actorless) {
|
|
6870
|
+
const check = checkActorlessActivity(activity, {
|
|
6871
|
+
verified: true,
|
|
6872
|
+
requestSigners,
|
|
6873
|
+
documentSigners,
|
|
6874
|
+
originAuthenticated: [],
|
|
6875
|
+
expandedActivity: [],
|
|
6876
|
+
baseUrl: null
|
|
6877
|
+
});
|
|
6878
|
+
if (check.status === "noAuthenticatedPrincipal") {
|
|
6879
|
+
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
6880
|
+
observation.authentication = {
|
|
6881
|
+
status: "rejected",
|
|
6882
|
+
reason: { type: "noAuthenticatedPrincipal" }
|
|
6883
|
+
};
|
|
6884
|
+
logger.error("The activity {activityId} declares no actor, and no principal is authenticated for it.", {
|
|
6885
|
+
activity: json,
|
|
6886
|
+
recipient,
|
|
6887
|
+
activityId: activity.id?.href
|
|
6888
|
+
});
|
|
6889
|
+
span.setStatus({
|
|
6890
|
+
code: _opentelemetry_api.SpanStatusCode.ERROR,
|
|
6891
|
+
message: "No principal is authenticated for the activity."
|
|
6892
|
+
});
|
|
6893
|
+
return new Response("No principal is authenticated for the activity.", {
|
|
6894
|
+
status: 401,
|
|
6895
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
6896
|
+
});
|
|
6897
|
+
} else if (check.status === "activityOriginMismatch") {
|
|
6898
|
+
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
6899
|
+
observation.authentication = {
|
|
6900
|
+
status: "rejected",
|
|
6901
|
+
reason: {
|
|
6902
|
+
type: "activityOriginMismatch",
|
|
6903
|
+
activityId: new URL(activity.id.href),
|
|
6904
|
+
actorIds: check.principals.map((principal) => principal.id)
|
|
6905
|
+
}
|
|
6906
|
+
};
|
|
6907
|
+
logger.error("The activity ({activityId}) does not have the same origin as the principals that authenticated it.", {
|
|
6908
|
+
activity: json,
|
|
6909
|
+
recipient,
|
|
6910
|
+
activityId: activity.id?.href,
|
|
6911
|
+
principals: check.principals.map((principal) => principal.id.href)
|
|
6912
|
+
});
|
|
6913
|
+
span.setStatus({
|
|
6914
|
+
code: _opentelemetry_api.SpanStatusCode.ERROR,
|
|
6915
|
+
message: `The activity (${activity.id?.href}) does not have the same origin as the principals that authenticated it.`
|
|
6916
|
+
});
|
|
6917
|
+
return new Response("The activity and its signers do not have the same origin.", {
|
|
6918
|
+
status: 401,
|
|
6919
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
6920
|
+
});
|
|
6921
|
+
}
|
|
6922
|
+
}
|
|
6005
6923
|
if (!skipSignatureVerification) {
|
|
6006
6924
|
const compoundJson = Array.isArray(json) ? json : jsonWithoutSig;
|
|
6007
6925
|
if (inspectCompoundPortableObjectApplicability(compoundJson, INBOX_COMPOUND_PROOF_LIMITS) !== "absent") {
|
|
@@ -6061,7 +6979,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
6061
6979
|
return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
|
|
6062
6980
|
}
|
|
6063
6981
|
}
|
|
6064
|
-
if (!skipSignatureVerification && activity.id != null && !actorsHaveSameOrigin(activity)) {
|
|
6982
|
+
if (!skipSignatureVerification && !actorless && activity.id != null && !actorsHaveSameOrigin(activity)) {
|
|
6065
6983
|
observation.authentication = {
|
|
6066
6984
|
status: "rejected",
|
|
6067
6985
|
reason: {
|
|
@@ -6090,7 +7008,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
6090
7008
|
attempts: observation.verification.attempts.filter((attempt) => attempt.status === "verified")
|
|
6091
7009
|
};
|
|
6092
7010
|
observation.stage = "dispatch";
|
|
6093
|
-
if (activity.actorId == null) {
|
|
7011
|
+
if (!actorless && activity.actorId == null) {
|
|
6094
7012
|
observation.result = {
|
|
6095
7013
|
disposition: "rejected",
|
|
6096
7014
|
reason: "missingActor"
|
|
@@ -6114,6 +7032,17 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
6114
7032
|
originalJson: json,
|
|
6115
7033
|
normalizedActivity: hasLdSignature && compactedJson !== json ? compactedJson : void 0,
|
|
6116
7034
|
ldSignatureVerified: hasLdSignature ? ldSigVerified : void 0,
|
|
7035
|
+
authentication: createAuthenticationEvidence({
|
|
7036
|
+
verified: !skipSignatureVerification,
|
|
7037
|
+
requestSigners,
|
|
7038
|
+
documentSigners,
|
|
7039
|
+
originAuthenticated: [],
|
|
7040
|
+
expandedActivity: deepFreeze(view.expanded),
|
|
7041
|
+
baseUrl: null
|
|
7042
|
+
}),
|
|
7043
|
+
verifiedView: view.view,
|
|
7044
|
+
replayable: view.replayable,
|
|
7045
|
+
actorless,
|
|
6117
7046
|
activity,
|
|
6118
7047
|
recipient,
|
|
6119
7048
|
inboxListeners,
|
|
@@ -6838,6 +7767,42 @@ const MIN_COMPONENTS = [
|
|
|
6838
7767
|
"@authority"
|
|
6839
7768
|
];
|
|
6840
7769
|
/**
|
|
7770
|
+
* Turns the signers of verified Object Integrity Proofs into principals.
|
|
7771
|
+
*/
|
|
7772
|
+
function toProofPrincipals(signers) {
|
|
7773
|
+
return signers.map((signer) => ({
|
|
7774
|
+
kind: signer.kind,
|
|
7775
|
+
id: signer.id,
|
|
7776
|
+
mechanism: "objectIntegrityProofs",
|
|
7777
|
+
keyId: signer.keyId
|
|
7778
|
+
}));
|
|
7779
|
+
}
|
|
7780
|
+
/**
|
|
7781
|
+
* Gets the principal of an HTTP Signature key, whose owner was confirmed by
|
|
7782
|
+
* the owner's own document, or by the signed document of a portable actor,
|
|
7783
|
+
* when the key was fetched.
|
|
7784
|
+
*/
|
|
7785
|
+
function getHttpKeyPrincipal(key) {
|
|
7786
|
+
const evidence = require_http.getVerifiedKeyOwnerEvidence(key);
|
|
7787
|
+
if (evidence != null) try {
|
|
7788
|
+
return {
|
|
7789
|
+
kind: "actor",
|
|
7790
|
+
id: (0, _fedify_vocab_runtime.parseIri)(evidence.ownerId),
|
|
7791
|
+
mechanism: "httpSignatures",
|
|
7792
|
+
keyId: key.id
|
|
7793
|
+
};
|
|
7794
|
+
} catch {
|
|
7795
|
+
return null;
|
|
7796
|
+
}
|
|
7797
|
+
if (key.ownerId == null || require_http.isPortableId(key.ownerId)) return null;
|
|
7798
|
+
return {
|
|
7799
|
+
kind: "actor",
|
|
7800
|
+
id: key.ownerId,
|
|
7801
|
+
mechanism: "httpSignatures",
|
|
7802
|
+
keyId: key.id
|
|
7803
|
+
};
|
|
7804
|
+
}
|
|
7805
|
+
/**
|
|
6841
7806
|
* Checks whether an HTTP Signature key is a key of an FEP-ef61 portable
|
|
6842
7807
|
* actor, i.e., its ID is a compatible identifier or an `ap:` or `ap+ef61:`
|
|
6843
7808
|
* URI, and it is owned by a portable actor, which only the portable actor's
|
|
@@ -9429,24 +10394,102 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
9429
10394
|
let dispatched;
|
|
9430
10395
|
let parseInput = void 0;
|
|
9431
10396
|
let parseContextLoader = context.contextLoader;
|
|
10397
|
+
let authentication = getEmptyAuthenticationEvidence();
|
|
10398
|
+
let actorPresence = "present";
|
|
10399
|
+
if (message.verifiedView !== void 0) {
|
|
10400
|
+
const view = parseSerializedVerifiedActivityView(message.verifiedView);
|
|
10401
|
+
let restored;
|
|
10402
|
+
let restoredAuthentication = null;
|
|
10403
|
+
let failure;
|
|
10404
|
+
if (view == null) failure = /* @__PURE__ */ new TypeError("The queued activity has a malformed verified view.");
|
|
10405
|
+
else try {
|
|
10406
|
+
if (view.source === "normalizedActivity") require_http.assertSafeJsonLd(message.normalizedActivity);
|
|
10407
|
+
restored = await restoreVerifiedActivityView(view, message);
|
|
10408
|
+
} catch (error) {
|
|
10409
|
+
if (!(error instanceof ContextSnapshotMissError) && !isPermanentInboxParseError(error)) {
|
|
10410
|
+
await handleRetriableFailure(error);
|
|
10411
|
+
return;
|
|
10412
|
+
}
|
|
10413
|
+
failure = error;
|
|
10414
|
+
}
|
|
10415
|
+
if (restored != null) {
|
|
10416
|
+
const expandedActivity = deepFreeze(restored.expanded);
|
|
10417
|
+
restoredAuthentication = message.authentication === void 0 ? createAuthenticationEvidence({
|
|
10418
|
+
...getEmptyAuthenticationEvidence(),
|
|
10419
|
+
expandedActivity,
|
|
10420
|
+
baseUrl: restored.baseUrl
|
|
10421
|
+
}) : deserializeAuthentication(message.authentication, expandedActivity, restored.baseUrl);
|
|
10422
|
+
if (restoredAuthentication == null) failure = /* @__PURE__ */ new TypeError("The queued activity has malformed authentication evidence.");
|
|
10423
|
+
}
|
|
10424
|
+
if (failure != null || restored == null || restoredAuthentication == null) {
|
|
10425
|
+
await reportInboxError(failure);
|
|
10426
|
+
logger.error("Failed to replay the queued incoming activity:\n{error}", {
|
|
10427
|
+
error: failure,
|
|
10428
|
+
trial: message.attempt,
|
|
10429
|
+
activity: message.activity,
|
|
10430
|
+
recipient: message.identifier
|
|
10431
|
+
});
|
|
10432
|
+
listenerSpan.setStatus({
|
|
10433
|
+
code: _opentelemetry_api.SpanStatusCode.ERROR,
|
|
10434
|
+
message: String(failure)
|
|
10435
|
+
});
|
|
10436
|
+
listenerSpan.end();
|
|
10437
|
+
return;
|
|
10438
|
+
}
|
|
10439
|
+
authentication = restoredAuthentication;
|
|
10440
|
+
actorPresence = getActorPresence(restored.expanded);
|
|
10441
|
+
const policy = view.policy;
|
|
10442
|
+
const replayLoader = createReleasableLoader(restored.loader, policy === "normalization" ? require_http.getNormalizationContextLoader(context.contextLoader) : require_http.wrapContextLoaderForJsonLd(context.contextLoader), policy);
|
|
10443
|
+
parseInput = restored.document;
|
|
10444
|
+
parseContextLoader = restored.loader;
|
|
10445
|
+
try {
|
|
10446
|
+
activity = await _fedify_vocab.Activity.fromJsonLd(restored.document, {
|
|
10447
|
+
...context,
|
|
10448
|
+
contextLoader: replayLoader.loader,
|
|
10449
|
+
...restored.baseUrl == null ? {} : { baseUrl: restored.baseUrl }
|
|
10450
|
+
});
|
|
10451
|
+
} catch (error) {
|
|
10452
|
+
if (isPermanentInboxParseError(error)) {
|
|
10453
|
+
await reportInboxError(error);
|
|
10454
|
+
logger.error("Failed to parse the queued incoming activity:\n{error}", {
|
|
10455
|
+
error,
|
|
10456
|
+
trial: message.attempt,
|
|
10457
|
+
activity: message.activity,
|
|
10458
|
+
recipient: message.identifier
|
|
10459
|
+
});
|
|
10460
|
+
listenerSpan.setStatus({
|
|
10461
|
+
code: _opentelemetry_api.SpanStatusCode.ERROR,
|
|
10462
|
+
message: String(error)
|
|
10463
|
+
});
|
|
10464
|
+
listenerSpan.end();
|
|
10465
|
+
return;
|
|
10466
|
+
}
|
|
10467
|
+
await handleRetriableFailure(error);
|
|
10468
|
+
return;
|
|
10469
|
+
} finally {
|
|
10470
|
+
replayLoader.release();
|
|
10471
|
+
}
|
|
10472
|
+
}
|
|
9432
10473
|
try {
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
|
|
10474
|
+
if (activity == null) {
|
|
10475
|
+
const hasSignatureField = require_http.hasSignature(message.activity);
|
|
10476
|
+
const shouldParseFromNormalizedSignedPayload = message.ldSignatureVerified === true || message.normalizedActivity != null || message.ldSignatureVerified == null && hasSignatureField;
|
|
10477
|
+
const parseContext = hasSignatureField ? {
|
|
10478
|
+
...context,
|
|
10479
|
+
contextLoader: require_http.getNormalizationContextLoader(context.contextLoader)
|
|
10480
|
+
} : {
|
|
10481
|
+
...context,
|
|
10482
|
+
contextLoader: require_http.wrapContextLoaderForJsonLd(context.contextLoader)
|
|
10483
|
+
};
|
|
10484
|
+
parseContextLoader = parseContext.contextLoader;
|
|
10485
|
+
let normalizedActivity;
|
|
10486
|
+
if (shouldParseFromNormalizedSignedPayload) {
|
|
10487
|
+
normalizedActivity = message.normalizedActivity ?? await require_http.compactJsonLd(message.activity, context.contextLoader);
|
|
10488
|
+
require_http.assertSafeJsonLd(normalizedActivity);
|
|
10489
|
+
}
|
|
10490
|
+
parseInput = shouldParseFromNormalizedSignedPayload ? require_http.detachSignature(normalizedActivity) : hasSignatureField ? require_http.detachSignature(message.activity) : message.activity;
|
|
10491
|
+
activity = await _fedify_vocab.Activity.fromJsonLd(parseInput, parseContext);
|
|
9447
10492
|
}
|
|
9448
|
-
parseInput = shouldParseFromNormalizedSignedPayload ? require_http.detachSignature(normalizedActivity) : hasSignatureField ? require_http.detachSignature(message.activity) : message.activity;
|
|
9449
|
-
activity = await _fedify_vocab.Activity.fromJsonLd(parseInput, parseContext);
|
|
9450
10493
|
activityType = (0, _fedify_vocab.getTypeId)(activity).href;
|
|
9451
10494
|
span.setAttribute("activitypub.activity.type", activityType);
|
|
9452
10495
|
listenerSpan.setAttribute("activitypub.activity.type", activityType);
|
|
@@ -9455,7 +10498,8 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
9455
10498
|
span.setAttribute("activitypub.activity.id", activity.id.href);
|
|
9456
10499
|
listenerSpan.setAttribute("activitypub.activity.id", activity.id.href);
|
|
9457
10500
|
}
|
|
9458
|
-
|
|
10501
|
+
const actorless = actorPresence === "absent" && !this.skipSignatureVerification && authentication.verified && this.inboxListeners?.dispatchWithClass(activity)?.options.allowActorless === true && isActorlessActivityAuthenticated(activity, authentication);
|
|
10502
|
+
if (activity.actorId == null && !actorless) {
|
|
9459
10503
|
logger.error("Missing actor in queued incoming activity.", {
|
|
9460
10504
|
activityId: activity.id?.href,
|
|
9461
10505
|
activity: message.activity,
|
|
@@ -9469,7 +10513,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
9469
10513
|
listenerSpan.end();
|
|
9470
10514
|
return;
|
|
9471
10515
|
}
|
|
9472
|
-
if (!this.skipSignatureVerification && activity.id != null && !actorsHaveSameOrigin(activity)) {
|
|
10516
|
+
if (!this.skipSignatureVerification && !actorless && activity.id != null && !actorsHaveSameOrigin(activity)) {
|
|
9473
10517
|
logger.error("The queued activity ({activityId}) and its actors do not have the same origin.", {
|
|
9474
10518
|
activityId: activity.id.href,
|
|
9475
10519
|
actorIds: activity.actorIds.map((actor) => actor.href),
|
|
@@ -9484,22 +10528,18 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
9484
10528
|
listenerSpan.end();
|
|
9485
10529
|
return;
|
|
9486
10530
|
}
|
|
9487
|
-
|
|
9488
|
-
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
|
|
9493
|
-
|
|
9494
|
-
|
|
9495
|
-
|
|
9496
|
-
|
|
9497
|
-
|
|
9498
|
-
|
|
9499
|
-
require_http.recordInboxActivity(this.meterProvider, "rejected", activityType);
|
|
9500
|
-
listenerSpan.end();
|
|
9501
|
-
return;
|
|
9502
|
-
}
|
|
10531
|
+
const parsedActivity = activity;
|
|
10532
|
+
const parsedActivityType = activityType;
|
|
10533
|
+
cacheKey = await getInboxIdempotencyKey(this.idempotencyStrategy, () => context.toInboxContext(message.identifier, message.activity, parsedActivity.id?.href, parsedActivityType, { authentication }), activity, context.origin, message.identifier, this.kvPrefixes.activityIdempotence);
|
|
10534
|
+
if (await isInboxActivityProcessed(this.kv, cacheKey, activity, context.origin, this.kvPrefixes.activityIdempotence)) {
|
|
10535
|
+
logger.debug("Activity {activityId} has already been processed.", {
|
|
10536
|
+
activityId: activity.id?.href,
|
|
10537
|
+
activity: message.activity,
|
|
10538
|
+
recipient: message.identifier
|
|
10539
|
+
});
|
|
10540
|
+
require_http.recordInboxActivity(this.meterProvider, "rejected", activityType);
|
|
10541
|
+
listenerSpan.end();
|
|
10542
|
+
return;
|
|
9503
10543
|
}
|
|
9504
10544
|
dispatched = this.inboxListeners?.dispatchWithClass(activity);
|
|
9505
10545
|
} catch (error) {
|
|
@@ -9558,7 +10598,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
9558
10598
|
try {
|
|
9559
10599
|
const started = performance.now();
|
|
9560
10600
|
try {
|
|
9561
|
-
await listener(context.toInboxContext(message.identifier, message.activity, activity.id?.href, activityType), activity);
|
|
10601
|
+
await listener(context.toInboxContext(message.identifier, message.activity, activity.id?.href, activityType, { authentication }), activity);
|
|
9562
10602
|
} finally {
|
|
9563
10603
|
this.metrics.recordInboxProcessingDuration(activityType, require_http.getDurationMs(started));
|
|
9564
10604
|
}
|
|
@@ -9960,7 +11000,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
9960
11000
|
onNotAcceptable ??= notAcceptable;
|
|
9961
11001
|
onUnauthorized ??= unauthorized;
|
|
9962
11002
|
const url = new URL(request.url);
|
|
9963
|
-
const route = this.router.route(url.pathname);
|
|
11003
|
+
const route = this.router.route(url.pathname, { allowUnencodedColon: true });
|
|
9964
11004
|
if (route == null) {
|
|
9965
11005
|
const media = this.hashlinkMediaDispatcher == null ? null : parseHashlinkGatewayRequest(url);
|
|
9966
11006
|
if (media != null) return await this.#fetchHashlinkMedia(request, media, {
|
|
@@ -10865,7 +11905,7 @@ var ContextImpl = class ContextImpl {
|
|
|
10865
11905
|
invokedFromActorKeyPairsDispatcher: this.invokedFromActorKeyPairsDispatcher
|
|
10866
11906
|
});
|
|
10867
11907
|
}
|
|
10868
|
-
toInboxContext(recipient, activity, activityId, activityType) {
|
|
11908
|
+
toInboxContext(recipient, activity, activityId, activityType, metadata) {
|
|
10869
11909
|
return new InboxContextImpl(recipient, activity, activityId, activityType, {
|
|
10870
11910
|
url: this.url,
|
|
10871
11911
|
federation: this.federation,
|
|
@@ -10873,7 +11913,7 @@ var ContextImpl = class ContextImpl {
|
|
|
10873
11913
|
documentLoader: this.documentLoader,
|
|
10874
11914
|
contextLoader: this.contextLoader,
|
|
10875
11915
|
invokedFromActorKeyPairsDispatcher: this.invokedFromActorKeyPairsDispatcher
|
|
10876
|
-
});
|
|
11916
|
+
}, metadata?.authentication);
|
|
10877
11917
|
}
|
|
10878
11918
|
toOutboxContext(identifier, activity, activityId, activityType) {
|
|
10879
11919
|
return new OutboxContextImpl(identifier, activity, activityId, activityType, {
|
|
@@ -11488,12 +12528,20 @@ var ContextImpl = class ContextImpl {
|
|
|
11488
12528
|
});
|
|
11489
12529
|
}
|
|
11490
12530
|
async routeActivityInternal(recipient, activity, options = {}, span) {
|
|
12531
|
+
const snapshot = createContextSnapshot(options.contextLoader ?? this.contextLoader);
|
|
12532
|
+
try {
|
|
12533
|
+
return await this.#routeActivityWithSnapshot(recipient, activity, options, span, snapshot);
|
|
12534
|
+
} finally {
|
|
12535
|
+
snapshot.release();
|
|
12536
|
+
}
|
|
12537
|
+
}
|
|
12538
|
+
async #routeActivityWithSnapshot(recipient, activity, options, span, snapshot) {
|
|
11491
12539
|
const logger = (0, _logtape_logtape.getLogger)([
|
|
11492
12540
|
"fedify",
|
|
11493
12541
|
"federation",
|
|
11494
12542
|
"inbox"
|
|
11495
12543
|
]);
|
|
11496
|
-
const contextLoader =
|
|
12544
|
+
const contextLoader = snapshot.loader("standard");
|
|
11497
12545
|
let json = await activity.toJsonLd({ contextLoader });
|
|
11498
12546
|
const keyCache = new KvKeyCache(this.federation.kv, this.federation.kvPrefixes.publicKey, {
|
|
11499
12547
|
documentLoader: this.documentLoader,
|
|
@@ -11501,13 +12549,19 @@ var ContextImpl = class ContextImpl {
|
|
|
11501
12549
|
tracerProvider: this.tracerProvider,
|
|
11502
12550
|
keyTtl: this.federation.publicKeyTtl
|
|
11503
12551
|
});
|
|
11504
|
-
|
|
12552
|
+
let documentSigners = [];
|
|
12553
|
+
let originAuthenticated = [];
|
|
12554
|
+
let viewSource;
|
|
12555
|
+
let fetchedView;
|
|
12556
|
+
const document = require_http.detachSignature(json);
|
|
12557
|
+
const verified = await require_http.verifyObjectDetailed(_fedify_vocab.Activity, document, {
|
|
11505
12558
|
contextLoader,
|
|
11506
12559
|
documentLoader: options.documentLoader ?? this.documentLoader,
|
|
11507
12560
|
meterProvider: this.meterProvider,
|
|
11508
12561
|
tracerProvider: options.tracerProvider ?? this.tracerProvider,
|
|
11509
12562
|
keyCache
|
|
11510
|
-
})
|
|
12563
|
+
});
|
|
12564
|
+
if (verified.status !== "verified") {
|
|
11511
12565
|
logger.debug("Object Integrity Proofs are not verified.", {
|
|
11512
12566
|
recipient,
|
|
11513
12567
|
activity: json
|
|
@@ -11519,8 +12573,29 @@ var ContextImpl = class ContextImpl {
|
|
|
11519
12573
|
});
|
|
11520
12574
|
return false;
|
|
11521
12575
|
}
|
|
11522
|
-
|
|
11523
|
-
|
|
12576
|
+
let candidate;
|
|
12577
|
+
let candidateView;
|
|
12578
|
+
const verifications = /* @__PURE__ */ new WeakMap();
|
|
12579
|
+
const fetched = await this.lookupObject(activity.id, {
|
|
12580
|
+
...options,
|
|
12581
|
+
contextLoader,
|
|
12582
|
+
verifyPortableObject: async (document, verifierOptions) => {
|
|
12583
|
+
const result = await this.verifyPortableObject(document, verifierOptions);
|
|
12584
|
+
if (document != null && typeof document === "object") verifications.set(document, result);
|
|
12585
|
+
return result;
|
|
12586
|
+
},
|
|
12587
|
+
[_fedify_vocab.lookupObserver]: { accept: async (accepted) => {
|
|
12588
|
+
candidate = accepted;
|
|
12589
|
+
candidateView = await buildVerifiedActivityView(createContextSnapshot(accepted.contextLoader), {
|
|
12590
|
+
policy: "standard",
|
|
12591
|
+
source: "activity",
|
|
12592
|
+
document: require_http.detachSignature(accepted.document),
|
|
12593
|
+
baseUrl: accepted.baseUrl
|
|
12594
|
+
});
|
|
12595
|
+
return true;
|
|
12596
|
+
} }
|
|
12597
|
+
});
|
|
12598
|
+
if (fetched == null || candidate == null || candidateView == null) {
|
|
11524
12599
|
logger.debug("Failed to fetch the remote activity object {activityId}.", {
|
|
11525
12600
|
recipient,
|
|
11526
12601
|
activity: json,
|
|
@@ -11556,11 +12631,50 @@ var ContextImpl = class ContextImpl {
|
|
|
11556
12631
|
}
|
|
11557
12632
|
logger.debug("Successfully fetched the remote activity object {activityId}; ignore the original activity and use the fetched one, which is trustworthy.");
|
|
11558
12633
|
activity = fetched;
|
|
11559
|
-
json =
|
|
11560
|
-
|
|
11561
|
-
|
|
11562
|
-
|
|
11563
|
-
|
|
12634
|
+
json = candidate.document;
|
|
12635
|
+
fetchedView = candidateView;
|
|
12636
|
+
viewSource = {
|
|
12637
|
+
policy: "standard",
|
|
12638
|
+
source: "activity",
|
|
12639
|
+
document: require_http.detachSignature(candidate.document),
|
|
12640
|
+
baseUrl: candidate.baseUrl
|
|
12641
|
+
};
|
|
12642
|
+
if (require_http.isPortableId(fetched.id)) {
|
|
12643
|
+
const result = verifications.get(candidate.document);
|
|
12644
|
+
if (result?.verified === true && result.method === "proof") documentSigners = result.keys.flatMap((key) => {
|
|
12645
|
+
const signer = require_http.getProofSigner(key);
|
|
12646
|
+
return signer == null ? [] : [{
|
|
12647
|
+
kind: signer.kind,
|
|
12648
|
+
id: signer.id,
|
|
12649
|
+
mechanism: "objectIntegrityProofs",
|
|
12650
|
+
keyId: signer.keyId
|
|
12651
|
+
}];
|
|
12652
|
+
});
|
|
12653
|
+
} else originAuthenticated = fetched.actorIds.map((actorId) => ({
|
|
12654
|
+
kind: "actor",
|
|
12655
|
+
id: actorId,
|
|
12656
|
+
mechanism: "origin",
|
|
12657
|
+
keyId: null
|
|
12658
|
+
}));
|
|
12659
|
+
} else {
|
|
12660
|
+
logger.debug("Object Integrity Proofs are verified.", {
|
|
12661
|
+
recipient,
|
|
12662
|
+
activity: json
|
|
12663
|
+
});
|
|
12664
|
+
activity = verified.object;
|
|
12665
|
+
documentSigners = verified.signers.map((signer) => ({
|
|
12666
|
+
kind: signer.kind,
|
|
12667
|
+
id: signer.id,
|
|
12668
|
+
mechanism: "objectIntegrityProofs",
|
|
12669
|
+
keyId: signer.keyId
|
|
12670
|
+
}));
|
|
12671
|
+
viewSource = {
|
|
12672
|
+
policy: "standard",
|
|
12673
|
+
source: "activity",
|
|
12674
|
+
document,
|
|
12675
|
+
baseUrl: null
|
|
12676
|
+
};
|
|
12677
|
+
}
|
|
11564
12678
|
if (activity.actorId == null) {
|
|
11565
12679
|
logger.debug("Activity is missing an actor; unable to verify.", {
|
|
11566
12680
|
recipient,
|
|
@@ -11575,10 +12689,21 @@ var ContextImpl = class ContextImpl {
|
|
|
11575
12689
|
});
|
|
11576
12690
|
return false;
|
|
11577
12691
|
}
|
|
12692
|
+
const view = fetchedView ?? await buildVerifiedActivityView(snapshot, viewSource);
|
|
11578
12693
|
const routeResult = await routeActivity({
|
|
11579
12694
|
context: this,
|
|
11580
12695
|
json,
|
|
11581
12696
|
ldSignatureVerified: false,
|
|
12697
|
+
authentication: createAuthenticationEvidence({
|
|
12698
|
+
verified: true,
|
|
12699
|
+
requestSigners: [],
|
|
12700
|
+
documentSigners,
|
|
12701
|
+
originAuthenticated,
|
|
12702
|
+
expandedActivity: deepFreeze(view.expanded),
|
|
12703
|
+
baseUrl: viewSource.baseUrl
|
|
12704
|
+
}),
|
|
12705
|
+
verifiedView: view.view,
|
|
12706
|
+
replayable: view.replayable,
|
|
11582
12707
|
activity,
|
|
11583
12708
|
recipient,
|
|
11584
12709
|
inboxListeners: this.federation.inboxListeners,
|
|
@@ -11694,52 +12819,92 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
|
|
|
11694
12819
|
if (object instanceof _fedify_vocab.Tombstone && !(object instanceof cls) && options?.tombstone !== "passthrough") return null;
|
|
11695
12820
|
return object;
|
|
11696
12821
|
}
|
|
11697
|
-
#signedKey =
|
|
12822
|
+
#signedKey = null;
|
|
12823
|
+
#signedKeyOwner = null;
|
|
12824
|
+
#signatureChecks = /* @__PURE__ */ new WeakMap();
|
|
12825
|
+
#getSignatureCheck(documentLoader, contextLoader) {
|
|
12826
|
+
let checks = this.#signatureChecks.get(documentLoader);
|
|
12827
|
+
if (checks == null) {
|
|
12828
|
+
checks = /* @__PURE__ */ new WeakMap();
|
|
12829
|
+
this.#signatureChecks.set(documentLoader, checks);
|
|
12830
|
+
}
|
|
12831
|
+
let check = checks.get(contextLoader);
|
|
12832
|
+
if (check == null) {
|
|
12833
|
+
check = {};
|
|
12834
|
+
checks.set(contextLoader, check);
|
|
12835
|
+
}
|
|
12836
|
+
return check;
|
|
12837
|
+
}
|
|
11698
12838
|
async getSignedKey(options = {}) {
|
|
11699
12839
|
if (this.#signedKey != null) return this.#signedKey;
|
|
11700
|
-
const
|
|
11701
|
-
|
|
11702
|
-
|
|
12840
|
+
const documentLoader = options.documentLoader ?? this.documentLoader;
|
|
12841
|
+
const contextLoader = options.contextLoader ?? this.contextLoader;
|
|
12842
|
+
const check = this.#getSignatureCheck(documentLoader, contextLoader);
|
|
12843
|
+
if (check.key != null) return check.key;
|
|
12844
|
+
const keyCache = documentLoader === this.documentLoader && contextLoader === this.contextLoader ? new KvKeyCache(this.federation.kv, this.federation.kvPrefixes.publicKey, {
|
|
12845
|
+
documentLoader,
|
|
12846
|
+
contextLoader,
|
|
11703
12847
|
tracerProvider: options.tracerProvider ?? this.tracerProvider,
|
|
11704
12848
|
keyTtl: this.federation.publicKeyTtl
|
|
11705
|
-
});
|
|
11706
|
-
|
|
12849
|
+
}) : void 0;
|
|
12850
|
+
const promise = require_http.verifyRequest(this.request, {
|
|
11707
12851
|
...this,
|
|
11708
|
-
contextLoader
|
|
11709
|
-
documentLoader
|
|
12852
|
+
contextLoader,
|
|
12853
|
+
documentLoader,
|
|
11710
12854
|
timeWindow: this.federation.signatureTimeWindow,
|
|
11711
12855
|
maxSignatures: this.federation.maxHttpSignatures,
|
|
12856
|
+
keyCache,
|
|
11712
12857
|
meterProvider: this.meterProvider,
|
|
11713
|
-
tracerProvider: options.tracerProvider ?? this.tracerProvider
|
|
11714
|
-
|
|
12858
|
+
tracerProvider: options.tracerProvider ?? this.tracerProvider
|
|
12859
|
+
}).then((key) => {
|
|
12860
|
+
if (key != null) this.#signedKey ??= key;
|
|
12861
|
+
return key;
|
|
12862
|
+
}).catch((error) => {
|
|
12863
|
+
if (check.key === promise) check.key = void 0;
|
|
12864
|
+
throw error;
|
|
11715
12865
|
});
|
|
12866
|
+
check.key = promise;
|
|
12867
|
+
return await promise;
|
|
11716
12868
|
}
|
|
11717
|
-
#signedKeyOwner = void 0;
|
|
11718
12869
|
async getSignedKeyOwner(options = {}) {
|
|
11719
12870
|
if (this.#signedKeyOwner != null) return this.#signedKeyOwner;
|
|
11720
|
-
const
|
|
11721
|
-
|
|
11722
|
-
|
|
11723
|
-
|
|
11724
|
-
|
|
11725
|
-
|
|
11726
|
-
|
|
11727
|
-
|
|
11728
|
-
|
|
11729
|
-
|
|
11730
|
-
|
|
11731
|
-
|
|
11732
|
-
|
|
11733
|
-
|
|
11734
|
-
|
|
11735
|
-
|
|
11736
|
-
|
|
11737
|
-
|
|
11738
|
-
|
|
11739
|
-
|
|
12871
|
+
const normalizedOptions = {
|
|
12872
|
+
documentLoader: options.documentLoader ?? this.documentLoader,
|
|
12873
|
+
contextLoader: options.contextLoader ?? this.contextLoader,
|
|
12874
|
+
tracerProvider: options.tracerProvider ?? this.tracerProvider
|
|
12875
|
+
};
|
|
12876
|
+
const check = this.#getSignatureCheck(normalizedOptions.documentLoader, normalizedOptions.contextLoader);
|
|
12877
|
+
if (check.owner != null) return check.owner;
|
|
12878
|
+
const promise = this.getSignedKey(normalizedOptions).then(async (key) => {
|
|
12879
|
+
if (key == null) {
|
|
12880
|
+
if (check.owner === promise) check.owner = void 0;
|
|
12881
|
+
return null;
|
|
12882
|
+
}
|
|
12883
|
+
try {
|
|
12884
|
+
const owner = await require_portable_collection.getKeyOwner(key, normalizedOptions);
|
|
12885
|
+
if (owner != null) this.#signedKeyOwner ??= owner;
|
|
12886
|
+
return owner;
|
|
12887
|
+
} catch (error) {
|
|
12888
|
+
if (error instanceof _fedify_vocab_runtime.FetchError) {
|
|
12889
|
+
(0, _logtape_logtape.getLogger)([
|
|
12890
|
+
"fedify",
|
|
12891
|
+
"federation",
|
|
12892
|
+
"actor"
|
|
12893
|
+
]).warn("Failed to fetch the key owner {keyOwner} of {keyId} while verifying the request signature; treating the request as unauthenticated: {error}", {
|
|
12894
|
+
keyId: key.id?.href,
|
|
12895
|
+
keyOwner: error.url.href,
|
|
12896
|
+
error
|
|
12897
|
+
});
|
|
12898
|
+
return null;
|
|
12899
|
+
}
|
|
12900
|
+
throw error;
|
|
11740
12901
|
}
|
|
12902
|
+
}).catch((error) => {
|
|
12903
|
+
if (check.owner === promise) check.owner = void 0;
|
|
11741
12904
|
throw error;
|
|
11742
|
-
}
|
|
12905
|
+
});
|
|
12906
|
+
check.owner = promise;
|
|
12907
|
+
return await promise;
|
|
11743
12908
|
}
|
|
11744
12909
|
async isSignedByAudience(object, options = {}) {
|
|
11745
12910
|
return await isInAudience(object, null) || await isInAudience(object, await this.getSignedKeyOwner(options), options);
|
|
@@ -11929,12 +13094,14 @@ var InboxContextImpl = class InboxContextImpl extends ContextImpl {
|
|
|
11929
13094
|
activity;
|
|
11930
13095
|
activityId;
|
|
11931
13096
|
activityType;
|
|
11932
|
-
|
|
13097
|
+
authentication;
|
|
13098
|
+
constructor(recipient, activity, activityId, activityType, options, authentication) {
|
|
11933
13099
|
super(options);
|
|
11934
13100
|
this.recipient = recipient;
|
|
11935
13101
|
this.activity = activity;
|
|
11936
13102
|
this.activityId = activityId;
|
|
11937
13103
|
this.activityType = activityType;
|
|
13104
|
+
this.authentication = authentication ?? getEmptyAuthenticationEvidence();
|
|
11938
13105
|
}
|
|
11939
13106
|
clone(data) {
|
|
11940
13107
|
return new InboxContextImpl(this.recipient, this.activity, this.activityId, this.activityType, {
|
|
@@ -11944,7 +13111,7 @@ var InboxContextImpl = class InboxContextImpl extends ContextImpl {
|
|
|
11944
13111
|
documentLoader: this.documentLoader,
|
|
11945
13112
|
contextLoader: this.contextLoader,
|
|
11946
13113
|
invokedFromActorKeyPairsDispatcher: this.invokedFromActorKeyPairsDispatcher
|
|
11947
|
-
});
|
|
13114
|
+
}, this.authentication);
|
|
11948
13115
|
}
|
|
11949
13116
|
forwardActivity(forwarder, recipients, options) {
|
|
11950
13117
|
return forwardActivity(this, "inbox", forwarder, recipients, options).then(() => void 0);
|
|
@@ -12237,6 +13404,11 @@ var ParallelMessageQueue = class ParallelMessageQueue {
|
|
|
12237
13404
|
* @since 2.4.0
|
|
12238
13405
|
*/
|
|
12239
13406
|
atomicEnqueueMany;
|
|
13407
|
+
/**
|
|
13408
|
+
* Inherits the message size limit from the wrapped queue.
|
|
13409
|
+
* @since 2.5.0
|
|
13410
|
+
*/
|
|
13411
|
+
maxMessageSize;
|
|
12240
13412
|
getDepth;
|
|
12241
13413
|
/**
|
|
12242
13414
|
* Tracks which ordering keys are currently being processed to ensure
|
|
@@ -12262,6 +13434,7 @@ var ParallelMessageQueue = class ParallelMessageQueue {
|
|
|
12262
13434
|
this.workers = workers;
|
|
12263
13435
|
this.nativeRetrial = queue.nativeRetrial;
|
|
12264
13436
|
this.nativeDeduplication = queue.nativeDeduplication;
|
|
13437
|
+
this.maxMessageSize = queue.maxMessageSize;
|
|
12265
13438
|
this.atomicEnqueueMany = queue.enqueueMany == null ? false : queue.atomicEnqueueMany;
|
|
12266
13439
|
if (queue.getDepth != null) this.getDepth = () => queue.getDepth();
|
|
12267
13440
|
}
|
|
@@ -12567,6 +13740,12 @@ Object.defineProperty(exports, "handleWebFinger", {
|
|
|
12567
13740
|
return handleWebFinger;
|
|
12568
13741
|
}
|
|
12569
13742
|
});
|
|
13743
|
+
Object.defineProperty(exports, "isActorAuthenticated", {
|
|
13744
|
+
enumerable: true,
|
|
13745
|
+
get: function() {
|
|
13746
|
+
return isActorAuthenticated;
|
|
13747
|
+
}
|
|
13748
|
+
});
|
|
12570
13749
|
Object.defineProperty(exports, "normalizeCircuitBreakerOptions", {
|
|
12571
13750
|
enumerable: true,
|
|
12572
13751
|
get: function() {
|