@fedify/fedify 2.3.0-dev.1145 → 2.3.0-dev.1150

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 (68) hide show
  1. package/dist/{builder-ShiR1K6b.mjs → builder-Bjm1Jq9n.mjs} +2 -2
  2. package/dist/compat/mod.d.cts +1 -1
  3. package/dist/compat/mod.d.ts +1 -1
  4. package/dist/compat/transformers.test.mjs +1 -1
  5. package/dist/{context-DI2gRbyN.d.cts → context-CRXCkTM6.d.cts} +48 -6
  6. package/dist/{context-DCtsSHDv.d.ts → context-MgCh7YGu.d.ts} +48 -6
  7. package/dist/{deno-h0TWFuEz.mjs → deno-CKFE6Uya.mjs} +1 -1
  8. package/dist/{docloader-BdDN0Aqx.mjs → docloader-B-ZE1cZf.mjs} +2 -2
  9. package/dist/federation/builder.test.mjs +1 -1
  10. package/dist/federation/handler.test.mjs +1363 -44
  11. package/dist/federation/idempotency.test.mjs +2 -2
  12. package/dist/federation/metrics.test.mjs +1 -1
  13. package/dist/federation/middleware.test.mjs +1667 -163
  14. package/dist/federation/mod.cjs +1 -1
  15. package/dist/federation/mod.d.cts +2 -2
  16. package/dist/federation/mod.d.ts +2 -2
  17. package/dist/federation/mod.js +1 -1
  18. package/dist/federation/retry.test.mjs +1 -1
  19. package/dist/federation/send.test.mjs +8 -8
  20. package/dist/federation/temporal.test.d.mts +2 -0
  21. package/dist/federation/temporal.test.mjs +71 -0
  22. package/dist/federation/webfinger.test.mjs +1 -1
  23. package/dist/{getMachineId-bsd-etIyxDet.mjs → getMachineId-bsd-BY01PL1n.mjs} +1 -1
  24. package/dist/{getMachineId-darwin-D23zTf4g.mjs → getMachineId-darwin-Dr1gkBkp.mjs} +1 -1
  25. package/dist/{getMachineId-win-Dpap6v5i.mjs → getMachineId-win-QEYwcJiy.mjs} +1 -1
  26. package/dist/{http-7kAB7PVx.cjs → http-DQYEA7AZ.cjs} +1 -1
  27. package/dist/{http-B2hxA7dO.js → http-WbS1gKzr.js} +1 -1
  28. package/dist/{http-QzW9IWfs.mjs → http-vHCgbhTg.mjs} +3 -3
  29. package/dist/{key-Dh2OK1XQ.mjs → key-N0zP_oJA.mjs} +2 -2
  30. package/dist/{kv-cache-b22dNkjt.js → kv-cache-DM2O-Yjy.js} +1 -1
  31. package/dist/{kv-cache-DCPp-MT0.cjs → kv-cache-Dsg_bi4N.cjs} +1 -1
  32. package/dist/{kv-cache-EZRIPZXD.mjs → kv-cache-GXXZEemD.mjs} +1 -1
  33. package/dist/{ld-eZbar1rr.mjs → ld-BwKhquPx.mjs} +302 -6
  34. package/dist/{metrics-E0hAHtLZ.mjs → metrics-7Vy9FvEw.mjs} +1 -1
  35. package/dist/{middleware-BrGIM_Ra.js → middleware-BscgvU-m.js} +428 -99
  36. package/dist/{middleware-BUl1BH4x.cjs → middleware-D_iXrYHJ.cjs} +429 -99
  37. package/dist/{middleware-mToCR2tG.mjs → middleware-Db1_qAFG.mjs} +1 -1
  38. package/dist/{middleware-CyJDCmNg.mjs → middleware-ZuUcO0t1.mjs} +348 -108
  39. package/dist/{mod-CI9fduEi.d.cts → mod-C7HOzGqH.d.cts} +1 -1
  40. package/dist/{mod-CkRiJHGA.d.ts → mod-CpQHB3Ys.d.ts} +1 -1
  41. package/dist/mod.cjs +4 -4
  42. package/dist/mod.d.cts +2 -2
  43. package/dist/mod.d.ts +2 -2
  44. package/dist/mod.js +4 -4
  45. package/dist/nodeinfo/handler.test.mjs +1 -1
  46. package/dist/{owner-ByO_Fw6U.mjs → owner-FD0H_vpj.mjs} +2 -2
  47. package/dist/{proof-jVqClF49.cjs → proof-CYK8T8IS.cjs} +353 -3
  48. package/dist/{proof-BkRyFchv.js → proof-I3EokKN-.js} +300 -4
  49. package/dist/{proof-CSo0S8OK.mjs → proof-V_lafPmA.mjs} +3 -3
  50. package/dist/{send-jzrTV1FU.mjs → send-Cc2_10tF.mjs} +3 -3
  51. package/dist/sig/http.test.mjs +2 -2
  52. package/dist/sig/key.test.mjs +1 -1
  53. package/dist/sig/ld.test.mjs +558 -2
  54. package/dist/sig/mod.cjs +2 -2
  55. package/dist/sig/mod.js +2 -2
  56. package/dist/sig/owner.test.mjs +1 -1
  57. package/dist/sig/proof.test.mjs +1 -1
  58. package/dist/temporal-BkmBfs__.mjs +95 -0
  59. package/dist/testing/mod.d.mts +48 -6
  60. package/dist/utils/docloader.test.mjs +2 -2
  61. package/dist/utils/kv-cache.test.mjs +1 -1
  62. package/dist/utils/mod.cjs +1 -1
  63. package/dist/utils/mod.js +1 -1
  64. package/package.json +7 -7
  65. /package/dist/{execAsync-DCBrgFiV.mjs → execAsync-Dxb7rNf3.mjs} +0 -0
  66. /package/dist/{getMachineId-linux-ObI47Hql.mjs → getMachineId-linux-Bbhofx-s.mjs} +0 -0
  67. /package/dist/{getMachineId-unsupported-Ddu-PFeh.mjs → getMachineId-unsupported-dIOte2Ct.mjs} +0 -0
  68. /package/dist/{retry-v_sGLH1d.mjs → retry-_VvV0h9f.mjs} +0 -0
@@ -1,25 +1,26 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-h0TWFuEz.mjs";
5
- import { a as instrumentDocumentLoader, d as recordInboxActivity, h as recordWebFingerHandle, i as getRemoteHost, m as recordOutboxEnqueue, n as getDurationMs, o as isAbortError, p as recordOutboxActivity, r as getFederationMetrics, u as recordFanoutRecipients } from "./metrics-E0hAHtLZ.mjs";
4
+ import { n as version, t as name } from "./deno-CKFE6Uya.mjs";
5
+ import { a as instrumentDocumentLoader, d as recordInboxActivity, h as recordWebFingerHandle, i as getRemoteHost, m as recordOutboxEnqueue, n as getDurationMs, o as isAbortError, p as recordOutboxActivity, r as getFederationMetrics, u as recordFanoutRecipients } from "./metrics-7Vy9FvEw.mjs";
6
6
  import { t as formatAcceptSignature } from "./accept-CceiKpCy.mjs";
7
- import { a as importJwk, o as validateCryptoKey, t as exportJwk } from "./key-Dh2OK1XQ.mjs";
8
- import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-QzW9IWfs.mjs";
9
- import { t as getAuthenticatedDocumentLoader } from "./docloader-BdDN0Aqx.mjs";
10
- import { n as kvCache } from "./kv-cache-EZRIPZXD.mjs";
11
- import { a as signJsonLd, i as hasSignatureLike, o as verifyJsonLd, r as detachSignature } from "./ld-eZbar1rr.mjs";
12
- import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-ByO_Fw6U.mjs";
7
+ import { a as importJwk, o as validateCryptoKey, t as exportJwk } from "./key-N0zP_oJA.mjs";
8
+ import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-vHCgbhTg.mjs";
9
+ import { t as getAuthenticatedDocumentLoader } from "./docloader-B-ZE1cZf.mjs";
10
+ import { n as kvCache } from "./kv-cache-GXXZEemD.mjs";
11
+ import { _ as wrapContextLoaderForJsonLd, a as compactJsonLd, c as getNormalizationContextLoader, d as isClearlyMalformedContextReference, f as isInvalidUrlTypeError, l as hasSignature, m as verifyCompactJsonLd, p as signJsonLd, r as assertSafeJsonLd, s as detachSignature, t as InvalidContextReferenceError, u as hasSignatureLike } from "./ld-BwKhquPx.mjs";
12
+ import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-FD0H_vpj.mjs";
13
13
  import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-BgFLCJQ_.mjs";
14
- import { i as verifyObject, n as hasProofLike, r as signObject } from "./proof-CSo0S8OK.mjs";
14
+ import { i as verifyObject, n as hasProofLike, r as signObject } from "./proof-V_lafPmA.mjs";
15
15
  import { t as getNodeInfo } from "./client-B_A6mfn3.mjs";
16
16
  import { t as nodeInfoToJson } from "./types-BFowWFTT.mjs";
17
- import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-ShiR1K6b.mjs";
17
+ import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-Bjm1Jq9n.mjs";
18
18
  import { t as buildCollectionSynchronizationHeader } from "./collection-CA3V5zyK.mjs";
19
19
  import { t as KvKeyCache } from "./keycache-BYMd8q7F.mjs";
20
20
  import { t as acceptsJsonLd } from "./negotiation-CDW-_gUU.mjs";
21
- import { t as createExponentialBackoffPolicy } from "./retry-v_sGLH1d.mjs";
22
- import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-jzrTV1FU.mjs";
21
+ import { t as hasMalformedKnownTemporalLiteral } from "./temporal-BkmBfs__.mjs";
22
+ import { t as createExponentialBackoffPolicy } from "./retry-_VvV0h9f.mjs";
23
+ import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-Cc2_10tF.mjs";
23
24
  import { getLogger, withContext } from "@logtape/logtape";
24
25
  import { RouterError } from "@fedify/uri-template";
25
26
  import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
@@ -155,7 +156,7 @@ function handleNodeInfoJrd(_request, context) {
155
156
  }
156
157
  //#endregion
157
158
  //#region src/federation/inbox.ts
158
- async function routeActivity({ context: ctx, json, activity, recipient, inboxListeners, inboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, meterProvider, tracerProvider, idempotencyStrategy }) {
159
+ async function routeActivity({ context: ctx, json, originalJson, normalizedActivity, ldSignatureVerified, activity, recipient, inboxListeners, inboxContextFactory, listenerInboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, meterProvider, tracerProvider, idempotencyStrategy }) {
159
160
  const logger = getLogger([
160
161
  "fedify",
161
162
  "federation",
@@ -214,7 +215,9 @@ async function routeActivity({ context: ctx, json, activity, recipient, inboxLis
214
215
  type: "inbox",
215
216
  id: crypto.randomUUID(),
216
217
  baseUrl: ctx.origin,
217
- activity: json,
218
+ activity: originalJson ?? json,
219
+ ...normalizedActivity == null ? {} : { normalizedActivity },
220
+ ...ldSignatureVerified == null ? {} : { ldSignatureVerified },
218
221
  identifier: recipient,
219
222
  attempt: 0,
220
223
  started: (/* @__PURE__ */ new Date()).toISOString(),
@@ -268,7 +271,8 @@ async function routeActivity({ context: ctx, json, activity, recipient, inboxLis
268
271
  const activityType = getTypeId(activity).href;
269
272
  const started = performance.now();
270
273
  try {
271
- await listener(inboxContextFactory(recipient, json, activity.id?.href, activityType), activity);
274
+ const contextFactory = listenerInboxContextFactory ?? inboxContextFactory;
275
+ await listener(contextFactory(recipient, contextFactory === inboxContextFactory ? json : originalJson ?? json, activity.id?.href, activityType), activity);
272
276
  } finally {
273
277
  getFederationMetrics(meterProvider).recordInboxProcessingDuration(activityType, getDurationMs(started));
274
278
  }
@@ -310,6 +314,33 @@ async function routeActivity({ context: ctx, json, activity, recipient, inboxLis
310
314
  }
311
315
  //#endregion
312
316
  //#region src/federation/handler.ts
317
+ const rawInboxContextFactorySymbol = Symbol("fedify.rawInboxContextFactory");
318
+ function isRemoteContextLoadingFailure$1(error) {
319
+ return error instanceof Error && typeof error.details === "object" && error.details != null && error.details.code === "loading remote context failed";
320
+ }
321
+ function isPermanentRemoteContextError$1(error) {
322
+ if (!(error instanceof Error) || error.name !== "jsonld.InvalidUrl") return false;
323
+ const details = error.details;
324
+ if (details?.code === "invalid remote context") return true;
325
+ return isRemoteContextLoadingFailure$1(error) && typeof details?.url === "string" && !URL.canParse(details.url) && isClearlyMalformedContextReference(details.url);
326
+ }
327
+ function isInvalidJsonLdError(error) {
328
+ if (!(error instanceof Error)) return false;
329
+ const name = error.name;
330
+ return name === "UnsafeJsonLdError" || error instanceof InvalidContextReferenceError || isPermanentRemoteContextError$1(error) || name === "jsonld.SyntaxError" && !isRemoteContextLoadingFailure$1(error);
331
+ }
332
+ function isValidationTypeError(error) {
333
+ return error instanceof TypeError && (/^(Invalid JSON-LD:|Invalid type:|Unexpected type:)/.test(error.message) || isInvalidUrlTypeError(error));
334
+ }
335
+ function isPermanentActivityParseError(error) {
336
+ return isInvalidJsonLdError(error) || isValidationTypeError(error);
337
+ }
338
+ function hasHttpSignatureHeaders(request) {
339
+ return request.headers.has("Signature") || request.headers.has("Signature-Input");
340
+ }
341
+ function hasObjectIntegrityProof(json) {
342
+ return typeof json === "object" && json != null && "proof" in json;
343
+ }
313
344
  /**
314
345
  * Handles an actor request.
315
346
  * @template TContextData The context data to pass to the context.
@@ -381,8 +412,8 @@ async function handleObject(request, { values, context, objectDispatcher, author
381
412
  * @param parameters The parameters for handling the collection.
382
413
  * @returns A promise that resolves to an HTTP response.
383
414
  */
384
- async function handleCollection(request, { name: name$2, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, onUnauthorized, onNotFound }) {
385
- const spanName = name$2.trim().replace(/\s+/g, "_");
415
+ async function handleCollection(request, { name: name$1, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, onUnauthorized, onNotFound }) {
416
+ const spanName = name$1.trim().replace(/\s+/g, "_");
386
417
  tracerProvider = tracerProvider ?? trace.getTracerProvider();
387
418
  const tracer = tracerProvider.getTracer(name, version);
388
419
  const cursor = new URL(request.url).searchParams.get("cursor");
@@ -424,7 +455,7 @@ async function handleCollection(request, { name: name$2, identifier, uriGetter,
424
455
  collection = new OrderedCollection({
425
456
  id: baseUri,
426
457
  totalItems: totalItems == null ? null : Number(totalItems),
427
- items: filterCollectionItems(itemsOrResponse, name$2, filterPredicate)
458
+ items: filterCollectionItems(itemsOrResponse, name$1, filterPredicate)
428
459
  });
429
460
  } else {
430
461
  const lastCursor = await collectionCallbacks.lastCursor?.(context, identifier);
@@ -445,7 +476,7 @@ async function handleCollection(request, { name: name$2, identifier, uriGetter,
445
476
  } else {
446
477
  const uri = new URL(baseUri);
447
478
  uri.searchParams.set("cursor", cursor);
448
- const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$2}`, {
479
+ const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$1}`, {
449
480
  kind: SpanKind.SERVER,
450
481
  attributes: {
451
482
  "activitypub.collection.id": uri.href,
@@ -489,7 +520,7 @@ async function handleCollection(request, { name: name$2, identifier, uriGetter,
489
520
  id: uri,
490
521
  prev,
491
522
  next,
492
- items: filterCollectionItems(items, name$2, filterPredicate),
523
+ items: filterCollectionItems(items, name$1, filterPredicate),
493
524
  partOf
494
525
  });
495
526
  }
@@ -836,29 +867,105 @@ async function handleInboxInternal(request, parameters, span) {
836
867
  });
837
868
  }
838
869
  const keyCache = new KvKeyCache(kv, kvPrefixes.publicKey, ctx);
839
- let ldSigVerified;
840
- try {
841
- ldSigVerified = await verifyJsonLd(json, {
842
- contextLoader: ctx.contextLoader,
843
- documentLoader: ctx.documentLoader,
844
- keyCache,
845
- meterProvider,
846
- tracerProvider
870
+ const jsonWithoutSig = detachSignature(json);
871
+ const hasLdSignature = hasSignature(json);
872
+ const canAttemptAlternateAuthAfterLdSignatureFailure = skipSignatureVerification || hasHttpSignatureHeaders(request) || hasObjectIntegrityProof(jsonWithoutSig);
873
+ let deferredLdSignatureError = void 0;
874
+ const respondInvalidActivity = async (error) => {
875
+ logger.error("Failed to parse activity:\n{error}", {
876
+ recipient,
877
+ activity: json,
878
+ error
847
879
  });
848
- } catch (error) {
849
- if (error instanceof Error && error.name === "jsonld.SyntaxError") {
850
- logger.error("Failed to parse JSON-LD:\n{error}", {
880
+ try {
881
+ await inboxErrorHandler?.(ctx, error);
882
+ } catch (error) {
883
+ logger.error("An unexpected error occurred in inbox error handler:\n{error}", {
884
+ error,
885
+ activity: json,
886
+ recipient
887
+ });
888
+ }
889
+ span.setStatus({
890
+ code: SpanStatusCode.ERROR,
891
+ message: `Failed to parse activity:\n${error}`
892
+ });
893
+ return new Response("Invalid activity.", {
894
+ status: 400,
895
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
896
+ });
897
+ };
898
+ let compactedJson = json;
899
+ let compactedJsonWithoutSig = jsonWithoutSig;
900
+ let ldSigVerified = false;
901
+ if (hasLdSignature) {
902
+ try {
903
+ compactedJson = await compactJsonLd(json, ctx.contextLoader);
904
+ } catch (error) {
905
+ if (isInvalidJsonLdError(error)) {
906
+ logger.error("Failed to parse JSON-LD:\n{error}", {
907
+ recipient,
908
+ error
909
+ });
910
+ return new Response("Invalid JSON-LD.", {
911
+ status: 400,
912
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
913
+ });
914
+ }
915
+ if (!canAttemptAlternateAuthAfterLdSignatureFailure) throw error;
916
+ if (!skipSignatureVerification) deferredLdSignatureError = error;
917
+ logger.debug("Failed to normalize JSON-LD for Linked Data Signatures; deferring to another authentication path only if it verifies:\n{error}", {
851
918
  recipient,
852
919
  error
853
920
  });
854
- return new Response("Invalid JSON-LD.", {
855
- status: 400,
856
- headers: { "Content-Type": "text/plain; charset=utf-8" }
857
- });
858
921
  }
859
- ldSigVerified = false;
922
+ if (compactedJson !== json) {
923
+ compactedJsonWithoutSig = detachSignature(compactedJson);
924
+ try {
925
+ ldSigVerified = await verifyCompactJsonLd(compactedJson, {
926
+ contextLoader: ctx.contextLoader,
927
+ documentLoader: ctx.documentLoader,
928
+ keyCache,
929
+ meterProvider,
930
+ tracerProvider
931
+ });
932
+ } catch (error) {
933
+ if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig, ctx.contextLoader)) return await respondInvalidActivity(error);
934
+ if (isInvalidJsonLdError(error)) {
935
+ logger.error("Failed to parse JSON-LD:\n{error}", {
936
+ recipient,
937
+ error
938
+ });
939
+ return new Response("Invalid JSON-LD.", {
940
+ status: 400,
941
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
942
+ });
943
+ }
944
+ if (!canAttemptAlternateAuthAfterLdSignatureFailure) throw error;
945
+ if (!skipSignatureVerification) try {
946
+ await Object$1.fromJsonLd(compactedJson, {
947
+ contextLoader: getNormalizationContextLoader(ctx.contextLoader),
948
+ documentLoader: ctx.documentLoader,
949
+ tracerProvider
950
+ });
951
+ } catch (parseError) {
952
+ if (parseError instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig, ctx.contextLoader)) return await respondInvalidActivity(parseError);
953
+ if (isInvalidJsonLdError(parseError)) {
954
+ logger.error("Failed to parse JSON-LD:\n{error}", {
955
+ recipient,
956
+ error: parseError
957
+ });
958
+ return new Response("Invalid JSON-LD.", {
959
+ status: 400,
960
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
961
+ });
962
+ }
963
+ deferredLdSignatureError = parseError;
964
+ }
965
+ ldSigVerified = false;
966
+ }
967
+ }
860
968
  }
861
- const jsonWithoutSig = detachSignature(json);
862
969
  let activity = null;
863
970
  let activityVerified = false;
864
971
  if (ldSigVerified) {
@@ -866,7 +973,16 @@ async function handleInboxInternal(request, parameters, span) {
866
973
  recipient,
867
974
  json
868
975
  });
869
- activity = await Activity.fromJsonLd(jsonWithoutSig, ctx);
976
+ try {
977
+ activity = await Activity.fromJsonLd(compactedJsonWithoutSig, {
978
+ ...ctx,
979
+ contextLoader: getNormalizationContextLoader(ctx.contextLoader)
980
+ });
981
+ } catch (error) {
982
+ if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(compactedJsonWithoutSig, ctx.contextLoader)) return await respondInvalidActivity(error);
983
+ if (!isPermanentActivityParseError(error)) throw error;
984
+ return await respondInvalidActivity(error);
985
+ }
870
986
  activityVerified = true;
871
987
  } else {
872
988
  logger.debug("Linked Data Signatures are not verified.", {
@@ -875,13 +991,22 @@ async function handleInboxInternal(request, parameters, span) {
875
991
  });
876
992
  try {
877
993
  activity = await verifyObject(Activity, jsonWithoutSig, {
878
- contextLoader: ctx.contextLoader,
994
+ contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
879
995
  documentLoader: ctx.documentLoader,
880
996
  keyCache,
881
997
  meterProvider,
882
998
  tracerProvider
883
999
  });
884
1000
  } catch (error) {
1001
+ if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(jsonWithoutSig, ctx.contextLoader)) return await respondInvalidActivity(error);
1002
+ if (deferredLdSignatureError != null) {
1003
+ logger.debug("Object Integrity Proof fallback did not supersede a deferred Linked Data Signature failure:\n{error}", {
1004
+ recipient,
1005
+ error
1006
+ });
1007
+ activity = null;
1008
+ }
1009
+ if (!isPermanentActivityParseError(error)) throw error;
885
1010
  logger.error("Failed to parse activity:\n{error}", {
886
1011
  recipient,
887
1012
  activity: json,
@@ -930,6 +1055,7 @@ async function handleInboxInternal(request, parameters, span) {
930
1055
  tracerProvider
931
1056
  });
932
1057
  if (verification.verified === false) {
1058
+ if (deferredLdSignatureError != null) throw deferredLdSignatureError;
933
1059
  const reason = verification.reason;
934
1060
  const remoteHost = "keyId" in reason && reason.keyId != null ? getRemoteHost(reason.keyId) : void 0;
935
1061
  getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure(reason.type, remoteHost);
@@ -1007,7 +1133,15 @@ async function handleInboxInternal(request, parameters, span) {
1007
1133
  }
1008
1134
  httpSigKey = verification.key;
1009
1135
  }
1010
- activity = await Activity.fromJsonLd(jsonWithoutSig, ctx);
1136
+ try {
1137
+ activity = await Activity.fromJsonLd(jsonWithoutSig, {
1138
+ ...ctx,
1139
+ contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader)
1140
+ });
1141
+ } catch (error) {
1142
+ if (!isPermanentActivityParseError(error)) throw error;
1143
+ return await respondInvalidActivity(error);
1144
+ }
1011
1145
  }
1012
1146
  if (activity.id != null) span.setAttribute("activitypub.activity.id", activity.id.href);
1013
1147
  span.setAttribute("activitypub.activity.type", getTypeId(activity).href);
@@ -1019,6 +1153,7 @@ async function handleInboxInternal(request, parameters, span) {
1019
1153
  "http_signatures.key_id": httpSigKey?.id?.href ?? ""
1020
1154
  });
1021
1155
  if (httpSigKey != null && !await doesActorOwnKey(activity, httpSigKey, ctx)) {
1156
+ if (deferredLdSignatureError != null) throw deferredLdSignatureError;
1022
1157
  getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("actorKeyMismatch", httpSigKey.id == null ? void 0 : getRemoteHost(httpSigKey.id));
1023
1158
  logger.error("The signer ({keyId}) and the actor ({actorId}) do not match.", {
1024
1159
  activity: json,
@@ -1045,10 +1180,14 @@ async function handleInboxInternal(request, parameters, span) {
1045
1180
  const routeResult = await routeActivity({
1046
1181
  context: ctx,
1047
1182
  json,
1183
+ originalJson: json,
1184
+ normalizedActivity: hasLdSignature && compactedJson !== json ? compactedJson : void 0,
1185
+ ldSignatureVerified: hasLdSignature ? ldSigVerified : void 0,
1048
1186
  activity,
1049
1187
  recipient,
1050
1188
  inboxListeners,
1051
1189
  inboxContextFactory,
1190
+ listenerInboxContextFactory: ldSigVerified ? inboxContextFactory[rawInboxContextFactorySymbol] : void 0,
1052
1191
  inboxErrorHandler,
1053
1192
  kv,
1054
1193
  kvPrefixes,
@@ -1175,8 +1314,8 @@ var CustomCollectionHandler = class {
1175
1314
  * @param CollectionPage The CollectionPage constructor.
1176
1315
  * @param filterPredicate Optional filter predicate for items.
1177
1316
  */
1178
- constructor(name$1, values, context, callbacks, tracerProvider = trace.getTracerProvider(), Collection, CollectionPage, filterPredicate) {
1179
- this.name = name$1;
1317
+ constructor(name$2, values, context, callbacks, tracerProvider = trace.getTracerProvider(), Collection, CollectionPage, filterPredicate) {
1318
+ this.name = name$2;
1180
1319
  this.values = values;
1181
1320
  this.context = context;
1182
1321
  this.callbacks = callbacks;
@@ -1773,6 +1912,18 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
1773
1912
  }
1774
1913
  //#endregion
1775
1914
  //#region src/federation/middleware.ts
1915
+ function isRemoteContextLoadingFailure(error) {
1916
+ return error instanceof Error && typeof error.details === "object" && error.details != null && error.details.code === "loading remote context failed";
1917
+ }
1918
+ function isPermanentRemoteContextError(error) {
1919
+ if (!(error instanceof Error) || error.name !== "jsonld.InvalidUrl") return false;
1920
+ const details = error.details;
1921
+ if (details?.code === "invalid remote context") return true;
1922
+ return isRemoteContextLoadingFailure(error) && typeof details?.url === "string" && !URL.canParse(details.url) && isClearlyMalformedContextReference(details.url);
1923
+ }
1924
+ function isPermanentInboxParseError(error) {
1925
+ return error instanceof Error && (error.name === "UnsafeJsonLdError" || error instanceof InvalidContextReferenceError || isPermanentRemoteContextError(error) || error.name === "jsonld.SyntaxError" && !isRemoteContextLoadingFailure(error)) || error instanceof TypeError && (/^(Invalid JSON-LD:|Invalid type:|Unexpected type:)/.test(error.message) || isInvalidUrlTypeError(error));
1926
+ }
1776
1927
  /**
1777
1928
  * Create a new {@link Federation} instance.
1778
1929
  * @param parameters Parameters for initializing the instance.
@@ -2275,78 +2426,37 @@ var FederationImpl = class extends FederationBuilderImpl {
2275
2426
  const identity = await this.sharedInboxKeyDispatcher(context);
2276
2427
  if (identity != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
2277
2428
  }
2278
- const activity = await Activity.fromJsonLd(message.activity, context);
2279
- const activityType = getTypeId(activity).href;
2280
- span.setAttribute("activitypub.activity.type", activityType);
2281
- onActivityType?.(activityType);
2282
- if (activity.id != null) span.setAttribute("activitypub.activity.id", activity.id.href);
2283
- const cacheKey = activity.id == null ? null : [
2284
- ...this.kvPrefixes.activityIdempotence,
2285
- context.origin,
2286
- activity.id.href
2287
- ];
2288
- if (cacheKey != null) {
2289
- if (await this.kv.get(cacheKey) === true) {
2290
- logger.debug("Activity {activityId} has already been processed.", {
2291
- activityId: activity.id?.href,
2292
- activity: message.activity,
2293
- recipient: message.identifier
2294
- });
2295
- recordInboxActivity(this.meterProvider, "rejected", activityType);
2296
- return;
2297
- }
2298
- }
2299
- await this._getTracer().startActiveSpan("activitypub.dispatch_inbox_listener", { kind: SpanKind.INTERNAL }, async (span) => {
2300
- const dispatched = this.inboxListeners?.dispatchWithClass(activity);
2301
- if (dispatched == null) {
2302
- logger.error("Unsupported activity type:\n{activity}", {
2303
- activityId: activity.id?.href,
2304
- activity: message.activity,
2305
- recipient: message.identifier,
2306
- trial: message.attempt
2307
- });
2308
- span.setStatus({
2309
- code: SpanStatusCode.ERROR,
2310
- message: `Unsupported activity type: ${activityType}`
2311
- });
2312
- recordInboxActivity(this.meterProvider, "rejected", activityType);
2313
- span.end();
2314
- return;
2315
- }
2316
- const { class: cls, listener } = dispatched;
2317
- span.updateName(`activitypub.dispatch_inbox_listener ${cls.name}`);
2318
- try {
2319
- const started = performance.now();
2320
- try {
2321
- await listener(context.toInboxContext(message.identifier, message.activity, activity.id?.href, activityType), activity);
2322
- } finally {
2323
- getFederationMetrics(this.meterProvider).recordInboxProcessingDuration(activityType, getDurationMs(started));
2324
- }
2325
- recordInboxActivity(this.meterProvider, "processed", activityType);
2326
- } catch (error) {
2429
+ await this._getTracer().startActiveSpan("activitypub.dispatch_inbox_listener", { kind: SpanKind.INTERNAL }, async (listenerSpan) => {
2430
+ let activity = null;
2431
+ let cacheKey = null;
2432
+ let activityType;
2433
+ const reportInboxError = async (error) => {
2327
2434
  try {
2328
2435
  await this.inboxErrorHandler?.(context, error);
2329
2436
  } catch (error) {
2330
2437
  logger.error("An unexpected error occurred in inbox error handler:\n{error}", {
2331
2438
  error,
2332
2439
  trial: message.attempt,
2333
- activityId: activity.id?.href,
2440
+ activityId: activity?.id?.href,
2334
2441
  activity: message.activity,
2335
2442
  recipient: message.identifier
2336
2443
  });
2337
2444
  }
2445
+ };
2446
+ const handleRetriableFailure = async (error) => {
2447
+ await reportInboxError(error);
2338
2448
  if (this.inboxQueue?.nativeRetrial) {
2339
2449
  logger.error("Failed to process the incoming activity {activityId}; backend will handle retry:\n{error}", {
2340
2450
  error,
2341
- activityId: activity.id?.href,
2451
+ activityId: activity?.id?.href,
2342
2452
  activity: message.activity,
2343
2453
  recipient: message.identifier
2344
2454
  });
2345
- span.setStatus({
2455
+ listenerSpan.setStatus({
2346
2456
  code: SpanStatusCode.ERROR,
2347
2457
  message: String(error)
2348
2458
  });
2349
- span.end();
2459
+ listenerSpan.end();
2350
2460
  throw error;
2351
2461
  }
2352
2462
  const delay = this.inboxRetryPolicy({
@@ -2357,20 +2467,27 @@ var FederationImpl = class extends FederationBuilderImpl {
2357
2467
  logger.error("Failed to process the incoming activity {activityId} (attempt #{attempt}); retry...:\n{error}", {
2358
2468
  error,
2359
2469
  attempt: message.attempt,
2360
- activityId: activity.id?.href,
2470
+ activityId: activity?.id?.href,
2361
2471
  activity: message.activity,
2362
2472
  recipient: message.identifier
2363
2473
  });
2474
+ if (this.inboxQueue == null) {
2475
+ listenerSpan.setStatus({
2476
+ code: SpanStatusCode.ERROR,
2477
+ message: String(error)
2478
+ });
2479
+ listenerSpan.end();
2480
+ throw error;
2481
+ }
2364
2482
  const retryMessage = {
2365
2483
  ...message,
2366
2484
  attempt: message.attempt + 1
2367
2485
  };
2368
- const { inboxQueue } = this;
2369
- if (inboxQueue != null) {
2370
- await inboxQueue.enqueue(retryMessage, { delay: Temporal.Duration.compare(delay, { seconds: 0 }) < 0 ? Temporal.Duration.from({ seconds: 0 }) : delay });
2486
+ await this.inboxQueue.enqueue(retryMessage, { delay: Temporal.Duration.compare(delay, { seconds: 0 }) < 0 ? Temporal.Duration.from({ seconds: 0 }) : delay });
2487
+ if (activityType != null) {
2371
2488
  getFederationMetrics(this.meterProvider).recordQueueTaskEnqueued({
2372
2489
  role: "inbox",
2373
- queue: inboxQueue,
2490
+ queue: this.inboxQueue,
2374
2491
  activityType
2375
2492
  }, retryMessage.attempt);
2376
2493
  recordInboxActivity(this.meterProvider, "retried", activityType);
@@ -2378,26 +2495,137 @@ var FederationImpl = class extends FederationBuilderImpl {
2378
2495
  } else {
2379
2496
  logger.error("Failed to process the incoming activity {activityId} after {trial} attempts; giving up:\n{error}", {
2380
2497
  error,
2381
- activityId: activity.id?.href,
2498
+ activityId: activity?.id?.href,
2382
2499
  activity: message.activity,
2383
2500
  recipient: message.identifier
2384
2501
  });
2385
- recordInboxActivity(this.meterProvider, "abandoned", activityType);
2502
+ if (activityType != null) recordInboxActivity(this.meterProvider, "abandoned", activityType);
2386
2503
  }
2387
- span.setStatus({
2504
+ listenerSpan.setStatus({
2388
2505
  code: SpanStatusCode.ERROR,
2389
2506
  message: String(error)
2390
2507
  });
2391
- span.end();
2508
+ listenerSpan.end();
2509
+ };
2510
+ let dispatched;
2511
+ let parseInput = void 0;
2512
+ let parseContextLoader = context.contextLoader;
2513
+ try {
2514
+ const hasSignatureField = hasSignature(message.activity);
2515
+ const shouldParseFromNormalizedSignedPayload = message.ldSignatureVerified === true || message.normalizedActivity != null || message.ldSignatureVerified == null && hasSignatureField;
2516
+ const parseContext = hasSignatureField ? {
2517
+ ...context,
2518
+ contextLoader: getNormalizationContextLoader(context.contextLoader)
2519
+ } : {
2520
+ ...context,
2521
+ contextLoader: wrapContextLoaderForJsonLd(context.contextLoader)
2522
+ };
2523
+ parseContextLoader = parseContext.contextLoader;
2524
+ let normalizedActivity;
2525
+ if (shouldParseFromNormalizedSignedPayload) {
2526
+ normalizedActivity = message.normalizedActivity ?? await compactJsonLd(message.activity, context.contextLoader);
2527
+ assertSafeJsonLd(normalizedActivity);
2528
+ }
2529
+ parseInput = shouldParseFromNormalizedSignedPayload ? detachSignature(normalizedActivity) : hasSignatureField ? detachSignature(message.activity) : message.activity;
2530
+ activity = await Activity.fromJsonLd(parseInput, parseContext);
2531
+ activityType = getTypeId(activity).href;
2532
+ span.setAttribute("activitypub.activity.type", activityType);
2533
+ listenerSpan.setAttribute("activitypub.activity.type", activityType);
2534
+ onActivityType?.(activityType);
2535
+ if (activity.id != null) {
2536
+ span.setAttribute("activitypub.activity.id", activity.id.href);
2537
+ listenerSpan.setAttribute("activitypub.activity.id", activity.id.href);
2538
+ }
2539
+ cacheKey = activity.id == null ? null : [
2540
+ ...this.kvPrefixes.activityIdempotence,
2541
+ context.origin,
2542
+ activity.id.href
2543
+ ];
2544
+ if (cacheKey != null) {
2545
+ if (await this.kv.get(cacheKey) === true) {
2546
+ logger.debug("Activity {activityId} has already been processed.", {
2547
+ activityId: activity.id?.href,
2548
+ activity: message.activity,
2549
+ recipient: message.identifier
2550
+ });
2551
+ recordInboxActivity(this.meterProvider, "rejected", activityType);
2552
+ listenerSpan.end();
2553
+ return;
2554
+ }
2555
+ }
2556
+ dispatched = this.inboxListeners?.dispatchWithClass(activity);
2557
+ } catch (error) {
2558
+ if (activity == null && error instanceof RangeError && await hasMalformedKnownTemporalLiteral(parseInput, parseContextLoader)) {
2559
+ await reportInboxError(error);
2560
+ logger.error("Failed to parse the queued incoming activity {activityId}:\n{error}", {
2561
+ error,
2562
+ trial: message.attempt,
2563
+ activityId: null,
2564
+ activity: message.activity,
2565
+ recipient: message.identifier
2566
+ });
2567
+ listenerSpan.setStatus({
2568
+ code: SpanStatusCode.ERROR,
2569
+ message: String(error)
2570
+ });
2571
+ listenerSpan.end();
2572
+ return;
2573
+ }
2574
+ if (isPermanentInboxParseError(error)) {
2575
+ await reportInboxError(error);
2576
+ logger.error("Failed to parse the queued incoming activity {activityId}:\n{error}", {
2577
+ error,
2578
+ trial: message.attempt,
2579
+ activityId: activity?.id?.href,
2580
+ activity: message.activity,
2581
+ recipient: message.identifier
2582
+ });
2583
+ listenerSpan.setStatus({
2584
+ code: SpanStatusCode.ERROR,
2585
+ message: String(error)
2586
+ });
2587
+ listenerSpan.end();
2588
+ return;
2589
+ }
2590
+ await handleRetriableFailure(error);
2591
+ return;
2592
+ }
2593
+ if (dispatched == null) {
2594
+ logger.error("Unsupported activity type:\n{activity}", {
2595
+ activityId: activity.id?.href,
2596
+ activity: message.activity,
2597
+ recipient: message.identifier,
2598
+ trial: message.attempt
2599
+ });
2600
+ listenerSpan.setStatus({
2601
+ code: SpanStatusCode.ERROR,
2602
+ message: `Unsupported activity type: ${activityType}`
2603
+ });
2604
+ recordInboxActivity(this.meterProvider, "rejected", activityType);
2605
+ listenerSpan.end();
2606
+ return;
2607
+ }
2608
+ const { class: cls, listener } = dispatched;
2609
+ listenerSpan.updateName(`activitypub.dispatch_inbox_listener ${cls.name}`);
2610
+ try {
2611
+ const started = performance.now();
2612
+ try {
2613
+ await listener(context.toInboxContext(message.identifier, message.activity, activity.id?.href, activityType), activity);
2614
+ } finally {
2615
+ getFederationMetrics(this.meterProvider).recordInboxProcessingDuration(activityType, getDurationMs(started));
2616
+ }
2617
+ recordInboxActivity(this.meterProvider, "processed", activityType);
2618
+ } catch (error) {
2619
+ await handleRetriableFailure(error);
2392
2620
  return;
2393
2621
  }
2394
2622
  if (cacheKey != null) await this.kv.set(cacheKey, true, { ttl: Temporal.Duration.from({ days: 1 }) });
2395
2623
  logger.info("Activity {activityId} has been processed.", {
2396
- activityId: activity.id?.href,
2624
+ activityId: activity?.id?.href,
2397
2625
  activity: message.activity,
2398
2626
  recipient: message.identifier
2399
2627
  });
2400
- span.end();
2628
+ listenerSpan.end();
2401
2629
  });
2402
2630
  }
2403
2631
  startQueue(contextData, options = {}) {
@@ -2778,16 +3006,18 @@ var FederationImpl = class extends FederationBuilderImpl {
2778
3006
  onNotFound
2779
3007
  });
2780
3008
  context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier: route.values.identifier }) });
2781
- case "sharedInbox":
3009
+ case "sharedInbox": {
2782
3010
  if (routeName !== "inbox" && this.sharedInboxKeyDispatcher != null) {
2783
3011
  const identity = await this.sharedInboxKeyDispatcher(context);
2784
3012
  if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
2785
3013
  }
2786
3014
  if (!this.manuallyStartQueue) this._startQueueInternal(contextData);
3015
+ const inboxContextFactory = context.toInboxContext.bind(context);
3016
+ inboxContextFactory[rawInboxContextFactorySymbol] = context.toInboxContext.bind(context);
2787
3017
  return await handleInbox(request, {
2788
3018
  recipient: route.values.identifier ?? null,
2789
3019
  context,
2790
- inboxContextFactory: context.toInboxContext.bind(context),
3020
+ inboxContextFactory,
2791
3021
  kv: this.kv,
2792
3022
  kvPrefixes: this.kvPrefixes,
2793
3023
  queue: this.inboxQueue,
@@ -2803,6 +3033,7 @@ var FederationImpl = class extends FederationBuilderImpl {
2803
3033
  tracerProvider: this.tracerProvider,
2804
3034
  idempotencyStrategy: this.idempotencyStrategy
2805
3035
  });
3036
+ }
2806
3037
  case "following": return await handleCollection(request, {
2807
3038
  name: "following",
2808
3039
  identifier: route.values.identifier,
@@ -3536,6 +3767,7 @@ var ContextImpl = class ContextImpl {
3536
3767
  const routeResult = await routeActivity({
3537
3768
  context: this,
3538
3769
  json,
3770
+ ldSignatureVerified: false,
3539
3771
  activity,
3540
3772
  recipient,
3541
3773
  inboxListeners: this.federation.inboxListeners,
@@ -3824,6 +4056,14 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
3824
4056
  }
3825
4057
  var InboxContextImpl = class InboxContextImpl extends ContextImpl {
3826
4058
  recipient;
4059
+ /**
4060
+ * The original received activity payload.
4061
+ *
4062
+ * Fedify may normalize a Linked Data Signature payload internally for safe
4063
+ * parsing, but forwarding must keep the sender's payload unchanged so
4064
+ * third-party signatures/proofs remain intact.
4065
+ * @internal
4066
+ */
3827
4067
  activity;
3828
4068
  activityId;
3829
4069
  activityType;