@fedify/fedify 2.4.2 → 2.4.4

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 (83) hide show
  1. package/dist/{builder-9ovCSqLp.mjs → builder-DdY0E0w9.mjs} +2 -2
  2. package/dist/compat/transformers.test.mjs +1 -1
  3. package/dist/{compound-proof-G2J_bU0u.mjs → compound-proof-fxkyiXYM.mjs} +2 -2
  4. package/dist/{deno-BZ4z-lJJ.mjs → deno-CO1X_K_q.mjs} +1 -1
  5. package/dist/{docloader-DUXXI-7G.mjs → docloader-DFB_TNVp.mjs} +6 -2
  6. package/dist/federation/builder.test.mjs +1 -1
  7. package/dist/federation/compound-fanout.test.mjs +3 -3
  8. package/dist/federation/handler-compound-proof.test.mjs +5 -5
  9. package/dist/federation/handler.test.mjs +258 -5
  10. package/dist/federation/hashlink-media.test.mjs +2 -2
  11. package/dist/federation/idempotency.test.mjs +2 -2
  12. package/dist/federation/inbox-report.test.mjs +5 -5
  13. package/dist/federation/inbox.test.mjs +43 -0
  14. package/dist/federation/keycache.test.mjs +20 -9
  15. package/dist/federation/metrics.test.mjs +1 -1
  16. package/dist/federation/middleware.test.mjs +173 -11
  17. package/dist/federation/mod.cjs +1 -1
  18. package/dist/federation/mod.js +1 -1
  19. package/dist/federation/outgoing-proof.test.mjs +4 -4
  20. package/dist/federation/portable-actor.test.mjs +3 -3
  21. package/dist/federation/portable-chain.test.mjs +3 -3
  22. package/dist/federation/portable-collection.test.mjs +4 -4
  23. package/dist/federation/portable-delivery.test.mjs +2 -2
  24. package/dist/federation/portable-gateway-keys.test.mjs +5 -5
  25. package/dist/federation/portable-inbox.test.mjs +4 -4
  26. package/dist/federation/portable-parse-uri.test.mjs +1 -1
  27. package/dist/federation/portable-route-activity.test.mjs +2 -2
  28. package/dist/federation/portable.test.mjs +3 -3
  29. package/dist/federation/send.test.mjs +3 -3
  30. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  31. package/dist/federation/tasks/tasks.test.mjs +2 -2
  32. package/dist/federation/temporal.test.mjs +1 -1
  33. package/dist/federation/webfinger.test.mjs +1 -1
  34. package/dist/{federation-DpT8bDM6.cjs → federation-Bf4FEork.cjs} +122 -21
  35. package/dist/{http-Qq7jR2rC.js → http-D_AGUmSq.js} +17 -3
  36. package/dist/{http-B5w_qcIg.mjs → http-DkLKT8Vp.mjs} +4 -4
  37. package/dist/{http-x1satM4y.cjs → http-g5f1D2tm.cjs} +17 -3
  38. package/dist/inbox-Di1-EKqa.mjs +188 -0
  39. package/dist/{keycache-D-MNltOk.mjs → keycache-N2opu4-3.mjs} +1 -1
  40. package/dist/{kv-cache-BgQMAKIg.mjs → kv-cache-BxfOpUP9.mjs} +1 -1
  41. package/dist/{kv-cache-B0SZfzFb.js → kv-cache-C0AsMZ4U.js} +5 -1
  42. package/dist/{kv-cache-D4throaQ.cjs → kv-cache-HsXADkl9.cjs} +5 -1
  43. package/dist/{ld-Dkkyk65e.mjs → ld-Cy86r-Ty.mjs} +18 -4
  44. package/dist/{metrics-CRv6_IBg.mjs → metrics-BguhdXt-.mjs} +1 -1
  45. package/dist/{middleware-BqefXH6B.js → middleware-BOBF7req.js} +122 -21
  46. package/dist/{middleware-DFtNaBcH.mjs → middleware-BYojyPi4.mjs} +1 -1
  47. package/dist/{middleware-C-swet0z.mjs → middleware-C_gtR3fV.mjs} +104 -180
  48. package/dist/mod.cjs +4 -4
  49. package/dist/mod.js +4 -4
  50. package/dist/nodeinfo/handler.test.mjs +1 -1
  51. package/dist/{owner-BHUhedVc.mjs → owner-Dc-Lg_Xn.mjs} +3 -3
  52. package/dist/{portable-collection-DL-1Wy1s.cjs → portable-collection-Bl2UkqcO.cjs} +1 -1
  53. package/dist/{portable-collection-CfIc190c.js → portable-collection-CjdP7fQ4.js} +1 -1
  54. package/dist/{portable-collection-DONEPTXV.mjs → portable-collection-DSKqMgvr.mjs} +2 -2
  55. package/dist/{portable-key-Cdw3vfl-.mjs → portable-key-ClXZ25LT.mjs} +1 -1
  56. package/dist/{proof-DuSE1ABD.mjs → proof-kJtQ49gr.mjs} +3 -3
  57. package/dist/{send-D9oTjA6A.mjs → send-D6hcL5qf.mjs} +3 -3
  58. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  59. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  60. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  61. package/dist/sig/compound-proof.test.mjs +2 -2
  62. package/dist/sig/http.test.mjs +3 -3
  63. package/dist/sig/key.test.mjs +196 -1
  64. package/dist/sig/ld.test.mjs +46 -1
  65. package/dist/sig/mitra-receive-interop.test.mjs +4 -4
  66. package/dist/sig/mitra-send-interop.test.mjs +3 -3
  67. package/dist/sig/mod.cjs +2 -2
  68. package/dist/sig/mod.js +2 -2
  69. package/dist/sig/owner.test.mjs +46 -1
  70. package/dist/sig/portable-collection.test.mjs +1 -1
  71. package/dist/sig/portable-interop.test.mjs +3 -3
  72. package/dist/sig/portable-key.test.mjs +6 -6
  73. package/dist/sig/proof.test.mjs +54 -3
  74. package/dist/sig/tootik-request-interop.test.mjs +4 -4
  75. package/dist/sig/tootik-send-interop.test.mjs +3 -3
  76. package/dist/sig/verification.test.mjs +3 -3
  77. package/dist/{temporal-iyAxBq0I.mjs → temporal-C8WqyM34.mjs} +1 -1
  78. package/dist/testing/kv.test.mjs +1 -1
  79. package/dist/utils/docloader.test.mjs +176 -37
  80. package/dist/utils/kv-cache.test.mjs +1 -1
  81. package/dist/utils/mod.cjs +1 -1
  82. package/dist/utils/mod.js +1 -1
  83. package/package.json +6 -6
@@ -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-x1satM4y.cjs");
4
+ const require_http = require("./http-g5f1D2tm.cjs");
5
5
  const require_types = require("./types-poIao40f.cjs");
6
- const require_portable_collection = require("./portable-collection-DL-1Wy1s.cjs");
7
- const require_kv_cache = require("./kv-cache-D4throaQ.cjs");
6
+ const require_portable_collection = require("./portable-collection-Bl2UkqcO.cjs");
7
+ const require_kv_cache = require("./kv-cache-HsXADkl9.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");
@@ -2779,12 +2779,41 @@ async function isInAudience(object, actor, options = {}) {
2779
2779
  //#endregion
2780
2780
  //#region src/federation/inbox.ts
2781
2781
  const Temporal$10 = require_http.Temporal;
2782
+ /**
2783
+ * Checks whether an activity's ID and all of its actor IDs have the same
2784
+ * [FEP-fe34] origin. Activities without an ID have no origin to authenticate.
2785
+ *
2786
+ * Origins are compared by `getAuthenticationOrigin()`, so a portable activity
2787
+ * and its portable actors, including compatible identifiers on any gateway,
2788
+ * are compared by their DIDs. An ID that has no supported origin, such as an
2789
+ * opaque URN, never matches.
2790
+ *
2791
+ * The caller is responsible for rejecting a missing primary actor ID before
2792
+ * relying on this check.
2793
+ *
2794
+ * [FEP-fe34]: https://w3id.org/fep/fe34
2795
+ * @internal
2796
+ */
2797
+ function actorsHaveSameOrigin(activity) {
2798
+ if (activity.id == null) return true;
2799
+ const origin = require_http.getAuthenticationOrigin(activity.id);
2800
+ return origin != null && activity.actorIds.every((actor) => require_http.getAuthenticationOrigin(actor) === origin);
2801
+ }
2782
2802
  async function routeActivity({ context: ctx, json, originalJson, normalizedActivity, ldSignatureVerified, activity, recipient, inboxListeners, inboxContextFactory, listenerInboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, meterProvider, tracerProvider, idempotencyStrategy }) {
2783
2803
  const logger = (0, _logtape_logtape.getLogger)([
2784
2804
  "fedify",
2785
2805
  "federation",
2786
2806
  "inbox"
2787
2807
  ]);
2808
+ if (activity.actorId == null) {
2809
+ logger.error("Missing actor.", { activity: json });
2810
+ span.setStatus({
2811
+ code: _opentelemetry_api.SpanStatusCode.ERROR,
2812
+ message: "Missing actor."
2813
+ });
2814
+ require_http.recordInboxActivity(meterProvider, "rejected", (0, _fedify_vocab.getTypeId)(activity).href);
2815
+ return "missingActor";
2816
+ }
2788
2817
  let cacheKey = null;
2789
2818
  if (activity.id != null) {
2790
2819
  const inboxContext = inboxContextFactory(recipient, json, activity.id?.href, (0, _fedify_vocab.getTypeId)(activity).href);
@@ -2820,15 +2849,6 @@ async function routeActivity({ context: ctx, json, originalJson, normalizedActiv
2820
2849
  return "alreadyProcessed";
2821
2850
  }
2822
2851
  }
2823
- if (activity.actorId == null) {
2824
- logger.error("Missing actor.", { activity: json });
2825
- span.setStatus({
2826
- code: _opentelemetry_api.SpanStatusCode.ERROR,
2827
- message: "Missing actor."
2828
- });
2829
- require_http.recordInboxActivity(meterProvider, "rejected", (0, _fedify_vocab.getTypeId)(activity).href);
2830
- return "missingActor";
2831
- }
2832
2852
  span.setAttribute("activitypub.actor.id", activity.actorId.href);
2833
2853
  if (queue != null) {
2834
2854
  const carrier = {};
@@ -2938,7 +2958,7 @@ async function routeActivity({ context: ctx, json, originalJson, normalizedActiv
2938
2958
  //#endregion
2939
2959
  //#region src/federation/keycache.ts
2940
2960
  const Temporal$9 = require_http.Temporal;
2941
- const KEY_CACHE_GENERATION = "2";
2961
+ const KEY_CACHE_GENERATION = "3";
2942
2962
  const COMPATIBLE_KEY_SEGMENT = "__compatible";
2943
2963
  const MAX_OWNER_BYTES = 32 * 1024;
2944
2964
  function isCompatibleKeyEntry(value) {
@@ -4746,13 +4766,13 @@ function recordCollectionMetrics(meterProvider, base, result, options = {}) {
4746
4766
  * @param parameters The parameters for handling the collection.
4747
4767
  * @returns A promise that resolves to an HTTP response.
4748
4768
  */
4749
- async function handleCollection(request, { name: name$2, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, portable, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
4750
- const spanName = name$2.trim().replace(/\s+/g, "_");
4769
+ async function handleCollection(request, { name: name$1, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, portable, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
4770
+ const spanName = name$1.trim().replace(/\s+/g, "_");
4751
4771
  tracerProvider = tracerProvider ?? _opentelemetry_api.trace.getTracerProvider();
4752
4772
  const tracer = tracerProvider.getTracer(require_http.name, require_http.version);
4753
4773
  const cursor = new URL(request.url).searchParams.get("cursor");
4754
4774
  const metricBase = {
4755
- kind: getCollectionMetricKind(name$2),
4775
+ kind: getCollectionMetricKind(name$1),
4756
4776
  page: cursor != null,
4757
4777
  dispatcher: "built_in"
4758
4778
  };
@@ -4798,7 +4818,7 @@ async function handleCollection(request, { name: name$2, identifier, uriGetter,
4798
4818
  span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR });
4799
4819
  return await onNotFound(request);
4800
4820
  }
4801
- const items = filterCollectionItems(page.items, name$2, filterPredicate);
4821
+ const items = filterCollectionItems(page.items, name$1, filterPredicate);
4802
4822
  itemCount = items.length;
4803
4823
  span.setAttribute("fedify.collection.items", itemCount);
4804
4824
  return items;
@@ -4840,7 +4860,7 @@ async function handleCollection(request, { name: name$2, identifier, uriGetter,
4840
4860
  } else {
4841
4861
  const uri = new URL(portable == null ? baseUri : pageBase);
4842
4862
  uri.searchParams.set("cursor", cursor);
4843
- const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$2}`, {
4863
+ const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$1}`, {
4844
4864
  kind: _opentelemetry_api.SpanKind.SERVER,
4845
4865
  attributes: {
4846
4866
  "activitypub.collection.id": uri.href,
@@ -4856,7 +4876,7 @@ async function handleCollection(request, { name: name$2, identifier, uriGetter,
4856
4876
  span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR });
4857
4877
  return await onNotFound(request);
4858
4878
  }
4859
- const items = filterCollectionItems(page.items, name$2, filterPredicate);
4879
+ const items = filterCollectionItems(page.items, name$1, filterPredicate);
4860
4880
  itemCount = items.length;
4861
4881
  span.setAttribute("fedify.collection.items", itemCount);
4862
4882
  return {
@@ -6005,11 +6025,49 @@ async function handleInboxInternal(request, parameters, span) {
6005
6025
  return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
6006
6026
  }
6007
6027
  }
6028
+ if (!skipSignatureVerification && activity.id != null && !actorsHaveSameOrigin(activity)) {
6029
+ observation.authentication = {
6030
+ status: "rejected",
6031
+ reason: { type: "verificationFailed" }
6032
+ };
6033
+ logger.error("The activity ({activityId}) and its actors do not have the same origin.", {
6034
+ activity: json,
6035
+ recipient,
6036
+ activityId: activity.id.href,
6037
+ actorIds: activity.actorIds.map((actor) => actor.href)
6038
+ });
6039
+ span.setStatus({
6040
+ code: _opentelemetry_api.SpanStatusCode.ERROR,
6041
+ message: `The activity (${activity.id.href}) and its actors do not have the same origin.`
6042
+ });
6043
+ return new Response("The activity and its actors do not have the same origin.", {
6044
+ status: 401,
6045
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
6046
+ });
6047
+ }
6008
6048
  observation.authentication = skipSignatureVerification ? { status: "skipped" } : {
6009
6049
  status: "verified",
6010
6050
  attempts: observation.verification.attempts.filter((attempt) => attempt.status === "verified")
6011
6051
  };
6012
6052
  observation.stage = "dispatch";
6053
+ if (activity.actorId == null) {
6054
+ observation.result = {
6055
+ disposition: "rejected",
6056
+ reason: "missingActor"
6057
+ };
6058
+ logger.error("Missing actor.", {
6059
+ activity: json,
6060
+ recipient
6061
+ });
6062
+ span.setStatus({
6063
+ code: _opentelemetry_api.SpanStatusCode.ERROR,
6064
+ message: "Missing actor."
6065
+ });
6066
+ return new Response("Missing actor.", {
6067
+ status: 400,
6068
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
6069
+ });
6070
+ }
6013
6071
  const routeResult = await routeActivity({
6014
6072
  context: ctx,
6015
6073
  json,
@@ -6230,8 +6288,8 @@ var CustomCollectionHandler = class {
6230
6288
  * @param CollectionPage The CollectionPage constructor.
6231
6289
  * @param filterPredicate Optional filter predicate for items.
6232
6290
  */
6233
- constructor(name$1, values, context, callbacks, tracerProvider = _opentelemetry_api.trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate, portable) {
6234
- this.name = name$1;
6291
+ constructor(name$2, values, context, callbacks, tracerProvider = _opentelemetry_api.trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate, portable) {
6292
+ this.name = name$2;
6235
6293
  this.values = values;
6236
6294
  this.context = context;
6237
6295
  this.callbacks = callbacks;
@@ -9357,6 +9415,35 @@ var FederationImpl = class extends FederationBuilderImpl {
9357
9415
  span.setAttribute("activitypub.activity.id", activity.id.href);
9358
9416
  listenerSpan.setAttribute("activitypub.activity.id", activity.id.href);
9359
9417
  }
9418
+ if (activity.actorId == null) {
9419
+ logger.error("Missing actor in queued incoming activity.", {
9420
+ activityId: activity.id?.href,
9421
+ activity: message.activity,
9422
+ recipient: message.identifier
9423
+ });
9424
+ listenerSpan.setStatus({
9425
+ code: _opentelemetry_api.SpanStatusCode.ERROR,
9426
+ message: "Missing actor."
9427
+ });
9428
+ require_http.recordInboxActivity(this.meterProvider, "rejected", activityType);
9429
+ listenerSpan.end();
9430
+ return;
9431
+ }
9432
+ if (!this.skipSignatureVerification && activity.id != null && !actorsHaveSameOrigin(activity)) {
9433
+ logger.error("The queued activity ({activityId}) and its actors do not have the same origin.", {
9434
+ activityId: activity.id.href,
9435
+ actorIds: activity.actorIds.map((actor) => actor.href),
9436
+ activity: message.activity,
9437
+ recipient: message.identifier
9438
+ });
9439
+ listenerSpan.setStatus({
9440
+ code: _opentelemetry_api.SpanStatusCode.ERROR,
9441
+ message: `The activity (${activity.id.href}) and its actors do not have the same origin.`
9442
+ });
9443
+ require_http.recordInboxActivity(this.meterProvider, "rejected", activityType);
9444
+ listenerSpan.end();
9445
+ return;
9446
+ }
9360
9447
  cacheKey = activity.id == null ? null : [
9361
9448
  ...this.kvPrefixes.activityIdempotence,
9362
9449
  context.origin,
@@ -11433,6 +11520,20 @@ var ContextImpl = class ContextImpl {
11433
11520
  recipient,
11434
11521
  activity: json
11435
11522
  });
11523
+ if (activity.actorId == null) {
11524
+ logger.debug("Activity is missing an actor; unable to verify.", {
11525
+ recipient,
11526
+ activity: json
11527
+ });
11528
+ return false;
11529
+ }
11530
+ if (activity.id != null && !actorsHaveSameOrigin(activity)) {
11531
+ logger.debug("Activity has actors from different origins; unable to verify.", {
11532
+ recipient,
11533
+ activity: json
11534
+ });
11535
+ return false;
11536
+ }
11436
11537
  const routeResult = await routeActivity({
11437
11538
  context: this,
11438
11539
  json,
@@ -15,7 +15,7 @@ import serialize from "json-canon";
15
15
  import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions";
16
16
  //#region deno.json
17
17
  var name = "@fedify/fedify";
18
- var version = "2.4.2";
18
+ var version = "2.4.4";
19
19
  //#endregion
20
20
  //#region src/sig/portable-key-id.ts
21
21
  const COMPATIBLE_KEY_ID_PATH_PATTERN = /^\/\.well-known\/apgateway\/did(?::|%3A)/i;
@@ -1570,16 +1570,23 @@ async function importJwk(jwk, type) {
1570
1570
  /**
1571
1571
  * Fetches the document that the given actor URI dereferences to, and returns
1572
1572
  * the actor only if the host that served the document is authoritative for
1573
- * the id the document claims.
1573
+ * the id the document claims, and that id is the actor that was asked for.
1574
1574
  *
1575
1575
  * Only the origin that serves an actor id can speak for it. Without that
1576
1576
  * rule any host could serve a document describing somebody else's actor—and
1577
1577
  * listing its own keys as that actor's.
1578
1578
  *
1579
+ * The returned actor always has exactly the requested `actorId` as its `id`.
1580
+ * A document loader follows redirects and alternate document links, so the
1581
+ * document an actor id leads to may describe a different actor altogether,
1582
+ * on the same origin or on another; accepting it would attribute that other
1583
+ * actor's keys to the requested one. See GHSA-8863-8468-7hq7.
1584
+ *
1579
1585
  * @param actorId The URI of the actor to fetch.
1580
1586
  * @param options Options for fetching the document.
1581
1587
  * @returns The actor, or `null` if the document cannot be fetched, is not an
1582
- * actor, or belongs to another origin.
1588
+ * actor, belongs to another origin, or describes an actor other
1589
+ * than the requested one.
1583
1590
  * @internal
1584
1591
  */
1585
1592
  async function fetchActorDocument(actorId, options = {}) {
@@ -1645,6 +1652,13 @@ async function fetchActorDocumentInternal(actorId, options, propagateContextFetc
1645
1652
  });
1646
1653
  return null;
1647
1654
  }
1655
+ if (object.id.href !== actorId.href) {
1656
+ logger.debug("The document fetched for the actor {actorId} is the actor {documentActorId} instead; refusing to treat it as the requested actor's document.", {
1657
+ actorId: actorId.href,
1658
+ documentActorId: object.id.href
1659
+ });
1660
+ return null;
1661
+ }
1648
1662
  return object;
1649
1663
  }
1650
1664
  /**
@@ -3,12 +3,12 @@ import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { m as parseKeyIdString } from "./portable-key-id-Ca30-hOa.mjs";
5
5
  import { a as verificationObservation, i as triedKey, n as observeCheck, t as observeAttempt } from "./verification-Dpfcw8ml.mjs";
6
- import { n as version, t as name } from "./deno-BZ4z-lJJ.mjs";
7
- import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-CRv6_IBg.mjs";
6
+ import { n as version, t as name } from "./deno-CO1X_K_q.mjs";
7
+ import { n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch } from "./metrics-BguhdXt-.mjs";
8
8
  import { i as validateAcceptSignature, n as fulfillAcceptSignature, r as parseAcceptSignature } from "./accept-CPVqki78.mjs";
9
9
  import { a as promoteKeyOwnerEvidence } from "./key-owner-evidence-APB6NpSk.mjs";
10
- import { S as fetchKeyDetailed, T as validateCryptoKey, v as bypassKeyCacheReads } from "./ld-Dkkyk65e.mjs";
11
- import { n as resolvePortableActorKey, r as verifyPortableGatewayKeyDocument } from "./portable-key-Cdw3vfl-.mjs";
10
+ import { S as fetchKeyDetailed, T as validateCryptoKey, v as bypassKeyCacheReads } from "./ld-Cy86r-Ty.mjs";
11
+ import { n as resolvePortableActorKey, r as verifyPortableGatewayKeyDocument } from "./portable-key-ClXZ25LT.mjs";
12
12
  import { getLogger } from "@logtape/logtape";
13
13
  import { CryptographicKey } from "@fedify/vocab";
14
14
  import { SpanStatusCode, trace } from "@opentelemetry/api";
@@ -17,7 +17,7 @@ json_canon = require_rolldown_runtime.__toESM(json_canon, 1);
17
17
  let _opentelemetry_semantic_conventions = require("@opentelemetry/semantic-conventions");
18
18
  //#region deno.json
19
19
  var name = "@fedify/fedify";
20
- var version = "2.4.2";
20
+ var version = "2.4.4";
21
21
  //#endregion
22
22
  //#region src/sig/portable-key-id.ts
23
23
  const COMPATIBLE_KEY_ID_PATH_PATTERN = /^\/\.well-known\/apgateway\/did(?::|%3A)/i;
@@ -5138,16 +5138,23 @@ async function importJwk(jwk, type) {
5138
5138
  /**
5139
5139
  * Fetches the document that the given actor URI dereferences to, and returns
5140
5140
  * the actor only if the host that served the document is authoritative for
5141
- * the id the document claims.
5141
+ * the id the document claims, and that id is the actor that was asked for.
5142
5142
  *
5143
5143
  * Only the origin that serves an actor id can speak for it. Without that
5144
5144
  * rule any host could serve a document describing somebody else's actor—and
5145
5145
  * listing its own keys as that actor's.
5146
5146
  *
5147
+ * The returned actor always has exactly the requested `actorId` as its `id`.
5148
+ * A document loader follows redirects and alternate document links, so the
5149
+ * document an actor id leads to may describe a different actor altogether,
5150
+ * on the same origin or on another; accepting it would attribute that other
5151
+ * actor's keys to the requested one. See GHSA-8863-8468-7hq7.
5152
+ *
5147
5153
  * @param actorId The URI of the actor to fetch.
5148
5154
  * @param options Options for fetching the document.
5149
5155
  * @returns The actor, or `null` if the document cannot be fetched, is not an
5150
- * actor, or belongs to another origin.
5156
+ * actor, belongs to another origin, or describes an actor other
5157
+ * than the requested one.
5151
5158
  * @internal
5152
5159
  */
5153
5160
  async function fetchActorDocument(actorId, options = {}) {
@@ -5213,6 +5220,13 @@ async function fetchActorDocumentInternal(actorId, options, propagateContextFetc
5213
5220
  });
5214
5221
  return null;
5215
5222
  }
5223
+ if (object.id.href !== actorId.href) {
5224
+ logger.debug("The document fetched for the actor {actorId} is the actor {documentActorId} instead; refusing to treat it as the requested actor's document.", {
5225
+ actorId: actorId.href,
5226
+ documentActorId: object.id.href
5227
+ });
5228
+ return null;
5229
+ }
5216
5230
  return object;
5217
5231
  }
5218
5232
  /**
@@ -0,0 +1,188 @@
1
+ import { Temporal } from "temporal-polyfill";
2
+ import "urlpattern-polyfill";
3
+ globalThis.addEventListener = () => {};
4
+ import { t as getAuthenticationOrigin } from "./portable-key-id-Ca30-hOa.mjs";
5
+ import { n as version, t as name } from "./deno-CO1X_K_q.mjs";
6
+ import { g as recordInboxActivity, n as getDurationMs, r as getFederationMetrics } from "./metrics-BguhdXt-.mjs";
7
+ import { getLogger } from "@logtape/logtape";
8
+ import { getTypeId } from "@fedify/vocab";
9
+ import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
10
+ //#region src/federation/inbox.ts
11
+ /**
12
+ * Checks whether an activity's ID and all of its actor IDs have the same
13
+ * [FEP-fe34] origin. Activities without an ID have no origin to authenticate.
14
+ *
15
+ * Origins are compared by `getAuthenticationOrigin()`, so a portable activity
16
+ * and its portable actors, including compatible identifiers on any gateway,
17
+ * are compared by their DIDs. An ID that has no supported origin, such as an
18
+ * opaque URN, never matches.
19
+ *
20
+ * The caller is responsible for rejecting a missing primary actor ID before
21
+ * relying on this check.
22
+ *
23
+ * [FEP-fe34]: https://w3id.org/fep/fe34
24
+ * @internal
25
+ */
26
+ function actorsHaveSameOrigin(activity) {
27
+ if (activity.id == null) return true;
28
+ const origin = getAuthenticationOrigin(activity.id);
29
+ return origin != null && activity.actorIds.every((actor) => getAuthenticationOrigin(actor) === origin);
30
+ }
31
+ async function routeActivity({ context: ctx, json, originalJson, normalizedActivity, ldSignatureVerified, activity, recipient, inboxListeners, inboxContextFactory, listenerInboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, meterProvider, tracerProvider, idempotencyStrategy }) {
32
+ const logger = getLogger([
33
+ "fedify",
34
+ "federation",
35
+ "inbox"
36
+ ]);
37
+ if (activity.actorId == null) {
38
+ logger.error("Missing actor.", { activity: json });
39
+ span.setStatus({
40
+ code: SpanStatusCode.ERROR,
41
+ message: "Missing actor."
42
+ });
43
+ recordInboxActivity(meterProvider, "rejected", getTypeId(activity).href);
44
+ return "missingActor";
45
+ }
46
+ let cacheKey = null;
47
+ if (activity.id != null) {
48
+ const inboxContext = inboxContextFactory(recipient, json, activity.id?.href, getTypeId(activity).href);
49
+ const strategy = idempotencyStrategy ?? "per-inbox";
50
+ let keyString;
51
+ if (typeof strategy === "function") keyString = await strategy(inboxContext, activity);
52
+ else switch (strategy) {
53
+ case "global":
54
+ keyString = activity.id.href;
55
+ break;
56
+ case "per-origin":
57
+ keyString = `${ctx.origin}\n${activity.id.href}`;
58
+ break;
59
+ case "per-inbox":
60
+ keyString = `${ctx.origin}\n${activity.id.href}\n${recipient == null ? "sharedInbox" : `inbox\n${recipient}`}`;
61
+ break;
62
+ default: keyString = `${ctx.origin}\n${activity.id.href}`;
63
+ }
64
+ if (keyString != null) cacheKey = [...kvPrefixes.activityIdempotence, keyString];
65
+ }
66
+ if (cacheKey != null) {
67
+ if (await kv.get(cacheKey) === true) {
68
+ logger.debug("Activity {activityId} has already been processed.", {
69
+ activityId: activity.id?.href,
70
+ activity: json,
71
+ recipient
72
+ });
73
+ span.setStatus({
74
+ code: SpanStatusCode.UNSET,
75
+ message: `Activity ${activity.id?.href} has already been processed.`
76
+ });
77
+ recordInboxActivity(meterProvider, "rejected", getTypeId(activity).href);
78
+ return "alreadyProcessed";
79
+ }
80
+ }
81
+ span.setAttribute("activitypub.actor.id", activity.actorId.href);
82
+ if (queue != null) {
83
+ const carrier = {};
84
+ propagation.inject(context.active(), carrier);
85
+ try {
86
+ await queue.enqueue({
87
+ type: "inbox",
88
+ id: crypto.randomUUID(),
89
+ baseUrl: ctx.origin,
90
+ activity: originalJson ?? json,
91
+ ...normalizedActivity == null ? {} : { normalizedActivity },
92
+ ...ldSignatureVerified == null ? {} : { ldSignatureVerified },
93
+ identifier: recipient,
94
+ attempt: 0,
95
+ started: (/* @__PURE__ */ new Date()).toISOString(),
96
+ traceContext: carrier
97
+ });
98
+ } catch (error) {
99
+ logger.error("Failed to enqueue the incoming activity {activityId}:\n{error}", {
100
+ error,
101
+ activityId: activity.id?.href,
102
+ activity: json,
103
+ recipient
104
+ });
105
+ span.setStatus({
106
+ code: SpanStatusCode.ERROR,
107
+ message: `Failed to enqueue the incoming activity ${activity.id?.href}.`
108
+ });
109
+ throw error;
110
+ }
111
+ getFederationMetrics(meterProvider).recordQueueTaskEnqueued({
112
+ role: "inbox",
113
+ queue,
114
+ activityType: getTypeId(activity).href
115
+ }, 0);
116
+ recordInboxActivity(meterProvider, "queued", getTypeId(activity).href);
117
+ logger.info("Activity {activityId} is enqueued.", {
118
+ activityId: activity.id?.href,
119
+ activity: json,
120
+ recipient
121
+ });
122
+ return "enqueued";
123
+ }
124
+ tracerProvider = tracerProvider ?? trace.getTracerProvider();
125
+ return await tracerProvider.getTracer(name, version).startActiveSpan("activitypub.dispatch_inbox_listener", { kind: SpanKind.INTERNAL }, async (span) => {
126
+ const dispatched = inboxListeners?.dispatchWithClass(activity);
127
+ if (dispatched == null) {
128
+ logger.error("Unsupported activity type:\n{activity}", {
129
+ activity: json,
130
+ recipient
131
+ });
132
+ span.setStatus({
133
+ code: SpanStatusCode.UNSET,
134
+ message: `Unsupported activity type: ${getTypeId(activity).href}`
135
+ });
136
+ recordInboxActivity(meterProvider, "rejected", getTypeId(activity).href);
137
+ span.end();
138
+ return "unsupportedActivity";
139
+ }
140
+ const { class: cls, listener } = dispatched;
141
+ span.updateName(`activitypub.dispatch_inbox_listener ${cls.name}`);
142
+ try {
143
+ const activityType = getTypeId(activity).href;
144
+ const started = performance.now();
145
+ try {
146
+ const contextFactory = listenerInboxContextFactory ?? inboxContextFactory;
147
+ await listener(contextFactory(recipient, contextFactory === inboxContextFactory ? json : originalJson ?? json, activity.id?.href, activityType), activity);
148
+ } finally {
149
+ getFederationMetrics(meterProvider).recordInboxProcessingDuration(activityType, getDurationMs(started));
150
+ }
151
+ } catch (error) {
152
+ try {
153
+ await inboxErrorHandler?.(ctx, error);
154
+ } catch (error) {
155
+ logger.error("An unexpected error occurred in inbox error handler:\n{error}", {
156
+ error,
157
+ activityId: activity.id?.href,
158
+ activity: json,
159
+ recipient
160
+ });
161
+ }
162
+ logger.error("Failed to process the incoming activity {activityId}:\n{error}", {
163
+ error,
164
+ activityId: activity.id?.href,
165
+ activity: json,
166
+ recipient
167
+ });
168
+ span.setStatus({
169
+ code: SpanStatusCode.ERROR,
170
+ message: String(error)
171
+ });
172
+ recordInboxActivity(meterProvider, "rejected", getTypeId(activity).href);
173
+ span.end();
174
+ return "error";
175
+ }
176
+ recordInboxActivity(meterProvider, "processed", getTypeId(activity).href);
177
+ if (cacheKey != null) await kv.set(cacheKey, true, { ttl: Temporal.Duration.from({ days: 1 }) });
178
+ logger.info("Activity {activityId} has been processed.", {
179
+ activityId: activity.id?.href,
180
+ activity: json,
181
+ recipient
182
+ });
183
+ span.end();
184
+ return "success";
185
+ });
186
+ }
187
+ //#endregion
188
+ export { routeActivity as n, actorsHaveSameOrigin as t };
@@ -5,7 +5,7 @@ import { t as MAX_PORTABLE_KEY_TTL } from "./key-owner-evidence-APB6NpSk.mjs";
5
5
  import { CryptographicKey, Multikey } from "@fedify/vocab";
6
6
  import { FetchError, UrlError } from "@fedify/vocab-runtime";
7
7
  //#region src/federation/keycache.ts
8
- const KEY_CACHE_GENERATION = "2";
8
+ const KEY_CACHE_GENERATION = "3";
9
9
  const COMPATIBLE_KEY_SEGMENT = "__compatible";
10
10
  const MAX_OWNER_BYTES = 32 * 1024;
11
11
  function isCompatibleKeyEntry(value) {
@@ -1,7 +1,7 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { p as recordDocumentCache } from "./metrics-CRv6_IBg.mjs";
4
+ import { p as recordDocumentCache } from "./metrics-BguhdXt-.mjs";
5
5
  import { getLogger } from "@logtape/logtape";
6
6
  import { preloadedContexts } from "@fedify/vocab-runtime";
7
7
  //#region src/utils/kv-cache.ts
@@ -1,6 +1,6 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
- import { G as validateCryptoKey, ot as recordDocumentCache, t as doubleKnock } from "./http-Qq7jR2rC.js";
3
+ import { G as validateCryptoKey, ot as recordDocumentCache, t as doubleKnock } from "./http-D_AGUmSq.js";
4
4
  import { getLogger } from "@logtape/logtape";
5
5
  import { FetchError, UrlError, createActivityPubRequest, getRemoteDocument, logRequest, preloadedContexts, resolveDocumentLoaderTimeout, validatePublicUrl, withDocumentLoaderTimeout } from "@fedify/vocab-runtime";
6
6
  import { curry } from "es-toolkit";
@@ -62,6 +62,10 @@ function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedi
62
62
  }, options);
63
63
  }
64
64
  async function validateUrl(url, options) {
65
+ const parsedUrl = new URL(url);
66
+ const protocol = parsedUrl.protocol;
67
+ if (protocol !== "http:" && protocol !== "https:") throw new UrlError(`Unsupported protocol: ${protocol}`);
68
+ if (parsedUrl.username !== "" || parsedUrl.password !== "") throw new UrlError("URLs with credentials are not allowed");
65
69
  if (!allowPrivateAddress) {
66
70
  try {
67
71
  await validatePublicUrl(url);
@@ -1,5 +1,5 @@
1
1
  const { URLPattern } = require("urlpattern-polyfill");
2
- const require_http = require("./http-x1satM4y.cjs");
2
+ const require_http = require("./http-g5f1D2tm.cjs");
3
3
  let _logtape_logtape = require("@logtape/logtape");
4
4
  let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
5
5
  let es_toolkit = require("es-toolkit");
@@ -61,6 +61,10 @@ function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, maxRedi
61
61
  }, options);
62
62
  }
63
63
  async function validateUrl(url, options) {
64
+ const parsedUrl = new URL(url);
65
+ const protocol = parsedUrl.protocol;
66
+ if (protocol !== "http:" && protocol !== "https:") throw new _fedify_vocab_runtime.UrlError(`Unsupported protocol: ${protocol}`);
67
+ if (parsedUrl.username !== "" || parsedUrl.password !== "") throw new _fedify_vocab_runtime.UrlError("URLs with credentials are not allowed");
64
68
  if (!allowPrivateAddress) {
65
69
  try {
66
70
  await (0, _fedify_vocab_runtime.validatePublicUrl)(url);
@@ -3,8 +3,8 @@ import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { c as isPortableActorDocument, d as isPortableUri, l as isPortableId, m as parseKeyIdString, s as isCompatibleKeyId } from "./portable-key-id-Ca30-hOa.mjs";
5
5
  import { a as verificationObservation, i as triedKey, n as observeCheck, t as observeAttempt } from "./verification-Dpfcw8ml.mjs";
6
- import { n as version, t as name } from "./deno-BZ4z-lJJ.mjs";
7
- import { _ as recordKeyLookup, n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch, t as classifyFetchError } from "./metrics-CRv6_IBg.mjs";
6
+ import { n as version, t as name } from "./deno-CO1X_K_q.mjs";
7
+ import { _ as recordKeyLookup, n as getDurationMs, r as getFederationMetrics, s as measureSignatureKeyFetch, t as classifyFetchError } from "./metrics-BguhdXt-.mjs";
8
8
  import { n as attachKeyOwnerEvidence, t as MAX_PORTABLE_KEY_TTL } from "./key-owner-evidence-APB6NpSk.mjs";
9
9
  import { getLogger } from "@logtape/logtape";
10
10
  import { Activity, CryptographicKey, Multikey, Object as Object$1, getTypeId, isActor } from "@fedify/vocab";
@@ -154,16 +154,23 @@ async function importJwk(jwk, type) {
154
154
  /**
155
155
  * Fetches the document that the given actor URI dereferences to, and returns
156
156
  * the actor only if the host that served the document is authoritative for
157
- * the id the document claims.
157
+ * the id the document claims, and that id is the actor that was asked for.
158
158
  *
159
159
  * Only the origin that serves an actor id can speak for it. Without that
160
160
  * rule any host could serve a document describing somebody else's actor—and
161
161
  * listing its own keys as that actor's.
162
162
  *
163
+ * The returned actor always has exactly the requested `actorId` as its `id`.
164
+ * A document loader follows redirects and alternate document links, so the
165
+ * document an actor id leads to may describe a different actor altogether,
166
+ * on the same origin or on another; accepting it would attribute that other
167
+ * actor's keys to the requested one. See GHSA-8863-8468-7hq7.
168
+ *
163
169
  * @param actorId The URI of the actor to fetch.
164
170
  * @param options Options for fetching the document.
165
171
  * @returns The actor, or `null` if the document cannot be fetched, is not an
166
- * actor, or belongs to another origin.
172
+ * actor, belongs to another origin, or describes an actor other
173
+ * than the requested one.
167
174
  * @internal
168
175
  */
169
176
  async function fetchActorDocument(actorId, options = {}) {
@@ -229,6 +236,13 @@ async function fetchActorDocumentInternal(actorId, options, propagateContextFetc
229
236
  });
230
237
  return null;
231
238
  }
239
+ if (object.id.href !== actorId.href) {
240
+ logger.debug("The document fetched for the actor {actorId} is the actor {documentActorId} instead; refusing to treat it as the requested actor's document.", {
241
+ actorId: actorId.href,
242
+ documentActorId: object.id.href
243
+ });
244
+ return null;
245
+ }
232
246
  return object;
233
247
  }
234
248
  /**
@@ -1,7 +1,7 @@
1
1
  import "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-BZ4z-lJJ.mjs";
4
+ import { n as version, t as name } from "./deno-CO1X_K_q.mjs";
5
5
  import { metrics } from "@opentelemetry/api";
6
6
  import { FetchError } from "@fedify/vocab-runtime";
7
7
  //#region src/federation/metrics.ts