@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 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,6 +1,6 @@
1
1
  export default {
2
2
  "name": "@fedify/fedify",
3
- "version": "1.3.0-dev.571+be4ca8c6",
3
+ "version": "1.3.0-dev.576+5eedba62",
4
4
  "license": "MIT",
5
5
  "exports": {
6
6
  ".": "./mod.ts",
@@ -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, { recipient, context, inboxContextFactory, kv, kvPrefixes, queue, actorDispatcher, inboxListeners, inboxErrorHandler, onNotFound, signatureTimeWindow, skipSignatureVerification, tracerProvider, }) {
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(context, recipient);
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?.(context, error);
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, context);
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(context);
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: context.contextLoader,
315
- documentLoader: context.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, context);
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: context.contextLoader,
330
- documentLoader: context.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?.(context, error);
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: context.contextLoader,
364
- documentLoader: context.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, context);
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
- const response = new Response("Missing actor.", {
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
- if (httpSigKey != null && !await doesActorOwnKey(activity, httpSigKey, context)) {
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
- const response = new Response("The signer and the actor do not match.", {
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
- await queue.enqueue({
424
- type: "inbox",
425
- id: dntShim.crypto.randomUUID(),
426
- baseUrl: request.url,
427
- activity: json,
428
- identifier: recipient,
429
- attempt: 0,
430
- started: new Date().toISOString(),
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
- const listener = inboxListeners?.dispatch(activity);
439
- if (listener == null) {
440
- logger.error("Unsupported activity type:\n{activity}", { activity: json, recipient });
441
- return new Response("", {
442
- status: 202,
443
- headers: { "Content-Type": "text/plain; charset=utf-8" },
444
- });
445
- }
446
- try {
447
- await listener(inboxContextFactory(recipient, json), activity);
448
- }
449
- catch (error) {
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 inboxErrorHandler?.(context, error);
534
+ await listener(inboxContextFactory(recipient, json, activity?.id?.href, getTypeId(activity).href), activity);
452
535
  }
453
536
  catch (error) {
454
- logger.error("An unexpected error occurred in inbox error handler:\n{error}", {
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
- logger.error("Failed to process the incoming activity {activityId}:\n{error}", {
462
- error,
463
- activityId: activity.id?.href,
464
- activity: json,
465
- recipient,
466
- });
467
- return new Response("Internal server error.", {
468
- status: 500,
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.
@@ -12,7 +12,7 @@ export class InboxListenerSet {
12
12
  }
13
13
  this.#listeners.set(type, listener);
14
14
  }
15
- dispatch(activity) {
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
  }