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