@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
@@ -1,10 +1,10 @@
1
1
  const { URLPattern } = require("urlpattern-polyfill");
2
2
  const require_rolldown_runtime = require("./rolldown-runtime-Dmm3t92W.cjs");
3
3
  const require_transformers = require("./transformers-BalCrSsr.cjs");
4
- const require_http = require("./http-DSaSAN7X.cjs");
4
+ const require_http = require("./http-Cala2Grs.cjs");
5
5
  const require_types = require("./types-Be2zbyeG.cjs");
6
- const require_owner = require("./owner-szIZl4qI.cjs");
7
- const require_kv_cache = require("./kv-cache-BSJAlP4C.cjs");
6
+ const require_owner = require("./owner-DCt8W_Cv.cjs");
7
+ const require_kv_cache = require("./kv-cache-Bl5EBFrU.cjs");
8
8
  let _logtape_logtape = require("@logtape/logtape");
9
9
  let _fedify_uri_template = require("@fedify/uri-template");
10
10
  let _fedify_vocab = require("@fedify/vocab");
@@ -2411,6 +2411,8 @@ function acceptsJsonLd(request) {
2411
2411
  const PORTABLE_OBJECT_CONTENT_TYPE = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"";
2412
2412
  const GATEWAY_OBJECT_PATH_PATTERN = /^\/\.well-known\/apgateway\/did(?::|%3A)/i;
2413
2413
  const BARE_DID_PATTERN = /^did:[a-z0-9]+:[^/?#]+$/i;
2414
+ const DID_KEY_PREFIX = "did:key:";
2415
+ const BASE58BTC_MULTIBASE_PATTERN = /^z[1-9A-HJ-NP-Za-km-z]+$/;
2414
2416
  const GATEWAY_PATH_PREFIX = "/.well-known/apgateway/";
2415
2417
  const HASHLINK_SCHEME_PATTERN = /^hl(?::|%3A)/i;
2416
2418
  /**
@@ -2446,6 +2448,15 @@ function parsePortableGatewayRequest(url) {
2446
2448
  throw error;
2447
2449
  }
2448
2450
  const authority = (0, _fedify_vocab_runtime.getFe34Origin)(id);
2451
+ try {
2452
+ assertBase58BtcDidKey(authority);
2453
+ } catch (error) {
2454
+ if (error instanceof TypeError) return {
2455
+ type: "malformed",
2456
+ error
2457
+ };
2458
+ throw error;
2459
+ }
2449
2460
  const href = id.href;
2450
2461
  return {
2451
2462
  type: "object",
@@ -2464,12 +2475,26 @@ function parsePortableGatewayRequest(url) {
2464
2475
  * @param authority The bare DID, e.g., `did:key:z6Mk...`.
2465
2476
  * @param path The object path, e.g., `/notes/123`.
2466
2477
  * @returns The portable ID.
2467
- * @throws {TypeError} If the authority is not a bare DID.
2478
+ * @throws {TypeError} If the authority is not a bare DID, or it is
2479
+ * a `did:key` DID that is not encoded in base58-btc.
2468
2480
  */
2469
2481
  function buildPortableUri(authority, path) {
2470
2482
  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.");
2471
- (0, _fedify_vocab_runtime.getFe34Origin)(authority);
2472
- return (0, _fedify_vocab_runtime.parseIri)(`ap+ef61://${authority}${path}`);
2483
+ const did = (0, _fedify_vocab_runtime.getFe34Origin)(authority);
2484
+ assertBase58BtcDidKey(did);
2485
+ return (0, _fedify_vocab_runtime.parseIri)(`ap+ef61://${did}${path}`);
2486
+ }
2487
+ /**
2488
+ * Checks that a normalized DID, if it is a `did:key` DID, is encoded in
2489
+ * base58-btc, as FEP-ef61 requires so that the same key does not yield two
2490
+ * portable IDs. This only checks the encoding, not whether the DID is
2491
+ * a valid key.
2492
+ * @param did The DID normalized by `getFe34Origin()`.
2493
+ * @throws {TypeError} If the DID is a `did:key` DID that is not encoded in
2494
+ * base58-btc.
2495
+ */
2496
+ function assertBase58BtcDidKey(did) {
2497
+ 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.");
2473
2498
  }
2474
2499
  /**
2475
2500
  * Recognizes an FEP-ef61 gateway request for a resource addressed by
@@ -2521,158 +2546,858 @@ function parseHashlinkGatewayRequest(url) {
2521
2546
  };
2522
2547
  }
2523
2548
  //#endregion
2524
- //#region src/federation/temporal.ts
2525
- const Temporal$8 = require_http.Temporal;
2526
- function isPlainObject$1(value) {
2527
- return typeof value === "object" && value != null && !Array.isArray(value);
2528
- }
2529
- function normalizeDateTimeLiteral(value) {
2530
- return value.substring(19).match(/[Z+-]/) ? value : value + "Z";
2531
- }
2532
- function isMalformedDateTimeLiteral(value) {
2533
- if (typeof value !== "string") return false;
2534
- try {
2535
- Temporal$8.Instant.from(normalizeDateTimeLiteral(value));
2536
- return false;
2537
- } catch {
2538
- return true;
2539
- }
2540
- }
2541
- function isMalformedDurationLiteral(value) {
2542
- if (typeof value !== "string") return false;
2543
- try {
2544
- Temporal$8.Duration.from(value);
2545
- return false;
2546
- } catch {
2547
- return true;
2549
+ //#region src/federation/send.ts
2550
+ /**
2551
+ * Extracts the inbox URLs from recipients.
2552
+ * @param parameters The parameters to extract the inboxes.
2553
+ * See also {@link ExtractInboxesParameters}.
2554
+ * @returns The inboxes as a map of inbox URL to actor URIs.
2555
+ */
2556
+ function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris }) {
2557
+ const inboxes = {};
2558
+ for (const recipient of recipients) {
2559
+ let inbox;
2560
+ let sharedInbox = false;
2561
+ if (preferSharedInbox && recipient.endpoints?.sharedInbox != null) {
2562
+ inbox = recipient.endpoints.sharedInbox;
2563
+ sharedInbox = true;
2564
+ } else inbox = recipient.inboxId;
2565
+ if (inbox != null && recipient.id != null) {
2566
+ if (excludeBaseUris != null && excludeBaseUris.some((u) => u.origin === inbox?.origin)) continue;
2567
+ inboxes[inbox.href] ??= {
2568
+ actorIds: /* @__PURE__ */ new Set(),
2569
+ sharedInbox
2570
+ };
2571
+ inboxes[inbox.href].actorIds.add(recipient.id.href);
2572
+ }
2548
2573
  }
2574
+ return inboxes;
2549
2575
  }
2550
- const TEMPORAL_DATE_TIME_IRIS = /* @__PURE__ */ new Set([
2551
- "https://www.w3.org/ns/activitystreams#deleted",
2552
- "https://www.w3.org/ns/activitystreams#endTime",
2553
- "https://www.w3.org/ns/activitystreams#published",
2554
- "https://www.w3.org/ns/activitystreams#startTime",
2555
- "https://www.w3.org/ns/activitystreams#updated",
2556
- "http://purl.org/dc/terms/created",
2557
- "https://w3id.org/security#created"
2558
- ]);
2559
- const TEMPORAL_DURATION_IRIS = /* @__PURE__ */ new Set(["https://www.w3.org/ns/activitystreams#duration"]);
2560
- const QUESTION_CLOSED_IRI = "https://www.w3.org/ns/activitystreams#closed";
2561
- const XSD_DATE_TIME_IRI = "http://www.w3.org/2001/XMLSchema#dateTime";
2562
- function hasMalformedExpandedDateTimeLiteral(value) {
2563
- if (Array.isArray(value)) return value.some(hasMalformedExpandedDateTimeLiteral);
2564
- return isPlainObject$1(value) && "@value" in value && isMalformedDateTimeLiteral(value["@value"]);
2565
- }
2566
- function hasMalformedExpandedQuestionClosedLiteral(value) {
2567
- if (Array.isArray(value)) return value.some(hasMalformedExpandedQuestionClosedLiteral);
2568
- if (!isPlainObject$1(value) || !("@value" in value)) return false;
2569
- const literal = value["@value"];
2570
- if (typeof literal === "boolean") return false;
2571
- if (typeof literal !== "string") return false;
2572
- if (value["@type"] !== XSD_DATE_TIME_IRI) return false;
2573
- if (new Date(literal).toString() === "Invalid Date") return false;
2574
- return isMalformedDateTimeLiteral(literal);
2576
+ /**
2577
+ * Sends an {@link Activity} to an inbox.
2578
+ *
2579
+ * @param parameters The parameters for sending the activity.
2580
+ * See also {@link SendActivityParameters}.
2581
+ * @throws {Error} If the activity fails to send.
2582
+ */
2583
+ function sendActivity(options) {
2584
+ const tracerProvider = options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider();
2585
+ return tracerProvider.getTracer(require_http.name, require_http.version).startActiveSpan("activitypub.send_activity", {
2586
+ kind: _opentelemetry_api.SpanKind.CLIENT,
2587
+ attributes: { "activitypub.shared_inbox": options.sharedInbox ?? false }
2588
+ }, async (span) => {
2589
+ if (options.activityId != null) span.setAttribute("activitypub.activity.id", options.activityId);
2590
+ if (options.activityType != null) span.setAttribute("activitypub.activity.type", options.activityType);
2591
+ try {
2592
+ await sendActivityInternal({
2593
+ ...options,
2594
+ tracerProvider
2595
+ }, span);
2596
+ } catch (e) {
2597
+ span.setStatus({
2598
+ code: _opentelemetry_api.SpanStatusCode.ERROR,
2599
+ message: String(e)
2600
+ });
2601
+ throw e;
2602
+ } finally {
2603
+ span.end();
2604
+ }
2605
+ });
2575
2606
  }
2576
- function hasMalformedExpandedDurationLiteral(value) {
2577
- if (Array.isArray(value)) return value.some(hasMalformedExpandedDurationLiteral);
2578
- return isPlainObject$1(value) && "@value" in value && isMalformedDurationLiteral(value["@value"]);
2607
+ const MAX_ERROR_RESPONSE_BODY_BYTES = 1024;
2608
+ function getActivityActorId(activity) {
2609
+ if (!isRecord(activity)) return void 0;
2610
+ return getIdValue(activity.actor);
2579
2611
  }
2580
- function hasMalformedKnownTemporalLiteralInternal(value, visited) {
2581
- if (Array.isArray(value)) return value.some((item) => hasMalformedKnownTemporalLiteralInternal(item, visited));
2582
- if (!isPlainObject$1(value)) return false;
2583
- if (visited.has(value)) return false;
2584
- visited.add(value);
2585
- if ("@value" in value) return false;
2586
- for (const [key, child] of Object.entries(value)) {
2587
- if (TEMPORAL_DATE_TIME_IRIS.has(key)) {
2588
- if (hasMalformedExpandedDateTimeLiteral(child)) return true;
2589
- continue;
2590
- }
2591
- if (key === QUESTION_CLOSED_IRI) {
2592
- if (hasMalformedExpandedQuestionClosedLiteral(child)) return true;
2593
- continue;
2594
- }
2595
- if (TEMPORAL_DURATION_IRIS.has(key)) {
2596
- if (hasMalformedExpandedDurationLiteral(child)) return true;
2597
- continue;
2612
+ function getIdValue(value) {
2613
+ if (typeof value === "string" && value !== "") return value;
2614
+ if (value instanceof URL) return value.href;
2615
+ if (Array.isArray(value)) {
2616
+ for (const item of value) {
2617
+ const id = getIdValue(item);
2618
+ if (id != null) return id;
2598
2619
  }
2599
- if (hasMalformedKnownTemporalLiteralInternal(child, visited)) return true;
2620
+ return;
2600
2621
  }
2601
- return false;
2622
+ if (isRecord(value)) return getIdValue(value.id);
2602
2623
  }
2603
- async function hasMalformedKnownTemporalLiteral(value, contextLoader) {
2624
+ function isRecord(value) {
2625
+ return typeof value === "object" && value != null;
2626
+ }
2627
+ async function readLimitedResponseBody(response, maxBytes) {
2628
+ if (response.body == null) return "";
2629
+ const reader = response.body.getReader();
2630
+ const decoder = new TextDecoder();
2631
+ const chunks = [];
2632
+ let totalBytes = 0;
2633
+ let truncated = false;
2604
2634
  try {
2605
- return hasMalformedKnownTemporalLiteralInternal(await _fedify_vocab_runtime_jsonld.default.expand(value, {
2606
- documentLoader: require_http.getNormalizationContextLoader(contextLoader),
2607
- keepFreeFloatingNodes: true
2608
- }), /* @__PURE__ */ new Set());
2609
- } catch {
2610
- return false;
2635
+ while (true) {
2636
+ const { done, value } = await reader.read();
2637
+ if (done) break;
2638
+ if (totalBytes + value.length > maxBytes) {
2639
+ const remaining = maxBytes - totalBytes;
2640
+ if (remaining > 0) chunks.push(decoder.decode(value.slice(0, remaining), { stream: true }));
2641
+ truncated = true;
2642
+ break;
2643
+ }
2644
+ chunks.push(decoder.decode(value, { stream: true }));
2645
+ totalBytes += value.length;
2646
+ }
2647
+ } finally {
2648
+ reader.releaseLock();
2611
2649
  }
2650
+ let result = chunks.join("");
2651
+ if (truncated) result += "… (truncated)";
2652
+ return result;
2612
2653
  }
2613
- //#endregion
2614
- //#region src/federation/handler.ts
2615
- const Temporal$7 = require_http.Temporal;
2616
- const rawInboxContextFactorySymbol = Symbol("fedify.rawInboxContextFactory");
2617
- const INBOX_COMPOUND_PROOF_LIMITS = {
2618
- maxDepth: 64,
2619
- maxMaps: 1e4,
2620
- maxProofs: 32,
2621
- maxBytes: 10 * 1024 * 1024
2622
- };
2623
- function isRemoteContextLoadingFailure$1(error) {
2624
- return error instanceof Error && typeof error.details === "object" && error.details != null && error.details.code === "loading remote context failed";
2625
- }
2626
- function isPermanentRemoteContextError$1(error) {
2627
- if (!(error instanceof Error) || error.name !== "jsonld.InvalidUrl") return false;
2628
- const details = error.details;
2629
- if (details?.code === "invalid remote context") return true;
2630
- return isRemoteContextLoadingFailure$1(error) && typeof details?.url === "string" && !URL.canParse(details.url) && require_http.isClearlyMalformedContextReference(details.url);
2631
- }
2632
- function isInvalidJsonLdError(error) {
2633
- if (!(error instanceof Error)) return false;
2634
- const name = error.name;
2635
- return name === "UnsafeJsonLdError" || error instanceof require_http.InvalidContextReferenceError || isPermanentRemoteContextError$1(error) || name === "jsonld.SyntaxError" && !isRemoteContextLoadingFailure$1(error);
2636
- }
2637
- function isValidationTypeError(error) {
2638
- return error instanceof TypeError && (/^(Invalid JSON-LD:|Invalid type:|Unexpected type:|Invalid @id:)/.test(error.message) || require_http.isInvalidUrlTypeError(error));
2639
- }
2640
- function isPermanentActivityParseError(error) {
2641
- return isInvalidJsonLdError(error) || isValidationTypeError(error);
2642
- }
2643
- function hasHttpSignatureHeaders(request) {
2644
- return request.headers.has("Signature") || request.headers.has("Signature-Input");
2645
- }
2646
- function hasObjectIntegrityProof(json) {
2647
- return typeof json === "object" && json != null && "proof" in json;
2648
- }
2649
- /**
2650
- * Handles an actor request.
2651
- * @template TContextData The context data to pass to the context.
2652
- * @param request The HTTP request.
2653
- * @param parameters The parameters for handling the actor.
2654
- * @returns A promise that resolves to an HTTP response.
2655
- */
2656
- async function handleActor(request, { identifier, context, actorDispatcher, authorizePredicate, onNotFound, onUnauthorized }) {
2654
+ async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, allowPrivateAddress, headers, specDeterminer, meterProvider, tracerProvider }, span) {
2657
2655
  const logger = (0, _logtape_logtape.getLogger)([
2658
2656
  "fedify",
2659
2657
  "federation",
2660
- "actor"
2658
+ "outbox"
2661
2659
  ]);
2662
- if (actorDispatcher == null) {
2663
- logger.debug("Actor dispatcher is not set.", { identifier });
2664
- return await onNotFound(request);
2665
- }
2666
- const actor = await actorDispatcher(context, identifier);
2667
- if (actor == null) {
2668
- logger.debug("Actor {identifier} not found.", { identifier });
2669
- return await onNotFound(request);
2670
- }
2671
- if (authorizePredicate != null) {
2672
- if (!await authorizePredicate(context, identifier)) return await onUnauthorized(request);
2660
+ const federationMetrics = require_http.getFederationMetrics(meterProvider);
2661
+ const started = performance.now();
2662
+ let deliverySuccess = false;
2663
+ async function validateUrl(url) {
2664
+ if (!allowPrivateAddress) try {
2665
+ await (0, _fedify_vocab_runtime.validatePublicUrl)(url);
2666
+ } catch (error) {
2667
+ if (error instanceof _fedify_vocab_runtime.UrlError) logger.error("Disallowed private URL: {url}", {
2668
+ url,
2669
+ error
2670
+ });
2671
+ throw error;
2672
+ }
2673
2673
  }
2674
- if (actor instanceof _fedify_vocab.Tombstone) {
2675
- const jsonLd = await actor.toJsonLd(context);
2674
+ await validateUrl(inbox.href);
2675
+ headers = new Headers(headers);
2676
+ headers.set("Content-Type", "application/activity+json");
2677
+ const request = new Request(inbox, {
2678
+ method: "POST",
2679
+ headers,
2680
+ body: JSON.stringify(activity),
2681
+ redirect: "manual"
2682
+ });
2683
+ let rsaKey = null;
2684
+ for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
2685
+ rsaKey = key;
2686
+ break;
2687
+ }
2688
+ if (rsaKey == null && keys.length < 1) logger.debug("Sending the activity {activityId} to {inbox} without a signature.", {
2689
+ activityId,
2690
+ inbox: inbox.href
2691
+ });
2692
+ 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.", {
2693
+ inbox: inbox.href,
2694
+ keys: keys.map((pair) => ({
2695
+ keyId: pair.keyId.href,
2696
+ privateKey: pair.privateKey
2697
+ }))
2698
+ });
2699
+ let response;
2700
+ try {
2701
+ response = rsaKey == null ? await fetchWithValidatedRedirects(request, validateUrl) : await require_http.doubleKnock(request, rsaKey, {
2702
+ tracerProvider,
2703
+ specDeterminer,
2704
+ validateRedirect: validateUrl
2705
+ });
2706
+ } catch (error) {
2707
+ const failure = error instanceof _fedify_vocab_runtime.FetchError || error instanceof _fedify_vocab_runtime.UrlError ? error : createFetchError(inbox.href, error);
2708
+ logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
2709
+ activityId,
2710
+ inbox: inbox.href,
2711
+ error: failure
2712
+ });
2713
+ federationMetrics.recordDelivery(inbox, require_http.getDurationMs(started), false, activityType);
2714
+ throw failure;
2715
+ }
2716
+ try {
2717
+ if (!response.ok) {
2718
+ let error;
2719
+ try {
2720
+ error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
2721
+ } catch (_) {
2722
+ error = "";
2723
+ }
2724
+ logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
2725
+ activityId,
2726
+ inbox: inbox.href,
2727
+ status: response.status,
2728
+ statusText: response.statusText,
2729
+ error
2730
+ });
2731
+ throw new SendActivityError(inbox, response.status, `Failed to send activity ${activityId} to ${inbox.href} (${response.status} ${response.statusText}):\n${error}`, error, response.headers);
2732
+ }
2733
+ deliverySuccess = true;
2734
+ const eventAttributes = {
2735
+ "activitypub.inbox.url": inbox.href,
2736
+ "activitypub.activity.id": activityId ?? ""
2737
+ };
2738
+ if (activityType != null) eventAttributes["activitypub.activity.type"] = activityType;
2739
+ const actorId = getActivityActorId(activity);
2740
+ if (actorId != null) eventAttributes["activitypub.actor.id"] = actorId;
2741
+ span.addEvent("activitypub.activity.sent", eventAttributes);
2742
+ } finally {
2743
+ federationMetrics.recordDelivery(inbox, require_http.getDurationMs(started), deliverySuccess, activityType);
2744
+ }
2745
+ }
2746
+ function createFetchError(url, cause) {
2747
+ const error = new _fedify_vocab_runtime.FetchError(url, cause instanceof Error ? cause.message : String(cause));
2748
+ error.cause = cause;
2749
+ return error;
2750
+ }
2751
+ async function fetchWithValidatedRedirects(request, validateUrl) {
2752
+ let body = await request.clone().text();
2753
+ for (let redirects = 0;; redirects++) {
2754
+ const response = await fetch(request, { redirect: "manual" });
2755
+ const location = response.headers.get("Location");
2756
+ if (![
2757
+ 301,
2758
+ 302,
2759
+ 303,
2760
+ 307,
2761
+ 308
2762
+ ].includes(response.status) || location == null) return response;
2763
+ await response.body?.cancel();
2764
+ if (redirects >= 20) throw new TypeError("Too many redirects");
2765
+ const url = new URL(location, request.url);
2766
+ await validateUrl(url.href);
2767
+ const headers = new Headers(request.headers);
2768
+ if (url.origin !== new URL(request.url).origin) {
2769
+ headers.delete("Authorization");
2770
+ headers.delete("Proxy-Authorization");
2771
+ headers.delete("Cookie");
2772
+ headers.delete("Host");
2773
+ }
2774
+ let method = request.method;
2775
+ if ((response.status === 301 || response.status === 302) && method === "POST" || response.status === 303 && method !== "GET" && method !== "HEAD") {
2776
+ method = "GET";
2777
+ body = void 0;
2778
+ for (const name of [
2779
+ "Content-Encoding",
2780
+ "Content-Language",
2781
+ "Content-Length",
2782
+ "Content-Location",
2783
+ "Content-Type"
2784
+ ]) headers.delete(name);
2785
+ }
2786
+ request = new Request(url, {
2787
+ method,
2788
+ headers,
2789
+ body,
2790
+ redirect: "manual"
2791
+ });
2792
+ }
2793
+ }
2794
+ /**
2795
+ * An error that is thrown when an activity fails to send to a remote inbox.
2796
+ * It contains structured information about the failure, including the HTTP
2797
+ * status code, the inbox URL, and the response body.
2798
+ * @since 2.0.0
2799
+ */
2800
+ var SendActivityError = class extends Error {
2801
+ /**
2802
+ * The inbox URL that the activity was being sent to.
2803
+ */
2804
+ inbox;
2805
+ /**
2806
+ * The HTTP status code returned by the inbox.
2807
+ */
2808
+ statusCode;
2809
+ /**
2810
+ * The response body from the inbox, if any. Note that this may be
2811
+ * truncated to a maximum of 1 KiB to prevent excessive memory consumption
2812
+ * when remote servers return large error pages (e.g., Cloudflare error pages).
2813
+ * If truncated, the string will end with `"… (truncated)"`.
2814
+ */
2815
+ responseBody;
2816
+ /**
2817
+ * The response headers from the inbox.
2818
+ * @since 2.3.0
2819
+ */
2820
+ responseHeaders;
2821
+ /**
2822
+ * Creates a new {@link SendActivityError}.
2823
+ * @param inbox The inbox URL.
2824
+ * @param statusCode The HTTP status code.
2825
+ * @param message The error message.
2826
+ * @param responseBody The response body.
2827
+ * @param responseHeaders The response headers.
2828
+ */
2829
+ constructor(inbox, statusCode, message, responseBody, responseHeaders) {
2830
+ super(message);
2831
+ this.name = "SendActivityError";
2832
+ this.inbox = inbox;
2833
+ this.statusCode = statusCode;
2834
+ this.responseBody = responseBody;
2835
+ this.responseHeaders = new Headers(responseHeaders);
2836
+ }
2837
+ };
2838
+ //#endregion
2839
+ //#region src/federation/portable-inbox.ts
2840
+ const Temporal$8 = require_http.Temporal;
2841
+ const MAX_TIMEOUT_MS = 2 ** 31 - 1;
2842
+ /**
2843
+ * Fills in the defaults of {@link PortableInboxForwardingOptions} and
2844
+ * validates them.
2845
+ * @param options The options.
2846
+ * @returns The resolved options.
2847
+ * @throws {RangeError} If `maxTargets` is not a non-negative integer, if `ttl`
2848
+ * is not positive, if `deadline` is negative or longer
2849
+ * than 2,147,483,647 milliseconds (about 24.8 days), or
2850
+ * if either duration has calendar units, i.e., weeks,
2851
+ * months, or years, whose length depends on the date.
2852
+ */
2853
+ function resolvePortableInboxForwardingOptions(options = {}) {
2854
+ const maxTargets = options.maxTargets ?? 10;
2855
+ if (!Number.isInteger(maxTargets) || maxTargets < 0) throw new RangeError("portableInboxForwarding.maxTargets must be a non-negative integer.");
2856
+ const ttl = Temporal$8.Duration.from(options.ttl ?? { days: 30 });
2857
+ if (ttl.total("millisecond") <= 0) throw new RangeError("portableInboxForwarding.ttl must be positive.");
2858
+ const deadline = Temporal$8.Duration.from(options.deadline ?? { seconds: 10 });
2859
+ const deadlineMs = deadline.total("millisecond");
2860
+ if (deadlineMs < 0 || deadlineMs > MAX_TIMEOUT_MS) throw new RangeError(`portableInboxForwarding.deadline must be between 0 and ${MAX_TIMEOUT_MS} milliseconds.`);
2861
+ return {
2862
+ maxTargets,
2863
+ ttl,
2864
+ deadline
2865
+ };
2866
+ }
2867
+ /**
2868
+ * Decides whether this server accepts a delivery to a portable inbox on behalf
2869
+ * of the given actor, which the actor dispatcher returned for the identifier
2870
+ * in the requested inbox path.
2871
+ *
2872
+ * The actor document comes from the application, so it is trusted as is and
2873
+ * its proof is not verified here.
2874
+ * @param actor The actor the actor dispatcher returned.
2875
+ * @param options The requested DID, the canonical form of the requested inbox
2876
+ * ID, and the origin of this server.
2877
+ * @returns The resolution.
2878
+ */
2879
+ function resolvePortableInboxRecipient(actor, { authority, canonicalInboxId, localOrigin }) {
2880
+ if (actor == null) return {
2881
+ status: "rejected",
2882
+ reason: "notFound"
2883
+ };
2884
+ const id = actor.id;
2885
+ const did = id == null || !require_http.isPortableId(id) ? null : require_http.getPortableDid(id);
2886
+ if (id == null || did == null) return {
2887
+ status: "rejected",
2888
+ reason: "notPortable"
2889
+ };
2890
+ if (did !== authority) return {
2891
+ status: "rejected",
2892
+ reason: "authorityMismatch"
2893
+ };
2894
+ const inboxId = actor.inboxId == null ? null : toPortableUri(actor.inboxId);
2895
+ if (inboxId == null || require_http.getCanonicalPortableId(inboxId) !== canonicalInboxId) return {
2896
+ status: "rejected",
2897
+ reason: "inboxMismatch"
2898
+ };
2899
+ const gateways = actor.gateways;
2900
+ if (!gateways.some((g) => (0, _fedify_vocab_runtime.isGatewayUrl)(g) && g.origin === localOrigin)) return {
2901
+ status: "rejected",
2902
+ reason: "notGateway"
2903
+ };
2904
+ return {
2905
+ status: "accepted",
2906
+ recipient: {
2907
+ actorId: id,
2908
+ inboxId,
2909
+ canonicalInboxId,
2910
+ gateways
2911
+ }
2912
+ };
2913
+ }
2914
+ /**
2915
+ * Gets the portable form of an `ap:` or `ap+ef61:` URI or a compatible
2916
+ * identifier, keeping its query.
2917
+ * @returns The portable URI, or `null` if the ID is neither, or is malformed.
2918
+ */
2919
+ function toPortableUri(id) {
2920
+ if (require_http.isPortableUri(id)) return id;
2921
+ try {
2922
+ return (0, _fedify_vocab_runtime.fromCompatibleEf61Id)(id);
2923
+ } catch (error) {
2924
+ if (error instanceof TypeError) return null;
2925
+ throw error;
2926
+ }
2927
+ }
2928
+ const RFC9421_DELIVERY_COMPONENTS = [
2929
+ "@method",
2930
+ "@target-uri",
2931
+ "content-digest"
2932
+ ];
2933
+ const DRAFT_DELIVERY_COMPONENTS = [
2934
+ "(request-target)",
2935
+ "host",
2936
+ "digest"
2937
+ ];
2938
+ /**
2939
+ * Checks whether an HTTP Signature covers the method, the target URI, and the
2940
+ * body of a request, which both Fedify and Mitra sign when forwarding.
2941
+ * @param signature The signature.
2942
+ * @returns `true` if the signature covers them.
2943
+ */
2944
+ function coversDelivery(signature) {
2945
+ 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));
2946
+ }
2947
+ /**
2948
+ * Forwards an activity received in a portable inbox to the inboxes of the same
2949
+ * portable actor on its other gateways, as FEP-ef61 recommends.
2950
+ *
2951
+ * FEP-ef61 forbids forwarding an activity from an inbox more than once, so
2952
+ * each target gateway is claimed with an atomic compare-and-swap before the
2953
+ * activity is handed off, and a claim is never released, even if the hand-off
2954
+ * fails, since a failed hand-off does not prove that nothing was delivered.
2955
+ * Forwarding is therefore best effort. Without {@link KvStore.cas}, nothing is
2956
+ * forwarded.
2957
+ *
2958
+ * Forwarded requests are signed with HTTP Signatures by the RSA key among
2959
+ * {@link ForwardPortableInboxActivityParameters.getKeys}, if any, so that
2960
+ * servers requiring HTTP Signatures accept them, and are sent unsigned
2961
+ * otherwise. Either way, the receiving gateways authenticate the activity by
2962
+ * its own proof.
2963
+ *
2964
+ * If the delivery is signed with a gateway key of the recipient by one of its
2965
+ * other gateways, that gateway has the activity already, so it is claimed
2966
+ * without forwarding the activity to it. See {@link identifySendingGateway}.
2967
+ * @param parameters The parameters.
2968
+ * @returns The target inbox URLs that the activity was handed off to.
2969
+ */
2970
+ async function forwardPortableInboxActivity(parameters) {
2971
+ const logger = (0, _logtape_logtape.getLogger)([
2972
+ "fedify",
2973
+ "federation",
2974
+ "inbox"
2975
+ ]);
2976
+ const { recipient, activityId, excludedOrigins, kv, kvPrefix, outboxQueue } = parameters;
2977
+ const options = parameters.options ?? resolvePortableInboxForwardingOptions();
2978
+ const { maxTargets, ttl } = options;
2979
+ if (kv.cas == null || maxTargets < 1) return [];
2980
+ const deadline = Date.now() + options.deadline.total("millisecond");
2981
+ const canonicalActivityId = require_http.getCanonicalPortableId(activityId) ?? activityId.href;
2982
+ const claimKey = (gateway) => [
2983
+ ...kvPrefix,
2984
+ recipient.canonicalInboxId,
2985
+ canonicalActivityId,
2986
+ gateway
2987
+ ];
2988
+ let targets = getForwardingTargets(recipient, excludedOrigins);
2989
+ const sender = await identifySendingGateway(parameters, targets.map((t) => t.gateway), claimKey, deadline);
2990
+ if (sender != null) {
2991
+ logger.debug("Not forwarding activity {activityId} from the portable inbox {inbox} back to the gateway {gateway}, which delivered it.", {
2992
+ activityId: activityId.href,
2993
+ inbox: recipient.canonicalInboxId,
2994
+ gateway: sender
2995
+ });
2996
+ await kv.cas(claimKey(sender), void 0, true, { ttl });
2997
+ targets = targets.filter((t) => t.gateway !== sender);
2998
+ }
2999
+ if (targets.length > maxTargets) {
3000
+ 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.", {
3001
+ inbox: recipient.canonicalInboxId,
3002
+ activityId: activityId.href,
3003
+ max: maxTargets
3004
+ });
3005
+ targets.length = maxTargets;
3006
+ }
3007
+ const claimed = [];
3008
+ for (const { gateway, inbox } of targets) if (await kv.cas(claimKey(gateway), void 0, true, { ttl })) claimed.push(inbox);
3009
+ if (claimed.length < 1) return [];
3010
+ logger.debug("Forwarding activity {activityId} from the portable inbox {inbox} to other gateways:\n{targets}", {
3011
+ activityId: activityId.href,
3012
+ inbox: recipient.canonicalInboxId,
3013
+ targets: claimed.map((t) => t.href)
3014
+ });
3015
+ if (outboxQueue == null) await forwardImmediately(parameters, claimed, deadline);
3016
+ else await enqueueForwarding(parameters, outboxQueue, claimed);
3017
+ return claimed;
3018
+ }
3019
+ /**
3020
+ * Identifies the gateway of the recipient that forwarded a delivery to its
3021
+ * portable inbox, by the delivery's HTTP Signature made with the recipient's
3022
+ * gateway key, which FEP-ef61 has each gateway use for requests on behalf of
3023
+ * an actor. Since the activity is authenticated by its own proof, the
3024
+ * signature only tells which gateway to skip, and a delivery whose signature
3025
+ * is invalid or cannot be verified is still forwarded to every gateway.
3026
+ *
3027
+ * Verifying the signature fetches the recipient's actor document from the
3028
+ * sending gateway, as gateway keys are not cached, so only the first signature
3029
+ * that may save a request is verified: one that covers the delivery and names
3030
+ * a gateway key of the recipient on one of the forwarding targets that the
3031
+ * activity has not been forwarded to yet.
3032
+ * @param parameters The forwarding parameters.
3033
+ * @param targets The origins of the gateways to forward to.
3034
+ * @param claimKey Gets the key of the claim for a gateway.
3035
+ * @param deadline The time by which identification has to finish, in
3036
+ * milliseconds since the epoch; afterwards, the sending
3037
+ * gateway is left unidentified.
3038
+ * @returns The origin of the sending gateway, or `null` if it is not
3039
+ * identified.
3040
+ */
3041
+ async function identifySendingGateway({ recipient, activityId, kv, signatures }, targets, claimKey, deadline) {
3042
+ if (signatures == null || signatures.length < 1) return null;
3043
+ const logger = (0, _logtape_logtape.getLogger)([
3044
+ "fedify",
3045
+ "federation",
3046
+ "inbox"
3047
+ ]);
3048
+ const actorId = require_http.getCanonicalPortableId(recipient.actorId);
3049
+ if (actorId == null) return null;
3050
+ const isGatewayKeyOfRecipient = (keyId) => {
3051
+ if (!require_http.isCompatibleKeyId(keyId) || !targets.includes(keyId.origin)) return false;
3052
+ const base = new URL(keyId.href);
3053
+ base.hash = "";
3054
+ return require_http.getCanonicalPortableId(base) === actorId;
3055
+ };
3056
+ let signature;
3057
+ for (const candidate of signatures) if (candidate.coversDelivery && isGatewayKeyOfRecipient(candidate.keyId) && await kv.get(claimKey(candidate.keyId.origin)) === void 0) {
3058
+ signature = candidate;
3059
+ break;
3060
+ }
3061
+ if (signature == null) return null;
3062
+ const { keyId } = signature;
3063
+ const logProperties = {
3064
+ activityId: activityId.href,
3065
+ inbox: recipient.canonicalInboxId,
3066
+ keyId: keyId.href
3067
+ };
3068
+ const verification = Promise.resolve().then(() => signature.verify()).then((key) => {
3069
+ 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);
3070
+ return key;
3071
+ }, (error) => {
3072
+ 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}", {
3073
+ ...logProperties,
3074
+ error
3075
+ });
3076
+ return null;
3077
+ });
3078
+ let timer;
3079
+ const key = await Promise.race([verification, new Promise((resolve) => {
3080
+ timer = setTimeout(() => {
3081
+ 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);
3082
+ resolve(null);
3083
+ }, Math.max(0, deadline - Date.now()));
3084
+ })]);
3085
+ clearTimeout(timer);
3086
+ if (key == null) return null;
3087
+ if (key.id == null || key.id.href !== keyId.href || !isGatewayKeyOfRecipient(key.id) || key.ownerId == null || !require_http.isPortableId(key.ownerId) || require_http.getCanonicalPortableId(key.ownerId) !== actorId) {
3088
+ 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);
3089
+ return null;
3090
+ }
3091
+ return key.id.origin;
3092
+ }
3093
+ function getForwardingTargets(recipient, excludedOrigins) {
3094
+ const logger = (0, _logtape_logtape.getLogger)([
3095
+ "fedify",
3096
+ "federation",
3097
+ "inbox"
3098
+ ]);
3099
+ const seen = new Set(excludedOrigins);
3100
+ const targets = [];
3101
+ for (const gateway of recipient.gateways) {
3102
+ if (!(0, _fedify_vocab_runtime.isGatewayUrl)(gateway) || seen.has(gateway.origin)) continue;
3103
+ seen.add(gateway.origin);
3104
+ let inbox;
3105
+ try {
3106
+ inbox = (0, _fedify_vocab_runtime.toCompatibleEf61Id)(recipient.inboxId, gateway.origin);
3107
+ } catch (error) {
3108
+ if (!(error instanceof TypeError)) throw error;
3109
+ logger.warn("Cannot forward to the gateway {gateway}, as the portable inbox {inbox} cannot be represented as its compatible identifier.", {
3110
+ gateway: gateway.origin,
3111
+ inbox: recipient.canonicalInboxId
3112
+ });
3113
+ continue;
3114
+ }
3115
+ targets.push({
3116
+ gateway: gateway.origin,
3117
+ inbox
3118
+ });
3119
+ }
3120
+ return targets;
3121
+ }
3122
+ /**
3123
+ * Resolves the key that signs forwarded requests: the first RSASSA-PKCS1-v1_5
3124
+ * key, as {@link sendActivity} signs requests only with such a key. Since
3125
+ * the targets have already been claimed, the activity is forwarded unsigned
3126
+ * rather than not at all if the key cannot be resolved or used.
3127
+ */
3128
+ async function resolveForwardingKey({ getKeys, activityId }) {
3129
+ if (getKeys == null) return null;
3130
+ const logger = (0, _logtape_logtape.getLogger)([
3131
+ "fedify",
3132
+ "federation",
3133
+ "inbox"
3134
+ ]);
3135
+ let key;
3136
+ try {
3137
+ key = (await getKeys()).find((k) => k.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5");
3138
+ } catch (error) {
3139
+ logger.error("Failed to get the gateway keys to sign forwarded requests with; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
3140
+ activityId: activityId.href,
3141
+ error
3142
+ });
3143
+ return null;
3144
+ }
3145
+ if (key == null) return null;
3146
+ try {
3147
+ require_http.validateCryptoKey(key.privateKey, "private");
3148
+ } catch (error) {
3149
+ if (!(error instanceof TypeError)) throw error;
3150
+ logger.warn("The gateway key {keyId} cannot sign forwarded requests; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
3151
+ keyId: key.keyId.href,
3152
+ activityId: activityId.href,
3153
+ error
3154
+ });
3155
+ return null;
3156
+ }
3157
+ return key;
3158
+ }
3159
+ async function forwardImmediately(parameters, inboxes, deadlineAt) {
3160
+ const { activity, activityId, activityType, allowPrivateAddress, specDeterminer, meterProvider, tracerProvider } = parameters;
3161
+ const logger = (0, _logtape_logtape.getLogger)([
3162
+ "fedify",
3163
+ "federation",
3164
+ "inbox"
3165
+ ]);
3166
+ const deadline = Math.max(0, deadlineAt - Date.now());
3167
+ const forwarding = resolveForwardingKey(parameters).then((key) => Promise.all(inboxes.map((inbox) => sendActivity({
3168
+ activity,
3169
+ activityId: activityId.href,
3170
+ activityType,
3171
+ keys: key == null ? [] : [key],
3172
+ inbox,
3173
+ allowPrivateAddress,
3174
+ specDeterminer,
3175
+ meterProvider,
3176
+ tracerProvider
3177
+ }).catch((error) => {
3178
+ logger.error("Failed to forward activity {activityId} to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
3179
+ activityId: activityId.href,
3180
+ inbox: inbox.href,
3181
+ error
3182
+ });
3183
+ }))));
3184
+ let timer;
3185
+ const timedOut = await Promise.race([forwarding.then(() => false), new Promise((resolve) => {
3186
+ timer = setTimeout(() => resolve(true), deadline);
3187
+ })]);
3188
+ clearTimeout(timer);
3189
+ if (timedOut) logger.warn("Forwarding activity {activityId} to other gateways did not finish in {deadline} ms; responding to the delivery without waiting for it.", {
3190
+ activityId: activityId.href,
3191
+ deadline
3192
+ });
3193
+ }
3194
+ async function enqueueForwarding(parameters, outboxQueue, inboxes) {
3195
+ const { activity, activityId, activityType, baseUrl, startQueue, meterProvider } = parameters;
3196
+ const logger = (0, _logtape_logtape.getLogger)([
3197
+ "fedify",
3198
+ "federation",
3199
+ "inbox"
3200
+ ]);
3201
+ const key = await resolveForwardingKey(parameters);
3202
+ const keys = [];
3203
+ if (key != null) try {
3204
+ keys.push({
3205
+ keyId: key.keyId.href,
3206
+ privateKey: await require_http.exportJwk(key.privateKey)
3207
+ });
3208
+ } catch (error) {
3209
+ logger.error("Failed to export the gateway key {keyId}; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
3210
+ keyId: key.keyId.href,
3211
+ activityId: activityId.href,
3212
+ error
3213
+ });
3214
+ }
3215
+ startQueue?.();
3216
+ const started = (/* @__PURE__ */ new Date()).toISOString();
3217
+ const traceContext = {};
3218
+ _opentelemetry_api.propagation.inject(_opentelemetry_api.context.active(), traceContext);
3219
+ await Promise.all(inboxes.map(async (inbox) => {
3220
+ const message = {
3221
+ type: "outbox",
3222
+ id: crypto.randomUUID(),
3223
+ baseUrl,
3224
+ keys,
3225
+ activity,
3226
+ activityId: activityId.href,
3227
+ activityType,
3228
+ inbox: inbox.href,
3229
+ sharedInbox: false,
3230
+ started,
3231
+ attempt: 0,
3232
+ headers: {},
3233
+ traceContext
3234
+ };
3235
+ try {
3236
+ await outboxQueue.enqueue(message);
3237
+ } catch (error) {
3238
+ logger.error("Failed to enqueue activity {activityId} to forward to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
3239
+ activityId: activityId.href,
3240
+ inbox: inbox.href,
3241
+ error
3242
+ });
3243
+ return;
3244
+ }
3245
+ require_http.recordOutboxEnqueue(meterProvider, outboxQueue, message);
3246
+ }));
3247
+ }
3248
+ //#endregion
3249
+ //#region src/federation/temporal.ts
3250
+ const Temporal$7 = require_http.Temporal;
3251
+ function isPlainObject$1(value) {
3252
+ return typeof value === "object" && value != null && !Array.isArray(value);
3253
+ }
3254
+ function normalizeDateTimeLiteral(value) {
3255
+ return value.substring(19).match(/[Z+-]/) ? value : value + "Z";
3256
+ }
3257
+ function isMalformedDateTimeLiteral(value) {
3258
+ if (typeof value !== "string") return false;
3259
+ try {
3260
+ Temporal$7.Instant.from(normalizeDateTimeLiteral(value));
3261
+ return false;
3262
+ } catch {
3263
+ return true;
3264
+ }
3265
+ }
3266
+ function isMalformedDurationLiteral(value) {
3267
+ if (typeof value !== "string") return false;
3268
+ try {
3269
+ Temporal$7.Duration.from(value);
3270
+ return false;
3271
+ } catch {
3272
+ return true;
3273
+ }
3274
+ }
3275
+ const TEMPORAL_DATE_TIME_IRIS = /* @__PURE__ */ new Set([
3276
+ "https://www.w3.org/ns/activitystreams#deleted",
3277
+ "https://www.w3.org/ns/activitystreams#endTime",
3278
+ "https://www.w3.org/ns/activitystreams#published",
3279
+ "https://www.w3.org/ns/activitystreams#startTime",
3280
+ "https://www.w3.org/ns/activitystreams#updated",
3281
+ "http://purl.org/dc/terms/created",
3282
+ "https://w3id.org/security#created"
3283
+ ]);
3284
+ const TEMPORAL_DURATION_IRIS = /* @__PURE__ */ new Set(["https://www.w3.org/ns/activitystreams#duration"]);
3285
+ const QUESTION_CLOSED_IRI = "https://www.w3.org/ns/activitystreams#closed";
3286
+ const XSD_DATE_TIME_IRI = "http://www.w3.org/2001/XMLSchema#dateTime";
3287
+ function hasMalformedExpandedDateTimeLiteral(value) {
3288
+ if (Array.isArray(value)) return value.some(hasMalformedExpandedDateTimeLiteral);
3289
+ return isPlainObject$1(value) && "@value" in value && isMalformedDateTimeLiteral(value["@value"]);
3290
+ }
3291
+ function hasMalformedExpandedQuestionClosedLiteral(value) {
3292
+ if (Array.isArray(value)) return value.some(hasMalformedExpandedQuestionClosedLiteral);
3293
+ if (!isPlainObject$1(value) || !("@value" in value)) return false;
3294
+ const literal = value["@value"];
3295
+ if (typeof literal === "boolean") return false;
3296
+ if (typeof literal !== "string") return false;
3297
+ if (value["@type"] !== XSD_DATE_TIME_IRI) return false;
3298
+ if (new Date(literal).toString() === "Invalid Date") return false;
3299
+ return isMalformedDateTimeLiteral(literal);
3300
+ }
3301
+ function hasMalformedExpandedDurationLiteral(value) {
3302
+ if (Array.isArray(value)) return value.some(hasMalformedExpandedDurationLiteral);
3303
+ return isPlainObject$1(value) && "@value" in value && isMalformedDurationLiteral(value["@value"]);
3304
+ }
3305
+ function hasMalformedKnownTemporalLiteralInternal(value, visited) {
3306
+ if (Array.isArray(value)) return value.some((item) => hasMalformedKnownTemporalLiteralInternal(item, visited));
3307
+ if (!isPlainObject$1(value)) return false;
3308
+ if (visited.has(value)) return false;
3309
+ visited.add(value);
3310
+ if ("@value" in value) return false;
3311
+ for (const [key, child] of Object.entries(value)) {
3312
+ if (TEMPORAL_DATE_TIME_IRIS.has(key)) {
3313
+ if (hasMalformedExpandedDateTimeLiteral(child)) return true;
3314
+ continue;
3315
+ }
3316
+ if (key === QUESTION_CLOSED_IRI) {
3317
+ if (hasMalformedExpandedQuestionClosedLiteral(child)) return true;
3318
+ continue;
3319
+ }
3320
+ if (TEMPORAL_DURATION_IRIS.has(key)) {
3321
+ if (hasMalformedExpandedDurationLiteral(child)) return true;
3322
+ continue;
3323
+ }
3324
+ if (hasMalformedKnownTemporalLiteralInternal(child, visited)) return true;
3325
+ }
3326
+ return false;
3327
+ }
3328
+ async function hasMalformedKnownTemporalLiteral(value, contextLoader) {
3329
+ try {
3330
+ return hasMalformedKnownTemporalLiteralInternal(await _fedify_vocab_runtime_jsonld.default.expand(value, {
3331
+ documentLoader: require_http.getNormalizationContextLoader(contextLoader),
3332
+ keepFreeFloatingNodes: true
3333
+ }), /* @__PURE__ */ new Set());
3334
+ } catch {
3335
+ return false;
3336
+ }
3337
+ }
3338
+ //#endregion
3339
+ //#region src/federation/handler.ts
3340
+ const Temporal$6 = require_http.Temporal;
3341
+ const rawInboxContextFactorySymbol = Symbol("fedify.rawInboxContextFactory");
3342
+ const INBOX_COMPOUND_PROOF_LIMITS = {
3343
+ maxDepth: 64,
3344
+ maxMaps: 1e4,
3345
+ maxProofs: 32,
3346
+ maxBytes: 10 * 1024 * 1024
3347
+ };
3348
+ function isRemoteContextLoadingFailure$1(error) {
3349
+ return error instanceof Error && typeof error.details === "object" && error.details != null && error.details.code === "loading remote context failed";
3350
+ }
3351
+ function isPermanentRemoteContextError$1(error) {
3352
+ if (!(error instanceof Error) || error.name !== "jsonld.InvalidUrl") return false;
3353
+ const details = error.details;
3354
+ if (details?.code === "invalid remote context") return true;
3355
+ return isRemoteContextLoadingFailure$1(error) && typeof details?.url === "string" && !URL.canParse(details.url) && require_http.isClearlyMalformedContextReference(details.url);
3356
+ }
3357
+ function isInvalidJsonLdError(error) {
3358
+ if (!(error instanceof Error)) return false;
3359
+ const name = error.name;
3360
+ return name === "UnsafeJsonLdError" || error instanceof require_http.InvalidContextReferenceError || isPermanentRemoteContextError$1(error) || name === "jsonld.SyntaxError" && !isRemoteContextLoadingFailure$1(error);
3361
+ }
3362
+ function isValidationTypeError(error) {
3363
+ return error instanceof TypeError && (/^(Invalid JSON-LD:|Invalid type:|Unexpected type:|Invalid @id:)/.test(error.message) || require_http.isInvalidUrlTypeError(error));
3364
+ }
3365
+ function isPermanentActivityParseError(error) {
3366
+ return isInvalidJsonLdError(error) || isValidationTypeError(error);
3367
+ }
3368
+ function hasHttpSignatureHeaders(request) {
3369
+ return request.headers.has("Signature") || request.headers.has("Signature-Input");
3370
+ }
3371
+ function hasObjectIntegrityProof(json) {
3372
+ return typeof json === "object" && json != null && "proof" in json;
3373
+ }
3374
+ /**
3375
+ * Handles an actor request.
3376
+ * @template TContextData The context data to pass to the context.
3377
+ * @param request The HTTP request.
3378
+ * @param parameters The parameters for handling the actor.
3379
+ * @returns A promise that resolves to an HTTP response.
3380
+ */
3381
+ async function handleActor(request, { identifier, context, actorDispatcher, authorizePredicate, onNotFound, onUnauthorized }) {
3382
+ const logger = (0, _logtape_logtape.getLogger)([
3383
+ "fedify",
3384
+ "federation",
3385
+ "actor"
3386
+ ]);
3387
+ if (actorDispatcher == null) {
3388
+ logger.debug("Actor dispatcher is not set.", { identifier });
3389
+ return await onNotFound(request);
3390
+ }
3391
+ const actor = await actorDispatcher(context, identifier);
3392
+ if (actor == null) {
3393
+ logger.debug("Actor {identifier} not found.", { identifier });
3394
+ return await onNotFound(request);
3395
+ }
3396
+ if (authorizePredicate != null) {
3397
+ if (!await authorizePredicate(context, identifier)) return await onUnauthorized(request);
3398
+ }
3399
+ if (actor instanceof _fedify_vocab.Tombstone) {
3400
+ const jsonLd = await actor.toJsonLd(context);
2676
3401
  return new Response(JSON.stringify(jsonLd), {
2677
3402
  status: 410,
2678
3403
  headers: {
@@ -2709,8 +3434,9 @@ async function handleObject(request, { values, context, objectDispatcher, author
2709
3434
  }
2710
3435
  /**
2711
3436
  * Handles an FEP-ef61 gateway request for a portable object through an object
2712
- * dispatcher. The object is served only if its ID canonically matches the
2713
- * requested portable ID and it satisfies the FEP-ef61 proof policy.
3437
+ * dispatcher, or the actor dispatcher adapted to one. The object is served
3438
+ * only if its ID canonically matches the requested portable ID and it
3439
+ * satisfies the FEP-ef61 proof policy.
2714
3440
  * @template TContextData The context data to pass to the context.
2715
3441
  * @param request The HTTP request.
2716
3442
  * @param parameters The parameters for handling the portable object.
@@ -3950,7 +4676,21 @@ async function handleInboxInternal(request, parameters, span) {
3950
4676
  inbox: portableInbox.recipient.canonicalInboxId
3951
4677
  });
3952
4678
  else try {
3953
- await portableInbox.forward(json, activity.id, (0, _fedify_vocab.getTypeId)(activity).href);
4679
+ await portableInbox.forward(json, activity.id, (0, _fedify_vocab.getTypeId)(activity).href, require_http.listRequestSignatures(request).map((signature) => ({
4680
+ keyId: signature.keyId,
4681
+ coversDelivery: coversDelivery(signature),
4682
+ verify: async () => {
4683
+ const verification = await require_http.verifyRequestDetailed(require_http.selectRequestSignature(request, signature), {
4684
+ contextLoader: ctx.contextLoader,
4685
+ documentLoader: ctx.documentLoader,
4686
+ timeWindow: signatureTimeWindow,
4687
+ keyCache,
4688
+ meterProvider,
4689
+ tracerProvider
4690
+ });
4691
+ return verification.verified ? verification.key : null;
4692
+ }
4693
+ })));
3954
4694
  } catch (error) {
3955
4695
  logger.error("Failed to forward activity {activityId} to the other gateways of the portable inbox {inbox}:\n{error}", {
3956
4696
  activityId: activity.id.href,
@@ -4483,575 +5223,285 @@ var HandlerError = class extends Error {
4483
5223
  }
4484
5224
  };
4485
5225
  /**
4486
- * Error thrown when items are not found in a collection.
4487
- * @since 1.8.0
4488
- */
4489
- var ItemsNotFoundError = class extends HandlerError {
4490
- constructor() {
4491
- super("Items not found in the collection.");
4492
- }
4493
- };
4494
- /**
4495
- * Error thrown when access to a collection is unauthorized.
4496
- * @since 1.8.0
4497
- */
4498
- var UnauthorizedError = class extends HandlerError {
4499
- constructor() {
4500
- super("Unauthorized access to the collection.");
4501
- }
4502
- };
4503
- /**
4504
- * Responds with the given object in JSON-LD format.
4505
- *
4506
- * @param object The object to respond with.
4507
- * @param options Options.
4508
- * @since 0.3.0
4509
- */
4510
- async function respondWithObject(object, options) {
4511
- const jsonLd = await object.toJsonLd(options);
4512
- return new Response(JSON.stringify(jsonLd), { headers: { "Content-Type": "application/activity+json" } });
4513
- }
4514
- /**
4515
- * Responds with the given object in JSON-LD format if the request accepts
4516
- * JSON-LD.
4517
- *
4518
- * @param object The object to respond with.
4519
- * @param request The request to check for JSON-LD acceptability.
4520
- * @param options Options.
4521
- * @since 0.3.0
4522
- */
4523
- async function respondWithObjectIfAcceptable(object, request, options) {
4524
- if (!acceptsJsonLd(request)) return null;
4525
- const response = await respondWithObject(object, options);
4526
- response.headers.set("Vary", "Accept");
4527
- return response;
4528
- }
4529
- function generateNonce() {
4530
- const bytes = /* @__PURE__ */ new Uint8Array(16);
4531
- crypto.getRandomValues(bytes);
4532
- return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
4533
- }
4534
- async function verifySignatureNonce(request, kv, noncePrefix, verifiedLabel) {
4535
- const signatureInput = request.headers.get("Signature-Input");
4536
- if (signatureInput == null) return false;
4537
- const parsed = require_http.parseRfc9421SignatureInput(signatureInput);
4538
- if (verifiedLabel == null) return false;
4539
- const sig = parsed[verifiedLabel];
4540
- if (sig == null) return false;
4541
- const nonce = sig.nonce;
4542
- if (nonce == null) return false;
4543
- const key = [...noncePrefix, nonce];
4544
- if (kv.cas != null) return await kv.cas(key, true, void 0);
4545
- if (await kv.get(key) != null) {
4546
- await kv.delete(key);
4547
- return true;
4548
- }
4549
- return false;
4550
- }
4551
- const getFailedSignatureResponse = async (policy, kv, kvPrefixes) => {
4552
- const headers = await getFailedSignatureHeaders(policy, kv, kvPrefixes);
4553
- return new Response("Failed to verify the request signature.", {
4554
- status: 401,
4555
- headers
4556
- });
4557
- };
4558
- const getFailedSignatureHeaders = async (policy, kv, kvPrefixes) => ({
4559
- "Content-Type": "text/plain; charset=utf-8",
4560
- ...policy?.enabled && {
4561
- "Accept-Signature": await buildAcceptSignatureHeader(policy, kv, kvPrefixes.acceptSignatureNonce),
4562
- "Cache-Control": "no-store",
4563
- "Vary": "Accept, Signature"
4564
- }
4565
- });
4566
- async function buildAcceptSignatureHeader(policy, kv, noncePrefix) {
4567
- const parameters = { created: true };
4568
- if (policy.requestNonce) {
4569
- const nonce = generateNonce();
4570
- await setKey(kv, [...noncePrefix, nonce], policy);
4571
- parameters.nonce = nonce;
4572
- }
4573
- const baseComponents = policy.components ?? DEF_COMPONENTS;
4574
- return require_http.formatAcceptSignature([{
4575
- label: "sig1",
4576
- components: (0, es_toolkit.uniq)(MIN_COMPONENTS.concat(baseComponents)).filter((c) => c !== "@status").map((v) => ({
4577
- value: v,
4578
- params: {}
4579
- })),
4580
- parameters
4581
- }]);
4582
- }
4583
- async function setKey(kv, key, policy) {
4584
- const seconds = policy.nonceTtlSeconds ?? 300;
4585
- const ttl = Temporal$7.Duration.from({ seconds });
4586
- await kv.set(key, true, { ttl });
4587
- }
4588
- const DEF_COMPONENTS = [
4589
- "@method",
4590
- "@target-uri",
4591
- "@authority",
4592
- "content-digest"
4593
- ];
4594
- const MIN_COMPONENTS = [
4595
- "@method",
4596
- "@target-uri",
4597
- "@authority"
4598
- ];
4599
- /**
4600
- * Checks whether an HTTP Signature key is a gateway key of an FEP-ef61
4601
- * portable actor, i.e., its ID is a compatible identifier and it is owned by
4602
- * a portable actor, which only the portable actor's signed document can
4603
- * establish.
4604
- */
4605
- function isPortableGatewayKey(key) {
4606
- return key?.id != null && require_http.isCompatibleKeyId(key.id) && key.ownerId != null && require_http.isPortableId(key.ownerId);
4607
- }
4608
- /**
4609
- * Checks whether an activity needs an Object Integrity Proof by its DID:
4610
- * either it is performed by an [FEP-ef61] portable actor, or its own ID is
4611
- * portable. Both `ap:` URIs and compatible identifiers count as portable.
4612
- *
4613
- * [FEP-ef61]: https://w3id.org/fep/ef61
4614
- */
4615
- function isPortableActivity(activity) {
4616
- return require_http.hasPortableActor(activity) || activity.id != null && require_http.isPortableId(activity.id);
4617
- }
4618
- /**
4619
- * Verifies that a portable activity's own ID belongs to the DID that signed
4620
- * it, on the JSON-LD document received. The ID the proof policy checks has
4621
- * to be the parsed activity's ID, so that a remote context that changes
4622
- * between loads cannot make the two name different objects.
4623
- * @returns `null` if the ID is verified, or the reason why it is not.
4624
- */
4625
- async function verifyPortableActivityId(activityId, jsonLd, options) {
4626
- const expectedId = require_http.getCanonicalPortableId(activityId);
4627
- if (expectedId == null) return "malformed portable ID";
4628
- let verification;
4629
- try {
4630
- verification = await require_http.verifyPortableObjectProofWithRoot(jsonLd, options);
4631
- } catch (error) {
4632
- if (error instanceof require_http.InvalidPortableObjectIdError) return "malformed portable ID";
4633
- if (!isPermanentActivityParseError(error)) throw error;
4634
- return "malformed document";
4635
- }
4636
- const { result, root } = verification;
4637
- if (!result.verified) return result.reason.type;
4638
- const rootId = root?.["@id"];
4639
- if (typeof rootId !== "string" || require_http.getCanonicalPortableId((0, _fedify_vocab_runtime.parseIri)(rootId)) !== expectedId) return "the verified document has another ID";
4640
- return null;
4641
- }
4642
- //#endregion
4643
- //#region src/federation/kv.ts
4644
- const Temporal$6 = require_http.Temporal;
4645
- /**
4646
- * A key–value store that stores values in memory.
4647
- * Do not use this in production as it does not persist values.
4648
- *
4649
- * @since 0.5.0
4650
- */
4651
- var MemoryKvStore = class {
4652
- #values = {};
4653
- #encodeKey(key) {
4654
- return JSON.stringify(key);
4655
- }
4656
- /**
4657
- * {@inheritDoc KvStore.get}
4658
- */
4659
- get(key) {
4660
- const encodedKey = this.#encodeKey(key);
4661
- const entry = this.#values[encodedKey];
4662
- if (entry == null) return Promise.resolve(void 0);
4663
- const [value, expiration] = entry;
4664
- if (expiration != null && Temporal$6.Now.instant().until(expiration).sign < 0) {
4665
- delete this.#values[encodedKey];
4666
- return Promise.resolve(void 0);
4667
- }
4668
- return Promise.resolve(value);
4669
- }
4670
- /**
4671
- * {@inheritDoc KvStore.set}
4672
- */
4673
- set(key, value, options) {
4674
- const encodedKey = this.#encodeKey(key);
4675
- const expiration = options?.ttl == null ? null : Temporal$6.Now.instant().add(options.ttl.round({ largestUnit: "hour" }));
4676
- this.#values[encodedKey] = [value, expiration];
4677
- return Promise.resolve();
4678
- }
4679
- /**
4680
- * {@inheritDoc KvStore.delete}
4681
- */
4682
- delete(key) {
4683
- const encodedKey = this.#encodeKey(key);
4684
- delete this.#values[encodedKey];
4685
- return Promise.resolve();
4686
- }
4687
- /**
4688
- * {@inheritDoc KvStore.cas}
4689
- */
4690
- cas(key, expectedValue, newValue, options) {
4691
- const encodedKey = this.#encodeKey(key);
4692
- const entry = this.#values[encodedKey];
4693
- let currentValue;
4694
- if (entry == null) currentValue = void 0;
4695
- else {
4696
- const [value, expiration] = entry;
4697
- if (expiration != null && Temporal$6.Now.instant().until(expiration).sign < 0) {
4698
- delete this.#values[encodedKey];
4699
- currentValue = void 0;
4700
- } else currentValue = value;
4701
- }
4702
- if (!(0, es_toolkit.isEqual)(currentValue, expectedValue)) return Promise.resolve(false);
4703
- const expiration = options?.ttl == null ? null : Temporal$6.Now.instant().add(options.ttl.round({ largestUnit: "hour" }));
4704
- this.#values[encodedKey] = [newValue, expiration];
4705
- return Promise.resolve(true);
4706
- }
4707
- /**
4708
- * {@inheritDoc KvStore.list}
4709
- */
4710
- async *list(prefix) {
4711
- const now = Temporal$6.Now.instant();
4712
- for (const [encodedKey, entry] of Object.entries(this.#values)) {
4713
- const key = JSON.parse(encodedKey);
4714
- if (prefix != null) {
4715
- if (key.length < prefix.length) continue;
4716
- if (!prefix.every((p, i) => key[i] === p)) continue;
4717
- }
4718
- const [value, expiration] = entry;
4719
- if (expiration != null && now.until(expiration).sign < 0) {
4720
- delete this.#values[encodedKey];
4721
- continue;
4722
- }
4723
- yield {
4724
- key,
4725
- value
4726
- };
4727
- }
4728
- }
4729
- };
4730
- //#endregion
4731
- //#region src/nodeinfo/handler.ts
4732
- /**
4733
- * Handles a NodeInfo request. You would not typically call this function
4734
- * directly, but instead use {@link Federation.handle} method.
4735
- * @param request The NodeInfo request to handle.
4736
- * @param parameters The parameters for handling the request.
4737
- * @returns The response to the request.
5226
+ * Error thrown when items are not found in a collection.
5227
+ * @since 1.8.0
4738
5228
  */
4739
- async function handleNodeInfo(_request, { context, nodeInfoDispatcher }) {
4740
- const promise = nodeInfoDispatcher(context);
4741
- const json = require_types.nodeInfoToJson(promise instanceof Promise ? await promise : promise);
4742
- return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json; profile=\"http://nodeinfo.diaspora.software/ns/schema/2.1#\"" } });
4743
- }
5229
+ var ItemsNotFoundError = class extends HandlerError {
5230
+ constructor() {
5231
+ super("Items not found in the collection.");
5232
+ }
5233
+ };
4744
5234
  /**
4745
- * Handles a request to `/.well-known/nodeinfo`. You would not typically call
4746
- * this function directly, but instead use {@link Federation.handle} method.
4747
- * @param request The request to handle.
4748
- * @param context The request context.
4749
- * @returns The response to the request.
5235
+ * Error thrown when access to a collection is unauthorized.
5236
+ * @since 1.8.0
4750
5237
  */
4751
- function handleNodeInfoJrd(_request, context) {
4752
- const links = [];
4753
- try {
4754
- links.push({
4755
- rel: "http://nodeinfo.diaspora.software/ns/schema/2.1",
4756
- href: context.getNodeInfoUri().href,
4757
- type: "application/json; profile=\"http://nodeinfo.diaspora.software/ns/schema/2.1#\""
4758
- });
4759
- } catch (e) {
4760
- if (!(e instanceof _fedify_uri_template.RouterError)) throw e;
5238
+ var UnauthorizedError = class extends HandlerError {
5239
+ constructor() {
5240
+ super("Unauthorized access to the collection.");
4761
5241
  }
4762
- const response = new Response(JSON.stringify({ links }), { headers: { "Content-Type": "application/jrd+json" } });
4763
- return Promise.resolve(response);
4764
- }
4765
- //#endregion
4766
- //#region src/federation/send.ts
5242
+ };
4767
5243
  /**
4768
- * Extracts the inbox URLs from recipients.
4769
- * @param parameters The parameters to extract the inboxes.
4770
- * See also {@link ExtractInboxesParameters}.
4771
- * @returns The inboxes as a map of inbox URL to actor URIs.
5244
+ * Responds with the given object in JSON-LD format.
5245
+ *
5246
+ * @param object The object to respond with.
5247
+ * @param options Options.
5248
+ * @since 0.3.0
4772
5249
  */
4773
- function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris }) {
4774
- const inboxes = {};
4775
- for (const recipient of recipients) {
4776
- let inbox;
4777
- let sharedInbox = false;
4778
- if (preferSharedInbox && recipient.endpoints?.sharedInbox != null) {
4779
- inbox = recipient.endpoints.sharedInbox;
4780
- sharedInbox = true;
4781
- } else inbox = recipient.inboxId;
4782
- if (inbox != null && recipient.id != null) {
4783
- if (excludeBaseUris != null && excludeBaseUris.some((u) => u.origin === inbox?.origin)) continue;
4784
- inboxes[inbox.href] ??= {
4785
- actorIds: /* @__PURE__ */ new Set(),
4786
- sharedInbox
4787
- };
4788
- inboxes[inbox.href].actorIds.add(recipient.id.href);
4789
- }
4790
- }
4791
- return inboxes;
5250
+ async function respondWithObject(object, options) {
5251
+ const jsonLd = await object.toJsonLd(options);
5252
+ return new Response(JSON.stringify(jsonLd), { headers: { "Content-Type": "application/activity+json" } });
4792
5253
  }
4793
5254
  /**
4794
- * Sends an {@link Activity} to an inbox.
5255
+ * Responds with the given object in JSON-LD format if the request accepts
5256
+ * JSON-LD.
4795
5257
  *
4796
- * @param parameters The parameters for sending the activity.
4797
- * See also {@link SendActivityParameters}.
4798
- * @throws {Error} If the activity fails to send.
5258
+ * @param object The object to respond with.
5259
+ * @param request The request to check for JSON-LD acceptability.
5260
+ * @param options Options.
5261
+ * @since 0.3.0
4799
5262
  */
4800
- function sendActivity(options) {
4801
- const tracerProvider = options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider();
4802
- return tracerProvider.getTracer(require_http.name, require_http.version).startActiveSpan("activitypub.send_activity", {
4803
- kind: _opentelemetry_api.SpanKind.CLIENT,
4804
- attributes: { "activitypub.shared_inbox": options.sharedInbox ?? false }
4805
- }, async (span) => {
4806
- if (options.activityId != null) span.setAttribute("activitypub.activity.id", options.activityId);
4807
- if (options.activityType != null) span.setAttribute("activitypub.activity.type", options.activityType);
4808
- try {
4809
- await sendActivityInternal({
4810
- ...options,
4811
- tracerProvider
4812
- }, span);
4813
- } catch (e) {
4814
- span.setStatus({
4815
- code: _opentelemetry_api.SpanStatusCode.ERROR,
4816
- message: String(e)
4817
- });
4818
- throw e;
4819
- } finally {
4820
- span.end();
4821
- }
4822
- });
4823
- }
4824
- const MAX_ERROR_RESPONSE_BODY_BYTES = 1024;
4825
- function getActivityActorId(activity) {
4826
- if (!isRecord(activity)) return void 0;
4827
- return getIdValue(activity.actor);
4828
- }
4829
- function getIdValue(value) {
4830
- if (typeof value === "string" && value !== "") return value;
4831
- if (value instanceof URL) return value.href;
4832
- if (Array.isArray(value)) {
4833
- for (const item of value) {
4834
- const id = getIdValue(item);
4835
- if (id != null) return id;
4836
- }
4837
- return;
4838
- }
4839
- if (isRecord(value)) return getIdValue(value.id);
4840
- }
4841
- function isRecord(value) {
4842
- return typeof value === "object" && value != null;
5263
+ async function respondWithObjectIfAcceptable(object, request, options) {
5264
+ if (!acceptsJsonLd(request)) return null;
5265
+ const response = await respondWithObject(object, options);
5266
+ response.headers.set("Vary", "Accept");
5267
+ return response;
4843
5268
  }
4844
- async function readLimitedResponseBody(response, maxBytes) {
4845
- if (response.body == null) return "";
4846
- const reader = response.body.getReader();
4847
- const decoder = new TextDecoder();
4848
- const chunks = [];
4849
- let totalBytes = 0;
4850
- let truncated = false;
4851
- try {
4852
- while (true) {
4853
- const { done, value } = await reader.read();
4854
- if (done) break;
4855
- if (totalBytes + value.length > maxBytes) {
4856
- const remaining = maxBytes - totalBytes;
4857
- if (remaining > 0) chunks.push(decoder.decode(value.slice(0, remaining), { stream: true }));
4858
- truncated = true;
4859
- break;
4860
- }
4861
- chunks.push(decoder.decode(value, { stream: true }));
4862
- totalBytes += value.length;
4863
- }
4864
- } finally {
4865
- reader.releaseLock();
4866
- }
4867
- let result = chunks.join("");
4868
- if (truncated) result += "… (truncated)";
4869
- return result;
5269
+ function generateNonce() {
5270
+ const bytes = /* @__PURE__ */ new Uint8Array(16);
5271
+ crypto.getRandomValues(bytes);
5272
+ return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
4870
5273
  }
4871
- async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, allowPrivateAddress, headers, specDeterminer, meterProvider, tracerProvider }, span) {
4872
- const logger = (0, _logtape_logtape.getLogger)([
4873
- "fedify",
4874
- "federation",
4875
- "outbox"
4876
- ]);
4877
- const federationMetrics = require_http.getFederationMetrics(meterProvider);
4878
- const started = performance.now();
4879
- let deliverySuccess = false;
4880
- async function validateUrl(url) {
4881
- if (!allowPrivateAddress) try {
4882
- await (0, _fedify_vocab_runtime.validatePublicUrl)(url);
4883
- } catch (error) {
4884
- if (error instanceof _fedify_vocab_runtime.UrlError) logger.error("Disallowed private URL: {url}", {
4885
- url,
4886
- error
4887
- });
4888
- throw error;
4889
- }
4890
- }
4891
- await validateUrl(inbox.href);
4892
- headers = new Headers(headers);
4893
- headers.set("Content-Type", "application/activity+json");
4894
- const request = new Request(inbox, {
4895
- method: "POST",
4896
- headers,
4897
- body: JSON.stringify(activity),
4898
- redirect: "manual"
4899
- });
4900
- let rsaKey = null;
4901
- for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
4902
- rsaKey = key;
4903
- break;
4904
- }
4905
- if (rsaKey == null && keys.length < 1) logger.debug("Sending the activity {activityId} to {inbox} without a signature.", {
4906
- activityId,
4907
- inbox: inbox.href
4908
- });
4909
- 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.", {
4910
- inbox: inbox.href,
4911
- keys: keys.map((pair) => ({
4912
- keyId: pair.keyId.href,
4913
- privateKey: pair.privateKey
4914
- }))
4915
- });
4916
- let response;
4917
- try {
4918
- response = rsaKey == null ? await fetchWithValidatedRedirects(request, validateUrl) : await require_http.doubleKnock(request, rsaKey, {
4919
- tracerProvider,
4920
- specDeterminer,
4921
- validateRedirect: validateUrl
4922
- });
4923
- } catch (error) {
4924
- const failure = error instanceof _fedify_vocab_runtime.FetchError || error instanceof _fedify_vocab_runtime.UrlError ? error : createFetchError(inbox.href, error);
4925
- logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
4926
- activityId,
4927
- inbox: inbox.href,
4928
- error: failure
4929
- });
4930
- federationMetrics.recordDelivery(inbox, require_http.getDurationMs(started), false, activityType);
4931
- throw failure;
4932
- }
4933
- try {
4934
- if (!response.ok) {
4935
- let error;
4936
- try {
4937
- error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
4938
- } catch (_) {
4939
- error = "";
4940
- }
4941
- logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
4942
- activityId,
4943
- inbox: inbox.href,
4944
- status: response.status,
4945
- statusText: response.statusText,
4946
- error
4947
- });
4948
- throw new SendActivityError(inbox, response.status, `Failed to send activity ${activityId} to ${inbox.href} (${response.status} ${response.statusText}):\n${error}`, error, response.headers);
4949
- }
4950
- deliverySuccess = true;
4951
- const eventAttributes = {
4952
- "activitypub.inbox.url": inbox.href,
4953
- "activitypub.activity.id": activityId ?? ""
4954
- };
4955
- if (activityType != null) eventAttributes["activitypub.activity.type"] = activityType;
4956
- const actorId = getActivityActorId(activity);
4957
- if (actorId != null) eventAttributes["activitypub.actor.id"] = actorId;
4958
- span.addEvent("activitypub.activity.sent", eventAttributes);
4959
- } finally {
4960
- federationMetrics.recordDelivery(inbox, require_http.getDurationMs(started), deliverySuccess, activityType);
5274
+ async function verifySignatureNonce(request, kv, noncePrefix, verifiedLabel) {
5275
+ const signatureInput = request.headers.get("Signature-Input");
5276
+ if (signatureInput == null) return false;
5277
+ const parsed = require_http.parseRfc9421SignatureInput(signatureInput);
5278
+ if (verifiedLabel == null) return false;
5279
+ const sig = parsed[verifiedLabel];
5280
+ if (sig == null) return false;
5281
+ const nonce = sig.nonce;
5282
+ if (nonce == null) return false;
5283
+ const key = [...noncePrefix, nonce];
5284
+ if (kv.cas != null) return await kv.cas(key, true, void 0);
5285
+ if (await kv.get(key) != null) {
5286
+ await kv.delete(key);
5287
+ return true;
4961
5288
  }
5289
+ return false;
4962
5290
  }
4963
- function createFetchError(url, cause) {
4964
- const error = new _fedify_vocab_runtime.FetchError(url, cause instanceof Error ? cause.message : String(cause));
4965
- error.cause = cause;
4966
- return error;
5291
+ const getFailedSignatureResponse = async (policy, kv, kvPrefixes) => {
5292
+ const headers = await getFailedSignatureHeaders(policy, kv, kvPrefixes);
5293
+ return new Response("Failed to verify the request signature.", {
5294
+ status: 401,
5295
+ headers
5296
+ });
5297
+ };
5298
+ const getFailedSignatureHeaders = async (policy, kv, kvPrefixes) => ({
5299
+ "Content-Type": "text/plain; charset=utf-8",
5300
+ ...policy?.enabled && {
5301
+ "Accept-Signature": await buildAcceptSignatureHeader(policy, kv, kvPrefixes.acceptSignatureNonce),
5302
+ "Cache-Control": "no-store",
5303
+ "Vary": "Accept, Signature"
5304
+ }
5305
+ });
5306
+ async function buildAcceptSignatureHeader(policy, kv, noncePrefix) {
5307
+ const parameters = { created: true };
5308
+ if (policy.requestNonce) {
5309
+ const nonce = generateNonce();
5310
+ await setKey(kv, [...noncePrefix, nonce], policy);
5311
+ parameters.nonce = nonce;
5312
+ }
5313
+ const baseComponents = policy.components ?? DEF_COMPONENTS;
5314
+ return require_http.formatAcceptSignature([{
5315
+ label: "sig1",
5316
+ components: (0, es_toolkit.uniq)(MIN_COMPONENTS.concat(baseComponents)).filter((c) => c !== "@status").map((v) => ({
5317
+ value: v,
5318
+ params: {}
5319
+ })),
5320
+ parameters
5321
+ }]);
4967
5322
  }
4968
- async function fetchWithValidatedRedirects(request, validateUrl) {
4969
- let body = await request.clone().text();
4970
- for (let redirects = 0;; redirects++) {
4971
- const response = await fetch(request, { redirect: "manual" });
4972
- const location = response.headers.get("Location");
4973
- if (![
4974
- 301,
4975
- 302,
4976
- 303,
4977
- 307,
4978
- 308
4979
- ].includes(response.status) || location == null) return response;
4980
- await response.body?.cancel();
4981
- if (redirects >= 20) throw new TypeError("Too many redirects");
4982
- const url = new URL(location, request.url);
4983
- await validateUrl(url.href);
4984
- const headers = new Headers(request.headers);
4985
- if (url.origin !== new URL(request.url).origin) {
4986
- headers.delete("Authorization");
4987
- headers.delete("Proxy-Authorization");
4988
- headers.delete("Cookie");
4989
- headers.delete("Host");
4990
- }
4991
- let method = request.method;
4992
- if ((response.status === 301 || response.status === 302) && method === "POST" || response.status === 303 && method !== "GET" && method !== "HEAD") {
4993
- method = "GET";
4994
- body = void 0;
4995
- for (const name of [
4996
- "Content-Encoding",
4997
- "Content-Language",
4998
- "Content-Length",
4999
- "Content-Location",
5000
- "Content-Type"
5001
- ]) headers.delete(name);
5002
- }
5003
- request = new Request(url, {
5004
- method,
5005
- headers,
5006
- body,
5007
- redirect: "manual"
5008
- });
5323
+ async function setKey(kv, key, policy) {
5324
+ const seconds = policy.nonceTtlSeconds ?? 300;
5325
+ const ttl = Temporal$6.Duration.from({ seconds });
5326
+ await kv.set(key, true, { ttl });
5327
+ }
5328
+ const DEF_COMPONENTS = [
5329
+ "@method",
5330
+ "@target-uri",
5331
+ "@authority",
5332
+ "content-digest"
5333
+ ];
5334
+ const MIN_COMPONENTS = [
5335
+ "@method",
5336
+ "@target-uri",
5337
+ "@authority"
5338
+ ];
5339
+ /**
5340
+ * Checks whether an HTTP Signature key is a gateway key of an FEP-ef61
5341
+ * portable actor, i.e., its ID is a compatible identifier and it is owned by
5342
+ * a portable actor, which only the portable actor's signed document can
5343
+ * establish.
5344
+ */
5345
+ function isPortableGatewayKey(key) {
5346
+ return key?.id != null && require_http.isCompatibleKeyId(key.id) && key.ownerId != null && require_http.isPortableId(key.ownerId);
5347
+ }
5348
+ /**
5349
+ * Checks whether an activity needs an Object Integrity Proof by its DID:
5350
+ * either it is performed by an [FEP-ef61] portable actor, or its own ID is
5351
+ * portable. Both `ap:` URIs and compatible identifiers count as portable.
5352
+ *
5353
+ * [FEP-ef61]: https://w3id.org/fep/ef61
5354
+ */
5355
+ function isPortableActivity(activity) {
5356
+ return require_http.hasPortableActor(activity) || activity.id != null && require_http.isPortableId(activity.id);
5357
+ }
5358
+ /**
5359
+ * Verifies that a portable activity's own ID belongs to the DID that signed
5360
+ * it, on the JSON-LD document received. The ID the proof policy checks has
5361
+ * to be the parsed activity's ID, so that a remote context that changes
5362
+ * between loads cannot make the two name different objects.
5363
+ * @returns `null` if the ID is verified, or the reason why it is not.
5364
+ */
5365
+ async function verifyPortableActivityId(activityId, jsonLd, options) {
5366
+ const expectedId = require_http.getCanonicalPortableId(activityId);
5367
+ if (expectedId == null) return "malformed portable ID";
5368
+ let verification;
5369
+ try {
5370
+ verification = await require_http.verifyPortableObjectProofWithRoot(jsonLd, options);
5371
+ } catch (error) {
5372
+ if (error instanceof require_http.InvalidPortableObjectIdError) return "malformed portable ID";
5373
+ if (!isPermanentActivityParseError(error)) throw error;
5374
+ return "malformed document";
5009
5375
  }
5376
+ const { result, root } = verification;
5377
+ if (!result.verified) return result.reason.type;
5378
+ const rootId = root?.["@id"];
5379
+ if (typeof rootId !== "string" || require_http.getCanonicalPortableId((0, _fedify_vocab_runtime.parseIri)(rootId)) !== expectedId) return "the verified document has another ID";
5380
+ return null;
5010
5381
  }
5382
+ //#endregion
5383
+ //#region src/federation/kv.ts
5384
+ const Temporal$5 = require_http.Temporal;
5011
5385
  /**
5012
- * An error that is thrown when an activity fails to send to a remote inbox.
5013
- * It contains structured information about the failure, including the HTTP
5014
- * status code, the inbox URL, and the response body.
5015
- * @since 2.0.0
5386
+ * A key–value store that stores values in memory.
5387
+ * Do not use this in production as it does not persist values.
5388
+ *
5389
+ * @since 0.5.0
5016
5390
  */
5017
- var SendActivityError = class extends Error {
5391
+ var MemoryKvStore = class {
5392
+ #values = {};
5393
+ #encodeKey(key) {
5394
+ return JSON.stringify(key);
5395
+ }
5018
5396
  /**
5019
- * The inbox URL that the activity was being sent to.
5397
+ * {@inheritDoc KvStore.get}
5020
5398
  */
5021
- inbox;
5399
+ get(key) {
5400
+ const encodedKey = this.#encodeKey(key);
5401
+ const entry = this.#values[encodedKey];
5402
+ if (entry == null) return Promise.resolve(void 0);
5403
+ const [value, expiration] = entry;
5404
+ if (expiration != null && Temporal$5.Now.instant().until(expiration).sign < 0) {
5405
+ delete this.#values[encodedKey];
5406
+ return Promise.resolve(void 0);
5407
+ }
5408
+ return Promise.resolve(value);
5409
+ }
5022
5410
  /**
5023
- * The HTTP status code returned by the inbox.
5411
+ * {@inheritDoc KvStore.set}
5024
5412
  */
5025
- statusCode;
5413
+ set(key, value, options) {
5414
+ const encodedKey = this.#encodeKey(key);
5415
+ const expiration = options?.ttl == null ? null : Temporal$5.Now.instant().add(options.ttl.round({ largestUnit: "hour" }));
5416
+ this.#values[encodedKey] = [value, expiration];
5417
+ return Promise.resolve();
5418
+ }
5026
5419
  /**
5027
- * The response body from the inbox, if any. Note that this may be
5028
- * truncated to a maximum of 1 KiB to prevent excessive memory consumption
5029
- * when remote servers return large error pages (e.g., Cloudflare error pages).
5030
- * If truncated, the string will end with `"… (truncated)"`.
5420
+ * {@inheritDoc KvStore.delete}
5031
5421
  */
5032
- responseBody;
5422
+ delete(key) {
5423
+ const encodedKey = this.#encodeKey(key);
5424
+ delete this.#values[encodedKey];
5425
+ return Promise.resolve();
5426
+ }
5033
5427
  /**
5034
- * The response headers from the inbox.
5035
- * @since 2.3.0
5428
+ * {@inheritDoc KvStore.cas}
5036
5429
  */
5037
- responseHeaders;
5430
+ cas(key, expectedValue, newValue, options) {
5431
+ const encodedKey = this.#encodeKey(key);
5432
+ const entry = this.#values[encodedKey];
5433
+ let currentValue;
5434
+ if (entry == null) currentValue = void 0;
5435
+ else {
5436
+ const [value, expiration] = entry;
5437
+ if (expiration != null && Temporal$5.Now.instant().until(expiration).sign < 0) {
5438
+ delete this.#values[encodedKey];
5439
+ currentValue = void 0;
5440
+ } else currentValue = value;
5441
+ }
5442
+ if (!(0, es_toolkit.isEqual)(currentValue, expectedValue)) return Promise.resolve(false);
5443
+ const expiration = options?.ttl == null ? null : Temporal$5.Now.instant().add(options.ttl.round({ largestUnit: "hour" }));
5444
+ this.#values[encodedKey] = [newValue, expiration];
5445
+ return Promise.resolve(true);
5446
+ }
5038
5447
  /**
5039
- * Creates a new {@link SendActivityError}.
5040
- * @param inbox The inbox URL.
5041
- * @param statusCode The HTTP status code.
5042
- * @param message The error message.
5043
- * @param responseBody The response body.
5044
- * @param responseHeaders The response headers.
5448
+ * {@inheritDoc KvStore.list}
5045
5449
  */
5046
- constructor(inbox, statusCode, message, responseBody, responseHeaders) {
5047
- super(message);
5048
- this.name = "SendActivityError";
5049
- this.inbox = inbox;
5050
- this.statusCode = statusCode;
5051
- this.responseBody = responseBody;
5052
- this.responseHeaders = new Headers(responseHeaders);
5450
+ async *list(prefix) {
5451
+ const now = Temporal$5.Now.instant();
5452
+ for (const [encodedKey, entry] of Object.entries(this.#values)) {
5453
+ const key = JSON.parse(encodedKey);
5454
+ if (prefix != null) {
5455
+ if (key.length < prefix.length) continue;
5456
+ if (!prefix.every((p, i) => key[i] === p)) continue;
5457
+ }
5458
+ const [value, expiration] = entry;
5459
+ if (expiration != null && now.until(expiration).sign < 0) {
5460
+ delete this.#values[encodedKey];
5461
+ continue;
5462
+ }
5463
+ yield {
5464
+ key,
5465
+ value
5466
+ };
5467
+ }
5468
+ }
5469
+ };
5470
+ //#endregion
5471
+ //#region src/nodeinfo/handler.ts
5472
+ /**
5473
+ * Handles a NodeInfo request. You would not typically call this function
5474
+ * directly, but instead use {@link Federation.handle} method.
5475
+ * @param request The NodeInfo request to handle.
5476
+ * @param parameters The parameters for handling the request.
5477
+ * @returns The response to the request.
5478
+ */
5479
+ async function handleNodeInfo(_request, { context, nodeInfoDispatcher }) {
5480
+ const promise = nodeInfoDispatcher(context);
5481
+ const json = require_types.nodeInfoToJson(promise instanceof Promise ? await promise : promise);
5482
+ return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json; profile=\"http://nodeinfo.diaspora.software/ns/schema/2.1#\"" } });
5483
+ }
5484
+ /**
5485
+ * Handles a request to `/.well-known/nodeinfo`. You would not typically call
5486
+ * this function directly, but instead use {@link Federation.handle} method.
5487
+ * @param request The request to handle.
5488
+ * @param context The request context.
5489
+ * @returns The response to the request.
5490
+ */
5491
+ function handleNodeInfoJrd(_request, context) {
5492
+ const links = [];
5493
+ try {
5494
+ links.push({
5495
+ rel: "http://nodeinfo.diaspora.software/ns/schema/2.1",
5496
+ href: context.getNodeInfoUri().href,
5497
+ type: "application/json; profile=\"http://nodeinfo.diaspora.software/ns/schema/2.1#\""
5498
+ });
5499
+ } catch (e) {
5500
+ if (!(e instanceof _fedify_uri_template.RouterError)) throw e;
5053
5501
  }
5054
- };
5502
+ const response = new Response(JSON.stringify({ links }), { headers: { "Content-Type": "application/jrd+json" } });
5503
+ return Promise.resolve(response);
5504
+ }
5055
5505
  //#endregion
5056
5506
  //#region src/federation/bench.ts
5057
5507
  /**
@@ -5137,418 +5587,115 @@ async function handleBenchmarkTrigger(request, context, options = {}) {
5137
5587
  const recipients = await parseRecipients(body.recipients, context);
5138
5588
  const activity = await parseActivity(body.activity, context);
5139
5589
  if (activity.id == null) throw new BenchmarkTriggerError("activity must have an id.");
5140
- const activityId = activity.id.href;
5141
- const inboxes = extractInboxes({ recipients });
5142
- const inboxUrls = Object.keys(inboxes);
5143
- if (inboxUrls.length < 1) throw new BenchmarkTriggerError("No valid recipient inboxes found. The recipients list must not be empty.");
5144
- const unsafeInboxes = options.allowUnsafeRecipients ? [] : inboxUrls.filter((inbox) => !options.sinks?.has(inbox));
5145
- if (unsafeInboxes.length > 0) return jsonResponse({
5146
- error: "unsafe_recipient",
5147
- unsafeInboxes
5148
- }, 403);
5149
- await context.sendActivity(sender, recipients, activity);
5150
- return jsonResponse({
5151
- version: 1,
5152
- activityId,
5153
- queueCorrelationId: activityId,
5154
- recipientCount: recipients.length,
5155
- inboxCount: inboxUrls.length
5156
- }, 202);
5157
- } catch (error) {
5158
- if (error instanceof BenchmarkTriggerError) return jsonResponse({ error: error.message }, error.status);
5159
- throw error;
5160
- }
5161
- }
5162
- var BenchmarkTriggerError = class extends Error {
5163
- status;
5164
- constructor(message, status = 400) {
5165
- super(message);
5166
- this.status = status;
5167
- }
5168
- };
5169
- function parseSender(value) {
5170
- const sender = asRecord(value, "sender");
5171
- if (typeof sender.identifier === "string") return { identifier: sender.identifier };
5172
- if (typeof sender.username === "string") return { username: sender.username };
5173
- throw new BenchmarkTriggerError("sender must be { identifier } or { username }.");
5174
- }
5175
- async function parseRecipients(value, context) {
5176
- if (!Array.isArray(value)) throw new BenchmarkTriggerError("recipients must be an array.");
5177
- return await Promise.all(value.map(async (item) => {
5178
- let object;
5179
- try {
5180
- object = await _fedify_vocab.Object.fromJsonLd(item, {
5181
- documentLoader: context.documentLoader,
5182
- contextLoader: context.contextLoader
5183
- });
5184
- } catch (error) {
5185
- throw new BenchmarkTriggerError(`Invalid ActivityPub recipient: ${error}`);
5186
- }
5187
- if (!isRecipient(object)) throw new BenchmarkTriggerError("each recipient must be an ActivityPub actor.");
5188
- const recipient = object;
5189
- if (recipient.id == null || recipient.inboxId == null) throw new BenchmarkTriggerError("each recipient must have id and inbox properties.");
5190
- return recipient;
5191
- }));
5192
- }
5193
- function isRecipient(value) {
5194
- return value != null && typeof value === "object" && "id" in value && "inboxId" in value;
5195
- }
5196
- async function parseActivity(value, context) {
5197
- try {
5198
- return await _fedify_vocab.Activity.fromJsonLd(value, {
5199
- documentLoader: context.documentLoader,
5200
- contextLoader: context.contextLoader
5201
- });
5202
- } catch (error) {
5203
- throw new BenchmarkTriggerError(`Invalid ActivityPub activity: ${error}`);
5204
- }
5205
- }
5206
- function asRecord(value, name) {
5207
- if (value == null || typeof value !== "object" || Array.isArray(value)) throw new BenchmarkTriggerError(`${name} must be an object.`);
5208
- return value;
5209
- }
5210
- function jsonResponse(body, status = 200) {
5211
- return new Response(JSON.stringify(body), {
5212
- status,
5213
- headers: { "Content-Type": "application/json" }
5214
- });
5215
- }
5216
- function serializeScopeMetrics(resourceMetrics) {
5217
- return resourceMetrics.scopeMetrics.map(serializeScope);
5218
- }
5219
- function serializeScope(scopeMetrics) {
5220
- return {
5221
- scope: {
5222
- name: scopeMetrics.scope.name,
5223
- version: scopeMetrics.scope.version
5224
- },
5225
- metrics: scopeMetrics.metrics.map(serializeMetric)
5226
- };
5227
- }
5228
- function serializeMetric(metric) {
5229
- return {
5230
- name: metric.descriptor.name,
5231
- description: metric.descriptor.description,
5232
- unit: metric.descriptor.unit,
5233
- dataPointType: serializeDataPointType(metric.dataPointType),
5234
- dataPoints: metric.dataPoints.map((point) => ({
5235
- attributes: { ...point.attributes },
5236
- startTime: point.startTime,
5237
- endTime: point.endTime,
5238
- value: point.value
5239
- }))
5240
- };
5241
- }
5242
- function serializeDataPointType(dataPointType) {
5243
- switch (dataPointType) {
5244
- case _opentelemetry_sdk_metrics.DataPointType.HISTOGRAM: return "histogram";
5245
- case _opentelemetry_sdk_metrics.DataPointType.EXPONENTIAL_HISTOGRAM: return "exponential_histogram";
5246
- case _opentelemetry_sdk_metrics.DataPointType.GAUGE: return "gauge";
5247
- case _opentelemetry_sdk_metrics.DataPointType.SUM: return "sum";
5248
- }
5249
- }
5250
- //#endregion
5251
- //#region src/federation/portable-inbox.ts
5252
- const Temporal$5 = require_http.Temporal;
5253
- const MAX_TIMEOUT_MS = 2 ** 31 - 1;
5254
- /**
5255
- * Fills in the defaults of {@link PortableInboxForwardingOptions} and
5256
- * validates them.
5257
- * @param options The options.
5258
- * @returns The resolved options.
5259
- * @throws {RangeError} If `maxTargets` is not a non-negative integer, if `ttl`
5260
- * is not positive, if `deadline` is negative or longer
5261
- * than 2,147,483,647 milliseconds (about 24.8 days), or
5262
- * if either duration has calendar units, i.e., weeks,
5263
- * months, or years, whose length depends on the date.
5264
- */
5265
- function resolvePortableInboxForwardingOptions(options = {}) {
5266
- const maxTargets = options.maxTargets ?? 10;
5267
- if (!Number.isInteger(maxTargets) || maxTargets < 0) throw new RangeError("portableInboxForwarding.maxTargets must be a non-negative integer.");
5268
- const ttl = Temporal$5.Duration.from(options.ttl ?? { days: 30 });
5269
- if (ttl.total("millisecond") <= 0) throw new RangeError("portableInboxForwarding.ttl must be positive.");
5270
- const deadline = Temporal$5.Duration.from(options.deadline ?? { seconds: 10 });
5271
- const deadlineMs = deadline.total("millisecond");
5272
- if (deadlineMs < 0 || deadlineMs > MAX_TIMEOUT_MS) throw new RangeError(`portableInboxForwarding.deadline must be between 0 and ${MAX_TIMEOUT_MS} milliseconds.`);
5273
- return {
5274
- maxTargets,
5275
- ttl,
5276
- deadline
5277
- };
5278
- }
5279
- /**
5280
- * Decides whether this server accepts a delivery to a portable inbox on behalf
5281
- * of the given actor, which the actor dispatcher returned for the identifier
5282
- * in the requested inbox path.
5283
- *
5284
- * The actor document comes from the application, so it is trusted as is and
5285
- * its proof is not verified here.
5286
- * @param actor The actor the actor dispatcher returned.
5287
- * @param options The requested DID, the canonical form of the requested inbox
5288
- * ID, and the origin of this server.
5289
- * @returns The resolution.
5290
- */
5291
- function resolvePortableInboxRecipient(actor, { authority, canonicalInboxId, localOrigin }) {
5292
- if (actor == null) return {
5293
- status: "rejected",
5294
- reason: "notFound"
5295
- };
5296
- const id = actor.id;
5297
- const did = id == null || !require_http.isPortableId(id) ? null : require_http.getPortableDid(id);
5298
- if (id == null || did == null) return {
5299
- status: "rejected",
5300
- reason: "notPortable"
5301
- };
5302
- if (did !== authority) return {
5303
- status: "rejected",
5304
- reason: "authorityMismatch"
5305
- };
5306
- const inboxId = actor.inboxId == null ? null : toPortableUri(actor.inboxId);
5307
- if (inboxId == null || require_http.getCanonicalPortableId(inboxId) !== canonicalInboxId) return {
5308
- status: "rejected",
5309
- reason: "inboxMismatch"
5310
- };
5311
- const gateways = actor.gateways;
5312
- if (!gateways.some((g) => (0, _fedify_vocab_runtime.isGatewayUrl)(g) && g.origin === localOrigin)) return {
5313
- status: "rejected",
5314
- reason: "notGateway"
5315
- };
5316
- return {
5317
- status: "accepted",
5318
- recipient: {
5319
- actorId: id,
5320
- inboxId,
5321
- canonicalInboxId,
5322
- gateways
5323
- }
5324
- };
5325
- }
5326
- /**
5327
- * Gets the portable form of an `ap:` or `ap+ef61:` URI or a compatible
5328
- * identifier, keeping its query.
5329
- * @returns The portable URI, or `null` if the ID is neither, or is malformed.
5330
- */
5331
- function toPortableUri(id) {
5332
- if (require_http.isPortableUri(id)) return id;
5333
- try {
5334
- return (0, _fedify_vocab_runtime.fromCompatibleEf61Id)(id);
5335
- } catch (error) {
5336
- if (error instanceof TypeError) return null;
5337
- throw error;
5338
- }
5339
- }
5340
- /**
5341
- * Forwards an activity received in a portable inbox to the inboxes of the same
5342
- * portable actor on its other gateways, as FEP-ef61 recommends.
5343
- *
5344
- * FEP-ef61 forbids forwarding an activity from an inbox more than once, so
5345
- * each target gateway is claimed with an atomic compare-and-swap before the
5346
- * activity is handed off, and a claim is never released, even if the hand-off
5347
- * fails, since a failed hand-off does not prove that nothing was delivered.
5348
- * Forwarding is therefore best effort. Without {@link KvStore.cas}, nothing is
5349
- * forwarded.
5350
- *
5351
- * Forwarded requests are signed with HTTP Signatures by the RSA key among
5352
- * {@link ForwardPortableInboxActivityParameters.getKeys}, if any, so that
5353
- * servers requiring HTTP Signatures accept them, and are sent unsigned
5354
- * otherwise. Either way, the receiving gateways authenticate the activity by
5355
- * its own proof.
5356
- * @param parameters The parameters.
5357
- * @returns The target inbox URLs that the activity was handed off to.
5358
- */
5359
- async function forwardPortableInboxActivity(parameters) {
5360
- const logger = (0, _logtape_logtape.getLogger)([
5361
- "fedify",
5362
- "federation",
5363
- "inbox"
5364
- ]);
5365
- const { recipient, activityId, excludedOrigins, kv, kvPrefix, outboxQueue } = parameters;
5366
- const { maxTargets, ttl } = parameters.options ?? resolvePortableInboxForwardingOptions();
5367
- if (kv.cas == null || maxTargets < 1) return [];
5368
- const canonicalActivityId = require_http.getCanonicalPortableId(activityId) ?? activityId.href;
5369
- const targets = getForwardingTargets(recipient, excludedOrigins);
5370
- if (targets.length > maxTargets) {
5371
- 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.", {
5372
- inbox: recipient.canonicalInboxId,
5373
- activityId: activityId.href,
5374
- max: maxTargets
5375
- });
5376
- targets.length = maxTargets;
5590
+ const activityId = activity.id.href;
5591
+ const inboxes = extractInboxes({ recipients });
5592
+ const inboxUrls = Object.keys(inboxes);
5593
+ if (inboxUrls.length < 1) throw new BenchmarkTriggerError("No valid recipient inboxes found. The recipients list must not be empty.");
5594
+ const unsafeInboxes = options.allowUnsafeRecipients ? [] : inboxUrls.filter((inbox) => !options.sinks?.has(inbox));
5595
+ if (unsafeInboxes.length > 0) return jsonResponse({
5596
+ error: "unsafe_recipient",
5597
+ unsafeInboxes
5598
+ }, 403);
5599
+ await context.sendActivity(sender, recipients, activity);
5600
+ return jsonResponse({
5601
+ version: 1,
5602
+ activityId,
5603
+ queueCorrelationId: activityId,
5604
+ recipientCount: recipients.length,
5605
+ inboxCount: inboxUrls.length
5606
+ }, 202);
5607
+ } catch (error) {
5608
+ if (error instanceof BenchmarkTriggerError) return jsonResponse({ error: error.message }, error.status);
5609
+ throw error;
5377
5610
  }
5378
- const claimed = [];
5379
- for (const { gateway, inbox } of targets) {
5380
- const key = [
5381
- ...kvPrefix,
5382
- recipient.canonicalInboxId,
5383
- canonicalActivityId,
5384
- gateway
5385
- ];
5386
- if (await kv.cas(key, void 0, true, { ttl })) claimed.push(inbox);
5611
+ }
5612
+ var BenchmarkTriggerError = class extends Error {
5613
+ status;
5614
+ constructor(message, status = 400) {
5615
+ super(message);
5616
+ this.status = status;
5387
5617
  }
5388
- if (claimed.length < 1) return [];
5389
- logger.debug("Forwarding activity {activityId} from the portable inbox {inbox} to other gateways:\n{targets}", {
5390
- activityId: activityId.href,
5391
- inbox: recipient.canonicalInboxId,
5392
- targets: claimed.map((t) => t.href)
5393
- });
5394
- if (outboxQueue == null) await forwardImmediately(parameters, claimed);
5395
- else await enqueueForwarding(parameters, outboxQueue, claimed);
5396
- return claimed;
5618
+ };
5619
+ function parseSender(value) {
5620
+ const sender = asRecord(value, "sender");
5621
+ if (typeof sender.identifier === "string") return { identifier: sender.identifier };
5622
+ if (typeof sender.username === "string") return { username: sender.username };
5623
+ throw new BenchmarkTriggerError("sender must be { identifier } or { username }.");
5397
5624
  }
5398
- function getForwardingTargets(recipient, excludedOrigins) {
5399
- const logger = (0, _logtape_logtape.getLogger)([
5400
- "fedify",
5401
- "federation",
5402
- "inbox"
5403
- ]);
5404
- const seen = new Set(excludedOrigins);
5405
- const targets = [];
5406
- for (const gateway of recipient.gateways) {
5407
- if (!(0, _fedify_vocab_runtime.isGatewayUrl)(gateway) || seen.has(gateway.origin)) continue;
5408
- seen.add(gateway.origin);
5409
- let inbox;
5625
+ async function parseRecipients(value, context) {
5626
+ if (!Array.isArray(value)) throw new BenchmarkTriggerError("recipients must be an array.");
5627
+ return await Promise.all(value.map(async (item) => {
5628
+ let object;
5410
5629
  try {
5411
- inbox = (0, _fedify_vocab_runtime.toCompatibleEf61Id)(recipient.inboxId, gateway.origin);
5412
- } catch (error) {
5413
- if (!(error instanceof TypeError)) throw error;
5414
- logger.warn("Cannot forward to the gateway {gateway}, as the portable inbox {inbox} cannot be represented as its compatible identifier.", {
5415
- gateway: gateway.origin,
5416
- inbox: recipient.canonicalInboxId
5630
+ object = await _fedify_vocab.Object.fromJsonLd(item, {
5631
+ documentLoader: context.documentLoader,
5632
+ contextLoader: context.contextLoader
5417
5633
  });
5418
- continue;
5634
+ } catch (error) {
5635
+ throw new BenchmarkTriggerError(`Invalid ActivityPub recipient: ${error}`);
5419
5636
  }
5420
- targets.push({
5421
- gateway: gateway.origin,
5422
- inbox
5423
- });
5424
- }
5425
- return targets;
5637
+ if (!isRecipient(object)) throw new BenchmarkTriggerError("each recipient must be an ActivityPub actor.");
5638
+ const recipient = object;
5639
+ if (recipient.id == null || recipient.inboxId == null) throw new BenchmarkTriggerError("each recipient must have id and inbox properties.");
5640
+ return recipient;
5641
+ }));
5426
5642
  }
5427
- /**
5428
- * Resolves the key that signs forwarded requests: the first RSASSA-PKCS1-v1_5
5429
- * key, as {@link sendActivity} signs requests only with such a key. Since
5430
- * the targets have already been claimed, the activity is forwarded unsigned
5431
- * rather than not at all if the key cannot be resolved or used.
5432
- */
5433
- async function resolveForwardingKey({ getKeys, activityId }) {
5434
- if (getKeys == null) return null;
5435
- const logger = (0, _logtape_logtape.getLogger)([
5436
- "fedify",
5437
- "federation",
5438
- "inbox"
5439
- ]);
5440
- let key;
5643
+ function isRecipient(value) {
5644
+ return value != null && typeof value === "object" && "id" in value && "inboxId" in value;
5645
+ }
5646
+ async function parseActivity(value, context) {
5441
5647
  try {
5442
- key = (await getKeys()).find((k) => k.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5");
5443
- } catch (error) {
5444
- logger.error("Failed to get the gateway keys to sign forwarded requests with; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
5445
- activityId: activityId.href,
5446
- error
5648
+ return await _fedify_vocab.Activity.fromJsonLd(value, {
5649
+ documentLoader: context.documentLoader,
5650
+ contextLoader: context.contextLoader
5447
5651
  });
5448
- return null;
5449
- }
5450
- if (key == null) return null;
5451
- try {
5452
- require_http.validateCryptoKey(key.privateKey, "private");
5453
5652
  } catch (error) {
5454
- if (!(error instanceof TypeError)) throw error;
5455
- logger.warn("The gateway key {keyId} cannot sign forwarded requests; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
5456
- keyId: key.keyId.href,
5457
- activityId: activityId.href,
5458
- error
5459
- });
5460
- return null;
5653
+ throw new BenchmarkTriggerError(`Invalid ActivityPub activity: ${error}`);
5461
5654
  }
5462
- return key;
5463
5655
  }
5464
- async function forwardImmediately(parameters, inboxes) {
5465
- const { activity, activityId, activityType, allowPrivateAddress, specDeterminer, options, meterProvider, tracerProvider } = parameters;
5466
- const logger = (0, _logtape_logtape.getLogger)([
5467
- "fedify",
5468
- "federation",
5469
- "inbox"
5470
- ]);
5471
- const deadline = (options ?? resolvePortableInboxForwardingOptions()).deadline.total("millisecond");
5472
- const forwarding = resolveForwardingKey(parameters).then((key) => Promise.all(inboxes.map((inbox) => sendActivity({
5473
- activity,
5474
- activityId: activityId.href,
5475
- activityType,
5476
- keys: key == null ? [] : [key],
5477
- inbox,
5478
- allowPrivateAddress,
5479
- specDeterminer,
5480
- meterProvider,
5481
- tracerProvider
5482
- }).catch((error) => {
5483
- logger.error("Failed to forward activity {activityId} to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
5484
- activityId: activityId.href,
5485
- inbox: inbox.href,
5486
- error
5487
- });
5488
- }))));
5489
- let timer;
5490
- const timedOut = await Promise.race([forwarding.then(() => false), new Promise((resolve) => {
5491
- timer = setTimeout(() => resolve(true), deadline);
5492
- })]);
5493
- clearTimeout(timer);
5494
- if (timedOut) logger.warn("Forwarding activity {activityId} to other gateways did not finish in {deadline} ms; responding to the delivery without waiting for it.", {
5495
- activityId: activityId.href,
5496
- deadline
5656
+ function asRecord(value, name) {
5657
+ if (value == null || typeof value !== "object" || Array.isArray(value)) throw new BenchmarkTriggerError(`${name} must be an object.`);
5658
+ return value;
5659
+ }
5660
+ function jsonResponse(body, status = 200) {
5661
+ return new Response(JSON.stringify(body), {
5662
+ status,
5663
+ headers: { "Content-Type": "application/json" }
5497
5664
  });
5498
5665
  }
5499
- async function enqueueForwarding(parameters, outboxQueue, inboxes) {
5500
- const { activity, activityId, activityType, baseUrl, startQueue, meterProvider } = parameters;
5501
- const logger = (0, _logtape_logtape.getLogger)([
5502
- "fedify",
5503
- "federation",
5504
- "inbox"
5505
- ]);
5506
- const key = await resolveForwardingKey(parameters);
5507
- const keys = [];
5508
- if (key != null) try {
5509
- keys.push({
5510
- keyId: key.keyId.href,
5511
- privateKey: await require_http.exportJwk(key.privateKey)
5512
- });
5513
- } catch (error) {
5514
- logger.error("Failed to export the gateway key {keyId}; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
5515
- keyId: key.keyId.href,
5516
- activityId: activityId.href,
5517
- error
5518
- });
5666
+ function serializeScopeMetrics(resourceMetrics) {
5667
+ return resourceMetrics.scopeMetrics.map(serializeScope);
5668
+ }
5669
+ function serializeScope(scopeMetrics) {
5670
+ return {
5671
+ scope: {
5672
+ name: scopeMetrics.scope.name,
5673
+ version: scopeMetrics.scope.version
5674
+ },
5675
+ metrics: scopeMetrics.metrics.map(serializeMetric)
5676
+ };
5677
+ }
5678
+ function serializeMetric(metric) {
5679
+ return {
5680
+ name: metric.descriptor.name,
5681
+ description: metric.descriptor.description,
5682
+ unit: metric.descriptor.unit,
5683
+ dataPointType: serializeDataPointType(metric.dataPointType),
5684
+ dataPoints: metric.dataPoints.map((point) => ({
5685
+ attributes: { ...point.attributes },
5686
+ startTime: point.startTime,
5687
+ endTime: point.endTime,
5688
+ value: point.value
5689
+ }))
5690
+ };
5691
+ }
5692
+ function serializeDataPointType(dataPointType) {
5693
+ switch (dataPointType) {
5694
+ case _opentelemetry_sdk_metrics.DataPointType.HISTOGRAM: return "histogram";
5695
+ case _opentelemetry_sdk_metrics.DataPointType.EXPONENTIAL_HISTOGRAM: return "exponential_histogram";
5696
+ case _opentelemetry_sdk_metrics.DataPointType.GAUGE: return "gauge";
5697
+ case _opentelemetry_sdk_metrics.DataPointType.SUM: return "sum";
5519
5698
  }
5520
- startQueue?.();
5521
- const started = (/* @__PURE__ */ new Date()).toISOString();
5522
- const traceContext = {};
5523
- _opentelemetry_api.propagation.inject(_opentelemetry_api.context.active(), traceContext);
5524
- await Promise.all(inboxes.map(async (inbox) => {
5525
- const message = {
5526
- type: "outbox",
5527
- id: crypto.randomUUID(),
5528
- baseUrl,
5529
- keys,
5530
- activity,
5531
- activityId: activityId.href,
5532
- activityType,
5533
- inbox: inbox.href,
5534
- sharedInbox: false,
5535
- started,
5536
- attempt: 0,
5537
- headers: {},
5538
- traceContext
5539
- };
5540
- try {
5541
- await outboxQueue.enqueue(message);
5542
- } catch (error) {
5543
- logger.error("Failed to enqueue activity {activityId} to forward to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
5544
- activityId: activityId.href,
5545
- inbox: inbox.href,
5546
- error
5547
- });
5548
- return;
5549
- }
5550
- require_http.recordOutboxEnqueue(meterProvider, outboxQueue, message);
5551
- }));
5552
5699
  }
5553
5700
  //#endregion
5554
5701
  //#region src/federation/outgoing-proof.ts
@@ -8119,17 +8266,39 @@ var FederationImpl = class extends FederationBuilderImpl {
8119
8266
  });
8120
8267
  }
8121
8268
  const route = this.router.route(portable.path);
8122
- if (route == null || !route.name.startsWith("object:")) {
8269
+ const isActor = route != null && (route.name === "actor" || route.name.startsWith("actorAlias:"));
8270
+ if (route == null || !isActor && !route.name.startsWith("object:")) {
8123
8271
  metricState.endpoint = "not_found";
8124
8272
  return await onNotFound(request);
8125
8273
  }
8126
8274
  metricState.routeTemplate = PORTABLE_GATEWAY_ROUTE_PREFIX + route.template;
8127
- metricState.endpoint = "object";
8275
+ metricState.endpoint = isActor ? "actor" : "object";
8128
8276
  span.updateName(`${request.method} ${metricState.routeTemplate}`);
8129
8277
  if (!acceptsJsonLd(request)) {
8130
8278
  metricState.endpoint = "not_acceptable";
8131
8279
  return await onNotAcceptable(request);
8132
8280
  }
8281
+ if (isActor) {
8282
+ const identifier = route.name.startsWith("actorAlias:") ? route.name.substring(11) : route.values.identifier;
8283
+ const context = this.#createContext(request, contextData, {
8284
+ invokedFromActorDispatcher: { identifier },
8285
+ portableRequest: portable.portableRequest
8286
+ });
8287
+ const actorDispatcher = this.actorCallbacks?.dispatcher;
8288
+ const authorizePredicate = this.actorCallbacks?.authorizePredicate;
8289
+ return await handlePortableObject(request, {
8290
+ values: { identifier },
8291
+ context,
8292
+ objectDispatcher: actorDispatcher == null ? void 0 : async (ctx) => {
8293
+ const actor = await actorDispatcher(ctx, identifier);
8294
+ return actor instanceof _fedify_vocab.Tombstone ? null : actor;
8295
+ },
8296
+ authorizePredicate: authorizePredicate == null ? void 0 : (ctx) => authorizePredicate(ctx, identifier),
8297
+ canonicalId: portable.canonicalId,
8298
+ onUnauthorized,
8299
+ onNotFound
8300
+ });
8301
+ }
8133
8302
  const typeId = route.name.replace(/^object:/, "");
8134
8303
  const callbacks = this.objectCallbacks[typeId];
8135
8304
  const cls = this.objectTypeIds[typeId];
@@ -8228,7 +8397,7 @@ var FederationImpl = class extends FederationBuilderImpl {
8228
8397
  onNotFound,
8229
8398
  portableInbox: {
8230
8399
  recipient,
8231
- forward: async (activity, activityId, activityType) => {
8400
+ forward: async (activity, activityId, activityType, signatures) => {
8232
8401
  if (this.portableInboxForwarding.maxTargets < 1) return;
8233
8402
  if (this.kv.cas == null) {
8234
8403
  if (!this.#portableInboxForwardingWarned) {
@@ -8251,6 +8420,7 @@ var FederationImpl = class extends FederationBuilderImpl {
8251
8420
  allowPrivateAddress: this.allowPrivateAddress,
8252
8421
  getKeys: () => this.#getPortableGatewayKeyPairs(context, identifier, recipient),
8253
8422
  specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
8423
+ signatures,
8254
8424
  options: this.portableInboxForwarding,
8255
8425
  meterProvider: this.meterProvider,
8256
8426
  tracerProvider: this.tracerProvider
@@ -8492,9 +8662,17 @@ var ContextImpl = class ContextImpl {
8492
8662
  return new URL(path, this.canonicalOrigin);
8493
8663
  }
8494
8664
  getActorUri(identifier) {
8665
+ return new URL(this.#getActorPath(identifier), this.canonicalOrigin);
8666
+ }
8667
+ getPortableActorUri(identifier, authority) {
8668
+ const path = this.#getActorPath(identifier);
8669
+ if (authority == null) throw new TypeError("The authority of a portable ID is required outside an FEP-ef61 gateway request.");
8670
+ return buildPortableUri(authority, path);
8671
+ }
8672
+ #getActorPath(identifier) {
8495
8673
  const path = this.federation.router.build(`actorAlias:${identifier}`, {}) ?? this.federation.router.build("actor", { identifier });
8496
8674
  if (path == null) throw new _fedify_uri_template.RouterError("No actor dispatcher registered.");
8497
- return new URL(path, this.canonicalOrigin);
8675
+ return path;
8498
8676
  }
8499
8677
  getObjectUri(cls, values) {
8500
8678
  return new URL(this.#getObjectPath(cls, values), this.canonicalOrigin);
@@ -9137,6 +9315,9 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
9137
9315
  request: this.request
9138
9316
  });
9139
9317
  }
9318
+ getPortableActorUri(identifier, authority) {
9319
+ return super.getPortableActorUri(identifier, authority ?? this.portableRequest?.authority);
9320
+ }
9140
9321
  getPortableObjectUri(cls, values, authority) {
9141
9322
  return super.getPortableObjectUri(cls, values, authority ?? this.portableRequest?.authority);
9142
9323
  }