@fedify/fedify 2.4.0-dev.2093 → 2.4.0-dev.2103

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 (84) hide show
  1. package/dist/{builder-Ld4Y9ymR.mjs → builder-CHht75pX.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/{compound-proof-CTx_lewQ.mjs → compound-proof-DKKH_Nr1.mjs} +2 -2
  6. package/dist/{context-BSywYspi.d.ts → context-CfJE_oQu.d.ts} +78 -13
  7. package/dist/{context-DlZhxdFT.mjs → context-CoNRxGYp.mjs} +2 -1
  8. package/dist/{context-CZjZYXFC.d.cts → context-mDTxTw-S.d.cts} +78 -13
  9. package/dist/{deno-8t8k_6Yv.mjs → deno-CkjiYK_Z.mjs} +1 -1
  10. package/dist/{docloader-DJKcsAiF.mjs → docloader-C1hAJnWc.mjs} +2 -2
  11. package/dist/federation/builder.test.mjs +1 -1
  12. package/dist/federation/compound-fanout.test.mjs +3 -3
  13. package/dist/federation/handler-compound-proof.test.mjs +4 -4
  14. package/dist/federation/handler.test.mjs +6 -6
  15. package/dist/federation/hashlink-media.test.mjs +2 -2
  16. package/dist/federation/idempotency.test.mjs +2 -2
  17. package/dist/federation/metrics.test.mjs +1 -1
  18. package/dist/federation/middleware.test.mjs +8 -8
  19. package/dist/federation/mod.cjs +1 -1
  20. package/dist/federation/mod.d.cts +2 -2
  21. package/dist/federation/mod.d.ts +2 -2
  22. package/dist/federation/mod.js +1 -1
  23. package/dist/federation/outgoing-proof.test.mjs +5 -5
  24. package/dist/federation/portable-actor.test.d.mts +2 -0
  25. package/dist/federation/portable-actor.test.mjs +446 -0
  26. package/dist/federation/portable-gateway-keys.test.mjs +4 -4
  27. package/dist/federation/portable-inbox.test.mjs +396 -10
  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 +2 -2
  31. package/dist/federation/tasks/tasks.test.mjs +3 -3
  32. package/dist/federation/temporal.test.mjs +1 -1
  33. package/dist/federation/webfinger.test.mjs +2 -2
  34. package/dist/{federation-aSHO04ob.cjs → federation-C1dL6SiT.cjs} +1267 -1086
  35. package/dist/{http-B2ftY0Ha.mjs → http-BU-8fUrz.mjs} +82 -7
  36. package/dist/{http-DSaSAN7X.cjs → http-Cala2Grs.cjs} +89 -2
  37. package/dist/{http-DyT7DP1Y.js → http-tTlTeFa0.js} +78 -3
  38. package/dist/{key-Dwm4VswT.mjs → key-I8gLBkcf.mjs} +2 -2
  39. package/dist/{kv-cache-BSJAlP4C.cjs → kv-cache-Bl5EBFrU.cjs} +1 -1
  40. package/dist/{kv-cache-DmroAz3M.js → kv-cache-CxQu_cd4.js} +1 -1
  41. package/dist/{kv-cache-D0yYukqk.mjs → kv-cache-DJFoGRWQ.mjs} +1 -1
  42. package/dist/{ld-BYZOoDzZ.mjs → ld-wiJbqJHa.mjs} +3 -3
  43. package/dist/{metrics-B7YwKTFr.mjs → metrics-BCAa5-kE.mjs} +1 -1
  44. package/dist/{middleware-BNeqKSsL.mjs → middleware-6p2dYEVA.mjs} +1 -1
  45. package/dist/{middleware-Blq0882d.js → middleware-B6IVTVL0.js} +1362 -1181
  46. package/dist/{middleware-E_0IH72m.mjs → middleware-FnITXhtz.mjs} +549 -368
  47. package/dist/{mod-JzkaEXsi.d.cts → mod-B3OzOobN.d.cts} +1 -1
  48. package/dist/{mod-B2h2KY0q.d.ts → mod-BYvPz_T0.d.ts} +1 -1
  49. package/dist/mod.cjs +5 -5
  50. package/dist/mod.d.cts +2 -2
  51. package/dist/mod.d.ts +2 -2
  52. package/dist/mod.js +5 -5
  53. package/dist/nodeinfo/handler.test.mjs +2 -2
  54. package/dist/{owner-Cgx2Dc7I.mjs → owner-BJp47LxN.mjs} +3 -3
  55. package/dist/{owner-szIZl4qI.cjs → owner-DCt8W_Cv.cjs} +1 -1
  56. package/dist/{owner-DAGSC5gX.js → owner-j5yrbXS1.js} +1 -1
  57. package/dist/{portable-key-Cn1WJSN1.mjs → portable-key-B5oKE_wZ.mjs} +1 -1
  58. package/dist/{proof-DVXC6nYz.mjs → proof-BE4T5zF6.mjs} +4 -4
  59. package/dist/{send-D-hOvYKy.mjs → send-BFITuKHw.mjs} +3 -3
  60. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  61. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  62. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  63. package/dist/sig/compound-proof.test.mjs +2 -2
  64. package/dist/sig/http.test.mjs +72 -2
  65. package/dist/sig/key.test.mjs +1 -1
  66. package/dist/sig/ld.test.mjs +2 -2
  67. package/dist/sig/mod.cjs +3 -3
  68. package/dist/sig/mod.js +3 -3
  69. package/dist/sig/owner.test.mjs +1 -1
  70. package/dist/sig/portable-collection.test.mjs +2 -2
  71. package/dist/sig/portable-key.test.mjs +3 -3
  72. package/dist/sig/proof.test.mjs +3 -3
  73. package/dist/{sig-B5Ell7pO.js → sig-CJHdGk9w.js} +2 -2
  74. package/dist/{sig-CXOKN4Br.cjs → sig-CRTRoouH.cjs} +2 -2
  75. package/dist/{temporal-CqpKGEPk.mjs → temporal-CyDDbE4i.mjs} +1 -1
  76. package/dist/testing/kv.test.mjs +1 -1
  77. package/dist/testing/mod.d.mts +78 -13
  78. package/dist/testing/mod.mjs +2 -2
  79. package/dist/{testing-CO_cEvQc.mjs → testing-BWq1dCzx.mjs} +1 -1
  80. package/dist/utils/docloader.test.mjs +2 -2
  81. package/dist/utils/kv-cache.test.mjs +1 -1
  82. package/dist/utils/mod.cjs +1 -1
  83. package/dist/utils/mod.js +1 -1
  84. package/package.json +7 -7
@@ -2,10 +2,10 @@ import { Temporal } from "temporal-polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
3
  import { t as __exportAll } from "./rolldown-runtime-BC-Eo5Mj.js";
4
4
  import { r as getDefaultActivityTransformers } from "./transformers-DAtXl_Kc.js";
5
- import { $ as recordFanoutRecipients, A as verifyCompactJsonLd, B as validateCryptoKey, C as detachSignature, D as isClearlyMalformedContextReference, E as hasSignatureLike, G as instrumentDocumentLoader, H as getDurationMs, J as recordCollectionDispatchDuration, K as isAbortError, N as wrapContextLoaderForJsonLd, O as isInvalidUrlTypeError, P as exportJwk, T as hasSignature, U as getFederationMetrics, W as getRemoteHost, X as recordCollectionRequest, Y as recordCollectionPageItems, Z as recordCollectionTotalItems, _ as normalizeOutgoingActivityJsonLd, a as verifyRequestDetailed, at as preloadedOnlyDocumentLoader, d as verifyMapLocalProof, dt as getGatewayKeyBase, et as recordInboxActivity, f as verifyObject, ft as getPortableDid, gt as isPortableUri, h as verifyPortableObjectProofWithRoot, ht as isPortableId, i as verifyRequest, it as registerQueueDepthGauge, k as signJsonLd, l as hasProofLike, m as verifyPortableObjectProofPolicy, mt as isCompatibleKeyId, n as parseRfc9421SignatureInput, nt as recordOutboxEnqueue, ot as formatAcceptSignature, p as verifyPortableObjectProof, pt as hasPortableActor, q as recordCircuitBreakerStateChange, rt as recordWebFingerHandle, s as InvalidPortableObjectIdError, t as doubleKnock, tt as recordOutboxActivity, u as signObject, ut as getCanonicalPortableId, v as InvalidContextReferenceError, vt as name, w as getNormalizationContextLoader, x as compactJsonLd, y as assertSafeJsonLd, yt as version, z as importJwk } from "./http-DyT7DP1Y.js";
5
+ import { $ as recordCollectionTotalItems, A as isInvalidUrlTypeError, C as compactJsonLd, D as hasSignature, E as getNormalizationContextLoader, F as wrapContextLoaderForJsonLd, G as getFederationMetrics, H as validateCryptoKey, I as exportJwk, J as isAbortError, K as getRemoteHost, M as verifyCompactJsonLd, O as hasSignatureLike, Q as recordCollectionRequest, T as detachSignature, V as importJwk, W as getDurationMs, X as recordCollectionDispatchDuration, Y as recordCircuitBreakerStateChange, Z as recordCollectionPageItems, _ as verifyPortableObjectProofWithRoot, _t as isPortableId, at as recordWebFingerHandle, b as InvalidContextReferenceError, bt as name, ct as formatAcceptSignature, d as hasProofLike, f as signObject, ft as getCanonicalPortableId, g as verifyPortableObjectProofPolicy, gt as isCompatibleKeyId, h as verifyPortableObjectProof, ht as hasPortableActor, i as selectRequestSignature, it as recordOutboxEnqueue, j as signJsonLd, k as isClearlyMalformedContextReference, l as InvalidPortableObjectIdError, m as verifyObject, mt as getPortableDid, n as listRequestSignatures, nt as recordInboxActivity, o as verifyRequest, ot as registerQueueDepthGauge, p as verifyMapLocalProof, pt as getGatewayKeyBase, q as instrumentDocumentLoader, r as parseRfc9421SignatureInput, rt as recordOutboxActivity, s as verifyRequestDetailed, st as preloadedOnlyDocumentLoader, t as doubleKnock, tt as recordFanoutRecipients, vt as isPortableUri, x as assertSafeJsonLd, xt as version, y as normalizeOutgoingActivityJsonLd } from "./http-tTlTeFa0.js";
6
6
  import { a as MAX_BODY_SIZE, i as BodyTooLargeError, n as getNodeInfo, o as readBoundedText, t as nodeInfoToJson } from "./types-C_cNlNVK.js";
7
- import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-DAGSC5gX.js";
8
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-DmroAz3M.js";
7
+ import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-j5yrbXS1.js";
8
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-CxQu_cd4.js";
9
9
  import { getLogger, withContext } from "@logtape/logtape";
10
10
  import { Router, RouterError, assertPath } from "@fedify/uri-template";
11
11
  import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
@@ -2408,6 +2408,8 @@ function acceptsJsonLd(request) {
2408
2408
  const PORTABLE_OBJECT_CONTENT_TYPE = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"";
2409
2409
  const GATEWAY_OBJECT_PATH_PATTERN = /^\/\.well-known\/apgateway\/did(?::|%3A)/i;
2410
2410
  const BARE_DID_PATTERN = /^did:[a-z0-9]+:[^/?#]+$/i;
2411
+ const DID_KEY_PREFIX = "did:key:";
2412
+ const BASE58BTC_MULTIBASE_PATTERN = /^z[1-9A-HJ-NP-Za-km-z]+$/;
2411
2413
  const GATEWAY_PATH_PREFIX = "/.well-known/apgateway/";
2412
2414
  const HASHLINK_SCHEME_PATTERN = /^hl(?::|%3A)/i;
2413
2415
  /**
@@ -2443,6 +2445,15 @@ function parsePortableGatewayRequest(url) {
2443
2445
  throw error;
2444
2446
  }
2445
2447
  const authority = getFe34Origin(id);
2448
+ try {
2449
+ assertBase58BtcDidKey(authority);
2450
+ } catch (error) {
2451
+ if (error instanceof TypeError) return {
2452
+ type: "malformed",
2453
+ error
2454
+ };
2455
+ throw error;
2456
+ }
2446
2457
  const href = id.href;
2447
2458
  return {
2448
2459
  type: "object",
@@ -2461,12 +2472,26 @@ function parsePortableGatewayRequest(url) {
2461
2472
  * @param authority The bare DID, e.g., `did:key:z6Mk...`.
2462
2473
  * @param path The object path, e.g., `/notes/123`.
2463
2474
  * @returns The portable ID.
2464
- * @throws {TypeError} If the authority is not a bare DID.
2475
+ * @throws {TypeError} If the authority is not a bare DID, or it is
2476
+ * a `did:key` DID that is not encoded in base58-btc.
2465
2477
  */
2466
2478
  function buildPortableUri(authority, path) {
2467
2479
  if (typeof authority !== "string" || !BARE_DID_PATTERN.test(authority)) throw new TypeError("The authority of a portable ID must be a DID without a path, query, or fragment.");
2468
- getFe34Origin(authority);
2469
- return parseIri(`ap+ef61://${authority}${path}`);
2480
+ const did = getFe34Origin(authority);
2481
+ assertBase58BtcDidKey(did);
2482
+ return parseIri(`ap+ef61://${did}${path}`);
2483
+ }
2484
+ /**
2485
+ * Checks that a normalized DID, if it is a `did:key` DID, is encoded in
2486
+ * base58-btc, as FEP-ef61 requires so that the same key does not yield two
2487
+ * portable IDs. This only checks the encoding, not whether the DID is
2488
+ * a valid key.
2489
+ * @param did The DID normalized by `getFe34Origin()`.
2490
+ * @throws {TypeError} If the DID is a `did:key` DID that is not encoded in
2491
+ * base58-btc.
2492
+ */
2493
+ function assertBase58BtcDidKey(did) {
2494
+ if (did.startsWith(DID_KEY_PREFIX) && !BASE58BTC_MULTIBASE_PATTERN.test(did.slice(8))) throw new TypeError("The did:key authority of a portable ID must be encoded in base58-btc, i.e., start with z.");
2470
2495
  }
2471
2496
  /**
2472
2497
  * Recognizes an FEP-ef61 gateway request for a resource addressed by
@@ -2518,317 +2543,1017 @@ function parseHashlinkGatewayRequest(url) {
2518
2543
  };
2519
2544
  }
2520
2545
  //#endregion
2521
- //#region src/federation/temporal.ts
2522
- function isPlainObject$1(value) {
2523
- return typeof value === "object" && value != null && !Array.isArray(value);
2524
- }
2525
- function normalizeDateTimeLiteral(value) {
2526
- return value.substring(19).match(/[Z+-]/) ? value : value + "Z";
2527
- }
2528
- function isMalformedDateTimeLiteral(value) {
2529
- if (typeof value !== "string") return false;
2530
- try {
2531
- Temporal.Instant.from(normalizeDateTimeLiteral(value));
2532
- return false;
2533
- } catch {
2534
- return true;
2535
- }
2536
- }
2537
- function isMalformedDurationLiteral(value) {
2538
- if (typeof value !== "string") return false;
2539
- try {
2540
- Temporal.Duration.from(value);
2541
- return false;
2542
- } catch {
2543
- return true;
2546
+ //#region src/federation/send.ts
2547
+ /**
2548
+ * Extracts the inbox URLs from recipients.
2549
+ * @param parameters The parameters to extract the inboxes.
2550
+ * See also {@link ExtractInboxesParameters}.
2551
+ * @returns The inboxes as a map of inbox URL to actor URIs.
2552
+ */
2553
+ function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris }) {
2554
+ const inboxes = {};
2555
+ for (const recipient of recipients) {
2556
+ let inbox;
2557
+ let sharedInbox = false;
2558
+ if (preferSharedInbox && recipient.endpoints?.sharedInbox != null) {
2559
+ inbox = recipient.endpoints.sharedInbox;
2560
+ sharedInbox = true;
2561
+ } else inbox = recipient.inboxId;
2562
+ if (inbox != null && recipient.id != null) {
2563
+ if (excludeBaseUris != null && excludeBaseUris.some((u) => u.origin === inbox?.origin)) continue;
2564
+ inboxes[inbox.href] ??= {
2565
+ actorIds: /* @__PURE__ */ new Set(),
2566
+ sharedInbox
2567
+ };
2568
+ inboxes[inbox.href].actorIds.add(recipient.id.href);
2569
+ }
2544
2570
  }
2571
+ return inboxes;
2545
2572
  }
2546
- const TEMPORAL_DATE_TIME_IRIS = /* @__PURE__ */ new Set([
2547
- "https://www.w3.org/ns/activitystreams#deleted",
2548
- "https://www.w3.org/ns/activitystreams#endTime",
2549
- "https://www.w3.org/ns/activitystreams#published",
2550
- "https://www.w3.org/ns/activitystreams#startTime",
2551
- "https://www.w3.org/ns/activitystreams#updated",
2552
- "http://purl.org/dc/terms/created",
2553
- "https://w3id.org/security#created"
2554
- ]);
2555
- const TEMPORAL_DURATION_IRIS = /* @__PURE__ */ new Set(["https://www.w3.org/ns/activitystreams#duration"]);
2556
- const QUESTION_CLOSED_IRI = "https://www.w3.org/ns/activitystreams#closed";
2557
- const XSD_DATE_TIME_IRI = "http://www.w3.org/2001/XMLSchema#dateTime";
2558
- function hasMalformedExpandedDateTimeLiteral(value) {
2559
- if (Array.isArray(value)) return value.some(hasMalformedExpandedDateTimeLiteral);
2560
- return isPlainObject$1(value) && "@value" in value && isMalformedDateTimeLiteral(value["@value"]);
2561
- }
2562
- function hasMalformedExpandedQuestionClosedLiteral(value) {
2563
- if (Array.isArray(value)) return value.some(hasMalformedExpandedQuestionClosedLiteral);
2564
- if (!isPlainObject$1(value) || !("@value" in value)) return false;
2565
- const literal = value["@value"];
2566
- if (typeof literal === "boolean") return false;
2567
- if (typeof literal !== "string") return false;
2568
- if (value["@type"] !== XSD_DATE_TIME_IRI) return false;
2569
- if (new Date(literal).toString() === "Invalid Date") return false;
2570
- return isMalformedDateTimeLiteral(literal);
2573
+ /**
2574
+ * Sends an {@link Activity} to an inbox.
2575
+ *
2576
+ * @param parameters The parameters for sending the activity.
2577
+ * See also {@link SendActivityParameters}.
2578
+ * @throws {Error} If the activity fails to send.
2579
+ */
2580
+ function sendActivity(options) {
2581
+ const tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
2582
+ return tracerProvider.getTracer(name, version).startActiveSpan("activitypub.send_activity", {
2583
+ kind: SpanKind.CLIENT,
2584
+ attributes: { "activitypub.shared_inbox": options.sharedInbox ?? false }
2585
+ }, async (span) => {
2586
+ if (options.activityId != null) span.setAttribute("activitypub.activity.id", options.activityId);
2587
+ if (options.activityType != null) span.setAttribute("activitypub.activity.type", options.activityType);
2588
+ try {
2589
+ await sendActivityInternal({
2590
+ ...options,
2591
+ tracerProvider
2592
+ }, span);
2593
+ } catch (e) {
2594
+ span.setStatus({
2595
+ code: SpanStatusCode.ERROR,
2596
+ message: String(e)
2597
+ });
2598
+ throw e;
2599
+ } finally {
2600
+ span.end();
2601
+ }
2602
+ });
2571
2603
  }
2572
- function hasMalformedExpandedDurationLiteral(value) {
2573
- if (Array.isArray(value)) return value.some(hasMalformedExpandedDurationLiteral);
2574
- return isPlainObject$1(value) && "@value" in value && isMalformedDurationLiteral(value["@value"]);
2604
+ const MAX_ERROR_RESPONSE_BODY_BYTES = 1024;
2605
+ function getActivityActorId(activity) {
2606
+ if (!isRecord(activity)) return void 0;
2607
+ return getIdValue(activity.actor);
2575
2608
  }
2576
- function hasMalformedKnownTemporalLiteralInternal(value, visited) {
2577
- if (Array.isArray(value)) return value.some((item) => hasMalformedKnownTemporalLiteralInternal(item, visited));
2578
- if (!isPlainObject$1(value)) return false;
2579
- if (visited.has(value)) return false;
2580
- visited.add(value);
2581
- if ("@value" in value) return false;
2582
- for (const [key, child] of Object.entries(value)) {
2583
- if (TEMPORAL_DATE_TIME_IRIS.has(key)) {
2584
- if (hasMalformedExpandedDateTimeLiteral(child)) return true;
2585
- continue;
2586
- }
2587
- if (key === QUESTION_CLOSED_IRI) {
2588
- if (hasMalformedExpandedQuestionClosedLiteral(child)) return true;
2589
- continue;
2590
- }
2591
- if (TEMPORAL_DURATION_IRIS.has(key)) {
2592
- if (hasMalformedExpandedDurationLiteral(child)) return true;
2593
- continue;
2609
+ function getIdValue(value) {
2610
+ if (typeof value === "string" && value !== "") return value;
2611
+ if (value instanceof URL) return value.href;
2612
+ if (Array.isArray(value)) {
2613
+ for (const item of value) {
2614
+ const id = getIdValue(item);
2615
+ if (id != null) return id;
2594
2616
  }
2595
- if (hasMalformedKnownTemporalLiteralInternal(child, visited)) return true;
2617
+ return;
2596
2618
  }
2597
- return false;
2619
+ if (isRecord(value)) return getIdValue(value.id);
2598
2620
  }
2599
- async function hasMalformedKnownTemporalLiteral(value, contextLoader) {
2621
+ function isRecord(value) {
2622
+ return typeof value === "object" && value != null;
2623
+ }
2624
+ async function readLimitedResponseBody(response, maxBytes) {
2625
+ if (response.body == null) return "";
2626
+ const reader = response.body.getReader();
2627
+ const decoder = new TextDecoder();
2628
+ const chunks = [];
2629
+ let totalBytes = 0;
2630
+ let truncated = false;
2600
2631
  try {
2601
- return hasMalformedKnownTemporalLiteralInternal(await jsonld.expand(value, {
2602
- documentLoader: getNormalizationContextLoader(contextLoader),
2603
- keepFreeFloatingNodes: true
2604
- }), /* @__PURE__ */ new Set());
2605
- } catch {
2606
- return false;
2632
+ while (true) {
2633
+ const { done, value } = await reader.read();
2634
+ if (done) break;
2635
+ if (totalBytes + value.length > maxBytes) {
2636
+ const remaining = maxBytes - totalBytes;
2637
+ if (remaining > 0) chunks.push(decoder.decode(value.slice(0, remaining), { stream: true }));
2638
+ truncated = true;
2639
+ break;
2640
+ }
2641
+ chunks.push(decoder.decode(value, { stream: true }));
2642
+ totalBytes += value.length;
2643
+ }
2644
+ } finally {
2645
+ reader.releaseLock();
2607
2646
  }
2647
+ let result = chunks.join("");
2648
+ if (truncated) result += "… (truncated)";
2649
+ return result;
2608
2650
  }
2609
- //#endregion
2610
- //#region src/federation/handler.ts
2611
- const rawInboxContextFactorySymbol = Symbol("fedify.rawInboxContextFactory");
2612
- const INBOX_COMPOUND_PROOF_LIMITS = {
2613
- maxDepth: 64,
2614
- maxMaps: 1e4,
2615
- maxProofs: 32,
2616
- maxBytes: 10 * 1024 * 1024
2617
- };
2618
- function isRemoteContextLoadingFailure$1(error) {
2619
- return error instanceof Error && typeof error.details === "object" && error.details != null && error.details.code === "loading remote context failed";
2620
- }
2621
- function isPermanentRemoteContextError$1(error) {
2622
- if (!(error instanceof Error) || error.name !== "jsonld.InvalidUrl") return false;
2623
- const details = error.details;
2624
- if (details?.code === "invalid remote context") return true;
2625
- return isRemoteContextLoadingFailure$1(error) && typeof details?.url === "string" && !URL.canParse(details.url) && isClearlyMalformedContextReference(details.url);
2626
- }
2627
- function isInvalidJsonLdError(error) {
2628
- if (!(error instanceof Error)) return false;
2629
- const name = error.name;
2630
- return name === "UnsafeJsonLdError" || error instanceof InvalidContextReferenceError || isPermanentRemoteContextError$1(error) || name === "jsonld.SyntaxError" && !isRemoteContextLoadingFailure$1(error);
2631
- }
2632
- function isValidationTypeError(error) {
2633
- return error instanceof TypeError && (/^(Invalid JSON-LD:|Invalid type:|Unexpected type:|Invalid @id:)/.test(error.message) || isInvalidUrlTypeError(error));
2634
- }
2635
- function isPermanentActivityParseError(error) {
2636
- return isInvalidJsonLdError(error) || isValidationTypeError(error);
2637
- }
2638
- function hasHttpSignatureHeaders(request) {
2639
- return request.headers.has("Signature") || request.headers.has("Signature-Input");
2640
- }
2641
- function hasObjectIntegrityProof(json) {
2642
- return typeof json === "object" && json != null && "proof" in json;
2643
- }
2644
- /**
2645
- * Handles an actor request.
2646
- * @template TContextData The context data to pass to the context.
2647
- * @param request The HTTP request.
2648
- * @param parameters The parameters for handling the actor.
2649
- * @returns A promise that resolves to an HTTP response.
2650
- */
2651
- async function handleActor(request, { identifier, context, actorDispatcher, authorizePredicate, onNotFound, onUnauthorized }) {
2651
+ async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, allowPrivateAddress, headers, specDeterminer, meterProvider, tracerProvider }, span) {
2652
2652
  const logger = getLogger([
2653
2653
  "fedify",
2654
2654
  "federation",
2655
- "actor"
2655
+ "outbox"
2656
2656
  ]);
2657
- if (actorDispatcher == null) {
2658
- logger.debug("Actor dispatcher is not set.", { identifier });
2659
- return await onNotFound(request);
2660
- }
2661
- const actor = await actorDispatcher(context, identifier);
2662
- if (actor == null) {
2663
- logger.debug("Actor {identifier} not found.", { identifier });
2664
- return await onNotFound(request);
2665
- }
2666
- if (authorizePredicate != null) {
2667
- if (!await authorizePredicate(context, identifier)) return await onUnauthorized(request);
2657
+ const federationMetrics = getFederationMetrics(meterProvider);
2658
+ const started = performance.now();
2659
+ let deliverySuccess = false;
2660
+ async function validateUrl(url) {
2661
+ if (!allowPrivateAddress) try {
2662
+ await validatePublicUrl(url);
2663
+ } catch (error) {
2664
+ if (error instanceof UrlError) logger.error("Disallowed private URL: {url}", {
2665
+ url,
2666
+ error
2667
+ });
2668
+ throw error;
2669
+ }
2668
2670
  }
2669
- if (actor instanceof Tombstone) {
2670
- const jsonLd = await actor.toJsonLd(context);
2671
- return new Response(JSON.stringify(jsonLd), {
2672
- status: 410,
2673
- headers: {
2674
- "Content-Type": "application/activity+json",
2675
- Vary: "Accept"
2676
- }
2677
- });
2678
- }
2679
- const jsonLd = await actor.toJsonLd(context);
2680
- return new Response(JSON.stringify(jsonLd), { headers: {
2681
- "Content-Type": "application/activity+json",
2682
- Vary: "Accept"
2683
- } });
2684
- }
2685
- /**
2686
- * Handles an object request.
2687
- * @template TContextData The context data to pass to the context.
2688
- * @param request The HTTP request.
2689
- * @param parameters The parameters for handling the object.
2690
- * @returns A promise that resolves to an HTTP response.
2691
- */
2692
- async function handleObject(request, { values, context, objectDispatcher, authorizePredicate, onNotFound, onUnauthorized }) {
2693
- if (objectDispatcher == null) return await onNotFound(request);
2694
- const object = await objectDispatcher(context, values);
2695
- if (object == null) return await onNotFound(request);
2696
- if (authorizePredicate != null) {
2697
- if (!await authorizePredicate(context, values)) return await onUnauthorized(request);
2698
- }
2699
- const jsonLd = await object.toJsonLd(context);
2700
- return new Response(JSON.stringify(jsonLd), { headers: {
2701
- "Content-Type": "application/activity+json",
2702
- Vary: "Accept"
2703
- } });
2704
- }
2705
- /**
2706
- * Handles an FEP-ef61 gateway request for a portable object through an object
2707
- * dispatcher. The object is served only if its ID canonically matches the
2708
- * requested portable ID and it satisfies the FEP-ef61 proof policy.
2709
- * @template TContextData The context data to pass to the context.
2710
- * @param request The HTTP request.
2711
- * @param parameters The parameters for handling the portable object.
2712
- * @returns A promise that resolves to an HTTP response.
2713
- */
2714
- async function handlePortableObject(request, { values, context, objectDispatcher, authorizePredicate, canonicalId, onNotFound, onUnauthorized }) {
2715
- const logger = getLogger([
2716
- "fedify",
2717
- "federation",
2718
- "object"
2719
- ]);
2720
- if (objectDispatcher == null) return await onNotFound(request);
2721
- const object = await objectDispatcher(context, values);
2722
- if (object == null) return await onNotFound(request);
2723
- if (!isRequestedPortableObject(object, canonicalId)) {
2724
- logger.debug("The object {objectId} does not match the requested portable object {portableId}.", {
2725
- objectId: object.id?.href,
2726
- portableId: canonicalId
2727
- });
2728
- return await onNotFound(request);
2729
- }
2730
- if (authorizePredicate != null) {
2731
- if (!await authorizePredicate(context, values)) return await onUnauthorized(request);
2671
+ await validateUrl(inbox.href);
2672
+ headers = new Headers(headers);
2673
+ headers.set("Content-Type", "application/activity+json");
2674
+ const request = new Request(inbox, {
2675
+ method: "POST",
2676
+ headers,
2677
+ body: JSON.stringify(activity),
2678
+ redirect: "manual"
2679
+ });
2680
+ let rsaKey = null;
2681
+ for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
2682
+ rsaKey = key;
2683
+ break;
2732
2684
  }
2733
- let jsonLd;
2734
- let servedId;
2735
- let result;
2685
+ if (rsaKey == null && keys.length < 1) logger.debug("Sending the activity {activityId} to {inbox} without a signature.", {
2686
+ activityId,
2687
+ inbox: inbox.href
2688
+ });
2689
+ 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.", {
2690
+ inbox: inbox.href,
2691
+ keys: keys.map((pair) => ({
2692
+ keyId: pair.keyId.href,
2693
+ privateKey: pair.privateKey
2694
+ }))
2695
+ });
2696
+ let response;
2736
2697
  try {
2737
- jsonLd = await object.toJsonLd(context);
2738
- servedId = await getJsonLdRootId(jsonLd, context.contextLoader);
2739
- result = await verifyPortableObjectProof(jsonLd, {
2740
- contextLoader: context.contextLoader,
2741
- documentLoader: context.documentLoader,
2742
- tracerProvider: context.tracerProvider,
2743
- meterProvider: context.meterProvider
2698
+ response = rsaKey == null ? await fetchWithValidatedRedirects(request, validateUrl) : await doubleKnock(request, rsaKey, {
2699
+ tracerProvider,
2700
+ specDeterminer,
2701
+ validateRedirect: validateUrl
2744
2702
  });
2745
2703
  } catch (error) {
2746
- logger.error("Failed to verify the Object Integrity Proofs of the portable object {portableId}:\n{error}", {
2747
- portableId: canonicalId,
2748
- error
2749
- });
2750
- return portableObjectInternalServerError(request);
2751
- }
2752
- if (servedId == null || !isRequestedPortableId(servedId, canonicalId)) {
2753
- logger.debug("The serialized object does not match the requested portable object {portableId}.", { portableId: canonicalId });
2754
- return await onNotFound(request);
2755
- }
2756
- if (!result.verified && result.reason.type !== "unsecuredCollection" && result.reason.type !== "unsupportedObjectType") {
2757
- logger.error("Refusing to serve the portable object {portableId}, as it does not satisfy the FEP-ef61 proof policy: {reason}. Portable actors, activities, and objects need an Object Integrity Proof made with a key of the DID in their ID.", {
2758
- portableId: canonicalId,
2759
- reason: result.reason.type
2704
+ const failure = error instanceof FetchError || error instanceof UrlError ? error : createFetchError(inbox.href, error);
2705
+ logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
2706
+ activityId,
2707
+ inbox: inbox.href,
2708
+ error: failure
2760
2709
  });
2761
- return portableObjectInternalServerError(request);
2710
+ federationMetrics.recordDelivery(inbox, getDurationMs(started), false, activityType);
2711
+ throw failure;
2762
2712
  }
2763
- return new Response(request.method === "HEAD" ? null : JSON.stringify(jsonLd), { headers: {
2764
- "Content-Type": PORTABLE_OBJECT_CONTENT_TYPE,
2765
- Vary: "Accept"
2766
- } });
2767
- }
2768
- function isRequestedPortableObject(object, canonicalId) {
2769
- return object.id != null && getCanonicalPortableId(object.id) === canonicalId;
2770
- }
2771
- async function getJsonLdRootId(jsonLd, contextLoader) {
2772
- const expanded = await jsonld.expand(jsonLd, {
2773
- documentLoader: getNormalizationContextLoader(contextLoader),
2774
- keepFreeFloatingNodes: true
2775
- });
2776
- if (expanded.length !== 1) return null;
2777
- const id = expanded[0]["@id"];
2778
- return typeof id === "string" ? id : null;
2779
- }
2780
- function isRequestedPortableId(id, canonicalId) {
2781
- if (!/^ap(?:\+ef61)?:\/\//i.test(id)) return URL.canParse(id) && getCanonicalPortableId(new URL(id)) === canonicalId;
2782
2713
  try {
2783
- return canonicalizePortableUri(id) === canonicalId;
2784
- } catch (error) {
2785
- if (error instanceof TypeError) return false;
2786
- throw error;
2714
+ if (!response.ok) {
2715
+ let error;
2716
+ try {
2717
+ error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
2718
+ } catch (_) {
2719
+ error = "";
2720
+ }
2721
+ logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
2722
+ activityId,
2723
+ inbox: inbox.href,
2724
+ status: response.status,
2725
+ statusText: response.statusText,
2726
+ error
2727
+ });
2728
+ throw new SendActivityError(inbox, response.status, `Failed to send activity ${activityId} to ${inbox.href} (${response.status} ${response.statusText}):\n${error}`, error, response.headers);
2729
+ }
2730
+ deliverySuccess = true;
2731
+ const eventAttributes = {
2732
+ "activitypub.inbox.url": inbox.href,
2733
+ "activitypub.activity.id": activityId ?? ""
2734
+ };
2735
+ if (activityType != null) eventAttributes["activitypub.activity.type"] = activityType;
2736
+ const actorId = getActivityActorId(activity);
2737
+ if (actorId != null) eventAttributes["activitypub.actor.id"] = actorId;
2738
+ span.addEvent("activitypub.activity.sent", eventAttributes);
2739
+ } finally {
2740
+ federationMetrics.recordDelivery(inbox, getDurationMs(started), deliverySuccess, activityType);
2787
2741
  }
2788
2742
  }
2789
- function portableObjectInternalServerError(request) {
2790
- return new Response(request.method === "HEAD" ? null : "Internal server error.", {
2791
- status: 500,
2792
- headers: { "Content-Type": "text/plain; charset=utf-8" }
2793
- });
2794
- }
2795
- const BUILT_IN_COLLECTION_METRIC_KINDS = /* @__PURE__ */ new Set([
2796
- "inbox",
2797
- "outbox",
2798
- "following",
2799
- "followers",
2800
- "liked",
2801
- "featured",
2802
- "featured_tags"
2803
- ]);
2804
- function getCollectionMetricKind(name) {
2805
- const normalized = name.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/\s+/g, "_");
2806
- return BUILT_IN_COLLECTION_METRIC_KINDS.has(normalized) ? normalized : "custom";
2807
- }
2808
- function collectionAttributes(base, result, response) {
2809
- return {
2810
- ...base,
2811
- result,
2812
- ...response == null ? {} : { statusCode: response.status }
2813
- };
2743
+ function createFetchError(url, cause) {
2744
+ const error = new FetchError(url, cause instanceof Error ? cause.message : String(cause));
2745
+ error.cause = cause;
2746
+ return error;
2814
2747
  }
2815
- function recordCollectionMetrics(meterProvider, base, result, options = {}) {
2816
- const attrs = collectionAttributes(base, result, options.response);
2817
- recordCollectionRequest(meterProvider, attrs);
2818
- if (options.dispatchDurationMs != null) recordCollectionDispatchDuration(meterProvider, options.dispatchDurationMs, attrs);
2819
- if (options.itemCount != null) recordCollectionPageItems(meterProvider, options.itemCount, attrs);
2820
- if (options.totalItems != null) recordCollectionTotalItems(meterProvider, options.totalItems, attrs);
2748
+ async function fetchWithValidatedRedirects(request, validateUrl) {
2749
+ let body = await request.clone().text();
2750
+ for (let redirects = 0;; redirects++) {
2751
+ const response = await fetch(request, { redirect: "manual" });
2752
+ const location = response.headers.get("Location");
2753
+ if (![
2754
+ 301,
2755
+ 302,
2756
+ 303,
2757
+ 307,
2758
+ 308
2759
+ ].includes(response.status) || location == null) return response;
2760
+ await response.body?.cancel();
2761
+ if (redirects >= 20) throw new TypeError("Too many redirects");
2762
+ const url = new URL(location, request.url);
2763
+ await validateUrl(url.href);
2764
+ const headers = new Headers(request.headers);
2765
+ if (url.origin !== new URL(request.url).origin) {
2766
+ headers.delete("Authorization");
2767
+ headers.delete("Proxy-Authorization");
2768
+ headers.delete("Cookie");
2769
+ headers.delete("Host");
2770
+ }
2771
+ let method = request.method;
2772
+ if ((response.status === 301 || response.status === 302) && method === "POST" || response.status === 303 && method !== "GET" && method !== "HEAD") {
2773
+ method = "GET";
2774
+ body = void 0;
2775
+ for (const name of [
2776
+ "Content-Encoding",
2777
+ "Content-Language",
2778
+ "Content-Length",
2779
+ "Content-Location",
2780
+ "Content-Type"
2781
+ ]) headers.delete(name);
2782
+ }
2783
+ request = new Request(url, {
2784
+ method,
2785
+ headers,
2786
+ body,
2787
+ redirect: "manual"
2788
+ });
2789
+ }
2821
2790
  }
2822
2791
  /**
2823
- * Handles a collection request.
2824
- * @template TItem The type of items in the collection.
2825
- * @template TContext The type of the context, extending {@link RequestContext}.
2826
- * @template TContextData The context data to pass to the `TContext`.
2827
- * @template TFilter The type of the filter.
2828
- * @param request The HTTP request.
2829
- * @param parameters The parameters for handling the collection.
2830
- * @returns A promise that resolves to an HTTP response.
2831
- */
2792
+ * An error that is thrown when an activity fails to send to a remote inbox.
2793
+ * It contains structured information about the failure, including the HTTP
2794
+ * status code, the inbox URL, and the response body.
2795
+ * @since 2.0.0
2796
+ */
2797
+ var SendActivityError = class extends Error {
2798
+ /**
2799
+ * The inbox URL that the activity was being sent to.
2800
+ */
2801
+ inbox;
2802
+ /**
2803
+ * The HTTP status code returned by the inbox.
2804
+ */
2805
+ statusCode;
2806
+ /**
2807
+ * The response body from the inbox, if any. Note that this may be
2808
+ * truncated to a maximum of 1 KiB to prevent excessive memory consumption
2809
+ * when remote servers return large error pages (e.g., Cloudflare error pages).
2810
+ * If truncated, the string will end with `"… (truncated)"`.
2811
+ */
2812
+ responseBody;
2813
+ /**
2814
+ * The response headers from the inbox.
2815
+ * @since 2.3.0
2816
+ */
2817
+ responseHeaders;
2818
+ /**
2819
+ * Creates a new {@link SendActivityError}.
2820
+ * @param inbox The inbox URL.
2821
+ * @param statusCode The HTTP status code.
2822
+ * @param message The error message.
2823
+ * @param responseBody The response body.
2824
+ * @param responseHeaders The response headers.
2825
+ */
2826
+ constructor(inbox, statusCode, message, responseBody, responseHeaders) {
2827
+ super(message);
2828
+ this.name = "SendActivityError";
2829
+ this.inbox = inbox;
2830
+ this.statusCode = statusCode;
2831
+ this.responseBody = responseBody;
2832
+ this.responseHeaders = new Headers(responseHeaders);
2833
+ }
2834
+ };
2835
+ //#endregion
2836
+ //#region src/federation/portable-inbox.ts
2837
+ const MAX_TIMEOUT_MS = 2 ** 31 - 1;
2838
+ /**
2839
+ * Fills in the defaults of {@link PortableInboxForwardingOptions} and
2840
+ * validates them.
2841
+ * @param options The options.
2842
+ * @returns The resolved options.
2843
+ * @throws {RangeError} If `maxTargets` is not a non-negative integer, if `ttl`
2844
+ * is not positive, if `deadline` is negative or longer
2845
+ * than 2,147,483,647 milliseconds (about 24.8 days), or
2846
+ * if either duration has calendar units, i.e., weeks,
2847
+ * months, or years, whose length depends on the date.
2848
+ */
2849
+ function resolvePortableInboxForwardingOptions(options = {}) {
2850
+ const maxTargets = options.maxTargets ?? 10;
2851
+ if (!Number.isInteger(maxTargets) || maxTargets < 0) throw new RangeError("portableInboxForwarding.maxTargets must be a non-negative integer.");
2852
+ const ttl = Temporal.Duration.from(options.ttl ?? { days: 30 });
2853
+ if (ttl.total("millisecond") <= 0) throw new RangeError("portableInboxForwarding.ttl must be positive.");
2854
+ const deadline = Temporal.Duration.from(options.deadline ?? { seconds: 10 });
2855
+ const deadlineMs = deadline.total("millisecond");
2856
+ if (deadlineMs < 0 || deadlineMs > MAX_TIMEOUT_MS) throw new RangeError(`portableInboxForwarding.deadline must be between 0 and ${MAX_TIMEOUT_MS} milliseconds.`);
2857
+ return {
2858
+ maxTargets,
2859
+ ttl,
2860
+ deadline
2861
+ };
2862
+ }
2863
+ /**
2864
+ * Decides whether this server accepts a delivery to a portable inbox on behalf
2865
+ * of the given actor, which the actor dispatcher returned for the identifier
2866
+ * in the requested inbox path.
2867
+ *
2868
+ * The actor document comes from the application, so it is trusted as is and
2869
+ * its proof is not verified here.
2870
+ * @param actor The actor the actor dispatcher returned.
2871
+ * @param options The requested DID, the canonical form of the requested inbox
2872
+ * ID, and the origin of this server.
2873
+ * @returns The resolution.
2874
+ */
2875
+ function resolvePortableInboxRecipient(actor, { authority, canonicalInboxId, localOrigin }) {
2876
+ if (actor == null) return {
2877
+ status: "rejected",
2878
+ reason: "notFound"
2879
+ };
2880
+ const id = actor.id;
2881
+ const did = id == null || !isPortableId(id) ? null : getPortableDid(id);
2882
+ if (id == null || did == null) return {
2883
+ status: "rejected",
2884
+ reason: "notPortable"
2885
+ };
2886
+ if (did !== authority) return {
2887
+ status: "rejected",
2888
+ reason: "authorityMismatch"
2889
+ };
2890
+ const inboxId = actor.inboxId == null ? null : toPortableUri(actor.inboxId);
2891
+ if (inboxId == null || getCanonicalPortableId(inboxId) !== canonicalInboxId) return {
2892
+ status: "rejected",
2893
+ reason: "inboxMismatch"
2894
+ };
2895
+ const gateways = actor.gateways;
2896
+ if (!gateways.some((g) => isGatewayUrl(g) && g.origin === localOrigin)) return {
2897
+ status: "rejected",
2898
+ reason: "notGateway"
2899
+ };
2900
+ return {
2901
+ status: "accepted",
2902
+ recipient: {
2903
+ actorId: id,
2904
+ inboxId,
2905
+ canonicalInboxId,
2906
+ gateways
2907
+ }
2908
+ };
2909
+ }
2910
+ /**
2911
+ * Gets the portable form of an `ap:` or `ap+ef61:` URI or a compatible
2912
+ * identifier, keeping its query.
2913
+ * @returns The portable URI, or `null` if the ID is neither, or is malformed.
2914
+ */
2915
+ function toPortableUri(id) {
2916
+ if (isPortableUri(id)) return id;
2917
+ try {
2918
+ return fromCompatibleEf61Id(id);
2919
+ } catch (error) {
2920
+ if (error instanceof TypeError) return null;
2921
+ throw error;
2922
+ }
2923
+ }
2924
+ const RFC9421_DELIVERY_COMPONENTS = [
2925
+ "@method",
2926
+ "@target-uri",
2927
+ "content-digest"
2928
+ ];
2929
+ const DRAFT_DELIVERY_COMPONENTS = [
2930
+ "(request-target)",
2931
+ "host",
2932
+ "digest"
2933
+ ];
2934
+ /**
2935
+ * Checks whether an HTTP Signature covers the method, the target URI, and the
2936
+ * body of a request, which both Fedify and Mitra sign when forwarding.
2937
+ * @param signature The signature.
2938
+ * @returns `true` if the signature covers them.
2939
+ */
2940
+ function coversDelivery(signature) {
2941
+ return (signature.label == null ? DRAFT_DELIVERY_COMPONENTS : RFC9421_DELIVERY_COMPONENTS).every((name) => signature.components.some((c) => c.value === name && Object.keys(c.params).length < 1));
2942
+ }
2943
+ /**
2944
+ * Forwards an activity received in a portable inbox to the inboxes of the same
2945
+ * portable actor on its other gateways, as FEP-ef61 recommends.
2946
+ *
2947
+ * FEP-ef61 forbids forwarding an activity from an inbox more than once, so
2948
+ * each target gateway is claimed with an atomic compare-and-swap before the
2949
+ * activity is handed off, and a claim is never released, even if the hand-off
2950
+ * fails, since a failed hand-off does not prove that nothing was delivered.
2951
+ * Forwarding is therefore best effort. Without {@link KvStore.cas}, nothing is
2952
+ * forwarded.
2953
+ *
2954
+ * Forwarded requests are signed with HTTP Signatures by the RSA key among
2955
+ * {@link ForwardPortableInboxActivityParameters.getKeys}, if any, so that
2956
+ * servers requiring HTTP Signatures accept them, and are sent unsigned
2957
+ * otherwise. Either way, the receiving gateways authenticate the activity by
2958
+ * its own proof.
2959
+ *
2960
+ * If the delivery is signed with a gateway key of the recipient by one of its
2961
+ * other gateways, that gateway has the activity already, so it is claimed
2962
+ * without forwarding the activity to it. See {@link identifySendingGateway}.
2963
+ * @param parameters The parameters.
2964
+ * @returns The target inbox URLs that the activity was handed off to.
2965
+ */
2966
+ async function forwardPortableInboxActivity(parameters) {
2967
+ const logger = getLogger([
2968
+ "fedify",
2969
+ "federation",
2970
+ "inbox"
2971
+ ]);
2972
+ const { recipient, activityId, excludedOrigins, kv, kvPrefix, outboxQueue } = parameters;
2973
+ const options = parameters.options ?? resolvePortableInboxForwardingOptions();
2974
+ const { maxTargets, ttl } = options;
2975
+ if (kv.cas == null || maxTargets < 1) return [];
2976
+ const deadline = Date.now() + options.deadline.total("millisecond");
2977
+ const canonicalActivityId = getCanonicalPortableId(activityId) ?? activityId.href;
2978
+ const claimKey = (gateway) => [
2979
+ ...kvPrefix,
2980
+ recipient.canonicalInboxId,
2981
+ canonicalActivityId,
2982
+ gateway
2983
+ ];
2984
+ let targets = getForwardingTargets(recipient, excludedOrigins);
2985
+ const sender = await identifySendingGateway(parameters, targets.map((t) => t.gateway), claimKey, deadline);
2986
+ if (sender != null) {
2987
+ logger.debug("Not forwarding activity {activityId} from the portable inbox {inbox} back to the gateway {gateway}, which delivered it.", {
2988
+ activityId: activityId.href,
2989
+ inbox: recipient.canonicalInboxId,
2990
+ gateway: sender
2991
+ });
2992
+ await kv.cas(claimKey(sender), void 0, true, { ttl });
2993
+ targets = targets.filter((t) => t.gateway !== sender);
2994
+ }
2995
+ if (targets.length > maxTargets) {
2996
+ logger.warn("The portable actor that owns the inbox {inbox} has more than {max} other gateways; the activity {activityId} is forwarded only to the first {max} of them.", {
2997
+ inbox: recipient.canonicalInboxId,
2998
+ activityId: activityId.href,
2999
+ max: maxTargets
3000
+ });
3001
+ targets.length = maxTargets;
3002
+ }
3003
+ const claimed = [];
3004
+ for (const { gateway, inbox } of targets) if (await kv.cas(claimKey(gateway), void 0, true, { ttl })) claimed.push(inbox);
3005
+ if (claimed.length < 1) return [];
3006
+ logger.debug("Forwarding activity {activityId} from the portable inbox {inbox} to other gateways:\n{targets}", {
3007
+ activityId: activityId.href,
3008
+ inbox: recipient.canonicalInboxId,
3009
+ targets: claimed.map((t) => t.href)
3010
+ });
3011
+ if (outboxQueue == null) await forwardImmediately(parameters, claimed, deadline);
3012
+ else await enqueueForwarding(parameters, outboxQueue, claimed);
3013
+ return claimed;
3014
+ }
3015
+ /**
3016
+ * Identifies the gateway of the recipient that forwarded a delivery to its
3017
+ * portable inbox, by the delivery's HTTP Signature made with the recipient's
3018
+ * gateway key, which FEP-ef61 has each gateway use for requests on behalf of
3019
+ * an actor. Since the activity is authenticated by its own proof, the
3020
+ * signature only tells which gateway to skip, and a delivery whose signature
3021
+ * is invalid or cannot be verified is still forwarded to every gateway.
3022
+ *
3023
+ * Verifying the signature fetches the recipient's actor document from the
3024
+ * sending gateway, as gateway keys are not cached, so only the first signature
3025
+ * that may save a request is verified: one that covers the delivery and names
3026
+ * a gateway key of the recipient on one of the forwarding targets that the
3027
+ * activity has not been forwarded to yet.
3028
+ * @param parameters The forwarding parameters.
3029
+ * @param targets The origins of the gateways to forward to.
3030
+ * @param claimKey Gets the key of the claim for a gateway.
3031
+ * @param deadline The time by which identification has to finish, in
3032
+ * milliseconds since the epoch; afterwards, the sending
3033
+ * gateway is left unidentified.
3034
+ * @returns The origin of the sending gateway, or `null` if it is not
3035
+ * identified.
3036
+ */
3037
+ async function identifySendingGateway({ recipient, activityId, kv, signatures }, targets, claimKey, deadline) {
3038
+ if (signatures == null || signatures.length < 1) return null;
3039
+ const logger = getLogger([
3040
+ "fedify",
3041
+ "federation",
3042
+ "inbox"
3043
+ ]);
3044
+ const actorId = getCanonicalPortableId(recipient.actorId);
3045
+ if (actorId == null) return null;
3046
+ const isGatewayKeyOfRecipient = (keyId) => {
3047
+ if (!isCompatibleKeyId(keyId) || !targets.includes(keyId.origin)) return false;
3048
+ const base = new URL(keyId.href);
3049
+ base.hash = "";
3050
+ return getCanonicalPortableId(base) === actorId;
3051
+ };
3052
+ let signature;
3053
+ for (const candidate of signatures) if (candidate.coversDelivery && isGatewayKeyOfRecipient(candidate.keyId) && await kv.get(claimKey(candidate.keyId.origin)) === void 0) {
3054
+ signature = candidate;
3055
+ break;
3056
+ }
3057
+ if (signature == null) return null;
3058
+ const { keyId } = signature;
3059
+ const logProperties = {
3060
+ activityId: activityId.href,
3061
+ inbox: recipient.canonicalInboxId,
3062
+ keyId: keyId.href
3063
+ };
3064
+ const verification = Promise.resolve().then(() => signature.verify()).then((key) => {
3065
+ if (key == null) logger.debug("The HTTP Signature of the delivery of activity {activityId} to the portable inbox {inbox} is not verified with the key {keyId}; forwarding it without identifying the sending gateway.", logProperties);
3066
+ return key;
3067
+ }, (error) => {
3068
+ logger.debug("Failed to verify the HTTP Signature of the delivery of activity {activityId} to the portable inbox {inbox} with the key {keyId}; forwarding it without identifying the sending gateway:\n{error}", {
3069
+ ...logProperties,
3070
+ error
3071
+ });
3072
+ return null;
3073
+ });
3074
+ let timer;
3075
+ const key = await Promise.race([verification, new Promise((resolve) => {
3076
+ timer = setTimeout(() => {
3077
+ logger.debug("Verifying the HTTP Signature of the delivery of activity {activityId} to the portable inbox {inbox} did not finish in time; forwarding it without identifying the sending gateway.", logProperties);
3078
+ resolve(null);
3079
+ }, Math.max(0, deadline - Date.now()));
3080
+ })]);
3081
+ clearTimeout(timer);
3082
+ if (key == null) return null;
3083
+ if (key.id == null || key.id.href !== keyId.href || !isGatewayKeyOfRecipient(key.id) || key.ownerId == null || !isPortableId(key.ownerId) || getCanonicalPortableId(key.ownerId) !== actorId) {
3084
+ logger.debug("The delivery of activity {activityId} to the portable inbox {inbox} is not signed with a gateway key of the recipient; forwarding it without identifying the sending gateway.", logProperties);
3085
+ return null;
3086
+ }
3087
+ return key.id.origin;
3088
+ }
3089
+ function getForwardingTargets(recipient, excludedOrigins) {
3090
+ const logger = getLogger([
3091
+ "fedify",
3092
+ "federation",
3093
+ "inbox"
3094
+ ]);
3095
+ const seen = new Set(excludedOrigins);
3096
+ const targets = [];
3097
+ for (const gateway of recipient.gateways) {
3098
+ if (!isGatewayUrl(gateway) || seen.has(gateway.origin)) continue;
3099
+ seen.add(gateway.origin);
3100
+ let inbox;
3101
+ try {
3102
+ inbox = toCompatibleEf61Id(recipient.inboxId, gateway.origin);
3103
+ } catch (error) {
3104
+ if (!(error instanceof TypeError)) throw error;
3105
+ logger.warn("Cannot forward to the gateway {gateway}, as the portable inbox {inbox} cannot be represented as its compatible identifier.", {
3106
+ gateway: gateway.origin,
3107
+ inbox: recipient.canonicalInboxId
3108
+ });
3109
+ continue;
3110
+ }
3111
+ targets.push({
3112
+ gateway: gateway.origin,
3113
+ inbox
3114
+ });
3115
+ }
3116
+ return targets;
3117
+ }
3118
+ /**
3119
+ * Resolves the key that signs forwarded requests: the first RSASSA-PKCS1-v1_5
3120
+ * key, as {@link sendActivity} signs requests only with such a key. Since
3121
+ * the targets have already been claimed, the activity is forwarded unsigned
3122
+ * rather than not at all if the key cannot be resolved or used.
3123
+ */
3124
+ async function resolveForwardingKey({ getKeys, activityId }) {
3125
+ if (getKeys == null) return null;
3126
+ const logger = getLogger([
3127
+ "fedify",
3128
+ "federation",
3129
+ "inbox"
3130
+ ]);
3131
+ let key;
3132
+ try {
3133
+ key = (await getKeys()).find((k) => k.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5");
3134
+ } catch (error) {
3135
+ logger.error("Failed to get the gateway keys to sign forwarded requests with; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
3136
+ activityId: activityId.href,
3137
+ error
3138
+ });
3139
+ return null;
3140
+ }
3141
+ if (key == null) return null;
3142
+ try {
3143
+ validateCryptoKey(key.privateKey, "private");
3144
+ } catch (error) {
3145
+ if (!(error instanceof TypeError)) throw error;
3146
+ logger.warn("The gateway key {keyId} cannot sign forwarded requests; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
3147
+ keyId: key.keyId.href,
3148
+ activityId: activityId.href,
3149
+ error
3150
+ });
3151
+ return null;
3152
+ }
3153
+ return key;
3154
+ }
3155
+ async function forwardImmediately(parameters, inboxes, deadlineAt) {
3156
+ const { activity, activityId, activityType, allowPrivateAddress, specDeterminer, meterProvider, tracerProvider } = parameters;
3157
+ const logger = getLogger([
3158
+ "fedify",
3159
+ "federation",
3160
+ "inbox"
3161
+ ]);
3162
+ const deadline = Math.max(0, deadlineAt - Date.now());
3163
+ const forwarding = resolveForwardingKey(parameters).then((key) => Promise.all(inboxes.map((inbox) => sendActivity({
3164
+ activity,
3165
+ activityId: activityId.href,
3166
+ activityType,
3167
+ keys: key == null ? [] : [key],
3168
+ inbox,
3169
+ allowPrivateAddress,
3170
+ specDeterminer,
3171
+ meterProvider,
3172
+ tracerProvider
3173
+ }).catch((error) => {
3174
+ logger.error("Failed to forward activity {activityId} to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
3175
+ activityId: activityId.href,
3176
+ inbox: inbox.href,
3177
+ error
3178
+ });
3179
+ }))));
3180
+ let timer;
3181
+ const timedOut = await Promise.race([forwarding.then(() => false), new Promise((resolve) => {
3182
+ timer = setTimeout(() => resolve(true), deadline);
3183
+ })]);
3184
+ clearTimeout(timer);
3185
+ if (timedOut) logger.warn("Forwarding activity {activityId} to other gateways did not finish in {deadline} ms; responding to the delivery without waiting for it.", {
3186
+ activityId: activityId.href,
3187
+ deadline
3188
+ });
3189
+ }
3190
+ async function enqueueForwarding(parameters, outboxQueue, inboxes) {
3191
+ const { activity, activityId, activityType, baseUrl, startQueue, meterProvider } = parameters;
3192
+ const logger = getLogger([
3193
+ "fedify",
3194
+ "federation",
3195
+ "inbox"
3196
+ ]);
3197
+ const key = await resolveForwardingKey(parameters);
3198
+ const keys = [];
3199
+ if (key != null) try {
3200
+ keys.push({
3201
+ keyId: key.keyId.href,
3202
+ privateKey: await exportJwk(key.privateKey)
3203
+ });
3204
+ } catch (error) {
3205
+ logger.error("Failed to export the gateway key {keyId}; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
3206
+ keyId: key.keyId.href,
3207
+ activityId: activityId.href,
3208
+ error
3209
+ });
3210
+ }
3211
+ startQueue?.();
3212
+ const started = (/* @__PURE__ */ new Date()).toISOString();
3213
+ const traceContext = {};
3214
+ propagation.inject(context.active(), traceContext);
3215
+ await Promise.all(inboxes.map(async (inbox) => {
3216
+ const message = {
3217
+ type: "outbox",
3218
+ id: crypto.randomUUID(),
3219
+ baseUrl,
3220
+ keys,
3221
+ activity,
3222
+ activityId: activityId.href,
3223
+ activityType,
3224
+ inbox: inbox.href,
3225
+ sharedInbox: false,
3226
+ started,
3227
+ attempt: 0,
3228
+ headers: {},
3229
+ traceContext
3230
+ };
3231
+ try {
3232
+ await outboxQueue.enqueue(message);
3233
+ } catch (error) {
3234
+ logger.error("Failed to enqueue activity {activityId} to forward to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
3235
+ activityId: activityId.href,
3236
+ inbox: inbox.href,
3237
+ error
3238
+ });
3239
+ return;
3240
+ }
3241
+ recordOutboxEnqueue(meterProvider, outboxQueue, message);
3242
+ }));
3243
+ }
3244
+ //#endregion
3245
+ //#region src/federation/temporal.ts
3246
+ function isPlainObject$1(value) {
3247
+ return typeof value === "object" && value != null && !Array.isArray(value);
3248
+ }
3249
+ function normalizeDateTimeLiteral(value) {
3250
+ return value.substring(19).match(/[Z+-]/) ? value : value + "Z";
3251
+ }
3252
+ function isMalformedDateTimeLiteral(value) {
3253
+ if (typeof value !== "string") return false;
3254
+ try {
3255
+ Temporal.Instant.from(normalizeDateTimeLiteral(value));
3256
+ return false;
3257
+ } catch {
3258
+ return true;
3259
+ }
3260
+ }
3261
+ function isMalformedDurationLiteral(value) {
3262
+ if (typeof value !== "string") return false;
3263
+ try {
3264
+ Temporal.Duration.from(value);
3265
+ return false;
3266
+ } catch {
3267
+ return true;
3268
+ }
3269
+ }
3270
+ const TEMPORAL_DATE_TIME_IRIS = /* @__PURE__ */ new Set([
3271
+ "https://www.w3.org/ns/activitystreams#deleted",
3272
+ "https://www.w3.org/ns/activitystreams#endTime",
3273
+ "https://www.w3.org/ns/activitystreams#published",
3274
+ "https://www.w3.org/ns/activitystreams#startTime",
3275
+ "https://www.w3.org/ns/activitystreams#updated",
3276
+ "http://purl.org/dc/terms/created",
3277
+ "https://w3id.org/security#created"
3278
+ ]);
3279
+ const TEMPORAL_DURATION_IRIS = /* @__PURE__ */ new Set(["https://www.w3.org/ns/activitystreams#duration"]);
3280
+ const QUESTION_CLOSED_IRI = "https://www.w3.org/ns/activitystreams#closed";
3281
+ const XSD_DATE_TIME_IRI = "http://www.w3.org/2001/XMLSchema#dateTime";
3282
+ function hasMalformedExpandedDateTimeLiteral(value) {
3283
+ if (Array.isArray(value)) return value.some(hasMalformedExpandedDateTimeLiteral);
3284
+ return isPlainObject$1(value) && "@value" in value && isMalformedDateTimeLiteral(value["@value"]);
3285
+ }
3286
+ function hasMalformedExpandedQuestionClosedLiteral(value) {
3287
+ if (Array.isArray(value)) return value.some(hasMalformedExpandedQuestionClosedLiteral);
3288
+ if (!isPlainObject$1(value) || !("@value" in value)) return false;
3289
+ const literal = value["@value"];
3290
+ if (typeof literal === "boolean") return false;
3291
+ if (typeof literal !== "string") return false;
3292
+ if (value["@type"] !== XSD_DATE_TIME_IRI) return false;
3293
+ if (new Date(literal).toString() === "Invalid Date") return false;
3294
+ return isMalformedDateTimeLiteral(literal);
3295
+ }
3296
+ function hasMalformedExpandedDurationLiteral(value) {
3297
+ if (Array.isArray(value)) return value.some(hasMalformedExpandedDurationLiteral);
3298
+ return isPlainObject$1(value) && "@value" in value && isMalformedDurationLiteral(value["@value"]);
3299
+ }
3300
+ function hasMalformedKnownTemporalLiteralInternal(value, visited) {
3301
+ if (Array.isArray(value)) return value.some((item) => hasMalformedKnownTemporalLiteralInternal(item, visited));
3302
+ if (!isPlainObject$1(value)) return false;
3303
+ if (visited.has(value)) return false;
3304
+ visited.add(value);
3305
+ if ("@value" in value) return false;
3306
+ for (const [key, child] of Object.entries(value)) {
3307
+ if (TEMPORAL_DATE_TIME_IRIS.has(key)) {
3308
+ if (hasMalformedExpandedDateTimeLiteral(child)) return true;
3309
+ continue;
3310
+ }
3311
+ if (key === QUESTION_CLOSED_IRI) {
3312
+ if (hasMalformedExpandedQuestionClosedLiteral(child)) return true;
3313
+ continue;
3314
+ }
3315
+ if (TEMPORAL_DURATION_IRIS.has(key)) {
3316
+ if (hasMalformedExpandedDurationLiteral(child)) return true;
3317
+ continue;
3318
+ }
3319
+ if (hasMalformedKnownTemporalLiteralInternal(child, visited)) return true;
3320
+ }
3321
+ return false;
3322
+ }
3323
+ async function hasMalformedKnownTemporalLiteral(value, contextLoader) {
3324
+ try {
3325
+ return hasMalformedKnownTemporalLiteralInternal(await jsonld.expand(value, {
3326
+ documentLoader: getNormalizationContextLoader(contextLoader),
3327
+ keepFreeFloatingNodes: true
3328
+ }), /* @__PURE__ */ new Set());
3329
+ } catch {
3330
+ return false;
3331
+ }
3332
+ }
3333
+ //#endregion
3334
+ //#region src/federation/handler.ts
3335
+ const rawInboxContextFactorySymbol = Symbol("fedify.rawInboxContextFactory");
3336
+ const INBOX_COMPOUND_PROOF_LIMITS = {
3337
+ maxDepth: 64,
3338
+ maxMaps: 1e4,
3339
+ maxProofs: 32,
3340
+ maxBytes: 10 * 1024 * 1024
3341
+ };
3342
+ function isRemoteContextLoadingFailure$1(error) {
3343
+ return error instanceof Error && typeof error.details === "object" && error.details != null && error.details.code === "loading remote context failed";
3344
+ }
3345
+ function isPermanentRemoteContextError$1(error) {
3346
+ if (!(error instanceof Error) || error.name !== "jsonld.InvalidUrl") return false;
3347
+ const details = error.details;
3348
+ if (details?.code === "invalid remote context") return true;
3349
+ return isRemoteContextLoadingFailure$1(error) && typeof details?.url === "string" && !URL.canParse(details.url) && isClearlyMalformedContextReference(details.url);
3350
+ }
3351
+ function isInvalidJsonLdError(error) {
3352
+ if (!(error instanceof Error)) return false;
3353
+ const name = error.name;
3354
+ return name === "UnsafeJsonLdError" || error instanceof InvalidContextReferenceError || isPermanentRemoteContextError$1(error) || name === "jsonld.SyntaxError" && !isRemoteContextLoadingFailure$1(error);
3355
+ }
3356
+ function isValidationTypeError(error) {
3357
+ return error instanceof TypeError && (/^(Invalid JSON-LD:|Invalid type:|Unexpected type:|Invalid @id:)/.test(error.message) || isInvalidUrlTypeError(error));
3358
+ }
3359
+ function isPermanentActivityParseError(error) {
3360
+ return isInvalidJsonLdError(error) || isValidationTypeError(error);
3361
+ }
3362
+ function hasHttpSignatureHeaders(request) {
3363
+ return request.headers.has("Signature") || request.headers.has("Signature-Input");
3364
+ }
3365
+ function hasObjectIntegrityProof(json) {
3366
+ return typeof json === "object" && json != null && "proof" in json;
3367
+ }
3368
+ /**
3369
+ * Handles an actor request.
3370
+ * @template TContextData The context data to pass to the context.
3371
+ * @param request The HTTP request.
3372
+ * @param parameters The parameters for handling the actor.
3373
+ * @returns A promise that resolves to an HTTP response.
3374
+ */
3375
+ async function handleActor(request, { identifier, context, actorDispatcher, authorizePredicate, onNotFound, onUnauthorized }) {
3376
+ const logger = getLogger([
3377
+ "fedify",
3378
+ "federation",
3379
+ "actor"
3380
+ ]);
3381
+ if (actorDispatcher == null) {
3382
+ logger.debug("Actor dispatcher is not set.", { identifier });
3383
+ return await onNotFound(request);
3384
+ }
3385
+ const actor = await actorDispatcher(context, identifier);
3386
+ if (actor == null) {
3387
+ logger.debug("Actor {identifier} not found.", { identifier });
3388
+ return await onNotFound(request);
3389
+ }
3390
+ if (authorizePredicate != null) {
3391
+ if (!await authorizePredicate(context, identifier)) return await onUnauthorized(request);
3392
+ }
3393
+ if (actor instanceof Tombstone) {
3394
+ const jsonLd = await actor.toJsonLd(context);
3395
+ return new Response(JSON.stringify(jsonLd), {
3396
+ status: 410,
3397
+ headers: {
3398
+ "Content-Type": "application/activity+json",
3399
+ Vary: "Accept"
3400
+ }
3401
+ });
3402
+ }
3403
+ const jsonLd = await actor.toJsonLd(context);
3404
+ return new Response(JSON.stringify(jsonLd), { headers: {
3405
+ "Content-Type": "application/activity+json",
3406
+ Vary: "Accept"
3407
+ } });
3408
+ }
3409
+ /**
3410
+ * Handles an object request.
3411
+ * @template TContextData The context data to pass to the context.
3412
+ * @param request The HTTP request.
3413
+ * @param parameters The parameters for handling the object.
3414
+ * @returns A promise that resolves to an HTTP response.
3415
+ */
3416
+ async function handleObject(request, { values, context, objectDispatcher, authorizePredicate, onNotFound, onUnauthorized }) {
3417
+ if (objectDispatcher == null) return await onNotFound(request);
3418
+ const object = await objectDispatcher(context, values);
3419
+ if (object == null) return await onNotFound(request);
3420
+ if (authorizePredicate != null) {
3421
+ if (!await authorizePredicate(context, values)) return await onUnauthorized(request);
3422
+ }
3423
+ const jsonLd = await object.toJsonLd(context);
3424
+ return new Response(JSON.stringify(jsonLd), { headers: {
3425
+ "Content-Type": "application/activity+json",
3426
+ Vary: "Accept"
3427
+ } });
3428
+ }
3429
+ /**
3430
+ * Handles an FEP-ef61 gateway request for a portable object through an object
3431
+ * dispatcher, or the actor dispatcher adapted to one. The object is served
3432
+ * only if its ID canonically matches the requested portable ID and it
3433
+ * satisfies the FEP-ef61 proof policy.
3434
+ * @template TContextData The context data to pass to the context.
3435
+ * @param request The HTTP request.
3436
+ * @param parameters The parameters for handling the portable object.
3437
+ * @returns A promise that resolves to an HTTP response.
3438
+ */
3439
+ async function handlePortableObject(request, { values, context, objectDispatcher, authorizePredicate, canonicalId, onNotFound, onUnauthorized }) {
3440
+ const logger = getLogger([
3441
+ "fedify",
3442
+ "federation",
3443
+ "object"
3444
+ ]);
3445
+ if (objectDispatcher == null) return await onNotFound(request);
3446
+ const object = await objectDispatcher(context, values);
3447
+ if (object == null) return await onNotFound(request);
3448
+ if (!isRequestedPortableObject(object, canonicalId)) {
3449
+ logger.debug("The object {objectId} does not match the requested portable object {portableId}.", {
3450
+ objectId: object.id?.href,
3451
+ portableId: canonicalId
3452
+ });
3453
+ return await onNotFound(request);
3454
+ }
3455
+ if (authorizePredicate != null) {
3456
+ if (!await authorizePredicate(context, values)) return await onUnauthorized(request);
3457
+ }
3458
+ let jsonLd;
3459
+ let servedId;
3460
+ let result;
3461
+ try {
3462
+ jsonLd = await object.toJsonLd(context);
3463
+ servedId = await getJsonLdRootId(jsonLd, context.contextLoader);
3464
+ result = await verifyPortableObjectProof(jsonLd, {
3465
+ contextLoader: context.contextLoader,
3466
+ documentLoader: context.documentLoader,
3467
+ tracerProvider: context.tracerProvider,
3468
+ meterProvider: context.meterProvider
3469
+ });
3470
+ } catch (error) {
3471
+ logger.error("Failed to verify the Object Integrity Proofs of the portable object {portableId}:\n{error}", {
3472
+ portableId: canonicalId,
3473
+ error
3474
+ });
3475
+ return portableObjectInternalServerError(request);
3476
+ }
3477
+ if (servedId == null || !isRequestedPortableId(servedId, canonicalId)) {
3478
+ logger.debug("The serialized object does not match the requested portable object {portableId}.", { portableId: canonicalId });
3479
+ return await onNotFound(request);
3480
+ }
3481
+ if (!result.verified && result.reason.type !== "unsecuredCollection" && result.reason.type !== "unsupportedObjectType") {
3482
+ logger.error("Refusing to serve the portable object {portableId}, as it does not satisfy the FEP-ef61 proof policy: {reason}. Portable actors, activities, and objects need an Object Integrity Proof made with a key of the DID in their ID.", {
3483
+ portableId: canonicalId,
3484
+ reason: result.reason.type
3485
+ });
3486
+ return portableObjectInternalServerError(request);
3487
+ }
3488
+ return new Response(request.method === "HEAD" ? null : JSON.stringify(jsonLd), { headers: {
3489
+ "Content-Type": PORTABLE_OBJECT_CONTENT_TYPE,
3490
+ Vary: "Accept"
3491
+ } });
3492
+ }
3493
+ function isRequestedPortableObject(object, canonicalId) {
3494
+ return object.id != null && getCanonicalPortableId(object.id) === canonicalId;
3495
+ }
3496
+ async function getJsonLdRootId(jsonLd, contextLoader) {
3497
+ const expanded = await jsonld.expand(jsonLd, {
3498
+ documentLoader: getNormalizationContextLoader(contextLoader),
3499
+ keepFreeFloatingNodes: true
3500
+ });
3501
+ if (expanded.length !== 1) return null;
3502
+ const id = expanded[0]["@id"];
3503
+ return typeof id === "string" ? id : null;
3504
+ }
3505
+ function isRequestedPortableId(id, canonicalId) {
3506
+ if (!/^ap(?:\+ef61)?:\/\//i.test(id)) return URL.canParse(id) && getCanonicalPortableId(new URL(id)) === canonicalId;
3507
+ try {
3508
+ return canonicalizePortableUri(id) === canonicalId;
3509
+ } catch (error) {
3510
+ if (error instanceof TypeError) return false;
3511
+ throw error;
3512
+ }
3513
+ }
3514
+ function portableObjectInternalServerError(request) {
3515
+ return new Response(request.method === "HEAD" ? null : "Internal server error.", {
3516
+ status: 500,
3517
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
3518
+ });
3519
+ }
3520
+ const BUILT_IN_COLLECTION_METRIC_KINDS = /* @__PURE__ */ new Set([
3521
+ "inbox",
3522
+ "outbox",
3523
+ "following",
3524
+ "followers",
3525
+ "liked",
3526
+ "featured",
3527
+ "featured_tags"
3528
+ ]);
3529
+ function getCollectionMetricKind(name) {
3530
+ const normalized = name.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/\s+/g, "_");
3531
+ return BUILT_IN_COLLECTION_METRIC_KINDS.has(normalized) ? normalized : "custom";
3532
+ }
3533
+ function collectionAttributes(base, result, response) {
3534
+ return {
3535
+ ...base,
3536
+ result,
3537
+ ...response == null ? {} : { statusCode: response.status }
3538
+ };
3539
+ }
3540
+ function recordCollectionMetrics(meterProvider, base, result, options = {}) {
3541
+ const attrs = collectionAttributes(base, result, options.response);
3542
+ recordCollectionRequest(meterProvider, attrs);
3543
+ if (options.dispatchDurationMs != null) recordCollectionDispatchDuration(meterProvider, options.dispatchDurationMs, attrs);
3544
+ if (options.itemCount != null) recordCollectionPageItems(meterProvider, options.itemCount, attrs);
3545
+ if (options.totalItems != null) recordCollectionTotalItems(meterProvider, options.totalItems, attrs);
3546
+ }
3547
+ /**
3548
+ * Handles a collection request.
3549
+ * @template TItem The type of items in the collection.
3550
+ * @template TContext The type of the context, extending {@link RequestContext}.
3551
+ * @template TContextData The context data to pass to the `TContext`.
3552
+ * @template TFilter The type of the filter.
3553
+ * @param request The HTTP request.
3554
+ * @param parameters The parameters for handling the collection.
3555
+ * @returns A promise that resolves to an HTTP response.
3556
+ */
2832
3557
  async function handleCollection(request, { name: name$2, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
2833
3558
  const spanName = name$2.trim().replace(/\s+/g, "_");
2834
3559
  tracerProvider = tracerProvider ?? trace.getTracerProvider();
@@ -3945,7 +4670,21 @@ async function handleInboxInternal(request, parameters, span) {
3945
4670
  inbox: portableInbox.recipient.canonicalInboxId
3946
4671
  });
3947
4672
  else try {
3948
- await portableInbox.forward(json, activity.id, getTypeId(activity).href);
4673
+ await portableInbox.forward(json, activity.id, getTypeId(activity).href, listRequestSignatures(request).map((signature) => ({
4674
+ keyId: signature.keyId,
4675
+ coversDelivery: coversDelivery(signature),
4676
+ verify: async () => {
4677
+ const verification = await verifyRequestDetailed(selectRequestSignature(request, signature), {
4678
+ contextLoader: ctx.contextLoader,
4679
+ documentLoader: ctx.documentLoader,
4680
+ timeWindow: signatureTimeWindow,
4681
+ keyCache,
4682
+ meterProvider,
4683
+ tracerProvider
4684
+ });
4685
+ return verification.verified ? verification.key : null;
4686
+ }
4687
+ })));
3949
4688
  } catch (error) {
3950
4689
  logger.error("Failed to forward activity {activityId} to the other gateways of the portable inbox {inbox}:\n{error}", {
3951
4690
  activityId: activity.id.href,
@@ -4442,523 +5181,233 @@ const authIfNeeded = async (context, values, { authorizePredicate: authorize = v
4442
5181
  * @template Cursor The type of the cursor (string, null, or undefined).
4443
5182
  * @param {URL} url The base URL to append the cursor to.
4444
5183
  * @param {string | null | undefined} cursor The cursor value to append.
4445
- * @returns The URL with cursor appended if cursor is a string, null otherwise.
4446
- * @since 1.8.0
4447
- */
4448
- const appendCursorIfExists = (url, cursor) => {
4449
- if (cursor === null || cursor === void 0) return null;
4450
- const copied = new URL(url);
4451
- copied.searchParams.set("cursor", cursor);
4452
- return copied;
4453
- };
4454
- /**
4455
- * Creates an HTTP response for ActivityPub data.
4456
- * @param {unknown} data The data to serialize as JSON-LD.
4457
- * @returns {Response} An HTTP response with the data as ActivityPub JSON.
4458
- * @since 1.8.0
4459
- */
4460
- const respondAsActivity = (data) => new Response(JSON.stringify(data), { headers: {
4461
- "Content-Type": "application/activity+json",
4462
- Vary: "Accept"
4463
- } });
4464
- /**
4465
- * Base class for handler errors.
4466
- * @since 1.8.0
4467
- */
4468
- var HandlerError = class extends Error {
4469
- constructor(message) {
4470
- super(message);
4471
- }
4472
- /**
4473
- * Throws this error.
4474
- * @returns Never returns, always throws.
4475
- */
4476
- throw() {
4477
- throw this;
4478
- }
4479
- };
4480
- /**
4481
- * Error thrown when items are not found in a collection.
4482
- * @since 1.8.0
4483
- */
4484
- var ItemsNotFoundError = class extends HandlerError {
4485
- constructor() {
4486
- super("Items not found in the collection.");
4487
- }
4488
- };
4489
- /**
4490
- * Error thrown when access to a collection is unauthorized.
4491
- * @since 1.8.0
4492
- */
4493
- var UnauthorizedError = class extends HandlerError {
4494
- constructor() {
4495
- super("Unauthorized access to the collection.");
4496
- }
4497
- };
4498
- /**
4499
- * Responds with the given object in JSON-LD format.
4500
- *
4501
- * @param object The object to respond with.
4502
- * @param options Options.
4503
- * @since 0.3.0
4504
- */
4505
- async function respondWithObject(object, options) {
4506
- const jsonLd = await object.toJsonLd(options);
4507
- return new Response(JSON.stringify(jsonLd), { headers: { "Content-Type": "application/activity+json" } });
4508
- }
4509
- /**
4510
- * Responds with the given object in JSON-LD format if the request accepts
4511
- * JSON-LD.
4512
- *
4513
- * @param object The object to respond with.
4514
- * @param request The request to check for JSON-LD acceptability.
4515
- * @param options Options.
4516
- * @since 0.3.0
4517
- */
4518
- async function respondWithObjectIfAcceptable(object, request, options) {
4519
- if (!acceptsJsonLd(request)) return null;
4520
- const response = await respondWithObject(object, options);
4521
- response.headers.set("Vary", "Accept");
4522
- return response;
4523
- }
4524
- function generateNonce() {
4525
- const bytes = /* @__PURE__ */ new Uint8Array(16);
4526
- crypto.getRandomValues(bytes);
4527
- return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
4528
- }
4529
- async function verifySignatureNonce(request, kv, noncePrefix, verifiedLabel) {
4530
- const signatureInput = request.headers.get("Signature-Input");
4531
- if (signatureInput == null) return false;
4532
- const parsed = parseRfc9421SignatureInput(signatureInput);
4533
- if (verifiedLabel == null) return false;
4534
- const sig = parsed[verifiedLabel];
4535
- if (sig == null) return false;
4536
- const nonce = sig.nonce;
4537
- if (nonce == null) return false;
4538
- const key = [...noncePrefix, nonce];
4539
- if (kv.cas != null) return await kv.cas(key, true, void 0);
4540
- if (await kv.get(key) != null) {
4541
- await kv.delete(key);
4542
- return true;
4543
- }
4544
- return false;
4545
- }
4546
- const getFailedSignatureResponse = async (policy, kv, kvPrefixes) => {
4547
- const headers = await getFailedSignatureHeaders(policy, kv, kvPrefixes);
4548
- return new Response("Failed to verify the request signature.", {
4549
- status: 401,
4550
- headers
4551
- });
4552
- };
4553
- const getFailedSignatureHeaders = async (policy, kv, kvPrefixes) => ({
4554
- "Content-Type": "text/plain; charset=utf-8",
4555
- ...policy?.enabled && {
4556
- "Accept-Signature": await buildAcceptSignatureHeader(policy, kv, kvPrefixes.acceptSignatureNonce),
4557
- "Cache-Control": "no-store",
4558
- "Vary": "Accept, Signature"
4559
- }
4560
- });
4561
- async function buildAcceptSignatureHeader(policy, kv, noncePrefix) {
4562
- const parameters = { created: true };
4563
- if (policy.requestNonce) {
4564
- const nonce = generateNonce();
4565
- await setKey(kv, [...noncePrefix, nonce], policy);
4566
- parameters.nonce = nonce;
4567
- }
4568
- const baseComponents = policy.components ?? DEF_COMPONENTS;
4569
- return formatAcceptSignature([{
4570
- label: "sig1",
4571
- components: uniq(MIN_COMPONENTS.concat(baseComponents)).filter((c) => c !== "@status").map((v) => ({
4572
- value: v,
4573
- params: {}
4574
- })),
4575
- parameters
4576
- }]);
4577
- }
4578
- async function setKey(kv, key, policy) {
4579
- const seconds = policy.nonceTtlSeconds ?? 300;
4580
- const ttl = Temporal.Duration.from({ seconds });
4581
- await kv.set(key, true, { ttl });
4582
- }
4583
- const DEF_COMPONENTS = [
4584
- "@method",
4585
- "@target-uri",
4586
- "@authority",
4587
- "content-digest"
4588
- ];
4589
- const MIN_COMPONENTS = [
4590
- "@method",
4591
- "@target-uri",
4592
- "@authority"
4593
- ];
4594
- /**
4595
- * Checks whether an HTTP Signature key is a gateway key of an FEP-ef61
4596
- * portable actor, i.e., its ID is a compatible identifier and it is owned by
4597
- * a portable actor, which only the portable actor's signed document can
4598
- * establish.
5184
+ * @returns The URL with cursor appended if cursor is a string, null otherwise.
5185
+ * @since 1.8.0
4599
5186
  */
4600
- function isPortableGatewayKey(key) {
4601
- return key?.id != null && isCompatibleKeyId(key.id) && key.ownerId != null && isPortableId(key.ownerId);
4602
- }
5187
+ const appendCursorIfExists = (url, cursor) => {
5188
+ if (cursor === null || cursor === void 0) return null;
5189
+ const copied = new URL(url);
5190
+ copied.searchParams.set("cursor", cursor);
5191
+ return copied;
5192
+ };
4603
5193
  /**
4604
- * Checks whether an activity needs an Object Integrity Proof by its DID:
4605
- * either it is performed by an [FEP-ef61] portable actor, or its own ID is
4606
- * portable. Both `ap:` URIs and compatible identifiers count as portable.
4607
- *
4608
- * [FEP-ef61]: https://w3id.org/fep/ef61
5194
+ * Creates an HTTP response for ActivityPub data.
5195
+ * @param {unknown} data The data to serialize as JSON-LD.
5196
+ * @returns {Response} An HTTP response with the data as ActivityPub JSON.
5197
+ * @since 1.8.0
4609
5198
  */
4610
- function isPortableActivity(activity) {
4611
- return hasPortableActor(activity) || activity.id != null && isPortableId(activity.id);
4612
- }
5199
+ const respondAsActivity = (data) => new Response(JSON.stringify(data), { headers: {
5200
+ "Content-Type": "application/activity+json",
5201
+ Vary: "Accept"
5202
+ } });
4613
5203
  /**
4614
- * Verifies that a portable activity's own ID belongs to the DID that signed
4615
- * it, on the JSON-LD document received. The ID the proof policy checks has
4616
- * to be the parsed activity's ID, so that a remote context that changes
4617
- * between loads cannot make the two name different objects.
4618
- * @returns `null` if the ID is verified, or the reason why it is not.
5204
+ * Base class for handler errors.
5205
+ * @since 1.8.0
4619
5206
  */
4620
- async function verifyPortableActivityId(activityId, jsonLd, options) {
4621
- const expectedId = getCanonicalPortableId(activityId);
4622
- if (expectedId == null) return "malformed portable ID";
4623
- let verification;
4624
- try {
4625
- verification = await verifyPortableObjectProofWithRoot(jsonLd, options);
4626
- } catch (error) {
4627
- if (error instanceof InvalidPortableObjectIdError) return "malformed portable ID";
4628
- if (!isPermanentActivityParseError(error)) throw error;
4629
- return "malformed document";
5207
+ var HandlerError = class extends Error {
5208
+ constructor(message) {
5209
+ super(message);
4630
5210
  }
4631
- const { result, root } = verification;
4632
- if (!result.verified) return result.reason.type;
4633
- const rootId = root?.["@id"];
4634
- if (typeof rootId !== "string" || getCanonicalPortableId(parseIri(rootId)) !== expectedId) return "the verified document has another ID";
4635
- return null;
4636
- }
4637
- //#endregion
4638
- //#region src/nodeinfo/handler.ts
5211
+ /**
5212
+ * Throws this error.
5213
+ * @returns Never returns, always throws.
5214
+ */
5215
+ throw() {
5216
+ throw this;
5217
+ }
5218
+ };
4639
5219
  /**
4640
- * Handles a NodeInfo request. You would not typically call this function
4641
- * directly, but instead use {@link Federation.handle} method.
4642
- * @param request The NodeInfo request to handle.
4643
- * @param parameters The parameters for handling the request.
4644
- * @returns The response to the request.
5220
+ * Error thrown when items are not found in a collection.
5221
+ * @since 1.8.0
4645
5222
  */
4646
- async function handleNodeInfo(_request, { context, nodeInfoDispatcher }) {
4647
- const promise = nodeInfoDispatcher(context);
4648
- const json = nodeInfoToJson(promise instanceof Promise ? await promise : promise);
4649
- return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json; profile=\"http://nodeinfo.diaspora.software/ns/schema/2.1#\"" } });
4650
- }
5223
+ var ItemsNotFoundError = class extends HandlerError {
5224
+ constructor() {
5225
+ super("Items not found in the collection.");
5226
+ }
5227
+ };
4651
5228
  /**
4652
- * Handles a request to `/.well-known/nodeinfo`. You would not typically call
4653
- * this function directly, but instead use {@link Federation.handle} method.
4654
- * @param request The request to handle.
4655
- * @param context The request context.
4656
- * @returns The response to the request.
5229
+ * Error thrown when access to a collection is unauthorized.
5230
+ * @since 1.8.0
4657
5231
  */
4658
- function handleNodeInfoJrd(_request, context) {
4659
- const links = [];
4660
- try {
4661
- links.push({
4662
- rel: "http://nodeinfo.diaspora.software/ns/schema/2.1",
4663
- href: context.getNodeInfoUri().href,
4664
- type: "application/json; profile=\"http://nodeinfo.diaspora.software/ns/schema/2.1#\""
4665
- });
4666
- } catch (e) {
4667
- if (!(e instanceof RouterError)) throw e;
5232
+ var UnauthorizedError = class extends HandlerError {
5233
+ constructor() {
5234
+ super("Unauthorized access to the collection.");
4668
5235
  }
4669
- const response = new Response(JSON.stringify({ links }), { headers: { "Content-Type": "application/jrd+json" } });
4670
- return Promise.resolve(response);
4671
- }
4672
- //#endregion
4673
- //#region src/federation/send.ts
5236
+ };
4674
5237
  /**
4675
- * Extracts the inbox URLs from recipients.
4676
- * @param parameters The parameters to extract the inboxes.
4677
- * See also {@link ExtractInboxesParameters}.
4678
- * @returns The inboxes as a map of inbox URL to actor URIs.
5238
+ * Responds with the given object in JSON-LD format.
5239
+ *
5240
+ * @param object The object to respond with.
5241
+ * @param options Options.
5242
+ * @since 0.3.0
4679
5243
  */
4680
- function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris }) {
4681
- const inboxes = {};
4682
- for (const recipient of recipients) {
4683
- let inbox;
4684
- let sharedInbox = false;
4685
- if (preferSharedInbox && recipient.endpoints?.sharedInbox != null) {
4686
- inbox = recipient.endpoints.sharedInbox;
4687
- sharedInbox = true;
4688
- } else inbox = recipient.inboxId;
4689
- if (inbox != null && recipient.id != null) {
4690
- if (excludeBaseUris != null && excludeBaseUris.some((u) => u.origin === inbox?.origin)) continue;
4691
- inboxes[inbox.href] ??= {
4692
- actorIds: /* @__PURE__ */ new Set(),
4693
- sharedInbox
4694
- };
4695
- inboxes[inbox.href].actorIds.add(recipient.id.href);
4696
- }
4697
- }
4698
- return inboxes;
5244
+ async function respondWithObject(object, options) {
5245
+ const jsonLd = await object.toJsonLd(options);
5246
+ return new Response(JSON.stringify(jsonLd), { headers: { "Content-Type": "application/activity+json" } });
4699
5247
  }
4700
5248
  /**
4701
- * Sends an {@link Activity} to an inbox.
5249
+ * Responds with the given object in JSON-LD format if the request accepts
5250
+ * JSON-LD.
4702
5251
  *
4703
- * @param parameters The parameters for sending the activity.
4704
- * See also {@link SendActivityParameters}.
4705
- * @throws {Error} If the activity fails to send.
5252
+ * @param object The object to respond with.
5253
+ * @param request The request to check for JSON-LD acceptability.
5254
+ * @param options Options.
5255
+ * @since 0.3.0
4706
5256
  */
4707
- function sendActivity(options) {
4708
- const tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
4709
- return tracerProvider.getTracer(name, version).startActiveSpan("activitypub.send_activity", {
4710
- kind: SpanKind.CLIENT,
4711
- attributes: { "activitypub.shared_inbox": options.sharedInbox ?? false }
4712
- }, async (span) => {
4713
- if (options.activityId != null) span.setAttribute("activitypub.activity.id", options.activityId);
4714
- if (options.activityType != null) span.setAttribute("activitypub.activity.type", options.activityType);
4715
- try {
4716
- await sendActivityInternal({
4717
- ...options,
4718
- tracerProvider
4719
- }, span);
4720
- } catch (e) {
4721
- span.setStatus({
4722
- code: SpanStatusCode.ERROR,
4723
- message: String(e)
4724
- });
4725
- throw e;
4726
- } finally {
4727
- span.end();
4728
- }
4729
- });
4730
- }
4731
- const MAX_ERROR_RESPONSE_BODY_BYTES = 1024;
4732
- function getActivityActorId(activity) {
4733
- if (!isRecord(activity)) return void 0;
4734
- return getIdValue(activity.actor);
4735
- }
4736
- function getIdValue(value) {
4737
- if (typeof value === "string" && value !== "") return value;
4738
- if (value instanceof URL) return value.href;
4739
- if (Array.isArray(value)) {
4740
- for (const item of value) {
4741
- const id = getIdValue(item);
4742
- if (id != null) return id;
4743
- }
4744
- return;
4745
- }
4746
- if (isRecord(value)) return getIdValue(value.id);
4747
- }
4748
- function isRecord(value) {
4749
- return typeof value === "object" && value != null;
4750
- }
4751
- async function readLimitedResponseBody(response, maxBytes) {
4752
- if (response.body == null) return "";
4753
- const reader = response.body.getReader();
4754
- const decoder = new TextDecoder();
4755
- const chunks = [];
4756
- let totalBytes = 0;
4757
- let truncated = false;
4758
- try {
4759
- while (true) {
4760
- const { done, value } = await reader.read();
4761
- if (done) break;
4762
- if (totalBytes + value.length > maxBytes) {
4763
- const remaining = maxBytes - totalBytes;
4764
- if (remaining > 0) chunks.push(decoder.decode(value.slice(0, remaining), { stream: true }));
4765
- truncated = true;
4766
- break;
4767
- }
4768
- chunks.push(decoder.decode(value, { stream: true }));
4769
- totalBytes += value.length;
4770
- }
4771
- } finally {
4772
- reader.releaseLock();
4773
- }
4774
- let result = chunks.join("");
4775
- if (truncated) result += "… (truncated)";
4776
- return result;
4777
- }
4778
- async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, allowPrivateAddress, headers, specDeterminer, meterProvider, tracerProvider }, span) {
4779
- const logger = getLogger([
4780
- "fedify",
4781
- "federation",
4782
- "outbox"
4783
- ]);
4784
- const federationMetrics = getFederationMetrics(meterProvider);
4785
- const started = performance.now();
4786
- let deliverySuccess = false;
4787
- async function validateUrl(url) {
4788
- if (!allowPrivateAddress) try {
4789
- await validatePublicUrl(url);
4790
- } catch (error) {
4791
- if (error instanceof UrlError) logger.error("Disallowed private URL: {url}", {
4792
- url,
4793
- error
4794
- });
4795
- throw error;
4796
- }
4797
- }
4798
- await validateUrl(inbox.href);
4799
- headers = new Headers(headers);
4800
- headers.set("Content-Type", "application/activity+json");
4801
- const request = new Request(inbox, {
4802
- method: "POST",
4803
- headers,
4804
- body: JSON.stringify(activity),
4805
- redirect: "manual"
4806
- });
4807
- let rsaKey = null;
4808
- for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
4809
- rsaKey = key;
4810
- break;
5257
+ async function respondWithObjectIfAcceptable(object, request, options) {
5258
+ if (!acceptsJsonLd(request)) return null;
5259
+ const response = await respondWithObject(object, options);
5260
+ response.headers.set("Vary", "Accept");
5261
+ return response;
5262
+ }
5263
+ function generateNonce() {
5264
+ const bytes = /* @__PURE__ */ new Uint8Array(16);
5265
+ crypto.getRandomValues(bytes);
5266
+ return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
5267
+ }
5268
+ async function verifySignatureNonce(request, kv, noncePrefix, verifiedLabel) {
5269
+ const signatureInput = request.headers.get("Signature-Input");
5270
+ if (signatureInput == null) return false;
5271
+ const parsed = parseRfc9421SignatureInput(signatureInput);
5272
+ if (verifiedLabel == null) return false;
5273
+ const sig = parsed[verifiedLabel];
5274
+ if (sig == null) return false;
5275
+ const nonce = sig.nonce;
5276
+ if (nonce == null) return false;
5277
+ const key = [...noncePrefix, nonce];
5278
+ if (kv.cas != null) return await kv.cas(key, true, void 0);
5279
+ if (await kv.get(key) != null) {
5280
+ await kv.delete(key);
5281
+ return true;
4811
5282
  }
4812
- if (rsaKey == null && keys.length < 1) logger.debug("Sending the activity {activityId} to {inbox} without a signature.", {
4813
- activityId,
4814
- inbox: inbox.href
4815
- });
4816
- 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.", {
4817
- inbox: inbox.href,
4818
- keys: keys.map((pair) => ({
4819
- keyId: pair.keyId.href,
4820
- privateKey: pair.privateKey
4821
- }))
5283
+ return false;
5284
+ }
5285
+ const getFailedSignatureResponse = async (policy, kv, kvPrefixes) => {
5286
+ const headers = await getFailedSignatureHeaders(policy, kv, kvPrefixes);
5287
+ return new Response("Failed to verify the request signature.", {
5288
+ status: 401,
5289
+ headers
4822
5290
  });
4823
- let response;
4824
- try {
4825
- response = rsaKey == null ? await fetchWithValidatedRedirects(request, validateUrl) : await doubleKnock(request, rsaKey, {
4826
- tracerProvider,
4827
- specDeterminer,
4828
- validateRedirect: validateUrl
4829
- });
4830
- } catch (error) {
4831
- const failure = error instanceof FetchError || error instanceof UrlError ? error : createFetchError(inbox.href, error);
4832
- logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
4833
- activityId,
4834
- inbox: inbox.href,
4835
- error: failure
4836
- });
4837
- federationMetrics.recordDelivery(inbox, getDurationMs(started), false, activityType);
4838
- throw failure;
5291
+ };
5292
+ const getFailedSignatureHeaders = async (policy, kv, kvPrefixes) => ({
5293
+ "Content-Type": "text/plain; charset=utf-8",
5294
+ ...policy?.enabled && {
5295
+ "Accept-Signature": await buildAcceptSignatureHeader(policy, kv, kvPrefixes.acceptSignatureNonce),
5296
+ "Cache-Control": "no-store",
5297
+ "Vary": "Accept, Signature"
4839
5298
  }
4840
- try {
4841
- if (!response.ok) {
4842
- let error;
4843
- try {
4844
- error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
4845
- } catch (_) {
4846
- error = "";
4847
- }
4848
- logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
4849
- activityId,
4850
- inbox: inbox.href,
4851
- status: response.status,
4852
- statusText: response.statusText,
4853
- error
4854
- });
4855
- throw new SendActivityError(inbox, response.status, `Failed to send activity ${activityId} to ${inbox.href} (${response.status} ${response.statusText}):\n${error}`, error, response.headers);
4856
- }
4857
- deliverySuccess = true;
4858
- const eventAttributes = {
4859
- "activitypub.inbox.url": inbox.href,
4860
- "activitypub.activity.id": activityId ?? ""
4861
- };
4862
- if (activityType != null) eventAttributes["activitypub.activity.type"] = activityType;
4863
- const actorId = getActivityActorId(activity);
4864
- if (actorId != null) eventAttributes["activitypub.actor.id"] = actorId;
4865
- span.addEvent("activitypub.activity.sent", eventAttributes);
4866
- } finally {
4867
- federationMetrics.recordDelivery(inbox, getDurationMs(started), deliverySuccess, activityType);
5299
+ });
5300
+ async function buildAcceptSignatureHeader(policy, kv, noncePrefix) {
5301
+ const parameters = { created: true };
5302
+ if (policy.requestNonce) {
5303
+ const nonce = generateNonce();
5304
+ await setKey(kv, [...noncePrefix, nonce], policy);
5305
+ parameters.nonce = nonce;
4868
5306
  }
5307
+ const baseComponents = policy.components ?? DEF_COMPONENTS;
5308
+ return formatAcceptSignature([{
5309
+ label: "sig1",
5310
+ components: uniq(MIN_COMPONENTS.concat(baseComponents)).filter((c) => c !== "@status").map((v) => ({
5311
+ value: v,
5312
+ params: {}
5313
+ })),
5314
+ parameters
5315
+ }]);
4869
5316
  }
4870
- function createFetchError(url, cause) {
4871
- const error = new FetchError(url, cause instanceof Error ? cause.message : String(cause));
4872
- error.cause = cause;
4873
- return error;
5317
+ async function setKey(kv, key, policy) {
5318
+ const seconds = policy.nonceTtlSeconds ?? 300;
5319
+ const ttl = Temporal.Duration.from({ seconds });
5320
+ await kv.set(key, true, { ttl });
4874
5321
  }
4875
- async function fetchWithValidatedRedirects(request, validateUrl) {
4876
- let body = await request.clone().text();
4877
- for (let redirects = 0;; redirects++) {
4878
- const response = await fetch(request, { redirect: "manual" });
4879
- const location = response.headers.get("Location");
4880
- if (![
4881
- 301,
4882
- 302,
4883
- 303,
4884
- 307,
4885
- 308
4886
- ].includes(response.status) || location == null) return response;
4887
- await response.body?.cancel();
4888
- if (redirects >= 20) throw new TypeError("Too many redirects");
4889
- const url = new URL(location, request.url);
4890
- await validateUrl(url.href);
4891
- const headers = new Headers(request.headers);
4892
- if (url.origin !== new URL(request.url).origin) {
4893
- headers.delete("Authorization");
4894
- headers.delete("Proxy-Authorization");
4895
- headers.delete("Cookie");
4896
- headers.delete("Host");
4897
- }
4898
- let method = request.method;
4899
- if ((response.status === 301 || response.status === 302) && method === "POST" || response.status === 303 && method !== "GET" && method !== "HEAD") {
4900
- method = "GET";
4901
- body = void 0;
4902
- for (const name of [
4903
- "Content-Encoding",
4904
- "Content-Language",
4905
- "Content-Length",
4906
- "Content-Location",
4907
- "Content-Type"
4908
- ]) headers.delete(name);
4909
- }
4910
- request = new Request(url, {
4911
- method,
4912
- headers,
4913
- body,
4914
- redirect: "manual"
4915
- });
5322
+ const DEF_COMPONENTS = [
5323
+ "@method",
5324
+ "@target-uri",
5325
+ "@authority",
5326
+ "content-digest"
5327
+ ];
5328
+ const MIN_COMPONENTS = [
5329
+ "@method",
5330
+ "@target-uri",
5331
+ "@authority"
5332
+ ];
5333
+ /**
5334
+ * Checks whether an HTTP Signature key is a gateway key of an FEP-ef61
5335
+ * portable actor, i.e., its ID is a compatible identifier and it is owned by
5336
+ * a portable actor, which only the portable actor's signed document can
5337
+ * establish.
5338
+ */
5339
+ function isPortableGatewayKey(key) {
5340
+ return key?.id != null && isCompatibleKeyId(key.id) && key.ownerId != null && isPortableId(key.ownerId);
5341
+ }
5342
+ /**
5343
+ * Checks whether an activity needs an Object Integrity Proof by its DID:
5344
+ * either it is performed by an [FEP-ef61] portable actor, or its own ID is
5345
+ * portable. Both `ap:` URIs and compatible identifiers count as portable.
5346
+ *
5347
+ * [FEP-ef61]: https://w3id.org/fep/ef61
5348
+ */
5349
+ function isPortableActivity(activity) {
5350
+ return hasPortableActor(activity) || activity.id != null && isPortableId(activity.id);
5351
+ }
5352
+ /**
5353
+ * Verifies that a portable activity's own ID belongs to the DID that signed
5354
+ * it, on the JSON-LD document received. The ID the proof policy checks has
5355
+ * to be the parsed activity's ID, so that a remote context that changes
5356
+ * between loads cannot make the two name different objects.
5357
+ * @returns `null` if the ID is verified, or the reason why it is not.
5358
+ */
5359
+ async function verifyPortableActivityId(activityId, jsonLd, options) {
5360
+ const expectedId = getCanonicalPortableId(activityId);
5361
+ if (expectedId == null) return "malformed portable ID";
5362
+ let verification;
5363
+ try {
5364
+ verification = await verifyPortableObjectProofWithRoot(jsonLd, options);
5365
+ } catch (error) {
5366
+ if (error instanceof InvalidPortableObjectIdError) return "malformed portable ID";
5367
+ if (!isPermanentActivityParseError(error)) throw error;
5368
+ return "malformed document";
4916
5369
  }
5370
+ const { result, root } = verification;
5371
+ if (!result.verified) return result.reason.type;
5372
+ const rootId = root?.["@id"];
5373
+ if (typeof rootId !== "string" || getCanonicalPortableId(parseIri(rootId)) !== expectedId) return "the verified document has another ID";
5374
+ return null;
4917
5375
  }
5376
+ //#endregion
5377
+ //#region src/nodeinfo/handler.ts
4918
5378
  /**
4919
- * An error that is thrown when an activity fails to send to a remote inbox.
4920
- * It contains structured information about the failure, including the HTTP
4921
- * status code, the inbox URL, and the response body.
4922
- * @since 2.0.0
5379
+ * Handles a NodeInfo request. You would not typically call this function
5380
+ * directly, but instead use {@link Federation.handle} method.
5381
+ * @param request The NodeInfo request to handle.
5382
+ * @param parameters The parameters for handling the request.
5383
+ * @returns The response to the request.
4923
5384
  */
4924
- var SendActivityError = class extends Error {
4925
- /**
4926
- * The inbox URL that the activity was being sent to.
4927
- */
4928
- inbox;
4929
- /**
4930
- * The HTTP status code returned by the inbox.
4931
- */
4932
- statusCode;
4933
- /**
4934
- * The response body from the inbox, if any. Note that this may be
4935
- * truncated to a maximum of 1 KiB to prevent excessive memory consumption
4936
- * when remote servers return large error pages (e.g., Cloudflare error pages).
4937
- * If truncated, the string will end with `"… (truncated)"`.
4938
- */
4939
- responseBody;
4940
- /**
4941
- * The response headers from the inbox.
4942
- * @since 2.3.0
4943
- */
4944
- responseHeaders;
4945
- /**
4946
- * Creates a new {@link SendActivityError}.
4947
- * @param inbox The inbox URL.
4948
- * @param statusCode The HTTP status code.
4949
- * @param message The error message.
4950
- * @param responseBody The response body.
4951
- * @param responseHeaders The response headers.
4952
- */
4953
- constructor(inbox, statusCode, message, responseBody, responseHeaders) {
4954
- super(message);
4955
- this.name = "SendActivityError";
4956
- this.inbox = inbox;
4957
- this.statusCode = statusCode;
4958
- this.responseBody = responseBody;
4959
- this.responseHeaders = new Headers(responseHeaders);
5385
+ async function handleNodeInfo(_request, { context, nodeInfoDispatcher }) {
5386
+ const promise = nodeInfoDispatcher(context);
5387
+ const json = nodeInfoToJson(promise instanceof Promise ? await promise : promise);
5388
+ return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json; profile=\"http://nodeinfo.diaspora.software/ns/schema/2.1#\"" } });
5389
+ }
5390
+ /**
5391
+ * Handles a request to `/.well-known/nodeinfo`. You would not typically call
5392
+ * this function directly, but instead use {@link Federation.handle} method.
5393
+ * @param request The request to handle.
5394
+ * @param context The request context.
5395
+ * @returns The response to the request.
5396
+ */
5397
+ function handleNodeInfoJrd(_request, context) {
5398
+ const links = [];
5399
+ try {
5400
+ links.push({
5401
+ rel: "http://nodeinfo.diaspora.software/ns/schema/2.1",
5402
+ href: context.getNodeInfoUri().href,
5403
+ type: "application/json; profile=\"http://nodeinfo.diaspora.software/ns/schema/2.1#\""
5404
+ });
5405
+ } catch (e) {
5406
+ if (!(e instanceof RouterError)) throw e;
4960
5407
  }
4961
- };
5408
+ const response = new Response(JSON.stringify({ links }), { headers: { "Content-Type": "application/jrd+json" } });
5409
+ return Promise.resolve(response);
5410
+ }
4962
5411
  //#endregion
4963
5412
  //#region src/federation/bench.ts
4964
5413
  /**
@@ -5044,417 +5493,115 @@ async function handleBenchmarkTrigger(request, context, options = {}) {
5044
5493
  const recipients = await parseRecipients(body.recipients, context);
5045
5494
  const activity = await parseActivity(body.activity, context);
5046
5495
  if (activity.id == null) throw new BenchmarkTriggerError("activity must have an id.");
5047
- const activityId = activity.id.href;
5048
- const inboxes = extractInboxes({ recipients });
5049
- const inboxUrls = Object.keys(inboxes);
5050
- if (inboxUrls.length < 1) throw new BenchmarkTriggerError("No valid recipient inboxes found. The recipients list must not be empty.");
5051
- const unsafeInboxes = options.allowUnsafeRecipients ? [] : inboxUrls.filter((inbox) => !options.sinks?.has(inbox));
5052
- if (unsafeInboxes.length > 0) return jsonResponse({
5053
- error: "unsafe_recipient",
5054
- unsafeInboxes
5055
- }, 403);
5056
- await context.sendActivity(sender, recipients, activity);
5057
- return jsonResponse({
5058
- version: 1,
5059
- activityId,
5060
- queueCorrelationId: activityId,
5061
- recipientCount: recipients.length,
5062
- inboxCount: inboxUrls.length
5063
- }, 202);
5064
- } catch (error) {
5065
- if (error instanceof BenchmarkTriggerError) return jsonResponse({ error: error.message }, error.status);
5066
- throw error;
5067
- }
5068
- }
5069
- var BenchmarkTriggerError = class extends Error {
5070
- status;
5071
- constructor(message, status = 400) {
5072
- super(message);
5073
- this.status = status;
5074
- }
5075
- };
5076
- function parseSender(value) {
5077
- const sender = asRecord(value, "sender");
5078
- if (typeof sender.identifier === "string") return { identifier: sender.identifier };
5079
- if (typeof sender.username === "string") return { username: sender.username };
5080
- throw new BenchmarkTriggerError("sender must be { identifier } or { username }.");
5081
- }
5082
- async function parseRecipients(value, context) {
5083
- if (!Array.isArray(value)) throw new BenchmarkTriggerError("recipients must be an array.");
5084
- return await Promise.all(value.map(async (item) => {
5085
- let object;
5086
- try {
5087
- object = await Object$1.fromJsonLd(item, {
5088
- documentLoader: context.documentLoader,
5089
- contextLoader: context.contextLoader
5090
- });
5091
- } catch (error) {
5092
- throw new BenchmarkTriggerError(`Invalid ActivityPub recipient: ${error}`);
5093
- }
5094
- if (!isRecipient(object)) throw new BenchmarkTriggerError("each recipient must be an ActivityPub actor.");
5095
- const recipient = object;
5096
- if (recipient.id == null || recipient.inboxId == null) throw new BenchmarkTriggerError("each recipient must have id and inbox properties.");
5097
- return recipient;
5098
- }));
5099
- }
5100
- function isRecipient(value) {
5101
- return value != null && typeof value === "object" && "id" in value && "inboxId" in value;
5102
- }
5103
- async function parseActivity(value, context) {
5104
- try {
5105
- return await Activity.fromJsonLd(value, {
5106
- documentLoader: context.documentLoader,
5107
- contextLoader: context.contextLoader
5108
- });
5109
- } catch (error) {
5110
- throw new BenchmarkTriggerError(`Invalid ActivityPub activity: ${error}`);
5111
- }
5112
- }
5113
- function asRecord(value, name) {
5114
- if (value == null || typeof value !== "object" || Array.isArray(value)) throw new BenchmarkTriggerError(`${name} must be an object.`);
5115
- return value;
5116
- }
5117
- function jsonResponse(body, status = 200) {
5118
- return new Response(JSON.stringify(body), {
5119
- status,
5120
- headers: { "Content-Type": "application/json" }
5121
- });
5122
- }
5123
- function serializeScopeMetrics(resourceMetrics) {
5124
- return resourceMetrics.scopeMetrics.map(serializeScope);
5125
- }
5126
- function serializeScope(scopeMetrics) {
5127
- return {
5128
- scope: {
5129
- name: scopeMetrics.scope.name,
5130
- version: scopeMetrics.scope.version
5131
- },
5132
- metrics: scopeMetrics.metrics.map(serializeMetric)
5133
- };
5134
- }
5135
- function serializeMetric(metric) {
5136
- return {
5137
- name: metric.descriptor.name,
5138
- description: metric.descriptor.description,
5139
- unit: metric.descriptor.unit,
5140
- dataPointType: serializeDataPointType(metric.dataPointType),
5141
- dataPoints: metric.dataPoints.map((point) => ({
5142
- attributes: { ...point.attributes },
5143
- startTime: point.startTime,
5144
- endTime: point.endTime,
5145
- value: point.value
5146
- }))
5147
- };
5148
- }
5149
- function serializeDataPointType(dataPointType) {
5150
- switch (dataPointType) {
5151
- case DataPointType.HISTOGRAM: return "histogram";
5152
- case DataPointType.EXPONENTIAL_HISTOGRAM: return "exponential_histogram";
5153
- case DataPointType.GAUGE: return "gauge";
5154
- case DataPointType.SUM: return "sum";
5155
- }
5156
- }
5157
- //#endregion
5158
- //#region src/federation/portable-inbox.ts
5159
- const MAX_TIMEOUT_MS = 2 ** 31 - 1;
5160
- /**
5161
- * Fills in the defaults of {@link PortableInboxForwardingOptions} and
5162
- * validates them.
5163
- * @param options The options.
5164
- * @returns The resolved options.
5165
- * @throws {RangeError} If `maxTargets` is not a non-negative integer, if `ttl`
5166
- * is not positive, if `deadline` is negative or longer
5167
- * than 2,147,483,647 milliseconds (about 24.8 days), or
5168
- * if either duration has calendar units, i.e., weeks,
5169
- * months, or years, whose length depends on the date.
5170
- */
5171
- function resolvePortableInboxForwardingOptions(options = {}) {
5172
- const maxTargets = options.maxTargets ?? 10;
5173
- if (!Number.isInteger(maxTargets) || maxTargets < 0) throw new RangeError("portableInboxForwarding.maxTargets must be a non-negative integer.");
5174
- const ttl = Temporal.Duration.from(options.ttl ?? { days: 30 });
5175
- if (ttl.total("millisecond") <= 0) throw new RangeError("portableInboxForwarding.ttl must be positive.");
5176
- const deadline = Temporal.Duration.from(options.deadline ?? { seconds: 10 });
5177
- const deadlineMs = deadline.total("millisecond");
5178
- if (deadlineMs < 0 || deadlineMs > MAX_TIMEOUT_MS) throw new RangeError(`portableInboxForwarding.deadline must be between 0 and ${MAX_TIMEOUT_MS} milliseconds.`);
5179
- return {
5180
- maxTargets,
5181
- ttl,
5182
- deadline
5183
- };
5184
- }
5185
- /**
5186
- * Decides whether this server accepts a delivery to a portable inbox on behalf
5187
- * of the given actor, which the actor dispatcher returned for the identifier
5188
- * in the requested inbox path.
5189
- *
5190
- * The actor document comes from the application, so it is trusted as is and
5191
- * its proof is not verified here.
5192
- * @param actor The actor the actor dispatcher returned.
5193
- * @param options The requested DID, the canonical form of the requested inbox
5194
- * ID, and the origin of this server.
5195
- * @returns The resolution.
5196
- */
5197
- function resolvePortableInboxRecipient(actor, { authority, canonicalInboxId, localOrigin }) {
5198
- if (actor == null) return {
5199
- status: "rejected",
5200
- reason: "notFound"
5201
- };
5202
- const id = actor.id;
5203
- const did = id == null || !isPortableId(id) ? null : getPortableDid(id);
5204
- if (id == null || did == null) return {
5205
- status: "rejected",
5206
- reason: "notPortable"
5207
- };
5208
- if (did !== authority) return {
5209
- status: "rejected",
5210
- reason: "authorityMismatch"
5211
- };
5212
- const inboxId = actor.inboxId == null ? null : toPortableUri(actor.inboxId);
5213
- if (inboxId == null || getCanonicalPortableId(inboxId) !== canonicalInboxId) return {
5214
- status: "rejected",
5215
- reason: "inboxMismatch"
5216
- };
5217
- const gateways = actor.gateways;
5218
- if (!gateways.some((g) => isGatewayUrl(g) && g.origin === localOrigin)) return {
5219
- status: "rejected",
5220
- reason: "notGateway"
5221
- };
5222
- return {
5223
- status: "accepted",
5224
- recipient: {
5225
- actorId: id,
5226
- inboxId,
5227
- canonicalInboxId,
5228
- gateways
5229
- }
5230
- };
5231
- }
5232
- /**
5233
- * Gets the portable form of an `ap:` or `ap+ef61:` URI or a compatible
5234
- * identifier, keeping its query.
5235
- * @returns The portable URI, or `null` if the ID is neither, or is malformed.
5236
- */
5237
- function toPortableUri(id) {
5238
- if (isPortableUri(id)) return id;
5239
- try {
5240
- return fromCompatibleEf61Id(id);
5241
- } catch (error) {
5242
- if (error instanceof TypeError) return null;
5243
- throw error;
5244
- }
5245
- }
5246
- /**
5247
- * Forwards an activity received in a portable inbox to the inboxes of the same
5248
- * portable actor on its other gateways, as FEP-ef61 recommends.
5249
- *
5250
- * FEP-ef61 forbids forwarding an activity from an inbox more than once, so
5251
- * each target gateway is claimed with an atomic compare-and-swap before the
5252
- * activity is handed off, and a claim is never released, even if the hand-off
5253
- * fails, since a failed hand-off does not prove that nothing was delivered.
5254
- * Forwarding is therefore best effort. Without {@link KvStore.cas}, nothing is
5255
- * forwarded.
5256
- *
5257
- * Forwarded requests are signed with HTTP Signatures by the RSA key among
5258
- * {@link ForwardPortableInboxActivityParameters.getKeys}, if any, so that
5259
- * servers requiring HTTP Signatures accept them, and are sent unsigned
5260
- * otherwise. Either way, the receiving gateways authenticate the activity by
5261
- * its own proof.
5262
- * @param parameters The parameters.
5263
- * @returns The target inbox URLs that the activity was handed off to.
5264
- */
5265
- async function forwardPortableInboxActivity(parameters) {
5266
- const logger = getLogger([
5267
- "fedify",
5268
- "federation",
5269
- "inbox"
5270
- ]);
5271
- const { recipient, activityId, excludedOrigins, kv, kvPrefix, outboxQueue } = parameters;
5272
- const { maxTargets, ttl } = parameters.options ?? resolvePortableInboxForwardingOptions();
5273
- if (kv.cas == null || maxTargets < 1) return [];
5274
- const canonicalActivityId = getCanonicalPortableId(activityId) ?? activityId.href;
5275
- const targets = getForwardingTargets(recipient, excludedOrigins);
5276
- if (targets.length > maxTargets) {
5277
- logger.warn("The portable actor that owns the inbox {inbox} has more than {max} other gateways; the activity {activityId} is forwarded only to the first {max} of them.", {
5278
- inbox: recipient.canonicalInboxId,
5279
- activityId: activityId.href,
5280
- max: maxTargets
5281
- });
5282
- targets.length = maxTargets;
5496
+ const activityId = activity.id.href;
5497
+ const inboxes = extractInboxes({ recipients });
5498
+ const inboxUrls = Object.keys(inboxes);
5499
+ if (inboxUrls.length < 1) throw new BenchmarkTriggerError("No valid recipient inboxes found. The recipients list must not be empty.");
5500
+ const unsafeInboxes = options.allowUnsafeRecipients ? [] : inboxUrls.filter((inbox) => !options.sinks?.has(inbox));
5501
+ if (unsafeInboxes.length > 0) return jsonResponse({
5502
+ error: "unsafe_recipient",
5503
+ unsafeInboxes
5504
+ }, 403);
5505
+ await context.sendActivity(sender, recipients, activity);
5506
+ return jsonResponse({
5507
+ version: 1,
5508
+ activityId,
5509
+ queueCorrelationId: activityId,
5510
+ recipientCount: recipients.length,
5511
+ inboxCount: inboxUrls.length
5512
+ }, 202);
5513
+ } catch (error) {
5514
+ if (error instanceof BenchmarkTriggerError) return jsonResponse({ error: error.message }, error.status);
5515
+ throw error;
5283
5516
  }
5284
- const claimed = [];
5285
- for (const { gateway, inbox } of targets) {
5286
- const key = [
5287
- ...kvPrefix,
5288
- recipient.canonicalInboxId,
5289
- canonicalActivityId,
5290
- gateway
5291
- ];
5292
- if (await kv.cas(key, void 0, true, { ttl })) claimed.push(inbox);
5517
+ }
5518
+ var BenchmarkTriggerError = class extends Error {
5519
+ status;
5520
+ constructor(message, status = 400) {
5521
+ super(message);
5522
+ this.status = status;
5293
5523
  }
5294
- if (claimed.length < 1) return [];
5295
- logger.debug("Forwarding activity {activityId} from the portable inbox {inbox} to other gateways:\n{targets}", {
5296
- activityId: activityId.href,
5297
- inbox: recipient.canonicalInboxId,
5298
- targets: claimed.map((t) => t.href)
5299
- });
5300
- if (outboxQueue == null) await forwardImmediately(parameters, claimed);
5301
- else await enqueueForwarding(parameters, outboxQueue, claimed);
5302
- return claimed;
5524
+ };
5525
+ function parseSender(value) {
5526
+ const sender = asRecord(value, "sender");
5527
+ if (typeof sender.identifier === "string") return { identifier: sender.identifier };
5528
+ if (typeof sender.username === "string") return { username: sender.username };
5529
+ throw new BenchmarkTriggerError("sender must be { identifier } or { username }.");
5303
5530
  }
5304
- function getForwardingTargets(recipient, excludedOrigins) {
5305
- const logger = getLogger([
5306
- "fedify",
5307
- "federation",
5308
- "inbox"
5309
- ]);
5310
- const seen = new Set(excludedOrigins);
5311
- const targets = [];
5312
- for (const gateway of recipient.gateways) {
5313
- if (!isGatewayUrl(gateway) || seen.has(gateway.origin)) continue;
5314
- seen.add(gateway.origin);
5315
- let inbox;
5531
+ async function parseRecipients(value, context) {
5532
+ if (!Array.isArray(value)) throw new BenchmarkTriggerError("recipients must be an array.");
5533
+ return await Promise.all(value.map(async (item) => {
5534
+ let object;
5316
5535
  try {
5317
- inbox = toCompatibleEf61Id(recipient.inboxId, gateway.origin);
5318
- } catch (error) {
5319
- if (!(error instanceof TypeError)) throw error;
5320
- logger.warn("Cannot forward to the gateway {gateway}, as the portable inbox {inbox} cannot be represented as its compatible identifier.", {
5321
- gateway: gateway.origin,
5322
- inbox: recipient.canonicalInboxId
5536
+ object = await Object$1.fromJsonLd(item, {
5537
+ documentLoader: context.documentLoader,
5538
+ contextLoader: context.contextLoader
5323
5539
  });
5324
- continue;
5540
+ } catch (error) {
5541
+ throw new BenchmarkTriggerError(`Invalid ActivityPub recipient: ${error}`);
5325
5542
  }
5326
- targets.push({
5327
- gateway: gateway.origin,
5328
- inbox
5329
- });
5330
- }
5331
- return targets;
5543
+ if (!isRecipient(object)) throw new BenchmarkTriggerError("each recipient must be an ActivityPub actor.");
5544
+ const recipient = object;
5545
+ if (recipient.id == null || recipient.inboxId == null) throw new BenchmarkTriggerError("each recipient must have id and inbox properties.");
5546
+ return recipient;
5547
+ }));
5332
5548
  }
5333
- /**
5334
- * Resolves the key that signs forwarded requests: the first RSASSA-PKCS1-v1_5
5335
- * key, as {@link sendActivity} signs requests only with such a key. Since
5336
- * the targets have already been claimed, the activity is forwarded unsigned
5337
- * rather than not at all if the key cannot be resolved or used.
5338
- */
5339
- async function resolveForwardingKey({ getKeys, activityId }) {
5340
- if (getKeys == null) return null;
5341
- const logger = getLogger([
5342
- "fedify",
5343
- "federation",
5344
- "inbox"
5345
- ]);
5346
- let key;
5549
+ function isRecipient(value) {
5550
+ return value != null && typeof value === "object" && "id" in value && "inboxId" in value;
5551
+ }
5552
+ async function parseActivity(value, context) {
5347
5553
  try {
5348
- key = (await getKeys()).find((k) => k.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5");
5349
- } catch (error) {
5350
- logger.error("Failed to get the gateway keys to sign forwarded requests with; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
5351
- activityId: activityId.href,
5352
- error
5554
+ return await Activity.fromJsonLd(value, {
5555
+ documentLoader: context.documentLoader,
5556
+ contextLoader: context.contextLoader
5353
5557
  });
5354
- return null;
5355
- }
5356
- if (key == null) return null;
5357
- try {
5358
- validateCryptoKey(key.privateKey, "private");
5359
5558
  } catch (error) {
5360
- if (!(error instanceof TypeError)) throw error;
5361
- logger.warn("The gateway key {keyId} cannot sign forwarded requests; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
5362
- keyId: key.keyId.href,
5363
- activityId: activityId.href,
5364
- error
5365
- });
5366
- return null;
5559
+ throw new BenchmarkTriggerError(`Invalid ActivityPub activity: ${error}`);
5367
5560
  }
5368
- return key;
5369
5561
  }
5370
- async function forwardImmediately(parameters, inboxes) {
5371
- const { activity, activityId, activityType, allowPrivateAddress, specDeterminer, options, meterProvider, tracerProvider } = parameters;
5372
- const logger = getLogger([
5373
- "fedify",
5374
- "federation",
5375
- "inbox"
5376
- ]);
5377
- const deadline = (options ?? resolvePortableInboxForwardingOptions()).deadline.total("millisecond");
5378
- const forwarding = resolveForwardingKey(parameters).then((key) => Promise.all(inboxes.map((inbox) => sendActivity({
5379
- activity,
5380
- activityId: activityId.href,
5381
- activityType,
5382
- keys: key == null ? [] : [key],
5383
- inbox,
5384
- allowPrivateAddress,
5385
- specDeterminer,
5386
- meterProvider,
5387
- tracerProvider
5388
- }).catch((error) => {
5389
- logger.error("Failed to forward activity {activityId} to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
5390
- activityId: activityId.href,
5391
- inbox: inbox.href,
5392
- error
5393
- });
5394
- }))));
5395
- let timer;
5396
- const timedOut = await Promise.race([forwarding.then(() => false), new Promise((resolve) => {
5397
- timer = setTimeout(() => resolve(true), deadline);
5398
- })]);
5399
- clearTimeout(timer);
5400
- if (timedOut) logger.warn("Forwarding activity {activityId} to other gateways did not finish in {deadline} ms; responding to the delivery without waiting for it.", {
5401
- activityId: activityId.href,
5402
- deadline
5562
+ function asRecord(value, name) {
5563
+ if (value == null || typeof value !== "object" || Array.isArray(value)) throw new BenchmarkTriggerError(`${name} must be an object.`);
5564
+ return value;
5565
+ }
5566
+ function jsonResponse(body, status = 200) {
5567
+ return new Response(JSON.stringify(body), {
5568
+ status,
5569
+ headers: { "Content-Type": "application/json" }
5403
5570
  });
5404
5571
  }
5405
- async function enqueueForwarding(parameters, outboxQueue, inboxes) {
5406
- const { activity, activityId, activityType, baseUrl, startQueue, meterProvider } = parameters;
5407
- const logger = getLogger([
5408
- "fedify",
5409
- "federation",
5410
- "inbox"
5411
- ]);
5412
- const key = await resolveForwardingKey(parameters);
5413
- const keys = [];
5414
- if (key != null) try {
5415
- keys.push({
5416
- keyId: key.keyId.href,
5417
- privateKey: await exportJwk(key.privateKey)
5418
- });
5419
- } catch (error) {
5420
- logger.error("Failed to export the gateway key {keyId}; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
5421
- keyId: key.keyId.href,
5422
- activityId: activityId.href,
5423
- error
5424
- });
5572
+ function serializeScopeMetrics(resourceMetrics) {
5573
+ return resourceMetrics.scopeMetrics.map(serializeScope);
5574
+ }
5575
+ function serializeScope(scopeMetrics) {
5576
+ return {
5577
+ scope: {
5578
+ name: scopeMetrics.scope.name,
5579
+ version: scopeMetrics.scope.version
5580
+ },
5581
+ metrics: scopeMetrics.metrics.map(serializeMetric)
5582
+ };
5583
+ }
5584
+ function serializeMetric(metric) {
5585
+ return {
5586
+ name: metric.descriptor.name,
5587
+ description: metric.descriptor.description,
5588
+ unit: metric.descriptor.unit,
5589
+ dataPointType: serializeDataPointType(metric.dataPointType),
5590
+ dataPoints: metric.dataPoints.map((point) => ({
5591
+ attributes: { ...point.attributes },
5592
+ startTime: point.startTime,
5593
+ endTime: point.endTime,
5594
+ value: point.value
5595
+ }))
5596
+ };
5597
+ }
5598
+ function serializeDataPointType(dataPointType) {
5599
+ switch (dataPointType) {
5600
+ case DataPointType.HISTOGRAM: return "histogram";
5601
+ case DataPointType.EXPONENTIAL_HISTOGRAM: return "exponential_histogram";
5602
+ case DataPointType.GAUGE: return "gauge";
5603
+ case DataPointType.SUM: return "sum";
5425
5604
  }
5426
- startQueue?.();
5427
- const started = (/* @__PURE__ */ new Date()).toISOString();
5428
- const traceContext = {};
5429
- propagation.inject(context.active(), traceContext);
5430
- await Promise.all(inboxes.map(async (inbox) => {
5431
- const message = {
5432
- type: "outbox",
5433
- id: crypto.randomUUID(),
5434
- baseUrl,
5435
- keys,
5436
- activity,
5437
- activityId: activityId.href,
5438
- activityType,
5439
- inbox: inbox.href,
5440
- sharedInbox: false,
5441
- started,
5442
- attempt: 0,
5443
- headers: {},
5444
- traceContext
5445
- };
5446
- try {
5447
- await outboxQueue.enqueue(message);
5448
- } catch (error) {
5449
- logger.error("Failed to enqueue activity {activityId} to forward to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
5450
- activityId: activityId.href,
5451
- inbox: inbox.href,
5452
- error
5453
- });
5454
- return;
5455
- }
5456
- recordOutboxEnqueue(meterProvider, outboxQueue, message);
5457
- }));
5458
5605
  }
5459
5606
  //#endregion
5460
5607
  //#region src/federation/outgoing-proof.ts
@@ -8022,17 +8169,39 @@ var FederationImpl = class extends FederationBuilderImpl {
8022
8169
  });
8023
8170
  }
8024
8171
  const route = this.router.route(portable.path);
8025
- if (route == null || !route.name.startsWith("object:")) {
8172
+ const isActor = route != null && (route.name === "actor" || route.name.startsWith("actorAlias:"));
8173
+ if (route == null || !isActor && !route.name.startsWith("object:")) {
8026
8174
  metricState.endpoint = "not_found";
8027
8175
  return await onNotFound(request);
8028
8176
  }
8029
8177
  metricState.routeTemplate = PORTABLE_GATEWAY_ROUTE_PREFIX + route.template;
8030
- metricState.endpoint = "object";
8178
+ metricState.endpoint = isActor ? "actor" : "object";
8031
8179
  span.updateName(`${request.method} ${metricState.routeTemplate}`);
8032
8180
  if (!acceptsJsonLd(request)) {
8033
8181
  metricState.endpoint = "not_acceptable";
8034
8182
  return await onNotAcceptable(request);
8035
8183
  }
8184
+ if (isActor) {
8185
+ const identifier = route.name.startsWith("actorAlias:") ? route.name.substring(11) : route.values.identifier;
8186
+ const context = this.#createContext(request, contextData, {
8187
+ invokedFromActorDispatcher: { identifier },
8188
+ portableRequest: portable.portableRequest
8189
+ });
8190
+ const actorDispatcher = this.actorCallbacks?.dispatcher;
8191
+ const authorizePredicate = this.actorCallbacks?.authorizePredicate;
8192
+ return await handlePortableObject(request, {
8193
+ values: { identifier },
8194
+ context,
8195
+ objectDispatcher: actorDispatcher == null ? void 0 : async (ctx) => {
8196
+ const actor = await actorDispatcher(ctx, identifier);
8197
+ return actor instanceof Tombstone ? null : actor;
8198
+ },
8199
+ authorizePredicate: authorizePredicate == null ? void 0 : (ctx) => authorizePredicate(ctx, identifier),
8200
+ canonicalId: portable.canonicalId,
8201
+ onUnauthorized,
8202
+ onNotFound
8203
+ });
8204
+ }
8036
8205
  const typeId = route.name.replace(/^object:/, "");
8037
8206
  const callbacks = this.objectCallbacks[typeId];
8038
8207
  const cls = this.objectTypeIds[typeId];
@@ -8131,7 +8300,7 @@ var FederationImpl = class extends FederationBuilderImpl {
8131
8300
  onNotFound,
8132
8301
  portableInbox: {
8133
8302
  recipient,
8134
- forward: async (activity, activityId, activityType) => {
8303
+ forward: async (activity, activityId, activityType, signatures) => {
8135
8304
  if (this.portableInboxForwarding.maxTargets < 1) return;
8136
8305
  if (this.kv.cas == null) {
8137
8306
  if (!this.#portableInboxForwardingWarned) {
@@ -8154,6 +8323,7 @@ var FederationImpl = class extends FederationBuilderImpl {
8154
8323
  allowPrivateAddress: this.allowPrivateAddress,
8155
8324
  getKeys: () => this.#getPortableGatewayKeyPairs(context, identifier, recipient),
8156
8325
  specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
8326
+ signatures,
8157
8327
  options: this.portableInboxForwarding,
8158
8328
  meterProvider: this.meterProvider,
8159
8329
  tracerProvider: this.tracerProvider
@@ -8395,9 +8565,17 @@ var ContextImpl = class ContextImpl {
8395
8565
  return new URL(path, this.canonicalOrigin);
8396
8566
  }
8397
8567
  getActorUri(identifier) {
8568
+ return new URL(this.#getActorPath(identifier), this.canonicalOrigin);
8569
+ }
8570
+ getPortableActorUri(identifier, authority) {
8571
+ const path = this.#getActorPath(identifier);
8572
+ if (authority == null) throw new TypeError("The authority of a portable ID is required outside an FEP-ef61 gateway request.");
8573
+ return buildPortableUri(authority, path);
8574
+ }
8575
+ #getActorPath(identifier) {
8398
8576
  const path = this.federation.router.build(`actorAlias:${identifier}`, {}) ?? this.federation.router.build("actor", { identifier });
8399
8577
  if (path == null) throw new RouterError("No actor dispatcher registered.");
8400
- return new URL(path, this.canonicalOrigin);
8578
+ return path;
8401
8579
  }
8402
8580
  getObjectUri(cls, values) {
8403
8581
  return new URL(this.#getObjectPath(cls, values), this.canonicalOrigin);
@@ -9040,6 +9218,9 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
9040
9218
  request: this.request
9041
9219
  });
9042
9220
  }
9221
+ getPortableActorUri(identifier, authority) {
9222
+ return super.getPortableActorUri(identifier, authority ?? this.portableRequest?.authority);
9223
+ }
9043
9224
  getPortableObjectUri(cls, values, authority) {
9044
9225
  return super.getPortableObjectUri(cls, values, authority ?? this.portableRequest?.authority);
9045
9226
  }
@@ -9436,4 +9617,4 @@ function getRequestId(request) {
9436
9617
  return `req_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
9437
9618
  }
9438
9619
  //#endregion
9439
- export { respondWithObject as a, digest as c, parseCircuitBreakerKvState as d, createFederationBuilder as f, SendActivityError as i, CircuitBreaker as l, handleWebFinger as n, respondWithObjectIfAcceptable as o, createExponentialBackoffPolicy as r, buildCollectionSynchronizationHeader as s, createFederation as t, normalizeCircuitBreakerOptions as u };
9620
+ export { respondWithObjectIfAcceptable as a, digest as c, parseCircuitBreakerKvState as d, createFederationBuilder as f, respondWithObject as i, CircuitBreaker as l, handleWebFinger as n, SendActivityError as o, createExponentialBackoffPolicy as r, buildCollectionSynchronizationHeader as s, createFederation as t, normalizeCircuitBreakerOptions as u };