@fedify/fedify 2.5.0-dev.2446 → 2.5.0-dev.2492
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/README.md +3 -0
- package/dist/{activity-listener-C-BSbKI7.mjs → activity-listener-CELFAodc.mjs} +8 -4
- package/dist/{builder-Da8b9O6v.mjs → builder-DphK9v7f.mjs} +5 -5
- package/dist/{client-D-XP_Ccb.d.ts → client-3InDZbba.d.cts} +0 -1
- package/dist/{client-D-XP_Ccb.d.cts → client-3InDZbba.d.ts} +0 -1
- package/dist/{client-CEnNz8O7.mjs → client-5SIFzW3q.mjs} +113 -64
- 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-D1djO5fa.mjs → compound-proof-cYtvlTgF.mjs} +2 -2
- package/dist/{context-8Tu9kea0.mjs → context-COHd1Rsh.mjs} +2 -0
- package/dist/{context-BdB54V-A.d.ts → context-DQJE9YfH.d.ts} +332 -6
- package/dist/{context-CQ9H8vNW.d.cts → context-DSuL3YWx.d.cts} +332 -6
- package/dist/{deno-eNpfj4oR.mjs → deno-iOAA7iRN.mjs} +1 -1
- package/dist/docloader-DCZkiTeF.mjs +136 -0
- package/dist/{exporter-DThFfe7P.mjs → exporter-DVG68ozH.mjs} +36 -30
- package/dist/federation/builder.test.mjs +1 -1
- package/dist/federation/compound-fanout.test.mjs +6 -3
- package/dist/federation/handler-compound-proof.test.mjs +5 -5
- package/dist/federation/handler.test.mjs +22 -7
- package/dist/federation/hashlink-media.test.mjs +2 -2
- package/dist/federation/idempotency.test.mjs +2 -2
- package/dist/federation/inbox-actorless.test.d.mts +2 -0
- package/dist/federation/inbox-actorless.test.mjs +487 -0
- package/dist/federation/inbox-authentication.test.d.mts +2 -0
- package/dist/federation/inbox-authentication.test.mjs +116 -0
- package/dist/federation/inbox-report.test.mjs +5 -5
- package/dist/federation/inbox-view.test.d.mts +2 -0
- package/dist/federation/inbox-view.test.mjs +369 -0
- package/dist/federation/inbox.test.mjs +22 -12
- package/dist/federation/metrics.test.mjs +1 -1
- package/dist/federation/middleware.test.mjs +470 -35
- package/dist/federation/mod.cjs +2 -1
- package/dist/federation/mod.d.cts +4 -4
- package/dist/federation/mod.d.ts +4 -4
- package/dist/federation/mod.js +8 -2
- package/dist/federation/mq.test.mjs +36 -1
- package/dist/federation/outgoing-proof.test.mjs +9 -6
- package/dist/federation/portable-actor.test.mjs +3 -3
- package/dist/federation/portable-chain.test.mjs +3 -3
- package/dist/federation/portable-collection.test.mjs +4 -4
- package/dist/federation/portable-delivery.test.mjs +2 -2
- package/dist/federation/portable-gateway-keys.test.mjs +13 -7
- package/dist/federation/portable-inbox.test.mjs +4 -4
- package/dist/federation/portable-parse-uri.test.mjs +1 -1
- package/dist/federation/portable-route-activity.test.mjs +2 -2
- package/dist/federation/portable.test.mjs +3 -3
- package/dist/federation/send.test.mjs +502 -93
- package/dist/federation/tasks/enqueue.test.mjs +3 -3
- package/dist/federation/tasks/tasks.test.mjs +4 -4
- package/dist/federation/temporal.test.mjs +1 -1
- package/dist/federation/webfinger.test.mjs +2 -2
- package/dist/{federation-BVH7drJA.cjs → federation-D0AZw_aw.cjs} +1415 -236
- package/dist/{http-BUJ5SFh6.mjs → http-BoJCMnjY.mjs} +204 -106
- package/dist/{http-DlNxxW_5.js → http-ChXUU_3c.js} +319 -123
- package/dist/{http-BfW3wyQ4.cjs → http-kv1LZw-r.cjs} +339 -125
- package/dist/inbox-BjihTKLo.mjs +312 -0
- package/dist/inbox-authentication-DflPYlLL.mjs +171 -0
- package/dist/inbox-view-D-skypg-.mjs +385 -0
- package/dist/{key-nYGY4QB9.d.cts → key-DbLNT-vA.d.ts} +1 -2
- package/dist/{key-DhsBxxZN.d.ts → key-DlHT4R6k.d.cts} +1 -2
- package/dist/{kv-cache-D_m3A241.mjs → kv-cache-Cmsyke4d.mjs} +1 -1
- package/dist/{kv-cache-7015lbrx.js → kv-cache-DQcte7ES.js} +73 -28
- package/dist/{kv-cache-kkBB0vbI.cjs → kv-cache-Ddh5sMxH.cjs} +72 -27
- package/dist/ld-CWVpo_NM.d.cts +250 -0
- package/dist/{ld-CD2CsPxq.mjs → ld-CdvcOEXQ.mjs} +52 -9
- package/dist/ld-Inagww4R.d.ts +250 -0
- package/dist/{metrics-D3LHk6Z4.mjs → metrics-CpxvUrBz.mjs} +1 -1
- package/dist/{middleware-N9eM16XE.js → middleware-BHb5toqA.js} +1413 -247
- package/dist/{middleware-CCDXvXOG.mjs → middleware-BKs2DLUF.mjs} +1 -1
- package/dist/{middleware-CzoWvrdZ.mjs → middleware-CoBLrTKX.mjs} +533 -132
- package/dist/{mod-B0dGgQsg.d.cts → mod-6TQ4UQZb.d.cts} +1 -1
- package/dist/mod-B4kBw1-8.d.ts +154 -0
- package/dist/{mod-DkhfZC1u.d.ts → mod-B7j30IQf.d.ts} +2 -9
- package/dist/mod-C1nT74IG.d.cts +154 -0
- package/dist/{mod-D0bxB8jS.d.ts → mod-CIeBgsOC.d.ts} +1 -1
- package/dist/{mod-B5-wdzmJ.d.cts → mod-YvA4Rk8J.d.cts} +2 -9
- package/dist/mod.cjs +6 -5
- package/dist/mod.d.cts +9 -9
- package/dist/mod.d.ts +9 -9
- package/dist/mod.js +6 -6
- package/dist/{mq-Dm1Wmtm-.mjs → mq-eF96QxUT.mjs} +6 -0
- package/dist/nodeinfo/client.test.mjs +337 -19
- package/dist/nodeinfo/handler.test.mjs +2 -2
- package/dist/nodeinfo/mod.cjs +1 -1
- package/dist/nodeinfo/mod.d.cts +1 -1
- package/dist/nodeinfo/mod.d.ts +1 -1
- package/dist/nodeinfo/mod.js +1 -1
- package/dist/otel/exporter.test.mjs +257 -1
- package/dist/otel/mod.cjs +36 -30
- package/dist/otel/mod.d.cts +13 -0
- package/dist/otel/mod.d.ts +13 -0
- package/dist/otel/mod.js +36 -30
- package/dist/{owner-C5-FVX6t.mjs → owner-DTnVpUKA.mjs} +3 -3
- package/dist/{portable-collection-Cony3_LC.mjs → portable-collection-CJeYTL61.mjs} +2 -2
- package/dist/{portable-collection-C-Z1SKDy.cjs → portable-collection-DciwBIia.cjs} +1 -1
- package/dist/{portable-collection-CC2zNAje.js → portable-collection-TmtpzD4E.js} +1 -1
- package/dist/{portable-key-DNc_flH-.mjs → portable-key-DcYok4ki.mjs} +1 -1
- package/dist/{proof-Cryb1c70.mjs → proof-GH7Poos_.mjs} +98 -10
- package/dist/{send-AF25pWaJ.mjs → send-BgGJh6pv.mjs} +174 -76
- 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 +553 -2
- package/dist/sig/key.test.mjs +1 -1
- package/dist/sig/ld.test.mjs +1 -1
- package/dist/sig/mitra-receive-interop.test.mjs +4 -4
- package/dist/sig/mitra-send-interop.test.mjs +3 -3
- package/dist/sig/mod.cjs +2 -2
- package/dist/sig/mod.d.cts +3 -3
- package/dist/sig/mod.d.ts +3 -3
- package/dist/sig/mod.js +2 -2
- package/dist/sig/owner.test.mjs +1 -1
- package/dist/sig/portable-collection.test.mjs +1 -1
- package/dist/sig/portable-interop.test.mjs +3 -3
- package/dist/sig/portable-key.test.mjs +5 -5
- package/dist/sig/proof.test.mjs +45 -5
- package/dist/sig/tootik-request-interop.test.mjs +4 -4
- package/dist/sig/tootik-send-interop.test.mjs +3 -3
- package/dist/sig/verification.test.mjs +3 -3
- package/dist/{temporal-BsAeuUJj.mjs → temporal-gJINzcL4.mjs} +1 -1
- package/dist/testing/kv.test.mjs +1 -1
- package/dist/testing/mod.d.mts +288 -2
- package/dist/testing/mod.mjs +2 -2
- package/dist/{testing-t8xDeJZ5.mjs → testing-Dbr21vZk.mjs} +1 -1
- package/dist/{types-_S2_MeLF.js → types-DUjZb4i6.js} +112 -63
- package/dist/{types-poIao40f.cjs → types-hKCCjii0.cjs} +111 -62
- package/dist/utils/docloader.test.mjs +344 -36
- package/dist/utils/kv-cache.test.mjs +1 -1
- 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
- package/dist/docloader-B-0nqOB1.mjs +0 -91
- package/dist/inbox-CHvMaIFX.mjs +0 -188
- package/dist/mod-BRpER-B0.d.ts +0 -320
- package/dist/mod-xAfaLS4T.d.cts +0 -320
- package/dist/owner-BF0L9cMt.d.cts +0 -78
- package/dist/owner-BSzr5-I8.d.ts +0 -78
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { Temporal } from "temporal-polyfill";
|
|
2
|
+
import "urlpattern-polyfill";
|
|
3
|
+
globalThis.addEventListener = () => {};
|
|
4
|
+
import { t as getAuthenticationOrigin } from "./portable-key-id-Ca30-hOa.mjs";
|
|
5
|
+
import { o as serializeAuthentication, r as getEffectivePrincipals } from "./inbox-authentication-DflPYlLL.mjs";
|
|
6
|
+
import { n as version, t as name } from "./deno-iOAA7iRN.mjs";
|
|
7
|
+
import { g as recordInboxActivity, n as getDurationMs, r as getFederationMetrics } from "./metrics-CpxvUrBz.mjs";
|
|
8
|
+
import { getLogger } from "@logtape/logtape";
|
|
9
|
+
import { getTypeId } from "@fedify/vocab";
|
|
10
|
+
import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
|
|
11
|
+
//#region src/federation/inbox.ts
|
|
12
|
+
/**
|
|
13
|
+
* Checks whether an activity's ID and all of its actor IDs have the same
|
|
14
|
+
* [FEP-fe34] origin. Activities without an ID have no origin to authenticate.
|
|
15
|
+
*
|
|
16
|
+
* Origins are compared by `getAuthenticationOrigin()`, so a portable activity
|
|
17
|
+
* and its portable actors, including compatible identifiers on any gateway,
|
|
18
|
+
* are compared by their DIDs. An ID that has no supported origin, such as an
|
|
19
|
+
* opaque URN, never matches.
|
|
20
|
+
*
|
|
21
|
+
* The caller is responsible for rejecting a missing primary actor ID before
|
|
22
|
+
* relying on this check.
|
|
23
|
+
*
|
|
24
|
+
* [FEP-fe34]: https://w3id.org/fep/fe34
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
function actorsHaveSameOrigin(activity) {
|
|
28
|
+
if (activity.id == null) return true;
|
|
29
|
+
const origin = getAuthenticationOrigin(activity.id);
|
|
30
|
+
return origin != null && activity.actorIds.every((actor) => getAuthenticationOrigin(actor) === origin);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The headroom that the size check of a queued inbox message leaves for the
|
|
34
|
+
* fields that grow when Fedify retries the message, e.g., its attempt count.
|
|
35
|
+
*/
|
|
36
|
+
const QUEUED_MESSAGE_HEADROOM = 64;
|
|
37
|
+
/**
|
|
38
|
+
* Computes the idempotency key of an incoming activity.
|
|
39
|
+
* @returns The key, or `null` if the activity is not deduplicated.
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
async function getInboxIdempotencyKey(strategy, inboxContext, activity, origin, recipient, prefix) {
|
|
43
|
+
if (activity.id == null) return null;
|
|
44
|
+
strategy ??= "per-inbox";
|
|
45
|
+
let keyString;
|
|
46
|
+
if (typeof strategy === "function") keyString = await strategy(inboxContext(), activity);
|
|
47
|
+
else switch (strategy) {
|
|
48
|
+
case "global":
|
|
49
|
+
keyString = activity.id.href;
|
|
50
|
+
break;
|
|
51
|
+
case "per-origin":
|
|
52
|
+
keyString = `${origin}\n${activity.id.href}`;
|
|
53
|
+
break;
|
|
54
|
+
case "per-inbox":
|
|
55
|
+
keyString = `${origin}\n${activity.id.href}\n${recipient == null ? "sharedInbox" : `inbox\n${recipient}`}`;
|
|
56
|
+
break;
|
|
57
|
+
default: keyString = `${origin}\n${activity.id.href}`;
|
|
58
|
+
}
|
|
59
|
+
return keyString == null ? null : [...prefix, keyString];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Checks whether an incoming activity has already been processed, by its
|
|
63
|
+
* idempotency key, or by the key that queue workers of older versions of
|
|
64
|
+
* Fedify recorded.
|
|
65
|
+
* @internal
|
|
66
|
+
*/
|
|
67
|
+
async function isInboxActivityProcessed(kv, cacheKey, activity, origin, prefix) {
|
|
68
|
+
if (cacheKey == null) return false;
|
|
69
|
+
if (await kv.get(cacheKey) === true) return true;
|
|
70
|
+
if (activity.id == null) return false;
|
|
71
|
+
return await kv.get([
|
|
72
|
+
...prefix,
|
|
73
|
+
origin,
|
|
74
|
+
activity.id.href
|
|
75
|
+
]) === true;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The bytes added to the UTF-8 JSON length of a message for each of its JSON
|
|
79
|
+
* values, to bound the size of serialization formats other than JSON, such
|
|
80
|
+
* as the structured serialization of Deno KV, which spends more bytes than
|
|
81
|
+
* JSON on type tags and lengths of small values, e.g., empty arrays, and up
|
|
82
|
+
* to nine bytes on a fractional number that JSON writes in three.
|
|
83
|
+
*/
|
|
84
|
+
const PER_VALUE_SERIALIZATION_OVERHEAD = 8;
|
|
85
|
+
/**
|
|
86
|
+
* Estimates, conservatively, how large a queued message is once a queue
|
|
87
|
+
* backend serializes it: the UTF-8 length of its JSON serialization, plus
|
|
88
|
+
* a fixed overhead for each JSON value in it, plus the bytes that encoding
|
|
89
|
+
* strings beyond Latin-1 as UTF-16 adds.
|
|
90
|
+
* @internal
|
|
91
|
+
*/
|
|
92
|
+
function estimateQueuedMessageSize(message) {
|
|
93
|
+
const encoder = new TextEncoder();
|
|
94
|
+
let values = 0;
|
|
95
|
+
let stringExpansion = 0;
|
|
96
|
+
const countString = (value) => {
|
|
97
|
+
values++;
|
|
98
|
+
if (!/[\u0100-\uffff]/.test(value)) return;
|
|
99
|
+
const expansion = value.length * 2 - encoder.encode(value).byteLength;
|
|
100
|
+
if (expansion > 0) stringExpansion += expansion;
|
|
101
|
+
};
|
|
102
|
+
const count = (value) => {
|
|
103
|
+
if (typeof value === "string") {
|
|
104
|
+
countString(value);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
values++;
|
|
108
|
+
if (Array.isArray(value)) for (const item of value) count(item);
|
|
109
|
+
else if (value != null && typeof value === "object") for (const [key, item] of Object.entries(value)) {
|
|
110
|
+
if (item === void 0) continue;
|
|
111
|
+
countString(key);
|
|
112
|
+
count(item);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
count(message);
|
|
116
|
+
return encoder.encode(JSON.stringify(message)).byteLength + stringExpansion + values * PER_VALUE_SERIALIZATION_OVERHEAD;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Checks whether an activity that declares no actor is authenticated: at
|
|
120
|
+
* least one principal must be authenticated for it, and its ID, if any, must
|
|
121
|
+
* have the same [FEP-fe34] origin as one of those principals.
|
|
122
|
+
*
|
|
123
|
+
* [FEP-fe34]: https://w3id.org/fep/fe34
|
|
124
|
+
* @returns `"ok"` if it is authenticated, or why it is not.
|
|
125
|
+
* @internal
|
|
126
|
+
*/
|
|
127
|
+
function checkActorlessActivity(activity, authentication) {
|
|
128
|
+
const principals = getEffectivePrincipals(authentication);
|
|
129
|
+
if (principals.length < 1) return {
|
|
130
|
+
status: "noAuthenticatedPrincipal",
|
|
131
|
+
principals
|
|
132
|
+
};
|
|
133
|
+
if (activity.id == null) return {
|
|
134
|
+
status: "ok",
|
|
135
|
+
principals
|
|
136
|
+
};
|
|
137
|
+
const origin = getAuthenticationOrigin(activity.id);
|
|
138
|
+
if (origin == null || !principals.some((principal) => getAuthenticationOrigin(principal.id) === origin)) return {
|
|
139
|
+
status: "activityOriginMismatch",
|
|
140
|
+
principals
|
|
141
|
+
};
|
|
142
|
+
return {
|
|
143
|
+
status: "ok",
|
|
144
|
+
principals
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Same as {@link checkActorlessActivity}, but returns a boolean.
|
|
149
|
+
* @internal
|
|
150
|
+
*/
|
|
151
|
+
function isActorlessActivityAuthenticated(activity, authentication) {
|
|
152
|
+
return checkActorlessActivity(activity, authentication).status === "ok";
|
|
153
|
+
}
|
|
154
|
+
async function routeActivity({ context: ctx, json, originalJson, normalizedActivity, ldSignatureVerified, authentication, verifiedView, replayable, actorless, activity, recipient, inboxListeners, inboxContextFactory, listenerInboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, meterProvider, tracerProvider, idempotencyStrategy }) {
|
|
155
|
+
const logger = getLogger([
|
|
156
|
+
"fedify",
|
|
157
|
+
"federation",
|
|
158
|
+
"inbox"
|
|
159
|
+
]);
|
|
160
|
+
if (activity.actorId == null && !actorless) {
|
|
161
|
+
logger.error("Missing actor.", { activity: json });
|
|
162
|
+
span.setStatus({
|
|
163
|
+
code: SpanStatusCode.ERROR,
|
|
164
|
+
message: "Missing actor."
|
|
165
|
+
});
|
|
166
|
+
recordInboxActivity(meterProvider, "rejected", getTypeId(activity).href);
|
|
167
|
+
return "missingActor";
|
|
168
|
+
}
|
|
169
|
+
const contextMetadata = { authentication };
|
|
170
|
+
const cacheKey = await getInboxIdempotencyKey(idempotencyStrategy, () => inboxContextFactory(recipient, json, activity.id?.href, getTypeId(activity).href, contextMetadata), activity, ctx.origin, recipient, kvPrefixes.activityIdempotence);
|
|
171
|
+
if (await isInboxActivityProcessed(kv, cacheKey, activity, ctx.origin, kvPrefixes.activityIdempotence)) {
|
|
172
|
+
logger.debug("Activity {activityId} has already been processed.", {
|
|
173
|
+
activityId: activity.id?.href,
|
|
174
|
+
activity: json,
|
|
175
|
+
recipient
|
|
176
|
+
});
|
|
177
|
+
span.setStatus({
|
|
178
|
+
code: SpanStatusCode.UNSET,
|
|
179
|
+
message: `Activity ${activity.id?.href} has already been processed.`
|
|
180
|
+
});
|
|
181
|
+
recordInboxActivity(meterProvider, "rejected", getTypeId(activity).href);
|
|
182
|
+
return "alreadyProcessed";
|
|
183
|
+
}
|
|
184
|
+
if (activity.actorId != null) span.setAttribute("activitypub.actor.id", activity.actorId.href);
|
|
185
|
+
let message;
|
|
186
|
+
if (queue != null) {
|
|
187
|
+
const carrier = {};
|
|
188
|
+
propagation.inject(context.active(), carrier);
|
|
189
|
+
message = {
|
|
190
|
+
type: "inbox",
|
|
191
|
+
id: crypto.randomUUID(),
|
|
192
|
+
baseUrl: ctx.origin,
|
|
193
|
+
activity: originalJson ?? json,
|
|
194
|
+
...normalizedActivity == null ? {} : { normalizedActivity },
|
|
195
|
+
...ldSignatureVerified == null ? {} : { ldSignatureVerified },
|
|
196
|
+
...verifiedView == null ? {} : { verifiedView },
|
|
197
|
+
...authentication == null ? {} : { authentication: serializeAuthentication(authentication) },
|
|
198
|
+
identifier: recipient,
|
|
199
|
+
attempt: 0,
|
|
200
|
+
started: (/* @__PURE__ */ new Date()).toISOString(),
|
|
201
|
+
traceContext: carrier
|
|
202
|
+
};
|
|
203
|
+
const maxMessageSize = queue.maxMessageSize;
|
|
204
|
+
if (replayable === false) {
|
|
205
|
+
logger.warn("The incoming activity {activityId} uses a JSON-LD context that cannot be kept for the queue; processing it immediately instead.", {
|
|
206
|
+
activityId: activity.id?.href,
|
|
207
|
+
recipient
|
|
208
|
+
});
|
|
209
|
+
message = void 0;
|
|
210
|
+
} else if (maxMessageSize != null && estimateQueuedMessageSize(message) > maxMessageSize - QUEUED_MESSAGE_HEADROOM) {
|
|
211
|
+
logger.warn("The incoming activity {activityId} is too large to enqueue; processing it immediately instead.", {
|
|
212
|
+
activityId: activity.id?.href,
|
|
213
|
+
recipient,
|
|
214
|
+
maxMessageSize
|
|
215
|
+
});
|
|
216
|
+
message = void 0;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (queue != null && message != null) {
|
|
220
|
+
try {
|
|
221
|
+
await queue.enqueue(message);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
logger.error("Failed to enqueue the incoming activity {activityId}:\n{error}", {
|
|
224
|
+
error,
|
|
225
|
+
activityId: activity.id?.href,
|
|
226
|
+
activity: json,
|
|
227
|
+
recipient
|
|
228
|
+
});
|
|
229
|
+
span.setStatus({
|
|
230
|
+
code: SpanStatusCode.ERROR,
|
|
231
|
+
message: `Failed to enqueue the incoming activity ${activity.id?.href}.`
|
|
232
|
+
});
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
getFederationMetrics(meterProvider).recordQueueTaskEnqueued({
|
|
236
|
+
role: "inbox",
|
|
237
|
+
queue,
|
|
238
|
+
activityType: getTypeId(activity).href
|
|
239
|
+
}, 0);
|
|
240
|
+
recordInboxActivity(meterProvider, "queued", getTypeId(activity).href);
|
|
241
|
+
logger.info("Activity {activityId} is enqueued.", {
|
|
242
|
+
activityId: activity.id?.href,
|
|
243
|
+
activity: json,
|
|
244
|
+
recipient
|
|
245
|
+
});
|
|
246
|
+
return "enqueued";
|
|
247
|
+
}
|
|
248
|
+
tracerProvider = tracerProvider ?? trace.getTracerProvider();
|
|
249
|
+
return await tracerProvider.getTracer(name, version).startActiveSpan("activitypub.dispatch_inbox_listener", { kind: SpanKind.INTERNAL }, async (span) => {
|
|
250
|
+
const dispatched = inboxListeners?.dispatchWithClass(activity);
|
|
251
|
+
if (dispatched == null) {
|
|
252
|
+
logger.error("Unsupported activity type:\n{activity}", {
|
|
253
|
+
activity: json,
|
|
254
|
+
recipient
|
|
255
|
+
});
|
|
256
|
+
span.setStatus({
|
|
257
|
+
code: SpanStatusCode.UNSET,
|
|
258
|
+
message: `Unsupported activity type: ${getTypeId(activity).href}`
|
|
259
|
+
});
|
|
260
|
+
recordInboxActivity(meterProvider, "rejected", getTypeId(activity).href);
|
|
261
|
+
span.end();
|
|
262
|
+
return "unsupportedActivity";
|
|
263
|
+
}
|
|
264
|
+
const { class: cls, listener } = dispatched;
|
|
265
|
+
span.updateName(`activitypub.dispatch_inbox_listener ${cls.name}`);
|
|
266
|
+
try {
|
|
267
|
+
const activityType = getTypeId(activity).href;
|
|
268
|
+
const started = performance.now();
|
|
269
|
+
try {
|
|
270
|
+
const contextFactory = listenerInboxContextFactory ?? inboxContextFactory;
|
|
271
|
+
await listener(contextFactory(recipient, contextFactory === inboxContextFactory ? json : originalJson ?? json, activity.id?.href, activityType, contextMetadata), activity);
|
|
272
|
+
} finally {
|
|
273
|
+
getFederationMetrics(meterProvider).recordInboxProcessingDuration(activityType, getDurationMs(started));
|
|
274
|
+
}
|
|
275
|
+
} catch (error) {
|
|
276
|
+
try {
|
|
277
|
+
await inboxErrorHandler?.(ctx, error);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
logger.error("An unexpected error occurred in inbox error handler:\n{error}", {
|
|
280
|
+
error,
|
|
281
|
+
activityId: activity.id?.href,
|
|
282
|
+
activity: json,
|
|
283
|
+
recipient
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
logger.error("Failed to process the incoming activity {activityId}:\n{error}", {
|
|
287
|
+
error,
|
|
288
|
+
activityId: activity.id?.href,
|
|
289
|
+
activity: json,
|
|
290
|
+
recipient
|
|
291
|
+
});
|
|
292
|
+
span.setStatus({
|
|
293
|
+
code: SpanStatusCode.ERROR,
|
|
294
|
+
message: String(error)
|
|
295
|
+
});
|
|
296
|
+
recordInboxActivity(meterProvider, "rejected", getTypeId(activity).href);
|
|
297
|
+
span.end();
|
|
298
|
+
return "error";
|
|
299
|
+
}
|
|
300
|
+
recordInboxActivity(meterProvider, "processed", getTypeId(activity).href);
|
|
301
|
+
if (cacheKey != null) await kv.set(cacheKey, true, { ttl: Temporal.Duration.from({ days: 1 }) });
|
|
302
|
+
logger.info("Activity {activityId} has been processed.", {
|
|
303
|
+
activityId: activity.id?.href,
|
|
304
|
+
activity: json,
|
|
305
|
+
recipient
|
|
306
|
+
});
|
|
307
|
+
span.end();
|
|
308
|
+
return "success";
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
//#endregion
|
|
312
|
+
export { isActorlessActivityAuthenticated as a, getInboxIdempotencyKey as i, checkActorlessActivity as n, isInboxActivityProcessed as o, estimateQueuedMessageSize as r, routeActivity as s, actorsHaveSameOrigin as t };
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import "temporal-polyfill";
|
|
2
|
+
import "urlpattern-polyfill";
|
|
3
|
+
globalThis.addEventListener = () => {};
|
|
4
|
+
import { i as getPortableDid, l as isPortableId, n as getCanonicalPortableId } from "./portable-key-id-Ca30-hOa.mjs";
|
|
5
|
+
//#region src/federation/inbox-authentication.ts
|
|
6
|
+
/**
|
|
7
|
+
* The evidence of an activity that has not been authenticated by Fedify.
|
|
8
|
+
*/
|
|
9
|
+
const EMPTY_EVIDENCE = Object.freeze({
|
|
10
|
+
verified: false,
|
|
11
|
+
requestSigners: Object.freeze([]),
|
|
12
|
+
documentSigners: Object.freeze([]),
|
|
13
|
+
originAuthenticated: Object.freeze([]),
|
|
14
|
+
expandedActivity: Object.freeze([]),
|
|
15
|
+
baseUrl: null
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* Gets the evidence of an activity that has not been authenticated.
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
function getEmptyAuthenticationEvidence() {
|
|
22
|
+
return EMPTY_EVIDENCE;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Gets the principals that an authorization decision should rely on.
|
|
26
|
+
* @param evidence The evidence.
|
|
27
|
+
* @param scope See {@link IsActorAuthenticatedOptions.scope}.
|
|
28
|
+
* @returns The principals.
|
|
29
|
+
* @internal
|
|
30
|
+
*/
|
|
31
|
+
function getEffectivePrincipals(evidence, scope = "effective") {
|
|
32
|
+
if (scope === "document" || evidence.documentSigners.length > 0) return evidence.documentSigners;
|
|
33
|
+
if (evidence.requestSigners.length > 0) return evidence.requestSigners;
|
|
34
|
+
return evidence.originAuthenticated;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Normalizes an actor ID for comparison: portable IDs and compatible
|
|
38
|
+
* identifiers to their canonical portable ID, other IDs to their `href`.
|
|
39
|
+
* @returns The normalized ID, or `null` for a malformed portable ID.
|
|
40
|
+
*/
|
|
41
|
+
function normalizeActorId(id) {
|
|
42
|
+
return isPortableId(id) ? getCanonicalPortableId(id) : id.href;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Checks whether the given principal authenticates the given actor.
|
|
46
|
+
* @param principal The principal.
|
|
47
|
+
* @param actorId The actor ID.
|
|
48
|
+
* @returns `true` if the principal authenticates the actor.
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
51
|
+
function principalAuthenticates(principal, actorId) {
|
|
52
|
+
if (principal.kind === "portableAuthority") {
|
|
53
|
+
if (!isPortableId(actorId)) return false;
|
|
54
|
+
const did = getPortableDid(actorId);
|
|
55
|
+
return did != null && did === getPortableDid(principal.id);
|
|
56
|
+
}
|
|
57
|
+
const expected = normalizeActorId(actorId);
|
|
58
|
+
return expected != null && normalizeActorId(principal.id) === expected;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Checks whether the given evidence authenticates the given actor.
|
|
62
|
+
*
|
|
63
|
+
* Actor IDs are compared exactly, except that [FEP-ef61] portable IDs and
|
|
64
|
+
* compatible identifiers are compared by their canonical portable IDs, and
|
|
65
|
+
* a portable authority (a DID) authenticates the portable actors of that DID.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ``` typescript
|
|
69
|
+
* federation
|
|
70
|
+
* .setInboxListeners("/users/{identifier}/inbox", "/inbox")
|
|
71
|
+
* .on(Follow, async (ctx, follow) => {
|
|
72
|
+
* if (follow.actorId == null) return;
|
|
73
|
+
* // The actor is always authenticated for activities that declare one,
|
|
74
|
+
* // but the same check works for any actor an activity names:
|
|
75
|
+
* isActorAuthenticated(ctx.authentication, follow.actorId);
|
|
76
|
+
* });
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
80
|
+
* @param evidence The authentication evidence, usually
|
|
81
|
+
* {@link InboxContext.authentication}.
|
|
82
|
+
* @param actorId The actor ID to check.
|
|
83
|
+
* @param options Options.
|
|
84
|
+
* @returns `true` if the actor is authenticated.
|
|
85
|
+
* @since 2.5.0
|
|
86
|
+
*/
|
|
87
|
+
function isActorAuthenticated(evidence, actorId, options = {}) {
|
|
88
|
+
return getEffectivePrincipals(evidence, options.scope).some((principal) => principalAuthenticates(principal, actorId));
|
|
89
|
+
}
|
|
90
|
+
function serializePrincipal(principal) {
|
|
91
|
+
return {
|
|
92
|
+
kind: principal.kind,
|
|
93
|
+
id: principal.id.href,
|
|
94
|
+
mechanism: principal.mechanism,
|
|
95
|
+
keyId: principal.keyId?.href ?? null
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Serializes the principals of the given evidence for a queued message.
|
|
100
|
+
* @internal
|
|
101
|
+
*/
|
|
102
|
+
function serializeAuthentication(evidence) {
|
|
103
|
+
return {
|
|
104
|
+
verified: evidence.verified,
|
|
105
|
+
requestSigners: evidence.requestSigners.map(serializePrincipal),
|
|
106
|
+
documentSigners: evidence.documentSigners.map(serializePrincipal),
|
|
107
|
+
originAuthenticated: evidence.originAuthenticated.map(serializePrincipal)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const KINDS = /* @__PURE__ */ new Set(["actor", "portableAuthority"]);
|
|
111
|
+
const MECHANISMS = /* @__PURE__ */ new Set([
|
|
112
|
+
"httpSignatures",
|
|
113
|
+
"linkedDataSignatures",
|
|
114
|
+
"objectIntegrityProofs",
|
|
115
|
+
"origin"
|
|
116
|
+
]);
|
|
117
|
+
function deserializePrincipals(value) {
|
|
118
|
+
if (!Array.isArray(value)) return null;
|
|
119
|
+
const principals = [];
|
|
120
|
+
for (const item of value) {
|
|
121
|
+
if (item == null || typeof item !== "object" || !KINDS.has(item.kind) || !MECHANISMS.has(item.mechanism) || typeof item.id !== "string" || !URL.canParse(item.id) || item.keyId != null && (typeof item.keyId !== "string" || !URL.canParse(item.keyId))) return null;
|
|
122
|
+
principals.push(Object.freeze({
|
|
123
|
+
kind: item.kind,
|
|
124
|
+
id: new URL(item.id),
|
|
125
|
+
mechanism: item.mechanism,
|
|
126
|
+
keyId: item.keyId == null ? null : new URL(item.keyId)
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
return principals;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Restores the evidence of a queued message.
|
|
133
|
+
* @param value The serialized principals.
|
|
134
|
+
* @param expandedActivity The expanded activity, restored from the queued
|
|
135
|
+
* verified view.
|
|
136
|
+
* @param baseUrl The base URL, restored from the queued verified view.
|
|
137
|
+
* @returns The evidence, or `null` if the serialized value is malformed.
|
|
138
|
+
* @internal
|
|
139
|
+
*/
|
|
140
|
+
function deserializeAuthentication(value, expandedActivity, baseUrl) {
|
|
141
|
+
if (value == null || typeof value !== "object" || typeof value.verified !== "boolean") return null;
|
|
142
|
+
const record = value;
|
|
143
|
+
const requestSigners = deserializePrincipals(record.requestSigners);
|
|
144
|
+
const documentSigners = deserializePrincipals(record.documentSigners);
|
|
145
|
+
const originAuthenticated = deserializePrincipals(record.originAuthenticated);
|
|
146
|
+
if (requestSigners == null || documentSigners == null || originAuthenticated == null) return null;
|
|
147
|
+
return createAuthenticationEvidence({
|
|
148
|
+
verified: record.verified,
|
|
149
|
+
requestSigners,
|
|
150
|
+
documentSigners,
|
|
151
|
+
originAuthenticated,
|
|
152
|
+
expandedActivity,
|
|
153
|
+
baseUrl
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Creates frozen evidence.
|
|
158
|
+
* @internal
|
|
159
|
+
*/
|
|
160
|
+
function createAuthenticationEvidence(evidence) {
|
|
161
|
+
return Object.freeze({
|
|
162
|
+
verified: evidence.verified,
|
|
163
|
+
requestSigners: Object.freeze([...evidence.requestSigners]),
|
|
164
|
+
documentSigners: Object.freeze([...evidence.documentSigners]),
|
|
165
|
+
originAuthenticated: Object.freeze([...evidence.originAuthenticated]),
|
|
166
|
+
expandedActivity: evidence.expandedActivity,
|
|
167
|
+
baseUrl: evidence.baseUrl
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
//#endregion
|
|
171
|
+
export { isActorAuthenticated as a, getEmptyAuthenticationEvidence as i, deserializeAuthentication as n, serializeAuthentication as o, getEffectivePrincipals as r, createAuthenticationEvidence as t };
|