@fedify/fedify 1.9.0-pr.428.1588 → 1.9.0-pr.431.1597
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/actor-CHOM_AN3.d.cts +128 -0
- package/dist/actor-CfaqWvJb.cjs +37783 -0
- package/dist/{actor-CdM_sJLC.js → actor-Detmisdi.js} +190 -190
- package/dist/{actor-ByOSf9n9.js → actor-Dw7av4Zk.js} +1 -1
- package/dist/{authdocloader-VVd0U8Qg.js → authdocloader-5UJ5Gme-.js} +6 -6
- package/dist/authdocloader-BVYv0Ct8.cjs +58 -0
- package/dist/{authdocloader-CfP6lfxM.js → authdocloader-ZlLoXSxM.js} +3 -3
- package/dist/{builder-mfXmda-W.js → builder-BbKfqPmR.js} +4 -4
- package/dist/chunk-DqRYRqnO.cjs +34 -0
- package/dist/{client-DB8N2RwI.js → client-DgRjF0ha.js} +1 -1
- package/dist/client-DjT_tegg.d.cts +294 -0
- package/dist/compat/mod.cjs +10 -0
- package/dist/compat/mod.d.cts +13 -0
- package/dist/compat/mod.js +5 -5
- package/dist/compat/transformers.test.js +16 -16
- package/dist/compat-DmDDELst.cjs +4 -0
- package/dist/compat-nxUqe4Z-.js +4 -0
- package/dist/context-KXVF2AhH.d.cts +2261 -0
- package/dist/{docloader-XJzCMWY-.js → docloader-BKf9RWX4.js} +4 -4
- package/dist/docloader-D-MrRyHl.d.cts +219 -0
- package/dist/docloader-eqgyMp7h.cjs +4682 -0
- package/dist/{esm-BVYQVacR.js → esm-B_ZjJ1sB.js} +1 -1
- package/dist/federation/builder.test.js +5 -5
- package/dist/federation/collection.test.js +3 -3
- package/dist/federation/handler.test.js +17 -17
- package/dist/federation/inbox.test.js +4 -4
- package/dist/federation/keycache.test.js +4 -4
- package/dist/federation/kv.test.js +3 -3
- package/dist/federation/middleware.test.js +18 -18
- package/dist/federation/mod.cjs +29 -0
- package/dist/federation/mod.d.cts +13 -0
- package/dist/federation/mod.js +15 -15
- package/dist/federation/mq.test.js +3 -3
- package/dist/federation/retry.test.js +3 -3
- package/dist/federation/router.test.js +3 -3
- package/dist/federation/send.test.js +10 -10
- package/dist/{federation-CMX7WzeL.js → federation-D1U8YY9t.js} +3 -3
- package/dist/federation-H2_En3j5.cjs +244 -0
- package/dist/http-B_zBcsai.d.cts +253 -0
- package/dist/{http-B_bBrm6N.js → http-C5XLveZw.js} +2 -2
- package/dist/{http-DYB4M8Pr.js → http-CwlUFNG4.js} +6 -6
- package/dist/http-_vjuGcXn.cjs +826 -0
- package/dist/{inbox-B4k2JQpE.js → inbox-sVXiVBbT.js} +1 -1
- package/dist/key-19P2dWvf.cjs +290 -0
- package/dist/{key-BdBFUTr5.js → key-BCWvPOkD.js} +5 -5
- package/dist/key-CsQ7J8-m.js +10 -0
- package/dist/key-Dt7qJaQT.cjs +10 -0
- package/dist/{key-feGm-IP3.js → key-Jrnr66vx.js} +2 -2
- package/dist/{key-BcVEPl--.js → key-lpATOAE4.js} +3 -3
- package/dist/{keycache-b7FYgzB9.js → keycache-ogQInQck.js} +1 -1
- package/dist/{keys-B7uX1KoX.js → keys-DcGsKtHW.js} +1 -1
- package/dist/kv-63Cil1MD.d.cts +81 -0
- package/dist/{ld-DJCemZO2.js → ld-BSE4jnyK.js} +2 -2
- package/dist/lookup-D96ipStp.cjs +137 -0
- package/dist/{lookup-CIsbr_Qi.js → lookup-D_-F1hLw.js} +4 -4
- package/dist/{lookup-CXaZBmuy.js → lookup-DdxOle8f.js} +1 -1
- package/dist/middleware-B0f850Ei.cjs +17 -0
- package/dist/middleware-B2DFqtJ-.cjs +4240 -0
- package/dist/middleware-COHAbwGs.js +17 -0
- package/dist/{middleware-D5JRdsEc.js → middleware-CuTcPjfP.js} +13 -13
- package/dist/middleware-Dt7C7qpw.js +26 -0
- package/dist/{middleware-e_Gw7sQy.js → middleware-T_y4Bnvw.js} +14 -14
- package/dist/mod-C2tOeRkN.d.cts +1 -0
- package/dist/mod-C3CGxYoF.d.cts +102 -0
- package/dist/mod-COw_caPC.d.cts +266 -0
- package/dist/mod-FZd39qVq.d.cts +1 -0
- package/dist/mod-NKH_G-IY.d.cts +289 -0
- package/dist/mod-YfAcrVbP.d.cts +80 -0
- package/dist/mod-jQ4OODsl.d.cts +113 -0
- package/dist/mod.cjs +152 -0
- package/dist/mod.d.cts +17 -0
- package/dist/mod.js +20 -20
- package/dist/mq-B7R1Q-M5.d.cts +140 -0
- package/dist/nodeinfo/client.test.js +5 -5
- package/dist/nodeinfo/handler.test.js +16 -16
- package/dist/nodeinfo/mod.cjs +13 -0
- package/dist/nodeinfo/mod.d.cts +5 -0
- package/dist/nodeinfo/mod.js +6 -6
- package/dist/nodeinfo/semver.test.js +3 -3
- package/dist/nodeinfo/types.test.js +3 -3
- package/dist/nodeinfo-Co9lJrWl.cjs +4 -0
- package/dist/nodeinfo-DfycQ8Wf.js +4 -0
- package/dist/owner-C9Ry0TOI.d.cts +67 -0
- package/dist/{owner-i9FkgZwa.js → owner-DJtc8evi.js} +2 -2
- package/dist/{proof-Bvtdd-Ul.js → proof-9OMp0o4n.js} +2 -2
- package/dist/{proof-DKGUTtBt.js → proof-BuPk23Er.js} +6 -6
- package/dist/proof-CRHppbRk.cjs +673 -0
- package/dist/runtime/authdocloader.test.js +9 -9
- package/dist/runtime/docloader.test.js +4 -4
- package/dist/runtime/key.test.js +5 -5
- package/dist/runtime/langstr.test.js +3 -3
- package/dist/runtime/mod.cjs +25 -0
- package/dist/runtime/mod.d.cts +6 -0
- package/dist/runtime/mod.js +10 -10
- package/dist/runtime/multibase/multibase.test.js +3 -3
- package/dist/runtime/url.test.js +3 -3
- package/dist/runtime-C58AJWSv.cjs +4 -0
- package/dist/runtime-DPYEDf-o.js +4 -0
- package/dist/{send-Cuw-wTCx.js → send-cXerEJm9.js} +2 -2
- package/dist/sig/http.test.js +8 -8
- package/dist/sig/key.test.js +6 -6
- package/dist/sig/ld.test.js +7 -7
- package/dist/sig/mod.cjs +30 -0
- package/dist/sig/mod.d.cts +8 -0
- package/dist/sig/mod.js +10 -10
- package/dist/sig/owner.test.js +7 -7
- package/dist/sig/proof.test.js +7 -7
- package/dist/sig-ByHXzqUi.cjs +4 -0
- package/dist/sig-Cj3tk-ig.js +4 -0
- package/dist/testing/docloader.test.js +3 -3
- package/dist/testing/mod.js +3 -3
- package/dist/{testing-DUFWSgSt.js → testing-BX6IA3LR.js} +2 -2
- package/dist/{transformers-Dna8Fg7k.js → transformers-BFT6d7J5.js} +3 -3
- package/dist/transformers-CoBS-oFG.cjs +116 -0
- package/dist/{type-DDwsy8fI.js → type-DF9yoIpt.js} +186 -186
- package/dist/types-DcKQIzdO.cjs +488 -0
- package/dist/{types-CuvNqe2X.js → types-Q17QxOOC.js} +4 -4
- package/dist/vocab/actor.test.js +5 -5
- package/dist/vocab/lookup.test.js +4 -4
- package/dist/vocab/mod.cjs +87 -0
- package/dist/vocab/mod.d.cts +6 -0
- package/dist/vocab/mod.js +7 -7
- package/dist/vocab/type.test.js +3 -3
- package/dist/vocab/vocab.test.js +4 -4
- package/dist/{vocab-CEXwobaS.js → vocab-BUc_4ZsW.js} +6 -6
- package/dist/vocab-BzGg7ltX.d.cts +14629 -0
- package/dist/vocab-Du8FV6H1.cjs +282 -0
- package/dist/webfinger/handler.test.js +16 -16
- package/dist/webfinger/lookup.test.js +4 -4
- package/dist/webfinger/mod.cjs +9 -0
- package/dist/webfinger/mod.d.cts +4 -0
- package/dist/webfinger/mod.js +6 -6
- package/dist/webfinger-BjOEdFPs.cjs +4 -0
- package/dist/webfinger-De_bU0iE.js +4 -0
- package/dist/x/cfworkers.cjs +100 -0
- package/dist/x/cfworkers.d.cts +59 -0
- package/dist/x/cfworkers.js +3 -3
- package/dist/x/cfworkers.test.js +3 -3
- package/dist/x/hono.cjs +61 -0
- package/dist/x/hono.d.cts +54 -0
- package/dist/x/hono.js +3 -3
- package/dist/x/sveltekit.cjs +69 -0
- package/dist/x/sveltekit.d.cts +46 -0
- package/dist/x/sveltekit.js +3 -3
- package/package.json +68 -12
- package/dist/compat-Bb5myD13.js +0 -4
- package/dist/key-0hFXQUQz.js +0 -10
- package/dist/middleware-BhQdOVAF.js +0 -17
- package/dist/middleware-DVy3ms0C.js +0 -26
- package/dist/nodeinfo-CyEbLjHs.js +0 -4
- package/dist/runtime-BSkOVUWM.js +0 -4
- package/dist/sig-BXJO--F9.js +0 -4
- package/dist/webfinger-C3GIyXIg.js +0 -4
@@ -0,0 +1,4240 @@
|
|
1
|
+
|
2
|
+
const { Temporal } = require("@js-temporal/polyfill");
|
3
|
+
const { URLPattern } = require("urlpattern-polyfill");
|
4
|
+
|
5
|
+
const require_chunk = require('./chunk-DqRYRqnO.cjs');
|
6
|
+
const require_transformers = require('./transformers-CoBS-oFG.cjs');
|
7
|
+
const require_docloader = require('./docloader-eqgyMp7h.cjs');
|
8
|
+
const require_actor = require('./actor-CfaqWvJb.cjs');
|
9
|
+
const require_lookup = require('./lookup-D96ipStp.cjs');
|
10
|
+
const require_key = require('./key-19P2dWvf.cjs');
|
11
|
+
const require_http = require('./http-_vjuGcXn.cjs');
|
12
|
+
const require_proof = require('./proof-CRHppbRk.cjs');
|
13
|
+
const require_types = require('./types-DcKQIzdO.cjs');
|
14
|
+
const require_authdocloader = require('./authdocloader-BVYv0Ct8.cjs');
|
15
|
+
const require_vocab = require('./vocab-Du8FV6H1.cjs');
|
16
|
+
const __logtape_logtape = require_chunk.__toESM(require("@logtape/logtape"));
|
17
|
+
const __opentelemetry_api = require_chunk.__toESM(require("@opentelemetry/api"));
|
18
|
+
const byte_encodings_hex = require_chunk.__toESM(require("byte-encodings/hex"));
|
19
|
+
const es_toolkit = require_chunk.__toESM(require("es-toolkit"));
|
20
|
+
const uri_template_router = require_chunk.__toESM(require("uri-template-router"));
|
21
|
+
const url_template = require_chunk.__toESM(require("url-template"));
|
22
|
+
const __opentelemetry_semantic_conventions = require_chunk.__toESM(require("@opentelemetry/semantic-conventions"));
|
23
|
+
const node_url = require_chunk.__toESM(require("node:url"));
|
24
|
+
|
25
|
+
//#region src/federation/inbox.ts
|
26
|
+
var InboxListenerSet = class InboxListenerSet {
|
27
|
+
#listeners;
|
28
|
+
constructor() {
|
29
|
+
this.#listeners = /* @__PURE__ */ new Map();
|
30
|
+
}
|
31
|
+
clone() {
|
32
|
+
const clone = new InboxListenerSet();
|
33
|
+
clone.#listeners = new Map(this.#listeners);
|
34
|
+
return clone;
|
35
|
+
}
|
36
|
+
add(type, listener) {
|
37
|
+
if (this.#listeners.has(type)) throw new TypeError("Listener already set for this type.");
|
38
|
+
this.#listeners.set(type, listener);
|
39
|
+
}
|
40
|
+
dispatchWithClass(activity) {
|
41
|
+
let cls = activity.constructor;
|
42
|
+
const inboxListeners = this.#listeners;
|
43
|
+
if (inboxListeners == null) return null;
|
44
|
+
while (true) {
|
45
|
+
if (inboxListeners.has(cls)) break;
|
46
|
+
if (cls === require_actor.Activity) return null;
|
47
|
+
cls = globalThis.Object.getPrototypeOf(cls);
|
48
|
+
}
|
49
|
+
const listener = inboxListeners.get(cls);
|
50
|
+
return {
|
51
|
+
class: cls,
|
52
|
+
listener
|
53
|
+
};
|
54
|
+
}
|
55
|
+
dispatch(activity) {
|
56
|
+
return this.dispatchWithClass(activity)?.listener ?? null;
|
57
|
+
}
|
58
|
+
};
|
59
|
+
async function routeActivity({ context: ctx, json, activity, recipient, inboxListeners, inboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, tracerProvider }) {
|
60
|
+
const logger$1 = (0, __logtape_logtape.getLogger)([
|
61
|
+
"fedify",
|
62
|
+
"federation",
|
63
|
+
"inbox"
|
64
|
+
]);
|
65
|
+
const cacheKey = activity.id == null ? null : [
|
66
|
+
...kvPrefixes.activityIdempotence,
|
67
|
+
ctx.origin,
|
68
|
+
activity.id.href
|
69
|
+
];
|
70
|
+
if (cacheKey != null) {
|
71
|
+
const cached = await kv.get(cacheKey);
|
72
|
+
if (cached === true) {
|
73
|
+
logger$1.debug("Activity {activityId} has already been processed.", {
|
74
|
+
activityId: activity.id?.href,
|
75
|
+
activity: json,
|
76
|
+
recipient
|
77
|
+
});
|
78
|
+
span.setStatus({
|
79
|
+
code: __opentelemetry_api.SpanStatusCode.UNSET,
|
80
|
+
message: `Activity ${activity.id?.href} has already been processed.`
|
81
|
+
});
|
82
|
+
return "alreadyProcessed";
|
83
|
+
}
|
84
|
+
}
|
85
|
+
if (activity.actorId == null) {
|
86
|
+
logger$1.error("Missing actor.", { activity: json });
|
87
|
+
span.setStatus({
|
88
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
89
|
+
message: "Missing actor."
|
90
|
+
});
|
91
|
+
return "missingActor";
|
92
|
+
}
|
93
|
+
span.setAttribute("activitypub.actor.id", activity.actorId.href);
|
94
|
+
if (queue != null) {
|
95
|
+
const carrier = {};
|
96
|
+
__opentelemetry_api.propagation.inject(__opentelemetry_api.context.active(), carrier);
|
97
|
+
try {
|
98
|
+
await queue.enqueue({
|
99
|
+
type: "inbox",
|
100
|
+
id: crypto.randomUUID(),
|
101
|
+
baseUrl: ctx.origin,
|
102
|
+
activity: json,
|
103
|
+
identifier: recipient,
|
104
|
+
attempt: 0,
|
105
|
+
started: (/* @__PURE__ */ new Date()).toISOString(),
|
106
|
+
traceContext: carrier
|
107
|
+
});
|
108
|
+
} catch (error) {
|
109
|
+
logger$1.error("Failed to enqueue the incoming activity {activityId}:\n{error}", {
|
110
|
+
error,
|
111
|
+
activityId: activity.id?.href,
|
112
|
+
activity: json,
|
113
|
+
recipient
|
114
|
+
});
|
115
|
+
span.setStatus({
|
116
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
117
|
+
message: `Failed to enqueue the incoming activity ${activity.id?.href}.`
|
118
|
+
});
|
119
|
+
throw error;
|
120
|
+
}
|
121
|
+
logger$1.info("Activity {activityId} is enqueued.", {
|
122
|
+
activityId: activity.id?.href,
|
123
|
+
activity: json,
|
124
|
+
recipient
|
125
|
+
});
|
126
|
+
return "enqueued";
|
127
|
+
}
|
128
|
+
tracerProvider = tracerProvider ?? __opentelemetry_api.trace.getTracerProvider();
|
129
|
+
const tracer = tracerProvider.getTracer(require_docloader.deno_default.name, require_docloader.deno_default.version);
|
130
|
+
return await tracer.startActiveSpan("activitypub.dispatch_inbox_listener", { kind: __opentelemetry_api.SpanKind.INTERNAL }, async (span$1) => {
|
131
|
+
const dispatched = inboxListeners?.dispatchWithClass(activity);
|
132
|
+
if (dispatched == null) {
|
133
|
+
logger$1.error("Unsupported activity type:\n{activity}", {
|
134
|
+
activity: json,
|
135
|
+
recipient
|
136
|
+
});
|
137
|
+
span$1.setStatus({
|
138
|
+
code: __opentelemetry_api.SpanStatusCode.UNSET,
|
139
|
+
message: `Unsupported activity type: ${require_actor.getTypeId(activity).href}`
|
140
|
+
});
|
141
|
+
span$1.end();
|
142
|
+
return "unsupportedActivity";
|
143
|
+
}
|
144
|
+
const { class: cls, listener } = dispatched;
|
145
|
+
span$1.updateName(`activitypub.dispatch_inbox_listener ${cls.name}`);
|
146
|
+
try {
|
147
|
+
await listener(inboxContextFactory(recipient, json, activity?.id?.href, require_actor.getTypeId(activity).href), activity);
|
148
|
+
} catch (error) {
|
149
|
+
try {
|
150
|
+
await inboxErrorHandler?.(ctx, error);
|
151
|
+
} catch (error$1) {
|
152
|
+
logger$1.error("An unexpected error occurred in inbox error handler:\n{error}", {
|
153
|
+
error: error$1,
|
154
|
+
activityId: activity.id?.href,
|
155
|
+
activity: json,
|
156
|
+
recipient
|
157
|
+
});
|
158
|
+
}
|
159
|
+
logger$1.error("Failed to process the incoming activity {activityId}:\n{error}", {
|
160
|
+
error,
|
161
|
+
activityId: activity.id?.href,
|
162
|
+
activity: json,
|
163
|
+
recipient
|
164
|
+
});
|
165
|
+
span$1.setStatus({
|
166
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
167
|
+
message: String(error)
|
168
|
+
});
|
169
|
+
span$1.end();
|
170
|
+
return "error";
|
171
|
+
}
|
172
|
+
if (cacheKey != null) await kv.set(cacheKey, true, { ttl: Temporal.Duration.from({ days: 1 }) });
|
173
|
+
logger$1.info("Activity {activityId} has been processed.", {
|
174
|
+
activityId: activity.id?.href,
|
175
|
+
activity: json,
|
176
|
+
recipient
|
177
|
+
});
|
178
|
+
span$1.end();
|
179
|
+
return "success";
|
180
|
+
});
|
181
|
+
}
|
182
|
+
|
183
|
+
//#endregion
|
184
|
+
//#region src/federation/router.ts
|
185
|
+
function cloneInnerRouter(router) {
|
186
|
+
const clone = new uri_template_router.Router();
|
187
|
+
clone.nid = router.nid;
|
188
|
+
clone.fsm = (0, es_toolkit.cloneDeep)(router.fsm);
|
189
|
+
clone.routeSet = new Set(router.routeSet);
|
190
|
+
clone.templateRouteMap = new Map(router.templateRouteMap);
|
191
|
+
clone.valueRouteMap = new Map(router.valueRouteMap);
|
192
|
+
clone.hierarchy = (0, es_toolkit.cloneDeep)(router.hierarchy);
|
193
|
+
return clone;
|
194
|
+
}
|
195
|
+
/**
|
196
|
+
* URL router and constructor based on URI Template
|
197
|
+
* ([RFC 6570](https://tools.ietf.org/html/rfc6570)).
|
198
|
+
*/
|
199
|
+
var Router = class Router {
|
200
|
+
#router;
|
201
|
+
#templates;
|
202
|
+
#templateStrings;
|
203
|
+
/**
|
204
|
+
* Whether to ignore trailing slashes when matching paths.
|
205
|
+
* @since 1.6.0
|
206
|
+
*/
|
207
|
+
trailingSlashInsensitive;
|
208
|
+
/**
|
209
|
+
* Create a new {@link Router}.
|
210
|
+
* @param options Options for the router.
|
211
|
+
*/
|
212
|
+
constructor(options = {}) {
|
213
|
+
this.#router = new uri_template_router.Router();
|
214
|
+
this.#templates = {};
|
215
|
+
this.#templateStrings = {};
|
216
|
+
this.trailingSlashInsensitive = options.trailingSlashInsensitive ?? false;
|
217
|
+
}
|
218
|
+
clone() {
|
219
|
+
const clone = new Router({ trailingSlashInsensitive: this.trailingSlashInsensitive });
|
220
|
+
clone.#router = cloneInnerRouter(this.#router);
|
221
|
+
clone.#templates = { ...this.#templates };
|
222
|
+
clone.#templateStrings = { ...this.#templateStrings };
|
223
|
+
return clone;
|
224
|
+
}
|
225
|
+
/**
|
226
|
+
* Checks if a path name exists in the router.
|
227
|
+
* @param name The name of the path.
|
228
|
+
* @returns `true` if the path name exists, otherwise `false`.
|
229
|
+
*/
|
230
|
+
has(name) {
|
231
|
+
return name in this.#templates;
|
232
|
+
}
|
233
|
+
/**
|
234
|
+
* Adds a new path rule to the router.
|
235
|
+
* @param template The path pattern.
|
236
|
+
* @param name The name of the path.
|
237
|
+
* @returns The names of the variables in the path pattern.
|
238
|
+
*/
|
239
|
+
add(template, name) {
|
240
|
+
if (!template.startsWith("/")) throw new RouterError("Path must start with a slash.");
|
241
|
+
const rule = this.#router.addTemplate(template, {}, name);
|
242
|
+
this.#templates[name] = (0, url_template.parseTemplate)(template);
|
243
|
+
this.#templateStrings[name] = template;
|
244
|
+
return new Set(rule.variables.map((v) => v.varname));
|
245
|
+
}
|
246
|
+
/**
|
247
|
+
* Resolves a path name and values from a URL, if any match.
|
248
|
+
* @param url The URL to resolve.
|
249
|
+
* @returns The name of the path and its values, if any match. Otherwise,
|
250
|
+
* `null`.
|
251
|
+
*/
|
252
|
+
route(url) {
|
253
|
+
let match = this.#router.resolveURI(url);
|
254
|
+
if (match == null) {
|
255
|
+
if (!this.trailingSlashInsensitive) return null;
|
256
|
+
url = url.endsWith("/") ? url.replace(/\/+$/, "") : `${url}/`;
|
257
|
+
match = this.#router.resolveURI(url);
|
258
|
+
if (match == null) return null;
|
259
|
+
}
|
260
|
+
return {
|
261
|
+
name: match.matchValue,
|
262
|
+
template: this.#templateStrings[match.matchValue],
|
263
|
+
values: match.params
|
264
|
+
};
|
265
|
+
}
|
266
|
+
/**
|
267
|
+
* Constructs a URL/path from a path name and values.
|
268
|
+
* @param name The name of the path.
|
269
|
+
* @param values The values to expand the path with.
|
270
|
+
* @returns The URL/path, if the name exists. Otherwise, `null`.
|
271
|
+
*/
|
272
|
+
build(name, values) {
|
273
|
+
if (name in this.#templates) return this.#templates[name].expand(values);
|
274
|
+
return null;
|
275
|
+
}
|
276
|
+
};
|
277
|
+
/**
|
278
|
+
* An error thrown by the {@link Router}.
|
279
|
+
*/
|
280
|
+
var RouterError = class extends Error {
|
281
|
+
/**
|
282
|
+
* Create a new {@link RouterError}.
|
283
|
+
* @param message The error message.
|
284
|
+
*/
|
285
|
+
constructor(message) {
|
286
|
+
super(message);
|
287
|
+
this.name = "RouterError";
|
288
|
+
}
|
289
|
+
};
|
290
|
+
|
291
|
+
//#endregion
|
292
|
+
//#region src/federation/builder.ts
|
293
|
+
var FederationBuilderImpl = class {
|
294
|
+
router;
|
295
|
+
actorCallbacks;
|
296
|
+
nodeInfoDispatcher;
|
297
|
+
webFingerLinksDispatcher;
|
298
|
+
objectCallbacks;
|
299
|
+
objectTypeIds;
|
300
|
+
inboxPath;
|
301
|
+
inboxCallbacks;
|
302
|
+
outboxCallbacks;
|
303
|
+
followingCallbacks;
|
304
|
+
followersCallbacks;
|
305
|
+
likedCallbacks;
|
306
|
+
featuredCallbacks;
|
307
|
+
featuredTagsCallbacks;
|
308
|
+
inboxListeners;
|
309
|
+
inboxErrorHandler;
|
310
|
+
sharedInboxKeyDispatcher;
|
311
|
+
collectionTypeIds;
|
312
|
+
collectionCallbacks;
|
313
|
+
/**
|
314
|
+
* Symbol registry for unique identification of unnamed symbols.
|
315
|
+
*/
|
316
|
+
#symbolRegistry = /* @__PURE__ */ new Map();
|
317
|
+
constructor() {
|
318
|
+
this.router = new Router();
|
319
|
+
this.objectCallbacks = {};
|
320
|
+
this.objectTypeIds = {};
|
321
|
+
this.collectionCallbacks = {};
|
322
|
+
this.collectionTypeIds = {};
|
323
|
+
}
|
324
|
+
async build(options) {
|
325
|
+
const { FederationImpl: FederationImpl$1 } = await Promise.resolve().then(() => require("./middleware-B0f850Ei.cjs"));
|
326
|
+
const f = new FederationImpl$1(options);
|
327
|
+
const trailingSlashInsensitiveValue = f.router.trailingSlashInsensitive;
|
328
|
+
f.router = this.router.clone();
|
329
|
+
f.router.trailingSlashInsensitive = trailingSlashInsensitiveValue;
|
330
|
+
f._initializeRouter();
|
331
|
+
f.actorCallbacks = this.actorCallbacks == null ? void 0 : { ...this.actorCallbacks };
|
332
|
+
f.nodeInfoDispatcher = this.nodeInfoDispatcher;
|
333
|
+
f.webFingerLinksDispatcher = this.webFingerLinksDispatcher;
|
334
|
+
f.objectCallbacks = { ...this.objectCallbacks };
|
335
|
+
f.objectTypeIds = { ...this.objectTypeIds };
|
336
|
+
f.inboxPath = this.inboxPath;
|
337
|
+
f.inboxCallbacks = this.inboxCallbacks == null ? void 0 : { ...this.inboxCallbacks };
|
338
|
+
f.outboxCallbacks = this.outboxCallbacks == null ? void 0 : { ...this.outboxCallbacks };
|
339
|
+
f.followingCallbacks = this.followingCallbacks == null ? void 0 : { ...this.followingCallbacks };
|
340
|
+
f.followersCallbacks = this.followersCallbacks == null ? void 0 : { ...this.followersCallbacks };
|
341
|
+
f.likedCallbacks = this.likedCallbacks == null ? void 0 : { ...this.likedCallbacks };
|
342
|
+
f.featuredCallbacks = this.featuredCallbacks == null ? void 0 : { ...this.featuredCallbacks };
|
343
|
+
f.featuredTagsCallbacks = this.featuredTagsCallbacks == null ? void 0 : { ...this.featuredTagsCallbacks };
|
344
|
+
f.inboxListeners = this.inboxListeners?.clone();
|
345
|
+
f.inboxErrorHandler = this.inboxErrorHandler;
|
346
|
+
f.sharedInboxKeyDispatcher = this.sharedInboxKeyDispatcher;
|
347
|
+
return f;
|
348
|
+
}
|
349
|
+
_getTracer() {
|
350
|
+
return __opentelemetry_api.trace.getTracer(require_docloader.deno_default.name, require_docloader.deno_default.version);
|
351
|
+
}
|
352
|
+
setActorDispatcher(path, dispatcher) {
|
353
|
+
if (this.router.has("actor")) throw new RouterError("Actor dispatcher already set.");
|
354
|
+
const variables = this.router.add(path, "actor");
|
355
|
+
if (variables.size !== 1 || !(variables.has("identifier") || variables.has("handle"))) throw new RouterError("Path for actor dispatcher must have one variable: {identifier}");
|
356
|
+
if (variables.has("handle")) (0, __logtape_logtape.getLogger)([
|
357
|
+
"fedify",
|
358
|
+
"federation",
|
359
|
+
"actor"
|
360
|
+
]).warn("The {{handle}} variable in the actor dispatcher path is deprecated. Use {{identifier}} instead.");
|
361
|
+
const callbacks = { dispatcher: async (context$2, identifier) => {
|
362
|
+
const actor = await this._getTracer().startActiveSpan("activitypub.dispatch_actor", {
|
363
|
+
kind: __opentelemetry_api.SpanKind.SERVER,
|
364
|
+
attributes: { "fedify.actor.identifier": identifier }
|
365
|
+
}, async (span) => {
|
366
|
+
try {
|
367
|
+
const actor$1 = await dispatcher(context$2, identifier);
|
368
|
+
span.setAttribute("activitypub.actor.id", (actor$1?.id ?? context$2.getActorUri(identifier)).href);
|
369
|
+
if (actor$1 == null) span.setStatus({ code: __opentelemetry_api.SpanStatusCode.ERROR });
|
370
|
+
else span.setAttribute("activitypub.actor.type", require_actor.getTypeId(actor$1).href);
|
371
|
+
return actor$1;
|
372
|
+
} catch (error) {
|
373
|
+
span.setStatus({
|
374
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
375
|
+
message: String(error)
|
376
|
+
});
|
377
|
+
throw error;
|
378
|
+
} finally {
|
379
|
+
span.end();
|
380
|
+
}
|
381
|
+
});
|
382
|
+
if (actor == null) return null;
|
383
|
+
const logger$1 = (0, __logtape_logtape.getLogger)([
|
384
|
+
"fedify",
|
385
|
+
"federation",
|
386
|
+
"actor"
|
387
|
+
]);
|
388
|
+
if (actor.id == null) logger$1.warn("Actor dispatcher returned an actor without an id property. Set the property with Context.getActorUri(identifier).");
|
389
|
+
else if (actor.id.href != context$2.getActorUri(identifier).href) logger$1.warn("Actor dispatcher returned an actor with an id property that does not match the actor URI. Set the property with Context.getActorUri(identifier).");
|
390
|
+
if (this.followingCallbacks != null && this.followingCallbacks.dispatcher != null) {
|
391
|
+
if (actor.followingId == null) logger$1.warn("You configured a following collection dispatcher, but the actor does not have a following property. Set the property with Context.getFollowingUri(identifier).");
|
392
|
+
else if (actor.followingId.href != context$2.getFollowingUri(identifier).href) logger$1.warn("You configured a following collection dispatcher, but the actor's following property does not match the following collection URI. Set the property with Context.getFollowingUri(identifier).");
|
393
|
+
}
|
394
|
+
if (this.followersCallbacks != null && this.followersCallbacks.dispatcher != null) {
|
395
|
+
if (actor.followersId == null) logger$1.warn("You configured a followers collection dispatcher, but the actor does not have a followers property. Set the property with Context.getFollowersUri(identifier).");
|
396
|
+
else if (actor.followersId.href != context$2.getFollowersUri(identifier).href) logger$1.warn("You configured a followers collection dispatcher, but the actor's followers property does not match the followers collection URI. Set the property with Context.getFollowersUri(identifier).");
|
397
|
+
}
|
398
|
+
if (this.outboxCallbacks != null && this.outboxCallbacks.dispatcher != null) {
|
399
|
+
if (actor?.outboxId == null) logger$1.warn("You configured an outbox collection dispatcher, but the actor does not have an outbox property. Set the property with Context.getOutboxUri(identifier).");
|
400
|
+
else if (actor.outboxId.href != context$2.getOutboxUri(identifier).href) logger$1.warn("You configured an outbox collection dispatcher, but the actor's outbox property does not match the outbox collection URI. Set the property with Context.getOutboxUri(identifier).");
|
401
|
+
}
|
402
|
+
if (this.likedCallbacks != null && this.likedCallbacks.dispatcher != null) {
|
403
|
+
if (actor?.likedId == null) logger$1.warn("You configured a liked collection dispatcher, but the actor does not have a liked property. Set the property with Context.getLikedUri(identifier).");
|
404
|
+
else if (actor.likedId.href != context$2.getLikedUri(identifier).href) logger$1.warn("You configured a liked collection dispatcher, but the actor's liked property does not match the liked collection URI. Set the property with Context.getLikedUri(identifier).");
|
405
|
+
}
|
406
|
+
if (this.featuredCallbacks != null && this.featuredCallbacks.dispatcher != null) {
|
407
|
+
if (actor?.featuredId == null) logger$1.warn("You configured a featured collection dispatcher, but the actor does not have a featured property. Set the property with Context.getFeaturedUri(identifier).");
|
408
|
+
else if (actor.featuredId.href != context$2.getFeaturedUri(identifier).href) logger$1.warn("You configured a featured collection dispatcher, but the actor's featured property does not match the featured collection URI. Set the property with Context.getFeaturedUri(identifier).");
|
409
|
+
}
|
410
|
+
if (this.featuredTagsCallbacks != null && this.featuredTagsCallbacks.dispatcher != null) {
|
411
|
+
if (actor?.featuredTagsId == null) logger$1.warn("You configured a featured tags collection dispatcher, but the actor does not have a featuredTags property. Set the property with Context.getFeaturedTagsUri(identifier).");
|
412
|
+
else if (actor.featuredTagsId.href != context$2.getFeaturedTagsUri(identifier).href) logger$1.warn("You configured a featured tags collection dispatcher, but the actor's featuredTags property does not match the featured tags collection URI. Set the property with Context.getFeaturedTagsUri(identifier).");
|
413
|
+
}
|
414
|
+
if (this.router.has("inbox")) {
|
415
|
+
if (actor.inboxId == null) logger$1.warn("You configured inbox listeners, but the actor does not have an inbox property. Set the property with Context.getInboxUri(identifier).");
|
416
|
+
else if (actor.inboxId.href != context$2.getInboxUri(identifier).href) logger$1.warn("You configured inbox listeners, but the actor's inbox property does not match the inbox URI. Set the property with Context.getInboxUri(identifier).");
|
417
|
+
if (actor.endpoints == null || actor.endpoints.sharedInbox == null) logger$1.warn("You configured inbox listeners, but the actor does not have a endpoints.sharedInbox property. Set the property with Context.getInboxUri().");
|
418
|
+
else if (actor.endpoints.sharedInbox.href != context$2.getInboxUri().href) logger$1.warn("You configured inbox listeners, but the actor's endpoints.sharedInbox property does not match the shared inbox URI. Set the property with Context.getInboxUri().");
|
419
|
+
}
|
420
|
+
if (callbacks.keyPairsDispatcher != null) {
|
421
|
+
if (actor.publicKeyId == null) logger$1.warn("You configured a key pairs dispatcher, but the actor does not have a publicKey property. Set the property with Context.getActorKeyPairs(identifier).");
|
422
|
+
if (actor.assertionMethodId == null) logger$1.warn("You configured a key pairs dispatcher, but the actor does not have an assertionMethod property. Set the property with Context.getActorKeyPairs(identifier).");
|
423
|
+
}
|
424
|
+
return actor;
|
425
|
+
} };
|
426
|
+
this.actorCallbacks = callbacks;
|
427
|
+
const setters = {
|
428
|
+
setKeyPairsDispatcher: (dispatcher$1) => {
|
429
|
+
callbacks.keyPairsDispatcher = (ctx, identifier) => this._getTracer().startActiveSpan("activitypub.dispatch_actor_key_pairs", {
|
430
|
+
kind: __opentelemetry_api.SpanKind.SERVER,
|
431
|
+
attributes: {
|
432
|
+
"activitypub.actor.id": ctx.getActorUri(identifier).href,
|
433
|
+
"fedify.actor.identifier": identifier
|
434
|
+
}
|
435
|
+
}, async (span) => {
|
436
|
+
try {
|
437
|
+
return await dispatcher$1(ctx, identifier);
|
438
|
+
} catch (e) {
|
439
|
+
span.setStatus({
|
440
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
441
|
+
message: String(e)
|
442
|
+
});
|
443
|
+
throw e;
|
444
|
+
} finally {
|
445
|
+
span.end();
|
446
|
+
}
|
447
|
+
});
|
448
|
+
return setters;
|
449
|
+
},
|
450
|
+
mapHandle(mapper) {
|
451
|
+
callbacks.handleMapper = mapper;
|
452
|
+
return setters;
|
453
|
+
},
|
454
|
+
mapAlias(mapper) {
|
455
|
+
callbacks.aliasMapper = mapper;
|
456
|
+
return setters;
|
457
|
+
},
|
458
|
+
authorize(predicate) {
|
459
|
+
callbacks.authorizePredicate = predicate;
|
460
|
+
return setters;
|
461
|
+
}
|
462
|
+
};
|
463
|
+
return setters;
|
464
|
+
}
|
465
|
+
setNodeInfoDispatcher(path, dispatcher) {
|
466
|
+
if (this.router.has("nodeInfo")) throw new RouterError("NodeInfo dispatcher already set.");
|
467
|
+
const variables = this.router.add(path, "nodeInfo");
|
468
|
+
if (variables.size !== 0) throw new RouterError("Path for NodeInfo dispatcher must have no variables.");
|
469
|
+
this.nodeInfoDispatcher = dispatcher;
|
470
|
+
}
|
471
|
+
setWebFingerLinksDispatcher(dispatcher) {
|
472
|
+
this.webFingerLinksDispatcher = dispatcher;
|
473
|
+
}
|
474
|
+
setObjectDispatcher(cls, path, dispatcher) {
|
475
|
+
const routeName = `object:${cls.typeId.href}`;
|
476
|
+
if (this.router.has(routeName)) throw new RouterError(`Object dispatcher for ${cls.name} already set.`);
|
477
|
+
const variables = this.router.add(path, routeName);
|
478
|
+
if (variables.size < 1) throw new RouterError("Path for object dispatcher must have at least one variable.");
|
479
|
+
const callbacks = {
|
480
|
+
dispatcher: (ctx, values) => {
|
481
|
+
const tracer = this._getTracer();
|
482
|
+
return tracer.startActiveSpan("activitypub.dispatch_object", {
|
483
|
+
kind: __opentelemetry_api.SpanKind.SERVER,
|
484
|
+
attributes: {
|
485
|
+
"fedify.object.type": cls.typeId.href,
|
486
|
+
...globalThis.Object.fromEntries(globalThis.Object.entries(values).map(([k, v]) => [`fedify.object.values.${k}`, v]))
|
487
|
+
}
|
488
|
+
}, async (span) => {
|
489
|
+
try {
|
490
|
+
const object = await dispatcher(ctx, values);
|
491
|
+
span.setAttribute("activitypub.object.id", (object?.id ?? ctx.getObjectUri(cls, values)).href);
|
492
|
+
if (object == null) span.setStatus({ code: __opentelemetry_api.SpanStatusCode.ERROR });
|
493
|
+
else span.setAttribute("activitypub.object.type", require_actor.getTypeId(object).href);
|
494
|
+
return object;
|
495
|
+
} catch (e) {
|
496
|
+
span.setStatus({
|
497
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
498
|
+
message: String(e)
|
499
|
+
});
|
500
|
+
throw e;
|
501
|
+
} finally {
|
502
|
+
span.end();
|
503
|
+
}
|
504
|
+
});
|
505
|
+
},
|
506
|
+
parameters: variables
|
507
|
+
};
|
508
|
+
this.objectCallbacks[cls.typeId.href] = callbacks;
|
509
|
+
this.objectTypeIds[cls.typeId.href] = cls;
|
510
|
+
const setters = { authorize(predicate) {
|
511
|
+
callbacks.authorizePredicate = predicate;
|
512
|
+
return setters;
|
513
|
+
} };
|
514
|
+
return setters;
|
515
|
+
}
|
516
|
+
setInboxDispatcher(path, dispatcher) {
|
517
|
+
if (this.inboxCallbacks != null) throw new RouterError("Inbox dispatcher already set.");
|
518
|
+
if (this.router.has("inbox")) {
|
519
|
+
if (this.inboxPath !== path) throw new RouterError("Inbox dispatcher path must match inbox listener path.");
|
520
|
+
} else {
|
521
|
+
const variables = this.router.add(path, "inbox");
|
522
|
+
if (variables.size !== 1 || !(variables.has("identifier") || variables.has("handle"))) throw new RouterError("Path for inbox dispatcher must have one variable: {identifier}");
|
523
|
+
if (variables.has("handle")) (0, __logtape_logtape.getLogger)([
|
524
|
+
"fedify",
|
525
|
+
"federation",
|
526
|
+
"inbox"
|
527
|
+
]).warn("The {{handle}} variable in the inbox dispatcher path is deprecated. Use {{identifier}} instead.");
|
528
|
+
this.inboxPath = path;
|
529
|
+
}
|
530
|
+
const callbacks = { dispatcher };
|
531
|
+
this.inboxCallbacks = callbacks;
|
532
|
+
const setters = {
|
533
|
+
setCounter(counter) {
|
534
|
+
callbacks.counter = counter;
|
535
|
+
return setters;
|
536
|
+
},
|
537
|
+
setFirstCursor(cursor) {
|
538
|
+
callbacks.firstCursor = cursor;
|
539
|
+
return setters;
|
540
|
+
},
|
541
|
+
setLastCursor(cursor) {
|
542
|
+
callbacks.lastCursor = cursor;
|
543
|
+
return setters;
|
544
|
+
},
|
545
|
+
authorize(predicate) {
|
546
|
+
callbacks.authorizePredicate = predicate;
|
547
|
+
return setters;
|
548
|
+
}
|
549
|
+
};
|
550
|
+
return setters;
|
551
|
+
}
|
552
|
+
setOutboxDispatcher(path, dispatcher) {
|
553
|
+
if (this.router.has("outbox")) throw new RouterError("Outbox dispatcher already set.");
|
554
|
+
const variables = this.router.add(path, "outbox");
|
555
|
+
if (variables.size !== 1 || !(variables.has("identifier") || variables.has("handle"))) throw new RouterError("Path for outbox dispatcher must have one variable: {identifier}");
|
556
|
+
if (variables.has("handle")) (0, __logtape_logtape.getLogger)([
|
557
|
+
"fedify",
|
558
|
+
"federation",
|
559
|
+
"outbox"
|
560
|
+
]).warn("The {{handle}} variable in the outbox dispatcher path is deprecated. Use {{identifier}} instead.");
|
561
|
+
const callbacks = { dispatcher };
|
562
|
+
this.outboxCallbacks = callbacks;
|
563
|
+
const setters = {
|
564
|
+
setCounter(counter) {
|
565
|
+
callbacks.counter = counter;
|
566
|
+
return setters;
|
567
|
+
},
|
568
|
+
setFirstCursor(cursor) {
|
569
|
+
callbacks.firstCursor = cursor;
|
570
|
+
return setters;
|
571
|
+
},
|
572
|
+
setLastCursor(cursor) {
|
573
|
+
callbacks.lastCursor = cursor;
|
574
|
+
return setters;
|
575
|
+
},
|
576
|
+
authorize(predicate) {
|
577
|
+
callbacks.authorizePredicate = predicate;
|
578
|
+
return setters;
|
579
|
+
}
|
580
|
+
};
|
581
|
+
return setters;
|
582
|
+
}
|
583
|
+
setFollowingDispatcher(path, dispatcher) {
|
584
|
+
if (this.router.has("following")) throw new RouterError("Following collection dispatcher already set.");
|
585
|
+
const variables = this.router.add(path, "following");
|
586
|
+
if (variables.size !== 1 || !(variables.has("identifier") || variables.has("handle"))) throw new RouterError("Path for following collection dispatcher must have one variable: {identifier}");
|
587
|
+
if (variables.has("handle")) (0, __logtape_logtape.getLogger)([
|
588
|
+
"fedify",
|
589
|
+
"federation",
|
590
|
+
"collection"
|
591
|
+
]).warn("The {{handle}} variable in the following collection dispatcher path is deprecated. Use {{identifier}} instead.");
|
592
|
+
const callbacks = { dispatcher };
|
593
|
+
this.followingCallbacks = callbacks;
|
594
|
+
const setters = {
|
595
|
+
setCounter(counter) {
|
596
|
+
callbacks.counter = counter;
|
597
|
+
return setters;
|
598
|
+
},
|
599
|
+
setFirstCursor(cursor) {
|
600
|
+
callbacks.firstCursor = cursor;
|
601
|
+
return setters;
|
602
|
+
},
|
603
|
+
setLastCursor(cursor) {
|
604
|
+
callbacks.lastCursor = cursor;
|
605
|
+
return setters;
|
606
|
+
},
|
607
|
+
authorize(predicate) {
|
608
|
+
callbacks.authorizePredicate = predicate;
|
609
|
+
return setters;
|
610
|
+
}
|
611
|
+
};
|
612
|
+
return setters;
|
613
|
+
}
|
614
|
+
setFollowersDispatcher(path, dispatcher) {
|
615
|
+
if (this.router.has("followers")) throw new RouterError("Followers collection dispatcher already set.");
|
616
|
+
const variables = this.router.add(path, "followers");
|
617
|
+
if (variables.size !== 1 || !(variables.has("identifier") || variables.has("handle"))) throw new RouterError("Path for followers collection dispatcher must have one variable: {identifier}");
|
618
|
+
if (variables.has("handle")) (0, __logtape_logtape.getLogger)([
|
619
|
+
"fedify",
|
620
|
+
"federation",
|
621
|
+
"collection"
|
622
|
+
]).warn("The {{handle}} variable in the followers collection dispatcher path is deprecated. Use {{identifier}} instead.");
|
623
|
+
const callbacks = { dispatcher };
|
624
|
+
this.followersCallbacks = callbacks;
|
625
|
+
const setters = {
|
626
|
+
setCounter(counter) {
|
627
|
+
callbacks.counter = counter;
|
628
|
+
return setters;
|
629
|
+
},
|
630
|
+
setFirstCursor(cursor) {
|
631
|
+
callbacks.firstCursor = cursor;
|
632
|
+
return setters;
|
633
|
+
},
|
634
|
+
setLastCursor(cursor) {
|
635
|
+
callbacks.lastCursor = cursor;
|
636
|
+
return setters;
|
637
|
+
},
|
638
|
+
authorize(predicate) {
|
639
|
+
callbacks.authorizePredicate = predicate;
|
640
|
+
return setters;
|
641
|
+
}
|
642
|
+
};
|
643
|
+
return setters;
|
644
|
+
}
|
645
|
+
setLikedDispatcher(path, dispatcher) {
|
646
|
+
if (this.router.has("liked")) throw new RouterError("Liked collection dispatcher already set.");
|
647
|
+
const variables = this.router.add(path, "liked");
|
648
|
+
if (variables.size !== 1 || !(variables.has("identifier") || variables.has("handle"))) throw new RouterError("Path for liked collection dispatcher must have one variable: {identifier}");
|
649
|
+
if (variables.has("handle")) (0, __logtape_logtape.getLogger)([
|
650
|
+
"fedify",
|
651
|
+
"federation",
|
652
|
+
"collection"
|
653
|
+
]).warn("The {{handle}} variable in the liked collection dispatcher path is deprecated. Use {{identifier}} instead.");
|
654
|
+
const callbacks = { dispatcher };
|
655
|
+
this.likedCallbacks = callbacks;
|
656
|
+
const setters = {
|
657
|
+
setCounter(counter) {
|
658
|
+
callbacks.counter = counter;
|
659
|
+
return setters;
|
660
|
+
},
|
661
|
+
setFirstCursor(cursor) {
|
662
|
+
callbacks.firstCursor = cursor;
|
663
|
+
return setters;
|
664
|
+
},
|
665
|
+
setLastCursor(cursor) {
|
666
|
+
callbacks.lastCursor = cursor;
|
667
|
+
return setters;
|
668
|
+
},
|
669
|
+
authorize(predicate) {
|
670
|
+
callbacks.authorizePredicate = predicate;
|
671
|
+
return setters;
|
672
|
+
}
|
673
|
+
};
|
674
|
+
return setters;
|
675
|
+
}
|
676
|
+
setFeaturedDispatcher(path, dispatcher) {
|
677
|
+
if (this.router.has("featured")) throw new RouterError("Featured collection dispatcher already set.");
|
678
|
+
const variables = this.router.add(path, "featured");
|
679
|
+
if (variables.size !== 1 || !(variables.has("identifier") || variables.has("handle"))) throw new RouterError("Path for featured collection dispatcher must have one variable: {identifier}");
|
680
|
+
if (variables.has("handle")) (0, __logtape_logtape.getLogger)([
|
681
|
+
"fedify",
|
682
|
+
"federation",
|
683
|
+
"collection"
|
684
|
+
]).warn("The {{handle}} variable in the featured collection dispatcher path is deprecated. Use {{identifier}} instead.");
|
685
|
+
const callbacks = { dispatcher };
|
686
|
+
this.featuredCallbacks = callbacks;
|
687
|
+
const setters = {
|
688
|
+
setCounter(counter) {
|
689
|
+
callbacks.counter = counter;
|
690
|
+
return setters;
|
691
|
+
},
|
692
|
+
setFirstCursor(cursor) {
|
693
|
+
callbacks.firstCursor = cursor;
|
694
|
+
return setters;
|
695
|
+
},
|
696
|
+
setLastCursor(cursor) {
|
697
|
+
callbacks.lastCursor = cursor;
|
698
|
+
return setters;
|
699
|
+
},
|
700
|
+
authorize(predicate) {
|
701
|
+
callbacks.authorizePredicate = predicate;
|
702
|
+
return setters;
|
703
|
+
}
|
704
|
+
};
|
705
|
+
return setters;
|
706
|
+
}
|
707
|
+
setFeaturedTagsDispatcher(path, dispatcher) {
|
708
|
+
if (this.router.has("featuredTags")) throw new RouterError("Featured tags collection dispatcher already set.");
|
709
|
+
const variables = this.router.add(path, "featuredTags");
|
710
|
+
if (variables.size !== 1 || !(variables.has("identifier") || variables.has("handle"))) throw new RouterError("Path for featured tags collection dispatcher must have one variable: {identifier}");
|
711
|
+
if (variables.has("handle")) (0, __logtape_logtape.getLogger)([
|
712
|
+
"fedify",
|
713
|
+
"federation",
|
714
|
+
"collection"
|
715
|
+
]).warn("The {{handle}} variable in the featured tags collection dispatcher path is deprecated. Use {{identifier}} instead.");
|
716
|
+
const callbacks = { dispatcher };
|
717
|
+
this.featuredTagsCallbacks = callbacks;
|
718
|
+
const setters = {
|
719
|
+
setCounter(counter) {
|
720
|
+
callbacks.counter = counter;
|
721
|
+
return setters;
|
722
|
+
},
|
723
|
+
setFirstCursor(cursor) {
|
724
|
+
callbacks.firstCursor = cursor;
|
725
|
+
return setters;
|
726
|
+
},
|
727
|
+
setLastCursor(cursor) {
|
728
|
+
callbacks.lastCursor = cursor;
|
729
|
+
return setters;
|
730
|
+
},
|
731
|
+
authorize(predicate) {
|
732
|
+
callbacks.authorizePredicate = predicate;
|
733
|
+
return setters;
|
734
|
+
}
|
735
|
+
};
|
736
|
+
return setters;
|
737
|
+
}
|
738
|
+
setInboxListeners(inboxPath, sharedInboxPath) {
|
739
|
+
if (this.inboxListeners != null) throw new RouterError("Inbox listeners already set.");
|
740
|
+
if (this.router.has("inbox")) {
|
741
|
+
if (this.inboxPath !== inboxPath) throw new RouterError("Inbox listener path must match inbox dispatcher path.");
|
742
|
+
} else {
|
743
|
+
const variables = this.router.add(inboxPath, "inbox");
|
744
|
+
if (variables.size !== 1 || !(variables.has("identifier") || variables.has("handle"))) throw new RouterError("Path for inbox must have one variable: {identifier}");
|
745
|
+
this.inboxPath = inboxPath;
|
746
|
+
if (variables.has("handle")) (0, __logtape_logtape.getLogger)([
|
747
|
+
"fedify",
|
748
|
+
"federation",
|
749
|
+
"inbox"
|
750
|
+
]).warn("The {{handle}} variable in the inbox path is deprecated. Use {{identifier}} instead.");
|
751
|
+
}
|
752
|
+
if (sharedInboxPath != null) {
|
753
|
+
const siVars = this.router.add(sharedInboxPath, "sharedInbox");
|
754
|
+
if (siVars.size !== 0) throw new RouterError("Path for shared inbox must have no variables.");
|
755
|
+
}
|
756
|
+
const listeners = this.inboxListeners = new InboxListenerSet();
|
757
|
+
const setters = {
|
758
|
+
on(type, listener) {
|
759
|
+
listeners.add(type, listener);
|
760
|
+
return setters;
|
761
|
+
},
|
762
|
+
onError: (handler) => {
|
763
|
+
this.inboxErrorHandler = handler;
|
764
|
+
return setters;
|
765
|
+
},
|
766
|
+
setSharedKeyDispatcher: (dispatcher) => {
|
767
|
+
this.sharedInboxKeyDispatcher = dispatcher;
|
768
|
+
return setters;
|
769
|
+
}
|
770
|
+
};
|
771
|
+
return setters;
|
772
|
+
}
|
773
|
+
setCollectionDispatcher(name, ...args) {
|
774
|
+
return this.#setCustomCollectionDispatcher(name, "collection", ...args);
|
775
|
+
}
|
776
|
+
setOrderedCollectionDispatcher(name, ...args) {
|
777
|
+
return this.#setCustomCollectionDispatcher(name, "orderedCollection", ...args);
|
778
|
+
}
|
779
|
+
#setCustomCollectionDispatcher(name, collectionType, itemType, path, dispatcher) {
|
780
|
+
const strName = String(name);
|
781
|
+
const routeName = `${collectionType}:${this.#uniqueCollectionId(name)}`;
|
782
|
+
if (this.router.has(routeName)) throw new RouterError(`Collection dispatcher for ${strName} already set.`);
|
783
|
+
if (this.collectionCallbacks[name] != null) throw new RouterError(`Collection dispatcher for ${strName} already set.`);
|
784
|
+
const variables = this.router.add(path, routeName);
|
785
|
+
if (variables.size < 1) throw new RouterError("Path for collection dispatcher must have at least one variable.");
|
786
|
+
const callbacks = { dispatcher };
|
787
|
+
this.collectionCallbacks[name] = callbacks;
|
788
|
+
this.collectionTypeIds[name] = itemType;
|
789
|
+
const setters = {
|
790
|
+
setCounter(counter) {
|
791
|
+
callbacks.counter = counter;
|
792
|
+
return setters;
|
793
|
+
},
|
794
|
+
setFirstCursor(cursor) {
|
795
|
+
callbacks.firstCursor = cursor;
|
796
|
+
return setters;
|
797
|
+
},
|
798
|
+
setLastCursor(cursor) {
|
799
|
+
callbacks.lastCursor = cursor;
|
800
|
+
return setters;
|
801
|
+
},
|
802
|
+
authorize(predicate) {
|
803
|
+
callbacks.authorizePredicate = predicate;
|
804
|
+
return setters;
|
805
|
+
}
|
806
|
+
};
|
807
|
+
return setters;
|
808
|
+
}
|
809
|
+
/**
|
810
|
+
* Get the URL path for a custom collection.
|
811
|
+
* If the collection is not registered, returns null.
|
812
|
+
* @template TParam The parameter names of the requested URL.
|
813
|
+
* @param {string | symbol} name The name of the custom collection.
|
814
|
+
* @param {TParam} values The values to fill in the URL parameters.
|
815
|
+
* @returns {string | null} The URL path for the custom collection, or null if not registered.
|
816
|
+
*/
|
817
|
+
getCollectionPath(name, values) {
|
818
|
+
if (!(name in this.collectionCallbacks)) return null;
|
819
|
+
const routeName = this.#uniqueCollectionId(name);
|
820
|
+
const path = this.router.build(`collection:${routeName}`, values) ?? this.router.build(`orderedCollection:${routeName}`, values);
|
821
|
+
return path;
|
822
|
+
}
|
823
|
+
/**
|
824
|
+
* Converts a name (string or symbol) to a unique string identifier.
|
825
|
+
* For symbols, generates and caches a UUID if not already present.
|
826
|
+
* For strings, returns the string as-is.
|
827
|
+
* @param name The name to convert to a unique identifier
|
828
|
+
* @returns A unique string identifier
|
829
|
+
*/
|
830
|
+
#uniqueCollectionId(name) {
|
831
|
+
if (typeof name === "string") return name;
|
832
|
+
if (!this.#symbolRegistry.has(name)) this.#symbolRegistry.set(name, crypto.randomUUID());
|
833
|
+
return this.#symbolRegistry.get(name);
|
834
|
+
}
|
835
|
+
};
|
836
|
+
/**
|
837
|
+
* Creates a new {@link FederationBuilder} instance.
|
838
|
+
* @returns A new {@link FederationBuilder} instance.
|
839
|
+
* @since 1.6.0
|
840
|
+
*/
|
841
|
+
function createFederationBuilder() {
|
842
|
+
return new FederationBuilderImpl();
|
843
|
+
}
|
844
|
+
|
845
|
+
//#endregion
|
846
|
+
//#region src/federation/collection.ts
|
847
|
+
/**
|
848
|
+
* Calculates the [partial follower collection digest][1].
|
849
|
+
*
|
850
|
+
* [1]: https://w3id.org/fep/8fcf#partial-follower-collection-digest
|
851
|
+
* @param uris The URIs to calculate the digest. Duplicate URIs are ignored.
|
852
|
+
* @returns The digest.
|
853
|
+
*/
|
854
|
+
async function digest(uris) {
|
855
|
+
const processed = /* @__PURE__ */ new Set();
|
856
|
+
const encoder = new TextEncoder();
|
857
|
+
const result = new Uint8Array(32);
|
858
|
+
for (const uri of uris) {
|
859
|
+
const u = uri instanceof URL ? uri.href : uri;
|
860
|
+
if (processed.has(u)) continue;
|
861
|
+
processed.add(u);
|
862
|
+
const encoded = encoder.encode(u);
|
863
|
+
const digest$1 = new Uint8Array(await crypto.subtle.digest("SHA-256", encoded));
|
864
|
+
for (let i = 0; i < 32; i++) result[i] ^= digest$1[i];
|
865
|
+
}
|
866
|
+
return result;
|
867
|
+
}
|
868
|
+
/**
|
869
|
+
* Builds [`Collection-Synchronization`][1] header content.
|
870
|
+
*
|
871
|
+
* [1]: https://w3id.org/fep/8fcf#the-collection-synchronization-http-header
|
872
|
+
*
|
873
|
+
* @param collectionId The sender's followers collection URI.
|
874
|
+
* @param actorIds The actor URIs to digest.
|
875
|
+
* @returns The header content.
|
876
|
+
*/
|
877
|
+
async function buildCollectionSynchronizationHeader(collectionId, actorIds) {
|
878
|
+
const [anyActorId] = actorIds;
|
879
|
+
const baseUrl = new URL(anyActorId);
|
880
|
+
const url = new URL(collectionId);
|
881
|
+
url.searchParams.set("base-url", `${baseUrl.origin}/`);
|
882
|
+
const hash = (0, byte_encodings_hex.encodeHex)(await digest(actorIds));
|
883
|
+
return `collectionId="${collectionId}", url="${url}", digest="${hash}"`;
|
884
|
+
}
|
885
|
+
|
886
|
+
//#endregion
|
887
|
+
//#region src/federation/keycache.ts
|
888
|
+
var KvKeyCache = class {
|
889
|
+
kv;
|
890
|
+
prefix;
|
891
|
+
options;
|
892
|
+
nullKeys;
|
893
|
+
constructor(kv, prefix, options = {}) {
|
894
|
+
this.kv = kv;
|
895
|
+
this.prefix = prefix;
|
896
|
+
this.nullKeys = /* @__PURE__ */ new Set();
|
897
|
+
this.options = options;
|
898
|
+
}
|
899
|
+
async get(keyId) {
|
900
|
+
if (this.nullKeys.has(keyId.href)) return null;
|
901
|
+
const serialized = await this.kv.get([...this.prefix, keyId.href]);
|
902
|
+
if (serialized == null) return void 0;
|
903
|
+
try {
|
904
|
+
return await require_actor.CryptographicKey.fromJsonLd(serialized, this.options);
|
905
|
+
} catch {
|
906
|
+
try {
|
907
|
+
return await require_actor.Multikey.fromJsonLd(serialized, this.options);
|
908
|
+
} catch {
|
909
|
+
await this.kv.delete([...this.prefix, keyId.href]);
|
910
|
+
return void 0;
|
911
|
+
}
|
912
|
+
}
|
913
|
+
}
|
914
|
+
async set(keyId, key) {
|
915
|
+
if (key == null) {
|
916
|
+
this.nullKeys.add(keyId.href);
|
917
|
+
await this.kv.delete([...this.prefix, keyId.href]);
|
918
|
+
return;
|
919
|
+
}
|
920
|
+
this.nullKeys.delete(keyId.href);
|
921
|
+
const serialized = await key.toJsonLd(this.options);
|
922
|
+
await this.kv.set([...this.prefix, keyId.href], serialized);
|
923
|
+
}
|
924
|
+
};
|
925
|
+
|
926
|
+
//#endregion
|
927
|
+
//#region src/federation/negotiation.ts
|
928
|
+
function compareSpecs(a, b) {
|
929
|
+
return b.q - a.q || (b.s ?? 0) - (a.s ?? 0) || (a.o ?? 0) - (b.o ?? 0) || a.i - b.i || 0;
|
930
|
+
}
|
931
|
+
function isQuality(spec) {
|
932
|
+
return spec.q > 0;
|
933
|
+
}
|
934
|
+
const simpleMediaTypeRegExp = /^\s*([^\s\/;]+)\/([^;\s]+)\s*(?:;(.*))?$/;
|
935
|
+
function splitKeyValuePair(str) {
|
936
|
+
const [key, value] = str.split("=");
|
937
|
+
return [key.toLowerCase(), value];
|
938
|
+
}
|
939
|
+
function parseMediaType(str, i) {
|
940
|
+
const match = simpleMediaTypeRegExp.exec(str);
|
941
|
+
if (!match) return;
|
942
|
+
const [, type, subtype, parameters] = match;
|
943
|
+
if (!type || !subtype) return;
|
944
|
+
const params = Object.create(null);
|
945
|
+
let q = 1;
|
946
|
+
if (parameters) {
|
947
|
+
const kvps = parameters.split(";").map((p) => p.trim()).map(splitKeyValuePair);
|
948
|
+
for (const [key, val] of kvps) {
|
949
|
+
const value = val && val[0] === `"` && val[val.length - 1] === `"` ? val.slice(1, val.length - 1) : val;
|
950
|
+
if (key === "q" && value) {
|
951
|
+
q = parseFloat(value);
|
952
|
+
break;
|
953
|
+
}
|
954
|
+
params[key] = value;
|
955
|
+
}
|
956
|
+
}
|
957
|
+
return {
|
958
|
+
type,
|
959
|
+
subtype,
|
960
|
+
params,
|
961
|
+
i,
|
962
|
+
o: void 0,
|
963
|
+
q,
|
964
|
+
s: void 0
|
965
|
+
};
|
966
|
+
}
|
967
|
+
function parseAccept(accept) {
|
968
|
+
const accepts = accept.split(",").map((p) => p.trim());
|
969
|
+
const mediaTypes = [];
|
970
|
+
for (const [index, accept$1] of accepts.entries()) {
|
971
|
+
const mediaType = parseMediaType(accept$1.trim(), index);
|
972
|
+
if (mediaType) mediaTypes.push(mediaType);
|
973
|
+
}
|
974
|
+
return mediaTypes;
|
975
|
+
}
|
976
|
+
function getFullType(spec) {
|
977
|
+
return `${spec.type}/${spec.subtype}`;
|
978
|
+
}
|
979
|
+
function preferredMediaTypes(accept) {
|
980
|
+
const accepts = parseAccept(accept === void 0 ? "*/*" : accept ?? "");
|
981
|
+
return accepts.filter(isQuality).sort(compareSpecs).map(getFullType);
|
982
|
+
}
|
983
|
+
|
984
|
+
//#endregion
|
985
|
+
//#region src/federation/handler.ts
|
986
|
+
function acceptsJsonLd(request) {
|
987
|
+
const accept = request.headers.get("Accept");
|
988
|
+
const types = accept ? preferredMediaTypes(accept) : ["*/*"];
|
989
|
+
if (types == null) return true;
|
990
|
+
if (types[0] === "text/html" || types[0] === "application/xhtml+xml") return false;
|
991
|
+
return types.includes("application/activity+json") || types.includes("application/ld+json") || types.includes("application/json");
|
992
|
+
}
|
993
|
+
/**
|
994
|
+
* Handles an actor request.
|
995
|
+
* @template TContextData The context data to pass to the context.
|
996
|
+
* @param request The HTTP request.
|
997
|
+
* @param parameters The parameters for handling the actor.
|
998
|
+
* @returns A promise that resolves to an HTTP response.
|
999
|
+
*/
|
1000
|
+
async function handleActor(request, { identifier, context: context$2, actorDispatcher, authorizePredicate, onNotFound, onNotAcceptable, onUnauthorized }) {
|
1001
|
+
const logger$1 = (0, __logtape_logtape.getLogger)([
|
1002
|
+
"fedify",
|
1003
|
+
"federation",
|
1004
|
+
"actor"
|
1005
|
+
]);
|
1006
|
+
if (actorDispatcher == null) {
|
1007
|
+
logger$1.debug("Actor dispatcher is not set.", { identifier });
|
1008
|
+
return await onNotFound(request);
|
1009
|
+
}
|
1010
|
+
const actor = await actorDispatcher(context$2, identifier);
|
1011
|
+
if (actor == null) {
|
1012
|
+
logger$1.debug("Actor {identifier} not found.", { identifier });
|
1013
|
+
return await onNotFound(request);
|
1014
|
+
}
|
1015
|
+
if (!acceptsJsonLd(request)) return await onNotAcceptable(request);
|
1016
|
+
if (authorizePredicate != null) {
|
1017
|
+
let key = await context$2.getSignedKey();
|
1018
|
+
key = key?.clone({}, { $warning: {
|
1019
|
+
category: [
|
1020
|
+
"fedify",
|
1021
|
+
"federation",
|
1022
|
+
"actor"
|
1023
|
+
],
|
1024
|
+
message: "The third parameter of AuthorizePredicate is deprecated in favor of RequestContext.getSignedKey() method. The third parameter will be removed in a future release."
|
1025
|
+
} }) ?? null;
|
1026
|
+
let keyOwner = await context$2.getSignedKeyOwner();
|
1027
|
+
keyOwner = keyOwner?.clone({}, { $warning: {
|
1028
|
+
category: [
|
1029
|
+
"fedify",
|
1030
|
+
"federation",
|
1031
|
+
"actor"
|
1032
|
+
],
|
1033
|
+
message: "The fourth parameter of AuthorizePredicate is deprecated in favor of RequestContext.getSignedKeyOwner() method. The fourth parameter will be removed in a future release."
|
1034
|
+
} }) ?? null;
|
1035
|
+
if (!await authorizePredicate(context$2, identifier, key, keyOwner)) return await onUnauthorized(request);
|
1036
|
+
}
|
1037
|
+
const jsonLd = await actor.toJsonLd(context$2);
|
1038
|
+
return new Response(JSON.stringify(jsonLd), { headers: {
|
1039
|
+
"Content-Type": "application/activity+json",
|
1040
|
+
Vary: "Accept"
|
1041
|
+
} });
|
1042
|
+
}
|
1043
|
+
/**
|
1044
|
+
* Handles an object request.
|
1045
|
+
* @template TContextData The context data to pass to the context.
|
1046
|
+
* @param request The HTTP request.
|
1047
|
+
* @param parameters The parameters for handling the object.
|
1048
|
+
* @returns A promise that resolves to an HTTP response.
|
1049
|
+
*/
|
1050
|
+
async function handleObject(request, { values, context: context$2, objectDispatcher, authorizePredicate, onNotFound, onNotAcceptable, onUnauthorized }) {
|
1051
|
+
if (objectDispatcher == null) return await onNotFound(request);
|
1052
|
+
const object = await objectDispatcher(context$2, values);
|
1053
|
+
if (object == null) return await onNotFound(request);
|
1054
|
+
if (!acceptsJsonLd(request)) return await onNotAcceptable(request);
|
1055
|
+
if (authorizePredicate != null) {
|
1056
|
+
let key = await context$2.getSignedKey();
|
1057
|
+
key = key?.clone({}, { $warning: {
|
1058
|
+
category: [
|
1059
|
+
"fedify",
|
1060
|
+
"federation",
|
1061
|
+
"object"
|
1062
|
+
],
|
1063
|
+
message: "The third parameter of ObjectAuthorizePredicate is deprecated in favor of RequestContext.getSignedKey() method. The third parameter will be removed in a future release."
|
1064
|
+
} }) ?? null;
|
1065
|
+
let keyOwner = await context$2.getSignedKeyOwner();
|
1066
|
+
keyOwner = keyOwner?.clone({}, { $warning: {
|
1067
|
+
category: [
|
1068
|
+
"fedify",
|
1069
|
+
"federation",
|
1070
|
+
"object"
|
1071
|
+
],
|
1072
|
+
message: "The fourth parameter of ObjectAuthorizePredicate is deprecated in favor of RequestContext.getSignedKeyOwner() method. The fourth parameter will be removed in a future release."
|
1073
|
+
} }) ?? null;
|
1074
|
+
if (!await authorizePredicate(context$2, values, key, keyOwner)) return await onUnauthorized(request);
|
1075
|
+
}
|
1076
|
+
const jsonLd = await object.toJsonLd(context$2);
|
1077
|
+
return new Response(JSON.stringify(jsonLd), { headers: {
|
1078
|
+
"Content-Type": "application/activity+json",
|
1079
|
+
Vary: "Accept"
|
1080
|
+
} });
|
1081
|
+
}
|
1082
|
+
/**
|
1083
|
+
* Handles a collection request.
|
1084
|
+
* @template TItem The type of items in the collection.
|
1085
|
+
* @template TContext The type of the context, extending {@link RequestContext}.
|
1086
|
+
* @template TContextData The context data to pass to the `TContext`.
|
1087
|
+
* @template TFilter The type of the filter.
|
1088
|
+
* @param request The HTTP request.
|
1089
|
+
* @param parameters The parameters for handling the collection.
|
1090
|
+
* @returns A promise that resolves to an HTTP response.
|
1091
|
+
*/
|
1092
|
+
async function handleCollection(request, { name, identifier, uriGetter, filter, filterPredicate, context: context$2, collectionCallbacks, tracerProvider, onUnauthorized, onNotFound, onNotAcceptable }) {
|
1093
|
+
const spanName = name.trim().replace(/\s+/g, "_");
|
1094
|
+
tracerProvider = tracerProvider ?? __opentelemetry_api.trace.getTracerProvider();
|
1095
|
+
const tracer = tracerProvider.getTracer(require_docloader.deno_default.name, require_docloader.deno_default.version);
|
1096
|
+
const url = new URL(request.url);
|
1097
|
+
const cursor = url.searchParams.get("cursor");
|
1098
|
+
if (collectionCallbacks == null) return await onNotFound(request);
|
1099
|
+
let collection;
|
1100
|
+
const baseUri = uriGetter(identifier);
|
1101
|
+
if (cursor == null) {
|
1102
|
+
const firstCursor = await collectionCallbacks.firstCursor?.(context$2, identifier);
|
1103
|
+
const totalItems = filter == null ? await collectionCallbacks.counter?.(context$2, identifier) : void 0;
|
1104
|
+
if (firstCursor == null) {
|
1105
|
+
const itemsOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection ${spanName}`, {
|
1106
|
+
kind: __opentelemetry_api.SpanKind.SERVER,
|
1107
|
+
attributes: {
|
1108
|
+
"activitypub.collection.id": baseUri.href,
|
1109
|
+
"activitypub.collection.type": require_actor.OrderedCollection.typeId.href
|
1110
|
+
}
|
1111
|
+
}, async (span) => {
|
1112
|
+
if (totalItems != null) span.setAttribute("activitypub.collection.total_items", Number(totalItems));
|
1113
|
+
try {
|
1114
|
+
const page = await collectionCallbacks.dispatcher(context$2, identifier, null, filter);
|
1115
|
+
if (page == null) {
|
1116
|
+
span.setStatus({ code: __opentelemetry_api.SpanStatusCode.ERROR });
|
1117
|
+
return await onNotFound(request);
|
1118
|
+
}
|
1119
|
+
const { items } = page;
|
1120
|
+
span.setAttribute("fedify.collection.items", items.length);
|
1121
|
+
return items;
|
1122
|
+
} catch (e) {
|
1123
|
+
span.setStatus({
|
1124
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
1125
|
+
message: String(e)
|
1126
|
+
});
|
1127
|
+
throw e;
|
1128
|
+
} finally {
|
1129
|
+
span.end();
|
1130
|
+
}
|
1131
|
+
});
|
1132
|
+
if (itemsOrResponse instanceof Response) return itemsOrResponse;
|
1133
|
+
collection = new require_actor.OrderedCollection({
|
1134
|
+
id: baseUri,
|
1135
|
+
totalItems: totalItems == null ? null : Number(totalItems),
|
1136
|
+
items: filterCollectionItems(itemsOrResponse, name, filterPredicate)
|
1137
|
+
});
|
1138
|
+
} else {
|
1139
|
+
const lastCursor = await collectionCallbacks.lastCursor?.(context$2, identifier);
|
1140
|
+
const first = new URL(context$2.url);
|
1141
|
+
first.searchParams.set("cursor", firstCursor);
|
1142
|
+
let last = null;
|
1143
|
+
if (lastCursor != null) {
|
1144
|
+
last = new URL(context$2.url);
|
1145
|
+
last.searchParams.set("cursor", lastCursor);
|
1146
|
+
}
|
1147
|
+
collection = new require_actor.OrderedCollection({
|
1148
|
+
id: baseUri,
|
1149
|
+
totalItems: totalItems == null ? null : Number(totalItems),
|
1150
|
+
first,
|
1151
|
+
last
|
1152
|
+
});
|
1153
|
+
}
|
1154
|
+
} else {
|
1155
|
+
const uri = new URL(baseUri);
|
1156
|
+
uri.searchParams.set("cursor", cursor);
|
1157
|
+
const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name}`, {
|
1158
|
+
kind: __opentelemetry_api.SpanKind.SERVER,
|
1159
|
+
attributes: {
|
1160
|
+
"activitypub.collection.id": uri.href,
|
1161
|
+
"activitypub.collection.type": require_actor.OrderedCollectionPage.typeId.href,
|
1162
|
+
"fedify.collection.cursor": cursor
|
1163
|
+
}
|
1164
|
+
}, async (span) => {
|
1165
|
+
try {
|
1166
|
+
const page = await collectionCallbacks.dispatcher(context$2, identifier, cursor, filter);
|
1167
|
+
if (page == null) {
|
1168
|
+
span.setStatus({ code: __opentelemetry_api.SpanStatusCode.ERROR });
|
1169
|
+
return await onNotFound(request);
|
1170
|
+
}
|
1171
|
+
span.setAttribute("fedify.collection.items", page.items.length);
|
1172
|
+
return page;
|
1173
|
+
} catch (e) {
|
1174
|
+
span.setStatus({
|
1175
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
1176
|
+
message: String(e)
|
1177
|
+
});
|
1178
|
+
throw e;
|
1179
|
+
} finally {
|
1180
|
+
span.end();
|
1181
|
+
}
|
1182
|
+
});
|
1183
|
+
if (pageOrResponse instanceof Response) return pageOrResponse;
|
1184
|
+
const { items, prevCursor, nextCursor } = pageOrResponse;
|
1185
|
+
let prev = null;
|
1186
|
+
if (prevCursor != null) {
|
1187
|
+
prev = new URL(context$2.url);
|
1188
|
+
prev.searchParams.set("cursor", prevCursor);
|
1189
|
+
}
|
1190
|
+
let next = null;
|
1191
|
+
if (nextCursor != null) {
|
1192
|
+
next = new URL(context$2.url);
|
1193
|
+
next.searchParams.set("cursor", nextCursor);
|
1194
|
+
}
|
1195
|
+
const partOf = new URL(context$2.url);
|
1196
|
+
partOf.searchParams.delete("cursor");
|
1197
|
+
collection = new require_actor.OrderedCollectionPage({
|
1198
|
+
id: uri,
|
1199
|
+
prev,
|
1200
|
+
next,
|
1201
|
+
items: filterCollectionItems(items, name, filterPredicate),
|
1202
|
+
partOf
|
1203
|
+
});
|
1204
|
+
}
|
1205
|
+
if (!acceptsJsonLd(request)) return await onNotAcceptable(request);
|
1206
|
+
if (collectionCallbacks.authorizePredicate != null) {
|
1207
|
+
let key = await context$2.getSignedKey();
|
1208
|
+
key = key?.clone({}, { $warning: {
|
1209
|
+
category: [
|
1210
|
+
"fedify",
|
1211
|
+
"federation",
|
1212
|
+
"collection"
|
1213
|
+
],
|
1214
|
+
message: "The third parameter of AuthorizePredicate is deprecated in favor of RequestContext.getSignedKey() method. The third parameter will be removed in a future release."
|
1215
|
+
} }) ?? null;
|
1216
|
+
let keyOwner = await context$2.getSignedKeyOwner();
|
1217
|
+
keyOwner = keyOwner?.clone({}, { $warning: {
|
1218
|
+
category: [
|
1219
|
+
"fedify",
|
1220
|
+
"federation",
|
1221
|
+
"collection"
|
1222
|
+
],
|
1223
|
+
message: "The fourth parameter of AuthorizePredicate is deprecated in favor of RequestContext.getSignedKeyOwner() method. The fourth parameter will be removed in a future release."
|
1224
|
+
} }) ?? null;
|
1225
|
+
if (!await collectionCallbacks.authorizePredicate(context$2, identifier, key, keyOwner)) return await onUnauthorized(request);
|
1226
|
+
}
|
1227
|
+
const jsonLd = await collection.toJsonLd(context$2);
|
1228
|
+
return new Response(JSON.stringify(jsonLd), { headers: {
|
1229
|
+
"Content-Type": "application/activity+json",
|
1230
|
+
Vary: "Accept"
|
1231
|
+
} });
|
1232
|
+
}
|
1233
|
+
/**
|
1234
|
+
* Filters collection items based on the provided predicate.
|
1235
|
+
* @template TItem The type of items to filter.
|
1236
|
+
* @param items The items to filter.
|
1237
|
+
* @param collectionName The name of the collection for logging purposes.
|
1238
|
+
* @param filterPredicate Optional predicate function to filter items.
|
1239
|
+
* @returns The filtered items as Objects, Links, or URLs.
|
1240
|
+
*/
|
1241
|
+
function filterCollectionItems(items, collectionName, filterPredicate) {
|
1242
|
+
const result = [];
|
1243
|
+
let logged = false;
|
1244
|
+
for (const item of items) {
|
1245
|
+
let mappedItem;
|
1246
|
+
if (item instanceof require_actor.Object || item instanceof require_actor.Link || item instanceof URL) mappedItem = item;
|
1247
|
+
else if (item.id == null) continue;
|
1248
|
+
else mappedItem = item.id;
|
1249
|
+
if (filterPredicate != null && !filterPredicate(item)) {
|
1250
|
+
if (!logged) {
|
1251
|
+
(0, __logtape_logtape.getLogger)([
|
1252
|
+
"fedify",
|
1253
|
+
"federation",
|
1254
|
+
"collection"
|
1255
|
+
]).warn(`The ${collectionName} collection apparently does not implement filtering. This may result in a large response payload. Please consider implementing filtering for the collection. See also: https://fedify.dev/manual/collections#filtering-by-server`);
|
1256
|
+
logged = true;
|
1257
|
+
}
|
1258
|
+
continue;
|
1259
|
+
}
|
1260
|
+
result.push(mappedItem);
|
1261
|
+
}
|
1262
|
+
return result;
|
1263
|
+
}
|
1264
|
+
/**
|
1265
|
+
* Handles an inbox request for ActivityPub activities.
|
1266
|
+
* @template TContextData The context data to pass to the context.
|
1267
|
+
* @param request The HTTP request.
|
1268
|
+
* @param options The parameters for handling the inbox.
|
1269
|
+
* @returns A promise that resolves to an HTTP response.
|
1270
|
+
*/
|
1271
|
+
async function handleInbox(request, options) {
|
1272
|
+
const tracerProvider = options.tracerProvider ?? __opentelemetry_api.trace.getTracerProvider();
|
1273
|
+
const tracer = tracerProvider.getTracer(require_docloader.deno_default.name, require_docloader.deno_default.version);
|
1274
|
+
return await tracer.startActiveSpan("activitypub.inbox", {
|
1275
|
+
kind: options.queue == null ? __opentelemetry_api.SpanKind.SERVER : __opentelemetry_api.SpanKind.PRODUCER,
|
1276
|
+
attributes: { "activitypub.shared_inbox": options.recipient == null }
|
1277
|
+
}, async (span) => {
|
1278
|
+
if (options.recipient != null) span.setAttribute("fedify.inbox.recipient", options.recipient);
|
1279
|
+
try {
|
1280
|
+
return await handleInboxInternal(request, options, span);
|
1281
|
+
} catch (e) {
|
1282
|
+
span.setStatus({
|
1283
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
1284
|
+
message: String(e)
|
1285
|
+
});
|
1286
|
+
throw e;
|
1287
|
+
} finally {
|
1288
|
+
span.end();
|
1289
|
+
}
|
1290
|
+
});
|
1291
|
+
}
|
1292
|
+
/**
|
1293
|
+
* Internal function for handling inbox requests with detailed processing.
|
1294
|
+
* @template TContextData The context data to pass to the context.
|
1295
|
+
* @param request The HTTP request.
|
1296
|
+
* @param options The parameters for handling the inbox.
|
1297
|
+
* @param span The OpenTelemetry span for tracing.
|
1298
|
+
* @returns A promise that resolves to an HTTP response.
|
1299
|
+
*/
|
1300
|
+
async function handleInboxInternal(request, { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, queue, actorDispatcher, inboxListeners, inboxErrorHandler, onNotFound, signatureTimeWindow, skipSignatureVerification, tracerProvider }, span) {
|
1301
|
+
const logger$1 = (0, __logtape_logtape.getLogger)([
|
1302
|
+
"fedify",
|
1303
|
+
"federation",
|
1304
|
+
"inbox"
|
1305
|
+
]);
|
1306
|
+
if (actorDispatcher == null) {
|
1307
|
+
logger$1.error("Actor dispatcher is not set.", { recipient });
|
1308
|
+
span.setStatus({
|
1309
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
1310
|
+
message: "Actor dispatcher is not set."
|
1311
|
+
});
|
1312
|
+
return await onNotFound(request);
|
1313
|
+
} else if (recipient != null) {
|
1314
|
+
const actor = await actorDispatcher(ctx, recipient);
|
1315
|
+
if (actor == null) {
|
1316
|
+
logger$1.error("Actor {recipient} not found.", { recipient });
|
1317
|
+
span.setStatus({
|
1318
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
1319
|
+
message: `Actor ${recipient} not found.`
|
1320
|
+
});
|
1321
|
+
return await onNotFound(request);
|
1322
|
+
}
|
1323
|
+
}
|
1324
|
+
if (request.bodyUsed) {
|
1325
|
+
logger$1.error("Request body has already been read.", { recipient });
|
1326
|
+
span.setStatus({
|
1327
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
1328
|
+
message: "Request body has already been read."
|
1329
|
+
});
|
1330
|
+
return new Response("Internal server error.", {
|
1331
|
+
status: 500,
|
1332
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
1333
|
+
});
|
1334
|
+
} else if (request.body?.locked) {
|
1335
|
+
logger$1.error("Request body is locked.", { recipient });
|
1336
|
+
span.setStatus({
|
1337
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
1338
|
+
message: "Request body is locked."
|
1339
|
+
});
|
1340
|
+
return new Response("Internal server error.", {
|
1341
|
+
status: 500,
|
1342
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
1343
|
+
});
|
1344
|
+
}
|
1345
|
+
let json;
|
1346
|
+
try {
|
1347
|
+
json = await request.clone().json();
|
1348
|
+
} catch (error) {
|
1349
|
+
logger$1.error("Failed to parse JSON:\n{error}", {
|
1350
|
+
recipient,
|
1351
|
+
error
|
1352
|
+
});
|
1353
|
+
try {
|
1354
|
+
await inboxErrorHandler?.(ctx, error);
|
1355
|
+
} catch (error$1) {
|
1356
|
+
logger$1.error("An unexpected error occurred in inbox error handler:\n{error}", {
|
1357
|
+
error: error$1,
|
1358
|
+
activity: json,
|
1359
|
+
recipient
|
1360
|
+
});
|
1361
|
+
}
|
1362
|
+
span.setStatus({
|
1363
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
1364
|
+
message: `Failed to parse JSON:\n${error}`
|
1365
|
+
});
|
1366
|
+
return new Response("Invalid JSON.", {
|
1367
|
+
status: 400,
|
1368
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
1369
|
+
});
|
1370
|
+
}
|
1371
|
+
const keyCache = new KvKeyCache(kv, kvPrefixes.publicKey, ctx);
|
1372
|
+
let ldSigVerified;
|
1373
|
+
try {
|
1374
|
+
ldSigVerified = await require_proof.verifyJsonLd(json, {
|
1375
|
+
contextLoader: ctx.contextLoader,
|
1376
|
+
documentLoader: ctx.documentLoader,
|
1377
|
+
keyCache,
|
1378
|
+
tracerProvider
|
1379
|
+
});
|
1380
|
+
} catch (error) {
|
1381
|
+
if (error instanceof Error && error.name === "jsonld.SyntaxError") {
|
1382
|
+
logger$1.error("Failed to parse JSON-LD:\n{error}", {
|
1383
|
+
recipient,
|
1384
|
+
error
|
1385
|
+
});
|
1386
|
+
return new Response("Invalid JSON-LD.", {
|
1387
|
+
status: 400,
|
1388
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
1389
|
+
});
|
1390
|
+
}
|
1391
|
+
ldSigVerified = false;
|
1392
|
+
}
|
1393
|
+
const jsonWithoutSig = require_proof.detachSignature(json);
|
1394
|
+
let activity = null;
|
1395
|
+
if (ldSigVerified) {
|
1396
|
+
logger$1.debug("Linked Data Signatures are verified.", {
|
1397
|
+
recipient,
|
1398
|
+
json
|
1399
|
+
});
|
1400
|
+
activity = await require_actor.Activity.fromJsonLd(jsonWithoutSig, ctx);
|
1401
|
+
} else {
|
1402
|
+
logger$1.debug("Linked Data Signatures are not verified.", {
|
1403
|
+
recipient,
|
1404
|
+
json
|
1405
|
+
});
|
1406
|
+
try {
|
1407
|
+
activity = await require_proof.verifyObject(require_actor.Activity, jsonWithoutSig, {
|
1408
|
+
contextLoader: ctx.contextLoader,
|
1409
|
+
documentLoader: ctx.documentLoader,
|
1410
|
+
keyCache,
|
1411
|
+
tracerProvider
|
1412
|
+
});
|
1413
|
+
} catch (error) {
|
1414
|
+
logger$1.error("Failed to parse activity:\n{error}", {
|
1415
|
+
recipient,
|
1416
|
+
activity: json,
|
1417
|
+
error
|
1418
|
+
});
|
1419
|
+
try {
|
1420
|
+
await inboxErrorHandler?.(ctx, error);
|
1421
|
+
} catch (error$1) {
|
1422
|
+
logger$1.error("An unexpected error occurred in inbox error handler:\n{error}", {
|
1423
|
+
error: error$1,
|
1424
|
+
activity: json,
|
1425
|
+
recipient
|
1426
|
+
});
|
1427
|
+
}
|
1428
|
+
span.setStatus({
|
1429
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
1430
|
+
message: `Failed to parse activity:\n${error}`
|
1431
|
+
});
|
1432
|
+
return new Response("Invalid activity.", {
|
1433
|
+
status: 400,
|
1434
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
1435
|
+
});
|
1436
|
+
}
|
1437
|
+
if (activity == null) logger$1.debug("Object Integrity Proofs are not verified.", {
|
1438
|
+
recipient,
|
1439
|
+
activity: json
|
1440
|
+
});
|
1441
|
+
else logger$1.debug("Object Integrity Proofs are verified.", {
|
1442
|
+
recipient,
|
1443
|
+
activity: json
|
1444
|
+
});
|
1445
|
+
}
|
1446
|
+
let httpSigKey = null;
|
1447
|
+
if (activity == null) {
|
1448
|
+
if (!skipSignatureVerification) {
|
1449
|
+
const key = await require_http.verifyRequest(request, {
|
1450
|
+
contextLoader: ctx.contextLoader,
|
1451
|
+
documentLoader: ctx.documentLoader,
|
1452
|
+
timeWindow: signatureTimeWindow,
|
1453
|
+
keyCache,
|
1454
|
+
tracerProvider
|
1455
|
+
});
|
1456
|
+
if (key == null) {
|
1457
|
+
logger$1.error("Failed to verify the request's HTTP Signatures.", { recipient });
|
1458
|
+
span.setStatus({
|
1459
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
1460
|
+
message: `Failed to verify the request's HTTP Signatures.`
|
1461
|
+
});
|
1462
|
+
const response = new Response("Failed to verify the request signature.", {
|
1463
|
+
status: 401,
|
1464
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
1465
|
+
});
|
1466
|
+
return response;
|
1467
|
+
} else logger$1.debug("HTTP Signatures are verified.", { recipient });
|
1468
|
+
httpSigKey = key;
|
1469
|
+
}
|
1470
|
+
activity = await require_actor.Activity.fromJsonLd(jsonWithoutSig, ctx);
|
1471
|
+
}
|
1472
|
+
if (activity.id != null) span.setAttribute("activitypub.activity.id", activity.id.href);
|
1473
|
+
span.setAttribute("activitypub.activity.type", require_actor.getTypeId(activity).href);
|
1474
|
+
if (httpSigKey != null && !await require_proof.doesActorOwnKey(activity, httpSigKey, ctx)) {
|
1475
|
+
logger$1.error("The signer ({keyId}) and the actor ({actorId}) do not match.", {
|
1476
|
+
activity: json,
|
1477
|
+
recipient,
|
1478
|
+
keyId: httpSigKey.id?.href,
|
1479
|
+
actorId: activity.actorId?.href
|
1480
|
+
});
|
1481
|
+
span.setStatus({
|
1482
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
1483
|
+
message: `The signer (${httpSigKey.id?.href}) and the actor (${activity.actorId?.href}) do not match.`
|
1484
|
+
});
|
1485
|
+
return new Response("The signer and the actor do not match.", {
|
1486
|
+
status: 401,
|
1487
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
1488
|
+
});
|
1489
|
+
}
|
1490
|
+
const routeResult = await routeActivity({
|
1491
|
+
context: ctx,
|
1492
|
+
json,
|
1493
|
+
activity,
|
1494
|
+
recipient,
|
1495
|
+
inboxListeners,
|
1496
|
+
inboxContextFactory,
|
1497
|
+
inboxErrorHandler,
|
1498
|
+
kv,
|
1499
|
+
kvPrefixes,
|
1500
|
+
queue,
|
1501
|
+
span,
|
1502
|
+
tracerProvider
|
1503
|
+
});
|
1504
|
+
if (routeResult === "alreadyProcessed") return new Response(`Activity <${activity.id}> has already been processed.`, {
|
1505
|
+
status: 202,
|
1506
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
1507
|
+
});
|
1508
|
+
else if (routeResult === "missingActor") return new Response("Missing actor.", {
|
1509
|
+
status: 400,
|
1510
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
1511
|
+
});
|
1512
|
+
else if (routeResult === "enqueued") return new Response("Activity is enqueued.", {
|
1513
|
+
status: 202,
|
1514
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
1515
|
+
});
|
1516
|
+
else if (routeResult === "unsupportedActivity") return new Response("", {
|
1517
|
+
status: 202,
|
1518
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
1519
|
+
});
|
1520
|
+
else if (routeResult === "error") return new Response("Internal server error.", {
|
1521
|
+
status: 500,
|
1522
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
1523
|
+
});
|
1524
|
+
else return new Response("", {
|
1525
|
+
status: 202,
|
1526
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
1527
|
+
});
|
1528
|
+
}
|
1529
|
+
/**
|
1530
|
+
* Handles a custom collection request.
|
1531
|
+
* @template TItem The type of items in the collection.
|
1532
|
+
* @template TParams The parameter names of the requested URL.
|
1533
|
+
* @template TContext The type of the context, extending {@link RequestContext}.
|
1534
|
+
* @template TContextData The context data to pass to the `TContext`.
|
1535
|
+
* @param request The HTTP request.
|
1536
|
+
* @param handleParams Parameters for handling the collection.
|
1537
|
+
* @returns A promise that resolves to an HTTP response.
|
1538
|
+
* @since 1.8.0
|
1539
|
+
*/
|
1540
|
+
const handleCustomCollection = exceptWrapper(_handleCustomCollection);
|
1541
|
+
async function _handleCustomCollection(request, { name, values, context: context$2, tracerProvider, collectionCallbacks: callbacks, filterPredicate }) {
|
1542
|
+
verifyDefined(callbacks);
|
1543
|
+
verifyJsonLdRequest(request);
|
1544
|
+
await authIfNeeded(context$2, values, callbacks);
|
1545
|
+
const cursor = new URL(request.url).searchParams.get("cursor");
|
1546
|
+
return await new CustomCollectionHandler(name, values, context$2, callbacks, tracerProvider, require_actor.Collection, require_actor.CollectionPage, filterPredicate).fetchCollection(cursor).toJsonLd().then(respondAsActivity);
|
1547
|
+
}
|
1548
|
+
/**
|
1549
|
+
* Handles an ordered collection request.
|
1550
|
+
* @template TItem The type of items in the collection.
|
1551
|
+
* @template TParams The parameter names of the requested URL.
|
1552
|
+
* @template TContext The type of the context, extending {@link RequestContext}.
|
1553
|
+
* @template TContextData The context data to pass to the `TContext`.
|
1554
|
+
* @param request The HTTP request.
|
1555
|
+
* @param handleParams Parameters for handling the collection.
|
1556
|
+
* @returns A promise that resolves to an HTTP response.
|
1557
|
+
* @since 1.8.0
|
1558
|
+
*/
|
1559
|
+
const handleOrderedCollection = exceptWrapper(_handleOrderedCollection);
|
1560
|
+
async function _handleOrderedCollection(request, { name, values, context: context$2, tracerProvider, collectionCallbacks: callbacks, filterPredicate }) {
|
1561
|
+
verifyDefined(callbacks);
|
1562
|
+
verifyJsonLdRequest(request);
|
1563
|
+
await authIfNeeded(context$2, values, callbacks);
|
1564
|
+
const cursor = new URL(request.url).searchParams.get("cursor");
|
1565
|
+
return await new CustomCollectionHandler(name, values, context$2, callbacks, tracerProvider, require_actor.OrderedCollection, require_actor.OrderedCollectionPage, filterPredicate).fetchCollection(cursor).toJsonLd().then(respondAsActivity);
|
1566
|
+
}
|
1567
|
+
/**
|
1568
|
+
* Handling custom collections with support for pagination and filtering.
|
1569
|
+
* The main flow is on `getCollection`, `dispatch`.
|
1570
|
+
*
|
1571
|
+
* @template TItem The type of items in the collection.
|
1572
|
+
* @template TParams The parameter names of the requested URL.
|
1573
|
+
* @template TContext The type of the context. {@link Context} or {@link RequestContext}.
|
1574
|
+
* @template TContextData The context data to pass to the `TContext`.
|
1575
|
+
* @template TCollection The type of the collection, extending {@link Collection}.
|
1576
|
+
* @template TCollectionPage The type of the collection page, extending {@link CollectionPage}.
|
1577
|
+
* @since 1.8.0
|
1578
|
+
*/
|
1579
|
+
var CustomCollectionHandler = class {
|
1580
|
+
/**
|
1581
|
+
* The tracer for telemetry.
|
1582
|
+
* @type {Tracer}
|
1583
|
+
*/
|
1584
|
+
#tracer;
|
1585
|
+
/**
|
1586
|
+
* The ID of the collection.
|
1587
|
+
* @type {URL}
|
1588
|
+
*/
|
1589
|
+
#id;
|
1590
|
+
/**
|
1591
|
+
* Store total count of items in the collection.
|
1592
|
+
* Use `this.totalItems` to access the total items count.
|
1593
|
+
* It is a promise because it may require an asynchronous operation to count items.
|
1594
|
+
* @type {Promise<number | null> | undefined}
|
1595
|
+
*/
|
1596
|
+
#totalItems = void 0;
|
1597
|
+
/**
|
1598
|
+
* The first cursor for pagination.
|
1599
|
+
* It is a promise because it may require an asynchronous operation to get the first cursor.
|
1600
|
+
* @type {Promise<string | null> | undefined}
|
1601
|
+
*/
|
1602
|
+
#dispatcher;
|
1603
|
+
#collection = null;
|
1604
|
+
/**
|
1605
|
+
* Creates a new CustomCollection instance.
|
1606
|
+
* @param {string} name The name of the collection.
|
1607
|
+
* @param {TParams} values The parameter values for the collection.
|
1608
|
+
* @param {TContext} context The request context.
|
1609
|
+
* @param {CustomCollectionCallbacks} callbacks The collection callbacks.
|
1610
|
+
* @param {TracerProvider} tracerProvider The tracer provider for telemetry.
|
1611
|
+
* @param {ConstructorWithTypeId<TCollection>} Collection The Collection constructor.
|
1612
|
+
* @param {ConstructorWithTypeId<TCollectionPage>} CollectionPage The CollectionPage constructor.
|
1613
|
+
* @param {(item: TItem) => boolean} filterPredicate Optional filter predicate for items.
|
1614
|
+
*/
|
1615
|
+
constructor(name, values, context$2, callbacks, tracerProvider = __opentelemetry_api.trace.getTracerProvider(), Collection$1, CollectionPage$1, filterPredicate) {
|
1616
|
+
this.name = name;
|
1617
|
+
this.values = values;
|
1618
|
+
this.context = context$2;
|
1619
|
+
this.callbacks = callbacks;
|
1620
|
+
this.tracerProvider = tracerProvider;
|
1621
|
+
this.Collection = Collection$1;
|
1622
|
+
this.CollectionPage = CollectionPage$1;
|
1623
|
+
this.filterPredicate = filterPredicate;
|
1624
|
+
this.name = this.name.trim().replace(/\s+/g, "_");
|
1625
|
+
this.#tracer = this.tracerProvider.getTracer(require_docloader.deno_default.name, require_docloader.deno_default.version);
|
1626
|
+
this.#id = new URL(this.context.url);
|
1627
|
+
this.#dispatcher = callbacks.dispatcher.bind(callbacks);
|
1628
|
+
}
|
1629
|
+
/**
|
1630
|
+
* Converts the collection to JSON-LD format.
|
1631
|
+
* @returns A promise that resolves to the JSON-LD representation.
|
1632
|
+
*/
|
1633
|
+
async toJsonLd() {
|
1634
|
+
return (await this.collection).toJsonLd(this.context);
|
1635
|
+
}
|
1636
|
+
/**
|
1637
|
+
* Fetches the collection with optional cursor for pagination.
|
1638
|
+
* This method is defined for method chaining and to show processing flow properly.
|
1639
|
+
* So it is no problem to call `toJsonLd` directly on the instance.
|
1640
|
+
* @param cursor The cursor for pagination, or null for the first page.
|
1641
|
+
* @returns The CustomCollection instance for method chaining.
|
1642
|
+
*/
|
1643
|
+
fetchCollection(cursor = null) {
|
1644
|
+
this.#collection = this.getCollection(cursor);
|
1645
|
+
return this;
|
1646
|
+
}
|
1647
|
+
/**
|
1648
|
+
* Gets the collection or collection page based on the cursor.
|
1649
|
+
* @param {string | null} cursor The cursor for pagination, or null for the main collection.
|
1650
|
+
* @returns {Promise<TCollection | TCollectionPage>} A promise that resolves to a Collection or CollectionPage.
|
1651
|
+
*/
|
1652
|
+
async getCollection(cursor = null) {
|
1653
|
+
if (cursor !== null) {
|
1654
|
+
const props$1 = await this.getPageProps(cursor);
|
1655
|
+
return new this.CollectionPage(props$1);
|
1656
|
+
}
|
1657
|
+
const firstCursor = await this.firstCursor;
|
1658
|
+
const props = typeof firstCursor === "string" ? await this.getProps(firstCursor) : await this.getPropsWithoutCursor();
|
1659
|
+
return new this.Collection(props);
|
1660
|
+
}
|
1661
|
+
/**
|
1662
|
+
* Gets the properties for a collection page.
|
1663
|
+
* Returns the page properties including items, previous and next cursors.
|
1664
|
+
* @param {string} cursor The cursor for the page.
|
1665
|
+
* @returns A promise that resolves to the page properties.
|
1666
|
+
*/
|
1667
|
+
async getPageProps(cursor) {
|
1668
|
+
const id = this.#id;
|
1669
|
+
const pages = await this.getPages({ cursor });
|
1670
|
+
const { prevCursor, nextCursor } = pages;
|
1671
|
+
const partOf = new URL(id);
|
1672
|
+
partOf.searchParams.delete("cursor");
|
1673
|
+
return {
|
1674
|
+
id,
|
1675
|
+
partOf,
|
1676
|
+
items: this.filterItems(pages.items),
|
1677
|
+
prev: this.appendToUrl(prevCursor),
|
1678
|
+
next: this.appendToUrl(nextCursor)
|
1679
|
+
};
|
1680
|
+
}
|
1681
|
+
/**
|
1682
|
+
* Gets the properties for a collection with cursors.
|
1683
|
+
* Returns the first cursor and last cursor as URL, along with total items count.
|
1684
|
+
* @param {string} firstCursor The first cursor for pagination.
|
1685
|
+
* @returns A promise that resolves to the collection properties.
|
1686
|
+
*/
|
1687
|
+
async getProps(firstCursor) {
|
1688
|
+
const lastCursor = await this.callbacks.lastCursor?.(this.context, this.values);
|
1689
|
+
return {
|
1690
|
+
id: this.#id,
|
1691
|
+
first: this.appendToUrl(firstCursor),
|
1692
|
+
last: this.appendToUrl(lastCursor),
|
1693
|
+
totalItems: await this.totalItems
|
1694
|
+
};
|
1695
|
+
}
|
1696
|
+
/**
|
1697
|
+
* Gets the properties for a collection of all items and the count.
|
1698
|
+
* @returns A promise that resolves to the collection properties.
|
1699
|
+
*/
|
1700
|
+
async getPropsWithoutCursor() {
|
1701
|
+
const totalItems = await this.totalItems;
|
1702
|
+
const pages = await this.getPages({ totalItems });
|
1703
|
+
return {
|
1704
|
+
id: this.#id,
|
1705
|
+
totalItems,
|
1706
|
+
items: this.filterItems(pages.items)
|
1707
|
+
};
|
1708
|
+
}
|
1709
|
+
/**
|
1710
|
+
* Gets a page of items from the collection.
|
1711
|
+
* Wraps the dispatcher in a span for telemetry.
|
1712
|
+
* @param options Options for getting the page, including cursor and total items.
|
1713
|
+
* @returns A promise that resolves to the page items.
|
1714
|
+
*/
|
1715
|
+
async getPages({ cursor = null, totalItems = null }) {
|
1716
|
+
return await this.#tracer.startActiveSpan(`${this.ATTRS.DISPATCH_COLLECTION} ${this.name}`, this.spanOptions(__opentelemetry_api.SpanKind.SERVER, cursor), this.spanPages({
|
1717
|
+
cursor,
|
1718
|
+
totalItems
|
1719
|
+
}));
|
1720
|
+
}
|
1721
|
+
/**
|
1722
|
+
* Creates span options for telemetry.
|
1723
|
+
* @param {SpanKind} kind The span kind.
|
1724
|
+
* @param {string | null} cursor The optional cursor value.
|
1725
|
+
* @returns {SpanOptions}The span options.
|
1726
|
+
*/
|
1727
|
+
spanOptions = (kind, cursor) => ({
|
1728
|
+
kind,
|
1729
|
+
attributes: {
|
1730
|
+
[this.ATTRS.ID]: this.#id.href,
|
1731
|
+
[this.ATTRS.TYPE]: this.Collection.typeId.href,
|
1732
|
+
...cursor ? { [this.ATTRS.CURSOR]: cursor } : {}
|
1733
|
+
}
|
1734
|
+
});
|
1735
|
+
/**
|
1736
|
+
* Creates a function to wrap the dispatcher so tracing can be applied.
|
1737
|
+
* @param params Parameters including cursor and total items.
|
1738
|
+
* @returns {(span: Span) => Promise<PageItems<TItem>>} A function that handles the span operation.
|
1739
|
+
*/
|
1740
|
+
spanPages = ({ totalItems = null, cursor = null }) => async (span) => {
|
1741
|
+
try {
|
1742
|
+
if (totalItems !== null) span.setAttribute(this.ATTRS.TOTAL_ITEMS, totalItems);
|
1743
|
+
const page = await this.dispatch(cursor);
|
1744
|
+
span.setAttribute(this.ATTRS.ITEMS, page.items.length);
|
1745
|
+
return page;
|
1746
|
+
} catch (e) {
|
1747
|
+
const message = e instanceof Error ? e.message : String(e);
|
1748
|
+
span.setStatus({
|
1749
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
1750
|
+
message
|
1751
|
+
});
|
1752
|
+
throw e;
|
1753
|
+
} finally {
|
1754
|
+
span.end();
|
1755
|
+
}
|
1756
|
+
};
|
1757
|
+
/**
|
1758
|
+
* Dispatches the collection request to get items.
|
1759
|
+
* @param {string | null} cursor The cursor for pagination, or null for the first page.
|
1760
|
+
* @returns {Promise<PageItems<TItem>>} A promise that resolves to the page items.
|
1761
|
+
*/
|
1762
|
+
async dispatch(cursor = null) {
|
1763
|
+
return await this.#dispatcher(this.context, this.values, cursor) ?? new ItemsNotFoundError().throw();
|
1764
|
+
}
|
1765
|
+
/**
|
1766
|
+
* Filters the items in the collection.
|
1767
|
+
* @param {TItem[]} items The items to filter.
|
1768
|
+
* @returns {(Object | Link | URL)[]} The filtered items.
|
1769
|
+
*/
|
1770
|
+
filterItems(items) {
|
1771
|
+
return filterCollectionItems(items, this.name, this.filterPredicate);
|
1772
|
+
}
|
1773
|
+
/**
|
1774
|
+
* Appends a cursor to the URL if it exists.
|
1775
|
+
* @param {string | null | undefined} cursor The cursor to append, or null/undefined.
|
1776
|
+
* @returns The URL with cursor appended, or null if cursor is null/undefined.
|
1777
|
+
*/
|
1778
|
+
appendToUrl(cursor) {
|
1779
|
+
return appendCursorIfExists(this.context.url, cursor);
|
1780
|
+
}
|
1781
|
+
/**
|
1782
|
+
* Gets the stored collection or collection page.
|
1783
|
+
* @returns {Promise<TCollection | TCollectionPage>} A promise that resolves to
|
1784
|
+
the collection or collection page.
|
1785
|
+
*/
|
1786
|
+
get collection() {
|
1787
|
+
if (this.#collection === null) this.#collection = this.getCollection();
|
1788
|
+
return this.#collection;
|
1789
|
+
}
|
1790
|
+
/**
|
1791
|
+
* Gets the total number of items in the collection.
|
1792
|
+
* @returns {Promise<number | null>} A promise that
|
1793
|
+
resolves to the total items count, or null if not available.
|
1794
|
+
*/
|
1795
|
+
get totalItems() {
|
1796
|
+
if (this.#totalItems === void 0) this.totalItems = this.callbacks.counter?.(this.context, this.values);
|
1797
|
+
return this.#totalItems;
|
1798
|
+
}
|
1799
|
+
/**
|
1800
|
+
* Sets the total number of items in the collection.
|
1801
|
+
* @param value The total items count or a promise that resolves to it.
|
1802
|
+
*/
|
1803
|
+
set totalItems(value) {
|
1804
|
+
const toNumber = (value$1) => value$1 == null ? null : Number(value$1);
|
1805
|
+
this.#totalItems = value instanceof Promise ? value.then(toNumber) : Promise.resolve(toNumber(value));
|
1806
|
+
}
|
1807
|
+
/**
|
1808
|
+
* Gets the first cursor for pagination.
|
1809
|
+
* @returns {Promise<string | null>} A promise that resolves to the first cursor,
|
1810
|
+
or null if not available.
|
1811
|
+
*/
|
1812
|
+
get firstCursor() {
|
1813
|
+
const cursor = this.callbacks.firstCursor?.(this.context, this.values);
|
1814
|
+
return Promise.resolve(cursor ?? null);
|
1815
|
+
}
|
1816
|
+
/**
|
1817
|
+
* Attribute constants for telemetry spans.
|
1818
|
+
*/
|
1819
|
+
ATTRS = {
|
1820
|
+
DISPATCH_COLLECTION: "activitypub.dispatch_collection",
|
1821
|
+
CURSOR: "fedify.collection.cursor",
|
1822
|
+
ID: "activitypub.collection.id",
|
1823
|
+
ITEMS: "fedify.collection.items",
|
1824
|
+
TOTAL_ITEMS: "activitypub.collection.total_items",
|
1825
|
+
TYPE: "activitypub.collection.type"
|
1826
|
+
};
|
1827
|
+
};
|
1828
|
+
/**
|
1829
|
+
* A wrapper function that catches specific errors and handles them appropriately.
|
1830
|
+
* @template TParams The type of parameters that extend ErrorHandlers.
|
1831
|
+
* @param handler The handler function to wrap.
|
1832
|
+
* @returns A wrapped handler function that catches and handles specific errors.
|
1833
|
+
* @since 1.8.0
|
1834
|
+
*/
|
1835
|
+
function exceptWrapper(handler) {
|
1836
|
+
return async (request, handlerParams) => {
|
1837
|
+
try {
|
1838
|
+
return await handler(request, handlerParams);
|
1839
|
+
} catch (error) {
|
1840
|
+
const { onNotFound, onNotAcceptable, onUnauthorized } = handlerParams;
|
1841
|
+
switch (error?.constructor) {
|
1842
|
+
case ItemsNotFoundError: return await onNotFound(request);
|
1843
|
+
case NotAcceptableError: return await onNotAcceptable(request);
|
1844
|
+
case UnauthorizedError: return await onUnauthorized(request);
|
1845
|
+
default: throw error;
|
1846
|
+
}
|
1847
|
+
}
|
1848
|
+
};
|
1849
|
+
}
|
1850
|
+
/**
|
1851
|
+
* Verifies that a value is defined (not undefined).
|
1852
|
+
* @template T The type of the value, excluding undefined.
|
1853
|
+
* @param callbacks The value to verify.
|
1854
|
+
* @throws {ItemsNotFoundError} If the value is undefined.
|
1855
|
+
* @since 1.8.0
|
1856
|
+
*/
|
1857
|
+
const verifyDefined = (callbacks) => {
|
1858
|
+
if (callbacks === void 0) throw new ItemsNotFoundError();
|
1859
|
+
};
|
1860
|
+
/**
|
1861
|
+
* Verifies that a request accepts JSON-LD content type.
|
1862
|
+
* @param request The HTTP request to verify.
|
1863
|
+
* @throws {NotAcceptableError} If the request doesn't accept JSON-LD.
|
1864
|
+
* @since 1.8.0
|
1865
|
+
*/
|
1866
|
+
const verifyJsonLdRequest = (request) => {
|
1867
|
+
if (!acceptsJsonLd(request)) throw new NotAcceptableError();
|
1868
|
+
};
|
1869
|
+
/**
|
1870
|
+
* Performs authorization if needed based on the authorization predicate.
|
1871
|
+
* @template TContextData The context data type.
|
1872
|
+
* @param {RequestContext<TContextData>} context The request context.
|
1873
|
+
* @param {Record<string, string>} values The parameter values.
|
1874
|
+
* @param options Options containing the authorization predicate.
|
1875
|
+
* @throws {UnauthorizedError} If authorization fails.
|
1876
|
+
* @since 1.8.0
|
1877
|
+
*/
|
1878
|
+
const authIfNeeded = async (context$2, values, { authorizePredicate: authorize = void 0 }) => {
|
1879
|
+
if (authorize === void 0) return;
|
1880
|
+
const key = (await context$2.getSignedKey())?.clone({}, warning.key) ?? null;
|
1881
|
+
const keyOwner = (await context$2.getSignedKeyOwner())?.clone({}, warning.keyOwner) ?? null;
|
1882
|
+
if (!await authorize(context$2, values, key, keyOwner)) throw new UnauthorizedError();
|
1883
|
+
};
|
1884
|
+
/** Warning messages for `authIfNeeded`. */
|
1885
|
+
const warning = {
|
1886
|
+
key: { $warning: {
|
1887
|
+
category: [
|
1888
|
+
"fedify",
|
1889
|
+
"federation",
|
1890
|
+
"collection"
|
1891
|
+
],
|
1892
|
+
message: "The third parameter of AuthorizePredicate is deprecated in favor of RequestContext.getSignedKey() method. The third parameter will be removed in a future release."
|
1893
|
+
} },
|
1894
|
+
keyOwner: { $warning: {
|
1895
|
+
category: [
|
1896
|
+
"fedify",
|
1897
|
+
"federation",
|
1898
|
+
"collection"
|
1899
|
+
],
|
1900
|
+
message: "The fourth parameter of AuthorizePredicate is deprecated in favor of RequestContext.getSignedKeyOwner() method. The fourth parameter will be removed in a future release."
|
1901
|
+
} }
|
1902
|
+
};
|
1903
|
+
/**
|
1904
|
+
* Appends a cursor parameter to a URL if the cursor exists.
|
1905
|
+
* @template Cursor The type of the cursor (string, null, or undefined).
|
1906
|
+
* @param {URL} url The base URL to append the cursor to.
|
1907
|
+
* @param {string | null | undefined} cursor The cursor value to append.
|
1908
|
+
* @returns The URL with cursor appended if cursor is a string, null otherwise.
|
1909
|
+
* @since 1.8.0
|
1910
|
+
*/
|
1911
|
+
const appendCursorIfExists = (url, cursor) => {
|
1912
|
+
if (cursor === null || cursor === void 0) return null;
|
1913
|
+
const copied = new URL(url);
|
1914
|
+
copied.searchParams.set("cursor", cursor);
|
1915
|
+
return copied;
|
1916
|
+
};
|
1917
|
+
/**
|
1918
|
+
* Creates an HTTP response for ActivityPub data.
|
1919
|
+
* @param {unknown} data The data to serialize as JSON-LD.
|
1920
|
+
* @returns {Response} An HTTP response with the data as ActivityPub JSON.
|
1921
|
+
* @since 1.8.0
|
1922
|
+
*/
|
1923
|
+
const respondAsActivity = (data) => new Response(JSON.stringify(data), { headers: {
|
1924
|
+
"Content-Type": "application/activity+json",
|
1925
|
+
Vary: "Accept"
|
1926
|
+
} });
|
1927
|
+
/**
|
1928
|
+
* Base class for handler errors.
|
1929
|
+
* @since 1.8.0
|
1930
|
+
*/
|
1931
|
+
var HandlerError = class extends Error {
|
1932
|
+
constructor(message) {
|
1933
|
+
super(message);
|
1934
|
+
}
|
1935
|
+
/**
|
1936
|
+
* Throws this error.
|
1937
|
+
* @returns Never returns, always throws.
|
1938
|
+
*/
|
1939
|
+
throw() {
|
1940
|
+
throw this;
|
1941
|
+
}
|
1942
|
+
};
|
1943
|
+
/**
|
1944
|
+
* Error thrown when items are not found in a collection.
|
1945
|
+
* @since 1.8.0
|
1946
|
+
*/
|
1947
|
+
var ItemsNotFoundError = class extends HandlerError {
|
1948
|
+
constructor() {
|
1949
|
+
super("Items not found in the collection.");
|
1950
|
+
}
|
1951
|
+
};
|
1952
|
+
/**
|
1953
|
+
* Error thrown when the request is not acceptable (e.g., wrong content type).
|
1954
|
+
* @since 1.8.0
|
1955
|
+
*/
|
1956
|
+
var NotAcceptableError = class extends HandlerError {
|
1957
|
+
constructor() {
|
1958
|
+
super("The request is not acceptable.");
|
1959
|
+
}
|
1960
|
+
};
|
1961
|
+
/**
|
1962
|
+
* Error thrown when access to a collection is unauthorized.
|
1963
|
+
* @since 1.8.0
|
1964
|
+
*/
|
1965
|
+
var UnauthorizedError = class extends HandlerError {
|
1966
|
+
constructor() {
|
1967
|
+
super("Unauthorized access to the collection.");
|
1968
|
+
}
|
1969
|
+
};
|
1970
|
+
/**
|
1971
|
+
* Responds with the given object in JSON-LD format.
|
1972
|
+
*
|
1973
|
+
* @param object The object to respond with.
|
1974
|
+
* @param options Options.
|
1975
|
+
* @since 0.3.0
|
1976
|
+
*/
|
1977
|
+
async function respondWithObject(object, options) {
|
1978
|
+
const jsonLd = await object.toJsonLd(options);
|
1979
|
+
return new Response(JSON.stringify(jsonLd), { headers: { "Content-Type": "application/activity+json" } });
|
1980
|
+
}
|
1981
|
+
/**
|
1982
|
+
* Responds with the given object in JSON-LD format if the request accepts
|
1983
|
+
* JSON-LD.
|
1984
|
+
*
|
1985
|
+
* @param object The object to respond with.
|
1986
|
+
* @param request The request to check for JSON-LD acceptability.
|
1987
|
+
* @param options Options.
|
1988
|
+
* @since 0.3.0
|
1989
|
+
*/
|
1990
|
+
async function respondWithObjectIfAcceptable(object, request, options) {
|
1991
|
+
if (!acceptsJsonLd(request)) return null;
|
1992
|
+
const response = await respondWithObject(object, options);
|
1993
|
+
response.headers.set("Vary", "Accept");
|
1994
|
+
return response;
|
1995
|
+
}
|
1996
|
+
|
1997
|
+
//#endregion
|
1998
|
+
//#region src/nodeinfo/handler.ts
|
1999
|
+
/**
|
2000
|
+
* Handles a NodeInfo request. You would not typically call this function
|
2001
|
+
* directly, but instead use {@link Federation.handle} method.
|
2002
|
+
* @param request The NodeInfo request to handle.
|
2003
|
+
* @param parameters The parameters for handling the request.
|
2004
|
+
* @returns The response to the request.
|
2005
|
+
*/
|
2006
|
+
async function handleNodeInfo(_request, { context: context$2, nodeInfoDispatcher }) {
|
2007
|
+
const promise = nodeInfoDispatcher(context$2);
|
2008
|
+
const nodeInfo = promise instanceof Promise ? await promise : promise;
|
2009
|
+
const json = require_types.nodeInfoToJson(nodeInfo);
|
2010
|
+
return new Response(JSON.stringify(json), { headers: { "Content-Type": "application/json; profile=\"http://nodeinfo.diaspora.software/ns/schema/2.1#\"" } });
|
2011
|
+
}
|
2012
|
+
/**
|
2013
|
+
* Handles a request to `/.well-known/nodeinfo`. You would not typically call
|
2014
|
+
* this function directly, but instead use {@link Federation.handle} method.
|
2015
|
+
* @param request The request to handle.
|
2016
|
+
* @param context The request context.
|
2017
|
+
* @returns The response to the request.
|
2018
|
+
*/
|
2019
|
+
function handleNodeInfoJrd(_request, context$2) {
|
2020
|
+
const links = [];
|
2021
|
+
try {
|
2022
|
+
links.push({
|
2023
|
+
rel: "http://nodeinfo.diaspora.software/ns/schema/2.1",
|
2024
|
+
href: context$2.getNodeInfoUri().href,
|
2025
|
+
type: "application/json; profile=\"http://nodeinfo.diaspora.software/ns/schema/2.1#\""
|
2026
|
+
});
|
2027
|
+
} catch (e) {
|
2028
|
+
if (!(e instanceof RouterError)) throw e;
|
2029
|
+
}
|
2030
|
+
const jrd = { links };
|
2031
|
+
const response = new Response(JSON.stringify(jrd), { headers: { "Content-Type": "application/jrd+json" } });
|
2032
|
+
return Promise.resolve(response);
|
2033
|
+
}
|
2034
|
+
|
2035
|
+
//#endregion
|
2036
|
+
//#region src/webfinger/handler.ts
|
2037
|
+
const logger = (0, __logtape_logtape.getLogger)([
|
2038
|
+
"fedify",
|
2039
|
+
"webfinger",
|
2040
|
+
"server"
|
2041
|
+
]);
|
2042
|
+
/**
|
2043
|
+
* Handles a WebFinger request. You would not typically call this function
|
2044
|
+
* directly, but instead use {@link Federation.fetch} method.
|
2045
|
+
* @param request The WebFinger request to handle.
|
2046
|
+
* @param parameters The parameters for handling the request.
|
2047
|
+
* @returns The response to the request.
|
2048
|
+
*/
|
2049
|
+
async function handleWebFinger(request, options) {
|
2050
|
+
if (options.tracer == null) return await handleWebFingerInternal(request, options);
|
2051
|
+
return await options.tracer.startActiveSpan("webfinger.handle", { kind: __opentelemetry_api.SpanKind.SERVER }, async (span) => {
|
2052
|
+
try {
|
2053
|
+
const response = await handleWebFingerInternal(request, options);
|
2054
|
+
span.setStatus({ code: response.ok ? __opentelemetry_api.SpanStatusCode.UNSET : __opentelemetry_api.SpanStatusCode.ERROR });
|
2055
|
+
return response;
|
2056
|
+
} catch (error) {
|
2057
|
+
span.setStatus({
|
2058
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
2059
|
+
message: String(error)
|
2060
|
+
});
|
2061
|
+
throw error;
|
2062
|
+
} finally {
|
2063
|
+
span.end();
|
2064
|
+
}
|
2065
|
+
});
|
2066
|
+
}
|
2067
|
+
async function handleWebFingerInternal(request, { context: context$2, host, actorDispatcher, actorHandleMapper, actorAliasMapper, onNotFound, span, webFingerLinksDispatcher }) {
|
2068
|
+
if (actorDispatcher == null) {
|
2069
|
+
logger.error("Actor dispatcher is not set.");
|
2070
|
+
return await onNotFound(request);
|
2071
|
+
}
|
2072
|
+
const resource = context$2.url.searchParams.get("resource");
|
2073
|
+
if (resource == null) return new Response("Missing resource parameter.", { status: 400 });
|
2074
|
+
span?.setAttribute("webfinger.resource", resource);
|
2075
|
+
let resourceUrl;
|
2076
|
+
try {
|
2077
|
+
resourceUrl = new URL(resource);
|
2078
|
+
} catch (e) {
|
2079
|
+
if (e instanceof TypeError) return new Response("Invalid resource URL.", { status: 400 });
|
2080
|
+
throw e;
|
2081
|
+
}
|
2082
|
+
span?.setAttribute("webfinger.resource.scheme", resourceUrl.protocol.replace(/:$/, ""));
|
2083
|
+
async function mapUsernameToIdentifier(username) {
|
2084
|
+
if (actorHandleMapper == null) {
|
2085
|
+
logger.error("No actor handle mapper is set; use the WebFinger username {username} as the actor's internal identifier.", { username });
|
2086
|
+
return username;
|
2087
|
+
}
|
2088
|
+
const identifier$1 = await actorHandleMapper(context$2, username);
|
2089
|
+
if (identifier$1 == null) {
|
2090
|
+
logger.error("Actor {username} not found.", { username });
|
2091
|
+
return null;
|
2092
|
+
}
|
2093
|
+
return identifier$1;
|
2094
|
+
}
|
2095
|
+
let identifier = null;
|
2096
|
+
const uriParsed = context$2.parseUri(resourceUrl);
|
2097
|
+
if (uriParsed?.type != "actor") {
|
2098
|
+
const match = /^acct:([^@]+)@([^@]+)$/.exec(resource);
|
2099
|
+
if (match == null) {
|
2100
|
+
const result = await actorAliasMapper?.(context$2, resourceUrl);
|
2101
|
+
if (result == null) return await onNotFound(request);
|
2102
|
+
if ("identifier" in result) identifier = result.identifier;
|
2103
|
+
else identifier = await mapUsernameToIdentifier(result.username);
|
2104
|
+
} else {
|
2105
|
+
const portMatch = /:\d+$/.exec(match[2]);
|
2106
|
+
const normalizedHost = portMatch == null ? (0, node_url.domainToASCII)(match[2].toLowerCase()) : (0, node_url.domainToASCII)(match[2].substring(0, portMatch.index).toLowerCase()) + portMatch[0];
|
2107
|
+
if (normalizedHost != context$2.url.host && normalizedHost != host) return await onNotFound(request);
|
2108
|
+
else {
|
2109
|
+
identifier = await mapUsernameToIdentifier(match[1]);
|
2110
|
+
resourceUrl = new URL(`acct:${match[1]}@${normalizedHost}`);
|
2111
|
+
}
|
2112
|
+
}
|
2113
|
+
} else identifier = uriParsed.identifier;
|
2114
|
+
if (identifier == null) return await onNotFound(request);
|
2115
|
+
const actor = await actorDispatcher(context$2, identifier);
|
2116
|
+
if (actor == null) {
|
2117
|
+
logger.error("Actor {identifier} not found.", { identifier });
|
2118
|
+
return await onNotFound(request);
|
2119
|
+
}
|
2120
|
+
const links = [{
|
2121
|
+
rel: "self",
|
2122
|
+
href: context$2.getActorUri(identifier).href,
|
2123
|
+
type: "application/activity+json"
|
2124
|
+
}];
|
2125
|
+
for (const url of actor.urls) if (url instanceof require_actor.Link && url.href != null) links.push({
|
2126
|
+
rel: url.rel ?? "http://webfinger.net/rel/profile-page",
|
2127
|
+
href: url.href.href,
|
2128
|
+
type: url.mediaType == null ? void 0 : url.mediaType
|
2129
|
+
});
|
2130
|
+
else if (url instanceof URL) links.push({
|
2131
|
+
rel: "http://webfinger.net/rel/profile-page",
|
2132
|
+
href: url.href
|
2133
|
+
});
|
2134
|
+
for await (const image of actor.getIcons()) {
|
2135
|
+
if (image.url?.href == null) continue;
|
2136
|
+
const link = {
|
2137
|
+
rel: "http://webfinger.net/rel/avatar",
|
2138
|
+
href: image.url.href.toString()
|
2139
|
+
};
|
2140
|
+
if (image.mediaType != null) link.type = image.mediaType;
|
2141
|
+
links.push(link);
|
2142
|
+
}
|
2143
|
+
if (webFingerLinksDispatcher != null) {
|
2144
|
+
const customLinks = await webFingerLinksDispatcher(context$2, resourceUrl);
|
2145
|
+
if (customLinks != null) for (const link of customLinks) links.push(link);
|
2146
|
+
}
|
2147
|
+
const aliases = [];
|
2148
|
+
if (resourceUrl.protocol != "acct:" && actor.preferredUsername != null) {
|
2149
|
+
aliases.push(`acct:${actor.preferredUsername}@${host ?? context$2.url.host}`);
|
2150
|
+
if (host != null && host !== context$2.url.host) aliases.push(`acct:${actor.preferredUsername}@${context$2.url.host}`);
|
2151
|
+
}
|
2152
|
+
if (resourceUrl.href !== context$2.getActorUri(identifier).href) aliases.push(context$2.getActorUri(identifier).href);
|
2153
|
+
if (resourceUrl.protocol === "acct:" && host != null && host !== context$2.url.host && !resourceUrl.href.endsWith(`@${host}`)) {
|
2154
|
+
const username = resourceUrl.href.replace(/^acct:/, "").replace(/@.*$/, "");
|
2155
|
+
aliases.push(`acct:${username}@${host}`);
|
2156
|
+
}
|
2157
|
+
const jrd = {
|
2158
|
+
subject: resourceUrl.href,
|
2159
|
+
aliases,
|
2160
|
+
links
|
2161
|
+
};
|
2162
|
+
return new Response(JSON.stringify(jrd), { headers: {
|
2163
|
+
"Content-Type": "application/jrd+json",
|
2164
|
+
"Access-Control-Allow-Origin": "*"
|
2165
|
+
} });
|
2166
|
+
}
|
2167
|
+
|
2168
|
+
//#endregion
|
2169
|
+
//#region src/federation/retry.ts
|
2170
|
+
/**
|
2171
|
+
* Creates an exponential backoff retry policy. The delay between retries
|
2172
|
+
* starts at the `initialDelay` and is multiplied by the `factor` for each
|
2173
|
+
* subsequent retry, up to the `maxDelay`. The policy will give up after
|
2174
|
+
* `maxAttempts` attempts. The actual delay is randomized to avoid
|
2175
|
+
* synchronization (jitter).
|
2176
|
+
* @param options The options for the policy.
|
2177
|
+
* @returns The retry policy.
|
2178
|
+
* @since 0.12.0
|
2179
|
+
*/
|
2180
|
+
function createExponentialBackoffPolicy(options = {}) {
|
2181
|
+
const initialDelay = Temporal.Duration.from(options.initialDelay ?? { seconds: 1 });
|
2182
|
+
const maxDelay = Temporal.Duration.from(options.maxDelay ?? { hours: 12 });
|
2183
|
+
const maxAttempts = options.maxAttempts ?? 10;
|
2184
|
+
const factor = options.factor ?? 2;
|
2185
|
+
const jitter = options.jitter ?? true;
|
2186
|
+
return ({ attempts }) => {
|
2187
|
+
if (attempts >= maxAttempts) return null;
|
2188
|
+
let milliseconds = initialDelay.total("millisecond");
|
2189
|
+
milliseconds *= factor ** attempts;
|
2190
|
+
if (jitter) {
|
2191
|
+
milliseconds *= 1 + Math.random();
|
2192
|
+
milliseconds = Math.round(milliseconds);
|
2193
|
+
}
|
2194
|
+
const delay = Temporal.Duration.from({ milliseconds });
|
2195
|
+
return Temporal.Duration.compare(delay, maxDelay) > 0 ? maxDelay : delay;
|
2196
|
+
};
|
2197
|
+
}
|
2198
|
+
|
2199
|
+
//#endregion
|
2200
|
+
//#region src/federation/send.ts
|
2201
|
+
/**
|
2202
|
+
* Extracts the inbox URLs from recipients.
|
2203
|
+
* @param parameters The parameters to extract the inboxes.
|
2204
|
+
* See also {@link ExtractInboxesParameters}.
|
2205
|
+
* @returns The inboxes as a map of inbox URL to actor URIs.
|
2206
|
+
*/
|
2207
|
+
function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris }) {
|
2208
|
+
const inboxes = {};
|
2209
|
+
for (const recipient of recipients) {
|
2210
|
+
let inbox;
|
2211
|
+
let sharedInbox = false;
|
2212
|
+
if (preferSharedInbox && recipient.endpoints?.sharedInbox != null) {
|
2213
|
+
inbox = recipient.endpoints.sharedInbox;
|
2214
|
+
sharedInbox = true;
|
2215
|
+
} else inbox = recipient.inboxId;
|
2216
|
+
if (inbox != null && recipient.id != null) {
|
2217
|
+
if (excludeBaseUris != null && excludeBaseUris.some((u) => u.origin === inbox?.origin)) continue;
|
2218
|
+
inboxes[inbox.href] ??= {
|
2219
|
+
actorIds: /* @__PURE__ */ new Set(),
|
2220
|
+
sharedInbox
|
2221
|
+
};
|
2222
|
+
inboxes[inbox.href].actorIds.add(recipient.id.href);
|
2223
|
+
}
|
2224
|
+
}
|
2225
|
+
return inboxes;
|
2226
|
+
}
|
2227
|
+
/**
|
2228
|
+
* Sends an {@link Activity} to an inbox.
|
2229
|
+
*
|
2230
|
+
* @param parameters The parameters for sending the activity.
|
2231
|
+
* See also {@link SendActivityParameters}.
|
2232
|
+
* @throws {Error} If the activity fails to send.
|
2233
|
+
*/
|
2234
|
+
function sendActivity(options) {
|
2235
|
+
const tracerProvider = options.tracerProvider ?? __opentelemetry_api.trace.getTracerProvider();
|
2236
|
+
const tracer = tracerProvider.getTracer(require_docloader.deno_default.name, require_docloader.deno_default.version);
|
2237
|
+
return tracer.startActiveSpan("activitypub.send_activity", {
|
2238
|
+
kind: __opentelemetry_api.SpanKind.CLIENT,
|
2239
|
+
attributes: { "activitypub.shared_inbox": options.sharedInbox ?? false }
|
2240
|
+
}, async (span) => {
|
2241
|
+
if (options.activityId != null) span.setAttribute("activitypub.activity.id", options.activityId);
|
2242
|
+
if (options.activityType != null) span.setAttribute("activitypub.activity.type", options.activityType);
|
2243
|
+
try {
|
2244
|
+
await sendActivityInternal({
|
2245
|
+
...options,
|
2246
|
+
tracerProvider
|
2247
|
+
});
|
2248
|
+
} catch (e) {
|
2249
|
+
span.setStatus({
|
2250
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
2251
|
+
message: String(e)
|
2252
|
+
});
|
2253
|
+
throw e;
|
2254
|
+
} finally {
|
2255
|
+
span.end();
|
2256
|
+
}
|
2257
|
+
});
|
2258
|
+
}
|
2259
|
+
async function sendActivityInternal({ activity, activityId, keys, inbox, headers, specDeterminer, tracerProvider }) {
|
2260
|
+
const logger$1 = (0, __logtape_logtape.getLogger)([
|
2261
|
+
"fedify",
|
2262
|
+
"federation",
|
2263
|
+
"outbox"
|
2264
|
+
]);
|
2265
|
+
headers = new Headers(headers);
|
2266
|
+
headers.set("Content-Type", "application/activity+json");
|
2267
|
+
const request = new Request(inbox, {
|
2268
|
+
method: "POST",
|
2269
|
+
headers,
|
2270
|
+
body: JSON.stringify(activity)
|
2271
|
+
});
|
2272
|
+
let rsaKey = null;
|
2273
|
+
for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
|
2274
|
+
rsaKey = key;
|
2275
|
+
break;
|
2276
|
+
}
|
2277
|
+
if (rsaKey == null) logger$1.warn("No supported key found to sign the request to {inbox}. The request will be sent without a signature. In order to sign the request, at least one RSASSA-PKCS1-v1_5 key must be provided.", {
|
2278
|
+
inbox: inbox.href,
|
2279
|
+
keys: keys.map((pair) => ({
|
2280
|
+
keyId: pair.keyId.href,
|
2281
|
+
privateKey: pair.privateKey
|
2282
|
+
}))
|
2283
|
+
});
|
2284
|
+
let response;
|
2285
|
+
try {
|
2286
|
+
response = rsaKey == null ? await fetch(request) : await require_http.doubleKnock(request, rsaKey, {
|
2287
|
+
tracerProvider,
|
2288
|
+
specDeterminer
|
2289
|
+
});
|
2290
|
+
} catch (error) {
|
2291
|
+
logger$1.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
|
2292
|
+
activityId,
|
2293
|
+
inbox: inbox.href,
|
2294
|
+
error
|
2295
|
+
});
|
2296
|
+
throw error;
|
2297
|
+
}
|
2298
|
+
if (!response.ok) {
|
2299
|
+
let error;
|
2300
|
+
try {
|
2301
|
+
error = await response.text();
|
2302
|
+
} catch (_) {
|
2303
|
+
error = "";
|
2304
|
+
}
|
2305
|
+
logger$1.error("Failed to send activity {activityId} to {inbox} ({status} {statusText}):\n{error}", {
|
2306
|
+
activityId,
|
2307
|
+
inbox: inbox.href,
|
2308
|
+
status: response.status,
|
2309
|
+
statusText: response.statusText,
|
2310
|
+
error
|
2311
|
+
});
|
2312
|
+
throw new Error(`Failed to send activity ${activityId} to ${inbox.href} (${response.status} ${response.statusText}):\n${error}`);
|
2313
|
+
}
|
2314
|
+
}
|
2315
|
+
|
2316
|
+
//#endregion
|
2317
|
+
//#region src/federation/middleware.ts
|
2318
|
+
/**
|
2319
|
+
* Create a new {@link Federation} instance.
|
2320
|
+
* @param parameters Parameters for initializing the instance.
|
2321
|
+
* @returns A new {@link Federation} instance.
|
2322
|
+
* @since 0.10.0
|
2323
|
+
*/
|
2324
|
+
function createFederation(options) {
|
2325
|
+
return new FederationImpl(options);
|
2326
|
+
}
|
2327
|
+
var FederationImpl = class extends FederationBuilderImpl {
|
2328
|
+
kv;
|
2329
|
+
kvPrefixes;
|
2330
|
+
inboxQueue;
|
2331
|
+
outboxQueue;
|
2332
|
+
fanoutQueue;
|
2333
|
+
inboxQueueStarted;
|
2334
|
+
outboxQueueStarted;
|
2335
|
+
fanoutQueueStarted;
|
2336
|
+
manuallyStartQueue;
|
2337
|
+
origin;
|
2338
|
+
documentLoaderFactory;
|
2339
|
+
contextLoaderFactory;
|
2340
|
+
authenticatedDocumentLoaderFactory;
|
2341
|
+
allowPrivateAddress;
|
2342
|
+
userAgent;
|
2343
|
+
onOutboxError;
|
2344
|
+
signatureTimeWindow;
|
2345
|
+
skipSignatureVerification;
|
2346
|
+
outboxRetryPolicy;
|
2347
|
+
inboxRetryPolicy;
|
2348
|
+
activityTransformers;
|
2349
|
+
tracerProvider;
|
2350
|
+
firstKnock;
|
2351
|
+
constructor(options) {
|
2352
|
+
super();
|
2353
|
+
const logger$1 = (0, __logtape_logtape.getLogger)(["fedify", "federation"]);
|
2354
|
+
this.kv = options.kv;
|
2355
|
+
this.kvPrefixes = {
|
2356
|
+
activityIdempotence: ["_fedify", "activityIdempotence"],
|
2357
|
+
remoteDocument: ["_fedify", "remoteDocument"],
|
2358
|
+
publicKey: ["_fedify", "publicKey"],
|
2359
|
+
httpMessageSignaturesSpec: ["_fedify", "httpMessageSignaturesSpec"],
|
2360
|
+
...options.kvPrefixes ?? {}
|
2361
|
+
};
|
2362
|
+
if (options.queue == null) {
|
2363
|
+
this.inboxQueue = void 0;
|
2364
|
+
this.outboxQueue = void 0;
|
2365
|
+
this.fanoutQueue = void 0;
|
2366
|
+
} else if ("enqueue" in options.queue && "listen" in options.queue) {
|
2367
|
+
this.inboxQueue = options.queue;
|
2368
|
+
this.outboxQueue = options.queue;
|
2369
|
+
this.fanoutQueue = options.queue;
|
2370
|
+
} else {
|
2371
|
+
this.inboxQueue = options.queue.inbox;
|
2372
|
+
this.outboxQueue = options.queue.outbox;
|
2373
|
+
this.fanoutQueue = options.queue.fanout;
|
2374
|
+
}
|
2375
|
+
this.inboxQueueStarted = false;
|
2376
|
+
this.outboxQueueStarted = false;
|
2377
|
+
this.fanoutQueueStarted = false;
|
2378
|
+
this.manuallyStartQueue = options.manuallyStartQueue ?? false;
|
2379
|
+
if (options.origin != null) if (typeof options.origin === "string") {
|
2380
|
+
if (!URL.canParse(options.origin) || !options.origin.match(/^https?:\/\//)) throw new TypeError(`Invalid origin: ${JSON.stringify(options.origin)}`);
|
2381
|
+
const origin = new URL(options.origin);
|
2382
|
+
if (!origin.pathname.match(/^\/*$/) || origin.search !== "" || origin.hash !== "") throw new TypeError(`Invalid origin: ${JSON.stringify(options.origin)}`);
|
2383
|
+
this.origin = {
|
2384
|
+
handleHost: origin.host,
|
2385
|
+
webOrigin: origin.origin
|
2386
|
+
};
|
2387
|
+
} else {
|
2388
|
+
const { handleHost, webOrigin } = options.origin;
|
2389
|
+
if (!URL.canParse(`https://${handleHost}/`) || handleHost.includes("/")) throw new TypeError(`Invalid origin.handleHost: ${JSON.stringify(handleHost)}`);
|
2390
|
+
if (!URL.canParse(webOrigin) || !webOrigin.match(/^https?:\/\//)) throw new TypeError(`Invalid origin.webOrigin: ${JSON.stringify(webOrigin)}`);
|
2391
|
+
const webOriginUrl = new URL(webOrigin);
|
2392
|
+
if (!webOriginUrl.pathname.match(/^\/*$/) || webOriginUrl.search !== "" || webOriginUrl.hash !== "") throw new TypeError(`Invalid origin.webOrigin: ${JSON.stringify(webOrigin)}`);
|
2393
|
+
this.origin = {
|
2394
|
+
handleHost: new URL(`https://${handleHost}/`).host,
|
2395
|
+
webOrigin: webOriginUrl.origin
|
2396
|
+
};
|
2397
|
+
}
|
2398
|
+
this.router.trailingSlashInsensitive = options.trailingSlashInsensitive ?? false;
|
2399
|
+
this._initializeRouter();
|
2400
|
+
if (options.allowPrivateAddress || options.userAgent != null) {
|
2401
|
+
if (options.documentLoader != null) throw new TypeError("Cannot set documentLoader with allowPrivateAddress or userAgent options.");
|
2402
|
+
else if (options.contextLoader != null) throw new TypeError("Cannot set contextLoader with allowPrivateAddress or userAgent options.");
|
2403
|
+
else if (options.authenticatedDocumentLoaderFactory != null) throw new TypeError("Cannot set authenticatedDocumentLoaderFactory with allowPrivateAddress or userAgent options.");
|
2404
|
+
}
|
2405
|
+
const { allowPrivateAddress, userAgent } = options;
|
2406
|
+
this.allowPrivateAddress = allowPrivateAddress ?? false;
|
2407
|
+
if (options.documentLoader != null) {
|
2408
|
+
if (options.documentLoaderFactory != null) throw new TypeError("Cannot set both documentLoader and documentLoaderFactory options at a time; use documentLoaderFactory only.");
|
2409
|
+
this.documentLoaderFactory = () => options.documentLoader;
|
2410
|
+
logger$1.warn("The documentLoader option is deprecated; use documentLoaderFactory option instead.");
|
2411
|
+
} else this.documentLoaderFactory = options.documentLoaderFactory ?? ((opts) => {
|
2412
|
+
return require_docloader.kvCache({
|
2413
|
+
loader: require_docloader.getDocumentLoader({
|
2414
|
+
allowPrivateAddress: opts?.allowPrivateAddress ?? allowPrivateAddress,
|
2415
|
+
userAgent: opts?.userAgent ?? userAgent
|
2416
|
+
}),
|
2417
|
+
kv: options.kv,
|
2418
|
+
prefix: this.kvPrefixes.remoteDocument
|
2419
|
+
});
|
2420
|
+
});
|
2421
|
+
if (options.contextLoader != null) {
|
2422
|
+
if (options.contextLoaderFactory != null) throw new TypeError("Cannot set both contextLoader and contextLoaderFactory options at a time; use contextLoaderFactory only.");
|
2423
|
+
this.contextLoaderFactory = () => options.contextLoader;
|
2424
|
+
logger$1.warn("The contextLoader option is deprecated; use contextLoaderFactory option instead.");
|
2425
|
+
} else this.contextLoaderFactory = options.contextLoaderFactory ?? this.documentLoaderFactory;
|
2426
|
+
this.authenticatedDocumentLoaderFactory = options.authenticatedDocumentLoaderFactory ?? ((identity) => require_authdocloader.getAuthenticatedDocumentLoader(identity, {
|
2427
|
+
allowPrivateAddress,
|
2428
|
+
userAgent,
|
2429
|
+
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, options.firstKnock),
|
2430
|
+
tracerProvider: this.tracerProvider
|
2431
|
+
}));
|
2432
|
+
this.userAgent = userAgent;
|
2433
|
+
this.onOutboxError = options.onOutboxError;
|
2434
|
+
this.signatureTimeWindow = options.signatureTimeWindow ?? { hours: 1 };
|
2435
|
+
this.skipSignatureVerification = options.skipSignatureVerification ?? false;
|
2436
|
+
this.outboxRetryPolicy = options.outboxRetryPolicy ?? createExponentialBackoffPolicy();
|
2437
|
+
this.inboxRetryPolicy = options.inboxRetryPolicy ?? createExponentialBackoffPolicy();
|
2438
|
+
this.activityTransformers = options.activityTransformers ?? require_transformers.getDefaultActivityTransformers();
|
2439
|
+
this.tracerProvider = options.tracerProvider ?? __opentelemetry_api.trace.getTracerProvider();
|
2440
|
+
this.firstKnock = options.firstKnock;
|
2441
|
+
}
|
2442
|
+
_initializeRouter() {
|
2443
|
+
this.router.add("/.well-known/webfinger", "webfinger");
|
2444
|
+
this.router.add("/.well-known/nodeinfo", "nodeInfoJrd");
|
2445
|
+
}
|
2446
|
+
_getTracer() {
|
2447
|
+
return this.tracerProvider.getTracer(require_docloader.deno_default.name, require_docloader.deno_default.version);
|
2448
|
+
}
|
2449
|
+
async _startQueueInternal(ctxData, signal, queue) {
|
2450
|
+
if (this.inboxQueue == null && this.outboxQueue == null) return;
|
2451
|
+
const logger$1 = (0, __logtape_logtape.getLogger)([
|
2452
|
+
"fedify",
|
2453
|
+
"federation",
|
2454
|
+
"queue"
|
2455
|
+
]);
|
2456
|
+
const promises = [];
|
2457
|
+
if (this.inboxQueue != null && (queue == null || queue === "inbox") && !this.inboxQueueStarted) {
|
2458
|
+
logger$1.debug("Starting an inbox task worker.");
|
2459
|
+
this.inboxQueueStarted = true;
|
2460
|
+
promises.push(this.inboxQueue.listen((msg) => this.processQueuedTask(ctxData, msg), { signal }));
|
2461
|
+
}
|
2462
|
+
if (this.outboxQueue != null && this.outboxQueue !== this.inboxQueue && (queue == null || queue === "outbox") && !this.outboxQueueStarted) {
|
2463
|
+
logger$1.debug("Starting an outbox task worker.");
|
2464
|
+
this.outboxQueueStarted = true;
|
2465
|
+
promises.push(this.outboxQueue.listen((msg) => this.processQueuedTask(ctxData, msg), { signal }));
|
2466
|
+
}
|
2467
|
+
if (this.fanoutQueue != null && this.fanoutQueue !== this.inboxQueue && this.fanoutQueue !== this.outboxQueue && (queue == null || queue === "fanout") && !this.fanoutQueueStarted) {
|
2468
|
+
logger$1.debug("Starting a fanout task worker.");
|
2469
|
+
this.fanoutQueueStarted = true;
|
2470
|
+
promises.push(this.fanoutQueue.listen((msg) => this.processQueuedTask(ctxData, msg), { signal }));
|
2471
|
+
}
|
2472
|
+
await Promise.all(promises);
|
2473
|
+
}
|
2474
|
+
processQueuedTask(contextData, message) {
|
2475
|
+
const tracer = this._getTracer();
|
2476
|
+
const extractedContext = __opentelemetry_api.propagation.extract(__opentelemetry_api.context.active(), message.traceContext);
|
2477
|
+
return (0, __logtape_logtape.withContext)({ messageId: message.id }, async () => {
|
2478
|
+
if (message.type === "fanout") await tracer.startActiveSpan("activitypub.fanout", {
|
2479
|
+
kind: __opentelemetry_api.SpanKind.CONSUMER,
|
2480
|
+
attributes: { "activitypub.activity.type": message.activityType }
|
2481
|
+
}, extractedContext, async (span) => {
|
2482
|
+
if (message.activityId != null) span.setAttribute("activitypub.activity.id", message.activityId);
|
2483
|
+
try {
|
2484
|
+
await this.#listenFanoutMessage(contextData, message);
|
2485
|
+
} catch (e) {
|
2486
|
+
span.setStatus({
|
2487
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
2488
|
+
message: String(e)
|
2489
|
+
});
|
2490
|
+
throw e;
|
2491
|
+
} finally {
|
2492
|
+
span.end();
|
2493
|
+
}
|
2494
|
+
});
|
2495
|
+
else if (message.type === "outbox") await tracer.startActiveSpan("activitypub.outbox", {
|
2496
|
+
kind: __opentelemetry_api.SpanKind.CONSUMER,
|
2497
|
+
attributes: {
|
2498
|
+
"activitypub.activity.type": message.activityType,
|
2499
|
+
"activitypub.activity.retries": message.attempt
|
2500
|
+
}
|
2501
|
+
}, extractedContext, async (span) => {
|
2502
|
+
if (message.activityId != null) span.setAttribute("activitypub.activity.id", message.activityId);
|
2503
|
+
try {
|
2504
|
+
await this.#listenOutboxMessage(contextData, message, span);
|
2505
|
+
} catch (e) {
|
2506
|
+
span.setStatus({
|
2507
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
2508
|
+
message: String(e)
|
2509
|
+
});
|
2510
|
+
throw e;
|
2511
|
+
} finally {
|
2512
|
+
span.end();
|
2513
|
+
}
|
2514
|
+
});
|
2515
|
+
else if (message.type === "inbox") await tracer.startActiveSpan("activitypub.inbox", {
|
2516
|
+
kind: __opentelemetry_api.SpanKind.CONSUMER,
|
2517
|
+
attributes: { "activitypub.shared_inbox": message.identifier == null }
|
2518
|
+
}, extractedContext, async (span) => {
|
2519
|
+
try {
|
2520
|
+
await this.#listenInboxMessage(contextData, message, span);
|
2521
|
+
} catch (e) {
|
2522
|
+
span.setStatus({
|
2523
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
2524
|
+
message: String(e)
|
2525
|
+
});
|
2526
|
+
throw e;
|
2527
|
+
} finally {
|
2528
|
+
span.end();
|
2529
|
+
}
|
2530
|
+
});
|
2531
|
+
});
|
2532
|
+
}
|
2533
|
+
async #listenFanoutMessage(data, message) {
|
2534
|
+
const logger$1 = (0, __logtape_logtape.getLogger)([
|
2535
|
+
"fedify",
|
2536
|
+
"federation",
|
2537
|
+
"fanout"
|
2538
|
+
]);
|
2539
|
+
logger$1.debug("Fanning out activity {activityId} to {inboxes} inbox(es)...", {
|
2540
|
+
activityId: message.activityId,
|
2541
|
+
inboxes: globalThis.Object.keys(message.inboxes).length
|
2542
|
+
});
|
2543
|
+
const keys = await Promise.all(message.keys.map(async ({ keyId, privateKey }) => ({
|
2544
|
+
keyId: new URL(keyId),
|
2545
|
+
privateKey: await require_key.importJwk(privateKey, "private")
|
2546
|
+
})));
|
2547
|
+
const activity = await require_actor.Activity.fromJsonLd(message.activity, {
|
2548
|
+
contextLoader: this.contextLoaderFactory({
|
2549
|
+
allowPrivateAddress: this.allowPrivateAddress,
|
2550
|
+
userAgent: this.userAgent
|
2551
|
+
}),
|
2552
|
+
documentLoader: this.documentLoaderFactory({
|
2553
|
+
allowPrivateAddress: this.allowPrivateAddress,
|
2554
|
+
userAgent: this.userAgent
|
2555
|
+
}),
|
2556
|
+
tracerProvider: this.tracerProvider
|
2557
|
+
});
|
2558
|
+
const context$2 = this.#createContext(new URL(message.baseUrl), data, { documentLoader: this.documentLoaderFactory({
|
2559
|
+
allowPrivateAddress: this.allowPrivateAddress,
|
2560
|
+
userAgent: this.userAgent
|
2561
|
+
}) });
|
2562
|
+
await this.sendActivity(keys, message.inboxes, activity, {
|
2563
|
+
collectionSync: message.collectionSync,
|
2564
|
+
context: context$2
|
2565
|
+
});
|
2566
|
+
}
|
2567
|
+
async #listenOutboxMessage(_, message, span) {
|
2568
|
+
const logger$1 = (0, __logtape_logtape.getLogger)([
|
2569
|
+
"fedify",
|
2570
|
+
"federation",
|
2571
|
+
"outbox"
|
2572
|
+
]);
|
2573
|
+
const logData = {
|
2574
|
+
keyIds: message.keys.map((pair) => pair.keyId),
|
2575
|
+
inbox: message.inbox,
|
2576
|
+
activity: message.activity,
|
2577
|
+
activityId: message.activityId,
|
2578
|
+
attempt: message.attempt,
|
2579
|
+
headers: message.headers
|
2580
|
+
};
|
2581
|
+
const keys = [];
|
2582
|
+
let rsaKeyPair = null;
|
2583
|
+
for (const { keyId, privateKey } of message.keys) {
|
2584
|
+
const pair = {
|
2585
|
+
keyId: new URL(keyId),
|
2586
|
+
privateKey: await require_key.importJwk(privateKey, "private")
|
2587
|
+
};
|
2588
|
+
if (rsaKeyPair == null && pair.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") rsaKeyPair = pair;
|
2589
|
+
keys.push(pair);
|
2590
|
+
}
|
2591
|
+
try {
|
2592
|
+
await sendActivity({
|
2593
|
+
keys,
|
2594
|
+
activity: message.activity,
|
2595
|
+
activityId: message.activityId,
|
2596
|
+
activityType: message.activityType,
|
2597
|
+
inbox: new URL(message.inbox),
|
2598
|
+
sharedInbox: message.sharedInbox,
|
2599
|
+
headers: new Headers(message.headers),
|
2600
|
+
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock),
|
2601
|
+
tracerProvider: this.tracerProvider
|
2602
|
+
});
|
2603
|
+
} catch (error) {
|
2604
|
+
span.setStatus({
|
2605
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
2606
|
+
message: String(error)
|
2607
|
+
});
|
2608
|
+
const loaderOptions = this.#getLoaderOptions(message.baseUrl);
|
2609
|
+
const activity = await require_actor.Activity.fromJsonLd(message.activity, {
|
2610
|
+
contextLoader: this.contextLoaderFactory(loaderOptions),
|
2611
|
+
documentLoader: rsaKeyPair == null ? this.documentLoaderFactory(loaderOptions) : this.authenticatedDocumentLoaderFactory(rsaKeyPair, loaderOptions),
|
2612
|
+
tracerProvider: this.tracerProvider
|
2613
|
+
});
|
2614
|
+
try {
|
2615
|
+
this.onOutboxError?.(error, activity);
|
2616
|
+
} catch (error$1) {
|
2617
|
+
logger$1.error("An unexpected error occurred in onError handler:\n{error}", {
|
2618
|
+
...logData,
|
2619
|
+
error: error$1
|
2620
|
+
});
|
2621
|
+
}
|
2622
|
+
if (this.outboxQueue?.nativeRetrial) {
|
2623
|
+
logger$1.error("Failed to send activity {activityId} to {inbox}; backend will handle retry:\n{error}", {
|
2624
|
+
...logData,
|
2625
|
+
error
|
2626
|
+
});
|
2627
|
+
throw error;
|
2628
|
+
}
|
2629
|
+
const delay = this.outboxRetryPolicy({
|
2630
|
+
elapsedTime: Temporal.Instant.from(message.started).until(Temporal.Now.instant()),
|
2631
|
+
attempts: message.attempt
|
2632
|
+
});
|
2633
|
+
if (delay != null) {
|
2634
|
+
logger$1.error("Failed to send activity {activityId} to {inbox} (attempt #{attempt}); retry...:\n{error}", {
|
2635
|
+
...logData,
|
2636
|
+
error
|
2637
|
+
});
|
2638
|
+
await this.outboxQueue?.enqueue({
|
2639
|
+
...message,
|
2640
|
+
attempt: message.attempt + 1
|
2641
|
+
}, { delay: Temporal.Duration.compare(delay, { seconds: 0 }) < 0 ? Temporal.Duration.from({ seconds: 0 }) : delay });
|
2642
|
+
} else logger$1.error("Failed to send activity {activityId} to {inbox} after {attempt} attempts; giving up:\n{error}", {
|
2643
|
+
...logData,
|
2644
|
+
error
|
2645
|
+
});
|
2646
|
+
return;
|
2647
|
+
}
|
2648
|
+
logger$1.info("Successfully sent activity {activityId} to {inbox}.", { ...logData });
|
2649
|
+
}
|
2650
|
+
async #listenInboxMessage(ctxData, message, span) {
|
2651
|
+
const logger$1 = (0, __logtape_logtape.getLogger)([
|
2652
|
+
"fedify",
|
2653
|
+
"federation",
|
2654
|
+
"inbox"
|
2655
|
+
]);
|
2656
|
+
const baseUrl = new URL(message.baseUrl);
|
2657
|
+
let context$2 = this.#createContext(baseUrl, ctxData);
|
2658
|
+
if (message.identifier != null) context$2 = this.#createContext(baseUrl, ctxData, { documentLoader: await context$2.getDocumentLoader({ identifier: message.identifier }) });
|
2659
|
+
else if (this.sharedInboxKeyDispatcher != null) {
|
2660
|
+
const identity = await this.sharedInboxKeyDispatcher(context$2);
|
2661
|
+
if (identity != null) context$2 = this.#createContext(baseUrl, ctxData, { documentLoader: "identifier" in identity || "username" in identity || "handle" in identity ? await context$2.getDocumentLoader(identity) : context$2.getDocumentLoader(identity) });
|
2662
|
+
}
|
2663
|
+
const activity = await require_actor.Activity.fromJsonLd(message.activity, context$2);
|
2664
|
+
span.setAttribute("activitypub.activity.type", require_actor.getTypeId(activity).href);
|
2665
|
+
if (activity.id != null) span.setAttribute("activitypub.activity.id", activity.id.href);
|
2666
|
+
const cacheKey = activity.id == null ? null : [
|
2667
|
+
...this.kvPrefixes.activityIdempotence,
|
2668
|
+
context$2.origin,
|
2669
|
+
activity.id.href
|
2670
|
+
];
|
2671
|
+
if (cacheKey != null) {
|
2672
|
+
const cached = await this.kv.get(cacheKey);
|
2673
|
+
if (cached === true) {
|
2674
|
+
logger$1.debug("Activity {activityId} has already been processed.", {
|
2675
|
+
activityId: activity.id?.href,
|
2676
|
+
activity: message.activity,
|
2677
|
+
recipient: message.identifier
|
2678
|
+
});
|
2679
|
+
return;
|
2680
|
+
}
|
2681
|
+
}
|
2682
|
+
await this._getTracer().startActiveSpan("activitypub.dispatch_inbox_listener", { kind: __opentelemetry_api.SpanKind.INTERNAL }, async (span$1) => {
|
2683
|
+
const dispatched = this.inboxListeners?.dispatchWithClass(activity);
|
2684
|
+
if (dispatched == null) {
|
2685
|
+
logger$1.error("Unsupported activity type:\n{activity}", {
|
2686
|
+
activityId: activity.id?.href,
|
2687
|
+
activity: message.activity,
|
2688
|
+
recipient: message.identifier,
|
2689
|
+
trial: message.attempt
|
2690
|
+
});
|
2691
|
+
span$1.setStatus({
|
2692
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
2693
|
+
message: `Unsupported activity type: ${require_actor.getTypeId(activity).href}`
|
2694
|
+
});
|
2695
|
+
span$1.end();
|
2696
|
+
return;
|
2697
|
+
}
|
2698
|
+
const { class: cls, listener } = dispatched;
|
2699
|
+
span$1.updateName(`activitypub.dispatch_inbox_listener ${cls.name}`);
|
2700
|
+
try {
|
2701
|
+
await listener(context$2.toInboxContext(message.identifier, message.activity, activity.id?.href, require_actor.getTypeId(activity).href), activity);
|
2702
|
+
} catch (error) {
|
2703
|
+
try {
|
2704
|
+
await this.inboxErrorHandler?.(context$2, error);
|
2705
|
+
} catch (error$1) {
|
2706
|
+
logger$1.error("An unexpected error occurred in inbox error handler:\n{error}", {
|
2707
|
+
error: error$1,
|
2708
|
+
trial: message.attempt,
|
2709
|
+
activityId: activity.id?.href,
|
2710
|
+
activity: message.activity,
|
2711
|
+
recipient: message.identifier
|
2712
|
+
});
|
2713
|
+
}
|
2714
|
+
if (this.inboxQueue?.nativeRetrial) {
|
2715
|
+
logger$1.error("Failed to process the incoming activity {activityId}; backend will handle retry:\n{error}", {
|
2716
|
+
error,
|
2717
|
+
activityId: activity.id?.href,
|
2718
|
+
activity: message.activity,
|
2719
|
+
recipient: message.identifier
|
2720
|
+
});
|
2721
|
+
span$1.setStatus({
|
2722
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
2723
|
+
message: String(error)
|
2724
|
+
});
|
2725
|
+
span$1.end();
|
2726
|
+
throw error;
|
2727
|
+
}
|
2728
|
+
const delay = this.inboxRetryPolicy({
|
2729
|
+
elapsedTime: Temporal.Instant.from(message.started).until(Temporal.Now.instant()),
|
2730
|
+
attempts: message.attempt
|
2731
|
+
});
|
2732
|
+
if (delay != null) {
|
2733
|
+
logger$1.error("Failed to process the incoming activity {activityId} (attempt #{attempt}); retry...:\n{error}", {
|
2734
|
+
error,
|
2735
|
+
attempt: message.attempt,
|
2736
|
+
activityId: activity.id?.href,
|
2737
|
+
activity: message.activity,
|
2738
|
+
recipient: message.identifier
|
2739
|
+
});
|
2740
|
+
await this.inboxQueue?.enqueue({
|
2741
|
+
...message,
|
2742
|
+
attempt: message.attempt + 1
|
2743
|
+
}, { delay: Temporal.Duration.compare(delay, { seconds: 0 }) < 0 ? Temporal.Duration.from({ seconds: 0 }) : delay });
|
2744
|
+
} else logger$1.error("Failed to process the incoming activity {activityId} after {trial} attempts; giving up:\n{error}", {
|
2745
|
+
error,
|
2746
|
+
activityId: activity.id?.href,
|
2747
|
+
activity: message.activity,
|
2748
|
+
recipient: message.identifier
|
2749
|
+
});
|
2750
|
+
span$1.setStatus({
|
2751
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
2752
|
+
message: String(error)
|
2753
|
+
});
|
2754
|
+
span$1.end();
|
2755
|
+
return;
|
2756
|
+
}
|
2757
|
+
if (cacheKey != null) await this.kv.set(cacheKey, true, { ttl: Temporal.Duration.from({ days: 1 }) });
|
2758
|
+
logger$1.info("Activity {activityId} has been processed.", {
|
2759
|
+
activityId: activity.id?.href,
|
2760
|
+
activity: message.activity,
|
2761
|
+
recipient: message.identifier
|
2762
|
+
});
|
2763
|
+
span$1.end();
|
2764
|
+
});
|
2765
|
+
}
|
2766
|
+
startQueue(contextData, options = {}) {
|
2767
|
+
return this._startQueueInternal(contextData, options.signal, options.queue);
|
2768
|
+
}
|
2769
|
+
createContext(urlOrRequest, contextData) {
|
2770
|
+
return urlOrRequest instanceof Request ? this.#createContext(urlOrRequest, contextData) : this.#createContext(urlOrRequest, contextData);
|
2771
|
+
}
|
2772
|
+
#createContext(urlOrRequest, contextData, opts = {}) {
|
2773
|
+
const request = urlOrRequest instanceof Request ? urlOrRequest : null;
|
2774
|
+
const url = urlOrRequest instanceof URL ? new URL(urlOrRequest) : new URL(urlOrRequest.url);
|
2775
|
+
if (request == null) {
|
2776
|
+
url.pathname = "/";
|
2777
|
+
url.hash = "";
|
2778
|
+
url.search = "";
|
2779
|
+
}
|
2780
|
+
const loaderOptions = this.#getLoaderOptions(url.origin);
|
2781
|
+
const ctxOptions = {
|
2782
|
+
url,
|
2783
|
+
federation: this,
|
2784
|
+
data: contextData,
|
2785
|
+
documentLoader: opts.documentLoader ?? this.documentLoaderFactory(loaderOptions),
|
2786
|
+
contextLoader: this.contextLoaderFactory(loaderOptions)
|
2787
|
+
};
|
2788
|
+
if (request == null) return new ContextImpl(ctxOptions);
|
2789
|
+
return new RequestContextImpl({
|
2790
|
+
...ctxOptions,
|
2791
|
+
request,
|
2792
|
+
invokedFromActorDispatcher: opts.invokedFromActorDispatcher,
|
2793
|
+
invokedFromObjectDispatcher: opts.invokedFromObjectDispatcher
|
2794
|
+
});
|
2795
|
+
}
|
2796
|
+
#getLoaderOptions(origin) {
|
2797
|
+
origin = typeof origin === "string" ? new URL(origin).origin : origin.origin;
|
2798
|
+
return {
|
2799
|
+
allowPrivateAddress: this.allowPrivateAddress,
|
2800
|
+
userAgent: typeof this.userAgent === "string" ? this.userAgent : {
|
2801
|
+
url: origin,
|
2802
|
+
...this.userAgent
|
2803
|
+
}
|
2804
|
+
};
|
2805
|
+
}
|
2806
|
+
async sendActivity(keys, inboxes, activity, options) {
|
2807
|
+
const logger$1 = (0, __logtape_logtape.getLogger)([
|
2808
|
+
"fedify",
|
2809
|
+
"federation",
|
2810
|
+
"outbox"
|
2811
|
+
]);
|
2812
|
+
const { immediate, collectionSync, context: ctx } = options;
|
2813
|
+
if (activity.id == null) throw new TypeError("The activity to send must have an id.");
|
2814
|
+
if (activity.actorId == null) throw new TypeError("The activity to send must have at least one actor property.");
|
2815
|
+
else if (keys.length < 1) throw new TypeError("The keys must not be empty.");
|
2816
|
+
const contextLoader = this.contextLoaderFactory(this.#getLoaderOptions(ctx.origin));
|
2817
|
+
const activityId = activity.id.href;
|
2818
|
+
let proofCreated = false;
|
2819
|
+
let rsaKey = null;
|
2820
|
+
for (const { keyId, privateKey } of keys) {
|
2821
|
+
require_key.validateCryptoKey(privateKey, "private");
|
2822
|
+
if (rsaKey == null && privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
|
2823
|
+
rsaKey = {
|
2824
|
+
keyId,
|
2825
|
+
privateKey
|
2826
|
+
};
|
2827
|
+
continue;
|
2828
|
+
}
|
2829
|
+
if (privateKey.algorithm.name === "Ed25519") {
|
2830
|
+
activity = await require_proof.signObject(activity, privateKey, keyId, {
|
2831
|
+
contextLoader,
|
2832
|
+
tracerProvider: this.tracerProvider
|
2833
|
+
});
|
2834
|
+
proofCreated = true;
|
2835
|
+
}
|
2836
|
+
}
|
2837
|
+
let jsonLd = await activity.toJsonLd({
|
2838
|
+
format: "compact",
|
2839
|
+
contextLoader
|
2840
|
+
});
|
2841
|
+
if (rsaKey == null) logger$1.warn("No supported key found to create a Linked Data signature for the activity {activityId}. The activity will be sent without a Linked Data signature. In order to create a Linked Data signature, at least one RSASSA-PKCS1-v1_5 key must be provided.", {
|
2842
|
+
activityId,
|
2843
|
+
keys: keys.map((pair) => ({
|
2844
|
+
keyId: pair.keyId.href,
|
2845
|
+
privateKey: pair.privateKey
|
2846
|
+
}))
|
2847
|
+
});
|
2848
|
+
else jsonLd = await require_proof.signJsonLd(jsonLd, rsaKey.privateKey, rsaKey.keyId, {
|
2849
|
+
contextLoader,
|
2850
|
+
tracerProvider: this.tracerProvider
|
2851
|
+
});
|
2852
|
+
if (!proofCreated) logger$1.warn("No supported key found to create a proof for the activity {activityId}. The activity will be sent without a proof. In order to create a proof, at least one Ed25519 key must be provided.", {
|
2853
|
+
activityId,
|
2854
|
+
keys: keys.map((pair) => ({
|
2855
|
+
keyId: pair.keyId.href,
|
2856
|
+
privateKey: pair.privateKey
|
2857
|
+
}))
|
2858
|
+
});
|
2859
|
+
if (immediate || this.outboxQueue == null) {
|
2860
|
+
if (immediate) logger$1.debug("Sending activity immediately without queue since immediate option is set.", {
|
2861
|
+
activityId: activity.id.href,
|
2862
|
+
activity: jsonLd
|
2863
|
+
});
|
2864
|
+
else logger$1.debug("Sending activity immediately without queue since queue is not set.", {
|
2865
|
+
activityId: activity.id.href,
|
2866
|
+
activity: jsonLd
|
2867
|
+
});
|
2868
|
+
const promises = [];
|
2869
|
+
for (const inbox in inboxes) promises.push(sendActivity({
|
2870
|
+
keys,
|
2871
|
+
activity: jsonLd,
|
2872
|
+
activityId: activity.id?.href,
|
2873
|
+
activityType: require_actor.getTypeId(activity).href,
|
2874
|
+
inbox: new URL(inbox),
|
2875
|
+
sharedInbox: inboxes[inbox].sharedInbox,
|
2876
|
+
headers: collectionSync == null ? void 0 : new Headers({ "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox].actorIds) }),
|
2877
|
+
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock),
|
2878
|
+
tracerProvider: this.tracerProvider
|
2879
|
+
}));
|
2880
|
+
await Promise.all(promises);
|
2881
|
+
return;
|
2882
|
+
}
|
2883
|
+
logger$1.debug("Enqueuing activity {activityId} to send later.", {
|
2884
|
+
activityId: activity.id.href,
|
2885
|
+
activity: jsonLd
|
2886
|
+
});
|
2887
|
+
const keyJwkPairs = [];
|
2888
|
+
for (const { keyId, privateKey } of keys) {
|
2889
|
+
const privateKeyJwk = await require_key.exportJwk(privateKey);
|
2890
|
+
keyJwkPairs.push({
|
2891
|
+
keyId: keyId.href,
|
2892
|
+
privateKey: privateKeyJwk
|
2893
|
+
});
|
2894
|
+
}
|
2895
|
+
if (!this.manuallyStartQueue) this._startQueueInternal(ctx.data);
|
2896
|
+
const carrier = {};
|
2897
|
+
__opentelemetry_api.propagation.inject(__opentelemetry_api.context.active(), carrier);
|
2898
|
+
const messages = [];
|
2899
|
+
for (const inbox in inboxes) {
|
2900
|
+
const message = {
|
2901
|
+
type: "outbox",
|
2902
|
+
id: crypto.randomUUID(),
|
2903
|
+
baseUrl: ctx.origin,
|
2904
|
+
keys: keyJwkPairs,
|
2905
|
+
activity: jsonLd,
|
2906
|
+
activityId: activity.id?.href,
|
2907
|
+
activityType: require_actor.getTypeId(activity).href,
|
2908
|
+
inbox,
|
2909
|
+
sharedInbox: inboxes[inbox].sharedInbox,
|
2910
|
+
started: (/* @__PURE__ */ new Date()).toISOString(),
|
2911
|
+
attempt: 0,
|
2912
|
+
headers: collectionSync == null ? {} : { "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox].actorIds) },
|
2913
|
+
traceContext: carrier
|
2914
|
+
};
|
2915
|
+
messages.push(message);
|
2916
|
+
}
|
2917
|
+
const { outboxQueue } = this;
|
2918
|
+
if (outboxQueue.enqueueMany == null) {
|
2919
|
+
const promises = messages.map((m) => outboxQueue.enqueue(m));
|
2920
|
+
const results = await Promise.allSettled(promises);
|
2921
|
+
const errors = results.filter((r) => r.status === "rejected").map((r) => r.reason);
|
2922
|
+
if (errors.length > 0) {
|
2923
|
+
logger$1.error("Failed to enqueue activity {activityId} to send later: {errors}", {
|
2924
|
+
activityId: activity.id.href,
|
2925
|
+
errors
|
2926
|
+
});
|
2927
|
+
if (errors.length > 1) throw new AggregateError(errors, `Failed to enqueue activity ${activityId} to send later.`);
|
2928
|
+
throw errors[0];
|
2929
|
+
}
|
2930
|
+
} else try {
|
2931
|
+
await outboxQueue.enqueueMany(messages);
|
2932
|
+
} catch (error) {
|
2933
|
+
logger$1.error("Failed to enqueue activity {activityId} to send later: {error}", {
|
2934
|
+
activityId: activity.id.href,
|
2935
|
+
error
|
2936
|
+
});
|
2937
|
+
throw error;
|
2938
|
+
}
|
2939
|
+
}
|
2940
|
+
fetch(request, options) {
|
2941
|
+
const requestId = getRequestId(request);
|
2942
|
+
return (0, __logtape_logtape.withContext)({ requestId }, async () => {
|
2943
|
+
const tracer = this._getTracer();
|
2944
|
+
return await tracer.startActiveSpan(request.method, {
|
2945
|
+
kind: __opentelemetry_api.SpanKind.SERVER,
|
2946
|
+
attributes: {
|
2947
|
+
[__opentelemetry_semantic_conventions.ATTR_HTTP_REQUEST_METHOD]: request.method,
|
2948
|
+
[__opentelemetry_semantic_conventions.ATTR_URL_FULL]: request.url
|
2949
|
+
}
|
2950
|
+
}, async (span) => {
|
2951
|
+
const logger$1 = (0, __logtape_logtape.getLogger)([
|
2952
|
+
"fedify",
|
2953
|
+
"federation",
|
2954
|
+
"http"
|
2955
|
+
]);
|
2956
|
+
if (span.isRecording()) for (const [k, v] of request.headers) span.setAttribute((0, __opentelemetry_semantic_conventions.ATTR_HTTP_REQUEST_HEADER)(k), [v]);
|
2957
|
+
let response;
|
2958
|
+
try {
|
2959
|
+
response = await this.#fetch(request, {
|
2960
|
+
...options,
|
2961
|
+
span,
|
2962
|
+
tracer
|
2963
|
+
});
|
2964
|
+
} catch (error) {
|
2965
|
+
span.setStatus({
|
2966
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
2967
|
+
message: `${error}`
|
2968
|
+
});
|
2969
|
+
span.end();
|
2970
|
+
logger$1.error("An error occurred while serving request {method} {url}: {error}", {
|
2971
|
+
method: request.method,
|
2972
|
+
url: request.url,
|
2973
|
+
error
|
2974
|
+
});
|
2975
|
+
throw error;
|
2976
|
+
}
|
2977
|
+
if (span.isRecording()) {
|
2978
|
+
span.setAttribute(__opentelemetry_semantic_conventions.ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);
|
2979
|
+
for (const [k, v] of response.headers) span.setAttribute((0, __opentelemetry_semantic_conventions.ATTR_HTTP_RESPONSE_HEADER)(k), [v]);
|
2980
|
+
span.setStatus({
|
2981
|
+
code: response.status >= 500 ? __opentelemetry_api.SpanStatusCode.ERROR : __opentelemetry_api.SpanStatusCode.UNSET,
|
2982
|
+
message: response.statusText
|
2983
|
+
});
|
2984
|
+
}
|
2985
|
+
span.end();
|
2986
|
+
const url = new URL(request.url);
|
2987
|
+
const logTpl = "{method} {path}: {status}";
|
2988
|
+
const values = {
|
2989
|
+
method: request.method,
|
2990
|
+
path: `${url.pathname}${url.search}`,
|
2991
|
+
url: request.url,
|
2992
|
+
status: response.status
|
2993
|
+
};
|
2994
|
+
if (response.status >= 500) logger$1.error(logTpl, values);
|
2995
|
+
else if (response.status >= 400) logger$1.warn(logTpl, values);
|
2996
|
+
else logger$1.info(logTpl, values);
|
2997
|
+
return response;
|
2998
|
+
});
|
2999
|
+
});
|
3000
|
+
}
|
3001
|
+
async #fetch(request, { onNotFound, onNotAcceptable, onUnauthorized, contextData, span, tracer }) {
|
3002
|
+
onNotFound ??= notFound;
|
3003
|
+
onNotAcceptable ??= notAcceptable;
|
3004
|
+
onUnauthorized ??= unauthorized;
|
3005
|
+
const url = new URL(request.url);
|
3006
|
+
const route = this.router.route(url.pathname);
|
3007
|
+
if (route == null) return await onNotFound(request);
|
3008
|
+
span.updateName(`${request.method} ${route.template}`);
|
3009
|
+
let context$2 = this.#createContext(request, contextData);
|
3010
|
+
const routeName = route.name.replace(/:.*$/, "");
|
3011
|
+
switch (routeName) {
|
3012
|
+
case "webfinger": return await handleWebFinger(request, {
|
3013
|
+
context: context$2,
|
3014
|
+
host: this.origin?.handleHost,
|
3015
|
+
actorDispatcher: this.actorCallbacks?.dispatcher,
|
3016
|
+
actorHandleMapper: this.actorCallbacks?.handleMapper,
|
3017
|
+
actorAliasMapper: this.actorCallbacks?.aliasMapper,
|
3018
|
+
webFingerLinksDispatcher: this.webFingerLinksDispatcher,
|
3019
|
+
onNotFound,
|
3020
|
+
tracer
|
3021
|
+
});
|
3022
|
+
case "nodeInfoJrd": return await handleNodeInfoJrd(request, context$2);
|
3023
|
+
case "nodeInfo": return await handleNodeInfo(request, {
|
3024
|
+
context: context$2,
|
3025
|
+
nodeInfoDispatcher: this.nodeInfoDispatcher
|
3026
|
+
});
|
3027
|
+
case "actor":
|
3028
|
+
context$2 = this.#createContext(request, contextData, { invokedFromActorDispatcher: { identifier: route.values.identifier ?? route.values.handle } });
|
3029
|
+
return await handleActor(request, {
|
3030
|
+
identifier: route.values.identifier ?? route.values.handle,
|
3031
|
+
context: context$2,
|
3032
|
+
actorDispatcher: this.actorCallbacks?.dispatcher,
|
3033
|
+
authorizePredicate: this.actorCallbacks?.authorizePredicate,
|
3034
|
+
onUnauthorized,
|
3035
|
+
onNotFound,
|
3036
|
+
onNotAcceptable
|
3037
|
+
});
|
3038
|
+
case "object": {
|
3039
|
+
const typeId = route.name.replace(/^object:/, "");
|
3040
|
+
const callbacks = this.objectCallbacks[typeId];
|
3041
|
+
const cls = this.objectTypeIds[typeId];
|
3042
|
+
context$2 = this.#createContext(request, contextData, { invokedFromObjectDispatcher: {
|
3043
|
+
cls,
|
3044
|
+
values: route.values
|
3045
|
+
} });
|
3046
|
+
return await handleObject(request, {
|
3047
|
+
values: route.values,
|
3048
|
+
context: context$2,
|
3049
|
+
objectDispatcher: callbacks?.dispatcher,
|
3050
|
+
authorizePredicate: callbacks?.authorizePredicate,
|
3051
|
+
onUnauthorized,
|
3052
|
+
onNotFound,
|
3053
|
+
onNotAcceptable
|
3054
|
+
});
|
3055
|
+
}
|
3056
|
+
case "outbox": return await handleCollection(request, {
|
3057
|
+
name: "outbox",
|
3058
|
+
identifier: route.values.identifier ?? route.values.handle,
|
3059
|
+
uriGetter: context$2.getOutboxUri.bind(context$2),
|
3060
|
+
context: context$2,
|
3061
|
+
collectionCallbacks: this.outboxCallbacks,
|
3062
|
+
tracerProvider: this.tracerProvider,
|
3063
|
+
onUnauthorized,
|
3064
|
+
onNotFound,
|
3065
|
+
onNotAcceptable
|
3066
|
+
});
|
3067
|
+
case "inbox":
|
3068
|
+
if (request.method !== "POST") return await handleCollection(request, {
|
3069
|
+
name: "inbox",
|
3070
|
+
identifier: route.values.identifier ?? route.values.handle,
|
3071
|
+
uriGetter: context$2.getInboxUri.bind(context$2),
|
3072
|
+
context: context$2,
|
3073
|
+
collectionCallbacks: this.inboxCallbacks,
|
3074
|
+
tracerProvider: this.tracerProvider,
|
3075
|
+
onUnauthorized,
|
3076
|
+
onNotFound,
|
3077
|
+
onNotAcceptable
|
3078
|
+
});
|
3079
|
+
context$2 = this.#createContext(request, contextData, { documentLoader: await context$2.getDocumentLoader({ identifier: route.values.identifier ?? route.values.handle }) });
|
3080
|
+
case "sharedInbox":
|
3081
|
+
if (routeName !== "inbox" && this.sharedInboxKeyDispatcher != null) {
|
3082
|
+
const identity = await this.sharedInboxKeyDispatcher(context$2);
|
3083
|
+
if (identity != null) context$2 = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity || "handle" in identity ? await context$2.getDocumentLoader(identity) : context$2.getDocumentLoader(identity) });
|
3084
|
+
}
|
3085
|
+
if (!this.manuallyStartQueue) this._startQueueInternal(contextData);
|
3086
|
+
return await handleInbox(request, {
|
3087
|
+
recipient: route.values.identifier ?? route.values.handle ?? null,
|
3088
|
+
context: context$2,
|
3089
|
+
inboxContextFactory: context$2.toInboxContext.bind(context$2),
|
3090
|
+
kv: this.kv,
|
3091
|
+
kvPrefixes: this.kvPrefixes,
|
3092
|
+
queue: this.inboxQueue,
|
3093
|
+
actorDispatcher: this.actorCallbacks?.dispatcher,
|
3094
|
+
inboxListeners: this.inboxListeners,
|
3095
|
+
inboxErrorHandler: this.inboxErrorHandler,
|
3096
|
+
onNotFound,
|
3097
|
+
signatureTimeWindow: this.signatureTimeWindow,
|
3098
|
+
skipSignatureVerification: this.skipSignatureVerification,
|
3099
|
+
tracerProvider: this.tracerProvider
|
3100
|
+
});
|
3101
|
+
case "following": return await handleCollection(request, {
|
3102
|
+
name: "following",
|
3103
|
+
identifier: route.values.identifier ?? route.values.handle,
|
3104
|
+
uriGetter: context$2.getFollowingUri.bind(context$2),
|
3105
|
+
context: context$2,
|
3106
|
+
collectionCallbacks: this.followingCallbacks,
|
3107
|
+
tracerProvider: this.tracerProvider,
|
3108
|
+
onUnauthorized,
|
3109
|
+
onNotFound,
|
3110
|
+
onNotAcceptable
|
3111
|
+
});
|
3112
|
+
case "followers": {
|
3113
|
+
let baseUrl = url.searchParams.get("base-url");
|
3114
|
+
if (baseUrl != null) try {
|
3115
|
+
baseUrl = `${new URL(baseUrl).origin}/`;
|
3116
|
+
} catch {
|
3117
|
+
baseUrl = null;
|
3118
|
+
}
|
3119
|
+
return await handleCollection(request, {
|
3120
|
+
name: "followers",
|
3121
|
+
identifier: route.values.identifier ?? route.values.handle,
|
3122
|
+
uriGetter: baseUrl == null ? context$2.getFollowersUri.bind(context$2) : (identifier) => {
|
3123
|
+
const uri = context$2.getFollowersUri(identifier);
|
3124
|
+
uri.searchParams.set("base-url", baseUrl);
|
3125
|
+
return uri;
|
3126
|
+
},
|
3127
|
+
context: context$2,
|
3128
|
+
filter: baseUrl != null ? new URL(baseUrl) : void 0,
|
3129
|
+
filterPredicate: baseUrl != null ? (i) => (i instanceof URL ? i.href : i.id?.href ?? "").startsWith(baseUrl) : void 0,
|
3130
|
+
collectionCallbacks: this.followersCallbacks,
|
3131
|
+
tracerProvider: this.tracerProvider,
|
3132
|
+
onUnauthorized,
|
3133
|
+
onNotFound,
|
3134
|
+
onNotAcceptable
|
3135
|
+
});
|
3136
|
+
}
|
3137
|
+
case "liked": return await handleCollection(request, {
|
3138
|
+
name: "liked",
|
3139
|
+
identifier: route.values.identifier ?? route.values.handle,
|
3140
|
+
uriGetter: context$2.getLikedUri.bind(context$2),
|
3141
|
+
context: context$2,
|
3142
|
+
collectionCallbacks: this.likedCallbacks,
|
3143
|
+
tracerProvider: this.tracerProvider,
|
3144
|
+
onUnauthorized,
|
3145
|
+
onNotFound,
|
3146
|
+
onNotAcceptable
|
3147
|
+
});
|
3148
|
+
case "featured": return await handleCollection(request, {
|
3149
|
+
name: "featured",
|
3150
|
+
identifier: route.values.identifier ?? route.values.handle,
|
3151
|
+
uriGetter: context$2.getFeaturedUri.bind(context$2),
|
3152
|
+
context: context$2,
|
3153
|
+
collectionCallbacks: this.featuredCallbacks,
|
3154
|
+
tracerProvider: this.tracerProvider,
|
3155
|
+
onUnauthorized,
|
3156
|
+
onNotFound,
|
3157
|
+
onNotAcceptable
|
3158
|
+
});
|
3159
|
+
case "featuredTags": return await handleCollection(request, {
|
3160
|
+
name: "featured tags",
|
3161
|
+
identifier: route.values.identifier ?? route.values.handle,
|
3162
|
+
uriGetter: context$2.getFeaturedTagsUri.bind(context$2),
|
3163
|
+
context: context$2,
|
3164
|
+
collectionCallbacks: this.featuredTagsCallbacks,
|
3165
|
+
tracerProvider: this.tracerProvider,
|
3166
|
+
onUnauthorized,
|
3167
|
+
onNotFound,
|
3168
|
+
onNotAcceptable
|
3169
|
+
});
|
3170
|
+
case "collection": {
|
3171
|
+
const name = route.name.replace(/^collection:/, "");
|
3172
|
+
const callbacks = this.collectionCallbacks[name];
|
3173
|
+
return await handleCustomCollection(request, {
|
3174
|
+
name,
|
3175
|
+
context: context$2,
|
3176
|
+
values: route.values,
|
3177
|
+
collectionCallbacks: callbacks,
|
3178
|
+
tracerProvider: this.tracerProvider,
|
3179
|
+
onUnauthorized,
|
3180
|
+
onNotFound,
|
3181
|
+
onNotAcceptable
|
3182
|
+
});
|
3183
|
+
}
|
3184
|
+
case "orderedCollection": {
|
3185
|
+
const name = route.name.replace(/^orderedCollection:/, "");
|
3186
|
+
const callbacks = this.collectionCallbacks[name];
|
3187
|
+
return await handleOrderedCollection(request, {
|
3188
|
+
name,
|
3189
|
+
context: context$2,
|
3190
|
+
values: route.values,
|
3191
|
+
collectionCallbacks: callbacks,
|
3192
|
+
tracerProvider: this.tracerProvider,
|
3193
|
+
onUnauthorized,
|
3194
|
+
onNotFound,
|
3195
|
+
onNotAcceptable
|
3196
|
+
});
|
3197
|
+
}
|
3198
|
+
default: {
|
3199
|
+
const response = onNotFound(request);
|
3200
|
+
return response instanceof Promise ? await response : response;
|
3201
|
+
}
|
3202
|
+
}
|
3203
|
+
}
|
3204
|
+
};
|
3205
|
+
const FANOUT_THRESHOLD = 5;
|
3206
|
+
var ContextImpl = class ContextImpl {
|
3207
|
+
url;
|
3208
|
+
federation;
|
3209
|
+
data;
|
3210
|
+
documentLoader;
|
3211
|
+
contextLoader;
|
3212
|
+
invokedFromActorKeyPairsDispatcher;
|
3213
|
+
constructor({ url, federation, data, documentLoader, contextLoader, invokedFromActorKeyPairsDispatcher }) {
|
3214
|
+
this.url = url;
|
3215
|
+
this.federation = federation;
|
3216
|
+
this.data = data;
|
3217
|
+
this.documentLoader = documentLoader;
|
3218
|
+
this.contextLoader = contextLoader;
|
3219
|
+
this.invokedFromActorKeyPairsDispatcher = invokedFromActorKeyPairsDispatcher;
|
3220
|
+
}
|
3221
|
+
clone(data) {
|
3222
|
+
return new ContextImpl({
|
3223
|
+
url: this.url,
|
3224
|
+
federation: this.federation,
|
3225
|
+
data,
|
3226
|
+
documentLoader: this.documentLoader,
|
3227
|
+
contextLoader: this.contextLoader,
|
3228
|
+
invokedFromActorKeyPairsDispatcher: this.invokedFromActorKeyPairsDispatcher
|
3229
|
+
});
|
3230
|
+
}
|
3231
|
+
toInboxContext(recipient, activity, activityId, activityType) {
|
3232
|
+
return new InboxContextImpl(recipient, activity, activityId, activityType, {
|
3233
|
+
url: this.url,
|
3234
|
+
federation: this.federation,
|
3235
|
+
data: this.data,
|
3236
|
+
documentLoader: this.documentLoader,
|
3237
|
+
contextLoader: this.contextLoader,
|
3238
|
+
invokedFromActorKeyPairsDispatcher: this.invokedFromActorKeyPairsDispatcher
|
3239
|
+
});
|
3240
|
+
}
|
3241
|
+
get hostname() {
|
3242
|
+
return this.url.hostname;
|
3243
|
+
}
|
3244
|
+
get host() {
|
3245
|
+
return this.url.host;
|
3246
|
+
}
|
3247
|
+
get origin() {
|
3248
|
+
return this.url.origin;
|
3249
|
+
}
|
3250
|
+
get canonicalOrigin() {
|
3251
|
+
return this.federation.origin?.webOrigin ?? this.origin;
|
3252
|
+
}
|
3253
|
+
get tracerProvider() {
|
3254
|
+
return this.federation.tracerProvider;
|
3255
|
+
}
|
3256
|
+
getNodeInfoUri() {
|
3257
|
+
const path = this.federation.router.build("nodeInfo", {});
|
3258
|
+
if (path == null) throw new RouterError("No NodeInfo dispatcher registered.");
|
3259
|
+
return new URL(path, this.canonicalOrigin);
|
3260
|
+
}
|
3261
|
+
getActorUri(identifier) {
|
3262
|
+
const path = this.federation.router.build("actor", {
|
3263
|
+
identifier,
|
3264
|
+
handle: identifier
|
3265
|
+
});
|
3266
|
+
if (path == null) throw new RouterError("No actor dispatcher registered.");
|
3267
|
+
return new URL(path, this.canonicalOrigin);
|
3268
|
+
}
|
3269
|
+
getObjectUri(cls, values) {
|
3270
|
+
const callbacks = this.federation.objectCallbacks[cls.typeId.href];
|
3271
|
+
if (callbacks == null) throw new RouterError("No object dispatcher registered.");
|
3272
|
+
for (const param of callbacks.parameters) if (!(param in values)) throw new TypeError(`Missing parameter: ${param}`);
|
3273
|
+
const path = this.federation.router.build(`object:${cls.typeId.href}`, values);
|
3274
|
+
if (path == null) throw new RouterError("No object dispatcher registered.");
|
3275
|
+
return new URL(path, this.canonicalOrigin);
|
3276
|
+
}
|
3277
|
+
getOutboxUri(identifier) {
|
3278
|
+
const path = this.federation.router.build("outbox", {
|
3279
|
+
identifier,
|
3280
|
+
handle: identifier
|
3281
|
+
});
|
3282
|
+
if (path == null) throw new RouterError("No outbox dispatcher registered.");
|
3283
|
+
return new URL(path, this.canonicalOrigin);
|
3284
|
+
}
|
3285
|
+
getInboxUri(identifier) {
|
3286
|
+
if (identifier == null) {
|
3287
|
+
const path$1 = this.federation.router.build("sharedInbox", {});
|
3288
|
+
if (path$1 == null) throw new RouterError("No shared inbox path registered.");
|
3289
|
+
return new URL(path$1, this.canonicalOrigin);
|
3290
|
+
}
|
3291
|
+
const path = this.federation.router.build("inbox", {
|
3292
|
+
identifier,
|
3293
|
+
handle: identifier
|
3294
|
+
});
|
3295
|
+
if (path == null) throw new RouterError("No inbox path registered.");
|
3296
|
+
return new URL(path, this.canonicalOrigin);
|
3297
|
+
}
|
3298
|
+
getFollowingUri(identifier) {
|
3299
|
+
const path = this.federation.router.build("following", {
|
3300
|
+
identifier,
|
3301
|
+
handle: identifier
|
3302
|
+
});
|
3303
|
+
if (path == null) throw new RouterError("No following collection path registered.");
|
3304
|
+
return new URL(path, this.canonicalOrigin);
|
3305
|
+
}
|
3306
|
+
getFollowersUri(identifier) {
|
3307
|
+
const path = this.federation.router.build("followers", {
|
3308
|
+
identifier,
|
3309
|
+
handle: identifier
|
3310
|
+
});
|
3311
|
+
if (path == null) throw new RouterError("No followers collection path registered.");
|
3312
|
+
return new URL(path, this.canonicalOrigin);
|
3313
|
+
}
|
3314
|
+
getLikedUri(identifier) {
|
3315
|
+
const path = this.federation.router.build("liked", {
|
3316
|
+
identifier,
|
3317
|
+
handle: identifier
|
3318
|
+
});
|
3319
|
+
if (path == null) throw new RouterError("No liked collection path registered.");
|
3320
|
+
return new URL(path, this.canonicalOrigin);
|
3321
|
+
}
|
3322
|
+
getFeaturedUri(identifier) {
|
3323
|
+
const path = this.federation.router.build("featured", {
|
3324
|
+
identifier,
|
3325
|
+
handle: identifier
|
3326
|
+
});
|
3327
|
+
if (path == null) throw new RouterError("No featured collection path registered.");
|
3328
|
+
return new URL(path, this.canonicalOrigin);
|
3329
|
+
}
|
3330
|
+
getFeaturedTagsUri(identifier) {
|
3331
|
+
const path = this.federation.router.build("featuredTags", {
|
3332
|
+
identifier,
|
3333
|
+
handle: identifier
|
3334
|
+
});
|
3335
|
+
if (path == null) throw new RouterError("No featured tags collection path registered.");
|
3336
|
+
return new URL(path, this.canonicalOrigin);
|
3337
|
+
}
|
3338
|
+
getCollectionUri(name, values) {
|
3339
|
+
const path = this.federation.getCollectionPath(name, values);
|
3340
|
+
if (path === null) throw new RouterError(`No collection dispatcher registered for "${String(name)}".`);
|
3341
|
+
return new URL(path, this.canonicalOrigin);
|
3342
|
+
}
|
3343
|
+
parseUri(uri) {
|
3344
|
+
if (uri == null) return null;
|
3345
|
+
if (uri.origin !== this.origin && uri.origin !== this.canonicalOrigin) return null;
|
3346
|
+
const route = this.federation.router.route(uri.pathname);
|
3347
|
+
const logger$1 = (0, __logtape_logtape.getLogger)(["fedify", "federation"]);
|
3348
|
+
if (route == null) return null;
|
3349
|
+
else if (route.name === "sharedInbox") return {
|
3350
|
+
type: "inbox",
|
3351
|
+
identifier: void 0,
|
3352
|
+
get handle() {
|
3353
|
+
logger$1.warn("The ParseUriResult.handle property is deprecated; use ParseUriResult.identifier instead.");
|
3354
|
+
return void 0;
|
3355
|
+
}
|
3356
|
+
};
|
3357
|
+
const identifier = "identifier" in route.values ? route.values.identifier : route.values.handle;
|
3358
|
+
if (route.name === "actor") return {
|
3359
|
+
type: "actor",
|
3360
|
+
identifier,
|
3361
|
+
get handle() {
|
3362
|
+
logger$1.warn("The ParseUriResult.handle property is deprecated; use ParseUriResult.identifier instead.");
|
3363
|
+
return identifier;
|
3364
|
+
}
|
3365
|
+
};
|
3366
|
+
else if (route.name.startsWith("object:")) {
|
3367
|
+
const typeId = route.name.replace(/^object:/, "");
|
3368
|
+
return {
|
3369
|
+
type: "object",
|
3370
|
+
class: this.federation.objectTypeIds[typeId],
|
3371
|
+
typeId: new URL(typeId),
|
3372
|
+
values: route.values
|
3373
|
+
};
|
3374
|
+
} else if (route.name === "inbox") return {
|
3375
|
+
type: "inbox",
|
3376
|
+
identifier,
|
3377
|
+
get handle() {
|
3378
|
+
logger$1.warn("The ParseUriResult.handle property is deprecated; use ParseUriResult.identifier instead.");
|
3379
|
+
return identifier;
|
3380
|
+
}
|
3381
|
+
};
|
3382
|
+
else if (route.name === "outbox") return {
|
3383
|
+
type: "outbox",
|
3384
|
+
identifier,
|
3385
|
+
get handle() {
|
3386
|
+
logger$1.warn("The ParseUriResult.handle property is deprecated; use ParseUriResult.identifier instead.");
|
3387
|
+
return identifier;
|
3388
|
+
}
|
3389
|
+
};
|
3390
|
+
else if (route.name === "following") return {
|
3391
|
+
type: "following",
|
3392
|
+
identifier,
|
3393
|
+
get handle() {
|
3394
|
+
logger$1.warn("The ParseUriResult.handle property is deprecated; use ParseUriResult.identifier instead.");
|
3395
|
+
return identifier;
|
3396
|
+
}
|
3397
|
+
};
|
3398
|
+
else if (route.name === "followers") return {
|
3399
|
+
type: "followers",
|
3400
|
+
identifier,
|
3401
|
+
get handle() {
|
3402
|
+
logger$1.warn("The ParseUriResult.handle property is deprecated; use ParseUriResult.identifier instead.");
|
3403
|
+
return identifier;
|
3404
|
+
}
|
3405
|
+
};
|
3406
|
+
else if (route.name === "liked") return {
|
3407
|
+
type: "liked",
|
3408
|
+
identifier,
|
3409
|
+
get handle() {
|
3410
|
+
logger$1.warn("The ParseUriResult.handle property is deprecated; use ParseUriResult.identifier instead.");
|
3411
|
+
return identifier;
|
3412
|
+
}
|
3413
|
+
};
|
3414
|
+
else if (route.name === "featured") return {
|
3415
|
+
type: "featured",
|
3416
|
+
identifier,
|
3417
|
+
get handle() {
|
3418
|
+
logger$1.warn("The ParseUriResult.handle property is deprecated; use ParseUriResult.identifier instead.");
|
3419
|
+
return identifier;
|
3420
|
+
}
|
3421
|
+
};
|
3422
|
+
else if (route.name === "featuredTags") return {
|
3423
|
+
type: "featuredTags",
|
3424
|
+
identifier,
|
3425
|
+
get handle() {
|
3426
|
+
logger$1.warn("The ParseUriResult.handle property is deprecated; use ParseUriResult.identifier instead.");
|
3427
|
+
return identifier;
|
3428
|
+
}
|
3429
|
+
};
|
3430
|
+
const collectionTypes = ["collection", "orderedCollection"];
|
3431
|
+
const collectionRegex = /* @__PURE__ */ new RegExp(`^(${collectionTypes.join("|")}):(.*)$`);
|
3432
|
+
const match = route.name.match(collectionRegex);
|
3433
|
+
if (match !== null) {
|
3434
|
+
const [, type, name] = match;
|
3435
|
+
const cls = this.federation.collectionTypeIds[name];
|
3436
|
+
return {
|
3437
|
+
type,
|
3438
|
+
name,
|
3439
|
+
class: cls,
|
3440
|
+
typeId: cls.typeId,
|
3441
|
+
values: route.values
|
3442
|
+
};
|
3443
|
+
}
|
3444
|
+
return null;
|
3445
|
+
}
|
3446
|
+
async getActorKeyPairs(identifier) {
|
3447
|
+
const logger$1 = (0, __logtape_logtape.getLogger)([
|
3448
|
+
"fedify",
|
3449
|
+
"federation",
|
3450
|
+
"actor"
|
3451
|
+
]);
|
3452
|
+
if (this.invokedFromActorKeyPairsDispatcher != null) logger$1.warn("Context.getActorKeyPairs({getActorKeyPairsIdentifier}) method is invoked from the actor key pairs dispatcher ({actorKeyPairsDispatcherIdentifier}); this may cause an infinite loop.", {
|
3453
|
+
getActorKeyPairsIdentifier: identifier,
|
3454
|
+
actorKeyPairsDispatcherIdentifier: this.invokedFromActorKeyPairsDispatcher.identifier
|
3455
|
+
});
|
3456
|
+
let keyPairs;
|
3457
|
+
try {
|
3458
|
+
keyPairs = await this.getKeyPairsFromIdentifier(identifier);
|
3459
|
+
} catch (_) {
|
3460
|
+
logger$1.warn("No actor key pairs dispatcher registered.");
|
3461
|
+
return [];
|
3462
|
+
}
|
3463
|
+
const owner = this.getActorUri(identifier);
|
3464
|
+
const result = [];
|
3465
|
+
for (const keyPair of keyPairs) {
|
3466
|
+
const newPair = {
|
3467
|
+
...keyPair,
|
3468
|
+
cryptographicKey: new require_actor.CryptographicKey({
|
3469
|
+
id: keyPair.keyId,
|
3470
|
+
owner,
|
3471
|
+
publicKey: keyPair.publicKey
|
3472
|
+
}),
|
3473
|
+
multikey: new require_actor.Multikey({
|
3474
|
+
id: keyPair.keyId,
|
3475
|
+
controller: owner,
|
3476
|
+
publicKey: keyPair.publicKey
|
3477
|
+
})
|
3478
|
+
};
|
3479
|
+
result.push(newPair);
|
3480
|
+
}
|
3481
|
+
return result;
|
3482
|
+
}
|
3483
|
+
async getKeyPairsFromIdentifier(identifier) {
|
3484
|
+
const logger$1 = (0, __logtape_logtape.getLogger)([
|
3485
|
+
"fedify",
|
3486
|
+
"federation",
|
3487
|
+
"actor"
|
3488
|
+
]);
|
3489
|
+
if (this.federation.actorCallbacks?.keyPairsDispatcher == null) throw new Error("No actor key pairs dispatcher registered.");
|
3490
|
+
const path = this.federation.router.build("actor", {
|
3491
|
+
identifier,
|
3492
|
+
handle: identifier
|
3493
|
+
});
|
3494
|
+
if (path == null) {
|
3495
|
+
logger$1.warn("No actor dispatcher registered.");
|
3496
|
+
return [];
|
3497
|
+
}
|
3498
|
+
const actorUri = new URL(path, this.canonicalOrigin);
|
3499
|
+
const keyPairs = await this.federation.actorCallbacks?.keyPairsDispatcher(new ContextImpl({
|
3500
|
+
...this,
|
3501
|
+
invokedFromActorKeyPairsDispatcher: { identifier }
|
3502
|
+
}), identifier);
|
3503
|
+
if (keyPairs.length < 1) logger$1.warn("No key pairs found for actor {identifier}.", { identifier });
|
3504
|
+
let i = 0;
|
3505
|
+
const result = [];
|
3506
|
+
for (const keyPair of keyPairs) {
|
3507
|
+
result.push({
|
3508
|
+
...keyPair,
|
3509
|
+
keyId: new URL(i == 0 ? `#main-key` : `#key-${i + 1}`, actorUri)
|
3510
|
+
});
|
3511
|
+
i++;
|
3512
|
+
}
|
3513
|
+
return result;
|
3514
|
+
}
|
3515
|
+
async getRsaKeyPairFromIdentifier(identifier) {
|
3516
|
+
const keyPairs = await this.getKeyPairsFromIdentifier(identifier);
|
3517
|
+
for (const keyPair of keyPairs) {
|
3518
|
+
const { privateKey } = keyPair;
|
3519
|
+
if (privateKey.algorithm.name === "RSASSA-PKCS1-v1_5" && privateKey.algorithm.hash.name === "SHA-256") return keyPair;
|
3520
|
+
}
|
3521
|
+
(0, __logtape_logtape.getLogger)([
|
3522
|
+
"fedify",
|
3523
|
+
"federation",
|
3524
|
+
"actor"
|
3525
|
+
]).warn("No RSA-PKCS#1-v1.5 SHA-256 key found for actor {identifier}.", { identifier });
|
3526
|
+
return null;
|
3527
|
+
}
|
3528
|
+
getDocumentLoader(identity) {
|
3529
|
+
if ("identifier" in identity || "username" in identity || "handle" in identity) {
|
3530
|
+
let identifierPromise;
|
3531
|
+
if ("username" in identity || "handle" in identity) {
|
3532
|
+
let username;
|
3533
|
+
if ("username" in identity) username = identity.username;
|
3534
|
+
else {
|
3535
|
+
username = identity.handle;
|
3536
|
+
(0, __logtape_logtape.getLogger)([
|
3537
|
+
"fedify",
|
3538
|
+
"runtime",
|
3539
|
+
"docloader"
|
3540
|
+
]).warn("The \"handle\" property is deprecated; use \"identifier\" or \"username\" instead.", { identity });
|
3541
|
+
}
|
3542
|
+
const mapper = this.federation.actorCallbacks?.handleMapper;
|
3543
|
+
if (mapper == null) identifierPromise = Promise.resolve(username);
|
3544
|
+
else {
|
3545
|
+
const identifier = mapper(this, username);
|
3546
|
+
identifierPromise = identifier instanceof Promise ? identifier : Promise.resolve(identifier);
|
3547
|
+
}
|
3548
|
+
} else identifierPromise = Promise.resolve(identity.identifier);
|
3549
|
+
return identifierPromise.then((identifier) => {
|
3550
|
+
if (identifier == null) return this.documentLoader;
|
3551
|
+
const keyPair = this.getRsaKeyPairFromIdentifier(identifier);
|
3552
|
+
return keyPair.then((pair) => pair == null ? this.documentLoader : this.federation.authenticatedDocumentLoaderFactory(pair));
|
3553
|
+
});
|
3554
|
+
}
|
3555
|
+
return this.federation.authenticatedDocumentLoaderFactory(identity);
|
3556
|
+
}
|
3557
|
+
lookupObject(identifier, options = {}) {
|
3558
|
+
return require_vocab.lookupObject(identifier, {
|
3559
|
+
...options,
|
3560
|
+
documentLoader: options.documentLoader ?? this.documentLoader,
|
3561
|
+
contextLoader: options.contextLoader ?? this.contextLoader,
|
3562
|
+
userAgent: options.userAgent ?? this.federation.userAgent,
|
3563
|
+
tracerProvider: options.tracerProvider ?? this.tracerProvider,
|
3564
|
+
allowPrivateAddress: this.federation.allowPrivateAddress
|
3565
|
+
});
|
3566
|
+
}
|
3567
|
+
traverseCollection(collection, options = {}) {
|
3568
|
+
return require_vocab.traverseCollection(collection, {
|
3569
|
+
...options,
|
3570
|
+
documentLoader: options.documentLoader ?? this.documentLoader,
|
3571
|
+
contextLoader: options.contextLoader ?? this.contextLoader
|
3572
|
+
});
|
3573
|
+
}
|
3574
|
+
lookupNodeInfo(url, options = {}) {
|
3575
|
+
return options.parse === "none" ? require_types.getNodeInfo(url, {
|
3576
|
+
parse: "none",
|
3577
|
+
direct: options.direct,
|
3578
|
+
userAgent: options?.userAgent ?? this.federation.userAgent
|
3579
|
+
}) : require_types.getNodeInfo(url, {
|
3580
|
+
parse: options.parse,
|
3581
|
+
direct: options.direct,
|
3582
|
+
userAgent: options?.userAgent ?? this.federation.userAgent
|
3583
|
+
});
|
3584
|
+
}
|
3585
|
+
lookupWebFinger(resource, options = {}) {
|
3586
|
+
return require_lookup.lookupWebFinger(resource, {
|
3587
|
+
...options,
|
3588
|
+
userAgent: options.userAgent ?? this.federation.userAgent,
|
3589
|
+
tracerProvider: options.tracerProvider ?? this.tracerProvider,
|
3590
|
+
allowPrivateAddress: this.federation.allowPrivateAddress
|
3591
|
+
});
|
3592
|
+
}
|
3593
|
+
sendActivity(sender, recipients, activity, options = {}) {
|
3594
|
+
const tracer = this.tracerProvider.getTracer(require_docloader.deno_default.name, require_docloader.deno_default.version);
|
3595
|
+
return tracer.startActiveSpan(this.federation.outboxQueue == null || options.immediate ? "activitypub.outbox" : "activitypub.fanout", {
|
3596
|
+
kind: this.federation.outboxQueue == null || options.immediate ? __opentelemetry_api.SpanKind.CLIENT : __opentelemetry_api.SpanKind.PRODUCER,
|
3597
|
+
attributes: {
|
3598
|
+
"activitypub.activity.type": require_actor.getTypeId(activity).href,
|
3599
|
+
"activitypub.activity.to": activity.toIds.map((to) => to.href),
|
3600
|
+
"activitypub.activity.cc": activity.toIds.map((cc) => cc.href),
|
3601
|
+
"activitypub.activity.bto": activity.btoIds.map((bto) => bto.href),
|
3602
|
+
"activitypub.activity.bcc": activity.toIds.map((bcc) => bcc.href)
|
3603
|
+
}
|
3604
|
+
}, async (span) => {
|
3605
|
+
try {
|
3606
|
+
if (activity.id != null) span.setAttribute("activitypub.activity.id", activity.id.href);
|
3607
|
+
await this.sendActivityInternal(sender, recipients, activity, options, span);
|
3608
|
+
} catch (e) {
|
3609
|
+
span.setStatus({
|
3610
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
3611
|
+
message: String(e)
|
3612
|
+
});
|
3613
|
+
throw e;
|
3614
|
+
} finally {
|
3615
|
+
span.end();
|
3616
|
+
}
|
3617
|
+
});
|
3618
|
+
}
|
3619
|
+
async sendActivityInternal(sender, recipients, activity, options, span) {
|
3620
|
+
const logger$1 = (0, __logtape_logtape.getLogger)([
|
3621
|
+
"fedify",
|
3622
|
+
"federation",
|
3623
|
+
"outbox"
|
3624
|
+
]);
|
3625
|
+
let keys;
|
3626
|
+
let identifier = null;
|
3627
|
+
if ("identifier" in sender || "username" in sender || "handle" in sender) {
|
3628
|
+
if ("identifier" in sender) identifier = sender.identifier;
|
3629
|
+
else {
|
3630
|
+
let username;
|
3631
|
+
if ("username" in sender) username = sender.username;
|
3632
|
+
else {
|
3633
|
+
username = sender.handle;
|
3634
|
+
logger$1.warn("The \"handle\" property for the sender parameter is deprecated; use \"identifier\" or \"username\" instead.", { sender });
|
3635
|
+
}
|
3636
|
+
if (this.federation.actorCallbacks?.handleMapper == null) identifier = username;
|
3637
|
+
else {
|
3638
|
+
const mapped = await this.federation.actorCallbacks.handleMapper(this, username);
|
3639
|
+
if (mapped == null) throw new Error(`No actor found for the given username ${JSON.stringify(username)}.`);
|
3640
|
+
identifier = mapped;
|
3641
|
+
}
|
3642
|
+
}
|
3643
|
+
span.setAttribute("fedify.actor.identifier", identifier);
|
3644
|
+
keys = await this.getKeyPairsFromIdentifier(identifier);
|
3645
|
+
if (keys.length < 1) throw new Error(`No key pair found for actor ${JSON.stringify(identifier)}.`);
|
3646
|
+
} else if (Array.isArray(sender)) {
|
3647
|
+
if (sender.length < 1) throw new Error("The sender's key pairs are empty.");
|
3648
|
+
keys = sender;
|
3649
|
+
} else keys = [sender];
|
3650
|
+
if (keys.length < 1) throw new TypeError("The sender's keys must not be empty.");
|
3651
|
+
for (const { privateKey } of keys) require_key.validateCryptoKey(privateKey, "private");
|
3652
|
+
const opts = { context: this };
|
3653
|
+
let expandedRecipients;
|
3654
|
+
if (Array.isArray(recipients)) expandedRecipients = recipients;
|
3655
|
+
else if (recipients === "followers") {
|
3656
|
+
if (identifier == null) throw new Error("If recipients is \"followers\", sender must be an actor identifier or username.");
|
3657
|
+
expandedRecipients = [];
|
3658
|
+
for await (const recipient of this.getFollowers(identifier)) expandedRecipients.push(recipient);
|
3659
|
+
if (options.syncCollection) {
|
3660
|
+
const collectionId = this.federation.router.build("followers", {
|
3661
|
+
identifier,
|
3662
|
+
handle: identifier
|
3663
|
+
});
|
3664
|
+
opts.collectionSync = collectionId == null ? void 0 : new URL(collectionId, this.canonicalOrigin).href;
|
3665
|
+
}
|
3666
|
+
} else expandedRecipients = [recipients];
|
3667
|
+
span.setAttribute("activitypub.inboxes", expandedRecipients.length);
|
3668
|
+
for (const activityTransformer of this.federation.activityTransformers) activity = activityTransformer(activity, this);
|
3669
|
+
span?.setAttribute("activitypub.activity.id", activity?.id?.href ?? "");
|
3670
|
+
if (activity.actorId == null) {
|
3671
|
+
logger$1.error("Activity {activityId} to send does not have an actor.", {
|
3672
|
+
activity,
|
3673
|
+
activityId: activity?.id?.href
|
3674
|
+
});
|
3675
|
+
throw new TypeError("The activity to send must have at least one actor property.");
|
3676
|
+
}
|
3677
|
+
const inboxes = extractInboxes({
|
3678
|
+
recipients: expandedRecipients,
|
3679
|
+
preferSharedInbox: options.preferSharedInbox,
|
3680
|
+
excludeBaseUris: options.excludeBaseUris
|
3681
|
+
});
|
3682
|
+
logger$1.debug("Sending activity {activityId} to inboxes:\n{inboxes}", {
|
3683
|
+
inboxes: globalThis.Object.keys(inboxes),
|
3684
|
+
activityId: activity.id?.href,
|
3685
|
+
activity
|
3686
|
+
});
|
3687
|
+
if (this.federation.fanoutQueue == null || options.immediate || options.fanout === "skip" || (options.fanout ?? "auto") === "auto" && globalThis.Object.keys(inboxes).length < FANOUT_THRESHOLD) {
|
3688
|
+
await this.federation.sendActivity(keys, inboxes, activity, opts);
|
3689
|
+
return;
|
3690
|
+
}
|
3691
|
+
const keyJwkPairs = await Promise.all(keys.map(async ({ keyId, privateKey }) => ({
|
3692
|
+
keyId: keyId.href,
|
3693
|
+
privateKey: await require_key.exportJwk(privateKey)
|
3694
|
+
})));
|
3695
|
+
const carrier = {};
|
3696
|
+
__opentelemetry_api.propagation.inject(__opentelemetry_api.context.active(), carrier);
|
3697
|
+
const message = {
|
3698
|
+
type: "fanout",
|
3699
|
+
id: crypto.randomUUID(),
|
3700
|
+
baseUrl: this.origin,
|
3701
|
+
keys: keyJwkPairs,
|
3702
|
+
inboxes: globalThis.Object.fromEntries(globalThis.Object.entries(inboxes).map(([k, { actorIds, sharedInbox }]) => [k, {
|
3703
|
+
actorIds: [...actorIds],
|
3704
|
+
sharedInbox
|
3705
|
+
}])),
|
3706
|
+
activity: await activity.toJsonLd({
|
3707
|
+
format: "compact",
|
3708
|
+
contextLoader: this.contextLoader
|
3709
|
+
}),
|
3710
|
+
activityId: activity.id?.href,
|
3711
|
+
activityType: require_actor.getTypeId(activity).href,
|
3712
|
+
collectionSync: opts.collectionSync,
|
3713
|
+
traceContext: carrier
|
3714
|
+
};
|
3715
|
+
if (!this.federation.manuallyStartQueue) this.federation._startQueueInternal(this.data);
|
3716
|
+
this.federation.fanoutQueue.enqueue(message);
|
3717
|
+
}
|
3718
|
+
async *getFollowers(identifier) {
|
3719
|
+
if (this.federation.followersCallbacks == null) throw new Error("No followers collection dispatcher registered.");
|
3720
|
+
const result = await this.federation.followersCallbacks.dispatcher(this, identifier, null);
|
3721
|
+
if (result != null) {
|
3722
|
+
for (const recipient of result.items) yield recipient;
|
3723
|
+
return;
|
3724
|
+
}
|
3725
|
+
if (this.federation.followersCallbacks.firstCursor == null) throw new Error("No first cursor dispatcher registered for followers collection.");
|
3726
|
+
let cursor = await this.federation.followersCallbacks.firstCursor(this, identifier);
|
3727
|
+
if (cursor != null) (0, __logtape_logtape.getLogger)([
|
3728
|
+
"fedify",
|
3729
|
+
"federation",
|
3730
|
+
"outbox"
|
3731
|
+
]).warn("Since the followers collection dispatcher returned null for no cursor (i.e., one-shot dispatcher), the pagination is used to fetch \"followers\". However, it is recommended to implement the one-shot dispatcher for better performance.", { identifier });
|
3732
|
+
while (cursor != null) {
|
3733
|
+
const result$1 = await this.federation.followersCallbacks.dispatcher(this, identifier, cursor);
|
3734
|
+
if (result$1 == null) break;
|
3735
|
+
for (const recipient of result$1.items) yield recipient;
|
3736
|
+
cursor = result$1.nextCursor ?? null;
|
3737
|
+
}
|
3738
|
+
}
|
3739
|
+
routeActivity(recipient, activity, options = {}) {
|
3740
|
+
const tracerProvider = this.tracerProvider ?? this.tracerProvider;
|
3741
|
+
const tracer = tracerProvider.getTracer(require_docloader.deno_default.name, require_docloader.deno_default.version);
|
3742
|
+
return tracer.startActiveSpan("activitypub.inbox", {
|
3743
|
+
kind: this.federation.inboxQueue == null || options.immediate ? __opentelemetry_api.SpanKind.INTERNAL : __opentelemetry_api.SpanKind.PRODUCER,
|
3744
|
+
attributes: { "activitypub.activity.type": require_actor.getTypeId(activity).href }
|
3745
|
+
}, async (span) => {
|
3746
|
+
if (activity.id != null) span.setAttribute("activitypub.activity.id", activity.id.href);
|
3747
|
+
if (activity.toIds.length > 0) span.setAttribute("activitypub.activity.to", activity.toIds.map((to) => to.href));
|
3748
|
+
if (activity.ccIds.length > 0) span.setAttribute("activitypub.activity.cc", activity.ccIds.map((cc) => cc.href));
|
3749
|
+
if (activity.btoIds.length > 0) span.setAttribute("activitypub.activity.bto", activity.btoIds.map((bto) => bto.href));
|
3750
|
+
if (activity.bccIds.length > 0) span.setAttribute("activitypub.activity.bcc", activity.bccIds.map((bcc) => bcc.href));
|
3751
|
+
try {
|
3752
|
+
const ok = await this.routeActivityInternal(recipient, activity, options, span);
|
3753
|
+
if (ok) {
|
3754
|
+
span.setAttribute("activitypub.shared_inbox", recipient == null);
|
3755
|
+
if (recipient != null) span.setAttribute("fedify.inbox.recipient", recipient);
|
3756
|
+
} else span.setStatus({ code: __opentelemetry_api.SpanStatusCode.ERROR });
|
3757
|
+
return ok;
|
3758
|
+
} catch (e) {
|
3759
|
+
span.setStatus({
|
3760
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
3761
|
+
message: String(e)
|
3762
|
+
});
|
3763
|
+
throw e;
|
3764
|
+
} finally {
|
3765
|
+
span.end();
|
3766
|
+
}
|
3767
|
+
});
|
3768
|
+
}
|
3769
|
+
async routeActivityInternal(recipient, activity, options = {}, span) {
|
3770
|
+
const logger$1 = (0, __logtape_logtape.getLogger)([
|
3771
|
+
"fedify",
|
3772
|
+
"federation",
|
3773
|
+
"inbox"
|
3774
|
+
]);
|
3775
|
+
const contextLoader = options.contextLoader ?? this.contextLoader;
|
3776
|
+
const json = await activity.toJsonLd({ contextLoader });
|
3777
|
+
const keyCache = new KvKeyCache(this.federation.kv, this.federation.kvPrefixes.publicKey, this);
|
3778
|
+
const verified = await require_proof.verifyObject(require_actor.Activity, json, {
|
3779
|
+
contextLoader,
|
3780
|
+
documentLoader: options.documentLoader ?? this.documentLoader,
|
3781
|
+
tracerProvider: options.tracerProvider ?? this.tracerProvider,
|
3782
|
+
keyCache
|
3783
|
+
});
|
3784
|
+
if (verified == null) {
|
3785
|
+
logger$1.debug("Object Integrity Proofs are not verified.", {
|
3786
|
+
recipient,
|
3787
|
+
activity: json
|
3788
|
+
});
|
3789
|
+
if (activity.id == null) {
|
3790
|
+
logger$1.debug("Activity is missing an ID; unable to fetch.", {
|
3791
|
+
recipient,
|
3792
|
+
activity: json
|
3793
|
+
});
|
3794
|
+
return false;
|
3795
|
+
}
|
3796
|
+
const fetched = await this.lookupObject(activity.id, options);
|
3797
|
+
if (fetched == null) {
|
3798
|
+
logger$1.debug("Failed to fetch the remote activity object {activityId}.", {
|
3799
|
+
recipient,
|
3800
|
+
activity: json,
|
3801
|
+
activityId: activity.id.href
|
3802
|
+
});
|
3803
|
+
return false;
|
3804
|
+
} else if (!(fetched instanceof require_actor.Activity)) {
|
3805
|
+
logger$1.debug("Fetched object is not an Activity.", {
|
3806
|
+
recipient,
|
3807
|
+
activity: await fetched.toJsonLd({ contextLoader })
|
3808
|
+
});
|
3809
|
+
return false;
|
3810
|
+
} else if (fetched.id?.href !== activity.id.href) {
|
3811
|
+
logger$1.debug("Fetched activity object has a different ID; failed to verify.", {
|
3812
|
+
recipient,
|
3813
|
+
activity: await fetched.toJsonLd({ contextLoader })
|
3814
|
+
});
|
3815
|
+
return false;
|
3816
|
+
} else if (fetched.actorIds.length < 1) {
|
3817
|
+
logger$1.debug("Fetched activity object is missing an actor; unable to verify.", {
|
3818
|
+
recipient,
|
3819
|
+
activity: await fetched.toJsonLd({ contextLoader })
|
3820
|
+
});
|
3821
|
+
return false;
|
3822
|
+
}
|
3823
|
+
const activityId = fetched.id;
|
3824
|
+
if (!fetched.actorIds.every((actor) => actor.origin === activityId.origin)) {
|
3825
|
+
logger$1.debug("Fetched activity object has actors from different origins; unable to verify.", {
|
3826
|
+
recipient,
|
3827
|
+
activity: await fetched.toJsonLd({ contextLoader })
|
3828
|
+
});
|
3829
|
+
return false;
|
3830
|
+
}
|
3831
|
+
logger$1.debug("Successfully fetched the remote activity object {activityId}; ignore the original activity and use the fetched one, which is trustworthy.");
|
3832
|
+
activity = fetched;
|
3833
|
+
} else logger$1.debug("Object Integrity Proofs are verified.", {
|
3834
|
+
recipient,
|
3835
|
+
activity: json
|
3836
|
+
});
|
3837
|
+
const routeResult = await routeActivity({
|
3838
|
+
context: this,
|
3839
|
+
json,
|
3840
|
+
activity,
|
3841
|
+
recipient,
|
3842
|
+
inboxListeners: this.federation.inboxListeners,
|
3843
|
+
inboxContextFactory: this.toInboxContext.bind(this),
|
3844
|
+
inboxErrorHandler: this.federation.inboxErrorHandler,
|
3845
|
+
kv: this.federation.kv,
|
3846
|
+
kvPrefixes: this.federation.kvPrefixes,
|
3847
|
+
queue: this.federation.inboxQueue,
|
3848
|
+
span,
|
3849
|
+
tracerProvider: options.tracerProvider ?? this.tracerProvider
|
3850
|
+
});
|
3851
|
+
return routeResult === "alreadyProcessed" || routeResult === "enqueued" || routeResult === "unsupportedActivity" || routeResult === "success";
|
3852
|
+
}
|
3853
|
+
};
|
3854
|
+
var RequestContextImpl = class RequestContextImpl extends ContextImpl {
|
3855
|
+
#invokedFromActorDispatcher;
|
3856
|
+
#invokedFromObjectDispatcher;
|
3857
|
+
request;
|
3858
|
+
url = void 0;
|
3859
|
+
constructor(options) {
|
3860
|
+
super(options);
|
3861
|
+
this.#invokedFromActorDispatcher = options.invokedFromActorDispatcher;
|
3862
|
+
this.#invokedFromObjectDispatcher = options.invokedFromObjectDispatcher;
|
3863
|
+
this.request = options.request;
|
3864
|
+
this.url = options.url;
|
3865
|
+
}
|
3866
|
+
clone(data) {
|
3867
|
+
return new RequestContextImpl({
|
3868
|
+
url: this.url,
|
3869
|
+
federation: this.federation,
|
3870
|
+
data,
|
3871
|
+
documentLoader: this.documentLoader,
|
3872
|
+
contextLoader: this.contextLoader,
|
3873
|
+
invokedFromActorKeyPairsDispatcher: this.invokedFromActorKeyPairsDispatcher,
|
3874
|
+
invokedFromActorDispatcher: this.#invokedFromActorDispatcher,
|
3875
|
+
invokedFromObjectDispatcher: this.#invokedFromObjectDispatcher,
|
3876
|
+
request: this.request
|
3877
|
+
});
|
3878
|
+
}
|
3879
|
+
async getActor(identifier) {
|
3880
|
+
if (this.federation.actorCallbacks == null || this.federation.actorCallbacks.dispatcher == null) throw new Error("No actor dispatcher registered.");
|
3881
|
+
if (this.#invokedFromActorDispatcher != null) (0, __logtape_logtape.getLogger)([
|
3882
|
+
"fedify",
|
3883
|
+
"federation",
|
3884
|
+
"actor"
|
3885
|
+
]).warn("RequestContext.getActor({getActorIdentifier}) is invoked from the actor dispatcher ({actorDispatcherIdentifier}); this may cause an infinite loop.", {
|
3886
|
+
getActorIdentifier: identifier,
|
3887
|
+
actorDispatcherIdentifier: this.#invokedFromActorDispatcher.identifier
|
3888
|
+
});
|
3889
|
+
return await this.federation.actorCallbacks.dispatcher(new RequestContextImpl({
|
3890
|
+
...this,
|
3891
|
+
invokedFromActorDispatcher: { identifier }
|
3892
|
+
}), identifier);
|
3893
|
+
}
|
3894
|
+
async getObject(cls, values) {
|
3895
|
+
const callbacks = this.federation.objectCallbacks[cls.typeId.href];
|
3896
|
+
if (callbacks == null) throw new Error("No object dispatcher registered.");
|
3897
|
+
for (const param of callbacks.parameters) if (!(param in values)) throw new TypeError(`Missing parameter: ${param}`);
|
3898
|
+
if (this.#invokedFromObjectDispatcher != null) (0, __logtape_logtape.getLogger)(["fedify", "federation"]).warn("RequestContext.getObject({getObjectClass}, {getObjectValues}) is invoked from the object dispatcher ({actorDispatcherClass}, {actorDispatcherValues}); this may cause an infinite loop.", {
|
3899
|
+
getObjectClass: cls.name,
|
3900
|
+
getObjectValues: values,
|
3901
|
+
actorDispatcherClass: this.#invokedFromObjectDispatcher.cls.name,
|
3902
|
+
actorDispatcherValues: this.#invokedFromObjectDispatcher.values
|
3903
|
+
});
|
3904
|
+
return await callbacks.dispatcher(new RequestContextImpl({
|
3905
|
+
...this,
|
3906
|
+
invokedFromObjectDispatcher: {
|
3907
|
+
cls,
|
3908
|
+
values
|
3909
|
+
}
|
3910
|
+
}), values);
|
3911
|
+
}
|
3912
|
+
#signedKey = void 0;
|
3913
|
+
async getSignedKey(options = {}) {
|
3914
|
+
if (this.#signedKey != null) return this.#signedKey;
|
3915
|
+
return this.#signedKey = await require_http.verifyRequest(this.request, {
|
3916
|
+
...this,
|
3917
|
+
contextLoader: options.contextLoader ?? this.contextLoader,
|
3918
|
+
documentLoader: options.documentLoader ?? this.documentLoader,
|
3919
|
+
timeWindow: this.federation.signatureTimeWindow,
|
3920
|
+
tracerProvider: options.tracerProvider ?? this.tracerProvider
|
3921
|
+
});
|
3922
|
+
}
|
3923
|
+
#signedKeyOwner = void 0;
|
3924
|
+
async getSignedKeyOwner(options = {}) {
|
3925
|
+
if (this.#signedKeyOwner != null) return this.#signedKeyOwner;
|
3926
|
+
const key = await this.getSignedKey(options);
|
3927
|
+
if (key == null) return this.#signedKeyOwner = null;
|
3928
|
+
return this.#signedKeyOwner = await require_proof.getKeyOwner(key, {
|
3929
|
+
contextLoader: options.contextLoader ?? this.contextLoader,
|
3930
|
+
documentLoader: options.documentLoader ?? this.documentLoader,
|
3931
|
+
tracerProvider: options.tracerProvider ?? this.tracerProvider
|
3932
|
+
});
|
3933
|
+
}
|
3934
|
+
};
|
3935
|
+
var InboxContextImpl = class InboxContextImpl extends ContextImpl {
|
3936
|
+
recipient;
|
3937
|
+
activity;
|
3938
|
+
activityId;
|
3939
|
+
activityType;
|
3940
|
+
constructor(recipient, activity, activityId, activityType, options) {
|
3941
|
+
super(options);
|
3942
|
+
this.recipient = recipient;
|
3943
|
+
this.activity = activity;
|
3944
|
+
this.activityId = activityId;
|
3945
|
+
this.activityType = activityType;
|
3946
|
+
}
|
3947
|
+
clone(data) {
|
3948
|
+
return new InboxContextImpl(this.recipient, this.activity, this.activityId, this.activityType, {
|
3949
|
+
url: this.url,
|
3950
|
+
federation: this.federation,
|
3951
|
+
data,
|
3952
|
+
documentLoader: this.documentLoader,
|
3953
|
+
contextLoader: this.contextLoader,
|
3954
|
+
invokedFromActorKeyPairsDispatcher: this.invokedFromActorKeyPairsDispatcher
|
3955
|
+
});
|
3956
|
+
}
|
3957
|
+
forwardActivity(forwarder, recipients, options) {
|
3958
|
+
const tracer = this.tracerProvider.getTracer(require_docloader.deno_default.name, require_docloader.deno_default.version);
|
3959
|
+
return tracer.startActiveSpan("activitypub.outbox", {
|
3960
|
+
kind: this.federation.outboxQueue == null || options?.immediate ? __opentelemetry_api.SpanKind.CLIENT : __opentelemetry_api.SpanKind.PRODUCER,
|
3961
|
+
attributes: { "activitypub.activity.type": this.activityType }
|
3962
|
+
}, async (span) => {
|
3963
|
+
try {
|
3964
|
+
if (this.activityId != null) span.setAttribute("activitypub.activity.id", this.activityId);
|
3965
|
+
await this.forwardActivityInternal(forwarder, recipients, options);
|
3966
|
+
} catch (e) {
|
3967
|
+
span.setStatus({
|
3968
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
3969
|
+
message: String(e)
|
3970
|
+
});
|
3971
|
+
throw e;
|
3972
|
+
} finally {
|
3973
|
+
span.end();
|
3974
|
+
}
|
3975
|
+
});
|
3976
|
+
}
|
3977
|
+
async forwardActivityInternal(forwarder, recipients, options) {
|
3978
|
+
const logger$1 = (0, __logtape_logtape.getLogger)([
|
3979
|
+
"fedify",
|
3980
|
+
"federation",
|
3981
|
+
"inbox"
|
3982
|
+
]);
|
3983
|
+
let keys;
|
3984
|
+
let identifier = null;
|
3985
|
+
if ("identifier" in forwarder || "username" in forwarder || "handle" in forwarder) {
|
3986
|
+
if ("identifier" in forwarder) identifier = forwarder.identifier;
|
3987
|
+
else {
|
3988
|
+
let username;
|
3989
|
+
if ("username" in forwarder) username = forwarder.username;
|
3990
|
+
else {
|
3991
|
+
username = forwarder.handle;
|
3992
|
+
logger$1.warn("The \"handle\" property for the forwarder parameter is deprecated; use \"identifier\" or \"username\" instead.", { forwarder });
|
3993
|
+
}
|
3994
|
+
if (this.federation.actorCallbacks?.handleMapper == null) identifier = username;
|
3995
|
+
else {
|
3996
|
+
const mapped = await this.federation.actorCallbacks.handleMapper(this, username);
|
3997
|
+
if (mapped == null) throw new Error(`No actor found for the given username ${JSON.stringify(username)}.`);
|
3998
|
+
identifier = mapped;
|
3999
|
+
}
|
4000
|
+
}
|
4001
|
+
keys = await this.getKeyPairsFromIdentifier(identifier);
|
4002
|
+
if (keys.length < 1) throw new Error(`No key pair found for actor ${JSON.stringify(identifier)}.`);
|
4003
|
+
} else if (Array.isArray(forwarder)) {
|
4004
|
+
if (forwarder.length < 1) throw new Error("The forwarder's key pairs are empty.");
|
4005
|
+
keys = forwarder;
|
4006
|
+
} else keys = [forwarder];
|
4007
|
+
if (!require_proof.hasSignature(this.activity)) {
|
4008
|
+
let hasProof;
|
4009
|
+
try {
|
4010
|
+
const activity = await require_actor.Activity.fromJsonLd(this.activity, this);
|
4011
|
+
hasProof = await activity.getProof() != null;
|
4012
|
+
} catch {
|
4013
|
+
hasProof = false;
|
4014
|
+
}
|
4015
|
+
if (!hasProof) {
|
4016
|
+
if (options?.skipIfUnsigned) return;
|
4017
|
+
logger$1.warn("The received activity {activityId} is not signed; even if it is forwarded to other servers as is, it may not be accepted by them due to the lack of a signature/proof.");
|
4018
|
+
}
|
4019
|
+
}
|
4020
|
+
if (recipients === "followers") {
|
4021
|
+
if (identifier == null) throw new Error("If recipients is \"followers\", forwarder must be an actor identifier or username.");
|
4022
|
+
const followers = [];
|
4023
|
+
for await (const recipient of this.getFollowers(identifier)) followers.push(recipient);
|
4024
|
+
recipients = followers;
|
4025
|
+
}
|
4026
|
+
const inboxes = extractInboxes({
|
4027
|
+
recipients: Array.isArray(recipients) ? recipients : [recipients],
|
4028
|
+
preferSharedInbox: options?.preferSharedInbox,
|
4029
|
+
excludeBaseUris: options?.excludeBaseUris
|
4030
|
+
});
|
4031
|
+
logger$1.debug("Forwarding activity {activityId} to inboxes:\n{inboxes}", {
|
4032
|
+
inboxes: globalThis.Object.keys(inboxes),
|
4033
|
+
activityId: this.activityId,
|
4034
|
+
activity: this.activity
|
4035
|
+
});
|
4036
|
+
if (options?.immediate || this.federation.outboxQueue == null) {
|
4037
|
+
if (options?.immediate) logger$1.debug("Forwarding activity immediately without queue since immediate option is set.");
|
4038
|
+
else logger$1.debug("Forwarding activity immediately without queue since queue is not set.");
|
4039
|
+
const promises = [];
|
4040
|
+
for (const inbox in inboxes) promises.push(sendActivity({
|
4041
|
+
keys,
|
4042
|
+
activity: this.activity,
|
4043
|
+
activityId: this.activityId,
|
4044
|
+
activityType: this.activityType,
|
4045
|
+
inbox: new URL(inbox),
|
4046
|
+
sharedInbox: inboxes[inbox].sharedInbox,
|
4047
|
+
tracerProvider: this.tracerProvider,
|
4048
|
+
specDeterminer: new KvSpecDeterminer(this.federation.kv, this.federation.kvPrefixes.httpMessageSignaturesSpec, this.federation.firstKnock)
|
4049
|
+
}));
|
4050
|
+
await Promise.all(promises);
|
4051
|
+
return;
|
4052
|
+
}
|
4053
|
+
logger$1.debug("Enqueuing activity {activityId} to forward later.", {
|
4054
|
+
activityId: this.activityId,
|
4055
|
+
activity: this.activity
|
4056
|
+
});
|
4057
|
+
const keyJwkPairs = [];
|
4058
|
+
for (const { keyId, privateKey } of keys) {
|
4059
|
+
const privateKeyJwk = await require_key.exportJwk(privateKey);
|
4060
|
+
keyJwkPairs.push({
|
4061
|
+
keyId: keyId.href,
|
4062
|
+
privateKey: privateKeyJwk
|
4063
|
+
});
|
4064
|
+
}
|
4065
|
+
const carrier = {};
|
4066
|
+
__opentelemetry_api.propagation.inject(__opentelemetry_api.context.active(), carrier);
|
4067
|
+
const messages = [];
|
4068
|
+
for (const inbox in inboxes) {
|
4069
|
+
const message = {
|
4070
|
+
type: "outbox",
|
4071
|
+
id: crypto.randomUUID(),
|
4072
|
+
baseUrl: this.origin,
|
4073
|
+
keys: keyJwkPairs,
|
4074
|
+
activity: this.activity,
|
4075
|
+
activityId: this.activityId,
|
4076
|
+
activityType: this.activityType,
|
4077
|
+
inbox,
|
4078
|
+
sharedInbox: inboxes[inbox].sharedInbox,
|
4079
|
+
started: (/* @__PURE__ */ new Date()).toISOString(),
|
4080
|
+
attempt: 0,
|
4081
|
+
headers: {},
|
4082
|
+
traceContext: carrier
|
4083
|
+
};
|
4084
|
+
messages.push(message);
|
4085
|
+
}
|
4086
|
+
const { outboxQueue } = this.federation;
|
4087
|
+
if (outboxQueue.enqueueMany == null) {
|
4088
|
+
const promises = messages.map((m) => outboxQueue.enqueue(m));
|
4089
|
+
const results = await Promise.allSettled(promises);
|
4090
|
+
const errors = results.filter((r) => r.status === "rejected").map((r) => r.reason);
|
4091
|
+
if (errors.length > 0) {
|
4092
|
+
logger$1.error("Failed to enqueue activity {activityId} to forward later:\n{errors}", {
|
4093
|
+
activityId: this.activityId,
|
4094
|
+
errors
|
4095
|
+
});
|
4096
|
+
if (errors.length > 1) throw new AggregateError(errors, `Failed to enqueue activity ${this.activityId} to forward later.`);
|
4097
|
+
throw errors[0];
|
4098
|
+
}
|
4099
|
+
} else try {
|
4100
|
+
await outboxQueue.enqueueMany(messages);
|
4101
|
+
} catch (error) {
|
4102
|
+
logger$1.error("Failed to enqueue activity {activityId} to forward later:\n{error}", {
|
4103
|
+
activityId: this.activityId,
|
4104
|
+
error
|
4105
|
+
});
|
4106
|
+
throw error;
|
4107
|
+
}
|
4108
|
+
}
|
4109
|
+
};
|
4110
|
+
var KvSpecDeterminer = class {
|
4111
|
+
kv;
|
4112
|
+
prefix;
|
4113
|
+
defaultSpec;
|
4114
|
+
constructor(kv, prefix, defaultSpec = "rfc9421") {
|
4115
|
+
this.kv = kv;
|
4116
|
+
this.prefix = prefix;
|
4117
|
+
this.defaultSpec = defaultSpec;
|
4118
|
+
}
|
4119
|
+
async determineSpec(origin) {
|
4120
|
+
return await this.kv.get([...this.prefix, origin]) ?? this.defaultSpec;
|
4121
|
+
}
|
4122
|
+
async rememberSpec(origin, spec) {
|
4123
|
+
await this.kv.set([...this.prefix, origin], spec);
|
4124
|
+
}
|
4125
|
+
};
|
4126
|
+
function notFound(_request) {
|
4127
|
+
return new Response("Not Found", { status: 404 });
|
4128
|
+
}
|
4129
|
+
function notAcceptable(_request) {
|
4130
|
+
return new Response("Not Acceptable", {
|
4131
|
+
status: 406,
|
4132
|
+
headers: { Vary: "Accept, Signature" }
|
4133
|
+
});
|
4134
|
+
}
|
4135
|
+
function unauthorized(_request) {
|
4136
|
+
return new Response("Unauthorized", {
|
4137
|
+
status: 401,
|
4138
|
+
headers: { Vary: "Accept, Signature" }
|
4139
|
+
});
|
4140
|
+
}
|
4141
|
+
/**
|
4142
|
+
* Generates or extracts a unique identifier for a request.
|
4143
|
+
*
|
4144
|
+
* This function first attempts to extract an existing request ID from standard
|
4145
|
+
* tracing headers. If none exists, it generates a new one. The ID format is:
|
4146
|
+
*
|
4147
|
+
* - If from headers, uses the existing ID.
|
4148
|
+
* - If generated, uses format `req_` followed by a base36 timestamp and
|
4149
|
+
* 6 random chars.
|
4150
|
+
*
|
4151
|
+
* @param request The incoming HTTP request.
|
4152
|
+
* @returns A string identifier unique to this request.
|
4153
|
+
*/
|
4154
|
+
function getRequestId(request) {
|
4155
|
+
const traceId = request.headers.get("X-Request-Id") || request.headers.get("X-Correlation-Id") || request.headers.get("Traceparent")?.split("-")[1];
|
4156
|
+
if (traceId != null) return traceId;
|
4157
|
+
const timestamp = Date.now().toString(36);
|
4158
|
+
const random = Math.random().toString(36).slice(2, 8);
|
4159
|
+
return `req_${timestamp}${random}`;
|
4160
|
+
}
|
4161
|
+
|
4162
|
+
//#endregion
|
4163
|
+
Object.defineProperty(exports, 'ContextImpl', {
|
4164
|
+
enumerable: true,
|
4165
|
+
get: function () {
|
4166
|
+
return ContextImpl;
|
4167
|
+
}
|
4168
|
+
});
|
4169
|
+
Object.defineProperty(exports, 'FederationImpl', {
|
4170
|
+
enumerable: true,
|
4171
|
+
get: function () {
|
4172
|
+
return FederationImpl;
|
4173
|
+
}
|
4174
|
+
});
|
4175
|
+
Object.defineProperty(exports, 'InboxContextImpl', {
|
4176
|
+
enumerable: true,
|
4177
|
+
get: function () {
|
4178
|
+
return InboxContextImpl;
|
4179
|
+
}
|
4180
|
+
});
|
4181
|
+
Object.defineProperty(exports, 'KvSpecDeterminer', {
|
4182
|
+
enumerable: true,
|
4183
|
+
get: function () {
|
4184
|
+
return KvSpecDeterminer;
|
4185
|
+
}
|
4186
|
+
});
|
4187
|
+
Object.defineProperty(exports, 'Router', {
|
4188
|
+
enumerable: true,
|
4189
|
+
get: function () {
|
4190
|
+
return Router;
|
4191
|
+
}
|
4192
|
+
});
|
4193
|
+
Object.defineProperty(exports, 'RouterError', {
|
4194
|
+
enumerable: true,
|
4195
|
+
get: function () {
|
4196
|
+
return RouterError;
|
4197
|
+
}
|
4198
|
+
});
|
4199
|
+
Object.defineProperty(exports, 'buildCollectionSynchronizationHeader', {
|
4200
|
+
enumerable: true,
|
4201
|
+
get: function () {
|
4202
|
+
return buildCollectionSynchronizationHeader;
|
4203
|
+
}
|
4204
|
+
});
|
4205
|
+
Object.defineProperty(exports, 'createExponentialBackoffPolicy', {
|
4206
|
+
enumerable: true,
|
4207
|
+
get: function () {
|
4208
|
+
return createExponentialBackoffPolicy;
|
4209
|
+
}
|
4210
|
+
});
|
4211
|
+
Object.defineProperty(exports, 'createFederation', {
|
4212
|
+
enumerable: true,
|
4213
|
+
get: function () {
|
4214
|
+
return createFederation;
|
4215
|
+
}
|
4216
|
+
});
|
4217
|
+
Object.defineProperty(exports, 'createFederationBuilder', {
|
4218
|
+
enumerable: true,
|
4219
|
+
get: function () {
|
4220
|
+
return createFederationBuilder;
|
4221
|
+
}
|
4222
|
+
});
|
4223
|
+
Object.defineProperty(exports, 'digest', {
|
4224
|
+
enumerable: true,
|
4225
|
+
get: function () {
|
4226
|
+
return digest;
|
4227
|
+
}
|
4228
|
+
});
|
4229
|
+
Object.defineProperty(exports, 'respondWithObject', {
|
4230
|
+
enumerable: true,
|
4231
|
+
get: function () {
|
4232
|
+
return respondWithObject;
|
4233
|
+
}
|
4234
|
+
});
|
4235
|
+
Object.defineProperty(exports, 'respondWithObjectIfAcceptable', {
|
4236
|
+
enumerable: true,
|
4237
|
+
get: function () {
|
4238
|
+
return respondWithObjectIfAcceptable;
|
4239
|
+
}
|
4240
|
+
});
|