@fedify/fedify 2.4.0-dev.2200 → 2.4.0-dev.2219
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/audience-CHQ-FlLD.mjs +95 -0
- package/dist/{builder-BGSNMnVH.mjs → builder-C5rYBZ16.mjs} +10 -2
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/public-audience.test.mjs +1 -1
- package/dist/compat/transformers.test.mjs +1 -1
- package/dist/{compound-proof-Zpg7Fbk9.mjs → compound-proof-D0xY7MBs.mjs} +2 -2
- package/dist/{context-B2rlkkyJ.mjs → context-8Tu9kea0.mjs} +2 -0
- package/dist/{context-CDcAgm4l.d.cts → context-B6X-t8gI.d.cts} +81 -2
- package/dist/{context-9T--NSo4.d.ts → context-CtI307_A.d.ts} +81 -2
- package/dist/{deno-CxoV48e0.mjs → deno-Dj1TFhf4.mjs} +1 -1
- package/dist/{docloader-U141HKIl.mjs → docloader-CzyJe90o.mjs} +3 -3
- package/dist/federation/audience.test.d.mts +2 -0
- package/dist/federation/audience.test.mjs +130 -0
- package/dist/federation/builder.test.mjs +37 -1
- package/dist/federation/compound-fanout.test.mjs +4 -4
- package/dist/federation/handler-compound-proof.test.mjs +5 -5
- package/dist/federation/handler.test.mjs +7 -7
- package/dist/federation/hashlink-media.test.mjs +3 -3
- package/dist/federation/idempotency.test.mjs +2 -2
- package/dist/federation/metrics.test.mjs +2 -2
- package/dist/federation/middleware.test.mjs +125 -9
- package/dist/federation/mod.cjs +1 -1
- package/dist/federation/mod.d.cts +4 -4
- package/dist/federation/mod.d.ts +4 -4
- package/dist/federation/mod.js +1 -1
- package/dist/federation/outgoing-proof.test.mjs +6 -6
- package/dist/federation/portable-actor.test.mjs +4 -4
- package/dist/federation/portable-chain.test.mjs +4 -4
- package/dist/federation/portable-collection.test.mjs +8 -6
- package/dist/federation/portable-delivery.test.d.mts +2 -0
- package/dist/federation/portable-delivery.test.mjs +746 -0
- package/dist/federation/portable-gateway-keys.test.mjs +6 -6
- package/dist/federation/portable-inbox.test.mjs +5 -5
- package/dist/federation/portable-parse-uri.test.mjs +2 -2
- package/dist/federation/portable-route-activity.test.mjs +3 -3
- package/dist/federation/portable.test.mjs +234 -19
- package/dist/federation/send.test.mjs +4 -4
- package/dist/federation/tasks/enqueue.test.mjs +2 -2
- package/dist/federation/tasks/tasks.test.mjs +3 -3
- package/dist/federation/temporal.test.mjs +1 -1
- package/dist/federation/webfinger.test.mjs +3 -3
- package/dist/{federation-Ij8VLHpy.cjs → federation-DC1yfb-8.cjs} +696 -41
- package/dist/{http-D8XakCKn.cjs → http-Cuk0w0rp.cjs} +1 -1
- package/dist/{http-BQq1Uldg.mjs → http-D-DpP4sd.mjs} +5 -5
- package/dist/{http-CshNXXui.js → http-Dx7yMmfw.js} +1 -1
- package/dist/{key-DYgoirTo.mjs → key-Djyt1BFx.mjs} +2 -2
- package/dist/{kv-BhNbN8Ed.d.cts → kv-B2mzK5tl.d.cts} +3 -2
- package/dist/{kv-BhNbN8Ed.d.ts → kv-B2mzK5tl.d.ts} +3 -2
- package/dist/{kv-cache-f_GNq0e7.cjs → kv-cache-BmLTVLXc.cjs} +1 -1
- package/dist/{kv-cache-Bpnt_Cck.js → kv-cache-CUZOCSKZ.js} +1 -1
- package/dist/{kv-cache-D0Mn-VQW.mjs → kv-cache-ecUXfCqW.mjs} +1 -1
- package/dist/{ld-eIekvNtm.mjs → ld-CNVWdooi.mjs} +3 -3
- package/dist/{metrics-BVpdjsaR.mjs → metrics-BnYleVpg.mjs} +1 -1
- package/dist/{middleware-CS3H_4tQ.js → middleware-BdzcazaN.js} +698 -43
- package/dist/{middleware-rpYJG85a.mjs → middleware-Bh592bk8.mjs} +1 -1
- package/dist/{middleware-BbTJ6C1-.mjs → middleware-C2_LcbL3.mjs} +431 -50
- package/dist/{mod-C5Mwx5ty.d.ts → mod-B8MI1mpi.d.ts} +1 -1
- package/dist/{mod-B3OBLj5g.d.ts → mod-BhTaDR5H.d.ts} +1 -1
- package/dist/{mod-D58KCqUg.d.cts → mod-C7_2j7VT.d.cts} +1 -1
- package/dist/{mod-Dp3Ij7sw.d.cts → mod-DFL6IxYQ.d.cts} +1 -1
- package/dist/mod.cjs +4 -4
- package/dist/mod.d.cts +6 -6
- package/dist/mod.d.ts +6 -6
- package/dist/mod.js +4 -4
- package/dist/nodeinfo/client.test.mjs +1 -1
- package/dist/nodeinfo/handler.test.mjs +2 -2
- package/dist/otel/mod.d.cts +1 -1
- package/dist/otel/mod.d.ts +1 -1
- package/dist/{owner-DpxyMeo9.mjs → owner-D_BDPwm2.mjs} +3 -3
- package/dist/{portable-collection-BvJ5om91.js → portable-collection-C0c0rUN9.js} +1 -1
- package/dist/{portable-collection-DyJwtPz1.cjs → portable-collection-DwJ8YJP2.cjs} +1 -1
- package/dist/{portable-collection-DOL3l2C9.mjs → portable-collection-hcONGyUs.mjs} +2 -2
- package/dist/{portable-key-B8kTV4n8.mjs → portable-key-B880yVlC.mjs} +1 -1
- package/dist/{proof-B9DEHHiO.mjs → proof-z6Prgi5I.mjs} +4 -4
- package/dist/{send-CZckda2X.mjs → send-Cw545L_2.mjs} +191 -10
- 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 +3 -3
- package/dist/sig/compound-proof.test.mjs +3 -3
- package/dist/sig/http.test.mjs +3 -3
- package/dist/sig/key-owner-evidence.test.mjs +2 -2
- package/dist/sig/key.test.mjs +2 -2
- package/dist/sig/ld.test.mjs +2 -2
- package/dist/sig/mod.cjs +2 -2
- package/dist/sig/mod.js +2 -2
- package/dist/sig/owner.test.mjs +1 -1
- package/dist/sig/portable-collection.test.mjs +2 -2
- package/dist/sig/portable-key-id.test.mjs +2 -2
- package/dist/sig/portable-key.test.mjs +7 -7
- package/dist/sig/proof.test.mjs +4 -4
- package/dist/{temporal-Cc1ymEup.mjs → temporal-Cy-8WCod.mjs} +1 -1
- package/dist/testing/kv.test.mjs +1 -1
- package/dist/testing/mod.d.mts +82 -2
- package/dist/testing/mod.mjs +2 -2
- package/dist/{testing-C1itIjqW.mjs → testing-t8xDeJZ5.mjs} +1 -1
- package/dist/utils/docloader.test.mjs +6 -6
- package/dist/utils/kv-cache.test.mjs +2 -2
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.d.cts +1 -1
- package/dist/utils/mod.d.ts +1 -1
- package/dist/utils/mod.js +1 -1
- package/package.json +7 -7
|
@@ -2,16 +2,16 @@ import { Temporal } from "temporal-polyfill";
|
|
|
2
2
|
import { URLPattern } from "urlpattern-polyfill";
|
|
3
3
|
import { t as __exportAll } from "./rolldown-runtime-BC-Eo5Mj.js";
|
|
4
4
|
import { r as getDefaultActivityTransformers } from "./transformers-DAtXl_Kc.js";
|
|
5
|
-
import { $ as instrumentDocumentLoader, A as hasSignature, At as name, C as InvalidContextReferenceError, Ct as hasPortableActor, Dt as isPortableUri, E as compactJsonLd, Et as isPortableKeyId, F as verifyCompactJsonLd, G as validateCryptoKey, M as isClearlyMalformedContextReference, N as isInvalidUrlTypeError, O as detachSignature, Ot as isSameObjectId, P as signJsonLd, Q as getRemoteHost, R as wrapContextLoaderForJsonLd, S as normalizeOutgoingActivityJsonLd, St as getRawCanonicalPortableId, Tt as isPortableId, W as importJwk, X as getDurationMs, Z as getFederationMetrics, _ as verifyObject, at as recordCollectionTotalItems, b as verifyPortableObjectProofWithRoot, bt as getGatewayKeyBase, c as verifyRequestDetailed, ct as recordInboxActivity, d as InvalidPortableObjectIdError, dt as recordWebFingerHandle, et as isAbortError, f as classifyFep2277CoreType, ft as registerQueueDepthGauge, g as verifyMapLocalProof, h as signObject, i as selectRequestSignature, it as recordCollectionRequest, j as hasSignatureLike, jt as version, k as getNormalizationContextLoader, lt as recordOutboxActivity, m as hasProofLike, mt as formatAcceptSignature, n as listRequestSignatures, nt as recordCollectionDispatchDuration, o as validateMaxSignatures, pt as preloadedOnlyDocumentLoader, q as MAX_PORTABLE_KEY_TTL, r as parseRfc9421SignatureInput, rt as recordCollectionPageItems, s as verifyRequest, st as recordFanoutRecipients, t as doubleKnock, tt as recordCircuitBreakerStateChange, ut as recordOutboxEnqueue, v as verifyPortableObjectProof, vt as getAuthenticationOrigin, w as assertSafeJsonLd, wt as isCompatibleKeyId, xt as getPortableDid, y as verifyPortableObjectProofPolicy, yt as getCanonicalPortableId, z as exportJwk } from "./http-
|
|
5
|
+
import { $ as instrumentDocumentLoader, A as hasSignature, At as name, C as InvalidContextReferenceError, Ct as hasPortableActor, Dt as isPortableUri, E as compactJsonLd, Et as isPortableKeyId, F as verifyCompactJsonLd, G as validateCryptoKey, M as isClearlyMalformedContextReference, N as isInvalidUrlTypeError, O as detachSignature, Ot as isSameObjectId, P as signJsonLd, Q as getRemoteHost, R as wrapContextLoaderForJsonLd, S as normalizeOutgoingActivityJsonLd, St as getRawCanonicalPortableId, Tt as isPortableId, W as importJwk, X as getDurationMs, Z as getFederationMetrics, _ as verifyObject, at as recordCollectionTotalItems, b as verifyPortableObjectProofWithRoot, bt as getGatewayKeyBase, c as verifyRequestDetailed, ct as recordInboxActivity, d as InvalidPortableObjectIdError, dt as recordWebFingerHandle, et as isAbortError, f as classifyFep2277CoreType, ft as registerQueueDepthGauge, g as verifyMapLocalProof, h as signObject, i as selectRequestSignature, it as recordCollectionRequest, j as hasSignatureLike, jt as version, k as getNormalizationContextLoader, lt as recordOutboxActivity, m as hasProofLike, mt as formatAcceptSignature, n as listRequestSignatures, nt as recordCollectionDispatchDuration, o as validateMaxSignatures, pt as preloadedOnlyDocumentLoader, q as MAX_PORTABLE_KEY_TTL, r as parseRfc9421SignatureInput, rt as recordCollectionPageItems, s as verifyRequest, st as recordFanoutRecipients, t as doubleKnock, tt as recordCircuitBreakerStateChange, ut as recordOutboxEnqueue, v as verifyPortableObjectProof, vt as getAuthenticationOrigin, w as assertSafeJsonLd, wt as isCompatibleKeyId, xt as getPortableDid, y as verifyPortableObjectProofPolicy, yt as getCanonicalPortableId, z as exportJwk } from "./http-Dx7yMmfw.js";
|
|
6
6
|
import { a as MAX_BODY_SIZE, i as BodyTooLargeError, n as getNodeInfo, o as readBoundedText, t as nodeInfoToJson } from "./types-_S2_MeLF.js";
|
|
7
|
-
import { n as doesActorOwnKey, r as getKeyOwner, t as verifyPortableObject } from "./portable-collection-
|
|
8
|
-
import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-
|
|
7
|
+
import { n as doesActorOwnKey, r as getKeyOwner, t as verifyPortableObject } from "./portable-collection-C0c0rUN9.js";
|
|
8
|
+
import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-CUZOCSKZ.js";
|
|
9
9
|
import { getLogger, withContext } from "@logtape/logtape";
|
|
10
10
|
import { Router, RouterError, assertPath } from "@fedify/uri-template";
|
|
11
|
-
import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, isActor, lookupObject, traverseCollection } from "@fedify/vocab";
|
|
11
|
+
import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, PUBLIC_COLLECTION, Tombstone, getTypeId, isActor, lookupObject, traverseCollection } from "@fedify/vocab";
|
|
12
12
|
import { SpanKind, SpanStatusCode, context, metrics, propagation, trace } from "@opentelemetry/api";
|
|
13
13
|
import { FetchError, UrlError, canonicalizePortableUri, decodeMultibase, formatIri, fromCompatibleEf61Id, getDocumentLoader, getFe34Origin, isGatewayUrl, parseDigestMultibase, parseHashlink, parseIri, preloadedContexts, resolveDocumentLoaderTimeout, toCompatibleEf61Id, validatePublicUrl } from "@fedify/vocab-runtime";
|
|
14
|
-
import { isCompatibleEf61Iri } from "@fedify/vocab-runtime/internal/portable-dereference";
|
|
14
|
+
import { getPortableGatewayCandidates, isCompatibleEf61Iri } from "@fedify/vocab-runtime/internal/portable-dereference";
|
|
15
15
|
import { encodeHex } from "byte-encodings/hex";
|
|
16
16
|
import jsonld from "@fedify/vocab-runtime/jsonld";
|
|
17
17
|
import { uniq } from "es-toolkit";
|
|
@@ -118,6 +118,7 @@ var FederationBuilderImpl = class {
|
|
|
118
118
|
* Symbol registry for unique identification of unnamed symbols.
|
|
119
119
|
*/
|
|
120
120
|
#symbolRegistry = /* @__PURE__ */ new Map();
|
|
121
|
+
#collectionNames = /* @__PURE__ */ new Map();
|
|
121
122
|
constructor() {
|
|
122
123
|
this.router = new Router();
|
|
123
124
|
this.objectCallbacks = {};
|
|
@@ -147,6 +148,8 @@ var FederationBuilderImpl = class {
|
|
|
147
148
|
f.objectTypeIds = { ...this.objectTypeIds };
|
|
148
149
|
f.collectionCallbacks = { ...this.collectionCallbacks };
|
|
149
150
|
f.collectionTypeIds = { ...this.collectionTypeIds };
|
|
151
|
+
f.#symbolRegistry = new Map(this.#symbolRegistry);
|
|
152
|
+
f.#collectionNames = new Map(this.#collectionNames);
|
|
150
153
|
f.inboxPath = this.inboxPath;
|
|
151
154
|
f.outboxPath = this.outboxPath;
|
|
152
155
|
f.inboxCallbacks = this.inboxCallbacks == null ? void 0 : { ...this.inboxCallbacks };
|
|
@@ -678,6 +681,7 @@ var FederationBuilderImpl = class {
|
|
|
678
681
|
if (this.collectionCallbacks[name] != null) throw new RouterError(`Collection dispatcher for ${strName} already set.`);
|
|
679
682
|
assertPath(path);
|
|
680
683
|
if (Router.variables(path).size < 1) throw new RouterError("Path for collection dispatcher must have at least one variable.");
|
|
684
|
+
this.#collectionNames.set(routeName, name);
|
|
681
685
|
this.router.add(path, routeName);
|
|
682
686
|
const callbacks = { dispatcher };
|
|
683
687
|
this.collectionCallbacks[name] = callbacks;
|
|
@@ -719,6 +723,10 @@ var FederationBuilderImpl = class {
|
|
|
719
723
|
const routeName = this.#uniqueCollectionId(name);
|
|
720
724
|
return this.router.build(`collection:${routeName}`, values) ?? this.router.build(`orderedCollection:${routeName}`, values);
|
|
721
725
|
}
|
|
726
|
+
/** Resolves a custom collection route to its original name. */
|
|
727
|
+
getCollectionName(routeName) {
|
|
728
|
+
return this.#collectionNames.get(routeName) ?? routeName.replace(/^(collection|orderedCollection):/, "");
|
|
729
|
+
}
|
|
722
730
|
setOutboxPermanentFailureHandler(handler) {
|
|
723
731
|
this.outboxPermanentFailureHandler = handler;
|
|
724
732
|
}
|
|
@@ -2358,6 +2366,95 @@ async function checkServedPortableMap({ map, contexts }, options) {
|
|
|
2358
2366
|
return UNSECURED_CORE_TYPES.has(type) ? null : "missingProof";
|
|
2359
2367
|
}
|
|
2360
2368
|
//#endregion
|
|
2369
|
+
//#region src/federation/audience.ts
|
|
2370
|
+
const AS_NAMESPACE = "https://www.w3.org/ns/activitystreams#";
|
|
2371
|
+
/**
|
|
2372
|
+
* The addressing properties that determine the audience of an object.
|
|
2373
|
+
*/
|
|
2374
|
+
const ADDRESSING_PROPERTIES = [
|
|
2375
|
+
"to",
|
|
2376
|
+
"cc",
|
|
2377
|
+
"bto",
|
|
2378
|
+
"bcc",
|
|
2379
|
+
"audience"
|
|
2380
|
+
];
|
|
2381
|
+
/**
|
|
2382
|
+
* The IDs that Fedify treats as the public collection. Besides the full IRI,
|
|
2383
|
+
* the compact forms `as:Public` and `Public` are reserved as public for
|
|
2384
|
+
* compatibility with documents whose compact forms survive JSON-LD expansion,
|
|
2385
|
+
* e.g., bare `Public`, which expands to a relative IRI.
|
|
2386
|
+
*/
|
|
2387
|
+
const PUBLIC_IDS = /* @__PURE__ */ new Set([
|
|
2388
|
+
PUBLIC_COLLECTION.href,
|
|
2389
|
+
"as:Public",
|
|
2390
|
+
"Public"
|
|
2391
|
+
]);
|
|
2392
|
+
/**
|
|
2393
|
+
* Checks whether an expanded JSON-LD node is addressed to the public
|
|
2394
|
+
* collection, i.e., any of its `to`, `cc`, `bto`, `bcc`, and `audience` has
|
|
2395
|
+
* the public collection. Only the node itself is checked, not the nodes
|
|
2396
|
+
* embedded in it.
|
|
2397
|
+
* @param node The expanded JSON-LD node.
|
|
2398
|
+
* @returns `true` if the node is publicly addressed.
|
|
2399
|
+
* @internal
|
|
2400
|
+
*/
|
|
2401
|
+
function isPubliclyAddressedNode(node) {
|
|
2402
|
+
for (const property of ADDRESSING_PROPERTIES) {
|
|
2403
|
+
const values = node[AS_NAMESPACE + property];
|
|
2404
|
+
if (!Array.isArray(values)) continue;
|
|
2405
|
+
for (const value of values) if (typeof value === "object" && value != null && "@id" in value && typeof value["@id"] === "string" && PUBLIC_IDS.has(value["@id"])) return true;
|
|
2406
|
+
}
|
|
2407
|
+
return false;
|
|
2408
|
+
}
|
|
2409
|
+
function getAudienceIds(object) {
|
|
2410
|
+
return [
|
|
2411
|
+
...object.toIds,
|
|
2412
|
+
...object.ccIds,
|
|
2413
|
+
...object.btoIds,
|
|
2414
|
+
...object.bccIds,
|
|
2415
|
+
...object.audienceIds
|
|
2416
|
+
];
|
|
2417
|
+
}
|
|
2418
|
+
/**
|
|
2419
|
+
* Checks whether two actor IDs refer to the same actor. [FEP-ef61] portable
|
|
2420
|
+
* IDs are compared by their canonical forms, so an `ap:` URI, an `ap+ef61:`
|
|
2421
|
+
* URI, and a compatible identifier on any gateway are the same actor if they
|
|
2422
|
+
* have the same canonical portable ID. Other IDs are compared as they are.
|
|
2423
|
+
*
|
|
2424
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
2425
|
+
*/
|
|
2426
|
+
function isSameActorId(a, b) {
|
|
2427
|
+
const canonicalA = getCanonicalPortableId(a);
|
|
2428
|
+
const canonicalB = getCanonicalPortableId(b);
|
|
2429
|
+
if (canonicalA != null || canonicalB != null) return canonicalA === canonicalB;
|
|
2430
|
+
return a.href === b.href;
|
|
2431
|
+
}
|
|
2432
|
+
/**
|
|
2433
|
+
* Checks whether an actor belongs to the audience of an object: the object is
|
|
2434
|
+
* publicly addressed, the actor is one of its addressees, or the actor is
|
|
2435
|
+
* a member of one of them according to `isMember()`.
|
|
2436
|
+
* @param object The object whose audience is checked.
|
|
2437
|
+
* @param actor The actor to check, or `null` if there is no actor. No actor
|
|
2438
|
+
* belongs to the audience of an object that is not public.
|
|
2439
|
+
* @param options Options for checking the audience.
|
|
2440
|
+
* @returns `true` if the actor belongs to the audience of the object.
|
|
2441
|
+
* @internal
|
|
2442
|
+
*/
|
|
2443
|
+
async function isInAudience(object, actor, options = {}) {
|
|
2444
|
+
const addressees = getAudienceIds(object);
|
|
2445
|
+
if (addressees.some((id) => PUBLIC_IDS.has(id.href))) return true;
|
|
2446
|
+
const actorId = actor?.id;
|
|
2447
|
+
if (actor == null || actorId == null) return false;
|
|
2448
|
+
const others = [];
|
|
2449
|
+
for (const addressee of addressees) {
|
|
2450
|
+
if (isSameActorId(addressee, actorId)) return true;
|
|
2451
|
+
others.push(addressee);
|
|
2452
|
+
}
|
|
2453
|
+
if (options.isMember == null) return false;
|
|
2454
|
+
for (const addressee of others) if (await options.isMember(addressee, actor)) return true;
|
|
2455
|
+
return false;
|
|
2456
|
+
}
|
|
2457
|
+
//#endregion
|
|
2361
2458
|
//#region src/federation/inbox.ts
|
|
2362
2459
|
async function routeActivity({ context: ctx, json, originalJson, normalizedActivity, ldSignatureVerified, activity, recipient, inboxListeners, inboxContextFactory, listenerInboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, meterProvider, tracerProvider, idempotencyStrategy }) {
|
|
2363
2460
|
const logger = getLogger([
|
|
@@ -2980,31 +3077,177 @@ function parseHashlinkGatewayRequest(url) {
|
|
|
2980
3077
|
})
|
|
2981
3078
|
};
|
|
2982
3079
|
}
|
|
3080
|
+
/**
|
|
3081
|
+
* Resolves a portable inbox, i.e., an `ap:` or `ap+ef61:` URI, to
|
|
3082
|
+
* the compatible identifiers of the inbox on the recipient's gateways.
|
|
3083
|
+
*
|
|
3084
|
+
* The gateways are taken from the recipient's `gateways`, or, if it has no
|
|
3085
|
+
* valid gateway, from the `@gateway` location hints of the inbox URI. Both
|
|
3086
|
+
* come from the recipient object, so they are as trustworthy as its inbox.
|
|
3087
|
+
* Gateways whose origins are in `excludeBaseUris` are skipped.
|
|
3088
|
+
* @param recipient The recipient that owns the inbox.
|
|
3089
|
+
* @param inbox The portable inbox.
|
|
3090
|
+
* @param excludeBaseUris The base URIs whose origins are not delivered to.
|
|
3091
|
+
* @returns The delivery target, or `null` if the inbox is malformed, or no
|
|
3092
|
+
* gateway is left to deliver through.
|
|
3093
|
+
*/
|
|
3094
|
+
function resolvePortableDeliveryTarget(recipient, inbox, excludeBaseUris) {
|
|
3095
|
+
const logger = getLogger([
|
|
3096
|
+
"fedify",
|
|
3097
|
+
"federation",
|
|
3098
|
+
"outbox"
|
|
3099
|
+
]);
|
|
3100
|
+
let portableInbox;
|
|
3101
|
+
try {
|
|
3102
|
+
portableInbox = canonicalizePortableUri(inbox.href);
|
|
3103
|
+
} catch (error) {
|
|
3104
|
+
if (!(error instanceof TypeError)) throw error;
|
|
3105
|
+
logger.warn("Skipping the recipient {recipient}, as its portable inbox {inbox} is malformed.", {
|
|
3106
|
+
recipient: recipient.id?.href,
|
|
3107
|
+
inbox: inbox.href,
|
|
3108
|
+
error
|
|
3109
|
+
});
|
|
3110
|
+
return null;
|
|
3111
|
+
}
|
|
3112
|
+
let gateways = (recipient.gateways ?? []).filter(isGatewayUrl);
|
|
3113
|
+
if (gateways.length < 1) gateways = getPortableGatewayCandidates(inbox);
|
|
3114
|
+
if (gateways.length < 1) {
|
|
3115
|
+
logger.warn("Skipping the recipient {recipient}, as it has no gateway to deliver to its portable inbox {inbox} through.", {
|
|
3116
|
+
recipient: recipient.id?.href,
|
|
3117
|
+
inbox: portableInbox
|
|
3118
|
+
});
|
|
3119
|
+
return null;
|
|
3120
|
+
}
|
|
3121
|
+
const excludedOrigins = new Set(excludeBaseUris?.map((u) => u.origin));
|
|
3122
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3123
|
+
const inboxes = [];
|
|
3124
|
+
for (const gateway of gateways) {
|
|
3125
|
+
if (inboxes.length >= 5) break;
|
|
3126
|
+
if (seen.has(gateway.origin)) continue;
|
|
3127
|
+
seen.add(gateway.origin);
|
|
3128
|
+
if (excludedOrigins.has(gateway.origin)) continue;
|
|
3129
|
+
try {
|
|
3130
|
+
inboxes.push(toCompatibleEf61Id(inbox, gateway.origin));
|
|
3131
|
+
} catch (error) {
|
|
3132
|
+
if (!(error instanceof TypeError)) throw error;
|
|
3133
|
+
logger.warn("Cannot deliver to the portable inbox {inbox} through the gateway {gateway}, as the inbox cannot be represented as its compatible identifier.", {
|
|
3134
|
+
inbox: portableInbox,
|
|
3135
|
+
gateway: gateway.origin,
|
|
3136
|
+
error
|
|
3137
|
+
});
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
if (inboxes.length < 1) {
|
|
3141
|
+
logger.debug("Skipping the recipient {recipient}, as no gateway is left to deliver to its portable inbox {inbox} through.", {
|
|
3142
|
+
recipient: recipient.id?.href,
|
|
3143
|
+
inbox: portableInbox
|
|
3144
|
+
});
|
|
3145
|
+
return null;
|
|
3146
|
+
}
|
|
3147
|
+
return {
|
|
3148
|
+
portableInbox,
|
|
3149
|
+
inboxes
|
|
3150
|
+
};
|
|
3151
|
+
}
|
|
3152
|
+
/**
|
|
3153
|
+
* Merges two ordered lists of compatible inbox URLs, keeping the first URL
|
|
3154
|
+
* for each gateway origin and at most {@link MAX_DELIVERY_GATEWAYS} URLs.
|
|
3155
|
+
* @param a The preferred list.
|
|
3156
|
+
* @param b The other list.
|
|
3157
|
+
* @returns The merged list.
|
|
3158
|
+
*/
|
|
3159
|
+
function mergeGatewayInboxes(a, b) {
|
|
3160
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3161
|
+
const merged = [];
|
|
3162
|
+
for (const inbox of [...a, ...b]) {
|
|
3163
|
+
if (merged.length >= 5) break;
|
|
3164
|
+
const { origin } = new URL(inbox);
|
|
3165
|
+
if (seen.has(origin)) continue;
|
|
3166
|
+
seen.add(origin);
|
|
3167
|
+
merged.push(inbox);
|
|
3168
|
+
}
|
|
3169
|
+
return merged;
|
|
3170
|
+
}
|
|
3171
|
+
/**
|
|
3172
|
+
* Gets the part of a delivery's ordering key that tells its destination
|
|
3173
|
+
* apart: the DID of a portable inbox, so that deliveries to it stay in order
|
|
3174
|
+
* whichever gateway they go through, or the origin of any other inbox.
|
|
3175
|
+
* @param inbox The inbox URL.
|
|
3176
|
+
* @param portableInbox The canonical portable inbox, if the inbox is
|
|
3177
|
+
* the compatible identifier of one.
|
|
3178
|
+
* @returns The destination part of the ordering key.
|
|
3179
|
+
*/
|
|
3180
|
+
function getOrderingDestination(inbox, portableInbox) {
|
|
3181
|
+
return portableInbox == null ? new URL(inbox).origin : getFe34Origin(portableInbox);
|
|
3182
|
+
}
|
|
2983
3183
|
//#endregion
|
|
2984
3184
|
//#region src/federation/send.ts
|
|
2985
3185
|
/**
|
|
2986
3186
|
* Extracts the inbox URLs from recipients.
|
|
3187
|
+
*
|
|
3188
|
+
* An [FEP-ef61] portable inbox, i.e., an `ap:` or `ap+ef61:` URI, is resolved
|
|
3189
|
+
* to its compatible identifiers on the recipient's gateways, or, if the
|
|
3190
|
+
* recipient has no valid gateway, on the gateways in the `@gateway` location
|
|
3191
|
+
* hints of the inbox URI. The inbox is keyed by the compatible identifier on
|
|
3192
|
+
* the first gateway, and the others are listed in
|
|
3193
|
+
* {@link ExtractedInbox.gatewayInboxes}. A recipient whose portable inbox
|
|
3194
|
+
* has no gateway to deliver through is skipped. The shared inbox of
|
|
3195
|
+
* a recipient with a portable inbox is never preferred, since only a portable
|
|
3196
|
+
* inbox is synchronized between the gateways.
|
|
3197
|
+
*
|
|
3198
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
2987
3199
|
* @param parameters The parameters to extract the inboxes.
|
|
2988
3200
|
* See also {@link ExtractInboxesParameters}.
|
|
2989
3201
|
* @returns The inboxes as a map of inbox URL to actor URIs.
|
|
2990
3202
|
*/
|
|
2991
3203
|
function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris }) {
|
|
2992
3204
|
const inboxes = {};
|
|
3205
|
+
const portableKeys = /* @__PURE__ */ new Map();
|
|
3206
|
+
const compatibleKeys = /* @__PURE__ */ new Map();
|
|
2993
3207
|
for (const recipient of recipients) {
|
|
3208
|
+
if (recipient.id == null) continue;
|
|
2994
3209
|
let inbox;
|
|
2995
3210
|
let sharedInbox = false;
|
|
2996
|
-
if (preferSharedInbox && recipient.endpoints?.sharedInbox != null) {
|
|
3211
|
+
if (preferSharedInbox && recipient.endpoints?.sharedInbox != null && (recipient.inboxId == null || !isPortableUri(recipient.inboxId))) {
|
|
2997
3212
|
inbox = recipient.endpoints.sharedInbox;
|
|
2998
3213
|
sharedInbox = true;
|
|
2999
3214
|
} else inbox = recipient.inboxId;
|
|
3000
|
-
if (inbox
|
|
3001
|
-
|
|
3002
|
-
|
|
3215
|
+
if (inbox == null) continue;
|
|
3216
|
+
if (isPortableUri(inbox)) {
|
|
3217
|
+
const target = resolvePortableDeliveryTarget(recipient, inbox, excludeBaseUris);
|
|
3218
|
+
if (target == null) continue;
|
|
3219
|
+
const gatewayInboxes = target.inboxes.map((u) => u.href);
|
|
3220
|
+
const existingKeys = /* @__PURE__ */ new Set();
|
|
3221
|
+
const portableKey = portableKeys.get(target.portableInbox);
|
|
3222
|
+
if (portableKey != null) existingKeys.add(portableKey);
|
|
3223
|
+
for (const i of gatewayInboxes) {
|
|
3224
|
+
const k = compatibleKeys.get(i) ?? i;
|
|
3225
|
+
if (Object.hasOwn(inboxes, k)) existingKeys.add(k);
|
|
3226
|
+
}
|
|
3227
|
+
const [key = gatewayInboxes[0], ...otherKeys] = existingKeys;
|
|
3228
|
+
portableKeys.set(target.portableInbox, key);
|
|
3229
|
+
const entry = inboxes[key] ??= {
|
|
3003
3230
|
actorIds: /* @__PURE__ */ new Set(),
|
|
3004
3231
|
sharedInbox
|
|
3005
3232
|
};
|
|
3006
|
-
|
|
3233
|
+
for (const k of otherKeys) {
|
|
3234
|
+
for (const actorId of inboxes[k].actorIds) entry.actorIds.add(actorId);
|
|
3235
|
+
delete inboxes[k];
|
|
3236
|
+
}
|
|
3237
|
+
entry.sharedInbox = false;
|
|
3238
|
+
entry.portableInbox = target.portableInbox;
|
|
3239
|
+
entry.gatewayInboxes = mergeGatewayInboxes(entry.gatewayInboxes ?? [key], gatewayInboxes);
|
|
3240
|
+
for (const i of entry.gatewayInboxes) compatibleKeys.set(i, key);
|
|
3241
|
+
entry.actorIds.add(recipient.id.href);
|
|
3242
|
+
continue;
|
|
3007
3243
|
}
|
|
3244
|
+
if (excludeBaseUris != null && excludeBaseUris.some((u) => u.origin === inbox?.origin)) continue;
|
|
3245
|
+
const key = compatibleKeys.get(inbox.href) ?? inbox.href;
|
|
3246
|
+
inboxes[key] ??= {
|
|
3247
|
+
actorIds: /* @__PURE__ */ new Set(),
|
|
3248
|
+
sharedInbox
|
|
3249
|
+
};
|
|
3250
|
+
inboxes[key].actorIds.add(recipient.id.href);
|
|
3008
3251
|
}
|
|
3009
3252
|
return inboxes;
|
|
3010
3253
|
}
|
|
@@ -3039,6 +3282,38 @@ function sendActivity(options) {
|
|
|
3039
3282
|
}
|
|
3040
3283
|
});
|
|
3041
3284
|
}
|
|
3285
|
+
/**
|
|
3286
|
+
* Sends an activity to the first of the given inboxes that accepts it, i.e.,
|
|
3287
|
+
* the compatible identifiers of a portable inbox on its gateways, trying them
|
|
3288
|
+
* one after another.
|
|
3289
|
+
* @param parameters The parameters, except for the inbox.
|
|
3290
|
+
* @param inboxes The inboxes to try, in order. Must not be empty.
|
|
3291
|
+
* @throws {Error} The error of the last inbox if no inbox accepts it.
|
|
3292
|
+
*/
|
|
3293
|
+
async function sendActivityThroughGateways(parameters, inboxes) {
|
|
3294
|
+
const logger = getLogger([
|
|
3295
|
+
"fedify",
|
|
3296
|
+
"federation",
|
|
3297
|
+
"outbox"
|
|
3298
|
+
]);
|
|
3299
|
+
let lastError;
|
|
3300
|
+
for (const [i, inbox] of inboxes.entries()) try {
|
|
3301
|
+
await sendActivity({
|
|
3302
|
+
...parameters,
|
|
3303
|
+
inbox
|
|
3304
|
+
});
|
|
3305
|
+
return;
|
|
3306
|
+
} catch (error) {
|
|
3307
|
+
lastError = error;
|
|
3308
|
+
if (i < inboxes.length - 1) logger.warn("Failed to send activity {activityId} to {inbox}; trying the next gateway {next}:\n{error}", {
|
|
3309
|
+
activityId: parameters.activityId,
|
|
3310
|
+
inbox: inbox.href,
|
|
3311
|
+
next: inboxes[i + 1].href,
|
|
3312
|
+
error
|
|
3313
|
+
});
|
|
3314
|
+
}
|
|
3315
|
+
throw lastError;
|
|
3316
|
+
}
|
|
3042
3317
|
const MAX_ERROR_RESPONSE_BODY_BYTES = 1024;
|
|
3043
3318
|
function getActivityActorId(activity) {
|
|
3044
3319
|
if (!isRecord(activity)) return void 0;
|
|
@@ -3891,6 +4166,15 @@ async function handleObject(request, { values, context, objectDispatcher, author
|
|
|
3891
4166
|
* only if its ID canonically matches the requested portable ID and it
|
|
3892
4167
|
* satisfies the FEP-ef61 proof policy.
|
|
3893
4168
|
*
|
|
4169
|
+
* FEP-ef61 forbids serving a non-public object to anyone but its intended
|
|
4170
|
+
* audience. If the dispatcher has an authorization predicate, it decides who
|
|
4171
|
+
* may retrieve the object, and the response is marked with
|
|
4172
|
+
* `Cache-Control: private`, as it depends on the requester. Otherwise, only
|
|
4173
|
+
* a publicly addressed object is served, and any other object is responded
|
|
4174
|
+
* to with `404 Not Found`, as if this server did not store it. Actors, and
|
|
4175
|
+
* anything the actor dispatcher returns, are exempt from the latter, as
|
|
4176
|
+
* actors have no audience.
|
|
4177
|
+
*
|
|
3894
4178
|
* A {@link Tombstone} is served with `410 Gone` if it has an Object Integrity
|
|
3895
4179
|
* Proof made with a key of the DID in its ID. An unsigned tombstone cannot
|
|
3896
4180
|
* be served as a portable object, so the response is the same as for
|
|
@@ -3900,7 +4184,7 @@ async function handleObject(request, { values, context, objectDispatcher, author
|
|
|
3900
4184
|
* @param parameters The parameters for handling the portable object.
|
|
3901
4185
|
* @returns A promise that resolves to an HTTP response.
|
|
3902
4186
|
*/
|
|
3903
|
-
async function handlePortableObject(request, { values, context, objectDispatcher, authorizePredicate, canonicalId, onNotFound, onUnauthorized }) {
|
|
4187
|
+
async function handlePortableObject(request, { values, context, objectDispatcher, authorizePredicate, canonicalId, kind, onNotFound, onUnauthorized }) {
|
|
3904
4188
|
const logger = getLogger([
|
|
3905
4189
|
"fedify",
|
|
3906
4190
|
"federation",
|
|
@@ -3919,14 +4203,32 @@ async function handlePortableObject(request, { values, context, objectDispatcher
|
|
|
3919
4203
|
if (authorizePredicate != null) {
|
|
3920
4204
|
if (!await authorizePredicate(context, values)) return await onUnauthorized(request);
|
|
3921
4205
|
}
|
|
4206
|
+
const contextLoader = getNormalizationContextLoader(context.contextLoader);
|
|
3922
4207
|
let jsonLd;
|
|
3923
|
-
let
|
|
3924
|
-
let result;
|
|
4208
|
+
let root;
|
|
3925
4209
|
try {
|
|
3926
4210
|
jsonLd = await object.toJsonLd(context);
|
|
3927
|
-
|
|
4211
|
+
root = await expandJsonLdRoot(jsonLd, contextLoader);
|
|
4212
|
+
} catch (error) {
|
|
4213
|
+
logger.error("Failed to serialize the portable object {portableId}:\n{error}", {
|
|
4214
|
+
portableId: canonicalId,
|
|
4215
|
+
error
|
|
4216
|
+
});
|
|
4217
|
+
return portableObjectInternalServerError(request);
|
|
4218
|
+
}
|
|
4219
|
+
const servedId = root?.["@id"];
|
|
4220
|
+
if (root == null || typeof servedId !== "string" || !isRequestedPortableId(servedId, canonicalId)) {
|
|
4221
|
+
logger.debug("The serialized object does not match the requested portable object {portableId}.", { portableId: canonicalId });
|
|
4222
|
+
return await onNotFound(request);
|
|
4223
|
+
}
|
|
4224
|
+
if (authorizePredicate == null && kind === "object" && !isActor(object) && !isPubliclyAddressedNode(root)) {
|
|
4225
|
+
logger.debug("Not serving the portable object {portableId}, as it is not publicly addressed and its object dispatcher has no authorization predicate; responding as if this server did not store the object. Set an authorization predicate that checks if the request is signed by an actor in the object's audience to serve it.", { portableId: canonicalId });
|
|
4226
|
+
return await onNotFound(request);
|
|
4227
|
+
}
|
|
4228
|
+
let result;
|
|
4229
|
+
try {
|
|
3928
4230
|
result = await verifyPortableObjectProof(jsonLd, {
|
|
3929
|
-
contextLoader
|
|
4231
|
+
contextLoader,
|
|
3930
4232
|
documentLoader: context.documentLoader,
|
|
3931
4233
|
tracerProvider: context.tracerProvider,
|
|
3932
4234
|
meterProvider: context.meterProvider
|
|
@@ -3938,10 +4240,6 @@ async function handlePortableObject(request, { values, context, objectDispatcher
|
|
|
3938
4240
|
});
|
|
3939
4241
|
return portableObjectInternalServerError(request);
|
|
3940
4242
|
}
|
|
3941
|
-
if (servedId == null || !isRequestedPortableId(servedId, canonicalId)) {
|
|
3942
|
-
logger.debug("The serialized object does not match the requested portable object {portableId}.", { portableId: canonicalId });
|
|
3943
|
-
return await onNotFound(request);
|
|
3944
|
-
}
|
|
3945
4243
|
const tombstone = object instanceof Tombstone;
|
|
3946
4244
|
if (!result.verified) {
|
|
3947
4245
|
if (tombstone && result.reason.type === "missingProof") {
|
|
@@ -3960,25 +4258,26 @@ async function handlePortableObject(request, { values, context, objectDispatcher
|
|
|
3960
4258
|
return portableObjectInternalServerError(request);
|
|
3961
4259
|
}
|
|
3962
4260
|
}
|
|
4261
|
+
const headers = new Headers({
|
|
4262
|
+
"Content-Type": PORTABLE_OBJECT_CONTENT_TYPE,
|
|
4263
|
+
Vary: "Accept"
|
|
4264
|
+
});
|
|
4265
|
+
if (authorizePredicate != null) headers.set("Cache-Control", "private");
|
|
3963
4266
|
return new Response(request.method === "HEAD" ? null : JSON.stringify(jsonLd), {
|
|
3964
4267
|
status: tombstone ? 410 : 200,
|
|
3965
|
-
headers
|
|
3966
|
-
"Content-Type": PORTABLE_OBJECT_CONTENT_TYPE,
|
|
3967
|
-
Vary: "Accept"
|
|
3968
|
-
}
|
|
4268
|
+
headers
|
|
3969
4269
|
});
|
|
3970
4270
|
}
|
|
3971
4271
|
function isRequestedPortableObject(object, canonicalId) {
|
|
3972
4272
|
return object.id != null && getCanonicalPortableId(object.id) === canonicalId;
|
|
3973
4273
|
}
|
|
3974
|
-
async function
|
|
4274
|
+
async function expandJsonLdRoot(jsonLd, contextLoader) {
|
|
3975
4275
|
const expanded = await jsonld.expand(jsonLd, {
|
|
3976
|
-
documentLoader:
|
|
4276
|
+
documentLoader: contextLoader,
|
|
3977
4277
|
keepFreeFloatingNodes: true
|
|
3978
4278
|
});
|
|
3979
4279
|
if (expanded.length !== 1) return null;
|
|
3980
|
-
|
|
3981
|
-
return typeof id === "string" ? id : null;
|
|
4280
|
+
return expanded[0];
|
|
3982
4281
|
}
|
|
3983
4282
|
function isRequestedPortableId(id, canonicalId) {
|
|
3984
4283
|
if (!/^ap(?:\+ef61)?:\/\//i.test(id)) return URL.canParse(id) && getCanonicalPortableId(new URL(id)) === canonicalId;
|
|
@@ -7133,6 +7432,37 @@ function getBenchmarkTriggerOptions(benchmarkOptions) {
|
|
|
7133
7432
|
function maxDelay(first, second) {
|
|
7134
7433
|
return Temporal.Duration.compare(first, second) >= 0 ? first : second;
|
|
7135
7434
|
}
|
|
7435
|
+
/**
|
|
7436
|
+
* Gets the fields of a queued message that describe the FEP-ef61 portable
|
|
7437
|
+
* inbox an inbox stands for, if any, omitting the absent ones.
|
|
7438
|
+
*/
|
|
7439
|
+
function getPortableMessageFields(portableInbox, gatewayInboxes) {
|
|
7440
|
+
return {
|
|
7441
|
+
...portableInbox == null ? {} : { portableInbox },
|
|
7442
|
+
...gatewayInboxes == null ? {} : { gatewayInboxes: [...gatewayInboxes] }
|
|
7443
|
+
};
|
|
7444
|
+
}
|
|
7445
|
+
/**
|
|
7446
|
+
* Sends an activity to an inbox, or, if the inbox is the compatible
|
|
7447
|
+
* identifier of an FEP-ef61 portable inbox on more than one gateway, through
|
|
7448
|
+
* the first gateway that accepts it.
|
|
7449
|
+
*/
|
|
7450
|
+
function sendActivityToInbox(inbox, gatewayInboxes, parameters) {
|
|
7451
|
+
if (gatewayInboxes == null || gatewayInboxes.length < 2) return sendActivity({
|
|
7452
|
+
...parameters,
|
|
7453
|
+
inbox: new URL(inbox)
|
|
7454
|
+
});
|
|
7455
|
+
return sendActivityThroughGateways(parameters, gatewayInboxes.map((i) => new URL(i)));
|
|
7456
|
+
}
|
|
7457
|
+
function parseInstant(value) {
|
|
7458
|
+
if (value == null) return void 0;
|
|
7459
|
+
try {
|
|
7460
|
+
return Temporal.Instant.from(value);
|
|
7461
|
+
} catch (error) {
|
|
7462
|
+
if (error instanceof RangeError) return void 0;
|
|
7463
|
+
throw error;
|
|
7464
|
+
}
|
|
7465
|
+
}
|
|
7136
7466
|
function isTransportDeliveryError(error) {
|
|
7137
7467
|
return error instanceof FetchError || isAbortError(error);
|
|
7138
7468
|
}
|
|
@@ -7713,6 +8043,20 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
7713
8043
|
}
|
|
7714
8044
|
recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
|
|
7715
8045
|
};
|
|
8046
|
+
const { gatewayInboxes } = message;
|
|
8047
|
+
if (gatewayInboxes != null && gatewayInboxes.length > 1) {
|
|
8048
|
+
await this.#listenGatewayOutboxMessage(_, {
|
|
8049
|
+
...message,
|
|
8050
|
+
gatewayInboxes
|
|
8051
|
+
}, span, {
|
|
8052
|
+
keys,
|
|
8053
|
+
logData,
|
|
8054
|
+
getActorIds,
|
|
8055
|
+
parseActivity,
|
|
8056
|
+
dropHeldOutboxMessage
|
|
8057
|
+
});
|
|
8058
|
+
return;
|
|
8059
|
+
}
|
|
7716
8060
|
try {
|
|
7717
8061
|
const inbox = new URL(message.inbox);
|
|
7718
8062
|
const circuit = this.outboxQueue == null ? void 0 : this.circuitBreaker;
|
|
@@ -7935,6 +8279,308 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
7935
8279
|
}
|
|
7936
8280
|
logger.info("Successfully sent activity {activityId} to {inbox}.", { ...logData });
|
|
7937
8281
|
}
|
|
8282
|
+
/**
|
|
8283
|
+
* Delivers a queued activity to an FEP-ef61 portable inbox through the first
|
|
8284
|
+
* of its gateways that accepts it, trying them one after another in a single
|
|
8285
|
+
* round, so that the round counts as one attempt for the retry policy, keeps
|
|
8286
|
+
* the message's place in its ordering key, and is replayed as a whole by
|
|
8287
|
+
* queues that retry natively.
|
|
8288
|
+
*
|
|
8289
|
+
* A gateway that fails with a permanent failure status is not tried again.
|
|
8290
|
+
* A gateway whose circuit is open, or which asked to be retried later with
|
|
8291
|
+
* `Retry-After`, is skipped in this round. If no gateway accepts the
|
|
8292
|
+
* activity, the message is held while every remaining gateway is held, and
|
|
8293
|
+
* otherwise retried according to the retry policy.
|
|
8294
|
+
*/
|
|
8295
|
+
async #listenGatewayOutboxMessage(contextData, message, span, helpers) {
|
|
8296
|
+
const logger = getLogger([
|
|
8297
|
+
"fedify",
|
|
8298
|
+
"federation",
|
|
8299
|
+
"outbox"
|
|
8300
|
+
]);
|
|
8301
|
+
const circuitLogger = getLogger([
|
|
8302
|
+
"fedify",
|
|
8303
|
+
"federation",
|
|
8304
|
+
"circuit"
|
|
8305
|
+
]);
|
|
8306
|
+
const { keys, logData, getActorIds, parseActivity, dropHeldOutboxMessage } = helpers;
|
|
8307
|
+
const circuit = this.outboxQueue == null ? void 0 : this.circuitBreaker;
|
|
8308
|
+
const specDeterminer = new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl });
|
|
8309
|
+
const survivors = [];
|
|
8310
|
+
let lastError;
|
|
8311
|
+
let lastPermanent;
|
|
8312
|
+
let activity;
|
|
8313
|
+
const getActivity = async () => activity ??= await parseActivity();
|
|
8314
|
+
const drop = async (circuit, remoteHost, inbox, heldSince) => {
|
|
8315
|
+
await dropHeldOutboxMessage(circuit, remoteHost, inbox, heldSince, await getActivity());
|
|
8316
|
+
};
|
|
8317
|
+
const heldSince = parseInstant(message.circuitHeldSince);
|
|
8318
|
+
if (circuit != null && heldSince != null && Temporal.Instant.compare(heldSince.add(circuit.options.heldActivityTtl), Temporal.Now.instant()) <= 0) {
|
|
8319
|
+
const inbox = new URL(message.gatewayInboxes[0]);
|
|
8320
|
+
await drop(circuit, getRemoteHost(inbox), inbox, heldSince);
|
|
8321
|
+
return;
|
|
8322
|
+
}
|
|
8323
|
+
for (const inboxHref of message.gatewayInboxes) {
|
|
8324
|
+
const inbox = new URL(inboxHref);
|
|
8325
|
+
const remoteHost = getRemoteHost(inbox);
|
|
8326
|
+
const now = Temporal.Now.instant();
|
|
8327
|
+
const notBefore = parseInstant(message.gatewayNotBefore?.[inboxHref]);
|
|
8328
|
+
if (notBefore != null && Temporal.Instant.compare(notBefore, now) > 0) {
|
|
8329
|
+
survivors.push({
|
|
8330
|
+
inbox: inboxHref,
|
|
8331
|
+
holdDelay: now.until(notBefore),
|
|
8332
|
+
notBefore
|
|
8333
|
+
});
|
|
8334
|
+
continue;
|
|
8335
|
+
}
|
|
8336
|
+
let decision;
|
|
8337
|
+
if (circuit != null) try {
|
|
8338
|
+
decision = await circuit.beforeSend(remoteHost, message);
|
|
8339
|
+
} catch (circuitError) {
|
|
8340
|
+
circuitLogger.error("Failed to check circuit breaker state before sending; proceeding with delivery:\n{error}", {
|
|
8341
|
+
...logData,
|
|
8342
|
+
remoteHost,
|
|
8343
|
+
error: circuitError
|
|
8344
|
+
});
|
|
8345
|
+
}
|
|
8346
|
+
if (decision != null && circuit != null) {
|
|
8347
|
+
if (decision.type === "drop") {
|
|
8348
|
+
await drop(circuit, remoteHost, inbox, decision.heldSince);
|
|
8349
|
+
return;
|
|
8350
|
+
} else if (decision.type === "hold") {
|
|
8351
|
+
recordCircuitBreakerHeldSpanEvent(span, remoteHost, decision.state);
|
|
8352
|
+
survivors.push({
|
|
8353
|
+
inbox: inboxHref,
|
|
8354
|
+
holdDelay: decision.delay,
|
|
8355
|
+
circuitHeld: true
|
|
8356
|
+
});
|
|
8357
|
+
continue;
|
|
8358
|
+
} else if (decision.stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, decision.stateChange);
|
|
8359
|
+
}
|
|
8360
|
+
try {
|
|
8361
|
+
await sendActivity({
|
|
8362
|
+
allowPrivateAddress: this.allowPrivateAddress,
|
|
8363
|
+
keys,
|
|
8364
|
+
activity: message.activity,
|
|
8365
|
+
activityId: message.activityId,
|
|
8366
|
+
activityType: message.activityType,
|
|
8367
|
+
inbox,
|
|
8368
|
+
sharedInbox: message.sharedInbox,
|
|
8369
|
+
headers: new Headers(message.headers),
|
|
8370
|
+
specDeterminer,
|
|
8371
|
+
meterProvider: this.meterProvider,
|
|
8372
|
+
tracerProvider: this.tracerProvider
|
|
8373
|
+
});
|
|
8374
|
+
} catch (error) {
|
|
8375
|
+
lastError = error;
|
|
8376
|
+
const isPermanentFailure = error instanceof SendActivityError && this.permanentFailureStatusCodes.includes(error.statusCode);
|
|
8377
|
+
let retryAfter = !isPermanentFailure && error instanceof SendActivityError && (error.statusCode === 429 || error.statusCode === 503) ? parseRetryAfter(error.responseHeaders) : void 0;
|
|
8378
|
+
let retryAt;
|
|
8379
|
+
if (retryAfter != null) try {
|
|
8380
|
+
retryAt = Temporal.Now.instant().add(retryAfter);
|
|
8381
|
+
} catch (e) {
|
|
8382
|
+
if (!(e instanceof RangeError)) throw e;
|
|
8383
|
+
retryAfter = void 0;
|
|
8384
|
+
}
|
|
8385
|
+
let postDecision;
|
|
8386
|
+
if (circuit != null) try {
|
|
8387
|
+
if (error instanceof SendActivityError) {
|
|
8388
|
+
const { statusCode } = error;
|
|
8389
|
+
const stateChange = isPermanentFailure || statusCode === 429 || statusCode >= 400 && statusCode < 500 ? await circuit.recordReachableFailure(remoteHost) : statusCode >= 500 ? await circuit.recordFailure(remoteHost) : void 0;
|
|
8390
|
+
if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
|
|
8391
|
+
} else if (isTransportDeliveryError(error)) {
|
|
8392
|
+
const stateChange = await circuit.recordFailure(remoteHost);
|
|
8393
|
+
if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
|
|
8394
|
+
}
|
|
8395
|
+
if (!isPermanentFailure) postDecision = await circuit.beforeSend(remoteHost, message);
|
|
8396
|
+
} catch (circuitError) {
|
|
8397
|
+
circuitLogger.error("Failed to update circuit breaker state after delivery failure; falling back to normal failure handling:\n{error}", {
|
|
8398
|
+
...logData,
|
|
8399
|
+
remoteHost,
|
|
8400
|
+
error: circuitError
|
|
8401
|
+
});
|
|
8402
|
+
}
|
|
8403
|
+
span.addEvent("activitypub.delivery.failed", {
|
|
8404
|
+
"activitypub.remote.host": remoteHost,
|
|
8405
|
+
"activitypub.delivery.attempt": message.attempt,
|
|
8406
|
+
"activitypub.delivery.permanent_failure": isPermanentFailure,
|
|
8407
|
+
...error instanceof SendActivityError ? { "http.response.status_code": error.statusCode } : {}
|
|
8408
|
+
});
|
|
8409
|
+
try {
|
|
8410
|
+
await this.onOutboxError?.(error, await getActivity());
|
|
8411
|
+
} catch (handlerError) {
|
|
8412
|
+
logger.error("An unexpected error occurred in onError handler:\n{error}", {
|
|
8413
|
+
...logData,
|
|
8414
|
+
inbox: inboxHref,
|
|
8415
|
+
error: handlerError
|
|
8416
|
+
});
|
|
8417
|
+
}
|
|
8418
|
+
if (isPermanentFailure) {
|
|
8419
|
+
logger.warn("Permanent delivery failure for activity {activityId} to {inbox} ({status}); not trying the gateway again.", {
|
|
8420
|
+
...logData,
|
|
8421
|
+
inbox: inboxHref,
|
|
8422
|
+
status: error.statusCode
|
|
8423
|
+
});
|
|
8424
|
+
lastPermanent = {
|
|
8425
|
+
error,
|
|
8426
|
+
inbox
|
|
8427
|
+
};
|
|
8428
|
+
continue;
|
|
8429
|
+
}
|
|
8430
|
+
if (postDecision?.type === "drop" && circuit != null) {
|
|
8431
|
+
await drop(circuit, remoteHost, inbox, postDecision.heldSince);
|
|
8432
|
+
return;
|
|
8433
|
+
} else if (postDecision?.type === "hold") {
|
|
8434
|
+
recordCircuitBreakerHeldSpanEvent(span, remoteHost, postDecision.state);
|
|
8435
|
+
survivors.push({
|
|
8436
|
+
inbox: inboxHref,
|
|
8437
|
+
holdDelay: retryAfter == null ? postDecision.delay : maxDelay(postDecision.delay, retryAfter),
|
|
8438
|
+
circuitHeld: true,
|
|
8439
|
+
postFailure: true,
|
|
8440
|
+
notBefore: retryAt,
|
|
8441
|
+
retryAfter
|
|
8442
|
+
});
|
|
8443
|
+
} else survivors.push({
|
|
8444
|
+
inbox: inboxHref,
|
|
8445
|
+
notBefore: retryAt,
|
|
8446
|
+
retryAfter
|
|
8447
|
+
});
|
|
8448
|
+
logger.warn("Failed to send activity {activityId} to {inbox} (attempt #{attempt}); trying the next gateway if any:\n{error}", {
|
|
8449
|
+
...logData,
|
|
8450
|
+
inbox: inboxHref,
|
|
8451
|
+
error
|
|
8452
|
+
});
|
|
8453
|
+
continue;
|
|
8454
|
+
}
|
|
8455
|
+
if (circuit != null) try {
|
|
8456
|
+
const stateChange = await circuit.recordSuccess(remoteHost);
|
|
8457
|
+
if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
|
|
8458
|
+
} catch (error) {
|
|
8459
|
+
circuitLogger.error("Failed to record successful delivery in circuit breaker state; the activity was already delivered:\n{error}", {
|
|
8460
|
+
...logData,
|
|
8461
|
+
remoteHost,
|
|
8462
|
+
error
|
|
8463
|
+
});
|
|
8464
|
+
}
|
|
8465
|
+
logger.info("Successfully sent activity {activityId} to {inbox}.", {
|
|
8466
|
+
...logData,
|
|
8467
|
+
inbox: inboxHref
|
|
8468
|
+
});
|
|
8469
|
+
return;
|
|
8470
|
+
}
|
|
8471
|
+
if (lastError != null) span.setStatus({
|
|
8472
|
+
code: SpanStatusCode.ERROR,
|
|
8473
|
+
message: String(lastError)
|
|
8474
|
+
});
|
|
8475
|
+
if (survivors.length < 1) {
|
|
8476
|
+
const { error, inbox } = lastPermanent;
|
|
8477
|
+
this.metrics.recordPermanentFailure(error.inbox, error.statusCode);
|
|
8478
|
+
logger.warn("Permanent delivery failure for activity {activityId} to every gateway of {portableInbox}; not retrying.", {
|
|
8479
|
+
...logData,
|
|
8480
|
+
portableInbox: message.portableInbox
|
|
8481
|
+
});
|
|
8482
|
+
if (this.outboxPermanentFailureHandler != null) {
|
|
8483
|
+
const ctx = this.#createContext(new URL(message.baseUrl), contextData, { documentLoader: this.documentLoaderFactory(this.#getLoaderOptions(message.baseUrl)) });
|
|
8484
|
+
try {
|
|
8485
|
+
await this.outboxPermanentFailureHandler(ctx, {
|
|
8486
|
+
reason: "http",
|
|
8487
|
+
inbox,
|
|
8488
|
+
activity: await getActivity(),
|
|
8489
|
+
error,
|
|
8490
|
+
statusCode: error.statusCode,
|
|
8491
|
+
actorIds: getActorIds()
|
|
8492
|
+
});
|
|
8493
|
+
} catch (handlerError) {
|
|
8494
|
+
logger.error("An unexpected error occurred in outboxPermanentFailureHandler:\n{error}", {
|
|
8495
|
+
...logData,
|
|
8496
|
+
error: handlerError
|
|
8497
|
+
});
|
|
8498
|
+
}
|
|
8499
|
+
}
|
|
8500
|
+
recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
|
|
8501
|
+
return;
|
|
8502
|
+
}
|
|
8503
|
+
const now = Temporal.Now.instant();
|
|
8504
|
+
const gatewayNotBefore = {};
|
|
8505
|
+
for (const survivor of survivors) if (survivor.notBefore != null && Temporal.Instant.compare(survivor.notBefore, now) > 0) gatewayNotBefore[survivor.inbox] = survivor.notBefore.toString();
|
|
8506
|
+
const { circuitHeld: _circuitHeld, gatewayNotBefore: _gatewayNotBefore, ...rest } = message;
|
|
8507
|
+
const nextMessage = {
|
|
8508
|
+
...rest,
|
|
8509
|
+
inbox: survivors[0].inbox,
|
|
8510
|
+
gatewayInboxes: survivors.map((s) => s.inbox),
|
|
8511
|
+
...globalThis.Object.keys(gatewayNotBefore).length < 1 ? {} : { gatewayNotBefore }
|
|
8512
|
+
};
|
|
8513
|
+
const { outboxQueue } = this;
|
|
8514
|
+
const policyDelay = this.outboxRetryPolicy({
|
|
8515
|
+
elapsedTime: Temporal.Instant.from(message.started).until(now),
|
|
8516
|
+
attempts: message.attempt
|
|
8517
|
+
});
|
|
8518
|
+
if (outboxQueue != null && survivors.every((s) => s.holdDelay != null) && (policyDelay != null || !survivors.some((s) => s.postFailure))) {
|
|
8519
|
+
let delay = survivors.map((s) => s.holdDelay).reduce((a, b) => Temporal.Duration.compare(a, b) <= 0 ? a : b);
|
|
8520
|
+
let heldMessage = nextMessage;
|
|
8521
|
+
if (survivors.some((s) => s.circuitHeld)) {
|
|
8522
|
+
const since = heldSince ?? now;
|
|
8523
|
+
if (circuit != null) delay = circuit.capHeldDelay(since, delay);
|
|
8524
|
+
heldMessage = {
|
|
8525
|
+
...nextMessage,
|
|
8526
|
+
circuitHeld: true,
|
|
8527
|
+
circuitHeldSince: since.toString()
|
|
8528
|
+
};
|
|
8529
|
+
}
|
|
8530
|
+
logger.warn("Failed to send activity {activityId} to {portableInbox}; holding it until one of its gateways can be tried again.", {
|
|
8531
|
+
...logData,
|
|
8532
|
+
portableInbox: message.portableInbox
|
|
8533
|
+
});
|
|
8534
|
+
await outboxQueue.enqueue(heldMessage, {
|
|
8535
|
+
delay: clampNegativeDelay(delay),
|
|
8536
|
+
orderingKey: message.orderingKey
|
|
8537
|
+
});
|
|
8538
|
+
this.metrics.recordQueueTaskEnqueued({
|
|
8539
|
+
role: "outbox",
|
|
8540
|
+
queue: outboxQueue,
|
|
8541
|
+
activityType: heldMessage.activityType
|
|
8542
|
+
}, heldMessage.attempt);
|
|
8543
|
+
return;
|
|
8544
|
+
}
|
|
8545
|
+
const hasRetryAfter = survivors.some((s) => s.retryAfter != null);
|
|
8546
|
+
if (outboxQueue?.nativeRetrial && !hasRetryAfter && survivors.length === message.gatewayInboxes.length && globalThis.Object.keys(gatewayNotBefore).length < 1) {
|
|
8547
|
+
logger.error("Failed to send activity {activityId} to every gateway of {portableInbox}; backend will handle retry:\n{error}", {
|
|
8548
|
+
...logData,
|
|
8549
|
+
portableInbox: message.portableInbox,
|
|
8550
|
+
error: lastError
|
|
8551
|
+
});
|
|
8552
|
+
throw lastError;
|
|
8553
|
+
}
|
|
8554
|
+
if (policyDelay == null || outboxQueue == null) {
|
|
8555
|
+
logger.error("Failed to send activity {activityId} to every gateway of {portableInbox} after {attempt} attempts; giving up:\n{error}", {
|
|
8556
|
+
...logData,
|
|
8557
|
+
portableInbox: message.portableInbox,
|
|
8558
|
+
error: lastError
|
|
8559
|
+
});
|
|
8560
|
+
recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
|
|
8561
|
+
return;
|
|
8562
|
+
}
|
|
8563
|
+
const delay = survivors.map((s) => s.holdDelay ?? s.retryAfter ?? policyDelay).reduce((a, b) => Temporal.Duration.compare(a, b) <= 0 ? a : b);
|
|
8564
|
+
const retryMessage = {
|
|
8565
|
+
...nextMessage,
|
|
8566
|
+
attempt: message.attempt + 1
|
|
8567
|
+
};
|
|
8568
|
+
logger.error("Failed to send activity {activityId} to every gateway of {portableInbox} (attempt #{attempt}); retry...:\n{error}", {
|
|
8569
|
+
...logData,
|
|
8570
|
+
portableInbox: message.portableInbox,
|
|
8571
|
+
error: lastError
|
|
8572
|
+
});
|
|
8573
|
+
await outboxQueue.enqueue(retryMessage, {
|
|
8574
|
+
delay: clampNegativeDelay(delay),
|
|
8575
|
+
orderingKey: message.orderingKey
|
|
8576
|
+
});
|
|
8577
|
+
this.metrics.recordQueueTaskEnqueued({
|
|
8578
|
+
role: "outbox",
|
|
8579
|
+
queue: outboxQueue,
|
|
8580
|
+
activityType: retryMessage.activityType
|
|
8581
|
+
}, retryMessage.attempt);
|
|
8582
|
+
recordOutboxActivity(this.meterProvider, "retried", retryMessage.activityType);
|
|
8583
|
+
}
|
|
7938
8584
|
async #listenInboxMessage(ctxData, message, span, onActivityType) {
|
|
7939
8585
|
const logger = getLogger([
|
|
7940
8586
|
"fedify",
|
|
@@ -8364,13 +9010,12 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
8364
9010
|
activity: jsonLd
|
|
8365
9011
|
});
|
|
8366
9012
|
const promises = [];
|
|
8367
|
-
for (const inbox in inboxes) promises.push(
|
|
9013
|
+
for (const inbox in inboxes) promises.push(sendActivityToInbox(inbox, inboxes[inbox].gatewayInboxes, {
|
|
8368
9014
|
allowPrivateAddress: this.allowPrivateAddress,
|
|
8369
9015
|
keys,
|
|
8370
9016
|
activity: jsonLd,
|
|
8371
9017
|
activityId: activity.id?.href,
|
|
8372
9018
|
activityType: getTypeId(activity).href,
|
|
8373
|
-
inbox: new URL(inbox),
|
|
8374
9019
|
sharedInbox: inboxes[inbox].sharedInbox,
|
|
8375
9020
|
headers: collectionSync == null ? void 0 : new Headers({ "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox].actorIds) }),
|
|
8376
9021
|
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
|
|
@@ -8397,8 +9042,8 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
8397
9042
|
propagation.inject(context.active(), carrier);
|
|
8398
9043
|
const messages = [];
|
|
8399
9044
|
for (const inbox in inboxes) {
|
|
8400
|
-
const
|
|
8401
|
-
const messageOrderingKey = orderingKey == null ? void 0 : `${orderingKey}\n${
|
|
9045
|
+
const { portableInbox, gatewayInboxes } = inboxes[inbox];
|
|
9046
|
+
const messageOrderingKey = orderingKey == null ? void 0 : `${orderingKey}\n${getOrderingDestination(inbox, portableInbox)}`;
|
|
8402
9047
|
const message = {
|
|
8403
9048
|
type: "outbox",
|
|
8404
9049
|
id: crypto.randomUUID(),
|
|
@@ -8414,6 +9059,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
8414
9059
|
attempt: 0,
|
|
8415
9060
|
headers: collectionSync == null ? {} : { "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox].actorIds) },
|
|
8416
9061
|
orderingKey: messageOrderingKey,
|
|
9062
|
+
...getPortableMessageFields(portableInbox, gatewayInboxes),
|
|
8417
9063
|
traceContext: carrier
|
|
8418
9064
|
};
|
|
8419
9065
|
messages.push({
|
|
@@ -8773,10 +9419,10 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
8773
9419
|
onNotFound
|
|
8774
9420
|
});
|
|
8775
9421
|
case "collection": {
|
|
8776
|
-
const name = route.name
|
|
9422
|
+
const name = this.getCollectionName(route.name);
|
|
8777
9423
|
const callbacks = this.collectionCallbacks[name];
|
|
8778
9424
|
return await handleCustomCollection(request, {
|
|
8779
|
-
name,
|
|
9425
|
+
name: String(name),
|
|
8780
9426
|
context,
|
|
8781
9427
|
values: route.values,
|
|
8782
9428
|
collectionCallbacks: callbacks,
|
|
@@ -8787,10 +9433,10 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
8787
9433
|
});
|
|
8788
9434
|
}
|
|
8789
9435
|
case "orderedCollection": {
|
|
8790
|
-
const name = route.name
|
|
9436
|
+
const name = this.getCollectionName(route.name);
|
|
8791
9437
|
const callbacks = this.collectionCallbacks[name];
|
|
8792
9438
|
return await handleOrderedCollection(request, {
|
|
8793
|
-
name,
|
|
9439
|
+
name: String(name),
|
|
8794
9440
|
context,
|
|
8795
9441
|
values: route.values,
|
|
8796
9442
|
collectionCallbacks: callbacks,
|
|
@@ -8863,6 +9509,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
8863
9509
|
objectDispatcher: actorDispatcher == null ? void 0 : (ctx) => actorDispatcher(ctx, identifier),
|
|
8864
9510
|
authorizePredicate: authorizePredicate == null ? void 0 : (ctx) => authorizePredicate(ctx, identifier),
|
|
8865
9511
|
canonicalId: portable.canonicalId,
|
|
9512
|
+
kind: "actor",
|
|
8866
9513
|
onUnauthorized,
|
|
8867
9514
|
onNotFound
|
|
8868
9515
|
});
|
|
@@ -8883,6 +9530,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
8883
9530
|
objectDispatcher: callbacks?.dispatcher,
|
|
8884
9531
|
authorizePredicate: callbacks?.authorizePredicate,
|
|
8885
9532
|
canonicalId: portable.canonicalId,
|
|
9533
|
+
kind: "object",
|
|
8886
9534
|
onUnauthorized,
|
|
8887
9535
|
onNotFound
|
|
8888
9536
|
});
|
|
@@ -9554,8 +10202,10 @@ var ContextImpl = class ContextImpl {
|
|
|
9554
10202
|
const collectionRegex = new RegExp(`^(${["collection", "orderedCollection"].join("|")}):(.*)$`);
|
|
9555
10203
|
const match = route.name.match(collectionRegex);
|
|
9556
10204
|
if (match !== null) {
|
|
9557
|
-
const [, type
|
|
10205
|
+
const [, type] = match;
|
|
10206
|
+
const name = this.federation.getCollectionName(route.name);
|
|
9558
10207
|
const cls = this.federation.collectionTypeIds[name];
|
|
10208
|
+
if (cls == null) return null;
|
|
9559
10209
|
return {
|
|
9560
10210
|
type,
|
|
9561
10211
|
name,
|
|
@@ -9879,9 +10529,10 @@ var ContextImpl = class ContextImpl {
|
|
|
9879
10529
|
id: crypto.randomUUID(),
|
|
9880
10530
|
baseUrl: this.origin,
|
|
9881
10531
|
keys: keyJwkPairs,
|
|
9882
|
-
inboxes: globalThis.Object.fromEntries(globalThis.Object.entries(inboxes).map(([k, { actorIds, sharedInbox }]) => [k, {
|
|
10532
|
+
inboxes: globalThis.Object.fromEntries(globalThis.Object.entries(inboxes).map(([k, { actorIds, sharedInbox, portableInbox, gatewayInboxes }]) => [k, {
|
|
9883
10533
|
actorIds: [...actorIds],
|
|
9884
|
-
sharedInbox
|
|
10534
|
+
sharedInbox,
|
|
10535
|
+
...getPortableMessageFields(portableInbox, gatewayInboxes)
|
|
9885
10536
|
}])),
|
|
9886
10537
|
activity: activityJsonLd,
|
|
9887
10538
|
activityId: activity.id?.href,
|
|
@@ -9957,7 +10608,7 @@ var ContextImpl = class ContextImpl {
|
|
|
9957
10608
|
"inbox"
|
|
9958
10609
|
]);
|
|
9959
10610
|
const contextLoader = options.contextLoader ?? this.contextLoader;
|
|
9960
|
-
|
|
10611
|
+
let json = await activity.toJsonLd({ contextLoader });
|
|
9961
10612
|
const keyCache = new KvKeyCache(this.federation.kv, this.federation.kvPrefixes.publicKey, {
|
|
9962
10613
|
documentLoader: this.documentLoader,
|
|
9963
10614
|
contextLoader: this.contextLoader,
|
|
@@ -10019,6 +10670,7 @@ var ContextImpl = class ContextImpl {
|
|
|
10019
10670
|
}
|
|
10020
10671
|
logger.debug("Successfully fetched the remote activity object {activityId}; ignore the original activity and use the fetched one, which is trustworthy.");
|
|
10021
10672
|
activity = fetched;
|
|
10673
|
+
json = await fetched.toJsonLd({ contextLoader });
|
|
10022
10674
|
} else logger.debug("Object Integrity Proofs are verified.", {
|
|
10023
10675
|
recipient,
|
|
10024
10676
|
activity: json
|
|
@@ -10178,6 +10830,9 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
|
|
|
10178
10830
|
throw error;
|
|
10179
10831
|
}
|
|
10180
10832
|
}
|
|
10833
|
+
async isSignedByAudience(object, options = {}) {
|
|
10834
|
+
return await isInAudience(object, null) || await isInAudience(object, await this.getSignedKeyOwner(options), options);
|
|
10835
|
+
}
|
|
10181
10836
|
};
|
|
10182
10837
|
function forwardActivity(ctx, loggerCategory, forwarder, recipients, options) {
|
|
10183
10838
|
return ctx.tracerProvider.getTracer(name, version).startActiveSpan(ctx.federation.outboxQueue == null || options?.immediate ? `activitypub.${loggerCategory}` : "activitypub.fanout", {
|
|
@@ -10265,13 +10920,12 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
|
|
|
10265
10920
|
if (options?.immediate) logger.debug("Forwarding activity immediately without queue since immediate option is set.");
|
|
10266
10921
|
else logger.debug("Forwarding activity immediately without queue since queue is not set.");
|
|
10267
10922
|
const promises = [];
|
|
10268
|
-
for (const inbox in inboxes) promises.push(
|
|
10923
|
+
for (const inbox in inboxes) promises.push(sendActivityToInbox(inbox, inboxes[inbox].gatewayInboxes, {
|
|
10269
10924
|
allowPrivateAddress: ctx.federation.allowPrivateAddress,
|
|
10270
10925
|
keys,
|
|
10271
10926
|
activity: ctx.activity,
|
|
10272
10927
|
activityId: ctx.activityId,
|
|
10273
10928
|
activityType: ctx.activityType,
|
|
10274
|
-
inbox: new URL(inbox),
|
|
10275
10929
|
sharedInbox: inboxes[inbox].sharedInbox,
|
|
10276
10930
|
meterProvider: ctx.meterProvider,
|
|
10277
10931
|
tracerProvider: ctx.tracerProvider,
|
|
@@ -10299,7 +10953,7 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
|
|
|
10299
10953
|
const started = (/* @__PURE__ */ new Date()).toISOString();
|
|
10300
10954
|
const messages = [];
|
|
10301
10955
|
for (const inbox in inboxes) {
|
|
10302
|
-
const
|
|
10956
|
+
const { portableInbox, gatewayInboxes } = inboxes[inbox];
|
|
10303
10957
|
const message = {
|
|
10304
10958
|
type: "outbox",
|
|
10305
10959
|
id: crypto.randomUUID(),
|
|
@@ -10314,7 +10968,8 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
|
|
|
10314
10968
|
started,
|
|
10315
10969
|
attempt: 0,
|
|
10316
10970
|
headers: {},
|
|
10317
|
-
orderingKey: orderingKey == null ? void 0 : `${orderingKey}\n${
|
|
10971
|
+
orderingKey: orderingKey == null ? void 0 : `${orderingKey}\n${getOrderingDestination(inbox, portableInbox)}`,
|
|
10972
|
+
...getPortableMessageFields(portableInbox, gatewayInboxes),
|
|
10318
10973
|
traceContext: carrier
|
|
10319
10974
|
};
|
|
10320
10975
|
messages.push({
|