@fedify/fedify 2.5.0-dev.2440 → 2.5.0-dev.2448
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{activity-listener-C-BSbKI7.mjs → activity-listener-CELFAodc.mjs} +8 -4
- package/dist/{builder-CTasaZgj.mjs → builder-DbC8bn6k.mjs} +5 -5
- 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-D5r-dQMr.mjs → compound-proof-heeXW2_A.mjs} +2 -2
- package/dist/{context-DPocjp1l.d.cts → context-BazwQrHn.d.cts} +307 -5
- package/dist/{context-8Tu9kea0.mjs → context-COHd1Rsh.mjs} +2 -0
- package/dist/{context-2wLkQrjb.d.ts → context-GlHrzdSP.d.ts} +307 -5
- package/dist/{deno-DR1uYWpm.mjs → deno-Dpewt_jJ.mjs} +1 -1
- package/dist/{docloader-BpCJ2dHm.mjs → docloader-CpsDyqVN.mjs} +2 -2
- package/dist/federation/builder.test.mjs +1 -1
- package/dist/federation/compound-fanout.test.mjs +3 -3
- package/dist/federation/handler-compound-proof.test.mjs +5 -5
- package/dist/federation/handler.test.mjs +21 -6
- 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 +24 -9
- package/dist/federation/mod.cjs +2 -1
- package/dist/federation/mod.d.cts +3 -3
- package/dist/federation/mod.d.ts +3 -3
- package/dist/federation/mod.js +8 -2
- package/dist/federation/mq.test.mjs +36 -1
- package/dist/federation/outgoing-proof.test.mjs +4 -4
- 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 +6 -6
- 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 +3 -3
- 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-eYRUxkUW.cjs → federation-D8YexkP1.cjs} +1174 -132
- package/dist/{http-CgaoFGXV.js → http-BBr-VLil.js} +143 -16
- package/dist/{http-CRrg14tX.mjs → http-BZkyQIkb.mjs} +4 -4
- package/dist/{http-CmFHJm6o.cjs → http-CAl7Num5.cjs} +161 -16
- package/dist/inbox-authentication-DflPYlLL.mjs +171 -0
- package/dist/inbox-view-NE9wXdLo.mjs +385 -0
- package/dist/inbox-wDuMKaFW.mjs +312 -0
- package/dist/{kv-cache-MxJrD9gD.mjs → kv-cache-CTQSfkmp.mjs} +1 -1
- package/dist/{kv-cache-DvVRGOMw.cjs → kv-cache-CjgCOz3i.cjs} +1 -1
- package/dist/{kv-cache-DeagaIIB.js → kv-cache-U7uz5nx4.js} +1 -1
- package/dist/{ld-CUM16-EE.mjs → ld-CQFuiah9.mjs} +52 -9
- package/dist/ld-CcuZ4NZa.d.ts +250 -0
- package/dist/ld-Cel1RRZ5.d.cts +250 -0
- package/dist/{metrics-DIM8ZCKq.mjs → metrics-B-QxRSSR.mjs} +1 -1
- package/dist/{middleware-Bx_QxJ0s.mjs → middleware-BdlNa8BD.mjs} +1 -1
- package/dist/{middleware-CDdFG6xf.mjs → middleware-CuVa0UuS.mjs} +461 -99
- package/dist/{middleware-CGfFooYv.js → middleware-P-AZEQMn.js} +1171 -142
- package/dist/mod-B2783_3v.d.cts +154 -0
- package/dist/mod-BHQE-7WQ.d.ts +154 -0
- package/dist/{mod-Dm-7RdhR.d.ts → mod-DjZfJj9R.d.ts} +1 -1
- package/dist/{mod-DKGoPoQC.d.cts → mod-DvWOv2Ho.d.cts} +1 -1
- package/dist/mod.cjs +5 -4
- package/dist/mod.d.cts +6 -6
- package/dist/mod.d.ts +6 -6
- package/dist/mod.js +5 -5
- package/dist/{mq-Dm1Wmtm-.mjs → mq-eF96QxUT.mjs} +6 -0
- package/dist/nodeinfo/handler.test.mjs +2 -2
- package/dist/{owner-DoQDS_g0.mjs → owner-ccKkQOAf.mjs} +3 -3
- package/dist/{portable-collection-DiBqtGSM.mjs → portable-collection-CndnpXfo.mjs} +2 -2
- package/dist/{portable-collection-BVKSWfBe.js → portable-collection-DUlQ3s2M.js} +1 -1
- package/dist/{portable-collection-C-etRRF0.cjs → portable-collection-TC-kvkVI.cjs} +1 -1
- package/dist/{portable-key-DSRuCUpF.mjs → portable-key-D5tFOoV-.mjs} +1 -1
- package/dist/{proof-DWpruPWs.mjs → proof-DPoF06nz.mjs} +98 -10
- package/dist/{send-H0z9Ey5l.mjs → send-CF44O1Df.mjs} +3 -3
- 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 +2 -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 +2 -2
- package/dist/sig/mod.d.ts +2 -2
- 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 +44 -4
- 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-BiSBCImA.mjs → temporal-XlZ4Wb94.mjs} +1 -1
- package/dist/testing/kv.test.mjs +1 -1
- package/dist/testing/mod.d.mts +265 -3
- package/dist/testing/mod.mjs +2 -2
- package/dist/{testing-t8xDeJZ5.mjs → testing-Dbr21vZk.mjs} +1 -1
- package/dist/utils/docloader.test.mjs +2 -2
- package/dist/utils/kv-cache.test.mjs +1 -1
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.js +1 -1
- package/package.json +7 -7
- package/dist/inbox-BwPoSUEv.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,19 +2,20 @@ 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
|
|
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 doubleKnock, 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-BBr-VLil.js";
|
|
6
6
|
import { a as MAX_BODY_SIZE, i as BodyTooLargeError, n as getNodeInfo, o as readBoundedText, t as nodeInfoToJson } from "./types-_S2_MeLF.js";
|
|
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-
|
|
7
|
+
import { n as doesActorOwnKey, r as getKeyOwner, t as verifyPortableObject } from "./portable-collection-DUlQ3s2M.js";
|
|
8
|
+
import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-U7uz5nx4.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
13
|
import { FetchError, UrlError, canonicalizePortableUri, decodeMultibase, formatIri, fromCompatibleEf61Id, getDocumentLoader, getFe34Origin, isGatewayUrl, parseDigestMultibase, parseHashlink, parseIri, preloadedContexts, resolveDocumentLoaderTimeout, toCompatibleEf61Id, validatePublicUrl } 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";
|
|
19
20
|
import { lookupWebFinger } from "@fedify/webfinger";
|
|
20
21
|
import { DataPointType, MeterProvider as MeterProvider$1, MetricReader } from "@opentelemetry/sdk-metrics";
|
|
@@ -32,9 +33,12 @@ var ActivityListenerSet = class {
|
|
|
32
33
|
clone.#listeners = new Map(this.#listeners);
|
|
33
34
|
return clone;
|
|
34
35
|
}
|
|
35
|
-
add(type, listener) {
|
|
36
|
+
add(type, listener, options = {}) {
|
|
36
37
|
if (this.#listeners.has(type)) throw new TypeError("Listener already set for this type.");
|
|
37
|
-
this.#listeners.set(type,
|
|
38
|
+
this.#listeners.set(type, {
|
|
39
|
+
listener,
|
|
40
|
+
options: { ...options }
|
|
41
|
+
});
|
|
38
42
|
}
|
|
39
43
|
dispatchWithClass(activity) {
|
|
40
44
|
let cls = activity.constructor;
|
|
@@ -44,10 +48,11 @@ var ActivityListenerSet = class {
|
|
|
44
48
|
cls = globalThis.Object.getPrototypeOf(cls);
|
|
45
49
|
}
|
|
46
50
|
if (cls == null) return null;
|
|
47
|
-
const listener = this.#listeners.get(cls);
|
|
51
|
+
const { listener, options } = this.#listeners.get(cls);
|
|
48
52
|
return {
|
|
49
53
|
class: cls,
|
|
50
|
-
listener
|
|
54
|
+
listener,
|
|
55
|
+
options
|
|
51
56
|
};
|
|
52
57
|
}
|
|
53
58
|
dispatch(activity) {
|
|
@@ -635,8 +640,8 @@ var FederationBuilderImpl = class {
|
|
|
635
640
|
}
|
|
636
641
|
const listeners = this.inboxListeners = new ActivityListenerSet();
|
|
637
642
|
const setters = {
|
|
638
|
-
on(type, listener) {
|
|
639
|
-
listeners.add(type, listener);
|
|
643
|
+
on(type, listener, options) {
|
|
644
|
+
listeners.add(type, listener, options);
|
|
640
645
|
return setters;
|
|
641
646
|
},
|
|
642
647
|
onError: (handler) => {
|
|
@@ -2804,6 +2809,172 @@ async function isInAudience(object, actor, options = {}) {
|
|
|
2804
2809
|
return false;
|
|
2805
2810
|
}
|
|
2806
2811
|
//#endregion
|
|
2812
|
+
//#region src/federation/inbox-authentication.ts
|
|
2813
|
+
/**
|
|
2814
|
+
* The evidence of an activity that has not been authenticated by Fedify.
|
|
2815
|
+
*/
|
|
2816
|
+
const EMPTY_EVIDENCE = Object.freeze({
|
|
2817
|
+
verified: false,
|
|
2818
|
+
requestSigners: Object.freeze([]),
|
|
2819
|
+
documentSigners: Object.freeze([]),
|
|
2820
|
+
originAuthenticated: Object.freeze([]),
|
|
2821
|
+
expandedActivity: Object.freeze([]),
|
|
2822
|
+
baseUrl: null
|
|
2823
|
+
});
|
|
2824
|
+
/**
|
|
2825
|
+
* Gets the evidence of an activity that has not been authenticated.
|
|
2826
|
+
* @internal
|
|
2827
|
+
*/
|
|
2828
|
+
function getEmptyAuthenticationEvidence() {
|
|
2829
|
+
return EMPTY_EVIDENCE;
|
|
2830
|
+
}
|
|
2831
|
+
/**
|
|
2832
|
+
* Gets the principals that an authorization decision should rely on.
|
|
2833
|
+
* @param evidence The evidence.
|
|
2834
|
+
* @param scope See {@link IsActorAuthenticatedOptions.scope}.
|
|
2835
|
+
* @returns The principals.
|
|
2836
|
+
* @internal
|
|
2837
|
+
*/
|
|
2838
|
+
function getEffectivePrincipals(evidence, scope = "effective") {
|
|
2839
|
+
if (scope === "document" || evidence.documentSigners.length > 0) return evidence.documentSigners;
|
|
2840
|
+
if (evidence.requestSigners.length > 0) return evidence.requestSigners;
|
|
2841
|
+
return evidence.originAuthenticated;
|
|
2842
|
+
}
|
|
2843
|
+
/**
|
|
2844
|
+
* Normalizes an actor ID for comparison: portable IDs and compatible
|
|
2845
|
+
* identifiers to their canonical portable ID, other IDs to their `href`.
|
|
2846
|
+
* @returns The normalized ID, or `null` for a malformed portable ID.
|
|
2847
|
+
*/
|
|
2848
|
+
function normalizeActorId(id) {
|
|
2849
|
+
return isPortableId(id) ? getCanonicalPortableId(id) : id.href;
|
|
2850
|
+
}
|
|
2851
|
+
/**
|
|
2852
|
+
* Checks whether the given principal authenticates the given actor.
|
|
2853
|
+
* @param principal The principal.
|
|
2854
|
+
* @param actorId The actor ID.
|
|
2855
|
+
* @returns `true` if the principal authenticates the actor.
|
|
2856
|
+
* @internal
|
|
2857
|
+
*/
|
|
2858
|
+
function principalAuthenticates(principal, actorId) {
|
|
2859
|
+
if (principal.kind === "portableAuthority") {
|
|
2860
|
+
if (!isPortableId(actorId)) return false;
|
|
2861
|
+
const did = getPortableDid(actorId);
|
|
2862
|
+
return did != null && did === getPortableDid(principal.id);
|
|
2863
|
+
}
|
|
2864
|
+
const expected = normalizeActorId(actorId);
|
|
2865
|
+
return expected != null && normalizeActorId(principal.id) === expected;
|
|
2866
|
+
}
|
|
2867
|
+
/**
|
|
2868
|
+
* Checks whether the given evidence authenticates the given actor.
|
|
2869
|
+
*
|
|
2870
|
+
* Actor IDs are compared exactly, except that [FEP-ef61] portable IDs and
|
|
2871
|
+
* compatible identifiers are compared by their canonical portable IDs, and
|
|
2872
|
+
* a portable authority (a DID) authenticates the portable actors of that DID.
|
|
2873
|
+
*
|
|
2874
|
+
* @example
|
|
2875
|
+
* ``` typescript
|
|
2876
|
+
* federation
|
|
2877
|
+
* .setInboxListeners("/users/{identifier}/inbox", "/inbox")
|
|
2878
|
+
* .on(Follow, async (ctx, follow) => {
|
|
2879
|
+
* if (follow.actorId == null) return;
|
|
2880
|
+
* // The actor is always authenticated for activities that declare one,
|
|
2881
|
+
* // but the same check works for any actor an activity names:
|
|
2882
|
+
* isActorAuthenticated(ctx.authentication, follow.actorId);
|
|
2883
|
+
* });
|
|
2884
|
+
* ```
|
|
2885
|
+
*
|
|
2886
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
2887
|
+
* @param evidence The authentication evidence, usually
|
|
2888
|
+
* {@link InboxContext.authentication}.
|
|
2889
|
+
* @param actorId The actor ID to check.
|
|
2890
|
+
* @param options Options.
|
|
2891
|
+
* @returns `true` if the actor is authenticated.
|
|
2892
|
+
* @since 2.5.0
|
|
2893
|
+
*/
|
|
2894
|
+
function isActorAuthenticated(evidence, actorId, options = {}) {
|
|
2895
|
+
return getEffectivePrincipals(evidence, options.scope).some((principal) => principalAuthenticates(principal, actorId));
|
|
2896
|
+
}
|
|
2897
|
+
function serializePrincipal(principal) {
|
|
2898
|
+
return {
|
|
2899
|
+
kind: principal.kind,
|
|
2900
|
+
id: principal.id.href,
|
|
2901
|
+
mechanism: principal.mechanism,
|
|
2902
|
+
keyId: principal.keyId?.href ?? null
|
|
2903
|
+
};
|
|
2904
|
+
}
|
|
2905
|
+
/**
|
|
2906
|
+
* Serializes the principals of the given evidence for a queued message.
|
|
2907
|
+
* @internal
|
|
2908
|
+
*/
|
|
2909
|
+
function serializeAuthentication(evidence) {
|
|
2910
|
+
return {
|
|
2911
|
+
verified: evidence.verified,
|
|
2912
|
+
requestSigners: evidence.requestSigners.map(serializePrincipal),
|
|
2913
|
+
documentSigners: evidence.documentSigners.map(serializePrincipal),
|
|
2914
|
+
originAuthenticated: evidence.originAuthenticated.map(serializePrincipal)
|
|
2915
|
+
};
|
|
2916
|
+
}
|
|
2917
|
+
const KINDS = /* @__PURE__ */ new Set(["actor", "portableAuthority"]);
|
|
2918
|
+
const MECHANISMS = /* @__PURE__ */ new Set([
|
|
2919
|
+
"httpSignatures",
|
|
2920
|
+
"linkedDataSignatures",
|
|
2921
|
+
"objectIntegrityProofs",
|
|
2922
|
+
"origin"
|
|
2923
|
+
]);
|
|
2924
|
+
function deserializePrincipals(value) {
|
|
2925
|
+
if (!Array.isArray(value)) return null;
|
|
2926
|
+
const principals = [];
|
|
2927
|
+
for (const item of value) {
|
|
2928
|
+
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;
|
|
2929
|
+
principals.push(Object.freeze({
|
|
2930
|
+
kind: item.kind,
|
|
2931
|
+
id: new URL(item.id),
|
|
2932
|
+
mechanism: item.mechanism,
|
|
2933
|
+
keyId: item.keyId == null ? null : new URL(item.keyId)
|
|
2934
|
+
}));
|
|
2935
|
+
}
|
|
2936
|
+
return principals;
|
|
2937
|
+
}
|
|
2938
|
+
/**
|
|
2939
|
+
* Restores the evidence of a queued message.
|
|
2940
|
+
* @param value The serialized principals.
|
|
2941
|
+
* @param expandedActivity The expanded activity, restored from the queued
|
|
2942
|
+
* verified view.
|
|
2943
|
+
* @param baseUrl The base URL, restored from the queued verified view.
|
|
2944
|
+
* @returns The evidence, or `null` if the serialized value is malformed.
|
|
2945
|
+
* @internal
|
|
2946
|
+
*/
|
|
2947
|
+
function deserializeAuthentication(value, expandedActivity, baseUrl) {
|
|
2948
|
+
if (value == null || typeof value !== "object" || typeof value.verified !== "boolean") return null;
|
|
2949
|
+
const record = value;
|
|
2950
|
+
const requestSigners = deserializePrincipals(record.requestSigners);
|
|
2951
|
+
const documentSigners = deserializePrincipals(record.documentSigners);
|
|
2952
|
+
const originAuthenticated = deserializePrincipals(record.originAuthenticated);
|
|
2953
|
+
if (requestSigners == null || documentSigners == null || originAuthenticated == null) return null;
|
|
2954
|
+
return createAuthenticationEvidence({
|
|
2955
|
+
verified: record.verified,
|
|
2956
|
+
requestSigners,
|
|
2957
|
+
documentSigners,
|
|
2958
|
+
originAuthenticated,
|
|
2959
|
+
expandedActivity,
|
|
2960
|
+
baseUrl
|
|
2961
|
+
});
|
|
2962
|
+
}
|
|
2963
|
+
/**
|
|
2964
|
+
* Creates frozen evidence.
|
|
2965
|
+
* @internal
|
|
2966
|
+
*/
|
|
2967
|
+
function createAuthenticationEvidence(evidence) {
|
|
2968
|
+
return Object.freeze({
|
|
2969
|
+
verified: evidence.verified,
|
|
2970
|
+
requestSigners: Object.freeze([...evidence.requestSigners]),
|
|
2971
|
+
documentSigners: Object.freeze([...evidence.documentSigners]),
|
|
2972
|
+
originAuthenticated: Object.freeze([...evidence.originAuthenticated]),
|
|
2973
|
+
expandedActivity: evidence.expandedActivity,
|
|
2974
|
+
baseUrl: evidence.baseUrl
|
|
2975
|
+
});
|
|
2976
|
+
}
|
|
2977
|
+
//#endregion
|
|
2807
2978
|
//#region src/federation/inbox.ts
|
|
2808
2979
|
/**
|
|
2809
2980
|
* Checks whether an activity's ID and all of its actor IDs have the same
|
|
@@ -2825,13 +2996,135 @@ function actorsHaveSameOrigin(activity) {
|
|
|
2825
2996
|
const origin = getAuthenticationOrigin(activity.id);
|
|
2826
2997
|
return origin != null && activity.actorIds.every((actor) => getAuthenticationOrigin(actor) === origin);
|
|
2827
2998
|
}
|
|
2828
|
-
|
|
2999
|
+
/**
|
|
3000
|
+
* The headroom that the size check of a queued inbox message leaves for the
|
|
3001
|
+
* fields that grow when Fedify retries the message, e.g., its attempt count.
|
|
3002
|
+
*/
|
|
3003
|
+
const QUEUED_MESSAGE_HEADROOM = 64;
|
|
3004
|
+
/**
|
|
3005
|
+
* Computes the idempotency key of an incoming activity.
|
|
3006
|
+
* @returns The key, or `null` if the activity is not deduplicated.
|
|
3007
|
+
* @internal
|
|
3008
|
+
*/
|
|
3009
|
+
async function getInboxIdempotencyKey(strategy, inboxContext, activity, origin, recipient, prefix) {
|
|
3010
|
+
if (activity.id == null) return null;
|
|
3011
|
+
strategy ??= "per-inbox";
|
|
3012
|
+
let keyString;
|
|
3013
|
+
if (typeof strategy === "function") keyString = await strategy(inboxContext(), activity);
|
|
3014
|
+
else switch (strategy) {
|
|
3015
|
+
case "global":
|
|
3016
|
+
keyString = activity.id.href;
|
|
3017
|
+
break;
|
|
3018
|
+
case "per-origin":
|
|
3019
|
+
keyString = `${origin}\n${activity.id.href}`;
|
|
3020
|
+
break;
|
|
3021
|
+
case "per-inbox":
|
|
3022
|
+
keyString = `${origin}\n${activity.id.href}\n${recipient == null ? "sharedInbox" : `inbox\n${recipient}`}`;
|
|
3023
|
+
break;
|
|
3024
|
+
default: keyString = `${origin}\n${activity.id.href}`;
|
|
3025
|
+
}
|
|
3026
|
+
return keyString == null ? null : [...prefix, keyString];
|
|
3027
|
+
}
|
|
3028
|
+
/**
|
|
3029
|
+
* Checks whether an incoming activity has already been processed, by its
|
|
3030
|
+
* idempotency key, or by the key that queue workers of older versions of
|
|
3031
|
+
* Fedify recorded.
|
|
3032
|
+
* @internal
|
|
3033
|
+
*/
|
|
3034
|
+
async function isInboxActivityProcessed(kv, cacheKey, activity, origin, prefix) {
|
|
3035
|
+
if (cacheKey == null) return false;
|
|
3036
|
+
if (await kv.get(cacheKey) === true) return true;
|
|
3037
|
+
if (activity.id == null) return false;
|
|
3038
|
+
return await kv.get([
|
|
3039
|
+
...prefix,
|
|
3040
|
+
origin,
|
|
3041
|
+
activity.id.href
|
|
3042
|
+
]) === true;
|
|
3043
|
+
}
|
|
3044
|
+
/**
|
|
3045
|
+
* The bytes added to the UTF-8 JSON length of a message for each of its JSON
|
|
3046
|
+
* values, to bound the size of serialization formats other than JSON, such
|
|
3047
|
+
* as the structured serialization of Deno KV, which spends more bytes than
|
|
3048
|
+
* JSON on type tags and lengths of small values, e.g., empty arrays, and up
|
|
3049
|
+
* to nine bytes on a fractional number that JSON writes in three.
|
|
3050
|
+
*/
|
|
3051
|
+
const PER_VALUE_SERIALIZATION_OVERHEAD = 8;
|
|
3052
|
+
/**
|
|
3053
|
+
* Estimates, conservatively, how large a queued message is once a queue
|
|
3054
|
+
* backend serializes it: the UTF-8 length of its JSON serialization, plus
|
|
3055
|
+
* a fixed overhead for each JSON value in it, plus the bytes that encoding
|
|
3056
|
+
* strings beyond Latin-1 as UTF-16 adds.
|
|
3057
|
+
* @internal
|
|
3058
|
+
*/
|
|
3059
|
+
function estimateQueuedMessageSize(message) {
|
|
3060
|
+
const encoder = new TextEncoder();
|
|
3061
|
+
let values = 0;
|
|
3062
|
+
let stringExpansion = 0;
|
|
3063
|
+
const countString = (value) => {
|
|
3064
|
+
values++;
|
|
3065
|
+
if (!/[\u0100-\uffff]/.test(value)) return;
|
|
3066
|
+
const expansion = value.length * 2 - encoder.encode(value).byteLength;
|
|
3067
|
+
if (expansion > 0) stringExpansion += expansion;
|
|
3068
|
+
};
|
|
3069
|
+
const count = (value) => {
|
|
3070
|
+
if (typeof value === "string") {
|
|
3071
|
+
countString(value);
|
|
3072
|
+
return;
|
|
3073
|
+
}
|
|
3074
|
+
values++;
|
|
3075
|
+
if (Array.isArray(value)) for (const item of value) count(item);
|
|
3076
|
+
else if (value != null && typeof value === "object") for (const [key, item] of Object.entries(value)) {
|
|
3077
|
+
if (item === void 0) continue;
|
|
3078
|
+
countString(key);
|
|
3079
|
+
count(item);
|
|
3080
|
+
}
|
|
3081
|
+
};
|
|
3082
|
+
count(message);
|
|
3083
|
+
return encoder.encode(JSON.stringify(message)).byteLength + stringExpansion + values * PER_VALUE_SERIALIZATION_OVERHEAD;
|
|
3084
|
+
}
|
|
3085
|
+
/**
|
|
3086
|
+
* Checks whether an activity that declares no actor is authenticated: at
|
|
3087
|
+
* least one principal must be authenticated for it, and its ID, if any, must
|
|
3088
|
+
* have the same [FEP-fe34] origin as one of those principals.
|
|
3089
|
+
*
|
|
3090
|
+
* [FEP-fe34]: https://w3id.org/fep/fe34
|
|
3091
|
+
* @returns `"ok"` if it is authenticated, or why it is not.
|
|
3092
|
+
* @internal
|
|
3093
|
+
*/
|
|
3094
|
+
function checkActorlessActivity(activity, authentication) {
|
|
3095
|
+
const principals = getEffectivePrincipals(authentication);
|
|
3096
|
+
if (principals.length < 1) return {
|
|
3097
|
+
status: "noAuthenticatedPrincipal",
|
|
3098
|
+
principals
|
|
3099
|
+
};
|
|
3100
|
+
if (activity.id == null) return {
|
|
3101
|
+
status: "ok",
|
|
3102
|
+
principals
|
|
3103
|
+
};
|
|
3104
|
+
const origin = getAuthenticationOrigin(activity.id);
|
|
3105
|
+
if (origin == null || !principals.some((principal) => getAuthenticationOrigin(principal.id) === origin)) return {
|
|
3106
|
+
status: "activityOriginMismatch",
|
|
3107
|
+
principals
|
|
3108
|
+
};
|
|
3109
|
+
return {
|
|
3110
|
+
status: "ok",
|
|
3111
|
+
principals
|
|
3112
|
+
};
|
|
3113
|
+
}
|
|
3114
|
+
/**
|
|
3115
|
+
* Same as {@link checkActorlessActivity}, but returns a boolean.
|
|
3116
|
+
* @internal
|
|
3117
|
+
*/
|
|
3118
|
+
function isActorlessActivityAuthenticated(activity, authentication) {
|
|
3119
|
+
return checkActorlessActivity(activity, authentication).status === "ok";
|
|
3120
|
+
}
|
|
3121
|
+
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
3122
|
const logger = getLogger([
|
|
2830
3123
|
"fedify",
|
|
2831
3124
|
"federation",
|
|
2832
3125
|
"inbox"
|
|
2833
3126
|
]);
|
|
2834
|
-
if (activity.actorId == null) {
|
|
3127
|
+
if (activity.actorId == null && !actorless) {
|
|
2835
3128
|
logger.error("Missing actor.", { activity: json });
|
|
2836
3129
|
span.setStatus({
|
|
2837
3130
|
code: SpanStatusCode.ERROR,
|
|
@@ -2840,58 +3133,59 @@ async function routeActivity({ context: ctx, json, originalJson, normalizedActiv
|
|
|
2840
3133
|
recordInboxActivity(meterProvider, "rejected", getTypeId(activity).href);
|
|
2841
3134
|
return "missingActor";
|
|
2842
3135
|
}
|
|
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];
|
|
3136
|
+
const contextMetadata = { authentication };
|
|
3137
|
+
const cacheKey = await getInboxIdempotencyKey(idempotencyStrategy, () => inboxContextFactory(recipient, json, activity.id?.href, getTypeId(activity).href, contextMetadata), activity, ctx.origin, recipient, kvPrefixes.activityIdempotence);
|
|
3138
|
+
if (await isInboxActivityProcessed(kv, cacheKey, activity, ctx.origin, kvPrefixes.activityIdempotence)) {
|
|
3139
|
+
logger.debug("Activity {activityId} has already been processed.", {
|
|
3140
|
+
activityId: activity.id?.href,
|
|
3141
|
+
activity: json,
|
|
3142
|
+
recipient
|
|
3143
|
+
});
|
|
3144
|
+
span.setStatus({
|
|
3145
|
+
code: SpanStatusCode.UNSET,
|
|
3146
|
+
message: `Activity ${activity.id?.href} has already been processed.`
|
|
3147
|
+
});
|
|
3148
|
+
recordInboxActivity(meterProvider, "rejected", getTypeId(activity).href);
|
|
3149
|
+
return "alreadyProcessed";
|
|
2862
3150
|
}
|
|
2863
|
-
if (
|
|
2864
|
-
|
|
2865
|
-
|
|
3151
|
+
if (activity.actorId != null) span.setAttribute("activitypub.actor.id", activity.actorId.href);
|
|
3152
|
+
let message;
|
|
3153
|
+
if (queue != null) {
|
|
3154
|
+
const carrier = {};
|
|
3155
|
+
propagation.inject(context.active(), carrier);
|
|
3156
|
+
message = {
|
|
3157
|
+
type: "inbox",
|
|
3158
|
+
id: crypto.randomUUID(),
|
|
3159
|
+
baseUrl: ctx.origin,
|
|
3160
|
+
activity: originalJson ?? json,
|
|
3161
|
+
...normalizedActivity == null ? {} : { normalizedActivity },
|
|
3162
|
+
...ldSignatureVerified == null ? {} : { ldSignatureVerified },
|
|
3163
|
+
...verifiedView == null ? {} : { verifiedView },
|
|
3164
|
+
...authentication == null ? {} : { authentication: serializeAuthentication(authentication) },
|
|
3165
|
+
identifier: recipient,
|
|
3166
|
+
attempt: 0,
|
|
3167
|
+
started: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3168
|
+
traceContext: carrier
|
|
3169
|
+
};
|
|
3170
|
+
const maxMessageSize = queue.maxMessageSize;
|
|
3171
|
+
if (replayable === false) {
|
|
3172
|
+
logger.warn("The incoming activity {activityId} uses a JSON-LD context that cannot be kept for the queue; processing it immediately instead.", {
|
|
2866
3173
|
activityId: activity.id?.href,
|
|
2867
|
-
activity: json,
|
|
2868
3174
|
recipient
|
|
2869
3175
|
});
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
3176
|
+
message = void 0;
|
|
3177
|
+
} else if (maxMessageSize != null && estimateQueuedMessageSize(message) > maxMessageSize - QUEUED_MESSAGE_HEADROOM) {
|
|
3178
|
+
logger.warn("The incoming activity {activityId} is too large to enqueue; processing it immediately instead.", {
|
|
3179
|
+
activityId: activity.id?.href,
|
|
3180
|
+
recipient,
|
|
3181
|
+
maxMessageSize
|
|
2873
3182
|
});
|
|
2874
|
-
|
|
2875
|
-
return "alreadyProcessed";
|
|
3183
|
+
message = void 0;
|
|
2876
3184
|
}
|
|
2877
3185
|
}
|
|
2878
|
-
|
|
2879
|
-
if (queue != null) {
|
|
2880
|
-
const carrier = {};
|
|
2881
|
-
propagation.inject(context.active(), carrier);
|
|
3186
|
+
if (queue != null && message != null) {
|
|
2882
3187
|
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
|
-
});
|
|
3188
|
+
await queue.enqueue(message);
|
|
2895
3189
|
} catch (error) {
|
|
2896
3190
|
logger.error("Failed to enqueue the incoming activity {activityId}:\n{error}", {
|
|
2897
3191
|
error,
|
|
@@ -2941,7 +3235,7 @@ async function routeActivity({ context: ctx, json, originalJson, normalizedActiv
|
|
|
2941
3235
|
const started = performance.now();
|
|
2942
3236
|
try {
|
|
2943
3237
|
const contextFactory = listenerInboxContextFactory ?? inboxContextFactory;
|
|
2944
|
-
await listener(contextFactory(recipient, contextFactory === inboxContextFactory ? json : originalJson ?? json, activity.id?.href, activityType), activity);
|
|
3238
|
+
await listener(contextFactory(recipient, contextFactory === inboxContextFactory ? json : originalJson ?? json, activity.id?.href, activityType, contextMetadata), activity);
|
|
2945
3239
|
} finally {
|
|
2946
3240
|
getFederationMetrics(meterProvider).recordInboxProcessingDuration(activityType, getDurationMs(started));
|
|
2947
3241
|
}
|
|
@@ -2982,6 +3276,382 @@ async function routeActivity({ context: ctx, json, originalJson, normalizedActiv
|
|
|
2982
3276
|
});
|
|
2983
3277
|
}
|
|
2984
3278
|
//#endregion
|
|
3279
|
+
//#region src/federation/inbox-view.ts
|
|
3280
|
+
/**
|
|
3281
|
+
* The interpretation of an incoming activity that Fedify authenticated, kept
|
|
3282
|
+
* so that listeners, authorization helpers, and queue workers observe the
|
|
3283
|
+
* same JSON-LD meaning that signature verification observed.
|
|
3284
|
+
*
|
|
3285
|
+
* Every JSON-LD processing step of one delivery goes through a request-scoped
|
|
3286
|
+
* *context snapshot*, which memoizes the remote context responses so that
|
|
3287
|
+
* a nondeterministic or mutable remote context cannot give verification and
|
|
3288
|
+
* dispatch different meanings. The snapshot is serialized into queued
|
|
3289
|
+
* messages, so that workers and retries replay the very same responses
|
|
3290
|
+
* without loading contexts again.
|
|
3291
|
+
* @module
|
|
3292
|
+
*/
|
|
3293
|
+
/**
|
|
3294
|
+
* Thrown when replaying an interpretation needs a context response that its
|
|
3295
|
+
* snapshot does not have, or has with a different content.
|
|
3296
|
+
* @internal
|
|
3297
|
+
*/
|
|
3298
|
+
var ContextSnapshotMissError = class extends Error {
|
|
3299
|
+
url;
|
|
3300
|
+
constructor(url, reason) {
|
|
3301
|
+
super(`Context ${url} cannot be replayed: ${reason}.`);
|
|
3302
|
+
this.name = "ContextSnapshotMissError";
|
|
3303
|
+
this.url = url;
|
|
3304
|
+
}
|
|
3305
|
+
};
|
|
3306
|
+
function normalizeUrl(url) {
|
|
3307
|
+
return URL.canParse(url) ? new URL(url).href : url;
|
|
3308
|
+
}
|
|
3309
|
+
/**
|
|
3310
|
+
* Creates the context snapshot of a delivery.
|
|
3311
|
+
* @param base The caller's context loader.
|
|
3312
|
+
* @returns The snapshot.
|
|
3313
|
+
* @internal
|
|
3314
|
+
*/
|
|
3315
|
+
function createContextSnapshot(base) {
|
|
3316
|
+
const underlying = {
|
|
3317
|
+
standard: wrapContextLoaderForJsonLd(base),
|
|
3318
|
+
normalization: getNormalizationContextLoader(base)
|
|
3319
|
+
};
|
|
3320
|
+
const cache = /* @__PURE__ */ new Map();
|
|
3321
|
+
const recorded = /* @__PURE__ */ new Map();
|
|
3322
|
+
const loaders = /* @__PURE__ */ new Map();
|
|
3323
|
+
let released = false;
|
|
3324
|
+
return {
|
|
3325
|
+
loader(policy) {
|
|
3326
|
+
let loader = loaders.get(policy);
|
|
3327
|
+
if (loader != null) return loader;
|
|
3328
|
+
const effective = underlying[policy];
|
|
3329
|
+
loader = markEffectiveContextLoader(async (url, options) => {
|
|
3330
|
+
if (released) return await effective(url, options);
|
|
3331
|
+
const normalizedUrl = normalizeUrl(url);
|
|
3332
|
+
const key = `${policy}\n${normalizedUrl}`;
|
|
3333
|
+
let promise = cache.get(key);
|
|
3334
|
+
if (promise == null) {
|
|
3335
|
+
promise = Promise.resolve(effective(url, options)).then((response) => {
|
|
3336
|
+
const cloned = normalizeResponse(structuredClone(response));
|
|
3337
|
+
recorded.set(key, {
|
|
3338
|
+
policy,
|
|
3339
|
+
url: normalizedUrl,
|
|
3340
|
+
response: structuredClone(cloned)
|
|
3341
|
+
});
|
|
3342
|
+
return cloned;
|
|
3343
|
+
});
|
|
3344
|
+
promise.catch(() => {});
|
|
3345
|
+
cache.set(key, promise);
|
|
3346
|
+
}
|
|
3347
|
+
return structuredClone(await promise);
|
|
3348
|
+
}, policy);
|
|
3349
|
+
loaders.set(policy, loader);
|
|
3350
|
+
return loader;
|
|
3351
|
+
},
|
|
3352
|
+
entries() {
|
|
3353
|
+
return [...recorded.values()];
|
|
3354
|
+
},
|
|
3355
|
+
release() {
|
|
3356
|
+
released = true;
|
|
3357
|
+
}
|
|
3358
|
+
};
|
|
3359
|
+
}
|
|
3360
|
+
function getBundledResponse(url) {
|
|
3361
|
+
if (!(url in preloadedContexts)) return null;
|
|
3362
|
+
return {
|
|
3363
|
+
contextUrl: null,
|
|
3364
|
+
document: preloadedContexts[url],
|
|
3365
|
+
documentUrl: url
|
|
3366
|
+
};
|
|
3367
|
+
}
|
|
3368
|
+
async function sha256(value) {
|
|
3369
|
+
return encodeHex(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(serialize(value))));
|
|
3370
|
+
}
|
|
3371
|
+
function normalizeResponse(response) {
|
|
3372
|
+
return {
|
|
3373
|
+
contextUrl: response.contextUrl ?? null,
|
|
3374
|
+
document: response.document,
|
|
3375
|
+
documentUrl: response.documentUrl
|
|
3376
|
+
};
|
|
3377
|
+
}
|
|
3378
|
+
/**
|
|
3379
|
+
* Serializes the entries of a context snapshot for a queued message.
|
|
3380
|
+
* @param entries The entries.
|
|
3381
|
+
* @returns The serialized entries, which are plain JSON values.
|
|
3382
|
+
* @internal
|
|
3383
|
+
*/
|
|
3384
|
+
async function serializeContextSnapshot(entries) {
|
|
3385
|
+
const result = [];
|
|
3386
|
+
for (const entry of entries) {
|
|
3387
|
+
const response = normalizeResponse(entry.response);
|
|
3388
|
+
const hash = await sha256(response);
|
|
3389
|
+
const bundled = getBundledResponse(entry.url);
|
|
3390
|
+
if (bundled != null && await sha256(bundled) === hash) result.push({
|
|
3391
|
+
policy: entry.policy,
|
|
3392
|
+
url: entry.url,
|
|
3393
|
+
sha256: hash,
|
|
3394
|
+
bundled: true
|
|
3395
|
+
});
|
|
3396
|
+
else result.push({
|
|
3397
|
+
policy: entry.policy,
|
|
3398
|
+
url: entry.url,
|
|
3399
|
+
sha256: hash,
|
|
3400
|
+
response: structuredClone(response)
|
|
3401
|
+
});
|
|
3402
|
+
}
|
|
3403
|
+
return result;
|
|
3404
|
+
}
|
|
3405
|
+
function isRemoteDocument(value) {
|
|
3406
|
+
if (value == null || typeof value !== "object") return false;
|
|
3407
|
+
const record = value;
|
|
3408
|
+
return typeof record.documentUrl === "string" && "document" in record && (record.contextUrl == null || typeof record.contextUrl === "string");
|
|
3409
|
+
}
|
|
3410
|
+
/**
|
|
3411
|
+
* Restores the entries of a serialized context snapshot, verifying the
|
|
3412
|
+
* hashes of stored responses. Bundled entries are reconstructed from the
|
|
3413
|
+
* contexts this version of Fedify bundles.
|
|
3414
|
+
* @param entries The serialized entries.
|
|
3415
|
+
* @returns The entries.
|
|
3416
|
+
* @throws {ContextSnapshotMissError} If an entry is malformed, or its
|
|
3417
|
+
* content does not match its hash.
|
|
3418
|
+
* @internal
|
|
3419
|
+
*/
|
|
3420
|
+
async function deserializeContextSnapshot(entries) {
|
|
3421
|
+
if (!Array.isArray(entries)) throw new ContextSnapshotMissError("(snapshot)", "malformed snapshot");
|
|
3422
|
+
const result = [];
|
|
3423
|
+
for (const entry of entries) {
|
|
3424
|
+
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");
|
|
3425
|
+
let response;
|
|
3426
|
+
if (entry.bundled === true) {
|
|
3427
|
+
response = getBundledResponse(entry.url);
|
|
3428
|
+
if (response == null) throw new ContextSnapshotMissError(entry.url, "not bundled");
|
|
3429
|
+
} else if (isRemoteDocument(entry.response)) response = normalizeResponse(entry.response);
|
|
3430
|
+
else throw new ContextSnapshotMissError(entry.url, "malformed entry");
|
|
3431
|
+
if (entry.bundled !== true && await sha256(response) !== entry.sha256) throw new ContextSnapshotMissError(entry.url, "content mismatch");
|
|
3432
|
+
result.push({
|
|
3433
|
+
policy: entry.policy,
|
|
3434
|
+
url: entry.url,
|
|
3435
|
+
response: structuredClone(response)
|
|
3436
|
+
});
|
|
3437
|
+
}
|
|
3438
|
+
return result;
|
|
3439
|
+
}
|
|
3440
|
+
/**
|
|
3441
|
+
* Creates a context loader of a policy that serves only the responses of
|
|
3442
|
+
* a snapshot. It is marked as the effective loader of the policy.
|
|
3443
|
+
* @param entries The snapshot's entries.
|
|
3444
|
+
* @param policy The policy.
|
|
3445
|
+
* @returns The loader, which throws {@link ContextSnapshotMissError} for
|
|
3446
|
+
* anything else.
|
|
3447
|
+
* @internal
|
|
3448
|
+
*/
|
|
3449
|
+
function createSnapshotOnlyLoader(entries, policy) {
|
|
3450
|
+
const responses = /* @__PURE__ */ new Map();
|
|
3451
|
+
for (const entry of entries) if (entry.policy === policy) responses.set(entry.url, entry.response);
|
|
3452
|
+
return markEffectiveContextLoader(async (url) => {
|
|
3453
|
+
const response = responses.get(normalizeUrl(url));
|
|
3454
|
+
if (response == null) throw new ContextSnapshotMissError(url, "not in the snapshot");
|
|
3455
|
+
return structuredClone(response);
|
|
3456
|
+
}, policy);
|
|
3457
|
+
}
|
|
3458
|
+
/**
|
|
3459
|
+
* Creates a {@link ReleasableLoader}.
|
|
3460
|
+
* @param strict The loader to use until release.
|
|
3461
|
+
* @param fallback The loader to use after release.
|
|
3462
|
+
* @param policy The policy both loaders apply.
|
|
3463
|
+
* @returns The releasable loader.
|
|
3464
|
+
* @internal
|
|
3465
|
+
*/
|
|
3466
|
+
function createReleasableLoader(strict, fallback, policy) {
|
|
3467
|
+
let released = false;
|
|
3468
|
+
return {
|
|
3469
|
+
loader: markEffectiveContextLoader((url, options) => released ? fallback(url, options) : strict(url, options), policy),
|
|
3470
|
+
release() {
|
|
3471
|
+
released = true;
|
|
3472
|
+
}
|
|
3473
|
+
};
|
|
3474
|
+
}
|
|
3475
|
+
/**
|
|
3476
|
+
* Expands a JSON-LD document with the given context loader.
|
|
3477
|
+
* @param document The document.
|
|
3478
|
+
* @param contextLoader The context loader.
|
|
3479
|
+
* @returns The expanded document.
|
|
3480
|
+
* @internal
|
|
3481
|
+
*/
|
|
3482
|
+
async function expandDocument(document, contextLoader) {
|
|
3483
|
+
return await jsonld.expand(document, { documentLoader: contextLoader });
|
|
3484
|
+
}
|
|
3485
|
+
/**
|
|
3486
|
+
* Computes the digest of an expanded document.
|
|
3487
|
+
* @param expanded The expanded document.
|
|
3488
|
+
* @returns The hex-encoded SHA-256 of its canonical JSON.
|
|
3489
|
+
* @internal
|
|
3490
|
+
*/
|
|
3491
|
+
function digestExpandedDocument(expanded) {
|
|
3492
|
+
return sha256(expanded);
|
|
3493
|
+
}
|
|
3494
|
+
/**
|
|
3495
|
+
* Deeply freezes a JSON value.
|
|
3496
|
+
* @param value The value.
|
|
3497
|
+
* @returns The same value, frozen.
|
|
3498
|
+
* @internal
|
|
3499
|
+
*/
|
|
3500
|
+
function deepFreeze(value) {
|
|
3501
|
+
if (value != null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
3502
|
+
Object.freeze(value);
|
|
3503
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
3504
|
+
}
|
|
3505
|
+
return value;
|
|
3506
|
+
}
|
|
3507
|
+
const ACTOR_IRI = "https://www.w3.org/ns/activitystreams#actor";
|
|
3508
|
+
/**
|
|
3509
|
+
* Gets the single root node of an expanded document.
|
|
3510
|
+
* @param expanded The expanded document.
|
|
3511
|
+
* @returns The root node, or `null` if there is not exactly one.
|
|
3512
|
+
* @internal
|
|
3513
|
+
*/
|
|
3514
|
+
function getRootNode(expanded) {
|
|
3515
|
+
if (expanded.length !== 1) return null;
|
|
3516
|
+
const node = expanded[0];
|
|
3517
|
+
if (node == null || typeof node !== "object" || Array.isArray(node)) return null;
|
|
3518
|
+
return node;
|
|
3519
|
+
}
|
|
3520
|
+
/**
|
|
3521
|
+
* Gets the values of a property of an expanded node, unwrapping a single
|
|
3522
|
+
* `@list` container the way generated vocabulary decoders do.
|
|
3523
|
+
* @param node The expanded node.
|
|
3524
|
+
* @param iri The property IRI.
|
|
3525
|
+
* @returns The values, or an empty array if the property is absent.
|
|
3526
|
+
* @internal
|
|
3527
|
+
*/
|
|
3528
|
+
function getPropertyValues(node, iri) {
|
|
3529
|
+
const values = node[iri];
|
|
3530
|
+
if (values == null) return [];
|
|
3531
|
+
if (!Array.isArray(values)) return [values];
|
|
3532
|
+
if (values.length === 1 && values[0] != null && typeof values[0] === "object" && "@list" in values[0]) {
|
|
3533
|
+
const list = values[0]["@list"];
|
|
3534
|
+
return Array.isArray(list) ? list : [list];
|
|
3535
|
+
}
|
|
3536
|
+
return values;
|
|
3537
|
+
}
|
|
3538
|
+
/**
|
|
3539
|
+
* Tells whether the root node of an expanded activity declares any `actor`
|
|
3540
|
+
* value at all, including blank nodes, anonymous or untyped nodes, and
|
|
3541
|
+
* literals, which ID accessors and typed parsing may drop.
|
|
3542
|
+
* @param expanded The expanded activity.
|
|
3543
|
+
* @returns `"absent"` if the root node declares no actor; `"present"`
|
|
3544
|
+
* otherwise, including when there is no single root node.
|
|
3545
|
+
* @internal
|
|
3546
|
+
*/
|
|
3547
|
+
function getActorPresence(expanded) {
|
|
3548
|
+
const root = getRootNode(expanded);
|
|
3549
|
+
if (root == null) return "present";
|
|
3550
|
+
return getPropertyValues(root, ACTOR_IRI).length < 1 ? "absent" : "present";
|
|
3551
|
+
}
|
|
3552
|
+
/**
|
|
3553
|
+
* Builds the serialized view of an authenticated activity. The document is
|
|
3554
|
+
* expanded through the snapshot, so that the snapshot holds every context
|
|
3555
|
+
* response the interpretation needs.
|
|
3556
|
+
* @param snapshot The delivery's context snapshot.
|
|
3557
|
+
* @param source Which document the activity was parsed from.
|
|
3558
|
+
* @returns The serialized view and the expanded activity.
|
|
3559
|
+
* @internal
|
|
3560
|
+
*/
|
|
3561
|
+
async function buildVerifiedActivityView(snapshot, source) {
|
|
3562
|
+
const loader = snapshot.loader(source.policy);
|
|
3563
|
+
const used = /* @__PURE__ */ new Set();
|
|
3564
|
+
const expanded = await expandDocument(source.document, markEffectiveContextLoader((url, options) => {
|
|
3565
|
+
used.add(normalizeUrl(url));
|
|
3566
|
+
return loader(url, options);
|
|
3567
|
+
}, source.policy));
|
|
3568
|
+
const entries = snapshot.entries().filter((entry) => entry.policy === source.policy && used.has(entry.url));
|
|
3569
|
+
const contexts = await serializeContextSnapshot(entries);
|
|
3570
|
+
const references = /* @__PURE__ */ new Set();
|
|
3571
|
+
collectContextReferences(source.document, source.baseUrl?.href, references);
|
|
3572
|
+
for (const entry of entries) {
|
|
3573
|
+
const { contextUrl, document } = entry.response;
|
|
3574
|
+
collectContextReferences(document, entry.url, references);
|
|
3575
|
+
if (contextUrl != null) collectContextReferences({ "@context": contextUrl }, entry.url, references);
|
|
3576
|
+
}
|
|
3577
|
+
return {
|
|
3578
|
+
replayable: [...references].every((url) => used.has(url)),
|
|
3579
|
+
view: {
|
|
3580
|
+
version: 1,
|
|
3581
|
+
policy: source.policy,
|
|
3582
|
+
source: source.source,
|
|
3583
|
+
baseUrl: source.baseUrl?.href ?? null,
|
|
3584
|
+
contexts,
|
|
3585
|
+
expandedDigest: await digestExpandedDocument(expanded)
|
|
3586
|
+
},
|
|
3587
|
+
expanded
|
|
3588
|
+
};
|
|
3589
|
+
}
|
|
3590
|
+
/**
|
|
3591
|
+
* Collects the remote context references of a JSON-LD document or context
|
|
3592
|
+
* document: every IRI given as a value of `@context` at any depth, including
|
|
3593
|
+
* scoped contexts of term definitions, and of `@import`.
|
|
3594
|
+
* @param value The document.
|
|
3595
|
+
* @param base The URL to resolve relative references against.
|
|
3596
|
+
* @param references The set to add the normalized references to.
|
|
3597
|
+
*/
|
|
3598
|
+
function collectContextReferences(value, base, references) {
|
|
3599
|
+
const add = (reference) => {
|
|
3600
|
+
let url = reference;
|
|
3601
|
+
if (base != null && URL.canParse(reference, base)) url = new URL(reference, base).href;
|
|
3602
|
+
references.add(normalizeUrl(url));
|
|
3603
|
+
};
|
|
3604
|
+
const collectContext = (context) => {
|
|
3605
|
+
if (typeof context === "string") add(context);
|
|
3606
|
+
else if (Array.isArray(context)) for (const item of context) collectContext(item);
|
|
3607
|
+
else if (context != null && typeof context === "object") {
|
|
3608
|
+
const imported = context["@import"];
|
|
3609
|
+
if (typeof imported === "string") add(imported);
|
|
3610
|
+
walk(context);
|
|
3611
|
+
}
|
|
3612
|
+
};
|
|
3613
|
+
const walk = (node) => {
|
|
3614
|
+
if (Array.isArray(node)) for (const item of node) walk(item);
|
|
3615
|
+
else if (node != null && typeof node === "object") for (const [key, item] of Object.entries(node)) if (key === "@context") collectContext(item);
|
|
3616
|
+
else walk(item);
|
|
3617
|
+
};
|
|
3618
|
+
walk(value);
|
|
3619
|
+
}
|
|
3620
|
+
/**
|
|
3621
|
+
* Validates the shape of a serialized view taken from a queued message.
|
|
3622
|
+
* @param value The value of the message's `verifiedView` field.
|
|
3623
|
+
* @returns The view, or `null` if it is malformed or of an unsupported
|
|
3624
|
+
* version.
|
|
3625
|
+
* @internal
|
|
3626
|
+
*/
|
|
3627
|
+
function parseSerializedVerifiedActivityView(value) {
|
|
3628
|
+
if (value == null || typeof value !== "object") return null;
|
|
3629
|
+
const view = value;
|
|
3630
|
+
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;
|
|
3631
|
+
return view;
|
|
3632
|
+
}
|
|
3633
|
+
/**
|
|
3634
|
+
* Restores a view from a queued message without loading any context.
|
|
3635
|
+
* @param view The serialized view.
|
|
3636
|
+
* @param message The fields of the message the view refers to.
|
|
3637
|
+
* @returns The restored view.
|
|
3638
|
+
* @throws {ContextSnapshotMissError} If the view cannot be replayed exactly.
|
|
3639
|
+
* @internal
|
|
3640
|
+
*/
|
|
3641
|
+
async function restoreVerifiedActivityView(view, message) {
|
|
3642
|
+
const loader = createSnapshotOnlyLoader(await deserializeContextSnapshot(view.contexts), view.policy);
|
|
3643
|
+
if (view.source === "normalizedActivity" && message.normalizedActivity == null) throw new ContextSnapshotMissError("(normalizedActivity)", "the normalized activity is missing");
|
|
3644
|
+
const document = detachSignature(view.source === "activity" ? message.activity : message.normalizedActivity);
|
|
3645
|
+
const expanded = await expandDocument(document, loader);
|
|
3646
|
+
if (await digestExpandedDocument(expanded) !== view.expandedDigest) throw new ContextSnapshotMissError("(activity)", "the interpretation does not match the authenticated one");
|
|
3647
|
+
return {
|
|
3648
|
+
document,
|
|
3649
|
+
loader,
|
|
3650
|
+
expanded,
|
|
3651
|
+
baseUrl: view.baseUrl == null ? null : new URL(view.baseUrl)
|
|
3652
|
+
};
|
|
3653
|
+
}
|
|
3654
|
+
//#endregion
|
|
2985
3655
|
//#region src/federation/keycache.ts
|
|
2986
3656
|
const KEY_CACHE_GENERATION = "3";
|
|
2987
3657
|
const COMPATIBLE_KEY_SEGMENT = "__compatible";
|
|
@@ -4792,13 +5462,13 @@ function recordCollectionMetrics(meterProvider, base, result, options = {}) {
|
|
|
4792
5462
|
* @param parameters The parameters for handling the collection.
|
|
4793
5463
|
* @returns A promise that resolves to an HTTP response.
|
|
4794
5464
|
*/
|
|
4795
|
-
async function handleCollection(request, { name: name$
|
|
4796
|
-
const spanName = name$
|
|
5465
|
+
async function handleCollection(request, { name: name$1, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, portable, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
|
|
5466
|
+
const spanName = name$1.trim().replace(/\s+/g, "_");
|
|
4797
5467
|
tracerProvider = tracerProvider ?? trace.getTracerProvider();
|
|
4798
5468
|
const tracer = tracerProvider.getTracer(name, version);
|
|
4799
5469
|
const cursor = new URL(request.url).searchParams.get("cursor");
|
|
4800
5470
|
const metricBase = {
|
|
4801
|
-
kind: getCollectionMetricKind(name$
|
|
5471
|
+
kind: getCollectionMetricKind(name$1),
|
|
4802
5472
|
page: cursor != null,
|
|
4803
5473
|
dispatcher: "built_in"
|
|
4804
5474
|
};
|
|
@@ -4844,7 +5514,7 @@ async function handleCollection(request, { name: name$2, identifier, uriGetter,
|
|
|
4844
5514
|
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
4845
5515
|
return await onNotFound(request);
|
|
4846
5516
|
}
|
|
4847
|
-
const items = filterCollectionItems(page.items, name$
|
|
5517
|
+
const items = filterCollectionItems(page.items, name$1, filterPredicate);
|
|
4848
5518
|
itemCount = items.length;
|
|
4849
5519
|
span.setAttribute("fedify.collection.items", itemCount);
|
|
4850
5520
|
return items;
|
|
@@ -4886,7 +5556,7 @@ async function handleCollection(request, { name: name$2, identifier, uriGetter,
|
|
|
4886
5556
|
} else {
|
|
4887
5557
|
const uri = new URL(portable == null ? baseUri : pageBase);
|
|
4888
5558
|
uri.searchParams.set("cursor", cursor);
|
|
4889
|
-
const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$
|
|
5559
|
+
const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$1}`, {
|
|
4890
5560
|
kind: SpanKind.SERVER,
|
|
4891
5561
|
attributes: {
|
|
4892
5562
|
"activitypub.collection.id": uri.href,
|
|
@@ -4902,7 +5572,7 @@ async function handleCollection(request, { name: name$2, identifier, uriGetter,
|
|
|
4902
5572
|
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
4903
5573
|
return await onNotFound(request);
|
|
4904
5574
|
}
|
|
4905
|
-
const items = filterCollectionItems(page.items, name$
|
|
5575
|
+
const items = filterCollectionItems(page.items, name$1, filterPredicate);
|
|
4906
5576
|
itemCount = items.length;
|
|
4907
5577
|
span.setAttribute("fedify.collection.items", itemCount);
|
|
4908
5578
|
return {
|
|
@@ -5399,11 +6069,12 @@ async function handleInbox(request, options) {
|
|
|
5399
6069
|
}, async (span) => {
|
|
5400
6070
|
if (options.recipient != null) span.setAttribute("fedify.inbox.recipient", options.recipient);
|
|
5401
6071
|
const spanContext = span.spanContext();
|
|
6072
|
+
const snapshot = createContextSnapshot(options.context.contextLoader);
|
|
5402
6073
|
try {
|
|
5403
6074
|
return await withContext({
|
|
5404
6075
|
traceId: spanContext.traceId,
|
|
5405
6076
|
spanId: spanContext.spanId
|
|
5406
|
-
}, () => handleInboxInternal(request, options, span));
|
|
6077
|
+
}, () => handleInboxInternal(request, options, span, snapshot));
|
|
5407
6078
|
} catch (e) {
|
|
5408
6079
|
options.observation.hasException = true;
|
|
5409
6080
|
span.setStatus({
|
|
@@ -5412,6 +6083,7 @@ async function handleInbox(request, options) {
|
|
|
5412
6083
|
});
|
|
5413
6084
|
throw e;
|
|
5414
6085
|
} finally {
|
|
6086
|
+
snapshot.release();
|
|
5415
6087
|
options.observation.project(span);
|
|
5416
6088
|
span.end();
|
|
5417
6089
|
}
|
|
@@ -5423,9 +6095,10 @@ async function handleInbox(request, options) {
|
|
|
5423
6095
|
* @param request The HTTP request.
|
|
5424
6096
|
* @param options The parameters for handling the inbox.
|
|
5425
6097
|
* @param span The OpenTelemetry span for tracing.
|
|
6098
|
+
* @param snapshot The context snapshot of the delivery.
|
|
5426
6099
|
* @returns A promise that resolves to an HTTP response.
|
|
5427
6100
|
*/
|
|
5428
|
-
async function handleInboxInternal(request, parameters, span) {
|
|
6101
|
+
async function handleInboxInternal(request, parameters, span, snapshot) {
|
|
5429
6102
|
const { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, publicKeyTtl, queue, actorDispatcher, inboxListeners, inboxErrorHandler, unverifiedActivityHandler, onNotFound, signatureTimeWindow, maxHttpSignatures, skipSignatureVerification, inboxChallengePolicy, meterProvider, tracerProvider, portableInbox } = parameters;
|
|
5430
6103
|
const observation = parameters.observation;
|
|
5431
6104
|
const signatureObservation = { [verificationObservation]: observation.verification };
|
|
@@ -5539,6 +6212,8 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5539
6212
|
tracerProvider,
|
|
5540
6213
|
keyTtl: publicKeyTtl
|
|
5541
6214
|
});
|
|
6215
|
+
const standardLoader = snapshot.loader("standard");
|
|
6216
|
+
const normalizationLoader = snapshot.loader("normalization");
|
|
5542
6217
|
const jsonWithoutSig = detachSignature(json);
|
|
5543
6218
|
const hasLdSignature = hasSignature(json);
|
|
5544
6219
|
const canAttemptAlternateAuthAfterLdSignatureFailure = skipSignatureVerification || hasHttpSignatureHeaders(request) || hasObjectIntegrityProof(jsonWithoutSig);
|
|
@@ -5574,9 +6249,10 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5574
6249
|
let compactedJson = json;
|
|
5575
6250
|
let compactedJsonWithoutSig = jsonWithoutSig;
|
|
5576
6251
|
let ldSigVerified = false;
|
|
6252
|
+
let ldSigKey = null;
|
|
5577
6253
|
if (hasLdSignature) {
|
|
5578
6254
|
try {
|
|
5579
|
-
compactedJson = await compactJsonLd(json,
|
|
6255
|
+
compactedJson = await compactJsonLd(json, normalizationLoader);
|
|
5580
6256
|
} catch (error) {
|
|
5581
6257
|
if (isInvalidJsonLdError(error)) {
|
|
5582
6258
|
logger.error("Failed to parse JSON-LD:\n{error}", {
|
|
@@ -5612,16 +6288,18 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5612
6288
|
if (compactedJson !== json) {
|
|
5613
6289
|
compactedJsonWithoutSig = detachSignature(compactedJson);
|
|
5614
6290
|
try {
|
|
5615
|
-
|
|
6291
|
+
const ldResult = await verifyCompactJsonLdDetailed(compactedJson, {
|
|
5616
6292
|
...signatureObservation,
|
|
5617
|
-
contextLoader:
|
|
6293
|
+
contextLoader: normalizationLoader,
|
|
5618
6294
|
documentLoader: ctx.documentLoader,
|
|
5619
6295
|
keyCache,
|
|
5620
6296
|
meterProvider,
|
|
5621
6297
|
tracerProvider
|
|
5622
6298
|
});
|
|
6299
|
+
ldSigVerified = ldResult.verified;
|
|
6300
|
+
ldSigKey = ldResult.key;
|
|
5623
6301
|
} catch (error) {
|
|
5624
|
-
if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig,
|
|
6302
|
+
if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig, normalizationLoader)) return await respondInvalidActivity(error);
|
|
5625
6303
|
if (isInvalidJsonLdError(error)) {
|
|
5626
6304
|
logger.error("Failed to parse JSON-LD:\n{error}", {
|
|
5627
6305
|
recipient,
|
|
@@ -5639,12 +6317,12 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5639
6317
|
if (!canAttemptAlternateAuthAfterLdSignatureFailure) throw error;
|
|
5640
6318
|
if (!skipSignatureVerification) try {
|
|
5641
6319
|
await Object$1.fromJsonLd(compactedJson, {
|
|
5642
|
-
contextLoader:
|
|
6320
|
+
contextLoader: normalizationLoader,
|
|
5643
6321
|
documentLoader: ctx.documentLoader,
|
|
5644
6322
|
tracerProvider
|
|
5645
6323
|
});
|
|
5646
6324
|
} catch (parseError) {
|
|
5647
|
-
if (parseError instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig,
|
|
6325
|
+
if (parseError instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig, normalizationLoader)) return await respondInvalidActivity(parseError);
|
|
5648
6326
|
if (isInvalidJsonLdError(parseError)) {
|
|
5649
6327
|
logger.error("Failed to parse JSON-LD:\n{error}", {
|
|
5650
6328
|
recipient,
|
|
@@ -5668,6 +6346,33 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5668
6346
|
let activity = null;
|
|
5669
6347
|
let activityVerified = false;
|
|
5670
6348
|
let proofVerified = false;
|
|
6349
|
+
let documentSigners = [];
|
|
6350
|
+
let requestSigners = [];
|
|
6351
|
+
let viewSource;
|
|
6352
|
+
const rawViewSource = {
|
|
6353
|
+
policy: "standard",
|
|
6354
|
+
source: "activity",
|
|
6355
|
+
document: jsonWithoutSig,
|
|
6356
|
+
baseUrl: null
|
|
6357
|
+
};
|
|
6358
|
+
const isActorlessCandidate = async (candidate, source) => {
|
|
6359
|
+
if (skipSignatureVerification) return false;
|
|
6360
|
+
if (inboxListeners?.dispatchWithClass(candidate)?.options.allowActorless !== true) return false;
|
|
6361
|
+
try {
|
|
6362
|
+
return getActorPresence(await expandDocument(source.document, snapshot.loader(source.policy))) === "absent";
|
|
6363
|
+
} catch {
|
|
6364
|
+
return false;
|
|
6365
|
+
}
|
|
6366
|
+
};
|
|
6367
|
+
const verifyProofs = (contextLoader = standardLoader) => verifyObjectDetailed(Activity, jsonWithoutSig, {
|
|
6368
|
+
...signatureObservation,
|
|
6369
|
+
contextLoader,
|
|
6370
|
+
documentLoader: ctx.documentLoader,
|
|
6371
|
+
keyCache,
|
|
6372
|
+
meterProvider,
|
|
6373
|
+
tracerProvider,
|
|
6374
|
+
verifyPortableObject: ctx.verifyPortableObject
|
|
6375
|
+
});
|
|
5671
6376
|
if (ldSigVerified) {
|
|
5672
6377
|
logger.debug("Linked Data Signatures are verified.", {
|
|
5673
6378
|
recipient,
|
|
@@ -5676,18 +6381,49 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5676
6381
|
try {
|
|
5677
6382
|
activity = observation.activity = await Activity.fromJsonLd(compactedJsonWithoutSig, {
|
|
5678
6383
|
...ctx,
|
|
5679
|
-
contextLoader:
|
|
6384
|
+
contextLoader: normalizationLoader
|
|
5680
6385
|
});
|
|
5681
6386
|
} catch (error) {
|
|
5682
|
-
if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig,
|
|
6387
|
+
if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig, normalizationLoader)) return await respondInvalidActivity(error);
|
|
5683
6388
|
if (!isPermanentActivityParseError(error)) throw error;
|
|
5684
6389
|
return await respondInvalidActivity(error);
|
|
5685
6390
|
}
|
|
5686
6391
|
activityVerified = true;
|
|
6392
|
+
viewSource = {
|
|
6393
|
+
policy: "normalization",
|
|
6394
|
+
source: "normalizedActivity",
|
|
6395
|
+
document: compactedJsonWithoutSig,
|
|
6396
|
+
baseUrl: null
|
|
6397
|
+
};
|
|
6398
|
+
if (ldSigKey?.ownerId != null) documentSigners = [{
|
|
6399
|
+
kind: "actor",
|
|
6400
|
+
id: ldSigKey.ownerId,
|
|
6401
|
+
mechanism: "linkedDataSignatures",
|
|
6402
|
+
keyId: ldSigKey.id
|
|
6403
|
+
}];
|
|
6404
|
+
if (await isActorlessCandidate(activity, viewSource)) {
|
|
6405
|
+
let result = null;
|
|
6406
|
+
try {
|
|
6407
|
+
result = await verifyProofs();
|
|
6408
|
+
} catch (error) {
|
|
6409
|
+
if (!isPermanentActivityParseError(error)) throw error;
|
|
6410
|
+
logger.debug("Failed to verify the Object Integrity Proofs of the activity without an actor:\n{error}", {
|
|
6411
|
+
recipient,
|
|
6412
|
+
error
|
|
6413
|
+
});
|
|
6414
|
+
}
|
|
6415
|
+
if (result?.status === "unavailable") throw result.error;
|
|
6416
|
+
if (result?.status === "verified" && result.signers.length > 0) {
|
|
6417
|
+
activity = observation.activity = result.object;
|
|
6418
|
+
documentSigners = toProofPrincipals(result.signers);
|
|
6419
|
+
viewSource = rawViewSource;
|
|
6420
|
+
proofVerified = true;
|
|
6421
|
+
}
|
|
6422
|
+
}
|
|
5687
6423
|
if (!skipSignatureVerification && isPortableActivity(activity)) try {
|
|
5688
6424
|
proofVerified = await verifyObject(Activity, jsonWithoutSig, {
|
|
5689
6425
|
...signatureObservation,
|
|
5690
|
-
contextLoader:
|
|
6426
|
+
contextLoader: standardLoader,
|
|
5691
6427
|
documentLoader: ctx.documentLoader,
|
|
5692
6428
|
keyCache,
|
|
5693
6429
|
meterProvider,
|
|
@@ -5705,18 +6441,11 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5705
6441
|
recipient,
|
|
5706
6442
|
json
|
|
5707
6443
|
});
|
|
6444
|
+
let proofResult;
|
|
5708
6445
|
try {
|
|
5709
|
-
|
|
5710
|
-
...signatureObservation,
|
|
5711
|
-
contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
|
|
5712
|
-
documentLoader: ctx.documentLoader,
|
|
5713
|
-
keyCache,
|
|
5714
|
-
meterProvider,
|
|
5715
|
-
tracerProvider,
|
|
5716
|
-
verifyPortableObject: ctx.verifyPortableObject
|
|
5717
|
-
});
|
|
6446
|
+
proofResult = await verifyProofs();
|
|
5718
6447
|
} catch (error) {
|
|
5719
|
-
if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(jsonWithoutSig,
|
|
6448
|
+
if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(jsonWithoutSig, standardLoader)) return await respondInvalidActivity(error);
|
|
5720
6449
|
if (deferredLdSignatureError != null) {
|
|
5721
6450
|
logger.debug("Object Integrity Proof fallback did not supersede a deferred Linked Data Signature failure:\n{error}", {
|
|
5722
6451
|
recipient,
|
|
@@ -5752,6 +6481,22 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5752
6481
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
5753
6482
|
});
|
|
5754
6483
|
}
|
|
6484
|
+
if (proofResult.status === "verified") if (proofResult.signers.length < 1 && await isActorlessCandidate(proofResult.object, rawViewSource)) activity = null;
|
|
6485
|
+
else {
|
|
6486
|
+
activity = proofResult.object;
|
|
6487
|
+
documentSigners = toProofPrincipals(proofResult.signers);
|
|
6488
|
+
viewSource = rawViewSource;
|
|
6489
|
+
}
|
|
6490
|
+
else if (proofResult.status === "unavailable") {
|
|
6491
|
+
let parsed = null;
|
|
6492
|
+
try {
|
|
6493
|
+
parsed = await Activity.fromJsonLd(jsonWithoutSig, {
|
|
6494
|
+
...ctx,
|
|
6495
|
+
contextLoader: standardLoader
|
|
6496
|
+
});
|
|
6497
|
+
} catch {}
|
|
6498
|
+
if (parsed != null && await isActorlessCandidate(parsed, rawViewSource)) throw proofResult.error;
|
|
6499
|
+
}
|
|
5755
6500
|
if (activity == null) logger.debug("Object Integrity Proofs are not verified.", {
|
|
5756
6501
|
recipient,
|
|
5757
6502
|
activity: json
|
|
@@ -5881,14 +6626,28 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5881
6626
|
try {
|
|
5882
6627
|
activity = observation.activity = await Activity.fromJsonLd(jsonWithoutSig, {
|
|
5883
6628
|
...ctx,
|
|
5884
|
-
contextLoader:
|
|
6629
|
+
contextLoader: standardLoader
|
|
5885
6630
|
});
|
|
5886
6631
|
} catch (error) {
|
|
5887
6632
|
if (!isPermanentActivityParseError(error)) throw error;
|
|
5888
6633
|
return await respondInvalidActivity(error);
|
|
5889
6634
|
}
|
|
6635
|
+
viewSource = rawViewSource;
|
|
5890
6636
|
}
|
|
5891
6637
|
observation.activity = activity;
|
|
6638
|
+
let view;
|
|
6639
|
+
try {
|
|
6640
|
+
view = await buildVerifiedActivityView(snapshot, viewSource ?? rawViewSource);
|
|
6641
|
+
} catch (error) {
|
|
6642
|
+
if (!isPermanentActivityParseError(error)) throw error;
|
|
6643
|
+
return await respondInvalidActivity(error);
|
|
6644
|
+
}
|
|
6645
|
+
const actorless = !skipSignatureVerification && getActorPresence(view.expanded) === "absent" && inboxListeners?.dispatchWithClass(activity)?.options.allowActorless === true;
|
|
6646
|
+
span.setAttribute("activitypub.activity.actorless", actorless);
|
|
6647
|
+
if (actorless && httpSigKey != null) {
|
|
6648
|
+
const principal = getHttpKeyPrincipal(httpSigKey);
|
|
6649
|
+
if (principal != null) requestSigners = [principal];
|
|
6650
|
+
}
|
|
5892
6651
|
observation.stage = "policy";
|
|
5893
6652
|
observation.result = {
|
|
5894
6653
|
disposition: "rejected",
|
|
@@ -5932,7 +6691,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5932
6691
|
const rejection = await observeAttempt(signatureObservation, "objectIntegrity", async (verification) => {
|
|
5933
6692
|
const rejection = await verifyPortableActivityId(activity.id, jsonWithoutSig, {
|
|
5934
6693
|
[verificationObservation]: verification,
|
|
5935
|
-
contextLoader:
|
|
6694
|
+
contextLoader: standardLoader,
|
|
5936
6695
|
documentLoader: ctx.documentLoader,
|
|
5937
6696
|
keyCache,
|
|
5938
6697
|
meterProvider,
|
|
@@ -5970,7 +6729,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5970
6729
|
});
|
|
5971
6730
|
}
|
|
5972
6731
|
}
|
|
5973
|
-
if (httpSigKey != null && !await doesActorOwnKey(activity, httpSigKey, ctx)) {
|
|
6732
|
+
if (httpSigKey != null && !actorless && !await doesActorOwnKey(activity, httpSigKey, ctx)) {
|
|
5974
6733
|
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
5975
6734
|
observation.authentication = {
|
|
5976
6735
|
status: "rejected",
|
|
@@ -5996,6 +6755,66 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
5996
6755
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
5997
6756
|
});
|
|
5998
6757
|
}
|
|
6758
|
+
if (httpSigKey != null && !actorless && activity.actorId != null) requestSigners = [{
|
|
6759
|
+
kind: "actor",
|
|
6760
|
+
id: activity.actorId,
|
|
6761
|
+
mechanism: "httpSignatures",
|
|
6762
|
+
keyId: httpSigKey.id
|
|
6763
|
+
}];
|
|
6764
|
+
if (actorless) {
|
|
6765
|
+
const check = checkActorlessActivity(activity, {
|
|
6766
|
+
verified: true,
|
|
6767
|
+
requestSigners,
|
|
6768
|
+
documentSigners,
|
|
6769
|
+
originAuthenticated: [],
|
|
6770
|
+
expandedActivity: [],
|
|
6771
|
+
baseUrl: null
|
|
6772
|
+
});
|
|
6773
|
+
if (check.status === "noAuthenticatedPrincipal") {
|
|
6774
|
+
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
6775
|
+
observation.authentication = {
|
|
6776
|
+
status: "rejected",
|
|
6777
|
+
reason: { type: "noAuthenticatedPrincipal" }
|
|
6778
|
+
};
|
|
6779
|
+
logger.error("The activity {activityId} declares no actor, and no principal is authenticated for it.", {
|
|
6780
|
+
activity: json,
|
|
6781
|
+
recipient,
|
|
6782
|
+
activityId: activity.id?.href
|
|
6783
|
+
});
|
|
6784
|
+
span.setStatus({
|
|
6785
|
+
code: SpanStatusCode.ERROR,
|
|
6786
|
+
message: "No principal is authenticated for the activity."
|
|
6787
|
+
});
|
|
6788
|
+
return new Response("No principal is authenticated for the activity.", {
|
|
6789
|
+
status: 401,
|
|
6790
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
6791
|
+
});
|
|
6792
|
+
} else if (check.status === "activityOriginMismatch") {
|
|
6793
|
+
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
6794
|
+
observation.authentication = {
|
|
6795
|
+
status: "rejected",
|
|
6796
|
+
reason: {
|
|
6797
|
+
type: "activityOriginMismatch",
|
|
6798
|
+
activityId: new URL(activity.id.href),
|
|
6799
|
+
actorIds: check.principals.map((principal) => principal.id)
|
|
6800
|
+
}
|
|
6801
|
+
};
|
|
6802
|
+
logger.error("The activity ({activityId}) does not have the same origin as the principals that authenticated it.", {
|
|
6803
|
+
activity: json,
|
|
6804
|
+
recipient,
|
|
6805
|
+
activityId: activity.id?.href,
|
|
6806
|
+
principals: check.principals.map((principal) => principal.id.href)
|
|
6807
|
+
});
|
|
6808
|
+
span.setStatus({
|
|
6809
|
+
code: SpanStatusCode.ERROR,
|
|
6810
|
+
message: `The activity (${activity.id?.href}) does not have the same origin as the principals that authenticated it.`
|
|
6811
|
+
});
|
|
6812
|
+
return new Response("The activity and its signers do not have the same origin.", {
|
|
6813
|
+
status: 401,
|
|
6814
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
6815
|
+
});
|
|
6816
|
+
}
|
|
6817
|
+
}
|
|
5999
6818
|
if (!skipSignatureVerification) {
|
|
6000
6819
|
const compoundJson = Array.isArray(json) ? json : jsonWithoutSig;
|
|
6001
6820
|
if (inspectCompoundPortableObjectApplicability(compoundJson, INBOX_COMPOUND_PROOF_LIMITS) !== "absent") {
|
|
@@ -6055,7 +6874,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
6055
6874
|
return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
|
|
6056
6875
|
}
|
|
6057
6876
|
}
|
|
6058
|
-
if (!skipSignatureVerification && activity.id != null && !actorsHaveSameOrigin(activity)) {
|
|
6877
|
+
if (!skipSignatureVerification && !actorless && activity.id != null && !actorsHaveSameOrigin(activity)) {
|
|
6059
6878
|
observation.authentication = {
|
|
6060
6879
|
status: "rejected",
|
|
6061
6880
|
reason: {
|
|
@@ -6084,7 +6903,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
6084
6903
|
attempts: observation.verification.attempts.filter((attempt) => attempt.status === "verified")
|
|
6085
6904
|
};
|
|
6086
6905
|
observation.stage = "dispatch";
|
|
6087
|
-
if (activity.actorId == null) {
|
|
6906
|
+
if (!actorless && activity.actorId == null) {
|
|
6088
6907
|
observation.result = {
|
|
6089
6908
|
disposition: "rejected",
|
|
6090
6909
|
reason: "missingActor"
|
|
@@ -6108,6 +6927,17 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
6108
6927
|
originalJson: json,
|
|
6109
6928
|
normalizedActivity: hasLdSignature && compactedJson !== json ? compactedJson : void 0,
|
|
6110
6929
|
ldSignatureVerified: hasLdSignature ? ldSigVerified : void 0,
|
|
6930
|
+
authentication: createAuthenticationEvidence({
|
|
6931
|
+
verified: !skipSignatureVerification,
|
|
6932
|
+
requestSigners,
|
|
6933
|
+
documentSigners,
|
|
6934
|
+
originAuthenticated: [],
|
|
6935
|
+
expandedActivity: deepFreeze(view.expanded),
|
|
6936
|
+
baseUrl: null
|
|
6937
|
+
}),
|
|
6938
|
+
verifiedView: view.view,
|
|
6939
|
+
replayable: view.replayable,
|
|
6940
|
+
actorless,
|
|
6111
6941
|
activity,
|
|
6112
6942
|
recipient,
|
|
6113
6943
|
inboxListeners,
|
|
@@ -6322,8 +7152,8 @@ var CustomCollectionHandler = class {
|
|
|
6322
7152
|
* @param CollectionPage The CollectionPage constructor.
|
|
6323
7153
|
* @param filterPredicate Optional filter predicate for items.
|
|
6324
7154
|
*/
|
|
6325
|
-
constructor(name$
|
|
6326
|
-
this.name = name$
|
|
7155
|
+
constructor(name$2, values, context, callbacks, tracerProvider = trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate, portable) {
|
|
7156
|
+
this.name = name$2;
|
|
6327
7157
|
this.values = values;
|
|
6328
7158
|
this.context = context;
|
|
6329
7159
|
this.callbacks = callbacks;
|
|
@@ -6832,6 +7662,42 @@ const MIN_COMPONENTS = [
|
|
|
6832
7662
|
"@authority"
|
|
6833
7663
|
];
|
|
6834
7664
|
/**
|
|
7665
|
+
* Turns the signers of verified Object Integrity Proofs into principals.
|
|
7666
|
+
*/
|
|
7667
|
+
function toProofPrincipals(signers) {
|
|
7668
|
+
return signers.map((signer) => ({
|
|
7669
|
+
kind: signer.kind,
|
|
7670
|
+
id: signer.id,
|
|
7671
|
+
mechanism: "objectIntegrityProofs",
|
|
7672
|
+
keyId: signer.keyId
|
|
7673
|
+
}));
|
|
7674
|
+
}
|
|
7675
|
+
/**
|
|
7676
|
+
* Gets the principal of an HTTP Signature key, whose owner was confirmed by
|
|
7677
|
+
* the owner's own document, or by the signed document of a portable actor,
|
|
7678
|
+
* when the key was fetched.
|
|
7679
|
+
*/
|
|
7680
|
+
function getHttpKeyPrincipal(key) {
|
|
7681
|
+
const evidence = getVerifiedKeyOwnerEvidence(key);
|
|
7682
|
+
if (evidence != null) try {
|
|
7683
|
+
return {
|
|
7684
|
+
kind: "actor",
|
|
7685
|
+
id: parseIri(evidence.ownerId),
|
|
7686
|
+
mechanism: "httpSignatures",
|
|
7687
|
+
keyId: key.id
|
|
7688
|
+
};
|
|
7689
|
+
} catch {
|
|
7690
|
+
return null;
|
|
7691
|
+
}
|
|
7692
|
+
if (key.ownerId == null || isPortableId(key.ownerId)) return null;
|
|
7693
|
+
return {
|
|
7694
|
+
kind: "actor",
|
|
7695
|
+
id: key.ownerId,
|
|
7696
|
+
mechanism: "httpSignatures",
|
|
7697
|
+
keyId: key.id
|
|
7698
|
+
};
|
|
7699
|
+
}
|
|
7700
|
+
/**
|
|
6835
7701
|
* Checks whether an HTTP Signature key is a key of an FEP-ef61 portable
|
|
6836
7702
|
* actor, i.e., its ID is a compatible identifier or an `ap:` or `ap+ef61:`
|
|
6837
7703
|
* URI, and it is owned by a portable actor, which only the portable actor's
|
|
@@ -9332,24 +10198,102 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
9332
10198
|
let dispatched;
|
|
9333
10199
|
let parseInput = void 0;
|
|
9334
10200
|
let parseContextLoader = context.contextLoader;
|
|
10201
|
+
let authentication = getEmptyAuthenticationEvidence();
|
|
10202
|
+
let actorPresence = "present";
|
|
10203
|
+
if (message.verifiedView !== void 0) {
|
|
10204
|
+
const view = parseSerializedVerifiedActivityView(message.verifiedView);
|
|
10205
|
+
let restored;
|
|
10206
|
+
let restoredAuthentication = null;
|
|
10207
|
+
let failure;
|
|
10208
|
+
if (view == null) failure = /* @__PURE__ */ new TypeError("The queued activity has a malformed verified view.");
|
|
10209
|
+
else try {
|
|
10210
|
+
if (view.source === "normalizedActivity") assertSafeJsonLd(message.normalizedActivity);
|
|
10211
|
+
restored = await restoreVerifiedActivityView(view, message);
|
|
10212
|
+
} catch (error) {
|
|
10213
|
+
if (!(error instanceof ContextSnapshotMissError) && !isPermanentInboxParseError(error)) {
|
|
10214
|
+
await handleRetriableFailure(error);
|
|
10215
|
+
return;
|
|
10216
|
+
}
|
|
10217
|
+
failure = error;
|
|
10218
|
+
}
|
|
10219
|
+
if (restored != null) {
|
|
10220
|
+
const expandedActivity = deepFreeze(restored.expanded);
|
|
10221
|
+
restoredAuthentication = message.authentication === void 0 ? createAuthenticationEvidence({
|
|
10222
|
+
...getEmptyAuthenticationEvidence(),
|
|
10223
|
+
expandedActivity,
|
|
10224
|
+
baseUrl: restored.baseUrl
|
|
10225
|
+
}) : deserializeAuthentication(message.authentication, expandedActivity, restored.baseUrl);
|
|
10226
|
+
if (restoredAuthentication == null) failure = /* @__PURE__ */ new TypeError("The queued activity has malformed authentication evidence.");
|
|
10227
|
+
}
|
|
10228
|
+
if (failure != null || restored == null || restoredAuthentication == null) {
|
|
10229
|
+
await reportInboxError(failure);
|
|
10230
|
+
logger.error("Failed to replay the queued incoming activity:\n{error}", {
|
|
10231
|
+
error: failure,
|
|
10232
|
+
trial: message.attempt,
|
|
10233
|
+
activity: message.activity,
|
|
10234
|
+
recipient: message.identifier
|
|
10235
|
+
});
|
|
10236
|
+
listenerSpan.setStatus({
|
|
10237
|
+
code: SpanStatusCode.ERROR,
|
|
10238
|
+
message: String(failure)
|
|
10239
|
+
});
|
|
10240
|
+
listenerSpan.end();
|
|
10241
|
+
return;
|
|
10242
|
+
}
|
|
10243
|
+
authentication = restoredAuthentication;
|
|
10244
|
+
actorPresence = getActorPresence(restored.expanded);
|
|
10245
|
+
const policy = view.policy;
|
|
10246
|
+
const replayLoader = createReleasableLoader(restored.loader, policy === "normalization" ? getNormalizationContextLoader(context.contextLoader) : wrapContextLoaderForJsonLd(context.contextLoader), policy);
|
|
10247
|
+
parseInput = restored.document;
|
|
10248
|
+
parseContextLoader = restored.loader;
|
|
10249
|
+
try {
|
|
10250
|
+
activity = await Activity.fromJsonLd(restored.document, {
|
|
10251
|
+
...context,
|
|
10252
|
+
contextLoader: replayLoader.loader,
|
|
10253
|
+
...restored.baseUrl == null ? {} : { baseUrl: restored.baseUrl }
|
|
10254
|
+
});
|
|
10255
|
+
} catch (error) {
|
|
10256
|
+
if (isPermanentInboxParseError(error)) {
|
|
10257
|
+
await reportInboxError(error);
|
|
10258
|
+
logger.error("Failed to parse the queued incoming activity:\n{error}", {
|
|
10259
|
+
error,
|
|
10260
|
+
trial: message.attempt,
|
|
10261
|
+
activity: message.activity,
|
|
10262
|
+
recipient: message.identifier
|
|
10263
|
+
});
|
|
10264
|
+
listenerSpan.setStatus({
|
|
10265
|
+
code: SpanStatusCode.ERROR,
|
|
10266
|
+
message: String(error)
|
|
10267
|
+
});
|
|
10268
|
+
listenerSpan.end();
|
|
10269
|
+
return;
|
|
10270
|
+
}
|
|
10271
|
+
await handleRetriableFailure(error);
|
|
10272
|
+
return;
|
|
10273
|
+
} finally {
|
|
10274
|
+
replayLoader.release();
|
|
10275
|
+
}
|
|
10276
|
+
}
|
|
9335
10277
|
try {
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
|
|
9346
|
-
|
|
9347
|
-
|
|
9348
|
-
|
|
9349
|
-
|
|
10278
|
+
if (activity == null) {
|
|
10279
|
+
const hasSignatureField = hasSignature(message.activity);
|
|
10280
|
+
const shouldParseFromNormalizedSignedPayload = message.ldSignatureVerified === true || message.normalizedActivity != null || message.ldSignatureVerified == null && hasSignatureField;
|
|
10281
|
+
const parseContext = hasSignatureField ? {
|
|
10282
|
+
...context,
|
|
10283
|
+
contextLoader: getNormalizationContextLoader(context.contextLoader)
|
|
10284
|
+
} : {
|
|
10285
|
+
...context,
|
|
10286
|
+
contextLoader: wrapContextLoaderForJsonLd(context.contextLoader)
|
|
10287
|
+
};
|
|
10288
|
+
parseContextLoader = parseContext.contextLoader;
|
|
10289
|
+
let normalizedActivity;
|
|
10290
|
+
if (shouldParseFromNormalizedSignedPayload) {
|
|
10291
|
+
normalizedActivity = message.normalizedActivity ?? await compactJsonLd(message.activity, context.contextLoader);
|
|
10292
|
+
assertSafeJsonLd(normalizedActivity);
|
|
10293
|
+
}
|
|
10294
|
+
parseInput = shouldParseFromNormalizedSignedPayload ? detachSignature(normalizedActivity) : hasSignatureField ? detachSignature(message.activity) : message.activity;
|
|
10295
|
+
activity = await Activity.fromJsonLd(parseInput, parseContext);
|
|
9350
10296
|
}
|
|
9351
|
-
parseInput = shouldParseFromNormalizedSignedPayload ? detachSignature(normalizedActivity) : hasSignatureField ? detachSignature(message.activity) : message.activity;
|
|
9352
|
-
activity = await Activity.fromJsonLd(parseInput, parseContext);
|
|
9353
10297
|
activityType = getTypeId(activity).href;
|
|
9354
10298
|
span.setAttribute("activitypub.activity.type", activityType);
|
|
9355
10299
|
listenerSpan.setAttribute("activitypub.activity.type", activityType);
|
|
@@ -9358,7 +10302,8 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
9358
10302
|
span.setAttribute("activitypub.activity.id", activity.id.href);
|
|
9359
10303
|
listenerSpan.setAttribute("activitypub.activity.id", activity.id.href);
|
|
9360
10304
|
}
|
|
9361
|
-
|
|
10305
|
+
const actorless = actorPresence === "absent" && !this.skipSignatureVerification && authentication.verified && this.inboxListeners?.dispatchWithClass(activity)?.options.allowActorless === true && isActorlessActivityAuthenticated(activity, authentication);
|
|
10306
|
+
if (activity.actorId == null && !actorless) {
|
|
9362
10307
|
logger.error("Missing actor in queued incoming activity.", {
|
|
9363
10308
|
activityId: activity.id?.href,
|
|
9364
10309
|
activity: message.activity,
|
|
@@ -9372,7 +10317,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
9372
10317
|
listenerSpan.end();
|
|
9373
10318
|
return;
|
|
9374
10319
|
}
|
|
9375
|
-
if (!this.skipSignatureVerification && activity.id != null && !actorsHaveSameOrigin(activity)) {
|
|
10320
|
+
if (!this.skipSignatureVerification && !actorless && activity.id != null && !actorsHaveSameOrigin(activity)) {
|
|
9376
10321
|
logger.error("The queued activity ({activityId}) and its actors do not have the same origin.", {
|
|
9377
10322
|
activityId: activity.id.href,
|
|
9378
10323
|
actorIds: activity.actorIds.map((actor) => actor.href),
|
|
@@ -9387,22 +10332,18 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
9387
10332
|
listenerSpan.end();
|
|
9388
10333
|
return;
|
|
9389
10334
|
}
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
|
|
9393
|
-
|
|
9394
|
-
|
|
9395
|
-
|
|
9396
|
-
|
|
9397
|
-
|
|
9398
|
-
|
|
9399
|
-
|
|
9400
|
-
|
|
9401
|
-
|
|
9402
|
-
recordInboxActivity(this.meterProvider, "rejected", activityType);
|
|
9403
|
-
listenerSpan.end();
|
|
9404
|
-
return;
|
|
9405
|
-
}
|
|
10335
|
+
const parsedActivity = activity;
|
|
10336
|
+
const parsedActivityType = activityType;
|
|
10337
|
+
cacheKey = await getInboxIdempotencyKey(this.idempotencyStrategy, () => context.toInboxContext(message.identifier, message.activity, parsedActivity.id?.href, parsedActivityType, { authentication }), activity, context.origin, message.identifier, this.kvPrefixes.activityIdempotence);
|
|
10338
|
+
if (await isInboxActivityProcessed(this.kv, cacheKey, activity, context.origin, this.kvPrefixes.activityIdempotence)) {
|
|
10339
|
+
logger.debug("Activity {activityId} has already been processed.", {
|
|
10340
|
+
activityId: activity.id?.href,
|
|
10341
|
+
activity: message.activity,
|
|
10342
|
+
recipient: message.identifier
|
|
10343
|
+
});
|
|
10344
|
+
recordInboxActivity(this.meterProvider, "rejected", activityType);
|
|
10345
|
+
listenerSpan.end();
|
|
10346
|
+
return;
|
|
9406
10347
|
}
|
|
9407
10348
|
dispatched = this.inboxListeners?.dispatchWithClass(activity);
|
|
9408
10349
|
} catch (error) {
|
|
@@ -9461,7 +10402,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
9461
10402
|
try {
|
|
9462
10403
|
const started = performance.now();
|
|
9463
10404
|
try {
|
|
9464
|
-
await listener(context.toInboxContext(message.identifier, message.activity, activity.id?.href, activityType), activity);
|
|
10405
|
+
await listener(context.toInboxContext(message.identifier, message.activity, activity.id?.href, activityType, { authentication }), activity);
|
|
9465
10406
|
} finally {
|
|
9466
10407
|
this.metrics.recordInboxProcessingDuration(activityType, getDurationMs(started));
|
|
9467
10408
|
}
|
|
@@ -10768,7 +11709,7 @@ var ContextImpl = class ContextImpl {
|
|
|
10768
11709
|
invokedFromActorKeyPairsDispatcher: this.invokedFromActorKeyPairsDispatcher
|
|
10769
11710
|
});
|
|
10770
11711
|
}
|
|
10771
|
-
toInboxContext(recipient, activity, activityId, activityType) {
|
|
11712
|
+
toInboxContext(recipient, activity, activityId, activityType, metadata) {
|
|
10772
11713
|
return new InboxContextImpl(recipient, activity, activityId, activityType, {
|
|
10773
11714
|
url: this.url,
|
|
10774
11715
|
federation: this.federation,
|
|
@@ -10776,7 +11717,7 @@ var ContextImpl = class ContextImpl {
|
|
|
10776
11717
|
documentLoader: this.documentLoader,
|
|
10777
11718
|
contextLoader: this.contextLoader,
|
|
10778
11719
|
invokedFromActorKeyPairsDispatcher: this.invokedFromActorKeyPairsDispatcher
|
|
10779
|
-
});
|
|
11720
|
+
}, metadata?.authentication);
|
|
10780
11721
|
}
|
|
10781
11722
|
toOutboxContext(identifier, activity, activityId, activityType) {
|
|
10782
11723
|
return new OutboxContextImpl(identifier, activity, activityId, activityType, {
|
|
@@ -11158,7 +12099,8 @@ var ContextImpl = class ContextImpl {
|
|
|
11158
12099
|
userAgent: options.userAgent ?? this.federation.userAgent,
|
|
11159
12100
|
tracerProvider: options.tracerProvider ?? this.tracerProvider,
|
|
11160
12101
|
meterProvider: options.meterProvider ?? this.federation._meterProvider,
|
|
11161
|
-
allowPrivateAddress: this.federation.allowPrivateAddress
|
|
12102
|
+
allowPrivateAddress: this.federation.allowPrivateAddress,
|
|
12103
|
+
withDiagnostics: false
|
|
11162
12104
|
});
|
|
11163
12105
|
}
|
|
11164
12106
|
async enqueueTask(task, data, options = {}) {
|
|
@@ -11390,12 +12332,20 @@ var ContextImpl = class ContextImpl {
|
|
|
11390
12332
|
});
|
|
11391
12333
|
}
|
|
11392
12334
|
async routeActivityInternal(recipient, activity, options = {}, span) {
|
|
12335
|
+
const snapshot = createContextSnapshot(options.contextLoader ?? this.contextLoader);
|
|
12336
|
+
try {
|
|
12337
|
+
return await this.#routeActivityWithSnapshot(recipient, activity, options, span, snapshot);
|
|
12338
|
+
} finally {
|
|
12339
|
+
snapshot.release();
|
|
12340
|
+
}
|
|
12341
|
+
}
|
|
12342
|
+
async #routeActivityWithSnapshot(recipient, activity, options, span, snapshot) {
|
|
11393
12343
|
const logger = getLogger([
|
|
11394
12344
|
"fedify",
|
|
11395
12345
|
"federation",
|
|
11396
12346
|
"inbox"
|
|
11397
12347
|
]);
|
|
11398
|
-
const contextLoader =
|
|
12348
|
+
const contextLoader = snapshot.loader("standard");
|
|
11399
12349
|
let json = await activity.toJsonLd({ contextLoader });
|
|
11400
12350
|
const keyCache = new KvKeyCache(this.federation.kv, this.federation.kvPrefixes.publicKey, {
|
|
11401
12351
|
documentLoader: this.documentLoader,
|
|
@@ -11403,13 +12353,19 @@ var ContextImpl = class ContextImpl {
|
|
|
11403
12353
|
tracerProvider: this.tracerProvider,
|
|
11404
12354
|
keyTtl: this.federation.publicKeyTtl
|
|
11405
12355
|
});
|
|
11406
|
-
|
|
12356
|
+
let documentSigners = [];
|
|
12357
|
+
let originAuthenticated = [];
|
|
12358
|
+
let viewSource;
|
|
12359
|
+
let fetchedView;
|
|
12360
|
+
const document = detachSignature(json);
|
|
12361
|
+
const verified = await verifyObjectDetailed(Activity, document, {
|
|
11407
12362
|
contextLoader,
|
|
11408
12363
|
documentLoader: options.documentLoader ?? this.documentLoader,
|
|
11409
12364
|
meterProvider: this.meterProvider,
|
|
11410
12365
|
tracerProvider: options.tracerProvider ?? this.tracerProvider,
|
|
11411
12366
|
keyCache
|
|
11412
|
-
})
|
|
12367
|
+
});
|
|
12368
|
+
if (verified.status !== "verified") {
|
|
11413
12369
|
logger.debug("Object Integrity Proofs are not verified.", {
|
|
11414
12370
|
recipient,
|
|
11415
12371
|
activity: json
|
|
@@ -11421,8 +12377,29 @@ var ContextImpl = class ContextImpl {
|
|
|
11421
12377
|
});
|
|
11422
12378
|
return false;
|
|
11423
12379
|
}
|
|
11424
|
-
|
|
11425
|
-
|
|
12380
|
+
let candidate;
|
|
12381
|
+
let candidateView;
|
|
12382
|
+
const verifications = /* @__PURE__ */ new WeakMap();
|
|
12383
|
+
const fetched = await this.lookupObject(activity.id, {
|
|
12384
|
+
...options,
|
|
12385
|
+
contextLoader,
|
|
12386
|
+
verifyPortableObject: async (document, verifierOptions) => {
|
|
12387
|
+
const result = await this.verifyPortableObject(document, verifierOptions);
|
|
12388
|
+
if (document != null && typeof document === "object") verifications.set(document, result);
|
|
12389
|
+
return result;
|
|
12390
|
+
},
|
|
12391
|
+
[lookupObserver]: { accept: async (accepted) => {
|
|
12392
|
+
candidate = accepted;
|
|
12393
|
+
candidateView = await buildVerifiedActivityView(createContextSnapshot(accepted.contextLoader), {
|
|
12394
|
+
policy: "standard",
|
|
12395
|
+
source: "activity",
|
|
12396
|
+
document: detachSignature(accepted.document),
|
|
12397
|
+
baseUrl: accepted.baseUrl
|
|
12398
|
+
});
|
|
12399
|
+
return true;
|
|
12400
|
+
} }
|
|
12401
|
+
});
|
|
12402
|
+
if (fetched == null || candidate == null || candidateView == null) {
|
|
11426
12403
|
logger.debug("Failed to fetch the remote activity object {activityId}.", {
|
|
11427
12404
|
recipient,
|
|
11428
12405
|
activity: json,
|
|
@@ -11458,11 +12435,50 @@ var ContextImpl = class ContextImpl {
|
|
|
11458
12435
|
}
|
|
11459
12436
|
logger.debug("Successfully fetched the remote activity object {activityId}; ignore the original activity and use the fetched one, which is trustworthy.");
|
|
11460
12437
|
activity = fetched;
|
|
11461
|
-
json =
|
|
11462
|
-
|
|
11463
|
-
|
|
11464
|
-
|
|
11465
|
-
|
|
12438
|
+
json = candidate.document;
|
|
12439
|
+
fetchedView = candidateView;
|
|
12440
|
+
viewSource = {
|
|
12441
|
+
policy: "standard",
|
|
12442
|
+
source: "activity",
|
|
12443
|
+
document: detachSignature(candidate.document),
|
|
12444
|
+
baseUrl: candidate.baseUrl
|
|
12445
|
+
};
|
|
12446
|
+
if (isPortableId(fetched.id)) {
|
|
12447
|
+
const result = verifications.get(candidate.document);
|
|
12448
|
+
if (result?.verified === true && result.method === "proof") documentSigners = result.keys.flatMap((key) => {
|
|
12449
|
+
const signer = getProofSigner(key);
|
|
12450
|
+
return signer == null ? [] : [{
|
|
12451
|
+
kind: signer.kind,
|
|
12452
|
+
id: signer.id,
|
|
12453
|
+
mechanism: "objectIntegrityProofs",
|
|
12454
|
+
keyId: signer.keyId
|
|
12455
|
+
}];
|
|
12456
|
+
});
|
|
12457
|
+
} else originAuthenticated = fetched.actorIds.map((actorId) => ({
|
|
12458
|
+
kind: "actor",
|
|
12459
|
+
id: actorId,
|
|
12460
|
+
mechanism: "origin",
|
|
12461
|
+
keyId: null
|
|
12462
|
+
}));
|
|
12463
|
+
} else {
|
|
12464
|
+
logger.debug("Object Integrity Proofs are verified.", {
|
|
12465
|
+
recipient,
|
|
12466
|
+
activity: json
|
|
12467
|
+
});
|
|
12468
|
+
activity = verified.object;
|
|
12469
|
+
documentSigners = verified.signers.map((signer) => ({
|
|
12470
|
+
kind: signer.kind,
|
|
12471
|
+
id: signer.id,
|
|
12472
|
+
mechanism: "objectIntegrityProofs",
|
|
12473
|
+
keyId: signer.keyId
|
|
12474
|
+
}));
|
|
12475
|
+
viewSource = {
|
|
12476
|
+
policy: "standard",
|
|
12477
|
+
source: "activity",
|
|
12478
|
+
document,
|
|
12479
|
+
baseUrl: null
|
|
12480
|
+
};
|
|
12481
|
+
}
|
|
11466
12482
|
if (activity.actorId == null) {
|
|
11467
12483
|
logger.debug("Activity is missing an actor; unable to verify.", {
|
|
11468
12484
|
recipient,
|
|
@@ -11477,10 +12493,21 @@ var ContextImpl = class ContextImpl {
|
|
|
11477
12493
|
});
|
|
11478
12494
|
return false;
|
|
11479
12495
|
}
|
|
12496
|
+
const view = fetchedView ?? await buildVerifiedActivityView(snapshot, viewSource);
|
|
11480
12497
|
const routeResult = await routeActivity({
|
|
11481
12498
|
context: this,
|
|
11482
12499
|
json,
|
|
11483
12500
|
ldSignatureVerified: false,
|
|
12501
|
+
authentication: createAuthenticationEvidence({
|
|
12502
|
+
verified: true,
|
|
12503
|
+
requestSigners: [],
|
|
12504
|
+
documentSigners,
|
|
12505
|
+
originAuthenticated,
|
|
12506
|
+
expandedActivity: deepFreeze(view.expanded),
|
|
12507
|
+
baseUrl: viewSource.baseUrl
|
|
12508
|
+
}),
|
|
12509
|
+
verifiedView: view.view,
|
|
12510
|
+
replayable: view.replayable,
|
|
11484
12511
|
activity,
|
|
11485
12512
|
recipient,
|
|
11486
12513
|
inboxListeners: this.federation.inboxListeners,
|
|
@@ -11831,12 +12858,14 @@ var InboxContextImpl = class InboxContextImpl extends ContextImpl {
|
|
|
11831
12858
|
activity;
|
|
11832
12859
|
activityId;
|
|
11833
12860
|
activityType;
|
|
11834
|
-
|
|
12861
|
+
authentication;
|
|
12862
|
+
constructor(recipient, activity, activityId, activityType, options, authentication) {
|
|
11835
12863
|
super(options);
|
|
11836
12864
|
this.recipient = recipient;
|
|
11837
12865
|
this.activity = activity;
|
|
11838
12866
|
this.activityId = activityId;
|
|
11839
12867
|
this.activityType = activityType;
|
|
12868
|
+
this.authentication = authentication ?? getEmptyAuthenticationEvidence();
|
|
11840
12869
|
}
|
|
11841
12870
|
clone(data) {
|
|
11842
12871
|
return new InboxContextImpl(this.recipient, this.activity, this.activityId, this.activityType, {
|
|
@@ -11846,7 +12875,7 @@ var InboxContextImpl = class InboxContextImpl extends ContextImpl {
|
|
|
11846
12875
|
documentLoader: this.documentLoader,
|
|
11847
12876
|
contextLoader: this.contextLoader,
|
|
11848
12877
|
invokedFromActorKeyPairsDispatcher: this.invokedFromActorKeyPairsDispatcher
|
|
11849
|
-
});
|
|
12878
|
+
}, this.authentication);
|
|
11850
12879
|
}
|
|
11851
12880
|
forwardActivity(forwarder, recipients, options) {
|
|
11852
12881
|
return forwardActivity(this, "inbox", forwarder, recipients, options).then(() => void 0);
|
|
@@ -11962,4 +12991,4 @@ function getRequestId(request) {
|
|
|
11962
12991
|
return `req_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
11963
12992
|
}
|
|
11964
12993
|
//#endregion
|
|
11965
|
-
export { respondWithObjectIfAcceptable as a,
|
|
12994
|
+
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 };
|