@sdxc/activitypub 0.0.0-pre.1

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.
Files changed (73) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +711 -0
  3. package/dist/collections.d.ts +67 -0
  4. package/dist/collections.js +38 -0
  5. package/dist/conformance.d.ts +80 -0
  6. package/dist/conformance.js +313 -0
  7. package/dist/discovery.d.ts +46 -0
  8. package/dist/discovery.js +105 -0
  9. package/dist/errors.d.ts +115 -0
  10. package/dist/errors.js +124 -0
  11. package/dist/federation.d.ts +281 -0
  12. package/dist/federation.js +513 -0
  13. package/dist/fixtures/gotosocial.d.ts +114 -0
  14. package/dist/fixtures/gotosocial.js +115 -0
  15. package/dist/fixtures/index.d.ts +15 -0
  16. package/dist/fixtures/index.js +50 -0
  17. package/dist/fixtures/mastodon.d.ts +489 -0
  18. package/dist/fixtures/mastodon.js +334 -0
  19. package/dist/fixtures/misskey.d.ts +254 -0
  20. package/dist/fixtures/misskey.js +163 -0
  21. package/dist/fixtures/pixelfed.d.ts +118 -0
  22. package/dist/fixtures/pixelfed.js +126 -0
  23. package/dist/fixtures/shared.d.ts +14 -0
  24. package/dist/fixtures/shared.js +14 -0
  25. package/dist/fixtures/threads.d.ts +82 -0
  26. package/dist/fixtures/threads.js +88 -0
  27. package/dist/index.d.ts +33 -0
  28. package/dist/index.js +22 -0
  29. package/dist/keys.d.ts +100 -0
  30. package/dist/keys.js +147 -0
  31. package/dist/lib/availability.d.ts +37 -0
  32. package/dist/lib/availability.js +52 -0
  33. package/dist/lib/compact.d.ts +145 -0
  34. package/dist/lib/compact.js +514 -0
  35. package/dist/lib/constants.d.ts +35 -0
  36. package/dist/lib/constants.js +65 -0
  37. package/dist/lib/delivery-fan-out.d.ts +72 -0
  38. package/dist/lib/delivery-fan-out.js +202 -0
  39. package/dist/lib/delivery-post.d.ts +108 -0
  40. package/dist/lib/delivery-post.js +318 -0
  41. package/dist/lib/fetch.d.ts +64 -0
  42. package/dist/lib/fetch.js +103 -0
  43. package/dist/lib/inbox-error.d.ts +45 -0
  44. package/dist/lib/inbox-error.js +74 -0
  45. package/dist/lib/inbox-handle.d.ts +119 -0
  46. package/dist/lib/inbox-handle.js +587 -0
  47. package/dist/lib/inbox-receive.d.ts +117 -0
  48. package/dist/lib/inbox-receive.js +233 -0
  49. package/dist/lib/inbox-signature.d.ts +61 -0
  50. package/dist/lib/inbox-signature.js +156 -0
  51. package/dist/lib/inbox-summarize.d.ts +41 -0
  52. package/dist/lib/inbox-summarize.js +97 -0
  53. package/dist/lib/messages.d.ts +36 -0
  54. package/dist/lib/messages.js +35 -0
  55. package/dist/lib/parse.d.ts +42 -0
  56. package/dist/lib/parse.js +348 -0
  57. package/dist/lib/response.d.ts +45 -0
  58. package/dist/lib/response.js +59 -0
  59. package/dist/lib/stringify.d.ts +51 -0
  60. package/dist/lib/stringify.js +122 -0
  61. package/dist/lib/tombstone.d.ts +24 -0
  62. package/dist/lib/tombstone.js +15 -0
  63. package/dist/lib/types.d.ts +249 -0
  64. package/dist/lib/types.js +0 -0
  65. package/dist/memory.d.ts +77 -0
  66. package/dist/memory.js +146 -0
  67. package/dist/remote.d.ts +105 -0
  68. package/dist/remote.js +262 -0
  69. package/dist/store.d.ts +78 -0
  70. package/dist/store.js +8 -0
  71. package/dist/testing.d.ts +12 -0
  72. package/dist/testing.js +10 -0
  73. package/package.json +54 -0
@@ -0,0 +1,587 @@
1
+ import { currentLog } from "@sdxc/logger";
2
+ import { failure, isFailure, success } from "@sdxc/result";
3
+ import { ActivityPubError } from "../errors.js";
4
+ import { parseActivity } from "./parse.js";
5
+ import { stringify } from "./stringify.js";
6
+ /** Long enough to absorb a sender's quick redeliveries; handlers stay idempotent past it. */
7
+ const DEFAULT_SEEN_TTL = "1 day";
8
+ /**
9
+ * Processes a verified activity. It claims the id in `seen` (a store failure is logged and
10
+ * processing goes on, since every step is idempotent), re-reads the activity, does the work
11
+ * the protocol requires for its type, then calls the app's handler for it.
12
+ *
13
+ * The sender is the document whose id is the activity's actor, and an object another
14
+ * origin embeds is fetched from its own origin (FEP-c7d3). A failure is retryable when a
15
+ * remote server, a store, a handler or `send` failed transiently; any other is acknowledged.
16
+ *
17
+ * @param input - What `receive` answered, as the job read it back from the queue.
18
+ * @param options - The local actor, the stores, the resolver, `send` and the handlers.
19
+ * @returns What happened to the activity.
20
+ * @example
21
+ * let outcome = await handle(ctx.input, { actor, keys, followers, seen, objects, resolver, blocked, send, on });
22
+ * if (isFailure(outcome) && outcome.error.retryable) return ctx.retry({ cause: outcome.error });
23
+ */
24
+ export async function handle(input, options) {
25
+ let parsed = parseActivity(input.activity);
26
+ if (isFailure(parsed))
27
+ return parsed;
28
+ let activity = parsed.data;
29
+ if ((options.attempts ?? 1) <= 1) {
30
+ let claimed = await options.seen.claim(activity.id, options.seenTtl ?? DEFAULT_SEEN_TTL);
31
+ if (isFailure(claimed)) {
32
+ currentLog()?.warn("activitypub.inbox.seen_unavailable", {
33
+ id: activity.id,
34
+ error: claimed.error.message,
35
+ });
36
+ }
37
+ else if (!claimed.data) {
38
+ return success(outcome("duplicate", activity, null));
39
+ }
40
+ }
41
+ let objectId = idOf(activity.object);
42
+ let deletesActor = activity.type === "Delete" && objectId === activity.actor;
43
+ let run = new Run(input, activity, options);
44
+ let actor = await options.resolver.actor(activity.actor);
45
+ if (isFailure(actor) || actor.data.id !== activity.actor) {
46
+ if (deletesActor)
47
+ return run.deleteActor(null);
48
+ if (isFailure(actor) && actor.error.retryable)
49
+ return failure(actor.error);
50
+ return success(outcome("ignored", activity, "actor-unavailable"));
51
+ }
52
+ let host = URL.parse(activity.actor)?.hostname ?? "";
53
+ if (await options.blocked(host)) {
54
+ if (activity.type === "Follow")
55
+ return run.follow(actor.data, true);
56
+ return success(outcome("ignored", activity, "blocked"));
57
+ }
58
+ switch (activity.type) {
59
+ case "Follow":
60
+ return run.follow(actor.data, false);
61
+ case "Undo":
62
+ return run.undo(actor.data);
63
+ case "Create":
64
+ return run.post(actor.data, "create");
65
+ case "Update":
66
+ if (objectId === activity.actor)
67
+ return run.updateActor();
68
+ return run.post(actor.data, "update");
69
+ case "Delete":
70
+ if (deletesActor)
71
+ return run.deleteActor(actor.data);
72
+ return run.deleteObject(actor.data);
73
+ case "Like":
74
+ case "EmojiReact":
75
+ return run.reaction(actor.data, "like");
76
+ case "Announce":
77
+ return run.reaction(actor.data, "announce");
78
+ case "Accept":
79
+ case "Reject":
80
+ return run.answer(actor.data, activity.type === "Accept" ? "accept" : "reject");
81
+ case "Move":
82
+ return run.move();
83
+ default:
84
+ currentLog()?.note("activitypub.inbox.unhandled", { id: activity.id, type: activity.type });
85
+ return success(outcome("ignored", activity, "unhandled"));
86
+ }
87
+ }
88
+ /** One activity being processed, with the steps each type runs. */
89
+ class Run {
90
+ #input;
91
+ #activity;
92
+ #options;
93
+ #local;
94
+ /**
95
+ * @param input - The job input.
96
+ * @param activity - The activity read from it.
97
+ * @param options - What `handle` was given.
98
+ */
99
+ constructor(input, activity, options) {
100
+ this.#input = input;
101
+ this.#activity = activity;
102
+ this.#options = options;
103
+ this.#local = options.actor;
104
+ }
105
+ /**
106
+ * A Follow of the local actor: the follower is stored, and an `Accept` embedding the
107
+ * Follow goes to the follower's own inbox, again on a repeated Follow, since that means
108
+ * the first never arrived. A blocked server, or the app's `reject`, gets a `Reject`.
109
+ *
110
+ * @param actor - The follower.
111
+ * @param blocked - Whether the follower's server is blocked.
112
+ */
113
+ async follow(actor, blocked) {
114
+ let activity = this.#activity;
115
+ if (idOf(activity.object) !== this.#local.id)
116
+ return this.#ignored("not-addressed");
117
+ let follow = { id: activity.id, type: "Follow", actor: actor.id, object: this.#local.id };
118
+ if (blocked)
119
+ return this.#refuse(actor, follow, "blocked");
120
+ let existing = store(await this.#options.followers.get(this.#local.id, actor.id));
121
+ if (isFailure(existing))
122
+ return existing;
123
+ let decision = this.#local.manuallyApprovesFollowers ? "pending" : "accept";
124
+ let answered = await settle(this.#options.on?.follow?.(this.#inbound(actor, this.#local, this.#local.id)));
125
+ if (isFailure(answered))
126
+ return answered;
127
+ decision = answered.data ?? decision;
128
+ if (decision === "pending" && existing.data?.state === "accepted")
129
+ decision = "accept";
130
+ if (decision === "reject")
131
+ return this.#refuse(actor, follow, "rejected");
132
+ let put = store(await this.#options.followers.put({
133
+ actor: this.#local.id,
134
+ id: actor.id,
135
+ inbox: actor.inbox,
136
+ sharedInbox: actor.endpoints.sharedInbox,
137
+ followId: activity.id,
138
+ state: decision === "accept" ? "accepted" : "pending",
139
+ }));
140
+ if (isFailure(put))
141
+ return put;
142
+ if (decision === "pending")
143
+ return this.#processed("pending");
144
+ let sent = await this.#send(answerTo("Accept", this.#local.id, follow), actor.inbox);
145
+ if (isFailure(sent))
146
+ return sent;
147
+ return this.#processed(null);
148
+ }
149
+ /**
150
+ * An Undo. A Follow is removed when its id is the stored `followId` or the embedded
151
+ * Follow is the sender's own; anything else must be an activity by the sender, embedded
152
+ * from its origin or fetched from there.
153
+ *
154
+ * @param actor - The sender.
155
+ */
156
+ async undo(actor) {
157
+ let activity = this.#activity;
158
+ let undoneId = idOf(activity.object);
159
+ if (undoneId === null)
160
+ return this.#ignored("unverifiable");
161
+ let undone = this.#trustedEmbed();
162
+ let reverted = undone !== null && isActivity(undone) ? undone : null;
163
+ if (reverted === null || reverted.type === "Follow") {
164
+ let existing = store(await this.#options.followers.get(this.#local.id, actor.id));
165
+ if (isFailure(existing))
166
+ return existing;
167
+ let named = existing.data !== null &&
168
+ (existing.data.followId === undoneId ||
169
+ (reverted !== null &&
170
+ reverted.actor === actor.id &&
171
+ idOf(reverted.object) === this.#local.id));
172
+ if (named) {
173
+ let removed = store(await this.#options.followers.remove(this.#local.id, actor.id));
174
+ if (isFailure(removed))
175
+ return removed;
176
+ return this.#call(this.#options.on?.undo?.(this.#inbound(actor, reverted, this.#local.id)));
177
+ }
178
+ if (reverted !== null)
179
+ return this.#ignored("unrelated");
180
+ }
181
+ if (reverted === null) {
182
+ let fetched = await this.#options.resolver.object(undoneId);
183
+ if (isFailure(fetched))
184
+ return this.#unreachable(fetched.error, "unverifiable");
185
+ if (!isActivity(fetched.data))
186
+ return this.#ignored("unverifiable");
187
+ reverted = fetched.data;
188
+ }
189
+ if (reverted.actor !== actor.id)
190
+ return this.#ignored("not-owner");
191
+ let target = idOf(reverted.object);
192
+ return this.#call(this.#options.on?.undo?.(this.#inbound(actor, reverted, target)));
193
+ }
194
+ /**
195
+ * A Create or Update of a post. The post must be the sender's, attributed to it and on
196
+ * its origin, and must reply to, quote, or mention something local.
197
+ *
198
+ * @param actor - The sender.
199
+ * @param kind - Which handler it reaches.
200
+ */
201
+ async post(actor, kind) {
202
+ let object = await this.#dereference();
203
+ if (isFailure(object))
204
+ return object;
205
+ if (object.data === null)
206
+ return this.#ignored("unverifiable");
207
+ let post = object.data;
208
+ if (!post.attributedTo.includes(actor.id) || originOf(post.id) !== originOf(actor.id)) {
209
+ return this.#ignored("not-owner");
210
+ }
211
+ let target = await this.#relevance(post);
212
+ if (isFailure(target))
213
+ return target;
214
+ if (target.data === null)
215
+ return this.#ignored("unrelated");
216
+ let inbound = this.#inbound(actor, post, target.data);
217
+ return this.#call(this.#options.on?.[kind]?.(inbound));
218
+ }
219
+ /**
220
+ * An Update of the sender's own actor: its cached document is dropped and refetched,
221
+ * and a stored follower picks up a changed inbox or shared inbox.
222
+ */
223
+ async updateActor() {
224
+ let id = this.#activity.actor;
225
+ await this.#options.resolver.evict(id);
226
+ let fresh = await this.#options.resolver.actor(id, { fresh: true });
227
+ if (isFailure(fresh))
228
+ return this.#unreachable(fresh.error, "actor-unavailable");
229
+ let actor = fresh.data;
230
+ let existing = store(await this.#options.followers.get(this.#local.id, id));
231
+ if (isFailure(existing))
232
+ return existing;
233
+ if (existing.data !== null) {
234
+ let put = store(await this.#options.followers.put({
235
+ ...existing.data,
236
+ inbox: actor.inbox,
237
+ sharedInbox: actor.endpoints.sharedInbox,
238
+ }));
239
+ if (isFailure(put))
240
+ return put;
241
+ }
242
+ return this.#call(this.#options.on?.update?.(this.#inbound(actor, actor, null)));
243
+ }
244
+ /**
245
+ * A deleted account: it stops following the local actor and its cached document is
246
+ * dropped.
247
+ *
248
+ * @param actor - The account's document, when it could still be fetched.
249
+ */
250
+ async deleteActor(actor) {
251
+ let id = this.#activity.actor;
252
+ let removed = store(await this.#options.followers.remove(this.#local.id, id));
253
+ if (isFailure(removed))
254
+ return removed;
255
+ await this.#options.resolver.evict(id);
256
+ return this.#call(this.#options.on?.delete?.(this.#deleteInbound(actor, "actor", id, null)));
257
+ }
258
+ /**
259
+ * A deleted object. Its own origin decides: a refetch that answers `404`, `410` or a
260
+ * Tombstone confirms the deletion of an object on the sender's origin, and one that
261
+ * still serves it must name the sender as its author.
262
+ *
263
+ * @param actor - The sender.
264
+ */
265
+ async deleteObject(actor) {
266
+ let id = idOf(this.#activity.object);
267
+ if (id === null)
268
+ return this.#ignored("unverifiable");
269
+ let fetched = await this.#options.resolver.object(id, { fresh: true });
270
+ if (isFailure(fetched)) {
271
+ let code = fetched.error.code;
272
+ if (code !== "gone" && code !== "not-found") {
273
+ return this.#unreachable(fetched.error, "unverifiable");
274
+ }
275
+ if (originOf(id) !== originOf(actor.id))
276
+ return this.#ignored("not-owner");
277
+ }
278
+ else if (!fetched.data.attributedTo.includes(actor.id)) {
279
+ return this.#ignored("not-owner");
280
+ }
281
+ let target = await this.#localTarget(id);
282
+ if (isFailure(target))
283
+ return target;
284
+ let inbound = this.#deleteInbound(actor, "object", id, target.data);
285
+ return this.#call(this.#options.on?.delete?.(inbound));
286
+ }
287
+ /**
288
+ * A Like, EmojiReact or Announce, which matters only when it names a local object.
289
+ *
290
+ * @param actor - The sender.
291
+ * @param kind - Which handler it reaches.
292
+ */
293
+ async reaction(actor, kind) {
294
+ let id = idOf(this.#activity.object);
295
+ if (id === null)
296
+ return this.#ignored("unrelated");
297
+ let found = store(await this.#options.objects.find(id));
298
+ if (isFailure(found))
299
+ return found;
300
+ if (found.data === null)
301
+ return this.#ignored("unrelated");
302
+ return this.#call(this.#options.on?.[kind]?.(this.#inbound(actor, found.data, id)));
303
+ }
304
+ /**
305
+ * An Accept or Reject of a Follow, which must be one a local actor sent to the sender:
306
+ * an embedded Follow by a local actor on the local origin naming the sender, or a Follow
307
+ * IRI on the local origin.
308
+ *
309
+ * @param actor - The sender.
310
+ * @param kind - Which handler it reaches.
311
+ */
312
+ async answer(actor, kind) {
313
+ let object = this.#activity.object;
314
+ let localOrigin = originOf(this.#local.id);
315
+ let follow = null;
316
+ if (typeof object === "string") {
317
+ if (originOf(object) !== localOrigin)
318
+ return this.#ignored("not-addressed");
319
+ }
320
+ else if (object !== null && isActivity(object) && object.type === "Follow") {
321
+ let local = await this.#isLocalActor(object.actor);
322
+ if (isFailure(local))
323
+ return local;
324
+ let sent = local.data && originOf(object.id) === localOrigin && idOf(object.object) === actor.id;
325
+ if (!sent)
326
+ return this.#ignored("not-addressed");
327
+ follow = object;
328
+ }
329
+ else {
330
+ return this.#ignored("not-addressed");
331
+ }
332
+ let target = follow?.actor ?? this.#local.id;
333
+ return this.#call(this.#options.on?.[kind]?.(this.#inbound(actor, follow, target)));
334
+ }
335
+ /**
336
+ * A Move of the sender's account. The target is fetched fresh and must list the sender
337
+ * in `alsoKnownAs`, and the sender, fetched fresh too, must name the target in `movedTo`,
338
+ * so neither side can claim the move alone.
339
+ */
340
+ async move() {
341
+ let activity = this.#activity;
342
+ let mover = activity.actor;
343
+ if (idOf(activity.object) !== mover)
344
+ return this.#ignored("not-owner");
345
+ if (activity.target === null)
346
+ return this.#ignored("unverifiable");
347
+ let target = await this.#options.resolver.actor(activity.target, { fresh: true });
348
+ if (isFailure(target))
349
+ return this.#unreachable(target.error, "unverifiable");
350
+ if (!target.data.alsoKnownAs.includes(mover))
351
+ return this.#ignored("unverifiable");
352
+ let moved = await this.#options.resolver.actor(mover, { fresh: true });
353
+ if (isFailure(moved))
354
+ return this.#unreachable(moved.error, "unverifiable");
355
+ if (moved.data.movedTo !== target.data.id)
356
+ return this.#ignored("unverifiable");
357
+ let inbound = this.#inbound(moved.data, target.data, null);
358
+ return this.#call(this.#options.on?.move?.(inbound));
359
+ }
360
+ /**
361
+ * The activity's object when it is embedded and has the actor's origin, the only case
362
+ * an embedded object is trusted as sent.
363
+ */
364
+ #trustedEmbed() {
365
+ let object = this.#activity.object;
366
+ if (object === null || typeof object === "string")
367
+ return null;
368
+ return originOf(object.id) === originOf(this.#activity.actor) ? object : null;
369
+ }
370
+ /**
371
+ * The activity's object, trusted: embedded from the actor's origin, else fetched from
372
+ * its own. `null` when the activity has no object or it cannot be fetched for good.
373
+ */
374
+ async #dereference() {
375
+ let trusted = this.#trustedEmbed();
376
+ if (trusted !== null)
377
+ return success(trusted);
378
+ let id = idOf(this.#activity.object);
379
+ if (id === null)
380
+ return success(null);
381
+ let fetched = await this.#options.resolver.object(id);
382
+ if (isFailure(fetched)) {
383
+ return fetched.error.retryable ? failure(fetched.error) : success(null);
384
+ }
385
+ return success(fetched.data);
386
+ }
387
+ /**
388
+ * The local thing a post concerns: what it replies to, then what it quotes, when the app
389
+ * serves it, then a local actor it mentions. `null` when it concerns nothing local.
390
+ *
391
+ * @param post - The post.
392
+ */
393
+ async #relevance(post) {
394
+ for (let id of [post.inReplyTo, post.quote]) {
395
+ if (id === null)
396
+ continue;
397
+ let found = await this.#localTarget(id);
398
+ if (isFailure(found) || found.data !== null)
399
+ return found;
400
+ }
401
+ for (let tag of post.tag) {
402
+ if (tag.type !== "Mention")
403
+ continue;
404
+ let local = await this.#isLocalActor(tag.href);
405
+ if (isFailure(local))
406
+ return local;
407
+ if (local.data)
408
+ return success(tag.href);
409
+ }
410
+ return success(null);
411
+ }
412
+ /**
413
+ * `id` when the app serves an object under it, else `null`.
414
+ *
415
+ * @param id - The IRI to look up.
416
+ */
417
+ async #localTarget(id) {
418
+ let found = store(await this.#options.objects.find(id));
419
+ if (isFailure(found))
420
+ return found;
421
+ return success(found.data === null ? null : id);
422
+ }
423
+ /**
424
+ * Whether `id` is the local actor or another actor the app hosts keys for.
425
+ *
426
+ * @param id - The actor IRI.
427
+ */
428
+ async #isLocalActor(id) {
429
+ if (id === this.#local.id)
430
+ return success(true);
431
+ let keys = store(await this.#options.keys.keysOf(id));
432
+ if (isFailure(keys))
433
+ return keys;
434
+ return success(keys.data !== null);
435
+ }
436
+ /**
437
+ * Refuses a Follow with a `Reject` embedding it, and forgets the follower.
438
+ *
439
+ * @param actor - The follower.
440
+ * @param follow - The Follow as the Reject embeds it.
441
+ * @param reason - Why, for the outcome.
442
+ */
443
+ async #refuse(actor, follow, reason) {
444
+ let removed = store(await this.#options.followers.remove(this.#local.id, actor.id));
445
+ if (isFailure(removed))
446
+ return removed;
447
+ let sent = await this.#send(answerTo("Reject", this.#local.id, follow), actor.inbox);
448
+ if (isFailure(sent))
449
+ return sent;
450
+ return this.#processed(reason === "blocked" ? "blocked" : "rejected");
451
+ }
452
+ /**
453
+ * Hands a serialized activity to the app's `send`.
454
+ *
455
+ * @param activity - The activity to deliver.
456
+ * @param inbox - Where to.
457
+ */
458
+ async #send(activity, inbox) {
459
+ let sent = await this.#options.send(stringify(activity), inbox);
460
+ if (isFailure(sent)) {
461
+ return failure(new ActivityPubError("send", `Could not queue ${activity.type} to ${inbox}`, {
462
+ retryable: true,
463
+ cause: sent.error,
464
+ }));
465
+ }
466
+ return success(undefined);
467
+ }
468
+ /**
469
+ * Calls a handler, when the app gave one, and answers `processed`.
470
+ *
471
+ * @param called - What the handler returned, `undefined` without one.
472
+ */
473
+ async #call(called) {
474
+ let settled = await settle(called);
475
+ if (isFailure(settled))
476
+ return settled;
477
+ return this.#processed(null);
478
+ }
479
+ /**
480
+ * A remote failure: retried when it can clear, else acknowledged as `reason`.
481
+ *
482
+ * @param error - The resolver's failure.
483
+ * @param reason - What the outcome says.
484
+ */
485
+ #unreachable(error, reason) {
486
+ return error.retryable ? failure(error) : this.#ignored(reason);
487
+ }
488
+ /** The Inbound a handler receives. */
489
+ #inbound(actor, object, target) {
490
+ return {
491
+ activity: this.#activity,
492
+ actor,
493
+ origin: this.#input.origin,
494
+ verification: this.#input.verification,
495
+ object,
496
+ target,
497
+ receivedAt: new Date(this.#input.receivedAt),
498
+ };
499
+ }
500
+ /** The Inbound a `delete` handler receives. */
501
+ #deleteInbound(actor, kind, id, target) {
502
+ return {
503
+ activity: this.#activity,
504
+ actor,
505
+ origin: this.#input.origin,
506
+ verification: this.#input.verification,
507
+ object: null,
508
+ target,
509
+ receivedAt: new Date(this.#input.receivedAt),
510
+ deleted: { kind, id },
511
+ };
512
+ }
513
+ /** A `processed` outcome. */
514
+ #processed(reason) {
515
+ return success(outcome("processed", this.#activity, reason));
516
+ }
517
+ /** An `ignored` outcome. */
518
+ #ignored(reason) {
519
+ return success(outcome("ignored", this.#activity, reason));
520
+ }
521
+ }
522
+ /**
523
+ * An `Accept` or `Reject` of a Follow, embedding it, since some servers match the answer
524
+ * on the embedded Follow rather than its id. The id derives from the Follow's, so a
525
+ * re-sent answer to the same Follow is the same activity.
526
+ *
527
+ * @param type - Which answer.
528
+ * @param local - The local actor answering.
529
+ * @param follow - The Follow answered.
530
+ */
531
+ function answerTo(type, local, follow) {
532
+ let path = type === "Accept" ? "accepts" : "rejects";
533
+ return {
534
+ id: `${local}#${path}/${encodeURIComponent(follow.id)}`,
535
+ type,
536
+ actor: local,
537
+ to: typeof follow.actor === "string" ? [follow.actor] : [],
538
+ object: follow,
539
+ };
540
+ }
541
+ /**
542
+ * Turns a handler's answer into the package's: a missing handler is a success, and a
543
+ * failure is retryable, since the handler may succeed on the next attempt.
544
+ *
545
+ * @param called - The handler's promise, `undefined` without one.
546
+ * @template T - What the handler answers.
547
+ */
548
+ async function settle(called) {
549
+ if (called === undefined)
550
+ return success(null);
551
+ let result = await called;
552
+ if (!isFailure(result))
553
+ return result;
554
+ if (result.error instanceof ActivityPubError)
555
+ return failure(result.error);
556
+ return failure(new ActivityPubError("handler", result.error.message, { retryable: true, cause: result.error }));
557
+ }
558
+ /**
559
+ * A store's answer, its failure made retryable, since a store that failed once can answer
560
+ * the next attempt.
561
+ *
562
+ * @param result - What the store answered.
563
+ * @template T - What the store answers.
564
+ */
565
+ function store(result) {
566
+ if (!isFailure(result))
567
+ return result;
568
+ return failure(new ActivityPubError("store", result.error.message, { retryable: true, cause: result.error }));
569
+ }
570
+ /** An outcome for `activity`. */
571
+ function outcome(status, activity, reason) {
572
+ return { status, type: activity.type, reason };
573
+ }
574
+ /** The IRI an activity's `object` names, embedded or not. */
575
+ function idOf(object) {
576
+ if (object === null)
577
+ return null;
578
+ return typeof object === "string" ? object : object.id;
579
+ }
580
+ /** Whether an embedded object is an activity, which is what names an `actor`. */
581
+ function isActivity(object) {
582
+ return "actor" in object && typeof object.actor === "string";
583
+ }
584
+ /** The origin of an IRI, or `null` for a string that is not a URL. */
585
+ function originOf(iri) {
586
+ return URL.parse(iri)?.origin ?? null;
587
+ }