@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
|
@@ -2,20 +2,22 @@ import { Temporal } from "temporal-polyfill";
|
|
|
2
2
|
import { URLPattern } from "urlpattern-polyfill";
|
|
3
3
|
import { t as __exportAll } from "./rolldown-runtime-BC-Eo5Mj.js";
|
|
4
4
|
import { r as getDefaultActivityTransformers } from "./transformers-DAtXl_Kc.js";
|
|
5
|
-
import { $ as
|
|
6
|
-
import { a as MAX_BODY_SIZE, i as BodyTooLargeError, n as getNodeInfo, o as readBoundedText, t as nodeInfoToJson } from "./types-
|
|
7
|
-
import { n as doesActorOwnKey, r as getKeyOwner, t as verifyPortableObject } from "./portable-collection-
|
|
8
|
-
import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-
|
|
5
|
+
import { $ as getVerifiedKeyOwnerEvidence, A as compactJsonLd, At as getRawCanonicalPortableId, B as verifyCompactJsonLdDetailed, C as verifyPortableObjectProofWithRoot, Ct as observeAttempt, D as InvalidContextReferenceError, Dt as getCanonicalPortableId, E as normalizeOutgoingActivityJsonLd, Et as getAuthenticationOrigin, F as hasSignatureLike, Ft as isPortableUri, I as isClearlyMalformedContextReference, It as isSameObjectId, L as isInvalidUrlTypeError, M as detachSignature, Mt as isCompatibleKeyId, N as getNormalizationContextLoader, Nt as isPortableId, O as assertSafeJsonLd, Ot as getGatewayKeyBase, P as hasSignature, Pt as isPortableKeyId, Q as MAX_PORTABLE_KEY_TTL, R as markEffectiveContextLoader, Rt as name, S as verifyPortableObjectProofPolicy, Tt as verificationObservation, U as wrapContextLoaderForJsonLd, W as exportJwk, X as validateCryptoKey, Y as importJwk, _ as signObject, _t as registerQueueDepthGauge, at as isAbortError, b as verifyObjectDetailed, c as verifyRequestDetailed, ct as recordCollectionPageItems, d as InvalidPortableObjectIdError, f as classifyFep2277CoreType, ft as recordFanoutRecipients, g as preparePortableObjectProofPolicy, gt as recordWebFingerHandle, h as hasProofLike, ht as recordOutboxEnqueue, i as selectRequestSignature, it as instrumentDocumentLoader, jt as hasPortableActor, kt as getPortableDid, lt as recordCollectionRequest, m as getProofSigner, mt as recordOutboxActivity, n as listRequestSignatures, nt as getFederationMetrics, o as validateMaxSignatures, ot as recordCircuitBreakerStateChange, pt as recordInboxActivity, r as parseRfc9421SignatureInput, rt as getRemoteHost, s as verifyRequest, st as recordCollectionDispatchDuration, t as doubleKnockScoped, tt as getDurationMs, ut as recordCollectionTotalItems, v as verifyMapLocalProof, vt as preloadedOnlyDocumentLoader, w as verifyPreparedPortableObjectProofPolicy, wt as snapshotKey, x as verifyPortableObjectProof, y as verifyObject, yt as formatAcceptSignature, z as signJsonLd, zt as version } from "./http-ChXUU_3c.js";
|
|
6
|
+
import { a as MAX_BODY_SIZE, i as BodyTooLargeError, n as getNodeInfo, o as readBoundedText, t as nodeInfoToJson } from "./types-DUjZb4i6.js";
|
|
7
|
+
import { n as doesActorOwnKey, r as getKeyOwner, t as verifyPortableObject } from "./portable-collection-TmtpzD4E.js";
|
|
8
|
+
import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-DQcte7ES.js";
|
|
9
9
|
import { getLogger, withContext } from "@logtape/logtape";
|
|
10
10
|
import { Router, RouterError, assertPath } from "@fedify/uri-template";
|
|
11
|
-
import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, PUBLIC_COLLECTION, Tombstone, getTypeId, isActor, lookupObject, traverseCollection } from "@fedify/vocab";
|
|
11
|
+
import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, PUBLIC_COLLECTION, Tombstone, getTypeId, isActor, lookupObject, lookupObserver, traverseCollection } from "@fedify/vocab";
|
|
12
12
|
import { SpanKind, SpanStatusCode, context, metrics, propagation, trace } from "@opentelemetry/api";
|
|
13
|
-
import { FetchError, UrlError, canonicalizePortableUri, decodeMultibase, formatIri, fromCompatibleEf61Id, getDocumentLoader, getFe34Origin, isGatewayUrl, parseDigestMultibase, parseHashlink, parseIri, preloadedContexts, resolveDocumentLoaderTimeout, toCompatibleEf61Id
|
|
13
|
+
import { FetchError, UrlError, canonicalizePortableUri, decodeMultibase, formatIri, fromCompatibleEf61Id, getDocumentLoader, getFe34Origin, isGatewayUrl, parseDigestMultibase, parseHashlink, parseIri, preloadedContexts, resolveDocumentLoaderTimeout, toCompatibleEf61Id } from "@fedify/vocab-runtime";
|
|
14
14
|
import { getPortableGatewayCandidates, isCompatibleEf61Iri } from "@fedify/vocab-runtime/internal/portable-dereference";
|
|
15
15
|
import { encodeHex } from "byte-encodings/hex";
|
|
16
16
|
import jsonld from "@fedify/vocab-runtime/jsonld";
|
|
17
17
|
import { uniq } from "es-toolkit";
|
|
18
|
+
import serialize from "json-canon";
|
|
18
19
|
import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_HEADER, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions";
|
|
20
|
+
import { openDirectFetch, openPublicFetch } from "@fedify/vocab-runtime/internal";
|
|
19
21
|
import { lookupWebFinger } from "@fedify/webfinger";
|
|
20
22
|
import { DataPointType, MeterProvider as MeterProvider$1, MetricReader } from "@opentelemetry/sdk-metrics";
|
|
21
23
|
import { parse, stringifyAsync } from "devalue";
|
|
@@ -32,9 +34,12 @@ var ActivityListenerSet = class {
|
|
|
32
34
|
clone.#listeners = new Map(this.#listeners);
|
|
33
35
|
return clone;
|
|
34
36
|
}
|
|
35
|
-
add(type, listener) {
|
|
37
|
+
add(type, listener, options = {}) {
|
|
36
38
|
if (this.#listeners.has(type)) throw new TypeError("Listener already set for this type.");
|
|
37
|
-
this.#listeners.set(type,
|
|
39
|
+
this.#listeners.set(type, {
|
|
40
|
+
listener,
|
|
41
|
+
options: { ...options }
|
|
42
|
+
});
|
|
38
43
|
}
|
|
39
44
|
dispatchWithClass(activity) {
|
|
40
45
|
let cls = activity.constructor;
|
|
@@ -44,10 +49,11 @@ var ActivityListenerSet = class {
|
|
|
44
49
|
cls = globalThis.Object.getPrototypeOf(cls);
|
|
45
50
|
}
|
|
46
51
|
if (cls == null) return null;
|
|
47
|
-
const listener = this.#listeners.get(cls);
|
|
52
|
+
const { listener, options } = this.#listeners.get(cls);
|
|
48
53
|
return {
|
|
49
54
|
class: cls,
|
|
50
|
-
listener
|
|
55
|
+
listener,
|
|
56
|
+
options
|
|
51
57
|
};
|
|
52
58
|
}
|
|
53
59
|
dispatch(activity) {
|
|
@@ -635,8 +641,8 @@ var FederationBuilderImpl = class {
|
|
|
635
641
|
}
|
|
636
642
|
const listeners = this.inboxListeners = new ActivityListenerSet();
|
|
637
643
|
const setters = {
|
|
638
|
-
on(type, listener) {
|
|
639
|
-
listeners.add(type, listener);
|
|
644
|
+
on(type, listener, options) {
|
|
645
|
+
listeners.add(type, listener, options);
|
|
640
646
|
return setters;
|
|
641
647
|
},
|
|
642
648
|
onError: (handler) => {
|
|
@@ -2804,6 +2810,172 @@ async function isInAudience(object, actor, options = {}) {
|
|
|
2804
2810
|
return false;
|
|
2805
2811
|
}
|
|
2806
2812
|
//#endregion
|
|
2813
|
+
//#region src/federation/inbox-authentication.ts
|
|
2814
|
+
/**
|
|
2815
|
+
* The evidence of an activity that has not been authenticated by Fedify.
|
|
2816
|
+
*/
|
|
2817
|
+
const EMPTY_EVIDENCE = Object.freeze({
|
|
2818
|
+
verified: false,
|
|
2819
|
+
requestSigners: Object.freeze([]),
|
|
2820
|
+
documentSigners: Object.freeze([]),
|
|
2821
|
+
originAuthenticated: Object.freeze([]),
|
|
2822
|
+
expandedActivity: Object.freeze([]),
|
|
2823
|
+
baseUrl: null
|
|
2824
|
+
});
|
|
2825
|
+
/**
|
|
2826
|
+
* Gets the evidence of an activity that has not been authenticated.
|
|
2827
|
+
* @internal
|
|
2828
|
+
*/
|
|
2829
|
+
function getEmptyAuthenticationEvidence() {
|
|
2830
|
+
return EMPTY_EVIDENCE;
|
|
2831
|
+
}
|
|
2832
|
+
/**
|
|
2833
|
+
* Gets the principals that an authorization decision should rely on.
|
|
2834
|
+
* @param evidence The evidence.
|
|
2835
|
+
* @param scope See {@link IsActorAuthenticatedOptions.scope}.
|
|
2836
|
+
* @returns The principals.
|
|
2837
|
+
* @internal
|
|
2838
|
+
*/
|
|
2839
|
+
function getEffectivePrincipals(evidence, scope = "effective") {
|
|
2840
|
+
if (scope === "document" || evidence.documentSigners.length > 0) return evidence.documentSigners;
|
|
2841
|
+
if (evidence.requestSigners.length > 0) return evidence.requestSigners;
|
|
2842
|
+
return evidence.originAuthenticated;
|
|
2843
|
+
}
|
|
2844
|
+
/**
|
|
2845
|
+
* Normalizes an actor ID for comparison: portable IDs and compatible
|
|
2846
|
+
* identifiers to their canonical portable ID, other IDs to their `href`.
|
|
2847
|
+
* @returns The normalized ID, or `null` for a malformed portable ID.
|
|
2848
|
+
*/
|
|
2849
|
+
function normalizeActorId(id) {
|
|
2850
|
+
return isPortableId(id) ? getCanonicalPortableId(id) : id.href;
|
|
2851
|
+
}
|
|
2852
|
+
/**
|
|
2853
|
+
* Checks whether the given principal authenticates the given actor.
|
|
2854
|
+
* @param principal The principal.
|
|
2855
|
+
* @param actorId The actor ID.
|
|
2856
|
+
* @returns `true` if the principal authenticates the actor.
|
|
2857
|
+
* @internal
|
|
2858
|
+
*/
|
|
2859
|
+
function principalAuthenticates(principal, actorId) {
|
|
2860
|
+
if (principal.kind === "portableAuthority") {
|
|
2861
|
+
if (!isPortableId(actorId)) return false;
|
|
2862
|
+
const did = getPortableDid(actorId);
|
|
2863
|
+
return did != null && did === getPortableDid(principal.id);
|
|
2864
|
+
}
|
|
2865
|
+
const expected = normalizeActorId(actorId);
|
|
2866
|
+
return expected != null && normalizeActorId(principal.id) === expected;
|
|
2867
|
+
}
|
|
2868
|
+
/**
|
|
2869
|
+
* Checks whether the given evidence authenticates the given actor.
|
|
2870
|
+
*
|
|
2871
|
+
* Actor IDs are compared exactly, except that [FEP-ef61] portable IDs and
|
|
2872
|
+
* compatible identifiers are compared by their canonical portable IDs, and
|
|
2873
|
+
* a portable authority (a DID) authenticates the portable actors of that DID.
|
|
2874
|
+
*
|
|
2875
|
+
* @example
|
|
2876
|
+
* ``` typescript
|
|
2877
|
+
* federation
|
|
2878
|
+
* .setInboxListeners("/users/{identifier}/inbox", "/inbox")
|
|
2879
|
+
* .on(Follow, async (ctx, follow) => {
|
|
2880
|
+
* if (follow.actorId == null) return;
|
|
2881
|
+
* // The actor is always authenticated for activities that declare one,
|
|
2882
|
+
* // but the same check works for any actor an activity names:
|
|
2883
|
+
* isActorAuthenticated(ctx.authentication, follow.actorId);
|
|
2884
|
+
* });
|
|
2885
|
+
* ```
|
|
2886
|
+
*
|
|
2887
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
2888
|
+
* @param evidence The authentication evidence, usually
|
|
2889
|
+
* {@link InboxContext.authentication}.
|
|
2890
|
+
* @param actorId The actor ID to check.
|
|
2891
|
+
* @param options Options.
|
|
2892
|
+
* @returns `true` if the actor is authenticated.
|
|
2893
|
+
* @since 2.5.0
|
|
2894
|
+
*/
|
|
2895
|
+
function isActorAuthenticated(evidence, actorId, options = {}) {
|
|
2896
|
+
return getEffectivePrincipals(evidence, options.scope).some((principal) => principalAuthenticates(principal, actorId));
|
|
2897
|
+
}
|
|
2898
|
+
function serializePrincipal(principal) {
|
|
2899
|
+
return {
|
|
2900
|
+
kind: principal.kind,
|
|
2901
|
+
id: principal.id.href,
|
|
2902
|
+
mechanism: principal.mechanism,
|
|
2903
|
+
keyId: principal.keyId?.href ?? null
|
|
2904
|
+
};
|
|
2905
|
+
}
|
|
2906
|
+
/**
|
|
2907
|
+
* Serializes the principals of the given evidence for a queued message.
|
|
2908
|
+
* @internal
|
|
2909
|
+
*/
|
|
2910
|
+
function serializeAuthentication(evidence) {
|
|
2911
|
+
return {
|
|
2912
|
+
verified: evidence.verified,
|
|
2913
|
+
requestSigners: evidence.requestSigners.map(serializePrincipal),
|
|
2914
|
+
documentSigners: evidence.documentSigners.map(serializePrincipal),
|
|
2915
|
+
originAuthenticated: evidence.originAuthenticated.map(serializePrincipal)
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
2918
|
+
const KINDS = /* @__PURE__ */ new Set(["actor", "portableAuthority"]);
|
|
2919
|
+
const MECHANISMS = /* @__PURE__ */ new Set([
|
|
2920
|
+
"httpSignatures",
|
|
2921
|
+
"linkedDataSignatures",
|
|
2922
|
+
"objectIntegrityProofs",
|
|
2923
|
+
"origin"
|
|
2924
|
+
]);
|
|
2925
|
+
function deserializePrincipals(value) {
|
|
2926
|
+
if (!Array.isArray(value)) return null;
|
|
2927
|
+
const principals = [];
|
|
2928
|
+
for (const item of value) {
|
|
2929
|
+
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;
|
|
2930
|
+
principals.push(Object.freeze({
|
|
2931
|
+
kind: item.kind,
|
|
2932
|
+
id: new URL(item.id),
|
|
2933
|
+
mechanism: item.mechanism,
|
|
2934
|
+
keyId: item.keyId == null ? null : new URL(item.keyId)
|
|
2935
|
+
}));
|
|
2936
|
+
}
|
|
2937
|
+
return principals;
|
|
2938
|
+
}
|
|
2939
|
+
/**
|
|
2940
|
+
* Restores the evidence of a queued message.
|
|
2941
|
+
* @param value The serialized principals.
|
|
2942
|
+
* @param expandedActivity The expanded activity, restored from the queued
|
|
2943
|
+
* verified view.
|
|
2944
|
+
* @param baseUrl The base URL, restored from the queued verified view.
|
|
2945
|
+
* @returns The evidence, or `null` if the serialized value is malformed.
|
|
2946
|
+
* @internal
|
|
2947
|
+
*/
|
|
2948
|
+
function deserializeAuthentication(value, expandedActivity, baseUrl) {
|
|
2949
|
+
if (value == null || typeof value !== "object" || typeof value.verified !== "boolean") return null;
|
|
2950
|
+
const record = value;
|
|
2951
|
+
const requestSigners = deserializePrincipals(record.requestSigners);
|
|
2952
|
+
const documentSigners = deserializePrincipals(record.documentSigners);
|
|
2953
|
+
const originAuthenticated = deserializePrincipals(record.originAuthenticated);
|
|
2954
|
+
if (requestSigners == null || documentSigners == null || originAuthenticated == null) return null;
|
|
2955
|
+
return createAuthenticationEvidence({
|
|
2956
|
+
verified: record.verified,
|
|
2957
|
+
requestSigners,
|
|
2958
|
+
documentSigners,
|
|
2959
|
+
originAuthenticated,
|
|
2960
|
+
expandedActivity,
|
|
2961
|
+
baseUrl
|
|
2962
|
+
});
|
|
2963
|
+
}
|
|
2964
|
+
/**
|
|
2965
|
+
* Creates frozen evidence.
|
|
2966
|
+
* @internal
|
|
2967
|
+
*/
|
|
2968
|
+
function createAuthenticationEvidence(evidence) {
|
|
2969
|
+
return Object.freeze({
|
|
2970
|
+
verified: evidence.verified,
|
|
2971
|
+
requestSigners: Object.freeze([...evidence.requestSigners]),
|
|
2972
|
+
documentSigners: Object.freeze([...evidence.documentSigners]),
|
|
2973
|
+
originAuthenticated: Object.freeze([...evidence.originAuthenticated]),
|
|
2974
|
+
expandedActivity: evidence.expandedActivity,
|
|
2975
|
+
baseUrl: evidence.baseUrl
|
|
2976
|
+
});
|
|
2977
|
+
}
|
|
2978
|
+
//#endregion
|
|
2807
2979
|
//#region src/federation/inbox.ts
|
|
2808
2980
|
/**
|
|
2809
2981
|
* Checks whether an activity's ID and all of its actor IDs have the same
|
|
@@ -2825,13 +2997,135 @@ function actorsHaveSameOrigin(activity) {
|
|
|
2825
2997
|
const origin = getAuthenticationOrigin(activity.id);
|
|
2826
2998
|
return origin != null && activity.actorIds.every((actor) => getAuthenticationOrigin(actor) === origin);
|
|
2827
2999
|
}
|
|
2828
|
-
|
|
3000
|
+
/**
|
|
3001
|
+
* The headroom that the size check of a queued inbox message leaves for the
|
|
3002
|
+
* fields that grow when Fedify retries the message, e.g., its attempt count.
|
|
3003
|
+
*/
|
|
3004
|
+
const QUEUED_MESSAGE_HEADROOM = 64;
|
|
3005
|
+
/**
|
|
3006
|
+
* Computes the idempotency key of an incoming activity.
|
|
3007
|
+
* @returns The key, or `null` if the activity is not deduplicated.
|
|
3008
|
+
* @internal
|
|
3009
|
+
*/
|
|
3010
|
+
async function getInboxIdempotencyKey(strategy, inboxContext, activity, origin, recipient, prefix) {
|
|
3011
|
+
if (activity.id == null) return null;
|
|
3012
|
+
strategy ??= "per-inbox";
|
|
3013
|
+
let keyString;
|
|
3014
|
+
if (typeof strategy === "function") keyString = await strategy(inboxContext(), activity);
|
|
3015
|
+
else switch (strategy) {
|
|
3016
|
+
case "global":
|
|
3017
|
+
keyString = activity.id.href;
|
|
3018
|
+
break;
|
|
3019
|
+
case "per-origin":
|
|
3020
|
+
keyString = `${origin}\n${activity.id.href}`;
|
|
3021
|
+
break;
|
|
3022
|
+
case "per-inbox":
|
|
3023
|
+
keyString = `${origin}\n${activity.id.href}\n${recipient == null ? "sharedInbox" : `inbox\n${recipient}`}`;
|
|
3024
|
+
break;
|
|
3025
|
+
default: keyString = `${origin}\n${activity.id.href}`;
|
|
3026
|
+
}
|
|
3027
|
+
return keyString == null ? null : [...prefix, keyString];
|
|
3028
|
+
}
|
|
3029
|
+
/**
|
|
3030
|
+
* Checks whether an incoming activity has already been processed, by its
|
|
3031
|
+
* idempotency key, or by the key that queue workers of older versions of
|
|
3032
|
+
* Fedify recorded.
|
|
3033
|
+
* @internal
|
|
3034
|
+
*/
|
|
3035
|
+
async function isInboxActivityProcessed(kv, cacheKey, activity, origin, prefix) {
|
|
3036
|
+
if (cacheKey == null) return false;
|
|
3037
|
+
if (await kv.get(cacheKey) === true) return true;
|
|
3038
|
+
if (activity.id == null) return false;
|
|
3039
|
+
return await kv.get([
|
|
3040
|
+
...prefix,
|
|
3041
|
+
origin,
|
|
3042
|
+
activity.id.href
|
|
3043
|
+
]) === true;
|
|
3044
|
+
}
|
|
3045
|
+
/**
|
|
3046
|
+
* The bytes added to the UTF-8 JSON length of a message for each of its JSON
|
|
3047
|
+
* values, to bound the size of serialization formats other than JSON, such
|
|
3048
|
+
* as the structured serialization of Deno KV, which spends more bytes than
|
|
3049
|
+
* JSON on type tags and lengths of small values, e.g., empty arrays, and up
|
|
3050
|
+
* to nine bytes on a fractional number that JSON writes in three.
|
|
3051
|
+
*/
|
|
3052
|
+
const PER_VALUE_SERIALIZATION_OVERHEAD = 8;
|
|
3053
|
+
/**
|
|
3054
|
+
* Estimates, conservatively, how large a queued message is once a queue
|
|
3055
|
+
* backend serializes it: the UTF-8 length of its JSON serialization, plus
|
|
3056
|
+
* a fixed overhead for each JSON value in it, plus the bytes that encoding
|
|
3057
|
+
* strings beyond Latin-1 as UTF-16 adds.
|
|
3058
|
+
* @internal
|
|
3059
|
+
*/
|
|
3060
|
+
function estimateQueuedMessageSize(message) {
|
|
3061
|
+
const encoder = new TextEncoder();
|
|
3062
|
+
let values = 0;
|
|
3063
|
+
let stringExpansion = 0;
|
|
3064
|
+
const countString = (value) => {
|
|
3065
|
+
values++;
|
|
3066
|
+
if (!/[\u0100-\uffff]/.test(value)) return;
|
|
3067
|
+
const expansion = value.length * 2 - encoder.encode(value).byteLength;
|
|
3068
|
+
if (expansion > 0) stringExpansion += expansion;
|
|
3069
|
+
};
|
|
3070
|
+
const count = (value) => {
|
|
3071
|
+
if (typeof value === "string") {
|
|
3072
|
+
countString(value);
|
|
3073
|
+
return;
|
|
3074
|
+
}
|
|
3075
|
+
values++;
|
|
3076
|
+
if (Array.isArray(value)) for (const item of value) count(item);
|
|
3077
|
+
else if (value != null && typeof value === "object") for (const [key, item] of Object.entries(value)) {
|
|
3078
|
+
if (item === void 0) continue;
|
|
3079
|
+
countString(key);
|
|
3080
|
+
count(item);
|
|
3081
|
+
}
|
|
3082
|
+
};
|
|
3083
|
+
count(message);
|
|
3084
|
+
return encoder.encode(JSON.stringify(message)).byteLength + stringExpansion + values * PER_VALUE_SERIALIZATION_OVERHEAD;
|
|
3085
|
+
}
|
|
3086
|
+
/**
|
|
3087
|
+
* Checks whether an activity that declares no actor is authenticated: at
|
|
3088
|
+
* least one principal must be authenticated for it, and its ID, if any, must
|
|
3089
|
+
* have the same [FEP-fe34] origin as one of those principals.
|
|
3090
|
+
*
|
|
3091
|
+
* [FEP-fe34]: https://w3id.org/fep/fe34
|
|
3092
|
+
* @returns `"ok"` if it is authenticated, or why it is not.
|
|
3093
|
+
* @internal
|
|
3094
|
+
*/
|
|
3095
|
+
function checkActorlessActivity(activity, authentication) {
|
|
3096
|
+
const principals = getEffectivePrincipals(authentication);
|
|
3097
|
+
if (principals.length < 1) return {
|
|
3098
|
+
status: "noAuthenticatedPrincipal",
|
|
3099
|
+
principals
|
|
3100
|
+
};
|
|
3101
|
+
if (activity.id == null) return {
|
|
3102
|
+
status: "ok",
|
|
3103
|
+
principals
|
|
3104
|
+
};
|
|
3105
|
+
const origin = getAuthenticationOrigin(activity.id);
|
|
3106
|
+
if (origin == null || !principals.some((principal) => getAuthenticationOrigin(principal.id) === origin)) return {
|
|
3107
|
+
status: "activityOriginMismatch",
|
|
3108
|
+
principals
|
|
3109
|
+
};
|
|
3110
|
+
return {
|
|
3111
|
+
status: "ok",
|
|
3112
|
+
principals
|
|
3113
|
+
};
|
|
3114
|
+
}
|
|
3115
|
+
/**
|
|
3116
|
+
* Same as {@link checkActorlessActivity}, but returns a boolean.
|
|
3117
|
+
* @internal
|
|
3118
|
+
*/
|
|
3119
|
+
function isActorlessActivityAuthenticated(activity, authentication) {
|
|
3120
|
+
return checkActorlessActivity(activity, authentication).status === "ok";
|
|
3121
|
+
}
|
|
3122
|
+
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 }) {
|
|
2829
3123
|
const logger = getLogger([
|
|
2830
3124
|
"fedify",
|
|
2831
3125
|
"federation",
|
|
2832
3126
|
"inbox"
|
|
2833
3127
|
]);
|
|
2834
|
-
if (activity.actorId == null) {
|
|
3128
|
+
if (activity.actorId == null && !actorless) {
|
|
2835
3129
|
logger.error("Missing actor.", { activity: json });
|
|
2836
3130
|
span.setStatus({
|
|
2837
3131
|
code: SpanStatusCode.ERROR,
|
|
@@ -2840,58 +3134,59 @@ async function routeActivity({ context: ctx, json, originalJson, normalizedActiv
|
|
|
2840
3134
|
recordInboxActivity(meterProvider, "rejected", getTypeId(activity).href);
|
|
2841
3135
|
return "missingActor";
|
|
2842
3136
|
}
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
keyString = `${ctx.origin}\n${activity.id.href}\n${recipient == null ? "sharedInbox" : `inbox\n${recipient}`}`;
|
|
2858
|
-
break;
|
|
2859
|
-
default: keyString = `${ctx.origin}\n${activity.id.href}`;
|
|
2860
|
-
}
|
|
2861
|
-
if (keyString != null) cacheKey = [...kvPrefixes.activityIdempotence, keyString];
|
|
3137
|
+
const contextMetadata = { authentication };
|
|
3138
|
+
const cacheKey = await getInboxIdempotencyKey(idempotencyStrategy, () => inboxContextFactory(recipient, json, activity.id?.href, getTypeId(activity).href, contextMetadata), activity, ctx.origin, recipient, kvPrefixes.activityIdempotence);
|
|
3139
|
+
if (await isInboxActivityProcessed(kv, cacheKey, activity, ctx.origin, kvPrefixes.activityIdempotence)) {
|
|
3140
|
+
logger.debug("Activity {activityId} has already been processed.", {
|
|
3141
|
+
activityId: activity.id?.href,
|
|
3142
|
+
activity: json,
|
|
3143
|
+
recipient
|
|
3144
|
+
});
|
|
3145
|
+
span.setStatus({
|
|
3146
|
+
code: SpanStatusCode.UNSET,
|
|
3147
|
+
message: `Activity ${activity.id?.href} has already been processed.`
|
|
3148
|
+
});
|
|
3149
|
+
recordInboxActivity(meterProvider, "rejected", getTypeId(activity).href);
|
|
3150
|
+
return "alreadyProcessed";
|
|
2862
3151
|
}
|
|
2863
|
-
if (
|
|
2864
|
-
|
|
2865
|
-
|
|
3152
|
+
if (activity.actorId != null) span.setAttribute("activitypub.actor.id", activity.actorId.href);
|
|
3153
|
+
let message;
|
|
3154
|
+
if (queue != null) {
|
|
3155
|
+
const carrier = {};
|
|
3156
|
+
propagation.inject(context.active(), carrier);
|
|
3157
|
+
message = {
|
|
3158
|
+
type: "inbox",
|
|
3159
|
+
id: crypto.randomUUID(),
|
|
3160
|
+
baseUrl: ctx.origin,
|
|
3161
|
+
activity: originalJson ?? json,
|
|
3162
|
+
...normalizedActivity == null ? {} : { normalizedActivity },
|
|
3163
|
+
...ldSignatureVerified == null ? {} : { ldSignatureVerified },
|
|
3164
|
+
...verifiedView == null ? {} : { verifiedView },
|
|
3165
|
+
...authentication == null ? {} : { authentication: serializeAuthentication(authentication) },
|
|
3166
|
+
identifier: recipient,
|
|
3167
|
+
attempt: 0,
|
|
3168
|
+
started: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3169
|
+
traceContext: carrier
|
|
3170
|
+
};
|
|
3171
|
+
const maxMessageSize = queue.maxMessageSize;
|
|
3172
|
+
if (replayable === false) {
|
|
3173
|
+
logger.warn("The incoming activity {activityId} uses a JSON-LD context that cannot be kept for the queue; processing it immediately instead.", {
|
|
2866
3174
|
activityId: activity.id?.href,
|
|
2867
|
-
activity: json,
|
|
2868
3175
|
recipient
|
|
2869
3176
|
});
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
3177
|
+
message = void 0;
|
|
3178
|
+
} else if (maxMessageSize != null && estimateQueuedMessageSize(message) > maxMessageSize - QUEUED_MESSAGE_HEADROOM) {
|
|
3179
|
+
logger.warn("The incoming activity {activityId} is too large to enqueue; processing it immediately instead.", {
|
|
3180
|
+
activityId: activity.id?.href,
|
|
3181
|
+
recipient,
|
|
3182
|
+
maxMessageSize
|
|
2873
3183
|
});
|
|
2874
|
-
|
|
2875
|
-
return "alreadyProcessed";
|
|
3184
|
+
message = void 0;
|
|
2876
3185
|
}
|
|
2877
3186
|
}
|
|
2878
|
-
|
|
2879
|
-
if (queue != null) {
|
|
2880
|
-
const carrier = {};
|
|
2881
|
-
propagation.inject(context.active(), carrier);
|
|
3187
|
+
if (queue != null && message != null) {
|
|
2882
3188
|
try {
|
|
2883
|
-
await queue.enqueue(
|
|
2884
|
-
type: "inbox",
|
|
2885
|
-
id: crypto.randomUUID(),
|
|
2886
|
-
baseUrl: ctx.origin,
|
|
2887
|
-
activity: originalJson ?? json,
|
|
2888
|
-
...normalizedActivity == null ? {} : { normalizedActivity },
|
|
2889
|
-
...ldSignatureVerified == null ? {} : { ldSignatureVerified },
|
|
2890
|
-
identifier: recipient,
|
|
2891
|
-
attempt: 0,
|
|
2892
|
-
started: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2893
|
-
traceContext: carrier
|
|
2894
|
-
});
|
|
3189
|
+
await queue.enqueue(message);
|
|
2895
3190
|
} catch (error) {
|
|
2896
3191
|
logger.error("Failed to enqueue the incoming activity {activityId}:\n{error}", {
|
|
2897
3192
|
error,
|
|
@@ -2941,7 +3236,7 @@ async function routeActivity({ context: ctx, json, originalJson, normalizedActiv
|
|
|
2941
3236
|
const started = performance.now();
|
|
2942
3237
|
try {
|
|
2943
3238
|
const contextFactory = listenerInboxContextFactory ?? inboxContextFactory;
|
|
2944
|
-
await listener(contextFactory(recipient, contextFactory === inboxContextFactory ? json : originalJson ?? json, activity.id?.href, activityType), activity);
|
|
3239
|
+
await listener(contextFactory(recipient, contextFactory === inboxContextFactory ? json : originalJson ?? json, activity.id?.href, activityType, contextMetadata), activity);
|
|
2945
3240
|
} finally {
|
|
2946
3241
|
getFederationMetrics(meterProvider).recordInboxProcessingDuration(activityType, getDurationMs(started));
|
|
2947
3242
|
}
|
|
@@ -2982,6 +3277,382 @@ async function routeActivity({ context: ctx, json, originalJson, normalizedActiv
|
|
|
2982
3277
|
});
|
|
2983
3278
|
}
|
|
2984
3279
|
//#endregion
|
|
3280
|
+
//#region src/federation/inbox-view.ts
|
|
3281
|
+
/**
|
|
3282
|
+
* The interpretation of an incoming activity that Fedify authenticated, kept
|
|
3283
|
+
* so that listeners, authorization helpers, and queue workers observe the
|
|
3284
|
+
* same JSON-LD meaning that signature verification observed.
|
|
3285
|
+
*
|
|
3286
|
+
* Every JSON-LD processing step of one delivery goes through a request-scoped
|
|
3287
|
+
* *context snapshot*, which memoizes the remote context responses so that
|
|
3288
|
+
* a nondeterministic or mutable remote context cannot give verification and
|
|
3289
|
+
* dispatch different meanings. The snapshot is serialized into queued
|
|
3290
|
+
* messages, so that workers and retries replay the very same responses
|
|
3291
|
+
* without loading contexts again.
|
|
3292
|
+
* @module
|
|
3293
|
+
*/
|
|
3294
|
+
/**
|
|
3295
|
+
* Thrown when replaying an interpretation needs a context response that its
|
|
3296
|
+
* snapshot does not have, or has with a different content.
|
|
3297
|
+
* @internal
|
|
3298
|
+
*/
|
|
3299
|
+
var ContextSnapshotMissError = class extends Error {
|
|
3300
|
+
url;
|
|
3301
|
+
constructor(url, reason) {
|
|
3302
|
+
super(`Context ${url} cannot be replayed: ${reason}.`);
|
|
3303
|
+
this.name = "ContextSnapshotMissError";
|
|
3304
|
+
this.url = url;
|
|
3305
|
+
}
|
|
3306
|
+
};
|
|
3307
|
+
function normalizeUrl(url) {
|
|
3308
|
+
return URL.canParse(url) ? new URL(url).href : url;
|
|
3309
|
+
}
|
|
3310
|
+
/**
|
|
3311
|
+
* Creates the context snapshot of a delivery.
|
|
3312
|
+
* @param base The caller's context loader.
|
|
3313
|
+
* @returns The snapshot.
|
|
3314
|
+
* @internal
|
|
3315
|
+
*/
|
|
3316
|
+
function createContextSnapshot(base) {
|
|
3317
|
+
const underlying = {
|
|
3318
|
+
standard: wrapContextLoaderForJsonLd(base),
|
|
3319
|
+
normalization: getNormalizationContextLoader(base)
|
|
3320
|
+
};
|
|
3321
|
+
const cache = /* @__PURE__ */ new Map();
|
|
3322
|
+
const recorded = /* @__PURE__ */ new Map();
|
|
3323
|
+
const loaders = /* @__PURE__ */ new Map();
|
|
3324
|
+
let released = false;
|
|
3325
|
+
return {
|
|
3326
|
+
loader(policy) {
|
|
3327
|
+
let loader = loaders.get(policy);
|
|
3328
|
+
if (loader != null) return loader;
|
|
3329
|
+
const effective = underlying[policy];
|
|
3330
|
+
loader = markEffectiveContextLoader(async (url, options) => {
|
|
3331
|
+
if (released) return await effective(url, options);
|
|
3332
|
+
const normalizedUrl = normalizeUrl(url);
|
|
3333
|
+
const key = `${policy}\n${normalizedUrl}`;
|
|
3334
|
+
let promise = cache.get(key);
|
|
3335
|
+
if (promise == null) {
|
|
3336
|
+
promise = Promise.resolve(effective(url, options)).then((response) => {
|
|
3337
|
+
const cloned = normalizeResponse(structuredClone(response));
|
|
3338
|
+
recorded.set(key, {
|
|
3339
|
+
policy,
|
|
3340
|
+
url: normalizedUrl,
|
|
3341
|
+
response: structuredClone(cloned)
|
|
3342
|
+
});
|
|
3343
|
+
return cloned;
|
|
3344
|
+
});
|
|
3345
|
+
promise.catch(() => {});
|
|
3346
|
+
cache.set(key, promise);
|
|
3347
|
+
}
|
|
3348
|
+
return structuredClone(await promise);
|
|
3349
|
+
}, policy);
|
|
3350
|
+
loaders.set(policy, loader);
|
|
3351
|
+
return loader;
|
|
3352
|
+
},
|
|
3353
|
+
entries() {
|
|
3354
|
+
return [...recorded.values()];
|
|
3355
|
+
},
|
|
3356
|
+
release() {
|
|
3357
|
+
released = true;
|
|
3358
|
+
}
|
|
3359
|
+
};
|
|
3360
|
+
}
|
|
3361
|
+
function getBundledResponse(url) {
|
|
3362
|
+
if (!(url in preloadedContexts)) return null;
|
|
3363
|
+
return {
|
|
3364
|
+
contextUrl: null,
|
|
3365
|
+
document: preloadedContexts[url],
|
|
3366
|
+
documentUrl: url
|
|
3367
|
+
};
|
|
3368
|
+
}
|
|
3369
|
+
async function sha256(value) {
|
|
3370
|
+
return encodeHex(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(serialize(value))));
|
|
3371
|
+
}
|
|
3372
|
+
function normalizeResponse(response) {
|
|
3373
|
+
return {
|
|
3374
|
+
contextUrl: response.contextUrl ?? null,
|
|
3375
|
+
document: response.document,
|
|
3376
|
+
documentUrl: response.documentUrl
|
|
3377
|
+
};
|
|
3378
|
+
}
|
|
3379
|
+
/**
|
|
3380
|
+
* Serializes the entries of a context snapshot for a queued message.
|
|
3381
|
+
* @param entries The entries.
|
|
3382
|
+
* @returns The serialized entries, which are plain JSON values.
|
|
3383
|
+
* @internal
|
|
3384
|
+
*/
|
|
3385
|
+
async function serializeContextSnapshot(entries) {
|
|
3386
|
+
const result = [];
|
|
3387
|
+
for (const entry of entries) {
|
|
3388
|
+
const response = normalizeResponse(entry.response);
|
|
3389
|
+
const hash = await sha256(response);
|
|
3390
|
+
const bundled = getBundledResponse(entry.url);
|
|
3391
|
+
if (bundled != null && await sha256(bundled) === hash) result.push({
|
|
3392
|
+
policy: entry.policy,
|
|
3393
|
+
url: entry.url,
|
|
3394
|
+
sha256: hash,
|
|
3395
|
+
bundled: true
|
|
3396
|
+
});
|
|
3397
|
+
else result.push({
|
|
3398
|
+
policy: entry.policy,
|
|
3399
|
+
url: entry.url,
|
|
3400
|
+
sha256: hash,
|
|
3401
|
+
response: structuredClone(response)
|
|
3402
|
+
});
|
|
3403
|
+
}
|
|
3404
|
+
return result;
|
|
3405
|
+
}
|
|
3406
|
+
function isRemoteDocument(value) {
|
|
3407
|
+
if (value == null || typeof value !== "object") return false;
|
|
3408
|
+
const record = value;
|
|
3409
|
+
return typeof record.documentUrl === "string" && "document" in record && (record.contextUrl == null || typeof record.contextUrl === "string");
|
|
3410
|
+
}
|
|
3411
|
+
/**
|
|
3412
|
+
* Restores the entries of a serialized context snapshot, verifying the
|
|
3413
|
+
* hashes of stored responses. Bundled entries are reconstructed from the
|
|
3414
|
+
* contexts this version of Fedify bundles.
|
|
3415
|
+
* @param entries The serialized entries.
|
|
3416
|
+
* @returns The entries.
|
|
3417
|
+
* @throws {ContextSnapshotMissError} If an entry is malformed, or its
|
|
3418
|
+
* content does not match its hash.
|
|
3419
|
+
* @internal
|
|
3420
|
+
*/
|
|
3421
|
+
async function deserializeContextSnapshot(entries) {
|
|
3422
|
+
if (!Array.isArray(entries)) throw new ContextSnapshotMissError("(snapshot)", "malformed snapshot");
|
|
3423
|
+
const result = [];
|
|
3424
|
+
for (const entry of entries) {
|
|
3425
|
+
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");
|
|
3426
|
+
let response;
|
|
3427
|
+
if (entry.bundled === true) {
|
|
3428
|
+
response = getBundledResponse(entry.url);
|
|
3429
|
+
if (response == null) throw new ContextSnapshotMissError(entry.url, "not bundled");
|
|
3430
|
+
} else if (isRemoteDocument(entry.response)) response = normalizeResponse(entry.response);
|
|
3431
|
+
else throw new ContextSnapshotMissError(entry.url, "malformed entry");
|
|
3432
|
+
if (entry.bundled !== true && await sha256(response) !== entry.sha256) throw new ContextSnapshotMissError(entry.url, "content mismatch");
|
|
3433
|
+
result.push({
|
|
3434
|
+
policy: entry.policy,
|
|
3435
|
+
url: entry.url,
|
|
3436
|
+
response: structuredClone(response)
|
|
3437
|
+
});
|
|
3438
|
+
}
|
|
3439
|
+
return result;
|
|
3440
|
+
}
|
|
3441
|
+
/**
|
|
3442
|
+
* Creates a context loader of a policy that serves only the responses of
|
|
3443
|
+
* a snapshot. It is marked as the effective loader of the policy.
|
|
3444
|
+
* @param entries The snapshot's entries.
|
|
3445
|
+
* @param policy The policy.
|
|
3446
|
+
* @returns The loader, which throws {@link ContextSnapshotMissError} for
|
|
3447
|
+
* anything else.
|
|
3448
|
+
* @internal
|
|
3449
|
+
*/
|
|
3450
|
+
function createSnapshotOnlyLoader(entries, policy) {
|
|
3451
|
+
const responses = /* @__PURE__ */ new Map();
|
|
3452
|
+
for (const entry of entries) if (entry.policy === policy) responses.set(entry.url, entry.response);
|
|
3453
|
+
return markEffectiveContextLoader(async (url) => {
|
|
3454
|
+
const response = responses.get(normalizeUrl(url));
|
|
3455
|
+
if (response == null) throw new ContextSnapshotMissError(url, "not in the snapshot");
|
|
3456
|
+
return structuredClone(response);
|
|
3457
|
+
}, policy);
|
|
3458
|
+
}
|
|
3459
|
+
/**
|
|
3460
|
+
* Creates a {@link ReleasableLoader}.
|
|
3461
|
+
* @param strict The loader to use until release.
|
|
3462
|
+
* @param fallback The loader to use after release.
|
|
3463
|
+
* @param policy The policy both loaders apply.
|
|
3464
|
+
* @returns The releasable loader.
|
|
3465
|
+
* @internal
|
|
3466
|
+
*/
|
|
3467
|
+
function createReleasableLoader(strict, fallback, policy) {
|
|
3468
|
+
let released = false;
|
|
3469
|
+
return {
|
|
3470
|
+
loader: markEffectiveContextLoader((url, options) => released ? fallback(url, options) : strict(url, options), policy),
|
|
3471
|
+
release() {
|
|
3472
|
+
released = true;
|
|
3473
|
+
}
|
|
3474
|
+
};
|
|
3475
|
+
}
|
|
3476
|
+
/**
|
|
3477
|
+
* Expands a JSON-LD document with the given context loader.
|
|
3478
|
+
* @param document The document.
|
|
3479
|
+
* @param contextLoader The context loader.
|
|
3480
|
+
* @returns The expanded document.
|
|
3481
|
+
* @internal
|
|
3482
|
+
*/
|
|
3483
|
+
async function expandDocument(document, contextLoader) {
|
|
3484
|
+
return await jsonld.expand(document, { documentLoader: contextLoader });
|
|
3485
|
+
}
|
|
3486
|
+
/**
|
|
3487
|
+
* Computes the digest of an expanded document.
|
|
3488
|
+
* @param expanded The expanded document.
|
|
3489
|
+
* @returns The hex-encoded SHA-256 of its canonical JSON.
|
|
3490
|
+
* @internal
|
|
3491
|
+
*/
|
|
3492
|
+
function digestExpandedDocument(expanded) {
|
|
3493
|
+
return sha256(expanded);
|
|
3494
|
+
}
|
|
3495
|
+
/**
|
|
3496
|
+
* Deeply freezes a JSON value.
|
|
3497
|
+
* @param value The value.
|
|
3498
|
+
* @returns The same value, frozen.
|
|
3499
|
+
* @internal
|
|
3500
|
+
*/
|
|
3501
|
+
function deepFreeze(value) {
|
|
3502
|
+
if (value != null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
3503
|
+
Object.freeze(value);
|
|
3504
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
3505
|
+
}
|
|
3506
|
+
return value;
|
|
3507
|
+
}
|
|
3508
|
+
const ACTOR_IRI = "https://www.w3.org/ns/activitystreams#actor";
|
|
3509
|
+
/**
|
|
3510
|
+
* Gets the single root node of an expanded document.
|
|
3511
|
+
* @param expanded The expanded document.
|
|
3512
|
+
* @returns The root node, or `null` if there is not exactly one.
|
|
3513
|
+
* @internal
|
|
3514
|
+
*/
|
|
3515
|
+
function getRootNode(expanded) {
|
|
3516
|
+
if (expanded.length !== 1) return null;
|
|
3517
|
+
const node = expanded[0];
|
|
3518
|
+
if (node == null || typeof node !== "object" || Array.isArray(node)) return null;
|
|
3519
|
+
return node;
|
|
3520
|
+
}
|
|
3521
|
+
/**
|
|
3522
|
+
* Gets the values of a property of an expanded node, unwrapping a single
|
|
3523
|
+
* `@list` container the way generated vocabulary decoders do.
|
|
3524
|
+
* @param node The expanded node.
|
|
3525
|
+
* @param iri The property IRI.
|
|
3526
|
+
* @returns The values, or an empty array if the property is absent.
|
|
3527
|
+
* @internal
|
|
3528
|
+
*/
|
|
3529
|
+
function getPropertyValues(node, iri) {
|
|
3530
|
+
const values = node[iri];
|
|
3531
|
+
if (values == null) return [];
|
|
3532
|
+
if (!Array.isArray(values)) return [values];
|
|
3533
|
+
if (values.length === 1 && values[0] != null && typeof values[0] === "object" && "@list" in values[0]) {
|
|
3534
|
+
const list = values[0]["@list"];
|
|
3535
|
+
return Array.isArray(list) ? list : [list];
|
|
3536
|
+
}
|
|
3537
|
+
return values;
|
|
3538
|
+
}
|
|
3539
|
+
/**
|
|
3540
|
+
* Tells whether the root node of an expanded activity declares any `actor`
|
|
3541
|
+
* value at all, including blank nodes, anonymous or untyped nodes, and
|
|
3542
|
+
* literals, which ID accessors and typed parsing may drop.
|
|
3543
|
+
* @param expanded The expanded activity.
|
|
3544
|
+
* @returns `"absent"` if the root node declares no actor; `"present"`
|
|
3545
|
+
* otherwise, including when there is no single root node.
|
|
3546
|
+
* @internal
|
|
3547
|
+
*/
|
|
3548
|
+
function getActorPresence(expanded) {
|
|
3549
|
+
const root = getRootNode(expanded);
|
|
3550
|
+
if (root == null) return "present";
|
|
3551
|
+
return getPropertyValues(root, ACTOR_IRI).length < 1 ? "absent" : "present";
|
|
3552
|
+
}
|
|
3553
|
+
/**
|
|
3554
|
+
* Builds the serialized view of an authenticated activity. The document is
|
|
3555
|
+
* expanded through the snapshot, so that the snapshot holds every context
|
|
3556
|
+
* response the interpretation needs.
|
|
3557
|
+
* @param snapshot The delivery's context snapshot.
|
|
3558
|
+
* @param source Which document the activity was parsed from.
|
|
3559
|
+
* @returns The serialized view and the expanded activity.
|
|
3560
|
+
* @internal
|
|
3561
|
+
*/
|
|
3562
|
+
async function buildVerifiedActivityView(snapshot, source) {
|
|
3563
|
+
const loader = snapshot.loader(source.policy);
|
|
3564
|
+
const used = /* @__PURE__ */ new Set();
|
|
3565
|
+
const expanded = await expandDocument(source.document, markEffectiveContextLoader((url, options) => {
|
|
3566
|
+
used.add(normalizeUrl(url));
|
|
3567
|
+
return loader(url, options);
|
|
3568
|
+
}, source.policy));
|
|
3569
|
+
const entries = snapshot.entries().filter((entry) => entry.policy === source.policy && used.has(entry.url));
|
|
3570
|
+
const contexts = await serializeContextSnapshot(entries);
|
|
3571
|
+
const references = /* @__PURE__ */ new Set();
|
|
3572
|
+
collectContextReferences(source.document, source.baseUrl?.href, references);
|
|
3573
|
+
for (const entry of entries) {
|
|
3574
|
+
const { contextUrl, document } = entry.response;
|
|
3575
|
+
collectContextReferences(document, entry.url, references);
|
|
3576
|
+
if (contextUrl != null) collectContextReferences({ "@context": contextUrl }, entry.url, references);
|
|
3577
|
+
}
|
|
3578
|
+
return {
|
|
3579
|
+
replayable: [...references].every((url) => used.has(url)),
|
|
3580
|
+
view: {
|
|
3581
|
+
version: 1,
|
|
3582
|
+
policy: source.policy,
|
|
3583
|
+
source: source.source,
|
|
3584
|
+
baseUrl: source.baseUrl?.href ?? null,
|
|
3585
|
+
contexts,
|
|
3586
|
+
expandedDigest: await digestExpandedDocument(expanded)
|
|
3587
|
+
},
|
|
3588
|
+
expanded
|
|
3589
|
+
};
|
|
3590
|
+
}
|
|
3591
|
+
/**
|
|
3592
|
+
* Collects the remote context references of a JSON-LD document or context
|
|
3593
|
+
* document: every IRI given as a value of `@context` at any depth, including
|
|
3594
|
+
* scoped contexts of term definitions, and of `@import`.
|
|
3595
|
+
* @param value The document.
|
|
3596
|
+
* @param base The URL to resolve relative references against.
|
|
3597
|
+
* @param references The set to add the normalized references to.
|
|
3598
|
+
*/
|
|
3599
|
+
function collectContextReferences(value, base, references) {
|
|
3600
|
+
const add = (reference) => {
|
|
3601
|
+
let url = reference;
|
|
3602
|
+
if (base != null && URL.canParse(reference, base)) url = new URL(reference, base).href;
|
|
3603
|
+
references.add(normalizeUrl(url));
|
|
3604
|
+
};
|
|
3605
|
+
const collectContext = (context) => {
|
|
3606
|
+
if (typeof context === "string") add(context);
|
|
3607
|
+
else if (Array.isArray(context)) for (const item of context) collectContext(item);
|
|
3608
|
+
else if (context != null && typeof context === "object") {
|
|
3609
|
+
const imported = context["@import"];
|
|
3610
|
+
if (typeof imported === "string") add(imported);
|
|
3611
|
+
walk(context);
|
|
3612
|
+
}
|
|
3613
|
+
};
|
|
3614
|
+
const walk = (node) => {
|
|
3615
|
+
if (Array.isArray(node)) for (const item of node) walk(item);
|
|
3616
|
+
else if (node != null && typeof node === "object") for (const [key, item] of Object.entries(node)) if (key === "@context") collectContext(item);
|
|
3617
|
+
else walk(item);
|
|
3618
|
+
};
|
|
3619
|
+
walk(value);
|
|
3620
|
+
}
|
|
3621
|
+
/**
|
|
3622
|
+
* Validates the shape of a serialized view taken from a queued message.
|
|
3623
|
+
* @param value The value of the message's `verifiedView` field.
|
|
3624
|
+
* @returns The view, or `null` if it is malformed or of an unsupported
|
|
3625
|
+
* version.
|
|
3626
|
+
* @internal
|
|
3627
|
+
*/
|
|
3628
|
+
function parseSerializedVerifiedActivityView(value) {
|
|
3629
|
+
if (value == null || typeof value !== "object") return null;
|
|
3630
|
+
const view = value;
|
|
3631
|
+
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;
|
|
3632
|
+
return view;
|
|
3633
|
+
}
|
|
3634
|
+
/**
|
|
3635
|
+
* Restores a view from a queued message without loading any context.
|
|
3636
|
+
* @param view The serialized view.
|
|
3637
|
+
* @param message The fields of the message the view refers to.
|
|
3638
|
+
* @returns The restored view.
|
|
3639
|
+
* @throws {ContextSnapshotMissError} If the view cannot be replayed exactly.
|
|
3640
|
+
* @internal
|
|
3641
|
+
*/
|
|
3642
|
+
async function restoreVerifiedActivityView(view, message) {
|
|
3643
|
+
const loader = createSnapshotOnlyLoader(await deserializeContextSnapshot(view.contexts), view.policy);
|
|
3644
|
+
if (view.source === "normalizedActivity" && message.normalizedActivity == null) throw new ContextSnapshotMissError("(normalizedActivity)", "the normalized activity is missing");
|
|
3645
|
+
const document = detachSignature(view.source === "activity" ? message.activity : message.normalizedActivity);
|
|
3646
|
+
const expanded = await expandDocument(document, loader);
|
|
3647
|
+
if (await digestExpandedDocument(expanded) !== view.expandedDigest) throw new ContextSnapshotMissError("(activity)", "the interpretation does not match the authenticated one");
|
|
3648
|
+
return {
|
|
3649
|
+
document,
|
|
3650
|
+
loader,
|
|
3651
|
+
expanded,
|
|
3652
|
+
baseUrl: view.baseUrl == null ? null : new URL(view.baseUrl)
|
|
3653
|
+
};
|
|
3654
|
+
}
|
|
3655
|
+
//#endregion
|
|
2985
3656
|
//#region src/federation/keycache.ts
|
|
2986
3657
|
const KEY_CACHE_GENERATION = "3";
|
|
2987
3658
|
const COMPATIBLE_KEY_SEGMENT = "__compatible";
|
|
@@ -3645,9 +4316,10 @@ function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris }) {
|
|
|
3645
4316
|
*
|
|
3646
4317
|
* @param parameters The parameters for sending the activity.
|
|
3647
4318
|
* See also {@link SendActivityParameters}.
|
|
4319
|
+
* @param deps The dependencies, for tests.
|
|
3648
4320
|
* @throws {Error} If the activity fails to send.
|
|
3649
4321
|
*/
|
|
3650
|
-
function sendActivity(options) {
|
|
4322
|
+
function sendActivity(options, deps = {}) {
|
|
3651
4323
|
const tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
|
|
3652
4324
|
return tracerProvider.getTracer(name, version).startActiveSpan("activitypub.send_activity", {
|
|
3653
4325
|
kind: SpanKind.CLIENT,
|
|
@@ -3663,7 +4335,7 @@ function sendActivity(options) {
|
|
|
3663
4335
|
}, () => sendActivityInternal({
|
|
3664
4336
|
...options,
|
|
3665
4337
|
tracerProvider
|
|
3666
|
-
}, span));
|
|
4338
|
+
}, span, deps));
|
|
3667
4339
|
} catch (e) {
|
|
3668
4340
|
span.setStatus({
|
|
3669
4341
|
code: SpanStatusCode.ERROR,
|
|
@@ -3748,13 +4420,14 @@ async function readLimitedResponseBody(response, maxBytes) {
|
|
|
3748
4420
|
totalBytes += value.length;
|
|
3749
4421
|
}
|
|
3750
4422
|
} finally {
|
|
4423
|
+
if (truncated) reader.cancel().catch(() => {});
|
|
3751
4424
|
reader.releaseLock();
|
|
3752
4425
|
}
|
|
3753
4426
|
let result = chunks.join("");
|
|
3754
4427
|
if (truncated) result += "… (truncated)";
|
|
3755
4428
|
return result;
|
|
3756
4429
|
}
|
|
3757
|
-
async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, allowPrivateAddress, headers, specDeterminer, meterProvider, tracerProvider }, span) {
|
|
4430
|
+
async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, allowPrivateAddress, headers, specDeterminer, meterProvider, tracerProvider }, span, deps) {
|
|
3758
4431
|
const logger = getLogger([
|
|
3759
4432
|
"fedify",
|
|
3760
4433
|
"federation",
|
|
@@ -3763,9 +4436,11 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
|
|
|
3763
4436
|
const federationMetrics = getFederationMetrics(meterProvider);
|
|
3764
4437
|
const started = performance.now();
|
|
3765
4438
|
let deliverySuccess = false;
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
4439
|
+
const openUnvalidatedHop = deps.openHop ?? (allowPrivateAddress ? openDirectFetch : (url) => openPublicFetch(url));
|
|
4440
|
+
async function openHop(url) {
|
|
4441
|
+
let session;
|
|
4442
|
+
try {
|
|
4443
|
+
session = await openUnvalidatedHop(url);
|
|
3769
4444
|
} catch (error) {
|
|
3770
4445
|
if (error instanceof UrlError && error.reason === "dns") {
|
|
3771
4446
|
logger.error("DNS resolution failed for URL: {url}", {
|
|
@@ -3782,82 +4457,90 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
|
|
|
3782
4457
|
});
|
|
3783
4458
|
throw error;
|
|
3784
4459
|
}
|
|
4460
|
+
return session;
|
|
3785
4461
|
}
|
|
4462
|
+
let initialSession;
|
|
3786
4463
|
try {
|
|
3787
|
-
await
|
|
4464
|
+
initialSession = await openHop(inbox.href);
|
|
3788
4465
|
} catch (error) {
|
|
3789
4466
|
if (error instanceof FetchError) federationMetrics.recordDelivery(inbox, getDurationMs(started), false, activityType);
|
|
3790
4467
|
throw error;
|
|
3791
4468
|
}
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
}
|
|
3800
|
-
let rsaKey = null;
|
|
3801
|
-
for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
|
|
3802
|
-
rsaKey = key;
|
|
3803
|
-
break;
|
|
3804
|
-
}
|
|
3805
|
-
if (rsaKey == null && keys.length < 1) logger.debug("Sending the activity {activityId} to {inbox} without a signature.", {
|
|
3806
|
-
activityId,
|
|
3807
|
-
inbox: inbox.href
|
|
3808
|
-
});
|
|
3809
|
-
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.", {
|
|
3810
|
-
inbox: inbox.href,
|
|
3811
|
-
keys: keys.map((pair) => ({
|
|
3812
|
-
keyId: pair.keyId.href,
|
|
3813
|
-
privateKey: pair.privateKey
|
|
3814
|
-
}))
|
|
3815
|
-
});
|
|
3816
|
-
let response;
|
|
4469
|
+
let initialPending = true;
|
|
4470
|
+
const openDeliveryHop = (url) => {
|
|
4471
|
+
if (initialPending) {
|
|
4472
|
+
initialPending = false;
|
|
4473
|
+
return Promise.resolve(initialSession);
|
|
4474
|
+
}
|
|
4475
|
+
return openHop(url);
|
|
4476
|
+
};
|
|
3817
4477
|
try {
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
4478
|
+
headers = new Headers(headers);
|
|
4479
|
+
headers.set("Content-Type", "application/activity+json");
|
|
4480
|
+
const request = new Request(inbox, {
|
|
4481
|
+
method: "POST",
|
|
4482
|
+
headers,
|
|
4483
|
+
body: JSON.stringify(activity),
|
|
4484
|
+
redirect: "manual"
|
|
3822
4485
|
});
|
|
3823
|
-
|
|
3824
|
-
const
|
|
3825
|
-
|
|
4486
|
+
let rsaKey = null;
|
|
4487
|
+
for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
|
|
4488
|
+
rsaKey = key;
|
|
4489
|
+
break;
|
|
4490
|
+
}
|
|
4491
|
+
if (rsaKey == null && keys.length < 1) logger.debug("Sending the activity {activityId} to {inbox} without a signature.", {
|
|
3826
4492
|
activityId,
|
|
4493
|
+
inbox: inbox.href
|
|
4494
|
+
});
|
|
4495
|
+
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.", {
|
|
3827
4496
|
inbox: inbox.href,
|
|
3828
|
-
|
|
4497
|
+
keys: keys.map((pair) => ({
|
|
4498
|
+
keyId: pair.keyId.href,
|
|
4499
|
+
privateKey: pair.privateKey
|
|
4500
|
+
}))
|
|
3829
4501
|
});
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
}
|
|
3839
|
-
error = "";
|
|
3840
|
-
}
|
|
3841
|
-
logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
|
|
4502
|
+
let outcome;
|
|
4503
|
+
try {
|
|
4504
|
+
outcome = rsaKey == null ? await fetchWithValidatedRedirects(request, openDeliveryHop, (response) => consumeSendActivityResponse(response, inbox)) : await doubleKnockScoped(request, rsaKey, {
|
|
4505
|
+
tracerProvider,
|
|
4506
|
+
specDeterminer
|
|
4507
|
+
}, openDeliveryHop, (response) => consumeSendActivityResponse(response, inbox));
|
|
4508
|
+
} catch (error) {
|
|
4509
|
+
const failure = error instanceof FetchError || error instanceof UrlError ? error : createFetchError(inbox.href, error);
|
|
4510
|
+
logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
|
|
3842
4511
|
activityId,
|
|
3843
4512
|
inbox: inbox.href,
|
|
3844
|
-
|
|
3845
|
-
statusText: response.statusText,
|
|
3846
|
-
error
|
|
4513
|
+
error: failure
|
|
3847
4514
|
});
|
|
3848
|
-
|
|
4515
|
+
federationMetrics.recordDelivery(inbox, getDurationMs(started), false, activityType);
|
|
4516
|
+
throw failure;
|
|
4517
|
+
}
|
|
4518
|
+
try {
|
|
4519
|
+
if (!outcome.ok) {
|
|
4520
|
+
const { status, statusText, error, responseHeaders } = outcome;
|
|
4521
|
+
logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
|
|
4522
|
+
activityId,
|
|
4523
|
+
inbox: inbox.href,
|
|
4524
|
+
status,
|
|
4525
|
+
statusText,
|
|
4526
|
+
error
|
|
4527
|
+
});
|
|
4528
|
+
throw new SendActivityError(inbox, status, `Failed to send activity ${activityId} to ${inbox.href} (${status} ${statusText}):\n${error}`, error, responseHeaders);
|
|
4529
|
+
}
|
|
4530
|
+
deliverySuccess = true;
|
|
4531
|
+
const eventAttributes = {
|
|
4532
|
+
"activitypub.inbox.url": inbox.href,
|
|
4533
|
+
"activitypub.activity.id": activityId ?? ""
|
|
4534
|
+
};
|
|
4535
|
+
if (activityType != null) eventAttributes["activitypub.activity.type"] = activityType;
|
|
4536
|
+
const actorId = getActivityActorId(activity);
|
|
4537
|
+
if (actorId != null) eventAttributes["activitypub.actor.id"] = actorId;
|
|
4538
|
+
span.addEvent("activitypub.activity.sent", eventAttributes);
|
|
4539
|
+
} finally {
|
|
4540
|
+
federationMetrics.recordDelivery(inbox, getDurationMs(started), deliverySuccess, activityType);
|
|
3849
4541
|
}
|
|
3850
|
-
deliverySuccess = true;
|
|
3851
|
-
const eventAttributes = {
|
|
3852
|
-
"activitypub.inbox.url": inbox.href,
|
|
3853
|
-
"activitypub.activity.id": activityId ?? ""
|
|
3854
|
-
};
|
|
3855
|
-
if (activityType != null) eventAttributes["activitypub.activity.type"] = activityType;
|
|
3856
|
-
const actorId = getActivityActorId(activity);
|
|
3857
|
-
if (actorId != null) eventAttributes["activitypub.actor.id"] = actorId;
|
|
3858
|
-
span.addEvent("activitypub.activity.sent", eventAttributes);
|
|
3859
4542
|
} finally {
|
|
3860
|
-
|
|
4543
|
+
if (initialPending) await initialSession.close();
|
|
3861
4544
|
}
|
|
3862
4545
|
}
|
|
3863
4546
|
function createFetchError(url, cause) {
|
|
@@ -3865,10 +4548,75 @@ function createFetchError(url, cause) {
|
|
|
3865
4548
|
error.cause = cause;
|
|
3866
4549
|
return error;
|
|
3867
4550
|
}
|
|
3868
|
-
|
|
4551
|
+
/**
|
|
4552
|
+
* Starts cancelling the body of a response without waiting for it to settle.
|
|
4553
|
+
* The cancellation of a branch of a tee (e.g., a body cloned by a fetch mock)
|
|
4554
|
+
* settles only after the other branch is cancelled too, per the Streams
|
|
4555
|
+
* standard, so awaiting it could block forever. Failures are only logged.
|
|
4556
|
+
*/
|
|
4557
|
+
function cancelResponseBody(response, url) {
|
|
4558
|
+
response.body?.cancel().catch((error) => {
|
|
4559
|
+
getLogger([
|
|
4560
|
+
"fedify",
|
|
4561
|
+
"federation",
|
|
4562
|
+
"outbox"
|
|
4563
|
+
]).debug("Failed to cancel the response body from {url}: {error}", {
|
|
4564
|
+
url,
|
|
4565
|
+
error
|
|
4566
|
+
});
|
|
4567
|
+
});
|
|
4568
|
+
}
|
|
4569
|
+
/**
|
|
4570
|
+
* Reads what is needed from the final response of a delivery while its
|
|
4571
|
+
* session is open: the body of a successful response is cancelled, and at
|
|
4572
|
+
* most 1 KiB of an error response body is read.
|
|
4573
|
+
*/
|
|
4574
|
+
async function consumeSendActivityResponse(response, inbox) {
|
|
4575
|
+
if (response.ok) {
|
|
4576
|
+
cancelResponseBody(response, response.url || inbox.href);
|
|
4577
|
+
return { ok: true };
|
|
4578
|
+
}
|
|
4579
|
+
let error;
|
|
4580
|
+
try {
|
|
4581
|
+
error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
|
|
4582
|
+
} catch (_) {
|
|
4583
|
+
error = "";
|
|
4584
|
+
}
|
|
4585
|
+
return {
|
|
4586
|
+
ok: false,
|
|
4587
|
+
status: response.status,
|
|
4588
|
+
statusText: response.statusText,
|
|
4589
|
+
error,
|
|
4590
|
+
responseHeaders: new Headers(response.headers)
|
|
4591
|
+
};
|
|
4592
|
+
}
|
|
4593
|
+
/**
|
|
4594
|
+
* Sends a request without signing it, following redirects manually like the
|
|
4595
|
+
* Fetch standard does. Every hop is opened with `openHop()`, which validates
|
|
4596
|
+
* the destination, after the previous hop is closed. The final response is
|
|
4597
|
+
* passed to `consume()`, and its session is closed after that.
|
|
4598
|
+
* This is exported only for tests; it is not exported from any entry point.
|
|
4599
|
+
* @param request The request to send.
|
|
4600
|
+
* @param openHop The function to open a session for each hop.
|
|
4601
|
+
* @param consume The function to consume the final response.
|
|
4602
|
+
* @returns The value returned by `consume`.
|
|
4603
|
+
*/
|
|
4604
|
+
async function fetchWithValidatedRedirects(request, openHop, consume) {
|
|
4605
|
+
const logger = getLogger([
|
|
4606
|
+
"fedify",
|
|
4607
|
+
"federation",
|
|
4608
|
+
"outbox"
|
|
4609
|
+
]);
|
|
3869
4610
|
let body = await request.clone().text();
|
|
3870
4611
|
for (let redirects = 0;; redirects++) {
|
|
3871
|
-
const
|
|
4612
|
+
const session = await openHop(request.url);
|
|
4613
|
+
let response;
|
|
4614
|
+
try {
|
|
4615
|
+
response = await session.fetch(request, { redirect: "manual" });
|
|
4616
|
+
} catch (error) {
|
|
4617
|
+
await closeAfterError(session, logger);
|
|
4618
|
+
throw error;
|
|
4619
|
+
}
|
|
3872
4620
|
const location = response.headers.get("Location");
|
|
3873
4621
|
if (![
|
|
3874
4622
|
301,
|
|
@@ -3876,11 +4624,21 @@ async function fetchWithValidatedRedirects(request, validateUrl) {
|
|
|
3876
4624
|
303,
|
|
3877
4625
|
307,
|
|
3878
4626
|
308
|
|
3879
|
-
].includes(response.status) || location == null)
|
|
3880
|
-
|
|
4627
|
+
].includes(response.status) || location == null) try {
|
|
4628
|
+
return await consume(response);
|
|
4629
|
+
} catch (error) {
|
|
4630
|
+
logger.debug("Failed to consume the response from {url}; closing the session: {error}", {
|
|
4631
|
+
url: request.url,
|
|
4632
|
+
error
|
|
4633
|
+
});
|
|
4634
|
+
throw error;
|
|
4635
|
+
} finally {
|
|
4636
|
+
await session.close();
|
|
4637
|
+
}
|
|
4638
|
+
cancelResponseBody(response, request.url);
|
|
4639
|
+
await session.close();
|
|
3881
4640
|
if (redirects >= 20) throw new TypeError("Too many redirects");
|
|
3882
4641
|
const url = new URL(location, request.url);
|
|
3883
|
-
await validateUrl(url.href);
|
|
3884
4642
|
const headers = new Headers(request.headers);
|
|
3885
4643
|
if (url.origin !== new URL(request.url).origin) {
|
|
3886
4644
|
headers.delete("Authorization");
|
|
@@ -3908,6 +4666,16 @@ async function fetchWithValidatedRedirects(request, validateUrl) {
|
|
|
3908
4666
|
});
|
|
3909
4667
|
}
|
|
3910
4668
|
}
|
|
4669
|
+
async function closeAfterError(session, logger) {
|
|
4670
|
+
try {
|
|
4671
|
+
await session.close();
|
|
4672
|
+
} catch (error) {
|
|
4673
|
+
logger.debug("Failed to close the session for {url}: {error}", {
|
|
4674
|
+
url: session.url.href,
|
|
4675
|
+
error
|
|
4676
|
+
});
|
|
4677
|
+
}
|
|
4678
|
+
}
|
|
3911
4679
|
/**
|
|
3912
4680
|
* An error that is thrown when an activity fails to send to a remote inbox.
|
|
3913
4681
|
* It contains structured information about the failure, including the HTTP
|
|
@@ -4792,13 +5560,13 @@ function recordCollectionMetrics(meterProvider, base, result, options = {}) {
|
|
|
4792
5560
|
* @param parameters The parameters for handling the collection.
|
|
4793
5561
|
* @returns A promise that resolves to an HTTP response.
|
|
4794
5562
|
*/
|
|
4795
|
-
async function handleCollection(request, { name: name$
|
|
4796
|
-
const spanName = name$
|
|
5563
|
+
async function handleCollection(request, { name: name$1, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, portable, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
|
|
5564
|
+
const spanName = name$1.trim().replace(/\s+/g, "_");
|
|
4797
5565
|
tracerProvider = tracerProvider ?? trace.getTracerProvider();
|
|
4798
5566
|
const tracer = tracerProvider.getTracer(name, version);
|
|
4799
5567
|
const cursor = new URL(request.url).searchParams.get("cursor");
|
|
4800
5568
|
const metricBase = {
|
|
4801
|
-
kind: getCollectionMetricKind(name$
|
|
5569
|
+
kind: getCollectionMetricKind(name$1),
|
|
4802
5570
|
page: cursor != null,
|
|
4803
5571
|
dispatcher: "built_in"
|
|
4804
5572
|
};
|
|
@@ -4844,7 +5612,7 @@ async function handleCollection(request, { name: name$2, identifier, uriGetter,
|
|
|
4844
5612
|
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
4845
5613
|
return await onNotFound(request);
|
|
4846
5614
|
}
|
|
4847
|
-
const items = filterCollectionItems(page.items, name$
|
|
5615
|
+
const items = filterCollectionItems(page.items, name$1, filterPredicate);
|
|
4848
5616
|
itemCount = items.length;
|
|
4849
5617
|
span.setAttribute("fedify.collection.items", itemCount);
|
|
4850
5618
|
return items;
|
|
@@ -4886,7 +5654,7 @@ async function handleCollection(request, { name: name$2, identifier, uriGetter,
|
|
|
4886
5654
|
} else {
|
|
4887
5655
|
const uri = new URL(portable == null ? baseUri : pageBase);
|
|
4888
5656
|
uri.searchParams.set("cursor", cursor);
|
|
4889
|
-
const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$
|
|
5657
|
+
const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$1}`, {
|
|
4890
5658
|
kind: SpanKind.SERVER,
|
|
4891
5659
|
attributes: {
|
|
4892
5660
|
"activitypub.collection.id": uri.href,
|
|
@@ -4902,7 +5670,7 @@ async function handleCollection(request, { name: name$2, identifier, uriGetter,
|
|
|
4902
5670
|
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
4903
5671
|
return await onNotFound(request);
|
|
4904
5672
|
}
|
|
4905
|
-
const items = filterCollectionItems(page.items, name$
|
|
5673
|
+
const items = filterCollectionItems(page.items, name$1, filterPredicate);
|
|
4906
5674
|
itemCount = items.length;
|
|
4907
5675
|
span.setAttribute("fedify.collection.items", itemCount);
|
|
4908
5676
|
return {
|
|
@@ -5399,11 +6167,12 @@ async function handleInbox(request, options) {
|
|
|
5399
6167
|
}, async (span) => {
|
|
5400
6168
|
if (options.recipient != null) span.setAttribute("fedify.inbox.recipient", options.recipient);
|
|
5401
6169
|
const spanContext = span.spanContext();
|
|
6170
|
+
const snapshot = createContextSnapshot(options.context.contextLoader);
|
|
5402
6171
|
try {
|
|
5403
6172
|
return await withContext({
|
|
5404
6173
|
traceId: spanContext.traceId,
|
|
5405
6174
|
spanId: spanContext.spanId
|
|
5406
|
-
}, () => handleInboxInternal(request, options, span));
|
|
6175
|
+
}, () => handleInboxInternal(request, options, span, snapshot));
|
|
5407
6176
|
} catch (e) {
|
|
5408
6177
|
options.observation.hasException = true;
|
|
5409
6178
|
span.setStatus({
|
|
@@ -5412,6 +6181,7 @@ async function handleInbox(request, options) {
|
|
|
5412
6181
|
});
|
|
5413
6182
|
throw e;
|
|
5414
6183
|
} finally {
|
|
6184
|
+
snapshot.release();
|
|
5415
6185
|
options.observation.project(span);
|
|
5416
6186
|
span.end();
|
|
5417
6187
|
}
|
|
@@ -5423,9 +6193,10 @@ async function handleInbox(request, options) {
|
|
|
5423
6193
|
* @param request The HTTP request.
|
|
5424
6194
|
* @param options The parameters for handling the inbox.
|
|
5425
6195
|
* @param span The OpenTelemetry span for tracing.
|
|
6196
|
+
* @param snapshot The context snapshot of the delivery.
|
|
5426
6197
|
* @returns A promise that resolves to an HTTP response.
|
|
5427
6198
|
*/
|
|
5428
|
-
async function handleInboxInternal(request, parameters, span) {
|
|
6199
|
+
async function handleInboxInternal(request, parameters, span, snapshot) {
|
|
5429
6200
|
const { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, publicKeyTtl, queue, actorDispatcher, inboxListeners, inboxErrorHandler, unverifiedActivityHandler, onNotFound, signatureTimeWindow, maxHttpSignatures, skipSignatureVerification, inboxChallengePolicy, meterProvider, tracerProvider, portableInbox } = parameters;
|
|
5430
6201
|
const observation = parameters.observation;
|
|
5431
6202
|
const signatureObservation = { [verificationObservation]: observation.verification };
|
|
@@ -5539,6 +6310,8 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5539
6310
|
tracerProvider,
|
|
5540
6311
|
keyTtl: publicKeyTtl
|
|
5541
6312
|
});
|
|
6313
|
+
const standardLoader = snapshot.loader("standard");
|
|
6314
|
+
const normalizationLoader = snapshot.loader("normalization");
|
|
5542
6315
|
const jsonWithoutSig = detachSignature(json);
|
|
5543
6316
|
const hasLdSignature = hasSignature(json);
|
|
5544
6317
|
const canAttemptAlternateAuthAfterLdSignatureFailure = skipSignatureVerification || hasHttpSignatureHeaders(request) || hasObjectIntegrityProof(jsonWithoutSig);
|
|
@@ -5574,9 +6347,10 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5574
6347
|
let compactedJson = json;
|
|
5575
6348
|
let compactedJsonWithoutSig = jsonWithoutSig;
|
|
5576
6349
|
let ldSigVerified = false;
|
|
6350
|
+
let ldSigKey = null;
|
|
5577
6351
|
if (hasLdSignature) {
|
|
5578
6352
|
try {
|
|
5579
|
-
compactedJson = await compactJsonLd(json,
|
|
6353
|
+
compactedJson = await compactJsonLd(json, normalizationLoader);
|
|
5580
6354
|
} catch (error) {
|
|
5581
6355
|
if (isInvalidJsonLdError(error)) {
|
|
5582
6356
|
logger.error("Failed to parse JSON-LD:\n{error}", {
|
|
@@ -5612,16 +6386,18 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5612
6386
|
if (compactedJson !== json) {
|
|
5613
6387
|
compactedJsonWithoutSig = detachSignature(compactedJson);
|
|
5614
6388
|
try {
|
|
5615
|
-
|
|
6389
|
+
const ldResult = await verifyCompactJsonLdDetailed(compactedJson, {
|
|
5616
6390
|
...signatureObservation,
|
|
5617
|
-
contextLoader:
|
|
6391
|
+
contextLoader: normalizationLoader,
|
|
5618
6392
|
documentLoader: ctx.documentLoader,
|
|
5619
6393
|
keyCache,
|
|
5620
6394
|
meterProvider,
|
|
5621
6395
|
tracerProvider
|
|
5622
6396
|
});
|
|
6397
|
+
ldSigVerified = ldResult.verified;
|
|
6398
|
+
ldSigKey = ldResult.key;
|
|
5623
6399
|
} catch (error) {
|
|
5624
|
-
if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig,
|
|
6400
|
+
if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig, normalizationLoader)) return await respondInvalidActivity(error);
|
|
5625
6401
|
if (isInvalidJsonLdError(error)) {
|
|
5626
6402
|
logger.error("Failed to parse JSON-LD:\n{error}", {
|
|
5627
6403
|
recipient,
|
|
@@ -5639,12 +6415,12 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5639
6415
|
if (!canAttemptAlternateAuthAfterLdSignatureFailure) throw error;
|
|
5640
6416
|
if (!skipSignatureVerification) try {
|
|
5641
6417
|
await Object$1.fromJsonLd(compactedJson, {
|
|
5642
|
-
contextLoader:
|
|
6418
|
+
contextLoader: normalizationLoader,
|
|
5643
6419
|
documentLoader: ctx.documentLoader,
|
|
5644
6420
|
tracerProvider
|
|
5645
6421
|
});
|
|
5646
6422
|
} catch (parseError) {
|
|
5647
|
-
if (parseError instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig,
|
|
6423
|
+
if (parseError instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig, normalizationLoader)) return await respondInvalidActivity(parseError);
|
|
5648
6424
|
if (isInvalidJsonLdError(parseError)) {
|
|
5649
6425
|
logger.error("Failed to parse JSON-LD:\n{error}", {
|
|
5650
6426
|
recipient,
|
|
@@ -5668,6 +6444,33 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5668
6444
|
let activity = null;
|
|
5669
6445
|
let activityVerified = false;
|
|
5670
6446
|
let proofVerified = false;
|
|
6447
|
+
let documentSigners = [];
|
|
6448
|
+
let requestSigners = [];
|
|
6449
|
+
let viewSource;
|
|
6450
|
+
const rawViewSource = {
|
|
6451
|
+
policy: "standard",
|
|
6452
|
+
source: "activity",
|
|
6453
|
+
document: jsonWithoutSig,
|
|
6454
|
+
baseUrl: null
|
|
6455
|
+
};
|
|
6456
|
+
const isActorlessCandidate = async (candidate, source) => {
|
|
6457
|
+
if (skipSignatureVerification) return false;
|
|
6458
|
+
if (inboxListeners?.dispatchWithClass(candidate)?.options.allowActorless !== true) return false;
|
|
6459
|
+
try {
|
|
6460
|
+
return getActorPresence(await expandDocument(source.document, snapshot.loader(source.policy))) === "absent";
|
|
6461
|
+
} catch {
|
|
6462
|
+
return false;
|
|
6463
|
+
}
|
|
6464
|
+
};
|
|
6465
|
+
const verifyProofs = (contextLoader = standardLoader) => verifyObjectDetailed(Activity, jsonWithoutSig, {
|
|
6466
|
+
...signatureObservation,
|
|
6467
|
+
contextLoader,
|
|
6468
|
+
documentLoader: ctx.documentLoader,
|
|
6469
|
+
keyCache,
|
|
6470
|
+
meterProvider,
|
|
6471
|
+
tracerProvider,
|
|
6472
|
+
verifyPortableObject: ctx.verifyPortableObject
|
|
6473
|
+
});
|
|
5671
6474
|
if (ldSigVerified) {
|
|
5672
6475
|
logger.debug("Linked Data Signatures are verified.", {
|
|
5673
6476
|
recipient,
|
|
@@ -5676,18 +6479,49 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5676
6479
|
try {
|
|
5677
6480
|
activity = observation.activity = await Activity.fromJsonLd(compactedJsonWithoutSig, {
|
|
5678
6481
|
...ctx,
|
|
5679
|
-
contextLoader:
|
|
6482
|
+
contextLoader: normalizationLoader
|
|
5680
6483
|
});
|
|
5681
6484
|
} catch (error) {
|
|
5682
|
-
if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig,
|
|
6485
|
+
if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig, normalizationLoader)) return await respondInvalidActivity(error);
|
|
5683
6486
|
if (!isPermanentActivityParseError(error)) throw error;
|
|
5684
6487
|
return await respondInvalidActivity(error);
|
|
5685
6488
|
}
|
|
5686
6489
|
activityVerified = true;
|
|
6490
|
+
viewSource = {
|
|
6491
|
+
policy: "normalization",
|
|
6492
|
+
source: "normalizedActivity",
|
|
6493
|
+
document: compactedJsonWithoutSig,
|
|
6494
|
+
baseUrl: null
|
|
6495
|
+
};
|
|
6496
|
+
if (ldSigKey?.ownerId != null) documentSigners = [{
|
|
6497
|
+
kind: "actor",
|
|
6498
|
+
id: ldSigKey.ownerId,
|
|
6499
|
+
mechanism: "linkedDataSignatures",
|
|
6500
|
+
keyId: ldSigKey.id
|
|
6501
|
+
}];
|
|
6502
|
+
if (await isActorlessCandidate(activity, viewSource)) {
|
|
6503
|
+
let result = null;
|
|
6504
|
+
try {
|
|
6505
|
+
result = await verifyProofs();
|
|
6506
|
+
} catch (error) {
|
|
6507
|
+
if (!isPermanentActivityParseError(error)) throw error;
|
|
6508
|
+
logger.debug("Failed to verify the Object Integrity Proofs of the activity without an actor:\n{error}", {
|
|
6509
|
+
recipient,
|
|
6510
|
+
error
|
|
6511
|
+
});
|
|
6512
|
+
}
|
|
6513
|
+
if (result?.status === "unavailable") throw result.error;
|
|
6514
|
+
if (result?.status === "verified" && result.signers.length > 0) {
|
|
6515
|
+
activity = observation.activity = result.object;
|
|
6516
|
+
documentSigners = toProofPrincipals(result.signers);
|
|
6517
|
+
viewSource = rawViewSource;
|
|
6518
|
+
proofVerified = true;
|
|
6519
|
+
}
|
|
6520
|
+
}
|
|
5687
6521
|
if (!skipSignatureVerification && isPortableActivity(activity)) try {
|
|
5688
6522
|
proofVerified = await verifyObject(Activity, jsonWithoutSig, {
|
|
5689
6523
|
...signatureObservation,
|
|
5690
|
-
contextLoader:
|
|
6524
|
+
contextLoader: standardLoader,
|
|
5691
6525
|
documentLoader: ctx.documentLoader,
|
|
5692
6526
|
keyCache,
|
|
5693
6527
|
meterProvider,
|
|
@@ -5705,18 +6539,11 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5705
6539
|
recipient,
|
|
5706
6540
|
json
|
|
5707
6541
|
});
|
|
6542
|
+
let proofResult;
|
|
5708
6543
|
try {
|
|
5709
|
-
|
|
5710
|
-
...signatureObservation,
|
|
5711
|
-
contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
|
|
5712
|
-
documentLoader: ctx.documentLoader,
|
|
5713
|
-
keyCache,
|
|
5714
|
-
meterProvider,
|
|
5715
|
-
tracerProvider,
|
|
5716
|
-
verifyPortableObject: ctx.verifyPortableObject
|
|
5717
|
-
});
|
|
6544
|
+
proofResult = await verifyProofs();
|
|
5718
6545
|
} catch (error) {
|
|
5719
|
-
if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(jsonWithoutSig,
|
|
6546
|
+
if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(jsonWithoutSig, standardLoader)) return await respondInvalidActivity(error);
|
|
5720
6547
|
if (deferredLdSignatureError != null) {
|
|
5721
6548
|
logger.debug("Object Integrity Proof fallback did not supersede a deferred Linked Data Signature failure:\n{error}", {
|
|
5722
6549
|
recipient,
|
|
@@ -5752,6 +6579,22 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5752
6579
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
5753
6580
|
});
|
|
5754
6581
|
}
|
|
6582
|
+
if (proofResult.status === "verified") if (proofResult.signers.length < 1 && await isActorlessCandidate(proofResult.object, rawViewSource)) activity = null;
|
|
6583
|
+
else {
|
|
6584
|
+
activity = proofResult.object;
|
|
6585
|
+
documentSigners = toProofPrincipals(proofResult.signers);
|
|
6586
|
+
viewSource = rawViewSource;
|
|
6587
|
+
}
|
|
6588
|
+
else if (proofResult.status === "unavailable") {
|
|
6589
|
+
let parsed = null;
|
|
6590
|
+
try {
|
|
6591
|
+
parsed = await Activity.fromJsonLd(jsonWithoutSig, {
|
|
6592
|
+
...ctx,
|
|
6593
|
+
contextLoader: standardLoader
|
|
6594
|
+
});
|
|
6595
|
+
} catch {}
|
|
6596
|
+
if (parsed != null && await isActorlessCandidate(parsed, rawViewSource)) throw proofResult.error;
|
|
6597
|
+
}
|
|
5755
6598
|
if (activity == null) logger.debug("Object Integrity Proofs are not verified.", {
|
|
5756
6599
|
recipient,
|
|
5757
6600
|
activity: json
|
|
@@ -5881,14 +6724,28 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5881
6724
|
try {
|
|
5882
6725
|
activity = observation.activity = await Activity.fromJsonLd(jsonWithoutSig, {
|
|
5883
6726
|
...ctx,
|
|
5884
|
-
contextLoader:
|
|
6727
|
+
contextLoader: standardLoader
|
|
5885
6728
|
});
|
|
5886
6729
|
} catch (error) {
|
|
5887
6730
|
if (!isPermanentActivityParseError(error)) throw error;
|
|
5888
6731
|
return await respondInvalidActivity(error);
|
|
5889
6732
|
}
|
|
6733
|
+
viewSource = rawViewSource;
|
|
5890
6734
|
}
|
|
5891
6735
|
observation.activity = activity;
|
|
6736
|
+
let view;
|
|
6737
|
+
try {
|
|
6738
|
+
view = await buildVerifiedActivityView(snapshot, viewSource ?? rawViewSource);
|
|
6739
|
+
} catch (error) {
|
|
6740
|
+
if (!isPermanentActivityParseError(error)) throw error;
|
|
6741
|
+
return await respondInvalidActivity(error);
|
|
6742
|
+
}
|
|
6743
|
+
const actorless = !skipSignatureVerification && getActorPresence(view.expanded) === "absent" && inboxListeners?.dispatchWithClass(activity)?.options.allowActorless === true;
|
|
6744
|
+
span.setAttribute("activitypub.activity.actorless", actorless);
|
|
6745
|
+
if (actorless && httpSigKey != null) {
|
|
6746
|
+
const principal = getHttpKeyPrincipal(httpSigKey);
|
|
6747
|
+
if (principal != null) requestSigners = [principal];
|
|
6748
|
+
}
|
|
5892
6749
|
observation.stage = "policy";
|
|
5893
6750
|
observation.result = {
|
|
5894
6751
|
disposition: "rejected",
|
|
@@ -5932,7 +6789,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5932
6789
|
const rejection = await observeAttempt(signatureObservation, "objectIntegrity", async (verification) => {
|
|
5933
6790
|
const rejection = await verifyPortableActivityId(activity.id, jsonWithoutSig, {
|
|
5934
6791
|
[verificationObservation]: verification,
|
|
5935
|
-
contextLoader:
|
|
6792
|
+
contextLoader: standardLoader,
|
|
5936
6793
|
documentLoader: ctx.documentLoader,
|
|
5937
6794
|
keyCache,
|
|
5938
6795
|
meterProvider,
|
|
@@ -5970,7 +6827,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5970
6827
|
});
|
|
5971
6828
|
}
|
|
5972
6829
|
}
|
|
5973
|
-
if (httpSigKey != null && !await doesActorOwnKey(activity, httpSigKey, ctx)) {
|
|
6830
|
+
if (httpSigKey != null && !actorless && !await doesActorOwnKey(activity, httpSigKey, ctx)) {
|
|
5974
6831
|
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
5975
6832
|
observation.authentication = {
|
|
5976
6833
|
status: "rejected",
|
|
@@ -5996,6 +6853,66 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5996
6853
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
5997
6854
|
});
|
|
5998
6855
|
}
|
|
6856
|
+
if (httpSigKey != null && !actorless && activity.actorId != null) requestSigners = [{
|
|
6857
|
+
kind: "actor",
|
|
6858
|
+
id: activity.actorId,
|
|
6859
|
+
mechanism: "httpSignatures",
|
|
6860
|
+
keyId: httpSigKey.id
|
|
6861
|
+
}];
|
|
6862
|
+
if (actorless) {
|
|
6863
|
+
const check = checkActorlessActivity(activity, {
|
|
6864
|
+
verified: true,
|
|
6865
|
+
requestSigners,
|
|
6866
|
+
documentSigners,
|
|
6867
|
+
originAuthenticated: [],
|
|
6868
|
+
expandedActivity: [],
|
|
6869
|
+
baseUrl: null
|
|
6870
|
+
});
|
|
6871
|
+
if (check.status === "noAuthenticatedPrincipal") {
|
|
6872
|
+
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
6873
|
+
observation.authentication = {
|
|
6874
|
+
status: "rejected",
|
|
6875
|
+
reason: { type: "noAuthenticatedPrincipal" }
|
|
6876
|
+
};
|
|
6877
|
+
logger.error("The activity {activityId} declares no actor, and no principal is authenticated for it.", {
|
|
6878
|
+
activity: json,
|
|
6879
|
+
recipient,
|
|
6880
|
+
activityId: activity.id?.href
|
|
6881
|
+
});
|
|
6882
|
+
span.setStatus({
|
|
6883
|
+
code: SpanStatusCode.ERROR,
|
|
6884
|
+
message: "No principal is authenticated for the activity."
|
|
6885
|
+
});
|
|
6886
|
+
return new Response("No principal is authenticated for the activity.", {
|
|
6887
|
+
status: 401,
|
|
6888
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
6889
|
+
});
|
|
6890
|
+
} else if (check.status === "activityOriginMismatch") {
|
|
6891
|
+
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
6892
|
+
observation.authentication = {
|
|
6893
|
+
status: "rejected",
|
|
6894
|
+
reason: {
|
|
6895
|
+
type: "activityOriginMismatch",
|
|
6896
|
+
activityId: new URL(activity.id.href),
|
|
6897
|
+
actorIds: check.principals.map((principal) => principal.id)
|
|
6898
|
+
}
|
|
6899
|
+
};
|
|
6900
|
+
logger.error("The activity ({activityId}) does not have the same origin as the principals that authenticated it.", {
|
|
6901
|
+
activity: json,
|
|
6902
|
+
recipient,
|
|
6903
|
+
activityId: activity.id?.href,
|
|
6904
|
+
principals: check.principals.map((principal) => principal.id.href)
|
|
6905
|
+
});
|
|
6906
|
+
span.setStatus({
|
|
6907
|
+
code: SpanStatusCode.ERROR,
|
|
6908
|
+
message: `The activity (${activity.id?.href}) does not have the same origin as the principals that authenticated it.`
|
|
6909
|
+
});
|
|
6910
|
+
return new Response("The activity and its signers do not have the same origin.", {
|
|
6911
|
+
status: 401,
|
|
6912
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
6913
|
+
});
|
|
6914
|
+
}
|
|
6915
|
+
}
|
|
5999
6916
|
if (!skipSignatureVerification) {
|
|
6000
6917
|
const compoundJson = Array.isArray(json) ? json : jsonWithoutSig;
|
|
6001
6918
|
if (inspectCompoundPortableObjectApplicability(compoundJson, INBOX_COMPOUND_PROOF_LIMITS) !== "absent") {
|
|
@@ -6055,7 +6972,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
6055
6972
|
return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
|
|
6056
6973
|
}
|
|
6057
6974
|
}
|
|
6058
|
-
if (!skipSignatureVerification && activity.id != null && !actorsHaveSameOrigin(activity)) {
|
|
6975
|
+
if (!skipSignatureVerification && !actorless && activity.id != null && !actorsHaveSameOrigin(activity)) {
|
|
6059
6976
|
observation.authentication = {
|
|
6060
6977
|
status: "rejected",
|
|
6061
6978
|
reason: {
|
|
@@ -6084,7 +7001,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
6084
7001
|
attempts: observation.verification.attempts.filter((attempt) => attempt.status === "verified")
|
|
6085
7002
|
};
|
|
6086
7003
|
observation.stage = "dispatch";
|
|
6087
|
-
if (activity.actorId == null) {
|
|
7004
|
+
if (!actorless && activity.actorId == null) {
|
|
6088
7005
|
observation.result = {
|
|
6089
7006
|
disposition: "rejected",
|
|
6090
7007
|
reason: "missingActor"
|
|
@@ -6108,6 +7025,17 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
6108
7025
|
originalJson: json,
|
|
6109
7026
|
normalizedActivity: hasLdSignature && compactedJson !== json ? compactedJson : void 0,
|
|
6110
7027
|
ldSignatureVerified: hasLdSignature ? ldSigVerified : void 0,
|
|
7028
|
+
authentication: createAuthenticationEvidence({
|
|
7029
|
+
verified: !skipSignatureVerification,
|
|
7030
|
+
requestSigners,
|
|
7031
|
+
documentSigners,
|
|
7032
|
+
originAuthenticated: [],
|
|
7033
|
+
expandedActivity: deepFreeze(view.expanded),
|
|
7034
|
+
baseUrl: null
|
|
7035
|
+
}),
|
|
7036
|
+
verifiedView: view.view,
|
|
7037
|
+
replayable: view.replayable,
|
|
7038
|
+
actorless,
|
|
6111
7039
|
activity,
|
|
6112
7040
|
recipient,
|
|
6113
7041
|
inboxListeners,
|
|
@@ -6322,8 +7250,8 @@ var CustomCollectionHandler = class {
|
|
|
6322
7250
|
* @param CollectionPage The CollectionPage constructor.
|
|
6323
7251
|
* @param filterPredicate Optional filter predicate for items.
|
|
6324
7252
|
*/
|
|
6325
|
-
constructor(name$
|
|
6326
|
-
this.name = name$
|
|
7253
|
+
constructor(name$2, values, context, callbacks, tracerProvider = trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate, portable) {
|
|
7254
|
+
this.name = name$2;
|
|
6327
7255
|
this.values = values;
|
|
6328
7256
|
this.context = context;
|
|
6329
7257
|
this.callbacks = callbacks;
|
|
@@ -6832,6 +7760,42 @@ const MIN_COMPONENTS = [
|
|
|
6832
7760
|
"@authority"
|
|
6833
7761
|
];
|
|
6834
7762
|
/**
|
|
7763
|
+
* Turns the signers of verified Object Integrity Proofs into principals.
|
|
7764
|
+
*/
|
|
7765
|
+
function toProofPrincipals(signers) {
|
|
7766
|
+
return signers.map((signer) => ({
|
|
7767
|
+
kind: signer.kind,
|
|
7768
|
+
id: signer.id,
|
|
7769
|
+
mechanism: "objectIntegrityProofs",
|
|
7770
|
+
keyId: signer.keyId
|
|
7771
|
+
}));
|
|
7772
|
+
}
|
|
7773
|
+
/**
|
|
7774
|
+
* Gets the principal of an HTTP Signature key, whose owner was confirmed by
|
|
7775
|
+
* the owner's own document, or by the signed document of a portable actor,
|
|
7776
|
+
* when the key was fetched.
|
|
7777
|
+
*/
|
|
7778
|
+
function getHttpKeyPrincipal(key) {
|
|
7779
|
+
const evidence = getVerifiedKeyOwnerEvidence(key);
|
|
7780
|
+
if (evidence != null) try {
|
|
7781
|
+
return {
|
|
7782
|
+
kind: "actor",
|
|
7783
|
+
id: parseIri(evidence.ownerId),
|
|
7784
|
+
mechanism: "httpSignatures",
|
|
7785
|
+
keyId: key.id
|
|
7786
|
+
};
|
|
7787
|
+
} catch {
|
|
7788
|
+
return null;
|
|
7789
|
+
}
|
|
7790
|
+
if (key.ownerId == null || isPortableId(key.ownerId)) return null;
|
|
7791
|
+
return {
|
|
7792
|
+
kind: "actor",
|
|
7793
|
+
id: key.ownerId,
|
|
7794
|
+
mechanism: "httpSignatures",
|
|
7795
|
+
keyId: key.id
|
|
7796
|
+
};
|
|
7797
|
+
}
|
|
7798
|
+
/**
|
|
6835
7799
|
* Checks whether an HTTP Signature key is a key of an FEP-ef61 portable
|
|
6836
7800
|
* actor, i.e., its ID is a compatible identifier or an `ap:` or `ap+ef61:`
|
|
6837
7801
|
* URI, and it is owned by a portable actor, which only the portable actor's
|
|
@@ -9332,24 +10296,102 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
9332
10296
|
let dispatched;
|
|
9333
10297
|
let parseInput = void 0;
|
|
9334
10298
|
let parseContextLoader = context.contextLoader;
|
|
10299
|
+
let authentication = getEmptyAuthenticationEvidence();
|
|
10300
|
+
let actorPresence = "present";
|
|
10301
|
+
if (message.verifiedView !== void 0) {
|
|
10302
|
+
const view = parseSerializedVerifiedActivityView(message.verifiedView);
|
|
10303
|
+
let restored;
|
|
10304
|
+
let restoredAuthentication = null;
|
|
10305
|
+
let failure;
|
|
10306
|
+
if (view == null) failure = /* @__PURE__ */ new TypeError("The queued activity has a malformed verified view.");
|
|
10307
|
+
else try {
|
|
10308
|
+
if (view.source === "normalizedActivity") assertSafeJsonLd(message.normalizedActivity);
|
|
10309
|
+
restored = await restoreVerifiedActivityView(view, message);
|
|
10310
|
+
} catch (error) {
|
|
10311
|
+
if (!(error instanceof ContextSnapshotMissError) && !isPermanentInboxParseError(error)) {
|
|
10312
|
+
await handleRetriableFailure(error);
|
|
10313
|
+
return;
|
|
10314
|
+
}
|
|
10315
|
+
failure = error;
|
|
10316
|
+
}
|
|
10317
|
+
if (restored != null) {
|
|
10318
|
+
const expandedActivity = deepFreeze(restored.expanded);
|
|
10319
|
+
restoredAuthentication = message.authentication === void 0 ? createAuthenticationEvidence({
|
|
10320
|
+
...getEmptyAuthenticationEvidence(),
|
|
10321
|
+
expandedActivity,
|
|
10322
|
+
baseUrl: restored.baseUrl
|
|
10323
|
+
}) : deserializeAuthentication(message.authentication, expandedActivity, restored.baseUrl);
|
|
10324
|
+
if (restoredAuthentication == null) failure = /* @__PURE__ */ new TypeError("The queued activity has malformed authentication evidence.");
|
|
10325
|
+
}
|
|
10326
|
+
if (failure != null || restored == null || restoredAuthentication == null) {
|
|
10327
|
+
await reportInboxError(failure);
|
|
10328
|
+
logger.error("Failed to replay the queued incoming activity:\n{error}", {
|
|
10329
|
+
error: failure,
|
|
10330
|
+
trial: message.attempt,
|
|
10331
|
+
activity: message.activity,
|
|
10332
|
+
recipient: message.identifier
|
|
10333
|
+
});
|
|
10334
|
+
listenerSpan.setStatus({
|
|
10335
|
+
code: SpanStatusCode.ERROR,
|
|
10336
|
+
message: String(failure)
|
|
10337
|
+
});
|
|
10338
|
+
listenerSpan.end();
|
|
10339
|
+
return;
|
|
10340
|
+
}
|
|
10341
|
+
authentication = restoredAuthentication;
|
|
10342
|
+
actorPresence = getActorPresence(restored.expanded);
|
|
10343
|
+
const policy = view.policy;
|
|
10344
|
+
const replayLoader = createReleasableLoader(restored.loader, policy === "normalization" ? getNormalizationContextLoader(context.contextLoader) : wrapContextLoaderForJsonLd(context.contextLoader), policy);
|
|
10345
|
+
parseInput = restored.document;
|
|
10346
|
+
parseContextLoader = restored.loader;
|
|
10347
|
+
try {
|
|
10348
|
+
activity = await Activity.fromJsonLd(restored.document, {
|
|
10349
|
+
...context,
|
|
10350
|
+
contextLoader: replayLoader.loader,
|
|
10351
|
+
...restored.baseUrl == null ? {} : { baseUrl: restored.baseUrl }
|
|
10352
|
+
});
|
|
10353
|
+
} catch (error) {
|
|
10354
|
+
if (isPermanentInboxParseError(error)) {
|
|
10355
|
+
await reportInboxError(error);
|
|
10356
|
+
logger.error("Failed to parse the queued incoming activity:\n{error}", {
|
|
10357
|
+
error,
|
|
10358
|
+
trial: message.attempt,
|
|
10359
|
+
activity: message.activity,
|
|
10360
|
+
recipient: message.identifier
|
|
10361
|
+
});
|
|
10362
|
+
listenerSpan.setStatus({
|
|
10363
|
+
code: SpanStatusCode.ERROR,
|
|
10364
|
+
message: String(error)
|
|
10365
|
+
});
|
|
10366
|
+
listenerSpan.end();
|
|
10367
|
+
return;
|
|
10368
|
+
}
|
|
10369
|
+
await handleRetriableFailure(error);
|
|
10370
|
+
return;
|
|
10371
|
+
} finally {
|
|
10372
|
+
replayLoader.release();
|
|
10373
|
+
}
|
|
10374
|
+
}
|
|
9335
10375
|
try {
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
|
|
9346
|
-
|
|
9347
|
-
|
|
9348
|
-
|
|
9349
|
-
|
|
10376
|
+
if (activity == null) {
|
|
10377
|
+
const hasSignatureField = hasSignature(message.activity);
|
|
10378
|
+
const shouldParseFromNormalizedSignedPayload = message.ldSignatureVerified === true || message.normalizedActivity != null || message.ldSignatureVerified == null && hasSignatureField;
|
|
10379
|
+
const parseContext = hasSignatureField ? {
|
|
10380
|
+
...context,
|
|
10381
|
+
contextLoader: getNormalizationContextLoader(context.contextLoader)
|
|
10382
|
+
} : {
|
|
10383
|
+
...context,
|
|
10384
|
+
contextLoader: wrapContextLoaderForJsonLd(context.contextLoader)
|
|
10385
|
+
};
|
|
10386
|
+
parseContextLoader = parseContext.contextLoader;
|
|
10387
|
+
let normalizedActivity;
|
|
10388
|
+
if (shouldParseFromNormalizedSignedPayload) {
|
|
10389
|
+
normalizedActivity = message.normalizedActivity ?? await compactJsonLd(message.activity, context.contextLoader);
|
|
10390
|
+
assertSafeJsonLd(normalizedActivity);
|
|
10391
|
+
}
|
|
10392
|
+
parseInput = shouldParseFromNormalizedSignedPayload ? detachSignature(normalizedActivity) : hasSignatureField ? detachSignature(message.activity) : message.activity;
|
|
10393
|
+
activity = await Activity.fromJsonLd(parseInput, parseContext);
|
|
9350
10394
|
}
|
|
9351
|
-
parseInput = shouldParseFromNormalizedSignedPayload ? detachSignature(normalizedActivity) : hasSignatureField ? detachSignature(message.activity) : message.activity;
|
|
9352
|
-
activity = await Activity.fromJsonLd(parseInput, parseContext);
|
|
9353
10395
|
activityType = getTypeId(activity).href;
|
|
9354
10396
|
span.setAttribute("activitypub.activity.type", activityType);
|
|
9355
10397
|
listenerSpan.setAttribute("activitypub.activity.type", activityType);
|
|
@@ -9358,7 +10400,8 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
9358
10400
|
span.setAttribute("activitypub.activity.id", activity.id.href);
|
|
9359
10401
|
listenerSpan.setAttribute("activitypub.activity.id", activity.id.href);
|
|
9360
10402
|
}
|
|
9361
|
-
|
|
10403
|
+
const actorless = actorPresence === "absent" && !this.skipSignatureVerification && authentication.verified && this.inboxListeners?.dispatchWithClass(activity)?.options.allowActorless === true && isActorlessActivityAuthenticated(activity, authentication);
|
|
10404
|
+
if (activity.actorId == null && !actorless) {
|
|
9362
10405
|
logger.error("Missing actor in queued incoming activity.", {
|
|
9363
10406
|
activityId: activity.id?.href,
|
|
9364
10407
|
activity: message.activity,
|
|
@@ -9372,7 +10415,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
9372
10415
|
listenerSpan.end();
|
|
9373
10416
|
return;
|
|
9374
10417
|
}
|
|
9375
|
-
if (!this.skipSignatureVerification && activity.id != null && !actorsHaveSameOrigin(activity)) {
|
|
10418
|
+
if (!this.skipSignatureVerification && !actorless && activity.id != null && !actorsHaveSameOrigin(activity)) {
|
|
9376
10419
|
logger.error("The queued activity ({activityId}) and its actors do not have the same origin.", {
|
|
9377
10420
|
activityId: activity.id.href,
|
|
9378
10421
|
actorIds: activity.actorIds.map((actor) => actor.href),
|
|
@@ -9387,22 +10430,18 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
9387
10430
|
listenerSpan.end();
|
|
9388
10431
|
return;
|
|
9389
10432
|
}
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
|
|
9393
|
-
|
|
9394
|
-
|
|
9395
|
-
|
|
9396
|
-
|
|
9397
|
-
|
|
9398
|
-
|
|
9399
|
-
|
|
9400
|
-
|
|
9401
|
-
|
|
9402
|
-
recordInboxActivity(this.meterProvider, "rejected", activityType);
|
|
9403
|
-
listenerSpan.end();
|
|
9404
|
-
return;
|
|
9405
|
-
}
|
|
10433
|
+
const parsedActivity = activity;
|
|
10434
|
+
const parsedActivityType = activityType;
|
|
10435
|
+
cacheKey = await getInboxIdempotencyKey(this.idempotencyStrategy, () => context.toInboxContext(message.identifier, message.activity, parsedActivity.id?.href, parsedActivityType, { authentication }), activity, context.origin, message.identifier, this.kvPrefixes.activityIdempotence);
|
|
10436
|
+
if (await isInboxActivityProcessed(this.kv, cacheKey, activity, context.origin, this.kvPrefixes.activityIdempotence)) {
|
|
10437
|
+
logger.debug("Activity {activityId} has already been processed.", {
|
|
10438
|
+
activityId: activity.id?.href,
|
|
10439
|
+
activity: message.activity,
|
|
10440
|
+
recipient: message.identifier
|
|
10441
|
+
});
|
|
10442
|
+
recordInboxActivity(this.meterProvider, "rejected", activityType);
|
|
10443
|
+
listenerSpan.end();
|
|
10444
|
+
return;
|
|
9406
10445
|
}
|
|
9407
10446
|
dispatched = this.inboxListeners?.dispatchWithClass(activity);
|
|
9408
10447
|
} catch (error) {
|
|
@@ -9461,7 +10500,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
9461
10500
|
try {
|
|
9462
10501
|
const started = performance.now();
|
|
9463
10502
|
try {
|
|
9464
|
-
await listener(context.toInboxContext(message.identifier, message.activity, activity.id?.href, activityType), activity);
|
|
10503
|
+
await listener(context.toInboxContext(message.identifier, message.activity, activity.id?.href, activityType, { authentication }), activity);
|
|
9465
10504
|
} finally {
|
|
9466
10505
|
this.metrics.recordInboxProcessingDuration(activityType, getDurationMs(started));
|
|
9467
10506
|
}
|
|
@@ -9863,7 +10902,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
9863
10902
|
onNotAcceptable ??= notAcceptable;
|
|
9864
10903
|
onUnauthorized ??= unauthorized;
|
|
9865
10904
|
const url = new URL(request.url);
|
|
9866
|
-
const route = this.router.route(url.pathname);
|
|
10905
|
+
const route = this.router.route(url.pathname, { allowUnencodedColon: true });
|
|
9867
10906
|
if (route == null) {
|
|
9868
10907
|
const media = this.hashlinkMediaDispatcher == null ? null : parseHashlinkGatewayRequest(url);
|
|
9869
10908
|
if (media != null) return await this.#fetchHashlinkMedia(request, media, {
|
|
@@ -10768,7 +11807,7 @@ var ContextImpl = class ContextImpl {
|
|
|
10768
11807
|
invokedFromActorKeyPairsDispatcher: this.invokedFromActorKeyPairsDispatcher
|
|
10769
11808
|
});
|
|
10770
11809
|
}
|
|
10771
|
-
toInboxContext(recipient, activity, activityId, activityType) {
|
|
11810
|
+
toInboxContext(recipient, activity, activityId, activityType, metadata) {
|
|
10772
11811
|
return new InboxContextImpl(recipient, activity, activityId, activityType, {
|
|
10773
11812
|
url: this.url,
|
|
10774
11813
|
federation: this.federation,
|
|
@@ -10776,7 +11815,7 @@ var ContextImpl = class ContextImpl {
|
|
|
10776
11815
|
documentLoader: this.documentLoader,
|
|
10777
11816
|
contextLoader: this.contextLoader,
|
|
10778
11817
|
invokedFromActorKeyPairsDispatcher: this.invokedFromActorKeyPairsDispatcher
|
|
10779
|
-
});
|
|
11818
|
+
}, metadata?.authentication);
|
|
10780
11819
|
}
|
|
10781
11820
|
toOutboxContext(identifier, activity, activityId, activityType) {
|
|
10782
11821
|
return new OutboxContextImpl(identifier, activity, activityId, activityType, {
|
|
@@ -11391,12 +12430,20 @@ var ContextImpl = class ContextImpl {
|
|
|
11391
12430
|
});
|
|
11392
12431
|
}
|
|
11393
12432
|
async routeActivityInternal(recipient, activity, options = {}, span) {
|
|
12433
|
+
const snapshot = createContextSnapshot(options.contextLoader ?? this.contextLoader);
|
|
12434
|
+
try {
|
|
12435
|
+
return await this.#routeActivityWithSnapshot(recipient, activity, options, span, snapshot);
|
|
12436
|
+
} finally {
|
|
12437
|
+
snapshot.release();
|
|
12438
|
+
}
|
|
12439
|
+
}
|
|
12440
|
+
async #routeActivityWithSnapshot(recipient, activity, options, span, snapshot) {
|
|
11394
12441
|
const logger = getLogger([
|
|
11395
12442
|
"fedify",
|
|
11396
12443
|
"federation",
|
|
11397
12444
|
"inbox"
|
|
11398
12445
|
]);
|
|
11399
|
-
const contextLoader =
|
|
12446
|
+
const contextLoader = snapshot.loader("standard");
|
|
11400
12447
|
let json = await activity.toJsonLd({ contextLoader });
|
|
11401
12448
|
const keyCache = new KvKeyCache(this.federation.kv, this.federation.kvPrefixes.publicKey, {
|
|
11402
12449
|
documentLoader: this.documentLoader,
|
|
@@ -11404,13 +12451,19 @@ var ContextImpl = class ContextImpl {
|
|
|
11404
12451
|
tracerProvider: this.tracerProvider,
|
|
11405
12452
|
keyTtl: this.federation.publicKeyTtl
|
|
11406
12453
|
});
|
|
11407
|
-
|
|
12454
|
+
let documentSigners = [];
|
|
12455
|
+
let originAuthenticated = [];
|
|
12456
|
+
let viewSource;
|
|
12457
|
+
let fetchedView;
|
|
12458
|
+
const document = detachSignature(json);
|
|
12459
|
+
const verified = await verifyObjectDetailed(Activity, document, {
|
|
11408
12460
|
contextLoader,
|
|
11409
12461
|
documentLoader: options.documentLoader ?? this.documentLoader,
|
|
11410
12462
|
meterProvider: this.meterProvider,
|
|
11411
12463
|
tracerProvider: options.tracerProvider ?? this.tracerProvider,
|
|
11412
12464
|
keyCache
|
|
11413
|
-
})
|
|
12465
|
+
});
|
|
12466
|
+
if (verified.status !== "verified") {
|
|
11414
12467
|
logger.debug("Object Integrity Proofs are not verified.", {
|
|
11415
12468
|
recipient,
|
|
11416
12469
|
activity: json
|
|
@@ -11422,8 +12475,29 @@ var ContextImpl = class ContextImpl {
|
|
|
11422
12475
|
});
|
|
11423
12476
|
return false;
|
|
11424
12477
|
}
|
|
11425
|
-
|
|
11426
|
-
|
|
12478
|
+
let candidate;
|
|
12479
|
+
let candidateView;
|
|
12480
|
+
const verifications = /* @__PURE__ */ new WeakMap();
|
|
12481
|
+
const fetched = await this.lookupObject(activity.id, {
|
|
12482
|
+
...options,
|
|
12483
|
+
contextLoader,
|
|
12484
|
+
verifyPortableObject: async (document, verifierOptions) => {
|
|
12485
|
+
const result = await this.verifyPortableObject(document, verifierOptions);
|
|
12486
|
+
if (document != null && typeof document === "object") verifications.set(document, result);
|
|
12487
|
+
return result;
|
|
12488
|
+
},
|
|
12489
|
+
[lookupObserver]: { accept: async (accepted) => {
|
|
12490
|
+
candidate = accepted;
|
|
12491
|
+
candidateView = await buildVerifiedActivityView(createContextSnapshot(accepted.contextLoader), {
|
|
12492
|
+
policy: "standard",
|
|
12493
|
+
source: "activity",
|
|
12494
|
+
document: detachSignature(accepted.document),
|
|
12495
|
+
baseUrl: accepted.baseUrl
|
|
12496
|
+
});
|
|
12497
|
+
return true;
|
|
12498
|
+
} }
|
|
12499
|
+
});
|
|
12500
|
+
if (fetched == null || candidate == null || candidateView == null) {
|
|
11427
12501
|
logger.debug("Failed to fetch the remote activity object {activityId}.", {
|
|
11428
12502
|
recipient,
|
|
11429
12503
|
activity: json,
|
|
@@ -11459,11 +12533,50 @@ var ContextImpl = class ContextImpl {
|
|
|
11459
12533
|
}
|
|
11460
12534
|
logger.debug("Successfully fetched the remote activity object {activityId}; ignore the original activity and use the fetched one, which is trustworthy.");
|
|
11461
12535
|
activity = fetched;
|
|
11462
|
-
json =
|
|
11463
|
-
|
|
11464
|
-
|
|
11465
|
-
|
|
11466
|
-
|
|
12536
|
+
json = candidate.document;
|
|
12537
|
+
fetchedView = candidateView;
|
|
12538
|
+
viewSource = {
|
|
12539
|
+
policy: "standard",
|
|
12540
|
+
source: "activity",
|
|
12541
|
+
document: detachSignature(candidate.document),
|
|
12542
|
+
baseUrl: candidate.baseUrl
|
|
12543
|
+
};
|
|
12544
|
+
if (isPortableId(fetched.id)) {
|
|
12545
|
+
const result = verifications.get(candidate.document);
|
|
12546
|
+
if (result?.verified === true && result.method === "proof") documentSigners = result.keys.flatMap((key) => {
|
|
12547
|
+
const signer = getProofSigner(key);
|
|
12548
|
+
return signer == null ? [] : [{
|
|
12549
|
+
kind: signer.kind,
|
|
12550
|
+
id: signer.id,
|
|
12551
|
+
mechanism: "objectIntegrityProofs",
|
|
12552
|
+
keyId: signer.keyId
|
|
12553
|
+
}];
|
|
12554
|
+
});
|
|
12555
|
+
} else originAuthenticated = fetched.actorIds.map((actorId) => ({
|
|
12556
|
+
kind: "actor",
|
|
12557
|
+
id: actorId,
|
|
12558
|
+
mechanism: "origin",
|
|
12559
|
+
keyId: null
|
|
12560
|
+
}));
|
|
12561
|
+
} else {
|
|
12562
|
+
logger.debug("Object Integrity Proofs are verified.", {
|
|
12563
|
+
recipient,
|
|
12564
|
+
activity: json
|
|
12565
|
+
});
|
|
12566
|
+
activity = verified.object;
|
|
12567
|
+
documentSigners = verified.signers.map((signer) => ({
|
|
12568
|
+
kind: signer.kind,
|
|
12569
|
+
id: signer.id,
|
|
12570
|
+
mechanism: "objectIntegrityProofs",
|
|
12571
|
+
keyId: signer.keyId
|
|
12572
|
+
}));
|
|
12573
|
+
viewSource = {
|
|
12574
|
+
policy: "standard",
|
|
12575
|
+
source: "activity",
|
|
12576
|
+
document,
|
|
12577
|
+
baseUrl: null
|
|
12578
|
+
};
|
|
12579
|
+
}
|
|
11467
12580
|
if (activity.actorId == null) {
|
|
11468
12581
|
logger.debug("Activity is missing an actor; unable to verify.", {
|
|
11469
12582
|
recipient,
|
|
@@ -11478,10 +12591,21 @@ var ContextImpl = class ContextImpl {
|
|
|
11478
12591
|
});
|
|
11479
12592
|
return false;
|
|
11480
12593
|
}
|
|
12594
|
+
const view = fetchedView ?? await buildVerifiedActivityView(snapshot, viewSource);
|
|
11481
12595
|
const routeResult = await routeActivity({
|
|
11482
12596
|
context: this,
|
|
11483
12597
|
json,
|
|
11484
12598
|
ldSignatureVerified: false,
|
|
12599
|
+
authentication: createAuthenticationEvidence({
|
|
12600
|
+
verified: true,
|
|
12601
|
+
requestSigners: [],
|
|
12602
|
+
documentSigners,
|
|
12603
|
+
originAuthenticated,
|
|
12604
|
+
expandedActivity: deepFreeze(view.expanded),
|
|
12605
|
+
baseUrl: viewSource.baseUrl
|
|
12606
|
+
}),
|
|
12607
|
+
verifiedView: view.view,
|
|
12608
|
+
replayable: view.replayable,
|
|
11485
12609
|
activity,
|
|
11486
12610
|
recipient,
|
|
11487
12611
|
inboxListeners: this.federation.inboxListeners,
|
|
@@ -11597,52 +12721,92 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
|
|
|
11597
12721
|
if (object instanceof Tombstone && !(object instanceof cls) && options?.tombstone !== "passthrough") return null;
|
|
11598
12722
|
return object;
|
|
11599
12723
|
}
|
|
11600
|
-
#signedKey =
|
|
12724
|
+
#signedKey = null;
|
|
12725
|
+
#signedKeyOwner = null;
|
|
12726
|
+
#signatureChecks = /* @__PURE__ */ new WeakMap();
|
|
12727
|
+
#getSignatureCheck(documentLoader, contextLoader) {
|
|
12728
|
+
let checks = this.#signatureChecks.get(documentLoader);
|
|
12729
|
+
if (checks == null) {
|
|
12730
|
+
checks = /* @__PURE__ */ new WeakMap();
|
|
12731
|
+
this.#signatureChecks.set(documentLoader, checks);
|
|
12732
|
+
}
|
|
12733
|
+
let check = checks.get(contextLoader);
|
|
12734
|
+
if (check == null) {
|
|
12735
|
+
check = {};
|
|
12736
|
+
checks.set(contextLoader, check);
|
|
12737
|
+
}
|
|
12738
|
+
return check;
|
|
12739
|
+
}
|
|
11601
12740
|
async getSignedKey(options = {}) {
|
|
11602
12741
|
if (this.#signedKey != null) return this.#signedKey;
|
|
11603
|
-
const
|
|
11604
|
-
|
|
11605
|
-
|
|
12742
|
+
const documentLoader = options.documentLoader ?? this.documentLoader;
|
|
12743
|
+
const contextLoader = options.contextLoader ?? this.contextLoader;
|
|
12744
|
+
const check = this.#getSignatureCheck(documentLoader, contextLoader);
|
|
12745
|
+
if (check.key != null) return check.key;
|
|
12746
|
+
const keyCache = documentLoader === this.documentLoader && contextLoader === this.contextLoader ? new KvKeyCache(this.federation.kv, this.federation.kvPrefixes.publicKey, {
|
|
12747
|
+
documentLoader,
|
|
12748
|
+
contextLoader,
|
|
11606
12749
|
tracerProvider: options.tracerProvider ?? this.tracerProvider,
|
|
11607
12750
|
keyTtl: this.federation.publicKeyTtl
|
|
11608
|
-
});
|
|
11609
|
-
|
|
12751
|
+
}) : void 0;
|
|
12752
|
+
const promise = verifyRequest(this.request, {
|
|
11610
12753
|
...this,
|
|
11611
|
-
contextLoader
|
|
11612
|
-
documentLoader
|
|
12754
|
+
contextLoader,
|
|
12755
|
+
documentLoader,
|
|
11613
12756
|
timeWindow: this.federation.signatureTimeWindow,
|
|
11614
12757
|
maxSignatures: this.federation.maxHttpSignatures,
|
|
12758
|
+
keyCache,
|
|
11615
12759
|
meterProvider: this.meterProvider,
|
|
11616
|
-
tracerProvider: options.tracerProvider ?? this.tracerProvider
|
|
11617
|
-
|
|
12760
|
+
tracerProvider: options.tracerProvider ?? this.tracerProvider
|
|
12761
|
+
}).then((key) => {
|
|
12762
|
+
if (key != null) this.#signedKey ??= key;
|
|
12763
|
+
return key;
|
|
12764
|
+
}).catch((error) => {
|
|
12765
|
+
if (check.key === promise) check.key = void 0;
|
|
12766
|
+
throw error;
|
|
11618
12767
|
});
|
|
12768
|
+
check.key = promise;
|
|
12769
|
+
return await promise;
|
|
11619
12770
|
}
|
|
11620
|
-
#signedKeyOwner = void 0;
|
|
11621
12771
|
async getSignedKeyOwner(options = {}) {
|
|
11622
12772
|
if (this.#signedKeyOwner != null) return this.#signedKeyOwner;
|
|
11623
|
-
const
|
|
11624
|
-
|
|
11625
|
-
|
|
11626
|
-
|
|
11627
|
-
|
|
11628
|
-
|
|
11629
|
-
|
|
11630
|
-
|
|
11631
|
-
|
|
11632
|
-
|
|
11633
|
-
|
|
11634
|
-
|
|
11635
|
-
|
|
11636
|
-
|
|
11637
|
-
|
|
11638
|
-
|
|
11639
|
-
|
|
11640
|
-
|
|
11641
|
-
|
|
11642
|
-
|
|
12773
|
+
const normalizedOptions = {
|
|
12774
|
+
documentLoader: options.documentLoader ?? this.documentLoader,
|
|
12775
|
+
contextLoader: options.contextLoader ?? this.contextLoader,
|
|
12776
|
+
tracerProvider: options.tracerProvider ?? this.tracerProvider
|
|
12777
|
+
};
|
|
12778
|
+
const check = this.#getSignatureCheck(normalizedOptions.documentLoader, normalizedOptions.contextLoader);
|
|
12779
|
+
if (check.owner != null) return check.owner;
|
|
12780
|
+
const promise = this.getSignedKey(normalizedOptions).then(async (key) => {
|
|
12781
|
+
if (key == null) {
|
|
12782
|
+
if (check.owner === promise) check.owner = void 0;
|
|
12783
|
+
return null;
|
|
12784
|
+
}
|
|
12785
|
+
try {
|
|
12786
|
+
const owner = await getKeyOwner(key, normalizedOptions);
|
|
12787
|
+
if (owner != null) this.#signedKeyOwner ??= owner;
|
|
12788
|
+
return owner;
|
|
12789
|
+
} catch (error) {
|
|
12790
|
+
if (error instanceof FetchError) {
|
|
12791
|
+
getLogger([
|
|
12792
|
+
"fedify",
|
|
12793
|
+
"federation",
|
|
12794
|
+
"actor"
|
|
12795
|
+
]).warn("Failed to fetch the key owner {keyOwner} of {keyId} while verifying the request signature; treating the request as unauthenticated: {error}", {
|
|
12796
|
+
keyId: key.id?.href,
|
|
12797
|
+
keyOwner: error.url.href,
|
|
12798
|
+
error
|
|
12799
|
+
});
|
|
12800
|
+
return null;
|
|
12801
|
+
}
|
|
12802
|
+
throw error;
|
|
11643
12803
|
}
|
|
12804
|
+
}).catch((error) => {
|
|
12805
|
+
if (check.owner === promise) check.owner = void 0;
|
|
11644
12806
|
throw error;
|
|
11645
|
-
}
|
|
12807
|
+
});
|
|
12808
|
+
check.owner = promise;
|
|
12809
|
+
return await promise;
|
|
11646
12810
|
}
|
|
11647
12811
|
async isSignedByAudience(object, options = {}) {
|
|
11648
12812
|
return await isInAudience(object, null) || await isInAudience(object, await this.getSignedKeyOwner(options), options);
|
|
@@ -11832,12 +12996,14 @@ var InboxContextImpl = class InboxContextImpl extends ContextImpl {
|
|
|
11832
12996
|
activity;
|
|
11833
12997
|
activityId;
|
|
11834
12998
|
activityType;
|
|
11835
|
-
|
|
12999
|
+
authentication;
|
|
13000
|
+
constructor(recipient, activity, activityId, activityType, options, authentication) {
|
|
11836
13001
|
super(options);
|
|
11837
13002
|
this.recipient = recipient;
|
|
11838
13003
|
this.activity = activity;
|
|
11839
13004
|
this.activityId = activityId;
|
|
11840
13005
|
this.activityType = activityType;
|
|
13006
|
+
this.authentication = authentication ?? getEmptyAuthenticationEvidence();
|
|
11841
13007
|
}
|
|
11842
13008
|
clone(data) {
|
|
11843
13009
|
return new InboxContextImpl(this.recipient, this.activity, this.activityId, this.activityType, {
|
|
@@ -11847,7 +13013,7 @@ var InboxContextImpl = class InboxContextImpl extends ContextImpl {
|
|
|
11847
13013
|
documentLoader: this.documentLoader,
|
|
11848
13014
|
contextLoader: this.contextLoader,
|
|
11849
13015
|
invokedFromActorKeyPairsDispatcher: this.invokedFromActorKeyPairsDispatcher
|
|
11850
|
-
});
|
|
13016
|
+
}, this.authentication);
|
|
11851
13017
|
}
|
|
11852
13018
|
forwardActivity(forwarder, recipients, options) {
|
|
11853
13019
|
return forwardActivity(this, "inbox", forwarder, recipients, options).then(() => void 0);
|
|
@@ -11963,4 +13129,4 @@ function getRequestId(request) {
|
|
|
11963
13129
|
return `req_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
11964
13130
|
}
|
|
11965
13131
|
//#endregion
|
|
11966
|
-
export { respondWithObjectIfAcceptable as a,
|
|
13132
|
+
export { respondWithObjectIfAcceptable as a, buildCollectionSynchronizationHeader as c, normalizeCircuitBreakerOptions as d, parseCircuitBreakerKvState as f, respondWithObject as i, digest as l, handleWebFinger as n, SendActivityError as o, createFederationBuilder as p, createExponentialBackoffPolicy as r, isActorAuthenticated as s, createFederation as t, CircuitBreaker as u };
|