@fedify/fedify
Version:
An ActivityPub server framework
194 lines (193 loc) • 6.27 kB
JavaScript
import "@js-temporal/polyfill";
import "urlpattern-polyfill";
globalThis.addEventListener = () => {};
import { n as version, t as name } from "./deno-DMg4SgCb.mjs";
import { n as doubleKnock } from "./http-C_edJspG.mjs";
import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
import { getLogger } from "@logtape/logtape";
//#region src/federation/send.ts
/**
* Extracts the inbox URLs from recipients.
* @param parameters The parameters to extract the inboxes.
* See also {@link ExtractInboxesParameters}.
* @returns The inboxes as a map of inbox URL to actor URIs.
*/
function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris }) {
const inboxes = {};
for (const recipient of recipients) {
let inbox;
let sharedInbox = false;
if (preferSharedInbox && recipient.endpoints?.sharedInbox != null) {
inbox = recipient.endpoints.sharedInbox;
sharedInbox = true;
} else inbox = recipient.inboxId;
if (inbox != null && recipient.id != null) {
if (excludeBaseUris != null && excludeBaseUris.some((u) => u.origin === inbox?.origin)) continue;
inboxes[inbox.href] ??= {
actorIds: /* @__PURE__ */ new Set(),
sharedInbox
};
inboxes[inbox.href].actorIds.add(recipient.id.href);
}
}
return inboxes;
}
/**
* Sends an {@link Activity} to an inbox.
*
* @param parameters The parameters for sending the activity.
* See also {@link SendActivityParameters}.
* @throws {Error} If the activity fails to send.
*/
function sendActivity(options) {
const tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
return tracerProvider.getTracer(name, version).startActiveSpan("activitypub.send_activity", {
kind: SpanKind.CLIENT,
attributes: { "activitypub.shared_inbox": options.sharedInbox ?? false }
}, async (span) => {
if (options.activityId != null) span.setAttribute("activitypub.activity.id", options.activityId);
if (options.activityType != null) span.setAttribute("activitypub.activity.type", options.activityType);
try {
await sendActivityInternal({
...options,
tracerProvider
}, span);
} catch (e) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: String(e)
});
throw e;
} finally {
span.end();
}
});
}
const MAX_ERROR_RESPONSE_BODY_BYTES = 1024;
async function readLimitedResponseBody(response, maxBytes) {
if (response.body == null) return "";
const reader = response.body.getReader();
const decoder = new TextDecoder();
const chunks = [];
let totalBytes = 0;
let truncated = false;
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (totalBytes + value.length > maxBytes) {
const remaining = maxBytes - totalBytes;
if (remaining > 0) chunks.push(decoder.decode(value.slice(0, remaining), { stream: true }));
truncated = true;
break;
}
chunks.push(decoder.decode(value, { stream: true }));
totalBytes += value.length;
}
} finally {
reader.releaseLock();
}
let result = chunks.join("");
if (truncated) result += "… (truncated)";
return result;
}
async function sendActivityInternal({ activity, activityId, keys, inbox, headers, specDeterminer, tracerProvider }, span) {
const logger = getLogger([
"fedify",
"federation",
"outbox"
]);
headers = new Headers(headers);
headers.set("Content-Type", "application/activity+json");
const request = new Request(inbox, {
method: "POST",
headers,
body: JSON.stringify(activity)
});
let rsaKey = null;
for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
rsaKey = key;
break;
}
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.", {
inbox: inbox.href,
keys: keys.map((pair) => ({
keyId: pair.keyId.href,
privateKey: pair.privateKey
}))
});
let response;
try {
response = rsaKey == null ? await fetch(request) : await doubleKnock(request, rsaKey, {
tracerProvider,
specDeterminer
});
} catch (error) {
logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
activityId,
inbox: inbox.href,
error
});
throw error;
}
if (!response.ok) {
let error;
try {
error = await readLimitedResponseBody(response, MAX_ERROR_RESPONSE_BODY_BYTES);
} catch (_) {
error = "";
}
logger.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
activityId,
inbox: inbox.href,
status: response.status,
statusText: response.statusText,
error
});
throw new SendActivityError(inbox, response.status, `Failed to send activity ${activityId} to ${inbox.href} (${response.status} ${response.statusText}):\n${error}`, error);
}
span.addEvent("activitypub.activity.sent", {
"activitypub.activity.json": JSON.stringify(activity),
"activitypub.inbox.url": inbox.href,
"activitypub.activity.id": activityId ?? ""
});
}
/**
* An error that is thrown when an activity fails to send to a remote inbox.
* It contains structured information about the failure, including the HTTP
* status code, the inbox URL, and the response body.
* @since 2.0.0
*/
var SendActivityError = class extends Error {
/**
* The inbox URL that the activity was being sent to.
*/
inbox;
/**
* The HTTP status code returned by the inbox.
*/
statusCode;
/**
* The response body from the inbox, if any. Note that this may be
* truncated to a maximum of 1 KiB to prevent excessive memory consumption
* when remote servers return large error pages (e.g., Cloudflare error pages).
* If truncated, the string will end with `"… (truncated)"`.
*/
responseBody;
/**
* Creates a new {@link SendActivityError}.
* @param inbox The inbox URL.
* @param statusCode The HTTP status code.
* @param message The error message.
* @param responseBody The response body.
*/
constructor(inbox, statusCode, message, responseBody) {
super(message);
this.name = "SendActivityError";
this.inbox = inbox;
this.statusCode = statusCode;
this.responseBody = responseBody;
}
};
//#endregion
export { extractInboxes as n, sendActivity as r, SendActivityError as t };