@fedify/fedify 1.3.0-dev.571 → 1.3.0-dev.576
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/CHANGES.md +4 -0
- package/esm/deno.js +1 -1
- package/esm/federation/handler.js +155 -59
- package/esm/federation/inbox.js +5 -2
- package/esm/federation/middleware.js +243 -92
- package/esm/federation/send.js +42 -7
- package/esm/vocab/vocab.js +173 -173
- package/package.json +1 -1
- package/types/federation/handler.d.ts +2 -2
- package/types/federation/handler.d.ts.map +1 -1
- package/types/federation/inbox.d.ts +4 -0
- package/types/federation/inbox.d.ts.map +1 -1
- package/types/federation/middleware.d.ts +14 -4
- package/types/federation/middleware.d.ts.map +1 -1
- package/types/federation/mod.d.ts +1 -1
- package/types/federation/mod.d.ts.map +1 -1
- package/types/federation/queue.d.ts +4 -0
- package/types/federation/queue.d.ts.map +1 -1
- package/types/federation/send.d.ts +16 -3
- package/types/federation/send.d.ts.map +1 -1
- package/types/sig/mod.d.ts +1 -1
- package/types/sig/mod.d.ts.map +1 -1
package/CHANGES.md
CHANGED
@@ -59,6 +59,9 @@ To be released.
|
|
59
59
|
|
60
60
|
- Added `getTypeId()` function.
|
61
61
|
|
62
|
+
- `Context.sendActivity()` and `InboxContext.forwardActivity()` methods now
|
63
|
+
reject when they fail to enqueue the task. [[#192]]
|
64
|
+
|
62
65
|
- Fedify now supports OpenTelemetry for tracing. [[#170]]
|
63
66
|
|
64
67
|
- Added `Context.tracerProvider` property.
|
@@ -103,6 +106,7 @@ To be released.
|
|
103
106
|
[#173]: https://github.com/dahlia/fedify/issues/173
|
104
107
|
[#183]: https://github.com/dahlia/fedify/pull/183
|
105
108
|
[#186]: https://github.com/dahlia/fedify/pull/186
|
109
|
+
[#192]: https://github.com/dahlia/fedify/issues/192
|
106
110
|
|
107
111
|
|
108
112
|
Version 1.2.8
|
package/esm/deno.js
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
import * as dntShim from "../_dnt.shims.js";
|
2
2
|
import { getLogger } from "@logtape/logtape";
|
3
|
-
import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
3
|
+
import { context, propagation, SpanKind, SpanStatusCode, trace, } from "@opentelemetry/api";
|
4
4
|
import { accepts } from "../deps/jsr.io/@std/http/1.0.11/negotiation.js";
|
5
5
|
import metadata from "../deno.js";
|
6
6
|
import { verifyRequest } from "../sig/http.js";
|
7
7
|
import { detachSignature, verifyJsonLd } from "../sig/ld.js";
|
8
8
|
import { doesActorOwnKey } from "../sig/owner.js";
|
9
9
|
import { verifyObject } from "../sig/proof.js";
|
10
|
+
import { getTypeId } from "../vocab/type.js";
|
10
11
|
import { Activity, CryptographicKey, Link, Multikey, Object, OrderedCollection, OrderedCollectionPage, } from "../vocab/vocab.js";
|
11
12
|
export function acceptsJsonLd(request) {
|
12
13
|
const types = accepts(request);
|
@@ -230,21 +231,55 @@ function filterCollectionItems(items, collectionName, filterPredicate) {
|
|
230
231
|
}
|
231
232
|
return result;
|
232
233
|
}
|
233
|
-
export async function handleInbox(request,
|
234
|
+
export async function handleInbox(request, options) {
|
235
|
+
const tracerProvider = options.tracerProvider ?? trace.getTracerProvider();
|
236
|
+
const tracer = tracerProvider.getTracer(metadata.name, metadata.version);
|
237
|
+
return await tracer.startActiveSpan("activitypub.inbox", {
|
238
|
+
kind: options.queue == null ? SpanKind.SERVER : SpanKind.PRODUCER,
|
239
|
+
attributes: { "activitypub.shared_inbox": options.recipient == null },
|
240
|
+
}, async (span) => {
|
241
|
+
if (options.recipient != null) {
|
242
|
+
span.setAttribute("fedify.inbox.recipient", options.recipient);
|
243
|
+
}
|
244
|
+
try {
|
245
|
+
return await handleInboxInternal(request, options, span);
|
246
|
+
}
|
247
|
+
catch (e) {
|
248
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: String(e) });
|
249
|
+
throw e;
|
250
|
+
}
|
251
|
+
finally {
|
252
|
+
span.end();
|
253
|
+
}
|
254
|
+
});
|
255
|
+
}
|
256
|
+
async function handleInboxInternal(request, { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, queue, actorDispatcher, inboxListeners, inboxErrorHandler, onNotFound, signatureTimeWindow, skipSignatureVerification, tracerProvider, }, span) {
|
234
257
|
const logger = getLogger(["fedify", "federation", "inbox"]);
|
235
258
|
if (actorDispatcher == null) {
|
236
259
|
logger.error("Actor dispatcher is not set.", { recipient });
|
260
|
+
span.setStatus({
|
261
|
+
code: SpanStatusCode.ERROR,
|
262
|
+
message: "Actor dispatcher is not set.",
|
263
|
+
});
|
237
264
|
return await onNotFound(request);
|
238
265
|
}
|
239
266
|
else if (recipient != null) {
|
240
|
-
const actor = await actorDispatcher(
|
267
|
+
const actor = await actorDispatcher(ctx, recipient);
|
241
268
|
if (actor == null) {
|
242
269
|
logger.error("Actor {recipient} not found.", { recipient });
|
270
|
+
span.setStatus({
|
271
|
+
code: SpanStatusCode.ERROR,
|
272
|
+
message: `Actor ${recipient} not found.`,
|
273
|
+
});
|
243
274
|
return await onNotFound(request);
|
244
275
|
}
|
245
276
|
}
|
246
277
|
if (request.bodyUsed) {
|
247
278
|
logger.error("Request body has already been read.", { recipient });
|
279
|
+
span.setStatus({
|
280
|
+
code: SpanStatusCode.ERROR,
|
281
|
+
message: "Request body has already been read.",
|
282
|
+
});
|
248
283
|
return new Response("Internal server error.", {
|
249
284
|
status: 500,
|
250
285
|
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
@@ -252,6 +287,10 @@ export async function handleInbox(request, { recipient, context, inboxContextFac
|
|
252
287
|
}
|
253
288
|
else if (request.body?.locked) {
|
254
289
|
logger.error("Request body is locked.", { recipient });
|
290
|
+
span.setStatus({
|
291
|
+
code: SpanStatusCode.ERROR,
|
292
|
+
message: "Request body is locked.",
|
293
|
+
});
|
255
294
|
return new Response("Internal server error.", {
|
256
295
|
status: 500,
|
257
296
|
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
@@ -264,11 +303,15 @@ export async function handleInbox(request, { recipient, context, inboxContextFac
|
|
264
303
|
catch (error) {
|
265
304
|
logger.error("Failed to parse JSON:\n{error}", { recipient, error });
|
266
305
|
try {
|
267
|
-
await inboxErrorHandler?.(
|
306
|
+
await inboxErrorHandler?.(ctx, error);
|
268
307
|
}
|
269
308
|
catch (error) {
|
270
309
|
logger.error("An unexpected error occurred in inbox error handler:\n{error}", { error, activity: json, recipient });
|
271
310
|
}
|
311
|
+
span.setStatus({
|
312
|
+
code: SpanStatusCode.ERROR,
|
313
|
+
message: `Failed to parse JSON:\n${error}`,
|
314
|
+
});
|
272
315
|
return new Response("Invalid JSON.", {
|
273
316
|
status: 400,
|
274
317
|
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
@@ -287,7 +330,7 @@ export async function handleInbox(request, { recipient, context, inboxContextFac
|
|
287
330
|
return undefined;
|
288
331
|
let object;
|
289
332
|
try {
|
290
|
-
object = await Object.fromJsonLd(serialized,
|
333
|
+
object = await Object.fromJsonLd(serialized, ctx);
|
291
334
|
}
|
292
335
|
catch {
|
293
336
|
await kv.delete([...kvPrefixes.publicKey, keyId.href]);
|
@@ -306,13 +349,13 @@ export async function handleInbox(request, { recipient, context, inboxContextFac
|
|
306
349
|
return;
|
307
350
|
}
|
308
351
|
this.nullKeys.delete(keyId.href);
|
309
|
-
const serialized = await key.toJsonLd(
|
352
|
+
const serialized = await key.toJsonLd(ctx);
|
310
353
|
await kv.set([...kvPrefixes.publicKey, keyId.href], serialized);
|
311
354
|
},
|
312
355
|
};
|
313
356
|
const ldSigVerified = await verifyJsonLd(json, {
|
314
|
-
contextLoader:
|
315
|
-
documentLoader:
|
357
|
+
contextLoader: ctx.contextLoader,
|
358
|
+
documentLoader: ctx.documentLoader,
|
316
359
|
keyCache,
|
317
360
|
tracerProvider,
|
318
361
|
});
|
@@ -320,14 +363,14 @@ export async function handleInbox(request, { recipient, context, inboxContextFac
|
|
320
363
|
let activity = null;
|
321
364
|
if (ldSigVerified) {
|
322
365
|
logger.debug("Linked Data Signatures are verified.", { recipient, json });
|
323
|
-
activity = await Activity.fromJsonLd(jsonWithoutSig,
|
366
|
+
activity = await Activity.fromJsonLd(jsonWithoutSig, ctx);
|
324
367
|
}
|
325
368
|
else {
|
326
369
|
logger.debug("Linked Data Signatures are not verified.", { recipient, json });
|
327
370
|
try {
|
328
371
|
activity = await verifyObject(Activity, jsonWithoutSig, {
|
329
|
-
contextLoader:
|
330
|
-
documentLoader:
|
372
|
+
contextLoader: ctx.contextLoader,
|
373
|
+
documentLoader: ctx.documentLoader,
|
331
374
|
keyCache,
|
332
375
|
tracerProvider,
|
333
376
|
});
|
@@ -339,11 +382,15 @@ export async function handleInbox(request, { recipient, context, inboxContextFac
|
|
339
382
|
error,
|
340
383
|
});
|
341
384
|
try {
|
342
|
-
await inboxErrorHandler?.(
|
385
|
+
await inboxErrorHandler?.(ctx, error);
|
343
386
|
}
|
344
387
|
catch (error) {
|
345
388
|
logger.error("An unexpected error occurred in inbox error handler:\n{error}", { error, activity: json, recipient });
|
346
389
|
}
|
390
|
+
span.setStatus({
|
391
|
+
code: SpanStatusCode.ERROR,
|
392
|
+
message: `Failed to parse activity:\n${error}`,
|
393
|
+
});
|
347
394
|
return new Response("Invalid activity.", {
|
348
395
|
status: 400,
|
349
396
|
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
@@ -360,14 +407,18 @@ export async function handleInbox(request, { recipient, context, inboxContextFac
|
|
360
407
|
if (activity == null) {
|
361
408
|
if (!skipSignatureVerification) {
|
362
409
|
const key = await verifyRequest(request, {
|
363
|
-
contextLoader:
|
364
|
-
documentLoader:
|
410
|
+
contextLoader: ctx.contextLoader,
|
411
|
+
documentLoader: ctx.documentLoader,
|
365
412
|
timeWindow: signatureTimeWindow,
|
366
413
|
keyCache,
|
367
414
|
tracerProvider,
|
368
415
|
});
|
369
416
|
if (key == null) {
|
370
417
|
logger.error("Failed to verify the request's HTTP Signatures.", { recipient });
|
418
|
+
span.setStatus({
|
419
|
+
code: SpanStatusCode.ERROR,
|
420
|
+
message: `Failed to verify the request's HTTP Signatures.`,
|
421
|
+
});
|
371
422
|
const response = new Response("Failed to verify the request signature.", {
|
372
423
|
status: 401,
|
373
424
|
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
@@ -379,8 +430,12 @@ export async function handleInbox(request, { recipient, context, inboxContextFac
|
|
379
430
|
}
|
380
431
|
httpSigKey = key;
|
381
432
|
}
|
382
|
-
activity = await Activity.fromJsonLd(jsonWithoutSig,
|
433
|
+
activity = await Activity.fromJsonLd(jsonWithoutSig, ctx);
|
434
|
+
}
|
435
|
+
if (activity.id != null) {
|
436
|
+
span.setAttribute("activitypub.activity.id", activity.id.href);
|
383
437
|
}
|
438
|
+
span.setAttribute("activitypub.activity.type", getTypeId(activity).href);
|
384
439
|
const cacheKey = activity.id == null
|
385
440
|
? null
|
386
441
|
: [...kvPrefixes.activityIdempotence, activity.id.href];
|
@@ -392,6 +447,10 @@ export async function handleInbox(request, { recipient, context, inboxContextFac
|
|
392
447
|
activity: json,
|
393
448
|
recipient,
|
394
449
|
});
|
450
|
+
span.setStatus({
|
451
|
+
code: SpanStatusCode.UNSET,
|
452
|
+
message: `Activity ${activity.id?.href} has already been processed.`,
|
453
|
+
});
|
395
454
|
return new Response(`Activity <${activity.id}> has already been processed.`, {
|
396
455
|
status: 202,
|
397
456
|
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
@@ -400,83 +459,120 @@ export async function handleInbox(request, { recipient, context, inboxContextFac
|
|
400
459
|
}
|
401
460
|
if (activity.actorId == null) {
|
402
461
|
logger.error("Missing actor.", { activity: json });
|
403
|
-
|
462
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: "Missing actor." });
|
463
|
+
return new Response("Missing actor.", {
|
404
464
|
status: 400,
|
405
465
|
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
406
466
|
});
|
407
|
-
return response;
|
408
467
|
}
|
409
|
-
|
468
|
+
span.setAttribute("activitypub.actor.id", activity.actorId.href);
|
469
|
+
if (httpSigKey != null && !await doesActorOwnKey(activity, httpSigKey, ctx)) {
|
410
470
|
logger.error("The signer ({keyId}) and the actor ({actorId}) do not match.", {
|
411
471
|
activity: json,
|
412
472
|
recipient,
|
413
473
|
keyId: httpSigKey.id?.href,
|
414
474
|
actorId: activity.actorId.href,
|
415
475
|
});
|
416
|
-
|
476
|
+
span.setStatus({
|
477
|
+
code: SpanStatusCode.ERROR,
|
478
|
+
message: `The signer (${httpSigKey.id?.href}) and ` +
|
479
|
+
`the actor (${activity.actorId.href}) do not match.`,
|
480
|
+
});
|
481
|
+
return new Response("The signer and the actor do not match.", {
|
417
482
|
status: 401,
|
418
483
|
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
419
484
|
});
|
420
|
-
return response;
|
421
485
|
}
|
422
486
|
if (queue != null) {
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
487
|
+
const carrier = {};
|
488
|
+
propagation.inject(context.active(), carrier);
|
489
|
+
try {
|
490
|
+
await queue.enqueue({
|
491
|
+
type: "inbox",
|
492
|
+
id: dntShim.crypto.randomUUID(),
|
493
|
+
baseUrl: request.url,
|
494
|
+
activity: json,
|
495
|
+
identifier: recipient,
|
496
|
+
attempt: 0,
|
497
|
+
started: new Date().toISOString(),
|
498
|
+
traceContext: carrier,
|
499
|
+
});
|
500
|
+
}
|
501
|
+
catch (error) {
|
502
|
+
logger.error("Failed to enqueue the incoming activity {activityId}:\n{error}", { error, activityId: activity.id?.href, activity: json, recipient });
|
503
|
+
span.setStatus({
|
504
|
+
code: SpanStatusCode.ERROR,
|
505
|
+
message: `Failed to enqueue the incoming activity ${activity.id?.href}.`,
|
506
|
+
});
|
507
|
+
throw error;
|
508
|
+
}
|
432
509
|
logger.info("Activity {activityId} is enqueued.", { activityId: activity.id?.href, activity: json, recipient });
|
433
510
|
return new Response("Activity is enqueued.", {
|
434
511
|
status: 202,
|
435
512
|
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
436
513
|
});
|
437
514
|
}
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
515
|
+
tracerProvider = tracerProvider ?? trace.getTracerProvider();
|
516
|
+
const tracer = tracerProvider.getTracer(metadata.name, metadata.version);
|
517
|
+
const response = await tracer.startActiveSpan("activitypub.dispatch_inbox_listener", { kind: SpanKind.INTERNAL }, async (span) => {
|
518
|
+
const dispatched = inboxListeners?.dispatchWithClass(activity);
|
519
|
+
if (dispatched == null) {
|
520
|
+
logger.error("Unsupported activity type:\n{activity}", { activity: json, recipient });
|
521
|
+
span.setStatus({
|
522
|
+
code: SpanStatusCode.UNSET,
|
523
|
+
message: `Unsupported activity type: ${getTypeId(activity).href}`,
|
524
|
+
});
|
525
|
+
span.end();
|
526
|
+
return new Response("", {
|
527
|
+
status: 202,
|
528
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
529
|
+
});
|
530
|
+
}
|
531
|
+
const { class: cls, listener } = dispatched;
|
532
|
+
span.updateName(`activitypub.dispatch_inbox_listener ${cls.name}`);
|
450
533
|
try {
|
451
|
-
await
|
534
|
+
await listener(inboxContextFactory(recipient, json, activity?.id?.href, getTypeId(activity).href), activity);
|
452
535
|
}
|
453
536
|
catch (error) {
|
454
|
-
|
537
|
+
try {
|
538
|
+
await inboxErrorHandler?.(ctx, error);
|
539
|
+
}
|
540
|
+
catch (error) {
|
541
|
+
logger.error("An unexpected error occurred in inbox error handler:\n{error}", {
|
542
|
+
error,
|
543
|
+
activityId: activity.id?.href,
|
544
|
+
activity: json,
|
545
|
+
recipient,
|
546
|
+
});
|
547
|
+
}
|
548
|
+
logger.error("Failed to process the incoming activity {activityId}:\n{error}", {
|
455
549
|
error,
|
456
550
|
activityId: activity.id?.href,
|
457
551
|
activity: json,
|
458
552
|
recipient,
|
459
553
|
});
|
554
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
|
555
|
+
span.end();
|
556
|
+
return new Response("Internal server error.", {
|
557
|
+
status: 500,
|
558
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
559
|
+
});
|
460
560
|
}
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
});
|
467
|
-
|
468
|
-
|
561
|
+
if (cacheKey != null) {
|
562
|
+
await kv.set(cacheKey, true, {
|
563
|
+
ttl: dntShim.Temporal.Duration.from({ days: 1 }),
|
564
|
+
});
|
565
|
+
}
|
566
|
+
logger.info("Activity {activityId} has been processed.", { activityId: activity.id?.href, activity: json, recipient });
|
567
|
+
span.end();
|
568
|
+
return new Response("", {
|
569
|
+
status: 202,
|
469
570
|
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
470
571
|
});
|
471
|
-
}
|
472
|
-
if (cacheKey != null) {
|
473
|
-
await kv.set(cacheKey, true, { ttl: dntShim.Temporal.Duration.from({ days: 1 }) });
|
474
|
-
}
|
475
|
-
logger.info("Activity {activityId} has been processed.", { activityId: activity.id?.href, activity: json, recipient });
|
476
|
-
return new Response("", {
|
477
|
-
status: 202,
|
478
|
-
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
479
572
|
});
|
573
|
+
if (response.status >= 500)
|
574
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
575
|
+
return response;
|
480
576
|
}
|
481
577
|
/**
|
482
578
|
* Responds with the given object in JSON-LD format.
|
package/esm/federation/inbox.js
CHANGED
@@ -12,7 +12,7 @@ export class InboxListenerSet {
|
|
12
12
|
}
|
13
13
|
this.#listeners.set(type, listener);
|
14
14
|
}
|
15
|
-
|
15
|
+
dispatchWithClass(activity) {
|
16
16
|
// deno-lint-ignore no-explicit-any
|
17
17
|
let cls = activity
|
18
18
|
// deno-lint-ignore no-explicit-any
|
@@ -29,6 +29,9 @@ export class InboxListenerSet {
|
|
29
29
|
cls = globalThis.Object.getPrototypeOf(cls);
|
30
30
|
}
|
31
31
|
const listener = inboxListeners.get(cls);
|
32
|
-
return listener;
|
32
|
+
return { class: cls, listener };
|
33
|
+
}
|
34
|
+
dispatch(activity) {
|
35
|
+
return this.dispatchWithClass(activity)?.listener ?? null;
|
33
36
|
}
|
34
37
|
}
|