@fedify/fedify 1.3.0-dev.577 → 1.3.0
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 +12 -1
- package/esm/deno.js +2 -2
- package/esm/federation/handler.js +48 -152
- package/esm/federation/inbox.js +108 -0
- package/esm/federation/keycache.js +42 -0
- package/esm/federation/middleware.js +115 -1
- package/esm/testing/fixtures/example.com/create +6 -0
- package/esm/testing/fixtures/example.com/cross-origin-actor +6 -0
- package/esm/testing/fixtures/example.com/invite +7 -0
- package/esm/vocab/vocab.js +5342 -2713
- package/package.json +1 -1
- package/types/federation/context.d.ts +47 -0
- package/types/federation/context.d.ts.map +1 -1
- package/types/federation/handler.d.ts +1 -1
- package/types/federation/handler.d.ts.map +1 -1
- package/types/federation/inbox.d.ts +23 -1
- package/types/federation/inbox.d.ts.map +1 -1
- package/types/federation/keycache.d.ts +19 -0
- package/types/federation/keycache.d.ts.map +1 -0
- package/types/federation/middleware.d.ts +3 -1
- package/types/federation/middleware.d.ts.map +1 -1
- package/types/vocab/vocab.d.ts +18 -38
- package/types/vocab/vocab.d.ts.map +1 -1
package/CHANGES.md
CHANGED
@@ -6,7 +6,7 @@ Fedify changelog
|
|
6
6
|
Version 1.3.0
|
7
7
|
-------------
|
8
8
|
|
9
|
-
|
9
|
+
Released on November 30, 2024.
|
10
10
|
|
11
11
|
- `MessageQueue`s now can be differently configured for incoming and outgoing
|
12
12
|
activities.
|
@@ -62,6 +62,16 @@ To be released.
|
|
62
62
|
- `Context.sendActivity()` and `InboxContext.forwardActivity()` methods now
|
63
63
|
reject when they fail to enqueue the task. [[#192]]
|
64
64
|
|
65
|
+
- Fedify now allows you to manually route an `Activity` to the corresponding
|
66
|
+
inbox listener. [[#193]]
|
67
|
+
|
68
|
+
- Added `Context.routeActivity()` method.
|
69
|
+
- Added `RouteActivityOptions` interface.
|
70
|
+
|
71
|
+
- `Object.toJsonLd()` without any `format` option now returns its original
|
72
|
+
JSON-LD object even if it not created from `Object.fromJsonLd()` but it is
|
73
|
+
returned from another `Object`'s `get*()` method.
|
74
|
+
|
65
75
|
- Fedify now supports OpenTelemetry for tracing. [[#170]]
|
66
76
|
|
67
77
|
- Added `Context.tracerProvider` property.
|
@@ -107,6 +117,7 @@ To be released.
|
|
107
117
|
[#183]: https://github.com/dahlia/fedify/pull/183
|
108
118
|
[#186]: https://github.com/dahlia/fedify/pull/186
|
109
119
|
[#192]: https://github.com/dahlia/fedify/issues/192
|
120
|
+
[#193]: https://github.com/dahlia/fedify/issues/193
|
110
121
|
|
111
122
|
|
112
123
|
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
|
3
|
+
"version": "1.3.0",
|
4
4
|
"license": "MIT",
|
5
5
|
"exports": {
|
6
6
|
".": "./mod.ts",
|
@@ -82,7 +82,7 @@ export default {
|
|
82
82
|
"cache": "deno task codegen && deno cache mod.ts",
|
83
83
|
"check": "deno task codegen && deno fmt --check && deno lint && deno check */*.ts",
|
84
84
|
"codegen": "deno run --allow-read --allow-write --check codegen/main.ts vocab/ ../runtime/ > vocab/vocab.ts && deno fmt vocab/vocab.ts && deno cache vocab/vocab.ts && deno check vocab/vocab.ts",
|
85
|
-
"test-without-codegen": "deno test --check --doc --allow-read --allow-write --allow-env --unstable-kv --trace-leaks",
|
85
|
+
"test-without-codegen": "deno test --check --doc --allow-read --allow-write --allow-env --unstable-kv --trace-leaks --parallel",
|
86
86
|
"test": "deno task codegen && deno task test-without-codegen",
|
87
87
|
"coverage": "rm -rf coverage/ && deno task test --coverage && deno coverage --html coverage",
|
88
88
|
"bench": "deno task codegen && deno bench --allow-read --allow-write --allow-net --allow-env --allow-run --unstable-kv",
|
@@ -1,6 +1,5 @@
|
|
1
|
-
import * as dntShim from "../_dnt.shims.js";
|
2
1
|
import { getLogger } from "@logtape/logtape";
|
3
|
-
import {
|
2
|
+
import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
4
3
|
import { accepts } from "../deps/jsr.io/@std/http/1.0.11/negotiation.js";
|
5
4
|
import metadata from "../deno.js";
|
6
5
|
import { verifyRequest } from "../sig/http.js";
|
@@ -8,7 +7,9 @@ import { detachSignature, verifyJsonLd } from "../sig/ld.js";
|
|
8
7
|
import { doesActorOwnKey } from "../sig/owner.js";
|
9
8
|
import { verifyObject } from "../sig/proof.js";
|
10
9
|
import { getTypeId } from "../vocab/type.js";
|
11
|
-
import { Activity,
|
10
|
+
import { Activity, Link, Object, OrderedCollection, OrderedCollectionPage, } from "../vocab/vocab.js";
|
11
|
+
import { routeActivity } from "./inbox.js";
|
12
|
+
import { KvKeyCache } from "./keycache.js";
|
12
13
|
export function acceptsJsonLd(request) {
|
13
14
|
const types = accepts(request);
|
14
15
|
if (types == null)
|
@@ -317,42 +318,7 @@ async function handleInboxInternal(request, { recipient, context: ctx, inboxCont
|
|
317
318
|
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
318
319
|
});
|
319
320
|
}
|
320
|
-
const keyCache =
|
321
|
-
nullKeys: new Set(),
|
322
|
-
async get(keyId) {
|
323
|
-
if (this.nullKeys.has(keyId.href))
|
324
|
-
return null;
|
325
|
-
const serialized = await kv.get([
|
326
|
-
...kvPrefixes.publicKey,
|
327
|
-
keyId.href,
|
328
|
-
]);
|
329
|
-
if (serialized == null)
|
330
|
-
return undefined;
|
331
|
-
let object;
|
332
|
-
try {
|
333
|
-
object = await Object.fromJsonLd(serialized, ctx);
|
334
|
-
}
|
335
|
-
catch {
|
336
|
-
await kv.delete([...kvPrefixes.publicKey, keyId.href]);
|
337
|
-
return undefined;
|
338
|
-
}
|
339
|
-
if (object instanceof CryptographicKey || object instanceof Multikey) {
|
340
|
-
return object;
|
341
|
-
}
|
342
|
-
await kv.delete([...kvPrefixes.publicKey, keyId.href]);
|
343
|
-
return undefined;
|
344
|
-
},
|
345
|
-
async set(keyId, key) {
|
346
|
-
if (key == null) {
|
347
|
-
this.nullKeys.add(keyId.href);
|
348
|
-
await kv.delete([...kvPrefixes.publicKey, keyId.href]);
|
349
|
-
return;
|
350
|
-
}
|
351
|
-
this.nullKeys.delete(keyId.href);
|
352
|
-
const serialized = await key.toJsonLd(ctx);
|
353
|
-
await kv.set([...kvPrefixes.publicKey, keyId.href], serialized);
|
354
|
-
},
|
355
|
-
};
|
321
|
+
const keyCache = new KvKeyCache(kv, kvPrefixes.publicKey, ctx);
|
356
322
|
const ldSigVerified = await verifyJsonLd(json, {
|
357
323
|
contextLoader: ctx.contextLoader,
|
358
324
|
documentLoader: ctx.documentLoader,
|
@@ -436,143 +402,73 @@ async function handleInboxInternal(request, { recipient, context: ctx, inboxCont
|
|
436
402
|
span.setAttribute("activitypub.activity.id", activity.id.href);
|
437
403
|
}
|
438
404
|
span.setAttribute("activitypub.activity.type", getTypeId(activity).href);
|
439
|
-
const
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
});
|
454
|
-
return new Response(`Activity <${activity.id}> has already been processed.`, {
|
455
|
-
status: 202,
|
456
|
-
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
457
|
-
});
|
458
|
-
}
|
459
|
-
}
|
460
|
-
if (activity.actorId == null) {
|
461
|
-
logger.error("Missing actor.", { activity: json });
|
462
|
-
span.setStatus({ code: SpanStatusCode.ERROR, message: "Missing actor." });
|
463
|
-
return new Response("Missing actor.", {
|
464
|
-
status: 400,
|
465
|
-
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
466
|
-
});
|
467
|
-
}
|
468
|
-
span.setAttribute("activitypub.actor.id", activity.actorId.href);
|
405
|
+
const routeResult = await routeActivity({
|
406
|
+
context: ctx,
|
407
|
+
json,
|
408
|
+
activity,
|
409
|
+
recipient,
|
410
|
+
inboxListeners,
|
411
|
+
inboxContextFactory,
|
412
|
+
inboxErrorHandler,
|
413
|
+
kv,
|
414
|
+
kvPrefixes,
|
415
|
+
queue,
|
416
|
+
span,
|
417
|
+
tracerProvider,
|
418
|
+
});
|
469
419
|
if (httpSigKey != null && !await doesActorOwnKey(activity, httpSigKey, ctx)) {
|
470
420
|
logger.error("The signer ({keyId}) and the actor ({actorId}) do not match.", {
|
471
421
|
activity: json,
|
472
422
|
recipient,
|
473
423
|
keyId: httpSigKey.id?.href,
|
474
|
-
actorId: activity.actorId
|
424
|
+
actorId: activity.actorId?.href,
|
475
425
|
});
|
476
426
|
span.setStatus({
|
477
427
|
code: SpanStatusCode.ERROR,
|
478
428
|
message: `The signer (${httpSigKey.id?.href}) and ` +
|
479
|
-
`the actor (${activity.actorId
|
429
|
+
`the actor (${activity.actorId?.href}) do not match.`,
|
480
430
|
});
|
481
431
|
return new Response("The signer and the actor do not match.", {
|
482
432
|
status: 401,
|
483
433
|
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
484
434
|
});
|
485
435
|
}
|
486
|
-
if (
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
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
|
-
}
|
509
|
-
logger.info("Activity {activityId} is enqueued.", { activityId: activity.id?.href, activity: json, recipient });
|
436
|
+
if (routeResult === "alreadyProcessed") {
|
437
|
+
return new Response(`Activity <${activity.id}> has already been processed.`, {
|
438
|
+
status: 202,
|
439
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
440
|
+
});
|
441
|
+
}
|
442
|
+
else if (routeResult === "missingActor") {
|
443
|
+
return new Response("Missing actor.", {
|
444
|
+
status: 400,
|
445
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
446
|
+
});
|
447
|
+
}
|
448
|
+
else if (routeResult === "enqueued") {
|
510
449
|
return new Response("Activity is enqueued.", {
|
511
450
|
status: 202,
|
512
451
|
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
513
452
|
});
|
514
453
|
}
|
515
|
-
|
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}`);
|
533
|
-
try {
|
534
|
-
await listener(inboxContextFactory(recipient, json, activity?.id?.href, getTypeId(activity).href), activity);
|
535
|
-
}
|
536
|
-
catch (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}", {
|
549
|
-
error,
|
550
|
-
activityId: activity.id?.href,
|
551
|
-
activity: json,
|
552
|
-
recipient,
|
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
|
-
});
|
560
|
-
}
|
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();
|
454
|
+
else if (routeResult === "unsupportedActivity") {
|
568
455
|
return new Response("", {
|
569
456
|
status: 202,
|
570
457
|
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
571
458
|
});
|
572
|
-
}
|
573
|
-
if (
|
574
|
-
|
575
|
-
|
459
|
+
}
|
460
|
+
else if (routeResult === "error") {
|
461
|
+
return new Response("Internal server error.", {
|
462
|
+
status: 500,
|
463
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
464
|
+
});
|
465
|
+
}
|
466
|
+
else {
|
467
|
+
return new Response("", {
|
468
|
+
status: 202,
|
469
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
470
|
+
});
|
471
|
+
}
|
576
472
|
}
|
577
473
|
/**
|
578
474
|
* Responds with the given object in JSON-LD format.
|
package/esm/federation/inbox.js
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
import * as dntShim from "../_dnt.shims.js";
|
2
|
+
import { getLogger } from "@logtape/logtape";
|
3
|
+
import { context, propagation, SpanKind, SpanStatusCode, trace, } from "@opentelemetry/api";
|
4
|
+
import metadata from "../deno.js";
|
5
|
+
import { getTypeId } from "../vocab/type.js";
|
1
6
|
import { Activity } from "../vocab/vocab.js";
|
2
7
|
export class InboxListenerSet {
|
3
8
|
#listeners;
|
@@ -35,3 +40,106 @@ export class InboxListenerSet {
|
|
35
40
|
return this.dispatchWithClass(activity)?.listener ?? null;
|
36
41
|
}
|
37
42
|
}
|
43
|
+
export async function routeActivity({ context: ctx, json, activity, recipient, inboxListeners, inboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, tracerProvider, }) {
|
44
|
+
const logger = getLogger(["fedify", "federation", "inbox"]);
|
45
|
+
const cacheKey = activity.id == null ? null : [
|
46
|
+
...kvPrefixes.activityIdempotence,
|
47
|
+
activity.id.href,
|
48
|
+
];
|
49
|
+
if (cacheKey != null) {
|
50
|
+
const cached = await kv.get(cacheKey);
|
51
|
+
if (cached === true) {
|
52
|
+
logger.debug("Activity {activityId} has already been processed.", {
|
53
|
+
activityId: activity.id?.href,
|
54
|
+
activity: json,
|
55
|
+
recipient,
|
56
|
+
});
|
57
|
+
span.setStatus({
|
58
|
+
code: SpanStatusCode.UNSET,
|
59
|
+
message: `Activity ${activity.id?.href} has already been processed.`,
|
60
|
+
});
|
61
|
+
return "alreadyProcessed";
|
62
|
+
}
|
63
|
+
}
|
64
|
+
if (activity.actorId == null) {
|
65
|
+
logger.error("Missing actor.", { activity: json });
|
66
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: "Missing actor." });
|
67
|
+
return "missingActor";
|
68
|
+
}
|
69
|
+
span.setAttribute("activitypub.actor.id", activity.actorId.href);
|
70
|
+
if (queue != null) {
|
71
|
+
const carrier = {};
|
72
|
+
propagation.inject(context.active(), carrier);
|
73
|
+
try {
|
74
|
+
await queue.enqueue({
|
75
|
+
type: "inbox",
|
76
|
+
id: dntShim.crypto.randomUUID(),
|
77
|
+
baseUrl: ctx.origin,
|
78
|
+
activity: json,
|
79
|
+
identifier: recipient,
|
80
|
+
attempt: 0,
|
81
|
+
started: new Date().toISOString(),
|
82
|
+
traceContext: carrier,
|
83
|
+
});
|
84
|
+
}
|
85
|
+
catch (error) {
|
86
|
+
logger.error("Failed to enqueue the incoming activity {activityId}:\n{error}", { error, activityId: activity.id?.href, activity: json, recipient });
|
87
|
+
span.setStatus({
|
88
|
+
code: SpanStatusCode.ERROR,
|
89
|
+
message: `Failed to enqueue the incoming activity ${activity.id?.href}.`,
|
90
|
+
});
|
91
|
+
throw error;
|
92
|
+
}
|
93
|
+
logger.info("Activity {activityId} is enqueued.", { activityId: activity.id?.href, activity: json, recipient });
|
94
|
+
return "enqueued";
|
95
|
+
}
|
96
|
+
tracerProvider = tracerProvider ?? trace.getTracerProvider();
|
97
|
+
const tracer = tracerProvider.getTracer(metadata.name, metadata.version);
|
98
|
+
return await tracer.startActiveSpan("activitypub.dispatch_inbox_listener", { kind: SpanKind.INTERNAL }, async (span) => {
|
99
|
+
const dispatched = inboxListeners?.dispatchWithClass(activity);
|
100
|
+
if (dispatched == null) {
|
101
|
+
logger.error("Unsupported activity type:\n{activity}", { activity: json, recipient });
|
102
|
+
span.setStatus({
|
103
|
+
code: SpanStatusCode.UNSET,
|
104
|
+
message: `Unsupported activity type: ${getTypeId(activity).href}`,
|
105
|
+
});
|
106
|
+
span.end();
|
107
|
+
return "unsupportedActivity";
|
108
|
+
}
|
109
|
+
const { class: cls, listener } = dispatched;
|
110
|
+
span.updateName(`activitypub.dispatch_inbox_listener ${cls.name}`);
|
111
|
+
try {
|
112
|
+
await listener(inboxContextFactory(recipient, json, activity?.id?.href, getTypeId(activity).href), activity);
|
113
|
+
}
|
114
|
+
catch (error) {
|
115
|
+
try {
|
116
|
+
await inboxErrorHandler?.(ctx, error);
|
117
|
+
}
|
118
|
+
catch (error) {
|
119
|
+
logger.error("An unexpected error occurred in inbox error handler:\n{error}", {
|
120
|
+
error,
|
121
|
+
activityId: activity.id?.href,
|
122
|
+
activity: json,
|
123
|
+
recipient,
|
124
|
+
});
|
125
|
+
}
|
126
|
+
logger.error("Failed to process the incoming activity {activityId}:\n{error}", {
|
127
|
+
error,
|
128
|
+
activityId: activity.id?.href,
|
129
|
+
activity: json,
|
130
|
+
recipient,
|
131
|
+
});
|
132
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
|
133
|
+
span.end();
|
134
|
+
return "error";
|
135
|
+
}
|
136
|
+
if (cacheKey != null) {
|
137
|
+
await kv.set(cacheKey, true, {
|
138
|
+
ttl: dntShim.Temporal.Duration.from({ days: 1 }),
|
139
|
+
});
|
140
|
+
}
|
141
|
+
logger.info("Activity {activityId} has been processed.", { activityId: activity.id?.href, activity: json, recipient });
|
142
|
+
span.end();
|
143
|
+
return "success";
|
144
|
+
});
|
145
|
+
}
|
@@ -0,0 +1,42 @@
|
|
1
|
+
import { CryptographicKey, Multikey } from "../vocab/vocab.js";
|
2
|
+
export class KvKeyCache {
|
3
|
+
kv;
|
4
|
+
prefix;
|
5
|
+
options;
|
6
|
+
nullKeys;
|
7
|
+
constructor(kv, prefix, options = {}) {
|
8
|
+
this.kv = kv;
|
9
|
+
this.prefix = prefix;
|
10
|
+
this.nullKeys = new Set();
|
11
|
+
this.options = options;
|
12
|
+
}
|
13
|
+
async get(keyId) {
|
14
|
+
if (this.nullKeys.has(keyId.href))
|
15
|
+
return null;
|
16
|
+
const serialized = await this.kv.get([...this.prefix, keyId.href]);
|
17
|
+
if (serialized == null)
|
18
|
+
return undefined;
|
19
|
+
try {
|
20
|
+
return await CryptographicKey.fromJsonLd(serialized, this.options);
|
21
|
+
}
|
22
|
+
catch {
|
23
|
+
try {
|
24
|
+
return await Multikey.fromJsonLd(serialized, this.options);
|
25
|
+
}
|
26
|
+
catch {
|
27
|
+
await this.kv.delete([...this.prefix, keyId.href]);
|
28
|
+
return undefined;
|
29
|
+
}
|
30
|
+
}
|
31
|
+
}
|
32
|
+
async set(keyId, key) {
|
33
|
+
if (key == null) {
|
34
|
+
this.nullKeys.add(keyId.href);
|
35
|
+
await this.kv.delete([...this.prefix, keyId.href]);
|
36
|
+
return;
|
37
|
+
}
|
38
|
+
this.nullKeys.delete(keyId.href);
|
39
|
+
const serialized = await key.toJsonLd(this.options);
|
40
|
+
await this.kv.set([...this.prefix, keyId.href], serialized);
|
41
|
+
}
|
42
|
+
}
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import * as dntShim from "../_dnt.shims.js";
|
2
|
+
import { verifyObject } from "../mod.js";
|
2
3
|
import { getLogger, withContext } from "@logtape/logtape";
|
3
4
|
import { context, propagation, SpanKind, SpanStatusCode, trace, } from "@opentelemetry/api";
|
4
5
|
import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_HEADER, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_URL_FULL, } from "@opentelemetry/semantic-conventions";
|
@@ -16,7 +17,8 @@ import { Activity, CryptographicKey, Multikey, } from "../vocab/vocab.js";
|
|
16
17
|
import { handleWebFinger } from "../webfinger/handler.js";
|
17
18
|
import { buildCollectionSynchronizationHeader } from "./collection.js";
|
18
19
|
import { handleActor, handleCollection, handleInbox, handleObject, } from "./handler.js";
|
19
|
-
import { InboxListenerSet } from "./inbox.js";
|
20
|
+
import { InboxListenerSet, routeActivity } from "./inbox.js";
|
21
|
+
import { KvKeyCache } from "./keycache.js";
|
20
22
|
import { createExponentialBackoffPolicy } from "./retry.js";
|
21
23
|
import { Router, RouterError } from "./router.js";
|
22
24
|
import { extractInboxes, sendActivity } from "./send.js";
|
@@ -1946,6 +1948,118 @@ export class ContextImpl {
|
|
1946
1948
|
cursor = result.nextCursor ?? null;
|
1947
1949
|
}
|
1948
1950
|
}
|
1951
|
+
routeActivity(recipient, activity, options = {}) {
|
1952
|
+
const tracerProvider = this.tracerProvider ?? this.tracerProvider;
|
1953
|
+
const tracer = tracerProvider.getTracer(metadata.name, metadata.version);
|
1954
|
+
return tracer.startActiveSpan("activitypub.inbox", {
|
1955
|
+
kind: this.federation.inboxQueue == null || options.immediate
|
1956
|
+
? SpanKind.INTERNAL
|
1957
|
+
: SpanKind.PRODUCER,
|
1958
|
+
attributes: {
|
1959
|
+
"activitypub.activity.type": getTypeId(activity).href,
|
1960
|
+
},
|
1961
|
+
}, async (span) => {
|
1962
|
+
if (activity.id != null) {
|
1963
|
+
span.setAttribute("activitypub.activity.id", activity.id.href);
|
1964
|
+
}
|
1965
|
+
if (activity.toIds.length > 0) {
|
1966
|
+
span.setAttribute("activitypub.activity.to", activity.toIds.map((to) => to.href));
|
1967
|
+
}
|
1968
|
+
if (activity.ccIds.length > 0) {
|
1969
|
+
span.setAttribute("activitypub.activity.cc", activity.ccIds.map((cc) => cc.href));
|
1970
|
+
}
|
1971
|
+
if (activity.btoIds.length > 0) {
|
1972
|
+
span.setAttribute("activitypub.activity.bto", activity.btoIds.map((bto) => bto.href));
|
1973
|
+
}
|
1974
|
+
if (activity.bccIds.length > 0) {
|
1975
|
+
span.setAttribute("activitypub.activity.bcc", activity.bccIds.map((bcc) => bcc.href));
|
1976
|
+
}
|
1977
|
+
try {
|
1978
|
+
const ok = await this.routeActivityInternal(recipient, activity, options, span);
|
1979
|
+
if (ok) {
|
1980
|
+
span.setAttribute("activitypub.shared_inbox", recipient == null);
|
1981
|
+
if (recipient != null) {
|
1982
|
+
span.setAttribute("fedify.inbox.recipient", recipient);
|
1983
|
+
}
|
1984
|
+
}
|
1985
|
+
else {
|
1986
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
1987
|
+
}
|
1988
|
+
return ok;
|
1989
|
+
}
|
1990
|
+
catch (e) {
|
1991
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: String(e) });
|
1992
|
+
throw e;
|
1993
|
+
}
|
1994
|
+
finally {
|
1995
|
+
span.end();
|
1996
|
+
}
|
1997
|
+
});
|
1998
|
+
}
|
1999
|
+
async routeActivityInternal(recipient, activity, options = {}, span) {
|
2000
|
+
const logger = getLogger(["fedify", "federation", "inbox"]);
|
2001
|
+
const contextLoader = options.contextLoader ?? this.contextLoader;
|
2002
|
+
const json = await activity.toJsonLd({ contextLoader });
|
2003
|
+
const keyCache = new KvKeyCache(this.federation.kv, this.federation.kvPrefixes.publicKey, this);
|
2004
|
+
const verified = await verifyObject(Activity, json, {
|
2005
|
+
contextLoader,
|
2006
|
+
documentLoader: options.documentLoader ?? this.documentLoader,
|
2007
|
+
tracerProvider: options.tracerProvider ?? this.tracerProvider,
|
2008
|
+
keyCache,
|
2009
|
+
});
|
2010
|
+
if (verified == null) {
|
2011
|
+
logger.debug("Object Integrity Proofs are not verified.", { recipient, activity: json });
|
2012
|
+
if (activity.id == null) {
|
2013
|
+
logger.debug("Activity is missing an ID; unable to fetch.", { recipient, activity: json });
|
2014
|
+
return false;
|
2015
|
+
}
|
2016
|
+
const fetched = await this.lookupObject(activity.id, options);
|
2017
|
+
if (fetched == null) {
|
2018
|
+
logger.debug("Failed to fetch the remote activity object {activityId}.", { recipient, activity: json, activityId: activity.id.href });
|
2019
|
+
return false;
|
2020
|
+
}
|
2021
|
+
else if (!(fetched instanceof Activity)) {
|
2022
|
+
logger.debug("Fetched object is not an Activity.", { recipient, activity: await fetched.toJsonLd({ contextLoader }) });
|
2023
|
+
return false;
|
2024
|
+
}
|
2025
|
+
else if (fetched.id?.href !== activity.id.href) {
|
2026
|
+
logger.debug("Fetched activity object has a different ID; failed to verify.", { recipient, activity: await fetched.toJsonLd({ contextLoader }) });
|
2027
|
+
return false;
|
2028
|
+
}
|
2029
|
+
else if (fetched.actorIds.length < 1) {
|
2030
|
+
logger.debug("Fetched activity object is missing an actor; unable to verify.", { recipient, activity: await fetched.toJsonLd({ contextLoader }) });
|
2031
|
+
return false;
|
2032
|
+
}
|
2033
|
+
const activityId = fetched.id;
|
2034
|
+
if (!fetched.actorIds.every((actor) => actor.origin === activityId.origin)) {
|
2035
|
+
logger.debug("Fetched activity object has actors from different origins; " +
|
2036
|
+
"unable to verify.", { recipient, activity: await fetched.toJsonLd({ contextLoader }) });
|
2037
|
+
return false;
|
2038
|
+
}
|
2039
|
+
logger.debug("Successfully fetched the remote activity object {activityId}; " +
|
2040
|
+
"ignore the original activity and use the fetched one, which is trustworthy.");
|
2041
|
+
activity = fetched;
|
2042
|
+
}
|
2043
|
+
else {
|
2044
|
+
logger.debug("Object Integrity Proofs are verified.", { recipient, activity: json });
|
2045
|
+
}
|
2046
|
+
const routeResult = await routeActivity({
|
2047
|
+
context: this,
|
2048
|
+
json,
|
2049
|
+
activity,
|
2050
|
+
recipient,
|
2051
|
+
inboxListeners: this.federation.inboxListeners,
|
2052
|
+
inboxContextFactory: this.toInboxContext.bind(this),
|
2053
|
+
inboxErrorHandler: this.federation.inboxErrorHandler,
|
2054
|
+
kv: this.federation.kv,
|
2055
|
+
kvPrefixes: this.federation.kvPrefixes,
|
2056
|
+
queue: this.federation.inboxQueue,
|
2057
|
+
span,
|
2058
|
+
tracerProvider: options.tracerProvider ?? this.tracerProvider,
|
2059
|
+
});
|
2060
|
+
return routeResult === "alreadyProcessed" || routeResult === "enqueued" ||
|
2061
|
+
routeResult === "unsupportedActivity" || routeResult === "success";
|
2062
|
+
}
|
1949
2063
|
}
|
1950
2064
|
class RequestContextImpl extends ContextImpl {
|
1951
2065
|
#invokedFromActorDispatcher;
|