@fedify/fedify 2.5.0-dev.2446 → 2.5.0-dev.2492

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