@fedify/fedify 2.4.0-dev.2093 → 2.4.0-dev.2099
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.
- package/dist/{builder-Ld4Y9ymR.mjs → builder-Bno54o_-.mjs} +2 -2
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/transformers.test.mjs +1 -1
- package/dist/{compound-proof-CTx_lewQ.mjs → compound-proof-X3SxXVbc.mjs} +2 -2
- package/dist/{context-CZjZYXFC.d.cts → context-C43spTGI.d.cts} +5 -2
- package/dist/{context-BSywYspi.d.ts → context-MORIQJIh.d.ts} +5 -2
- package/dist/{deno-8t8k_6Yv.mjs → deno-dxdvl2XZ.mjs} +1 -1
- package/dist/{docloader-DJKcsAiF.mjs → docloader-eBxQDsk9.mjs} +2 -2
- package/dist/federation/builder.test.mjs +1 -1
- package/dist/federation/compound-fanout.test.mjs +3 -3
- package/dist/federation/handler-compound-proof.test.mjs +3 -3
- package/dist/federation/handler.test.mjs +5 -5
- package/dist/federation/hashlink-media.test.mjs +2 -2
- package/dist/federation/idempotency.test.mjs +2 -2
- package/dist/federation/metrics.test.mjs +1 -1
- package/dist/federation/middleware.test.mjs +7 -7
- package/dist/federation/mod.cjs +1 -1
- package/dist/federation/mod.d.cts +2 -2
- package/dist/federation/mod.d.ts +2 -2
- package/dist/federation/mod.js +1 -1
- package/dist/federation/outgoing-proof.test.mjs +5 -5
- package/dist/federation/portable-gateway-keys.test.mjs +3 -3
- package/dist/federation/portable-inbox.test.mjs +375 -10
- package/dist/federation/portable.test.mjs +3 -3
- package/dist/federation/send.test.mjs +3 -3
- package/dist/federation/tasks/enqueue.test.mjs +1 -1
- package/dist/federation/tasks/tasks.test.mjs +2 -2
- package/dist/federation/temporal.test.mjs +1 -1
- package/dist/federation/webfinger.test.mjs +1 -1
- package/dist/{federation-aSHO04ob.cjs → federation-C4Ef3JKa.cjs} +1211 -1089
- package/dist/{http-DSaSAN7X.cjs → http-C8cZTE3Y.cjs} +89 -2
- package/dist/{http-DyT7DP1Y.js → http-D-rbr_Sy.js} +78 -3
- package/dist/{http-B2ftY0Ha.mjs → http-DsgawTd8.mjs} +82 -7
- package/dist/{key-Dwm4VswT.mjs → key-DYfmjnmp.mjs} +2 -2
- package/dist/{kv-cache-DmroAz3M.js → kv-cache-DIxwMeNi.js} +1 -1
- package/dist/{kv-cache-BSJAlP4C.cjs → kv-cache-Dvd-Q-E2.cjs} +1 -1
- package/dist/{kv-cache-D0yYukqk.mjs → kv-cache-EvOgrFsS.mjs} +1 -1
- package/dist/{ld-BYZOoDzZ.mjs → ld-B00wWHzc.mjs} +3 -3
- package/dist/{metrics-B7YwKTFr.mjs → metrics-BP3vdBpb.mjs} +1 -1
- package/dist/{middleware-E_0IH72m.mjs → middleware-Br1_d-Ga.mjs} +627 -505
- package/dist/{middleware-Blq0882d.js → middleware-CcXMe400.js} +1307 -1185
- package/dist/{middleware-BNeqKSsL.mjs → middleware-CeBVBa61.mjs} +1 -1
- package/dist/{mod-B2h2KY0q.d.ts → mod-BL5BQHCq.d.ts} +1 -1
- package/dist/{mod-JzkaEXsi.d.cts → mod-DMI4DbOo.d.cts} +1 -1
- package/dist/mod.cjs +5 -5
- package/dist/mod.d.cts +2 -2
- package/dist/mod.d.ts +2 -2
- package/dist/mod.js +5 -5
- package/dist/nodeinfo/handler.test.mjs +1 -1
- package/dist/{owner-Cgx2Dc7I.mjs → owner-BJPDRDRq.mjs} +3 -3
- package/dist/{owner-szIZl4qI.cjs → owner-DSzH7wro.cjs} +1 -1
- package/dist/{owner-DAGSC5gX.js → owner-U12fDpQW.js} +1 -1
- package/dist/{portable-key-Cn1WJSN1.mjs → portable-key-D0mWbV9G.mjs} +1 -1
- package/dist/{proof-DVXC6nYz.mjs → proof-ChjBwnep.mjs} +4 -4
- package/dist/{send-D-hOvYKy.mjs → send-yFX0E-ye.mjs} +3 -3
- package/dist/sig/compound-proof-discovery.test.mjs +1 -1
- package/dist/sig/compound-proof-producer.test.mjs +1 -1
- package/dist/sig/compound-proof-verification.test.mjs +2 -2
- package/dist/sig/compound-proof.test.mjs +2 -2
- package/dist/sig/http.test.mjs +72 -2
- package/dist/sig/key.test.mjs +1 -1
- package/dist/sig/ld.test.mjs +2 -2
- package/dist/sig/mod.cjs +3 -3
- package/dist/sig/mod.js +3 -3
- package/dist/sig/owner.test.mjs +1 -1
- package/dist/sig/portable-collection.test.mjs +2 -2
- package/dist/sig/portable-key.test.mjs +3 -3
- package/dist/sig/proof.test.mjs +3 -3
- package/dist/{sig-CXOKN4Br.cjs → sig-B689Nj7y.cjs} +2 -2
- package/dist/{sig-B5Ell7pO.js → sig-C85KqAcx.js} +2 -2
- package/dist/{temporal-CqpKGEPk.mjs → temporal-Dl9x-Qw4.mjs} +1 -1
- package/dist/testing/kv.test.mjs +1 -1
- package/dist/testing/mod.d.mts +5 -2
- package/dist/utils/docloader.test.mjs +2 -2
- package/dist/utils/kv-cache.test.mjs +1 -1
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.js +1 -1
- package/package.json +6 -6
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
const { URLPattern } = require("urlpattern-polyfill");
|
|
2
2
|
const require_rolldown_runtime = require("./rolldown-runtime-Dmm3t92W.cjs");
|
|
3
3
|
const require_transformers = require("./transformers-BalCrSsr.cjs");
|
|
4
|
-
const require_http = require("./http-
|
|
4
|
+
const require_http = require("./http-C8cZTE3Y.cjs");
|
|
5
5
|
const require_types = require("./types-Be2zbyeG.cjs");
|
|
6
|
-
const require_owner = require("./owner-
|
|
7
|
-
const require_kv_cache = require("./kv-cache-
|
|
6
|
+
const require_owner = require("./owner-DSzH7wro.cjs");
|
|
7
|
+
const require_kv_cache = require("./kv-cache-Dvd-Q-E2.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");
|
|
@@ -2521,157 +2521,857 @@ function parseHashlinkGatewayRequest(url) {
|
|
|
2521
2521
|
};
|
|
2522
2522
|
}
|
|
2523
2523
|
//#endregion
|
|
2524
|
-
//#region src/federation/
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
}
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
}
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2524
|
+
//#region src/federation/send.ts
|
|
2525
|
+
/**
|
|
2526
|
+
* Extracts the inbox URLs from recipients.
|
|
2527
|
+
* @param parameters The parameters to extract the inboxes.
|
|
2528
|
+
* See also {@link ExtractInboxesParameters}.
|
|
2529
|
+
* @returns The inboxes as a map of inbox URL to actor URIs.
|
|
2530
|
+
*/
|
|
2531
|
+
function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris }) {
|
|
2532
|
+
const inboxes = {};
|
|
2533
|
+
for (const recipient of recipients) {
|
|
2534
|
+
let inbox;
|
|
2535
|
+
let sharedInbox = false;
|
|
2536
|
+
if (preferSharedInbox && recipient.endpoints?.sharedInbox != null) {
|
|
2537
|
+
inbox = recipient.endpoints.sharedInbox;
|
|
2538
|
+
sharedInbox = true;
|
|
2539
|
+
} else inbox = recipient.inboxId;
|
|
2540
|
+
if (inbox != null && recipient.id != null) {
|
|
2541
|
+
if (excludeBaseUris != null && excludeBaseUris.some((u) => u.origin === inbox?.origin)) continue;
|
|
2542
|
+
inboxes[inbox.href] ??= {
|
|
2543
|
+
actorIds: /* @__PURE__ */ new Set(),
|
|
2544
|
+
sharedInbox
|
|
2545
|
+
};
|
|
2546
|
+
inboxes[inbox.href].actorIds.add(recipient.id.href);
|
|
2547
|
+
}
|
|
2548
2548
|
}
|
|
2549
|
+
return inboxes;
|
|
2549
2550
|
}
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2551
|
+
/**
|
|
2552
|
+
* Sends an {@link Activity} to an inbox.
|
|
2553
|
+
*
|
|
2554
|
+
* @param parameters The parameters for sending the activity.
|
|
2555
|
+
* See also {@link SendActivityParameters}.
|
|
2556
|
+
* @throws {Error} If the activity fails to send.
|
|
2557
|
+
*/
|
|
2558
|
+
function sendActivity(options) {
|
|
2559
|
+
const tracerProvider = options.tracerProvider ?? _opentelemetry_api.trace.getTracerProvider();
|
|
2560
|
+
return tracerProvider.getTracer(require_http.name, require_http.version).startActiveSpan("activitypub.send_activity", {
|
|
2561
|
+
kind: _opentelemetry_api.SpanKind.CLIENT,
|
|
2562
|
+
attributes: { "activitypub.shared_inbox": options.sharedInbox ?? false }
|
|
2563
|
+
}, async (span) => {
|
|
2564
|
+
if (options.activityId != null) span.setAttribute("activitypub.activity.id", options.activityId);
|
|
2565
|
+
if (options.activityType != null) span.setAttribute("activitypub.activity.type", options.activityType);
|
|
2566
|
+
try {
|
|
2567
|
+
await sendActivityInternal({
|
|
2568
|
+
...options,
|
|
2569
|
+
tracerProvider
|
|
2570
|
+
}, span);
|
|
2571
|
+
} catch (e) {
|
|
2572
|
+
span.setStatus({
|
|
2573
|
+
code: _opentelemetry_api.SpanStatusCode.ERROR,
|
|
2574
|
+
message: String(e)
|
|
2575
|
+
});
|
|
2576
|
+
throw e;
|
|
2577
|
+
} finally {
|
|
2578
|
+
span.end();
|
|
2579
|
+
}
|
|
2580
|
+
});
|
|
2575
2581
|
}
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2582
|
+
const MAX_ERROR_RESPONSE_BODY_BYTES = 1024;
|
|
2583
|
+
function getActivityActorId(activity) {
|
|
2584
|
+
if (!isRecord(activity)) return void 0;
|
|
2585
|
+
return getIdValue(activity.actor);
|
|
2579
2586
|
}
|
|
2580
|
-
function
|
|
2581
|
-
if (
|
|
2582
|
-
if (
|
|
2583
|
-
if (
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
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;
|
|
2587
|
+
function getIdValue(value) {
|
|
2588
|
+
if (typeof value === "string" && value !== "") return value;
|
|
2589
|
+
if (value instanceof URL) return value.href;
|
|
2590
|
+
if (Array.isArray(value)) {
|
|
2591
|
+
for (const item of value) {
|
|
2592
|
+
const id = getIdValue(item);
|
|
2593
|
+
if (id != null) return id;
|
|
2598
2594
|
}
|
|
2599
|
-
|
|
2595
|
+
return;
|
|
2600
2596
|
}
|
|
2601
|
-
return
|
|
2597
|
+
if (isRecord(value)) return getIdValue(value.id);
|
|
2602
2598
|
}
|
|
2603
|
-
|
|
2599
|
+
function isRecord(value) {
|
|
2600
|
+
return typeof value === "object" && value != null;
|
|
2601
|
+
}
|
|
2602
|
+
async function readLimitedResponseBody(response, maxBytes) {
|
|
2603
|
+
if (response.body == null) return "";
|
|
2604
|
+
const reader = response.body.getReader();
|
|
2605
|
+
const decoder = new TextDecoder();
|
|
2606
|
+
const chunks = [];
|
|
2607
|
+
let totalBytes = 0;
|
|
2608
|
+
let truncated = false;
|
|
2604
2609
|
try {
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2610
|
+
while (true) {
|
|
2611
|
+
const { done, value } = await reader.read();
|
|
2612
|
+
if (done) break;
|
|
2613
|
+
if (totalBytes + value.length > maxBytes) {
|
|
2614
|
+
const remaining = maxBytes - totalBytes;
|
|
2615
|
+
if (remaining > 0) chunks.push(decoder.decode(value.slice(0, remaining), { stream: true }));
|
|
2616
|
+
truncated = true;
|
|
2617
|
+
break;
|
|
2618
|
+
}
|
|
2619
|
+
chunks.push(decoder.decode(value, { stream: true }));
|
|
2620
|
+
totalBytes += value.length;
|
|
2621
|
+
}
|
|
2622
|
+
} finally {
|
|
2623
|
+
reader.releaseLock();
|
|
2611
2624
|
}
|
|
2625
|
+
let result = chunks.join("");
|
|
2626
|
+
if (truncated) result += "… (truncated)";
|
|
2627
|
+
return result;
|
|
2612
2628
|
}
|
|
2613
|
-
|
|
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 }) {
|
|
2629
|
+
async function sendActivityInternal({ activity, activityId, activityType, keys, inbox, allowPrivateAddress, headers, specDeterminer, meterProvider, tracerProvider }, span) {
|
|
2657
2630
|
const logger = (0, _logtape_logtape.getLogger)([
|
|
2658
2631
|
"fedify",
|
|
2659
2632
|
"federation",
|
|
2660
|
-
"
|
|
2633
|
+
"outbox"
|
|
2661
2634
|
]);
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2635
|
+
const federationMetrics = require_http.getFederationMetrics(meterProvider);
|
|
2636
|
+
const started = performance.now();
|
|
2637
|
+
let deliverySuccess = false;
|
|
2638
|
+
async function validateUrl(url) {
|
|
2639
|
+
if (!allowPrivateAddress) try {
|
|
2640
|
+
await (0, _fedify_vocab_runtime.validatePublicUrl)(url);
|
|
2641
|
+
} catch (error) {
|
|
2642
|
+
if (error instanceof _fedify_vocab_runtime.UrlError) logger.error("Disallowed private URL: {url}", {
|
|
2643
|
+
url,
|
|
2644
|
+
error
|
|
2645
|
+
});
|
|
2646
|
+
throw error;
|
|
2647
|
+
}
|
|
2670
2648
|
}
|
|
2671
|
-
|
|
2672
|
-
|
|
2649
|
+
await validateUrl(inbox.href);
|
|
2650
|
+
headers = new Headers(headers);
|
|
2651
|
+
headers.set("Content-Type", "application/activity+json");
|
|
2652
|
+
const request = new Request(inbox, {
|
|
2653
|
+
method: "POST",
|
|
2654
|
+
headers,
|
|
2655
|
+
body: JSON.stringify(activity),
|
|
2656
|
+
redirect: "manual"
|
|
2657
|
+
});
|
|
2658
|
+
let rsaKey = null;
|
|
2659
|
+
for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
|
|
2660
|
+
rsaKey = key;
|
|
2661
|
+
break;
|
|
2673
2662
|
}
|
|
2674
|
-
if (
|
|
2663
|
+
if (rsaKey == null && keys.length < 1) logger.debug("Sending the activity {activityId} to {inbox} without a signature.", {
|
|
2664
|
+
activityId,
|
|
2665
|
+
inbox: inbox.href
|
|
2666
|
+
});
|
|
2667
|
+
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.", {
|
|
2668
|
+
inbox: inbox.href,
|
|
2669
|
+
keys: keys.map((pair) => ({
|
|
2670
|
+
keyId: pair.keyId.href,
|
|
2671
|
+
privateKey: pair.privateKey
|
|
2672
|
+
}))
|
|
2673
|
+
});
|
|
2674
|
+
let response;
|
|
2675
|
+
try {
|
|
2676
|
+
response = rsaKey == null ? await fetchWithValidatedRedirects(request, validateUrl) : await require_http.doubleKnock(request, rsaKey, {
|
|
2677
|
+
tracerProvider,
|
|
2678
|
+
specDeterminer,
|
|
2679
|
+
validateRedirect: validateUrl
|
|
2680
|
+
});
|
|
2681
|
+
} catch (error) {
|
|
2682
|
+
const failure = error instanceof _fedify_vocab_runtime.FetchError || error instanceof _fedify_vocab_runtime.UrlError ? error : createFetchError(inbox.href, error);
|
|
2683
|
+
logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
|
|
2684
|
+
activityId,
|
|
2685
|
+
inbox: inbox.href,
|
|
2686
|
+
error: failure
|
|
2687
|
+
});
|
|
2688
|
+
federationMetrics.recordDelivery(inbox, require_http.getDurationMs(started), false, activityType);
|
|
2689
|
+
throw failure;
|
|
2690
|
+
}
|
|
2691
|
+
try {
|
|
2692
|
+
if (!response.ok) {
|
|
2693
|
+
let error;
|
|
2694
|
+
try {
|
|
2695
|
+
error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
|
|
2696
|
+
} catch (_) {
|
|
2697
|
+
error = "";
|
|
2698
|
+
}
|
|
2699
|
+
logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
|
|
2700
|
+
activityId,
|
|
2701
|
+
inbox: inbox.href,
|
|
2702
|
+
status: response.status,
|
|
2703
|
+
statusText: response.statusText,
|
|
2704
|
+
error
|
|
2705
|
+
});
|
|
2706
|
+
throw new SendActivityError(inbox, response.status, `Failed to send activity ${activityId} to ${inbox.href} (${response.status} ${response.statusText}):\n${error}`, error, response.headers);
|
|
2707
|
+
}
|
|
2708
|
+
deliverySuccess = true;
|
|
2709
|
+
const eventAttributes = {
|
|
2710
|
+
"activitypub.inbox.url": inbox.href,
|
|
2711
|
+
"activitypub.activity.id": activityId ?? ""
|
|
2712
|
+
};
|
|
2713
|
+
if (activityType != null) eventAttributes["activitypub.activity.type"] = activityType;
|
|
2714
|
+
const actorId = getActivityActorId(activity);
|
|
2715
|
+
if (actorId != null) eventAttributes["activitypub.actor.id"] = actorId;
|
|
2716
|
+
span.addEvent("activitypub.activity.sent", eventAttributes);
|
|
2717
|
+
} finally {
|
|
2718
|
+
federationMetrics.recordDelivery(inbox, require_http.getDurationMs(started), deliverySuccess, activityType);
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
function createFetchError(url, cause) {
|
|
2722
|
+
const error = new _fedify_vocab_runtime.FetchError(url, cause instanceof Error ? cause.message : String(cause));
|
|
2723
|
+
error.cause = cause;
|
|
2724
|
+
return error;
|
|
2725
|
+
}
|
|
2726
|
+
async function fetchWithValidatedRedirects(request, validateUrl) {
|
|
2727
|
+
let body = await request.clone().text();
|
|
2728
|
+
for (let redirects = 0;; redirects++) {
|
|
2729
|
+
const response = await fetch(request, { redirect: "manual" });
|
|
2730
|
+
const location = response.headers.get("Location");
|
|
2731
|
+
if (![
|
|
2732
|
+
301,
|
|
2733
|
+
302,
|
|
2734
|
+
303,
|
|
2735
|
+
307,
|
|
2736
|
+
308
|
|
2737
|
+
].includes(response.status) || location == null) return response;
|
|
2738
|
+
await response.body?.cancel();
|
|
2739
|
+
if (redirects >= 20) throw new TypeError("Too many redirects");
|
|
2740
|
+
const url = new URL(location, request.url);
|
|
2741
|
+
await validateUrl(url.href);
|
|
2742
|
+
const headers = new Headers(request.headers);
|
|
2743
|
+
if (url.origin !== new URL(request.url).origin) {
|
|
2744
|
+
headers.delete("Authorization");
|
|
2745
|
+
headers.delete("Proxy-Authorization");
|
|
2746
|
+
headers.delete("Cookie");
|
|
2747
|
+
headers.delete("Host");
|
|
2748
|
+
}
|
|
2749
|
+
let method = request.method;
|
|
2750
|
+
if ((response.status === 301 || response.status === 302) && method === "POST" || response.status === 303 && method !== "GET" && method !== "HEAD") {
|
|
2751
|
+
method = "GET";
|
|
2752
|
+
body = void 0;
|
|
2753
|
+
for (const name of [
|
|
2754
|
+
"Content-Encoding",
|
|
2755
|
+
"Content-Language",
|
|
2756
|
+
"Content-Length",
|
|
2757
|
+
"Content-Location",
|
|
2758
|
+
"Content-Type"
|
|
2759
|
+
]) headers.delete(name);
|
|
2760
|
+
}
|
|
2761
|
+
request = new Request(url, {
|
|
2762
|
+
method,
|
|
2763
|
+
headers,
|
|
2764
|
+
body,
|
|
2765
|
+
redirect: "manual"
|
|
2766
|
+
});
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
/**
|
|
2770
|
+
* An error that is thrown when an activity fails to send to a remote inbox.
|
|
2771
|
+
* It contains structured information about the failure, including the HTTP
|
|
2772
|
+
* status code, the inbox URL, and the response body.
|
|
2773
|
+
* @since 2.0.0
|
|
2774
|
+
*/
|
|
2775
|
+
var SendActivityError = class extends Error {
|
|
2776
|
+
/**
|
|
2777
|
+
* The inbox URL that the activity was being sent to.
|
|
2778
|
+
*/
|
|
2779
|
+
inbox;
|
|
2780
|
+
/**
|
|
2781
|
+
* The HTTP status code returned by the inbox.
|
|
2782
|
+
*/
|
|
2783
|
+
statusCode;
|
|
2784
|
+
/**
|
|
2785
|
+
* The response body from the inbox, if any. Note that this may be
|
|
2786
|
+
* truncated to a maximum of 1 KiB to prevent excessive memory consumption
|
|
2787
|
+
* when remote servers return large error pages (e.g., Cloudflare error pages).
|
|
2788
|
+
* If truncated, the string will end with `"… (truncated)"`.
|
|
2789
|
+
*/
|
|
2790
|
+
responseBody;
|
|
2791
|
+
/**
|
|
2792
|
+
* The response headers from the inbox.
|
|
2793
|
+
* @since 2.3.0
|
|
2794
|
+
*/
|
|
2795
|
+
responseHeaders;
|
|
2796
|
+
/**
|
|
2797
|
+
* Creates a new {@link SendActivityError}.
|
|
2798
|
+
* @param inbox The inbox URL.
|
|
2799
|
+
* @param statusCode The HTTP status code.
|
|
2800
|
+
* @param message The error message.
|
|
2801
|
+
* @param responseBody The response body.
|
|
2802
|
+
* @param responseHeaders The response headers.
|
|
2803
|
+
*/
|
|
2804
|
+
constructor(inbox, statusCode, message, responseBody, responseHeaders) {
|
|
2805
|
+
super(message);
|
|
2806
|
+
this.name = "SendActivityError";
|
|
2807
|
+
this.inbox = inbox;
|
|
2808
|
+
this.statusCode = statusCode;
|
|
2809
|
+
this.responseBody = responseBody;
|
|
2810
|
+
this.responseHeaders = new Headers(responseHeaders);
|
|
2811
|
+
}
|
|
2812
|
+
};
|
|
2813
|
+
//#endregion
|
|
2814
|
+
//#region src/federation/portable-inbox.ts
|
|
2815
|
+
const Temporal$8 = require_http.Temporal;
|
|
2816
|
+
const MAX_TIMEOUT_MS = 2 ** 31 - 1;
|
|
2817
|
+
/**
|
|
2818
|
+
* Fills in the defaults of {@link PortableInboxForwardingOptions} and
|
|
2819
|
+
* validates them.
|
|
2820
|
+
* @param options The options.
|
|
2821
|
+
* @returns The resolved options.
|
|
2822
|
+
* @throws {RangeError} If `maxTargets` is not a non-negative integer, if `ttl`
|
|
2823
|
+
* is not positive, if `deadline` is negative or longer
|
|
2824
|
+
* than 2,147,483,647 milliseconds (about 24.8 days), or
|
|
2825
|
+
* if either duration has calendar units, i.e., weeks,
|
|
2826
|
+
* months, or years, whose length depends on the date.
|
|
2827
|
+
*/
|
|
2828
|
+
function resolvePortableInboxForwardingOptions(options = {}) {
|
|
2829
|
+
const maxTargets = options.maxTargets ?? 10;
|
|
2830
|
+
if (!Number.isInteger(maxTargets) || maxTargets < 0) throw new RangeError("portableInboxForwarding.maxTargets must be a non-negative integer.");
|
|
2831
|
+
const ttl = Temporal$8.Duration.from(options.ttl ?? { days: 30 });
|
|
2832
|
+
if (ttl.total("millisecond") <= 0) throw new RangeError("portableInboxForwarding.ttl must be positive.");
|
|
2833
|
+
const deadline = Temporal$8.Duration.from(options.deadline ?? { seconds: 10 });
|
|
2834
|
+
const deadlineMs = deadline.total("millisecond");
|
|
2835
|
+
if (deadlineMs < 0 || deadlineMs > MAX_TIMEOUT_MS) throw new RangeError(`portableInboxForwarding.deadline must be between 0 and ${MAX_TIMEOUT_MS} milliseconds.`);
|
|
2836
|
+
return {
|
|
2837
|
+
maxTargets,
|
|
2838
|
+
ttl,
|
|
2839
|
+
deadline
|
|
2840
|
+
};
|
|
2841
|
+
}
|
|
2842
|
+
/**
|
|
2843
|
+
* Decides whether this server accepts a delivery to a portable inbox on behalf
|
|
2844
|
+
* of the given actor, which the actor dispatcher returned for the identifier
|
|
2845
|
+
* in the requested inbox path.
|
|
2846
|
+
*
|
|
2847
|
+
* The actor document comes from the application, so it is trusted as is and
|
|
2848
|
+
* its proof is not verified here.
|
|
2849
|
+
* @param actor The actor the actor dispatcher returned.
|
|
2850
|
+
* @param options The requested DID, the canonical form of the requested inbox
|
|
2851
|
+
* ID, and the origin of this server.
|
|
2852
|
+
* @returns The resolution.
|
|
2853
|
+
*/
|
|
2854
|
+
function resolvePortableInboxRecipient(actor, { authority, canonicalInboxId, localOrigin }) {
|
|
2855
|
+
if (actor == null) return {
|
|
2856
|
+
status: "rejected",
|
|
2857
|
+
reason: "notFound"
|
|
2858
|
+
};
|
|
2859
|
+
const id = actor.id;
|
|
2860
|
+
const did = id == null || !require_http.isPortableId(id) ? null : require_http.getPortableDid(id);
|
|
2861
|
+
if (id == null || did == null) return {
|
|
2862
|
+
status: "rejected",
|
|
2863
|
+
reason: "notPortable"
|
|
2864
|
+
};
|
|
2865
|
+
if (did !== authority) return {
|
|
2866
|
+
status: "rejected",
|
|
2867
|
+
reason: "authorityMismatch"
|
|
2868
|
+
};
|
|
2869
|
+
const inboxId = actor.inboxId == null ? null : toPortableUri(actor.inboxId);
|
|
2870
|
+
if (inboxId == null || require_http.getCanonicalPortableId(inboxId) !== canonicalInboxId) return {
|
|
2871
|
+
status: "rejected",
|
|
2872
|
+
reason: "inboxMismatch"
|
|
2873
|
+
};
|
|
2874
|
+
const gateways = actor.gateways;
|
|
2875
|
+
if (!gateways.some((g) => (0, _fedify_vocab_runtime.isGatewayUrl)(g) && g.origin === localOrigin)) return {
|
|
2876
|
+
status: "rejected",
|
|
2877
|
+
reason: "notGateway"
|
|
2878
|
+
};
|
|
2879
|
+
return {
|
|
2880
|
+
status: "accepted",
|
|
2881
|
+
recipient: {
|
|
2882
|
+
actorId: id,
|
|
2883
|
+
inboxId,
|
|
2884
|
+
canonicalInboxId,
|
|
2885
|
+
gateways
|
|
2886
|
+
}
|
|
2887
|
+
};
|
|
2888
|
+
}
|
|
2889
|
+
/**
|
|
2890
|
+
* Gets the portable form of an `ap:` or `ap+ef61:` URI or a compatible
|
|
2891
|
+
* identifier, keeping its query.
|
|
2892
|
+
* @returns The portable URI, or `null` if the ID is neither, or is malformed.
|
|
2893
|
+
*/
|
|
2894
|
+
function toPortableUri(id) {
|
|
2895
|
+
if (require_http.isPortableUri(id)) return id;
|
|
2896
|
+
try {
|
|
2897
|
+
return (0, _fedify_vocab_runtime.fromCompatibleEf61Id)(id);
|
|
2898
|
+
} catch (error) {
|
|
2899
|
+
if (error instanceof TypeError) return null;
|
|
2900
|
+
throw error;
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
const RFC9421_DELIVERY_COMPONENTS = [
|
|
2904
|
+
"@method",
|
|
2905
|
+
"@target-uri",
|
|
2906
|
+
"content-digest"
|
|
2907
|
+
];
|
|
2908
|
+
const DRAFT_DELIVERY_COMPONENTS = [
|
|
2909
|
+
"(request-target)",
|
|
2910
|
+
"host",
|
|
2911
|
+
"digest"
|
|
2912
|
+
];
|
|
2913
|
+
/**
|
|
2914
|
+
* Checks whether an HTTP Signature covers the method, the target URI, and the
|
|
2915
|
+
* body of a request, which both Fedify and Mitra sign when forwarding.
|
|
2916
|
+
* @param signature The signature.
|
|
2917
|
+
* @returns `true` if the signature covers them.
|
|
2918
|
+
*/
|
|
2919
|
+
function coversDelivery(signature) {
|
|
2920
|
+
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));
|
|
2921
|
+
}
|
|
2922
|
+
/**
|
|
2923
|
+
* Forwards an activity received in a portable inbox to the inboxes of the same
|
|
2924
|
+
* portable actor on its other gateways, as FEP-ef61 recommends.
|
|
2925
|
+
*
|
|
2926
|
+
* FEP-ef61 forbids forwarding an activity from an inbox more than once, so
|
|
2927
|
+
* each target gateway is claimed with an atomic compare-and-swap before the
|
|
2928
|
+
* activity is handed off, and a claim is never released, even if the hand-off
|
|
2929
|
+
* fails, since a failed hand-off does not prove that nothing was delivered.
|
|
2930
|
+
* Forwarding is therefore best effort. Without {@link KvStore.cas}, nothing is
|
|
2931
|
+
* forwarded.
|
|
2932
|
+
*
|
|
2933
|
+
* Forwarded requests are signed with HTTP Signatures by the RSA key among
|
|
2934
|
+
* {@link ForwardPortableInboxActivityParameters.getKeys}, if any, so that
|
|
2935
|
+
* servers requiring HTTP Signatures accept them, and are sent unsigned
|
|
2936
|
+
* otherwise. Either way, the receiving gateways authenticate the activity by
|
|
2937
|
+
* its own proof.
|
|
2938
|
+
*
|
|
2939
|
+
* If the delivery is signed with a gateway key of the recipient by one of its
|
|
2940
|
+
* other gateways, that gateway has the activity already, so it is claimed
|
|
2941
|
+
* without forwarding the activity to it. See {@link identifySendingGateway}.
|
|
2942
|
+
* @param parameters The parameters.
|
|
2943
|
+
* @returns The target inbox URLs that the activity was handed off to.
|
|
2944
|
+
*/
|
|
2945
|
+
async function forwardPortableInboxActivity(parameters) {
|
|
2946
|
+
const logger = (0, _logtape_logtape.getLogger)([
|
|
2947
|
+
"fedify",
|
|
2948
|
+
"federation",
|
|
2949
|
+
"inbox"
|
|
2950
|
+
]);
|
|
2951
|
+
const { recipient, activityId, excludedOrigins, kv, kvPrefix, outboxQueue } = parameters;
|
|
2952
|
+
const options = parameters.options ?? resolvePortableInboxForwardingOptions();
|
|
2953
|
+
const { maxTargets, ttl } = options;
|
|
2954
|
+
if (kv.cas == null || maxTargets < 1) return [];
|
|
2955
|
+
const deadline = Date.now() + options.deadline.total("millisecond");
|
|
2956
|
+
const canonicalActivityId = require_http.getCanonicalPortableId(activityId) ?? activityId.href;
|
|
2957
|
+
const claimKey = (gateway) => [
|
|
2958
|
+
...kvPrefix,
|
|
2959
|
+
recipient.canonicalInboxId,
|
|
2960
|
+
canonicalActivityId,
|
|
2961
|
+
gateway
|
|
2962
|
+
];
|
|
2963
|
+
let targets = getForwardingTargets(recipient, excludedOrigins);
|
|
2964
|
+
const sender = await identifySendingGateway(parameters, targets.map((t) => t.gateway), claimKey, deadline);
|
|
2965
|
+
if (sender != null) {
|
|
2966
|
+
logger.debug("Not forwarding activity {activityId} from the portable inbox {inbox} back to the gateway {gateway}, which delivered it.", {
|
|
2967
|
+
activityId: activityId.href,
|
|
2968
|
+
inbox: recipient.canonicalInboxId,
|
|
2969
|
+
gateway: sender
|
|
2970
|
+
});
|
|
2971
|
+
await kv.cas(claimKey(sender), void 0, true, { ttl });
|
|
2972
|
+
targets = targets.filter((t) => t.gateway !== sender);
|
|
2973
|
+
}
|
|
2974
|
+
if (targets.length > maxTargets) {
|
|
2975
|
+
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.", {
|
|
2976
|
+
inbox: recipient.canonicalInboxId,
|
|
2977
|
+
activityId: activityId.href,
|
|
2978
|
+
max: maxTargets
|
|
2979
|
+
});
|
|
2980
|
+
targets.length = maxTargets;
|
|
2981
|
+
}
|
|
2982
|
+
const claimed = [];
|
|
2983
|
+
for (const { gateway, inbox } of targets) if (await kv.cas(claimKey(gateway), void 0, true, { ttl })) claimed.push(inbox);
|
|
2984
|
+
if (claimed.length < 1) return [];
|
|
2985
|
+
logger.debug("Forwarding activity {activityId} from the portable inbox {inbox} to other gateways:\n{targets}", {
|
|
2986
|
+
activityId: activityId.href,
|
|
2987
|
+
inbox: recipient.canonicalInboxId,
|
|
2988
|
+
targets: claimed.map((t) => t.href)
|
|
2989
|
+
});
|
|
2990
|
+
if (outboxQueue == null) await forwardImmediately(parameters, claimed, deadline);
|
|
2991
|
+
else await enqueueForwarding(parameters, outboxQueue, claimed);
|
|
2992
|
+
return claimed;
|
|
2993
|
+
}
|
|
2994
|
+
/**
|
|
2995
|
+
* Identifies the gateway of the recipient that forwarded a delivery to its
|
|
2996
|
+
* portable inbox, by the delivery's HTTP Signature made with the recipient's
|
|
2997
|
+
* gateway key, which FEP-ef61 has each gateway use for requests on behalf of
|
|
2998
|
+
* an actor. Since the activity is authenticated by its own proof, the
|
|
2999
|
+
* signature only tells which gateway to skip, and a delivery whose signature
|
|
3000
|
+
* is invalid or cannot be verified is still forwarded to every gateway.
|
|
3001
|
+
*
|
|
3002
|
+
* Verifying the signature fetches the recipient's actor document from the
|
|
3003
|
+
* sending gateway, as gateway keys are not cached, so only the first signature
|
|
3004
|
+
* that may save a request is verified: one that covers the delivery and names
|
|
3005
|
+
* a gateway key of the recipient on one of the forwarding targets that the
|
|
3006
|
+
* activity has not been forwarded to yet.
|
|
3007
|
+
* @param parameters The forwarding parameters.
|
|
3008
|
+
* @param targets The origins of the gateways to forward to.
|
|
3009
|
+
* @param claimKey Gets the key of the claim for a gateway.
|
|
3010
|
+
* @param deadline The time by which identification has to finish, in
|
|
3011
|
+
* milliseconds since the epoch; afterwards, the sending
|
|
3012
|
+
* gateway is left unidentified.
|
|
3013
|
+
* @returns The origin of the sending gateway, or `null` if it is not
|
|
3014
|
+
* identified.
|
|
3015
|
+
*/
|
|
3016
|
+
async function identifySendingGateway({ recipient, activityId, kv, signatures }, targets, claimKey, deadline) {
|
|
3017
|
+
if (signatures == null || signatures.length < 1) return null;
|
|
3018
|
+
const logger = (0, _logtape_logtape.getLogger)([
|
|
3019
|
+
"fedify",
|
|
3020
|
+
"federation",
|
|
3021
|
+
"inbox"
|
|
3022
|
+
]);
|
|
3023
|
+
const actorId = require_http.getCanonicalPortableId(recipient.actorId);
|
|
3024
|
+
if (actorId == null) return null;
|
|
3025
|
+
const isGatewayKeyOfRecipient = (keyId) => {
|
|
3026
|
+
if (!require_http.isCompatibleKeyId(keyId) || !targets.includes(keyId.origin)) return false;
|
|
3027
|
+
const base = new URL(keyId.href);
|
|
3028
|
+
base.hash = "";
|
|
3029
|
+
return require_http.getCanonicalPortableId(base) === actorId;
|
|
3030
|
+
};
|
|
3031
|
+
let signature;
|
|
3032
|
+
for (const candidate of signatures) if (candidate.coversDelivery && isGatewayKeyOfRecipient(candidate.keyId) && await kv.get(claimKey(candidate.keyId.origin)) === void 0) {
|
|
3033
|
+
signature = candidate;
|
|
3034
|
+
break;
|
|
3035
|
+
}
|
|
3036
|
+
if (signature == null) return null;
|
|
3037
|
+
const { keyId } = signature;
|
|
3038
|
+
const logProperties = {
|
|
3039
|
+
activityId: activityId.href,
|
|
3040
|
+
inbox: recipient.canonicalInboxId,
|
|
3041
|
+
keyId: keyId.href
|
|
3042
|
+
};
|
|
3043
|
+
const verification = Promise.resolve().then(() => signature.verify()).then((key) => {
|
|
3044
|
+
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);
|
|
3045
|
+
return key;
|
|
3046
|
+
}, (error) => {
|
|
3047
|
+
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}", {
|
|
3048
|
+
...logProperties,
|
|
3049
|
+
error
|
|
3050
|
+
});
|
|
3051
|
+
return null;
|
|
3052
|
+
});
|
|
3053
|
+
let timer;
|
|
3054
|
+
const key = await Promise.race([verification, new Promise((resolve) => {
|
|
3055
|
+
timer = setTimeout(() => {
|
|
3056
|
+
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);
|
|
3057
|
+
resolve(null);
|
|
3058
|
+
}, Math.max(0, deadline - Date.now()));
|
|
3059
|
+
})]);
|
|
3060
|
+
clearTimeout(timer);
|
|
3061
|
+
if (key == null) return null;
|
|
3062
|
+
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) {
|
|
3063
|
+
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);
|
|
3064
|
+
return null;
|
|
3065
|
+
}
|
|
3066
|
+
return key.id.origin;
|
|
3067
|
+
}
|
|
3068
|
+
function getForwardingTargets(recipient, excludedOrigins) {
|
|
3069
|
+
const logger = (0, _logtape_logtape.getLogger)([
|
|
3070
|
+
"fedify",
|
|
3071
|
+
"federation",
|
|
3072
|
+
"inbox"
|
|
3073
|
+
]);
|
|
3074
|
+
const seen = new Set(excludedOrigins);
|
|
3075
|
+
const targets = [];
|
|
3076
|
+
for (const gateway of recipient.gateways) {
|
|
3077
|
+
if (!(0, _fedify_vocab_runtime.isGatewayUrl)(gateway) || seen.has(gateway.origin)) continue;
|
|
3078
|
+
seen.add(gateway.origin);
|
|
3079
|
+
let inbox;
|
|
3080
|
+
try {
|
|
3081
|
+
inbox = (0, _fedify_vocab_runtime.toCompatibleEf61Id)(recipient.inboxId, gateway.origin);
|
|
3082
|
+
} catch (error) {
|
|
3083
|
+
if (!(error instanceof TypeError)) throw error;
|
|
3084
|
+
logger.warn("Cannot forward to the gateway {gateway}, as the portable inbox {inbox} cannot be represented as its compatible identifier.", {
|
|
3085
|
+
gateway: gateway.origin,
|
|
3086
|
+
inbox: recipient.canonicalInboxId
|
|
3087
|
+
});
|
|
3088
|
+
continue;
|
|
3089
|
+
}
|
|
3090
|
+
targets.push({
|
|
3091
|
+
gateway: gateway.origin,
|
|
3092
|
+
inbox
|
|
3093
|
+
});
|
|
3094
|
+
}
|
|
3095
|
+
return targets;
|
|
3096
|
+
}
|
|
3097
|
+
/**
|
|
3098
|
+
* Resolves the key that signs forwarded requests: the first RSASSA-PKCS1-v1_5
|
|
3099
|
+
* key, as {@link sendActivity} signs requests only with such a key. Since
|
|
3100
|
+
* the targets have already been claimed, the activity is forwarded unsigned
|
|
3101
|
+
* rather than not at all if the key cannot be resolved or used.
|
|
3102
|
+
*/
|
|
3103
|
+
async function resolveForwardingKey({ getKeys, activityId }) {
|
|
3104
|
+
if (getKeys == null) return null;
|
|
3105
|
+
const logger = (0, _logtape_logtape.getLogger)([
|
|
3106
|
+
"fedify",
|
|
3107
|
+
"federation",
|
|
3108
|
+
"inbox"
|
|
3109
|
+
]);
|
|
3110
|
+
let key;
|
|
3111
|
+
try {
|
|
3112
|
+
key = (await getKeys()).find((k) => k.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5");
|
|
3113
|
+
} catch (error) {
|
|
3114
|
+
logger.error("Failed to get the gateway keys to sign forwarded requests with; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
|
|
3115
|
+
activityId: activityId.href,
|
|
3116
|
+
error
|
|
3117
|
+
});
|
|
3118
|
+
return null;
|
|
3119
|
+
}
|
|
3120
|
+
if (key == null) return null;
|
|
3121
|
+
try {
|
|
3122
|
+
require_http.validateCryptoKey(key.privateKey, "private");
|
|
3123
|
+
} catch (error) {
|
|
3124
|
+
if (!(error instanceof TypeError)) throw error;
|
|
3125
|
+
logger.warn("The gateway key {keyId} cannot sign forwarded requests; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
|
|
3126
|
+
keyId: key.keyId.href,
|
|
3127
|
+
activityId: activityId.href,
|
|
3128
|
+
error
|
|
3129
|
+
});
|
|
3130
|
+
return null;
|
|
3131
|
+
}
|
|
3132
|
+
return key;
|
|
3133
|
+
}
|
|
3134
|
+
async function forwardImmediately(parameters, inboxes, deadlineAt) {
|
|
3135
|
+
const { activity, activityId, activityType, allowPrivateAddress, specDeterminer, meterProvider, tracerProvider } = parameters;
|
|
3136
|
+
const logger = (0, _logtape_logtape.getLogger)([
|
|
3137
|
+
"fedify",
|
|
3138
|
+
"federation",
|
|
3139
|
+
"inbox"
|
|
3140
|
+
]);
|
|
3141
|
+
const deadline = Math.max(0, deadlineAt - Date.now());
|
|
3142
|
+
const forwarding = resolveForwardingKey(parameters).then((key) => Promise.all(inboxes.map((inbox) => sendActivity({
|
|
3143
|
+
activity,
|
|
3144
|
+
activityId: activityId.href,
|
|
3145
|
+
activityType,
|
|
3146
|
+
keys: key == null ? [] : [key],
|
|
3147
|
+
inbox,
|
|
3148
|
+
allowPrivateAddress,
|
|
3149
|
+
specDeterminer,
|
|
3150
|
+
meterProvider,
|
|
3151
|
+
tracerProvider
|
|
3152
|
+
}).catch((error) => {
|
|
3153
|
+
logger.error("Failed to forward activity {activityId} to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
|
|
3154
|
+
activityId: activityId.href,
|
|
3155
|
+
inbox: inbox.href,
|
|
3156
|
+
error
|
|
3157
|
+
});
|
|
3158
|
+
}))));
|
|
3159
|
+
let timer;
|
|
3160
|
+
const timedOut = await Promise.race([forwarding.then(() => false), new Promise((resolve) => {
|
|
3161
|
+
timer = setTimeout(() => resolve(true), deadline);
|
|
3162
|
+
})]);
|
|
3163
|
+
clearTimeout(timer);
|
|
3164
|
+
if (timedOut) logger.warn("Forwarding activity {activityId} to other gateways did not finish in {deadline} ms; responding to the delivery without waiting for it.", {
|
|
3165
|
+
activityId: activityId.href,
|
|
3166
|
+
deadline
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
3169
|
+
async function enqueueForwarding(parameters, outboxQueue, inboxes) {
|
|
3170
|
+
const { activity, activityId, activityType, baseUrl, startQueue, meterProvider } = parameters;
|
|
3171
|
+
const logger = (0, _logtape_logtape.getLogger)([
|
|
3172
|
+
"fedify",
|
|
3173
|
+
"federation",
|
|
3174
|
+
"inbox"
|
|
3175
|
+
]);
|
|
3176
|
+
const key = await resolveForwardingKey(parameters);
|
|
3177
|
+
const keys = [];
|
|
3178
|
+
if (key != null) try {
|
|
3179
|
+
keys.push({
|
|
3180
|
+
keyId: key.keyId.href,
|
|
3181
|
+
privateKey: await require_http.exportJwk(key.privateKey)
|
|
3182
|
+
});
|
|
3183
|
+
} catch (error) {
|
|
3184
|
+
logger.error("Failed to export the gateway key {keyId}; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
|
|
3185
|
+
keyId: key.keyId.href,
|
|
3186
|
+
activityId: activityId.href,
|
|
3187
|
+
error
|
|
3188
|
+
});
|
|
3189
|
+
}
|
|
3190
|
+
startQueue?.();
|
|
3191
|
+
const started = (/* @__PURE__ */ new Date()).toISOString();
|
|
3192
|
+
const traceContext = {};
|
|
3193
|
+
_opentelemetry_api.propagation.inject(_opentelemetry_api.context.active(), traceContext);
|
|
3194
|
+
await Promise.all(inboxes.map(async (inbox) => {
|
|
3195
|
+
const message = {
|
|
3196
|
+
type: "outbox",
|
|
3197
|
+
id: crypto.randomUUID(),
|
|
3198
|
+
baseUrl,
|
|
3199
|
+
keys,
|
|
3200
|
+
activity,
|
|
3201
|
+
activityId: activityId.href,
|
|
3202
|
+
activityType,
|
|
3203
|
+
inbox: inbox.href,
|
|
3204
|
+
sharedInbox: false,
|
|
3205
|
+
started,
|
|
3206
|
+
attempt: 0,
|
|
3207
|
+
headers: {},
|
|
3208
|
+
traceContext
|
|
3209
|
+
};
|
|
3210
|
+
try {
|
|
3211
|
+
await outboxQueue.enqueue(message);
|
|
3212
|
+
} catch (error) {
|
|
3213
|
+
logger.error("Failed to enqueue activity {activityId} to forward to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
|
|
3214
|
+
activityId: activityId.href,
|
|
3215
|
+
inbox: inbox.href,
|
|
3216
|
+
error
|
|
3217
|
+
});
|
|
3218
|
+
return;
|
|
3219
|
+
}
|
|
3220
|
+
require_http.recordOutboxEnqueue(meterProvider, outboxQueue, message);
|
|
3221
|
+
}));
|
|
3222
|
+
}
|
|
3223
|
+
//#endregion
|
|
3224
|
+
//#region src/federation/temporal.ts
|
|
3225
|
+
const Temporal$7 = require_http.Temporal;
|
|
3226
|
+
function isPlainObject$1(value) {
|
|
3227
|
+
return typeof value === "object" && value != null && !Array.isArray(value);
|
|
3228
|
+
}
|
|
3229
|
+
function normalizeDateTimeLiteral(value) {
|
|
3230
|
+
return value.substring(19).match(/[Z+-]/) ? value : value + "Z";
|
|
3231
|
+
}
|
|
3232
|
+
function isMalformedDateTimeLiteral(value) {
|
|
3233
|
+
if (typeof value !== "string") return false;
|
|
3234
|
+
try {
|
|
3235
|
+
Temporal$7.Instant.from(normalizeDateTimeLiteral(value));
|
|
3236
|
+
return false;
|
|
3237
|
+
} catch {
|
|
3238
|
+
return true;
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
function isMalformedDurationLiteral(value) {
|
|
3242
|
+
if (typeof value !== "string") return false;
|
|
3243
|
+
try {
|
|
3244
|
+
Temporal$7.Duration.from(value);
|
|
3245
|
+
return false;
|
|
3246
|
+
} catch {
|
|
3247
|
+
return true;
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
const TEMPORAL_DATE_TIME_IRIS = /* @__PURE__ */ new Set([
|
|
3251
|
+
"https://www.w3.org/ns/activitystreams#deleted",
|
|
3252
|
+
"https://www.w3.org/ns/activitystreams#endTime",
|
|
3253
|
+
"https://www.w3.org/ns/activitystreams#published",
|
|
3254
|
+
"https://www.w3.org/ns/activitystreams#startTime",
|
|
3255
|
+
"https://www.w3.org/ns/activitystreams#updated",
|
|
3256
|
+
"http://purl.org/dc/terms/created",
|
|
3257
|
+
"https://w3id.org/security#created"
|
|
3258
|
+
]);
|
|
3259
|
+
const TEMPORAL_DURATION_IRIS = /* @__PURE__ */ new Set(["https://www.w3.org/ns/activitystreams#duration"]);
|
|
3260
|
+
const QUESTION_CLOSED_IRI = "https://www.w3.org/ns/activitystreams#closed";
|
|
3261
|
+
const XSD_DATE_TIME_IRI = "http://www.w3.org/2001/XMLSchema#dateTime";
|
|
3262
|
+
function hasMalformedExpandedDateTimeLiteral(value) {
|
|
3263
|
+
if (Array.isArray(value)) return value.some(hasMalformedExpandedDateTimeLiteral);
|
|
3264
|
+
return isPlainObject$1(value) && "@value" in value && isMalformedDateTimeLiteral(value["@value"]);
|
|
3265
|
+
}
|
|
3266
|
+
function hasMalformedExpandedQuestionClosedLiteral(value) {
|
|
3267
|
+
if (Array.isArray(value)) return value.some(hasMalformedExpandedQuestionClosedLiteral);
|
|
3268
|
+
if (!isPlainObject$1(value) || !("@value" in value)) return false;
|
|
3269
|
+
const literal = value["@value"];
|
|
3270
|
+
if (typeof literal === "boolean") return false;
|
|
3271
|
+
if (typeof literal !== "string") return false;
|
|
3272
|
+
if (value["@type"] !== XSD_DATE_TIME_IRI) return false;
|
|
3273
|
+
if (new Date(literal).toString() === "Invalid Date") return false;
|
|
3274
|
+
return isMalformedDateTimeLiteral(literal);
|
|
3275
|
+
}
|
|
3276
|
+
function hasMalformedExpandedDurationLiteral(value) {
|
|
3277
|
+
if (Array.isArray(value)) return value.some(hasMalformedExpandedDurationLiteral);
|
|
3278
|
+
return isPlainObject$1(value) && "@value" in value && isMalformedDurationLiteral(value["@value"]);
|
|
3279
|
+
}
|
|
3280
|
+
function hasMalformedKnownTemporalLiteralInternal(value, visited) {
|
|
3281
|
+
if (Array.isArray(value)) return value.some((item) => hasMalformedKnownTemporalLiteralInternal(item, visited));
|
|
3282
|
+
if (!isPlainObject$1(value)) return false;
|
|
3283
|
+
if (visited.has(value)) return false;
|
|
3284
|
+
visited.add(value);
|
|
3285
|
+
if ("@value" in value) return false;
|
|
3286
|
+
for (const [key, child] of Object.entries(value)) {
|
|
3287
|
+
if (TEMPORAL_DATE_TIME_IRIS.has(key)) {
|
|
3288
|
+
if (hasMalformedExpandedDateTimeLiteral(child)) return true;
|
|
3289
|
+
continue;
|
|
3290
|
+
}
|
|
3291
|
+
if (key === QUESTION_CLOSED_IRI) {
|
|
3292
|
+
if (hasMalformedExpandedQuestionClosedLiteral(child)) return true;
|
|
3293
|
+
continue;
|
|
3294
|
+
}
|
|
3295
|
+
if (TEMPORAL_DURATION_IRIS.has(key)) {
|
|
3296
|
+
if (hasMalformedExpandedDurationLiteral(child)) return true;
|
|
3297
|
+
continue;
|
|
3298
|
+
}
|
|
3299
|
+
if (hasMalformedKnownTemporalLiteralInternal(child, visited)) return true;
|
|
3300
|
+
}
|
|
3301
|
+
return false;
|
|
3302
|
+
}
|
|
3303
|
+
async function hasMalformedKnownTemporalLiteral(value, contextLoader) {
|
|
3304
|
+
try {
|
|
3305
|
+
return hasMalformedKnownTemporalLiteralInternal(await _fedify_vocab_runtime_jsonld.default.expand(value, {
|
|
3306
|
+
documentLoader: require_http.getNormalizationContextLoader(contextLoader),
|
|
3307
|
+
keepFreeFloatingNodes: true
|
|
3308
|
+
}), /* @__PURE__ */ new Set());
|
|
3309
|
+
} catch {
|
|
3310
|
+
return false;
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
//#endregion
|
|
3314
|
+
//#region src/federation/handler.ts
|
|
3315
|
+
const Temporal$6 = require_http.Temporal;
|
|
3316
|
+
const rawInboxContextFactorySymbol = Symbol("fedify.rawInboxContextFactory");
|
|
3317
|
+
const INBOX_COMPOUND_PROOF_LIMITS = {
|
|
3318
|
+
maxDepth: 64,
|
|
3319
|
+
maxMaps: 1e4,
|
|
3320
|
+
maxProofs: 32,
|
|
3321
|
+
maxBytes: 10 * 1024 * 1024
|
|
3322
|
+
};
|
|
3323
|
+
function isRemoteContextLoadingFailure$1(error) {
|
|
3324
|
+
return error instanceof Error && typeof error.details === "object" && error.details != null && error.details.code === "loading remote context failed";
|
|
3325
|
+
}
|
|
3326
|
+
function isPermanentRemoteContextError$1(error) {
|
|
3327
|
+
if (!(error instanceof Error) || error.name !== "jsonld.InvalidUrl") return false;
|
|
3328
|
+
const details = error.details;
|
|
3329
|
+
if (details?.code === "invalid remote context") return true;
|
|
3330
|
+
return isRemoteContextLoadingFailure$1(error) && typeof details?.url === "string" && !URL.canParse(details.url) && require_http.isClearlyMalformedContextReference(details.url);
|
|
3331
|
+
}
|
|
3332
|
+
function isInvalidJsonLdError(error) {
|
|
3333
|
+
if (!(error instanceof Error)) return false;
|
|
3334
|
+
const name = error.name;
|
|
3335
|
+
return name === "UnsafeJsonLdError" || error instanceof require_http.InvalidContextReferenceError || isPermanentRemoteContextError$1(error) || name === "jsonld.SyntaxError" && !isRemoteContextLoadingFailure$1(error);
|
|
3336
|
+
}
|
|
3337
|
+
function isValidationTypeError(error) {
|
|
3338
|
+
return error instanceof TypeError && (/^(Invalid JSON-LD:|Invalid type:|Unexpected type:|Invalid @id:)/.test(error.message) || require_http.isInvalidUrlTypeError(error));
|
|
3339
|
+
}
|
|
3340
|
+
function isPermanentActivityParseError(error) {
|
|
3341
|
+
return isInvalidJsonLdError(error) || isValidationTypeError(error);
|
|
3342
|
+
}
|
|
3343
|
+
function hasHttpSignatureHeaders(request) {
|
|
3344
|
+
return request.headers.has("Signature") || request.headers.has("Signature-Input");
|
|
3345
|
+
}
|
|
3346
|
+
function hasObjectIntegrityProof(json) {
|
|
3347
|
+
return typeof json === "object" && json != null && "proof" in json;
|
|
3348
|
+
}
|
|
3349
|
+
/**
|
|
3350
|
+
* Handles an actor request.
|
|
3351
|
+
* @template TContextData The context data to pass to the context.
|
|
3352
|
+
* @param request The HTTP request.
|
|
3353
|
+
* @param parameters The parameters for handling the actor.
|
|
3354
|
+
* @returns A promise that resolves to an HTTP response.
|
|
3355
|
+
*/
|
|
3356
|
+
async function handleActor(request, { identifier, context, actorDispatcher, authorizePredicate, onNotFound, onUnauthorized }) {
|
|
3357
|
+
const logger = (0, _logtape_logtape.getLogger)([
|
|
3358
|
+
"fedify",
|
|
3359
|
+
"federation",
|
|
3360
|
+
"actor"
|
|
3361
|
+
]);
|
|
3362
|
+
if (actorDispatcher == null) {
|
|
3363
|
+
logger.debug("Actor dispatcher is not set.", { identifier });
|
|
3364
|
+
return await onNotFound(request);
|
|
3365
|
+
}
|
|
3366
|
+
const actor = await actorDispatcher(context, identifier);
|
|
3367
|
+
if (actor == null) {
|
|
3368
|
+
logger.debug("Actor {identifier} not found.", { identifier });
|
|
3369
|
+
return await onNotFound(request);
|
|
3370
|
+
}
|
|
3371
|
+
if (authorizePredicate != null) {
|
|
3372
|
+
if (!await authorizePredicate(context, identifier)) return await onUnauthorized(request);
|
|
3373
|
+
}
|
|
3374
|
+
if (actor instanceof _fedify_vocab.Tombstone) {
|
|
2675
3375
|
const jsonLd = await actor.toJsonLd(context);
|
|
2676
3376
|
return new Response(JSON.stringify(jsonLd), {
|
|
2677
3377
|
status: 410,
|
|
@@ -3950,7 +4650,21 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
3950
4650
|
inbox: portableInbox.recipient.canonicalInboxId
|
|
3951
4651
|
});
|
|
3952
4652
|
else try {
|
|
3953
|
-
await portableInbox.forward(json, activity.id, (0, _fedify_vocab.getTypeId)(activity).href)
|
|
4653
|
+
await portableInbox.forward(json, activity.id, (0, _fedify_vocab.getTypeId)(activity).href, require_http.listRequestSignatures(request).map((signature) => ({
|
|
4654
|
+
keyId: signature.keyId,
|
|
4655
|
+
coversDelivery: coversDelivery(signature),
|
|
4656
|
+
verify: async () => {
|
|
4657
|
+
const verification = await require_http.verifyRequestDetailed(require_http.selectRequestSignature(request, signature), {
|
|
4658
|
+
contextLoader: ctx.contextLoader,
|
|
4659
|
+
documentLoader: ctx.documentLoader,
|
|
4660
|
+
timeWindow: signatureTimeWindow,
|
|
4661
|
+
keyCache,
|
|
4662
|
+
meterProvider,
|
|
4663
|
+
tracerProvider
|
|
4664
|
+
});
|
|
4665
|
+
return verification.verified ? verification.key : null;
|
|
4666
|
+
}
|
|
4667
|
+
})));
|
|
3954
4668
|
} catch (error) {
|
|
3955
4669
|
logger.error("Failed to forward activity {activityId} to the other gateways of the portable inbox {inbox}:\n{error}", {
|
|
3956
4670
|
activityId: activity.id.href,
|
|
@@ -4481,577 +5195,287 @@ var HandlerError = class extends Error {
|
|
|
4481
5195
|
throw() {
|
|
4482
5196
|
throw this;
|
|
4483
5197
|
}
|
|
4484
|
-
};
|
|
4485
|
-
/**
|
|
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.
|
|
4738
|
-
*/
|
|
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
|
-
}
|
|
5198
|
+
};
|
|
4744
5199
|
/**
|
|
4745
|
-
*
|
|
4746
|
-
*
|
|
4747
|
-
* @param request The request to handle.
|
|
4748
|
-
* @param context The request context.
|
|
4749
|
-
* @returns The response to the request.
|
|
5200
|
+
* Error thrown when items are not found in a collection.
|
|
5201
|
+
* @since 1.8.0
|
|
4750
5202
|
*/
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
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;
|
|
5203
|
+
var ItemsNotFoundError = class extends HandlerError {
|
|
5204
|
+
constructor() {
|
|
5205
|
+
super("Items not found in the collection.");
|
|
4761
5206
|
}
|
|
4762
|
-
|
|
4763
|
-
return Promise.resolve(response);
|
|
4764
|
-
}
|
|
4765
|
-
//#endregion
|
|
4766
|
-
//#region src/federation/send.ts
|
|
5207
|
+
};
|
|
4767
5208
|
/**
|
|
4768
|
-
*
|
|
4769
|
-
* @
|
|
4770
|
-
* See also {@link ExtractInboxesParameters}.
|
|
4771
|
-
* @returns The inboxes as a map of inbox URL to actor URIs.
|
|
5209
|
+
* Error thrown when access to a collection is unauthorized.
|
|
5210
|
+
* @since 1.8.0
|
|
4772
5211
|
*/
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
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
|
-
}
|
|
5212
|
+
var UnauthorizedError = class extends HandlerError {
|
|
5213
|
+
constructor() {
|
|
5214
|
+
super("Unauthorized access to the collection.");
|
|
4790
5215
|
}
|
|
4791
|
-
|
|
4792
|
-
}
|
|
5216
|
+
};
|
|
4793
5217
|
/**
|
|
4794
|
-
*
|
|
5218
|
+
* Responds with the given object in JSON-LD format.
|
|
4795
5219
|
*
|
|
4796
|
-
* @param
|
|
4797
|
-
*
|
|
4798
|
-
* @
|
|
5220
|
+
* @param object The object to respond with.
|
|
5221
|
+
* @param options Options.
|
|
5222
|
+
* @since 0.3.0
|
|
4799
5223
|
*/
|
|
4800
|
-
function
|
|
4801
|
-
const
|
|
4802
|
-
return
|
|
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);
|
|
5224
|
+
async function respondWithObject(object, options) {
|
|
5225
|
+
const jsonLd = await object.toJsonLd(options);
|
|
5226
|
+
return new Response(JSON.stringify(jsonLd), { headers: { "Content-Type": "application/activity+json" } });
|
|
4828
5227
|
}
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
if (
|
|
5228
|
+
/**
|
|
5229
|
+
* Responds with the given object in JSON-LD format if the request accepts
|
|
5230
|
+
* JSON-LD.
|
|
5231
|
+
*
|
|
5232
|
+
* @param object The object to respond with.
|
|
5233
|
+
* @param request The request to check for JSON-LD acceptability.
|
|
5234
|
+
* @param options Options.
|
|
5235
|
+
* @since 0.3.0
|
|
5236
|
+
*/
|
|
5237
|
+
async function respondWithObjectIfAcceptable(object, request, options) {
|
|
5238
|
+
if (!acceptsJsonLd(request)) return null;
|
|
5239
|
+
const response = await respondWithObject(object, options);
|
|
5240
|
+
response.headers.set("Vary", "Accept");
|
|
5241
|
+
return response;
|
|
4840
5242
|
}
|
|
4841
|
-
function
|
|
4842
|
-
|
|
5243
|
+
function generateNonce() {
|
|
5244
|
+
const bytes = /* @__PURE__ */ new Uint8Array(16);
|
|
5245
|
+
crypto.getRandomValues(bytes);
|
|
5246
|
+
return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
4843
5247
|
}
|
|
4844
|
-
async function
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
const
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
truncated = true;
|
|
4859
|
-
break;
|
|
4860
|
-
}
|
|
4861
|
-
chunks.push(decoder.decode(value, { stream: true }));
|
|
4862
|
-
totalBytes += value.length;
|
|
4863
|
-
}
|
|
4864
|
-
} finally {
|
|
4865
|
-
reader.releaseLock();
|
|
5248
|
+
async function verifySignatureNonce(request, kv, noncePrefix, verifiedLabel) {
|
|
5249
|
+
const signatureInput = request.headers.get("Signature-Input");
|
|
5250
|
+
if (signatureInput == null) return false;
|
|
5251
|
+
const parsed = require_http.parseRfc9421SignatureInput(signatureInput);
|
|
5252
|
+
if (verifiedLabel == null) return false;
|
|
5253
|
+
const sig = parsed[verifiedLabel];
|
|
5254
|
+
if (sig == null) return false;
|
|
5255
|
+
const nonce = sig.nonce;
|
|
5256
|
+
if (nonce == null) return false;
|
|
5257
|
+
const key = [...noncePrefix, nonce];
|
|
5258
|
+
if (kv.cas != null) return await kv.cas(key, true, void 0);
|
|
5259
|
+
if (await kv.get(key) != null) {
|
|
5260
|
+
await kv.delete(key);
|
|
5261
|
+
return true;
|
|
4866
5262
|
}
|
|
4867
|
-
|
|
4868
|
-
if (truncated) result += "… (truncated)";
|
|
4869
|
-
return result;
|
|
5263
|
+
return false;
|
|
4870
5264
|
}
|
|
4871
|
-
|
|
4872
|
-
const
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
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);
|
|
5265
|
+
const getFailedSignatureResponse = async (policy, kv, kvPrefixes) => {
|
|
5266
|
+
const headers = await getFailedSignatureHeaders(policy, kv, kvPrefixes);
|
|
5267
|
+
return new Response("Failed to verify the request signature.", {
|
|
5268
|
+
status: 401,
|
|
5269
|
+
headers
|
|
5270
|
+
});
|
|
5271
|
+
};
|
|
5272
|
+
const getFailedSignatureHeaders = async (policy, kv, kvPrefixes) => ({
|
|
5273
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
5274
|
+
...policy?.enabled && {
|
|
5275
|
+
"Accept-Signature": await buildAcceptSignatureHeader(policy, kv, kvPrefixes.acceptSignatureNonce),
|
|
5276
|
+
"Cache-Control": "no-store",
|
|
5277
|
+
"Vary": "Accept, Signature"
|
|
5278
|
+
}
|
|
5279
|
+
});
|
|
5280
|
+
async function buildAcceptSignatureHeader(policy, kv, noncePrefix) {
|
|
5281
|
+
const parameters = { created: true };
|
|
5282
|
+
if (policy.requestNonce) {
|
|
5283
|
+
const nonce = generateNonce();
|
|
5284
|
+
await setKey(kv, [...noncePrefix, nonce], policy);
|
|
5285
|
+
parameters.nonce = nonce;
|
|
4961
5286
|
}
|
|
5287
|
+
const baseComponents = policy.components ?? DEF_COMPONENTS;
|
|
5288
|
+
return require_http.formatAcceptSignature([{
|
|
5289
|
+
label: "sig1",
|
|
5290
|
+
components: (0, es_toolkit.uniq)(MIN_COMPONENTS.concat(baseComponents)).filter((c) => c !== "@status").map((v) => ({
|
|
5291
|
+
value: v,
|
|
5292
|
+
params: {}
|
|
5293
|
+
})),
|
|
5294
|
+
parameters
|
|
5295
|
+
}]);
|
|
4962
5296
|
}
|
|
4963
|
-
function
|
|
4964
|
-
const
|
|
4965
|
-
|
|
4966
|
-
|
|
5297
|
+
async function setKey(kv, key, policy) {
|
|
5298
|
+
const seconds = policy.nonceTtlSeconds ?? 300;
|
|
5299
|
+
const ttl = Temporal$6.Duration.from({ seconds });
|
|
5300
|
+
await kv.set(key, true, { ttl });
|
|
4967
5301
|
}
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5302
|
+
const DEF_COMPONENTS = [
|
|
5303
|
+
"@method",
|
|
5304
|
+
"@target-uri",
|
|
5305
|
+
"@authority",
|
|
5306
|
+
"content-digest"
|
|
5307
|
+
];
|
|
5308
|
+
const MIN_COMPONENTS = [
|
|
5309
|
+
"@method",
|
|
5310
|
+
"@target-uri",
|
|
5311
|
+
"@authority"
|
|
5312
|
+
];
|
|
5313
|
+
/**
|
|
5314
|
+
* Checks whether an HTTP Signature key is a gateway key of an FEP-ef61
|
|
5315
|
+
* portable actor, i.e., its ID is a compatible identifier and it is owned by
|
|
5316
|
+
* a portable actor, which only the portable actor's signed document can
|
|
5317
|
+
* establish.
|
|
5318
|
+
*/
|
|
5319
|
+
function isPortableGatewayKey(key) {
|
|
5320
|
+
return key?.id != null && require_http.isCompatibleKeyId(key.id) && key.ownerId != null && require_http.isPortableId(key.ownerId);
|
|
5321
|
+
}
|
|
5322
|
+
/**
|
|
5323
|
+
* Checks whether an activity needs an Object Integrity Proof by its DID:
|
|
5324
|
+
* either it is performed by an [FEP-ef61] portable actor, or its own ID is
|
|
5325
|
+
* portable. Both `ap:` URIs and compatible identifiers count as portable.
|
|
5326
|
+
*
|
|
5327
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
5328
|
+
*/
|
|
5329
|
+
function isPortableActivity(activity) {
|
|
5330
|
+
return require_http.hasPortableActor(activity) || activity.id != null && require_http.isPortableId(activity.id);
|
|
5331
|
+
}
|
|
5332
|
+
/**
|
|
5333
|
+
* Verifies that a portable activity's own ID belongs to the DID that signed
|
|
5334
|
+
* it, on the JSON-LD document received. The ID the proof policy checks has
|
|
5335
|
+
* to be the parsed activity's ID, so that a remote context that changes
|
|
5336
|
+
* between loads cannot make the two name different objects.
|
|
5337
|
+
* @returns `null` if the ID is verified, or the reason why it is not.
|
|
5338
|
+
*/
|
|
5339
|
+
async function verifyPortableActivityId(activityId, jsonLd, options) {
|
|
5340
|
+
const expectedId = require_http.getCanonicalPortableId(activityId);
|
|
5341
|
+
if (expectedId == null) return "malformed portable ID";
|
|
5342
|
+
let verification;
|
|
5343
|
+
try {
|
|
5344
|
+
verification = await require_http.verifyPortableObjectProofWithRoot(jsonLd, options);
|
|
5345
|
+
} catch (error) {
|
|
5346
|
+
if (error instanceof require_http.InvalidPortableObjectIdError) return "malformed portable ID";
|
|
5347
|
+
if (!isPermanentActivityParseError(error)) throw error;
|
|
5348
|
+
return "malformed document";
|
|
5009
5349
|
}
|
|
5350
|
+
const { result, root } = verification;
|
|
5351
|
+
if (!result.verified) return result.reason.type;
|
|
5352
|
+
const rootId = root?.["@id"];
|
|
5353
|
+
if (typeof rootId !== "string" || require_http.getCanonicalPortableId((0, _fedify_vocab_runtime.parseIri)(rootId)) !== expectedId) return "the verified document has another ID";
|
|
5354
|
+
return null;
|
|
5010
5355
|
}
|
|
5356
|
+
//#endregion
|
|
5357
|
+
//#region src/federation/kv.ts
|
|
5358
|
+
const Temporal$5 = require_http.Temporal;
|
|
5011
5359
|
/**
|
|
5012
|
-
*
|
|
5013
|
-
*
|
|
5014
|
-
*
|
|
5015
|
-
* @since
|
|
5360
|
+
* A key–value store that stores values in memory.
|
|
5361
|
+
* Do not use this in production as it does not persist values.
|
|
5362
|
+
*
|
|
5363
|
+
* @since 0.5.0
|
|
5016
5364
|
*/
|
|
5017
|
-
var
|
|
5365
|
+
var MemoryKvStore = class {
|
|
5366
|
+
#values = {};
|
|
5367
|
+
#encodeKey(key) {
|
|
5368
|
+
return JSON.stringify(key);
|
|
5369
|
+
}
|
|
5018
5370
|
/**
|
|
5019
|
-
*
|
|
5371
|
+
* {@inheritDoc KvStore.get}
|
|
5020
5372
|
*/
|
|
5021
|
-
|
|
5373
|
+
get(key) {
|
|
5374
|
+
const encodedKey = this.#encodeKey(key);
|
|
5375
|
+
const entry = this.#values[encodedKey];
|
|
5376
|
+
if (entry == null) return Promise.resolve(void 0);
|
|
5377
|
+
const [value, expiration] = entry;
|
|
5378
|
+
if (expiration != null && Temporal$5.Now.instant().until(expiration).sign < 0) {
|
|
5379
|
+
delete this.#values[encodedKey];
|
|
5380
|
+
return Promise.resolve(void 0);
|
|
5381
|
+
}
|
|
5382
|
+
return Promise.resolve(value);
|
|
5383
|
+
}
|
|
5022
5384
|
/**
|
|
5023
|
-
*
|
|
5385
|
+
* {@inheritDoc KvStore.set}
|
|
5024
5386
|
*/
|
|
5025
|
-
|
|
5387
|
+
set(key, value, options) {
|
|
5388
|
+
const encodedKey = this.#encodeKey(key);
|
|
5389
|
+
const expiration = options?.ttl == null ? null : Temporal$5.Now.instant().add(options.ttl.round({ largestUnit: "hour" }));
|
|
5390
|
+
this.#values[encodedKey] = [value, expiration];
|
|
5391
|
+
return Promise.resolve();
|
|
5392
|
+
}
|
|
5026
5393
|
/**
|
|
5027
|
-
*
|
|
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)"`.
|
|
5394
|
+
* {@inheritDoc KvStore.delete}
|
|
5031
5395
|
*/
|
|
5032
|
-
|
|
5396
|
+
delete(key) {
|
|
5397
|
+
const encodedKey = this.#encodeKey(key);
|
|
5398
|
+
delete this.#values[encodedKey];
|
|
5399
|
+
return Promise.resolve();
|
|
5400
|
+
}
|
|
5033
5401
|
/**
|
|
5034
|
-
*
|
|
5035
|
-
* @since 2.3.0
|
|
5402
|
+
* {@inheritDoc KvStore.cas}
|
|
5036
5403
|
*/
|
|
5037
|
-
|
|
5404
|
+
cas(key, expectedValue, newValue, options) {
|
|
5405
|
+
const encodedKey = this.#encodeKey(key);
|
|
5406
|
+
const entry = this.#values[encodedKey];
|
|
5407
|
+
let currentValue;
|
|
5408
|
+
if (entry == null) currentValue = void 0;
|
|
5409
|
+
else {
|
|
5410
|
+
const [value, expiration] = entry;
|
|
5411
|
+
if (expiration != null && Temporal$5.Now.instant().until(expiration).sign < 0) {
|
|
5412
|
+
delete this.#values[encodedKey];
|
|
5413
|
+
currentValue = void 0;
|
|
5414
|
+
} else currentValue = value;
|
|
5415
|
+
}
|
|
5416
|
+
if (!(0, es_toolkit.isEqual)(currentValue, expectedValue)) return Promise.resolve(false);
|
|
5417
|
+
const expiration = options?.ttl == null ? null : Temporal$5.Now.instant().add(options.ttl.round({ largestUnit: "hour" }));
|
|
5418
|
+
this.#values[encodedKey] = [newValue, expiration];
|
|
5419
|
+
return Promise.resolve(true);
|
|
5420
|
+
}
|
|
5038
5421
|
/**
|
|
5039
|
-
*
|
|
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.
|
|
5422
|
+
* {@inheritDoc KvStore.list}
|
|
5045
5423
|
*/
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
this
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5424
|
+
async *list(prefix) {
|
|
5425
|
+
const now = Temporal$5.Now.instant();
|
|
5426
|
+
for (const [encodedKey, entry] of Object.entries(this.#values)) {
|
|
5427
|
+
const key = JSON.parse(encodedKey);
|
|
5428
|
+
if (prefix != null) {
|
|
5429
|
+
if (key.length < prefix.length) continue;
|
|
5430
|
+
if (!prefix.every((p, i) => key[i] === p)) continue;
|
|
5431
|
+
}
|
|
5432
|
+
const [value, expiration] = entry;
|
|
5433
|
+
if (expiration != null && now.until(expiration).sign < 0) {
|
|
5434
|
+
delete this.#values[encodedKey];
|
|
5435
|
+
continue;
|
|
5436
|
+
}
|
|
5437
|
+
yield {
|
|
5438
|
+
key,
|
|
5439
|
+
value
|
|
5440
|
+
};
|
|
5441
|
+
}
|
|
5442
|
+
}
|
|
5443
|
+
};
|
|
5444
|
+
//#endregion
|
|
5445
|
+
//#region src/nodeinfo/handler.ts
|
|
5446
|
+
/**
|
|
5447
|
+
* Handles a NodeInfo request. You would not typically call this function
|
|
5448
|
+
* directly, but instead use {@link Federation.handle} method.
|
|
5449
|
+
* @param request The NodeInfo request to handle.
|
|
5450
|
+
* @param parameters The parameters for handling the request.
|
|
5451
|
+
* @returns The response to the request.
|
|
5452
|
+
*/
|
|
5453
|
+
async function handleNodeInfo(_request, { context, nodeInfoDispatcher }) {
|
|
5454
|
+
const promise = nodeInfoDispatcher(context);
|
|
5455
|
+
const json = require_types.nodeInfoToJson(promise instanceof Promise ? await promise : promise);
|
|
5456
|
+
return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json; profile=\"http://nodeinfo.diaspora.software/ns/schema/2.1#\"" } });
|
|
5457
|
+
}
|
|
5458
|
+
/**
|
|
5459
|
+
* Handles a request to `/.well-known/nodeinfo`. You would not typically call
|
|
5460
|
+
* this function directly, but instead use {@link Federation.handle} method.
|
|
5461
|
+
* @param request The request to handle.
|
|
5462
|
+
* @param context The request context.
|
|
5463
|
+
* @returns The response to the request.
|
|
5464
|
+
*/
|
|
5465
|
+
function handleNodeInfoJrd(_request, context) {
|
|
5466
|
+
const links = [];
|
|
5467
|
+
try {
|
|
5468
|
+
links.push({
|
|
5469
|
+
rel: "http://nodeinfo.diaspora.software/ns/schema/2.1",
|
|
5470
|
+
href: context.getNodeInfoUri().href,
|
|
5471
|
+
type: "application/json; profile=\"http://nodeinfo.diaspora.software/ns/schema/2.1#\""
|
|
5472
|
+
});
|
|
5473
|
+
} catch (e) {
|
|
5474
|
+
if (!(e instanceof _fedify_uri_template.RouterError)) throw e;
|
|
5053
5475
|
}
|
|
5054
|
-
};
|
|
5476
|
+
const response = new Response(JSON.stringify({ links }), { headers: { "Content-Type": "application/jrd+json" } });
|
|
5477
|
+
return Promise.resolve(response);
|
|
5478
|
+
}
|
|
5055
5479
|
//#endregion
|
|
5056
5480
|
//#region src/federation/bench.ts
|
|
5057
5481
|
/**
|
|
@@ -5124,431 +5548,128 @@ async function handleBenchmarkTrigger(request, context, options = {}) {
|
|
|
5124
5548
|
if (request.method !== "POST") return new Response("Method not allowed", {
|
|
5125
5549
|
status: 405,
|
|
5126
5550
|
headers: { "Allow": "POST" }
|
|
5127
|
-
});
|
|
5128
|
-
let json;
|
|
5129
|
-
try {
|
|
5130
|
-
json = await request.json();
|
|
5131
|
-
} catch {
|
|
5132
|
-
return jsonResponse({ error: "Invalid JSON request body." }, 400);
|
|
5133
|
-
}
|
|
5134
|
-
try {
|
|
5135
|
-
const body = asRecord(json, "request body");
|
|
5136
|
-
const sender = parseSender(body.sender);
|
|
5137
|
-
const recipients = await parseRecipients(body.recipients, context);
|
|
5138
|
-
const activity = await parseActivity(body.activity, context);
|
|
5139
|
-
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;
|
|
5551
|
+
});
|
|
5552
|
+
let json;
|
|
5553
|
+
try {
|
|
5554
|
+
json = await request.json();
|
|
5555
|
+
} catch {
|
|
5556
|
+
return jsonResponse({ error: "Invalid JSON request body." }, 400);
|
|
5377
5557
|
}
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
const
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5558
|
+
try {
|
|
5559
|
+
const body = asRecord(json, "request body");
|
|
5560
|
+
const sender = parseSender(body.sender);
|
|
5561
|
+
const recipients = await parseRecipients(body.recipients, context);
|
|
5562
|
+
const activity = await parseActivity(body.activity, context);
|
|
5563
|
+
if (activity.id == null) throw new BenchmarkTriggerError("activity must have an id.");
|
|
5564
|
+
const activityId = activity.id.href;
|
|
5565
|
+
const inboxes = extractInboxes({ recipients });
|
|
5566
|
+
const inboxUrls = Object.keys(inboxes);
|
|
5567
|
+
if (inboxUrls.length < 1) throw new BenchmarkTriggerError("No valid recipient inboxes found. The recipients list must not be empty.");
|
|
5568
|
+
const unsafeInboxes = options.allowUnsafeRecipients ? [] : inboxUrls.filter((inbox) => !options.sinks?.has(inbox));
|
|
5569
|
+
if (unsafeInboxes.length > 0) return jsonResponse({
|
|
5570
|
+
error: "unsafe_recipient",
|
|
5571
|
+
unsafeInboxes
|
|
5572
|
+
}, 403);
|
|
5573
|
+
await context.sendActivity(sender, recipients, activity);
|
|
5574
|
+
return jsonResponse({
|
|
5575
|
+
version: 1,
|
|
5576
|
+
activityId,
|
|
5577
|
+
queueCorrelationId: activityId,
|
|
5578
|
+
recipientCount: recipients.length,
|
|
5579
|
+
inboxCount: inboxUrls.length
|
|
5580
|
+
}, 202);
|
|
5581
|
+
} catch (error) {
|
|
5582
|
+
if (error instanceof BenchmarkTriggerError) return jsonResponse({ error: error.message }, error.status);
|
|
5583
|
+
throw error;
|
|
5387
5584
|
}
|
|
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;
|
|
5397
5585
|
}
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5586
|
+
var BenchmarkTriggerError = class extends Error {
|
|
5587
|
+
status;
|
|
5588
|
+
constructor(message, status = 400) {
|
|
5589
|
+
super(message);
|
|
5590
|
+
this.status = status;
|
|
5591
|
+
}
|
|
5592
|
+
};
|
|
5593
|
+
function parseSender(value) {
|
|
5594
|
+
const sender = asRecord(value, "sender");
|
|
5595
|
+
if (typeof sender.identifier === "string") return { identifier: sender.identifier };
|
|
5596
|
+
if (typeof sender.username === "string") return { username: sender.username };
|
|
5597
|
+
throw new BenchmarkTriggerError("sender must be { identifier } or { username }.");
|
|
5598
|
+
}
|
|
5599
|
+
async function parseRecipients(value, context) {
|
|
5600
|
+
if (!Array.isArray(value)) throw new BenchmarkTriggerError("recipients must be an array.");
|
|
5601
|
+
return await Promise.all(value.map(async (item) => {
|
|
5602
|
+
let object;
|
|
5410
5603
|
try {
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
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
|
|
5604
|
+
object = await _fedify_vocab.Object.fromJsonLd(item, {
|
|
5605
|
+
documentLoader: context.documentLoader,
|
|
5606
|
+
contextLoader: context.contextLoader
|
|
5417
5607
|
});
|
|
5418
|
-
|
|
5608
|
+
} catch (error) {
|
|
5609
|
+
throw new BenchmarkTriggerError(`Invalid ActivityPub recipient: ${error}`);
|
|
5419
5610
|
}
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
}
|
|
5425
|
-
return targets;
|
|
5611
|
+
if (!isRecipient(object)) throw new BenchmarkTriggerError("each recipient must be an ActivityPub actor.");
|
|
5612
|
+
const recipient = object;
|
|
5613
|
+
if (recipient.id == null || recipient.inboxId == null) throw new BenchmarkTriggerError("each recipient must have id and inbox properties.");
|
|
5614
|
+
return recipient;
|
|
5615
|
+
}));
|
|
5426
5616
|
}
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
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;
|
|
5617
|
+
function isRecipient(value) {
|
|
5618
|
+
return value != null && typeof value === "object" && "id" in value && "inboxId" in value;
|
|
5619
|
+
}
|
|
5620
|
+
async function parseActivity(value, context) {
|
|
5441
5621
|
try {
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
activityId: activityId.href,
|
|
5446
|
-
error
|
|
5622
|
+
return await _fedify_vocab.Activity.fromJsonLd(value, {
|
|
5623
|
+
documentLoader: context.documentLoader,
|
|
5624
|
+
contextLoader: context.contextLoader
|
|
5447
5625
|
});
|
|
5448
|
-
return null;
|
|
5449
|
-
}
|
|
5450
|
-
if (key == null) return null;
|
|
5451
|
-
try {
|
|
5452
|
-
require_http.validateCryptoKey(key.privateKey, "private");
|
|
5453
5626
|
} catch (error) {
|
|
5454
|
-
|
|
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;
|
|
5627
|
+
throw new BenchmarkTriggerError(`Invalid ActivityPub activity: ${error}`);
|
|
5461
5628
|
}
|
|
5462
|
-
return key;
|
|
5463
5629
|
}
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
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
|
|
5630
|
+
function asRecord(value, name) {
|
|
5631
|
+
if (value == null || typeof value !== "object" || Array.isArray(value)) throw new BenchmarkTriggerError(`${name} must be an object.`);
|
|
5632
|
+
return value;
|
|
5633
|
+
}
|
|
5634
|
+
function jsonResponse(body, status = 200) {
|
|
5635
|
+
return new Response(JSON.stringify(body), {
|
|
5636
|
+
status,
|
|
5637
|
+
headers: { "Content-Type": "application/json" }
|
|
5497
5638
|
});
|
|
5498
5639
|
}
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5640
|
+
function serializeScopeMetrics(resourceMetrics) {
|
|
5641
|
+
return resourceMetrics.scopeMetrics.map(serializeScope);
|
|
5642
|
+
}
|
|
5643
|
+
function serializeScope(scopeMetrics) {
|
|
5644
|
+
return {
|
|
5645
|
+
scope: {
|
|
5646
|
+
name: scopeMetrics.scope.name,
|
|
5647
|
+
version: scopeMetrics.scope.version
|
|
5648
|
+
},
|
|
5649
|
+
metrics: scopeMetrics.metrics.map(serializeMetric)
|
|
5650
|
+
};
|
|
5651
|
+
}
|
|
5652
|
+
function serializeMetric(metric) {
|
|
5653
|
+
return {
|
|
5654
|
+
name: metric.descriptor.name,
|
|
5655
|
+
description: metric.descriptor.description,
|
|
5656
|
+
unit: metric.descriptor.unit,
|
|
5657
|
+
dataPointType: serializeDataPointType(metric.dataPointType),
|
|
5658
|
+
dataPoints: metric.dataPoints.map((point) => ({
|
|
5659
|
+
attributes: { ...point.attributes },
|
|
5660
|
+
startTime: point.startTime,
|
|
5661
|
+
endTime: point.endTime,
|
|
5662
|
+
value: point.value
|
|
5663
|
+
}))
|
|
5664
|
+
};
|
|
5665
|
+
}
|
|
5666
|
+
function serializeDataPointType(dataPointType) {
|
|
5667
|
+
switch (dataPointType) {
|
|
5668
|
+
case _opentelemetry_sdk_metrics.DataPointType.HISTOGRAM: return "histogram";
|
|
5669
|
+
case _opentelemetry_sdk_metrics.DataPointType.EXPONENTIAL_HISTOGRAM: return "exponential_histogram";
|
|
5670
|
+
case _opentelemetry_sdk_metrics.DataPointType.GAUGE: return "gauge";
|
|
5671
|
+
case _opentelemetry_sdk_metrics.DataPointType.SUM: return "sum";
|
|
5519
5672
|
}
|
|
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
5673
|
}
|
|
5553
5674
|
//#endregion
|
|
5554
5675
|
//#region src/federation/outgoing-proof.ts
|
|
@@ -8228,7 +8349,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
8228
8349
|
onNotFound,
|
|
8229
8350
|
portableInbox: {
|
|
8230
8351
|
recipient,
|
|
8231
|
-
forward: async (activity, activityId, activityType) => {
|
|
8352
|
+
forward: async (activity, activityId, activityType, signatures) => {
|
|
8232
8353
|
if (this.portableInboxForwarding.maxTargets < 1) return;
|
|
8233
8354
|
if (this.kv.cas == null) {
|
|
8234
8355
|
if (!this.#portableInboxForwardingWarned) {
|
|
@@ -8251,6 +8372,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
8251
8372
|
allowPrivateAddress: this.allowPrivateAddress,
|
|
8252
8373
|
getKeys: () => this.#getPortableGatewayKeyPairs(context, identifier, recipient),
|
|
8253
8374
|
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
|
|
8375
|
+
signatures,
|
|
8254
8376
|
options: this.portableInboxForwarding,
|
|
8255
8377
|
meterProvider: this.meterProvider,
|
|
8256
8378
|
tracerProvider: this.tracerProvider
|