@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.
- package/LICENSE.md +21 -0
- package/README.md +711 -0
- package/dist/collections.d.ts +67 -0
- package/dist/collections.js +38 -0
- package/dist/conformance.d.ts +80 -0
- package/dist/conformance.js +313 -0
- package/dist/discovery.d.ts +46 -0
- package/dist/discovery.js +105 -0
- package/dist/errors.d.ts +115 -0
- package/dist/errors.js +124 -0
- package/dist/federation.d.ts +281 -0
- package/dist/federation.js +513 -0
- package/dist/fixtures/gotosocial.d.ts +114 -0
- package/dist/fixtures/gotosocial.js +115 -0
- package/dist/fixtures/index.d.ts +15 -0
- package/dist/fixtures/index.js +50 -0
- package/dist/fixtures/mastodon.d.ts +489 -0
- package/dist/fixtures/mastodon.js +334 -0
- package/dist/fixtures/misskey.d.ts +254 -0
- package/dist/fixtures/misskey.js +163 -0
- package/dist/fixtures/pixelfed.d.ts +118 -0
- package/dist/fixtures/pixelfed.js +126 -0
- package/dist/fixtures/shared.d.ts +14 -0
- package/dist/fixtures/shared.js +14 -0
- package/dist/fixtures/threads.d.ts +82 -0
- package/dist/fixtures/threads.js +88 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +22 -0
- package/dist/keys.d.ts +100 -0
- package/dist/keys.js +147 -0
- package/dist/lib/availability.d.ts +37 -0
- package/dist/lib/availability.js +52 -0
- package/dist/lib/compact.d.ts +145 -0
- package/dist/lib/compact.js +514 -0
- package/dist/lib/constants.d.ts +35 -0
- package/dist/lib/constants.js +65 -0
- package/dist/lib/delivery-fan-out.d.ts +72 -0
- package/dist/lib/delivery-fan-out.js +202 -0
- package/dist/lib/delivery-post.d.ts +108 -0
- package/dist/lib/delivery-post.js +318 -0
- package/dist/lib/fetch.d.ts +64 -0
- package/dist/lib/fetch.js +103 -0
- package/dist/lib/inbox-error.d.ts +45 -0
- package/dist/lib/inbox-error.js +74 -0
- package/dist/lib/inbox-handle.d.ts +119 -0
- package/dist/lib/inbox-handle.js +587 -0
- package/dist/lib/inbox-receive.d.ts +117 -0
- package/dist/lib/inbox-receive.js +233 -0
- package/dist/lib/inbox-signature.d.ts +61 -0
- package/dist/lib/inbox-signature.js +156 -0
- package/dist/lib/inbox-summarize.d.ts +41 -0
- package/dist/lib/inbox-summarize.js +97 -0
- package/dist/lib/messages.d.ts +36 -0
- package/dist/lib/messages.js +35 -0
- package/dist/lib/parse.d.ts +42 -0
- package/dist/lib/parse.js +348 -0
- package/dist/lib/response.d.ts +45 -0
- package/dist/lib/response.js +59 -0
- package/dist/lib/stringify.d.ts +51 -0
- package/dist/lib/stringify.js +122 -0
- package/dist/lib/tombstone.d.ts +24 -0
- package/dist/lib/tombstone.js +15 -0
- package/dist/lib/types.d.ts +249 -0
- package/dist/lib/types.js +0 -0
- package/dist/memory.d.ts +77 -0
- package/dist/memory.js +146 -0
- package/dist/remote.d.ts +105 -0
- package/dist/remote.js +262 -0
- package/dist/store.d.ts +78 -0
- package/dist/store.js +8 -0
- package/dist/testing.d.ts +12 -0
- package/dist/testing.js +10 -0
- package/package.json +54 -0
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The failures this package answers inside a `Result`: a base with a code and a retry
|
|
3
|
+
* verdict that a job consumer acts on, the parse failure every `parse*` returns, and the
|
|
4
|
+
* fetch failure discovery and the remote resolver return, and the failure a queued
|
|
5
|
+
* federation message ends with.
|
|
6
|
+
*
|
|
7
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
8
|
+
* @copyright Sergio Xalambrí 2026
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Every failure of this package. `code` is stable for a caller to branch on, and
|
|
12
|
+
* `retryable` tells a job consumer whether asking again later can change the outcome.
|
|
13
|
+
*
|
|
14
|
+
* @template Code - The codes a subclass can carry.
|
|
15
|
+
*/
|
|
16
|
+
export declare class ActivityPubError<Code extends string = string> extends Error {
|
|
17
|
+
name: string;
|
|
18
|
+
/** Machine-readable cause, stable across releases of a subclass. */
|
|
19
|
+
readonly code: Code;
|
|
20
|
+
/** Whether a later attempt can succeed, so a job retries it rather than acknowledging. */
|
|
21
|
+
readonly retryable: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* @param code - Why the operation failed.
|
|
24
|
+
* @param message - The explanation a log shows.
|
|
25
|
+
* @param options - Whether a retry can help, and the underlying error.
|
|
26
|
+
*/
|
|
27
|
+
constructor(code: Code, message: string, options?: ActivityPubError.Options);
|
|
28
|
+
}
|
|
29
|
+
export declare namespace ActivityPubError {
|
|
30
|
+
/** How a failure is classified beyond its code. */
|
|
31
|
+
interface Options {
|
|
32
|
+
/** @default false */
|
|
33
|
+
retryable?: boolean;
|
|
34
|
+
cause?: unknown;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A document that is not the ActivityStreams shape asked for. Every issue found is
|
|
39
|
+
* listed with a JSON Pointer into the received document, and `message` summarizes the
|
|
40
|
+
* first; a resent copy of the same document fails the same way, so it never retries.
|
|
41
|
+
*/
|
|
42
|
+
export declare class ActivityPubParseError extends ActivityPubError<"invalid-document"> {
|
|
43
|
+
name: string;
|
|
44
|
+
/** What was being read: `activity`, `actor`, `object` or `collection`. */
|
|
45
|
+
readonly kind: ActivityPubParseError.Kind;
|
|
46
|
+
readonly issues: ActivityPubParseError.Issue[];
|
|
47
|
+
/**
|
|
48
|
+
* @param kind - What was being read.
|
|
49
|
+
* @param issues - Every problem found, at least one.
|
|
50
|
+
*/
|
|
51
|
+
constructor(kind: ActivityPubParseError.Kind, issues: ActivityPubParseError.Issue[]);
|
|
52
|
+
}
|
|
53
|
+
export declare namespace ActivityPubParseError {
|
|
54
|
+
/** The document reader that failed. */
|
|
55
|
+
type Kind = "activity" | "actor" | "object" | "collection";
|
|
56
|
+
/** One place a document breaks the shape asked for. */
|
|
57
|
+
interface Issue {
|
|
58
|
+
/** A JSON Pointer into the received document; `""` for the document as a whole. */
|
|
59
|
+
at: string;
|
|
60
|
+
message: string;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Why a remote ActivityPub or WebFinger document could not be used. `gone` means the
|
|
65
|
+
* server answered `410` or a `Tombstone`, so a caller forgets the resource; `timeout`,
|
|
66
|
+
* `network` and a `5xx` or `429` under `http` are the outcomes a retry can change.
|
|
67
|
+
*/
|
|
68
|
+
export type ActivityPubFetchErrorCode = "gone" | "not-found" | "unauthorized" | "refused-url" | "too-large" | "timeout" | "network" | "invalid-document" | "id-mismatch" | "http";
|
|
69
|
+
/**
|
|
70
|
+
* A remote document that could not be fetched or trusted. `url` is the document that
|
|
71
|
+
* failed, and `status` the HTTP status when a response arrived at all.
|
|
72
|
+
*/
|
|
73
|
+
export declare class ActivityPubFetchError extends ActivityPubError<ActivityPubFetchErrorCode> {
|
|
74
|
+
name: string;
|
|
75
|
+
readonly url: string;
|
|
76
|
+
/** The response status, or `null` when the request was refused, timed out or never connected. */
|
|
77
|
+
readonly status: number | null;
|
|
78
|
+
/**
|
|
79
|
+
* `retryable` defaults from the code and status: `timeout` and `network` always, and
|
|
80
|
+
* `http` for a `5xx` or `429`; every other outcome repeats on a retry.
|
|
81
|
+
*
|
|
82
|
+
* @param code - Why the document could not be used.
|
|
83
|
+
* @param url - The document's URL.
|
|
84
|
+
* @param message - The explanation a log shows.
|
|
85
|
+
* @param options - The status, an explicit retry verdict, and the underlying error.
|
|
86
|
+
*/
|
|
87
|
+
constructor(code: ActivityPubFetchErrorCode, url: string, message: string, options?: ActivityPubFetchError.Options);
|
|
88
|
+
}
|
|
89
|
+
export declare namespace ActivityPubFetchError {
|
|
90
|
+
/** How a fetch failure is classified beyond its code. */
|
|
91
|
+
interface Options extends ActivityPubError.Options {
|
|
92
|
+
status?: number | null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Why `Federation#process` could not finish a queued message. `retryable` and `delay` are
|
|
97
|
+
* all a job needs (retry after `delay` milliseconds, or acknowledge); `code` and `cause`
|
|
98
|
+
* keep the failure underneath, such as a `DeliveryError` or an `ActivityPubFetchError`.
|
|
99
|
+
*/
|
|
100
|
+
export declare class FederationError extends ActivityPubError {
|
|
101
|
+
name: string;
|
|
102
|
+
/** Which kind of message failed. */
|
|
103
|
+
readonly kind: "inbox" | "fanOut" | "deliver";
|
|
104
|
+
/**
|
|
105
|
+
* Milliseconds to wait before the retry: the backoff step for this attempt, or longer
|
|
106
|
+
* when the inbox named a `Retry-After`. `0` when the failure is not retryable.
|
|
107
|
+
*/
|
|
108
|
+
readonly delay: number;
|
|
109
|
+
/**
|
|
110
|
+
* @param kind - Which kind of message failed.
|
|
111
|
+
* @param cause - The failure underneath, whose code, message and verdict this one keeps.
|
|
112
|
+
* @param delay - The wait before a retry, in milliseconds.
|
|
113
|
+
*/
|
|
114
|
+
constructor(kind: FederationError["kind"], cause: ActivityPubError, delay: number);
|
|
115
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The failures this package answers inside a `Result`: a base with a code and a retry
|
|
3
|
+
* verdict that a job consumer acts on, the parse failure every `parse*` returns, and the
|
|
4
|
+
* fetch failure discovery and the remote resolver return, and the failure a queued
|
|
5
|
+
* federation message ends with.
|
|
6
|
+
*
|
|
7
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
8
|
+
* @copyright Sergio Xalambrí 2026
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Every failure of this package. `code` is stable for a caller to branch on, and
|
|
12
|
+
* `retryable` tells a job consumer whether asking again later can change the outcome.
|
|
13
|
+
*
|
|
14
|
+
* @template Code - The codes a subclass can carry.
|
|
15
|
+
*/
|
|
16
|
+
export class ActivityPubError extends Error {
|
|
17
|
+
name = "ActivityPubError";
|
|
18
|
+
/** Machine-readable cause, stable across releases of a subclass. */
|
|
19
|
+
code;
|
|
20
|
+
/** Whether a later attempt can succeed, so a job retries it rather than acknowledging. */
|
|
21
|
+
retryable;
|
|
22
|
+
/**
|
|
23
|
+
* @param code - Why the operation failed.
|
|
24
|
+
* @param message - The explanation a log shows.
|
|
25
|
+
* @param options - Whether a retry can help, and the underlying error.
|
|
26
|
+
*/
|
|
27
|
+
constructor(code, message, options = {}) {
|
|
28
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
29
|
+
this.code = code;
|
|
30
|
+
this.retryable = options.retryable ?? false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A document that is not the ActivityStreams shape asked for. Every issue found is
|
|
35
|
+
* listed with a JSON Pointer into the received document, and `message` summarizes the
|
|
36
|
+
* first; a resent copy of the same document fails the same way, so it never retries.
|
|
37
|
+
*/
|
|
38
|
+
export class ActivityPubParseError extends ActivityPubError {
|
|
39
|
+
name = "ActivityPubParseError";
|
|
40
|
+
/** What was being read: `activity`, `actor`, `object` or `collection`. */
|
|
41
|
+
kind;
|
|
42
|
+
issues;
|
|
43
|
+
/**
|
|
44
|
+
* @param kind - What was being read.
|
|
45
|
+
* @param issues - Every problem found, at least one.
|
|
46
|
+
*/
|
|
47
|
+
constructor(kind, issues) {
|
|
48
|
+
let first = issues[0];
|
|
49
|
+
let where = first === undefined || first.at === "" ? "" : ` at ${first.at}`;
|
|
50
|
+
let more = issues.length > 1 ? ` (and ${issues.length - 1} more)` : "";
|
|
51
|
+
super("invalid-document", `Invalid ${kind}${where}: ${first?.message ?? "the document is invalid."}${more}`);
|
|
52
|
+
this.kind = kind;
|
|
53
|
+
this.issues = issues;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Codes a later attempt can clear regardless of status: the deadline and the transport. */
|
|
57
|
+
const RETRYABLE_FETCH_CODES = new Set(["timeout", "network"]);
|
|
58
|
+
/**
|
|
59
|
+
* A remote document that could not be fetched or trusted. `url` is the document that
|
|
60
|
+
* failed, and `status` the HTTP status when a response arrived at all.
|
|
61
|
+
*/
|
|
62
|
+
export class ActivityPubFetchError extends ActivityPubError {
|
|
63
|
+
name = "ActivityPubFetchError";
|
|
64
|
+
url;
|
|
65
|
+
/** The response status, or `null` when the request was refused, timed out or never connected. */
|
|
66
|
+
status;
|
|
67
|
+
/**
|
|
68
|
+
* `retryable` defaults from the code and status: `timeout` and `network` always, and
|
|
69
|
+
* `http` for a `5xx` or `429`; every other outcome repeats on a retry.
|
|
70
|
+
*
|
|
71
|
+
* @param code - Why the document could not be used.
|
|
72
|
+
* @param url - The document's URL.
|
|
73
|
+
* @param message - The explanation a log shows.
|
|
74
|
+
* @param options - The status, an explicit retry verdict, and the underlying error.
|
|
75
|
+
*/
|
|
76
|
+
constructor(code, url, message, options = {}) {
|
|
77
|
+
let status = options.status ?? null;
|
|
78
|
+
super(code, message, {
|
|
79
|
+
retryable: options.retryable ?? isRetryableFetch(code, status),
|
|
80
|
+
cause: options.cause,
|
|
81
|
+
});
|
|
82
|
+
this.url = url;
|
|
83
|
+
this.status = status;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Whether a fetch failure can clear on its own: a deadline, a dropped connection, a
|
|
88
|
+
* server error or a rate limit.
|
|
89
|
+
*
|
|
90
|
+
* @param code - The failure's code.
|
|
91
|
+
* @param status - The response status, when one arrived.
|
|
92
|
+
*/
|
|
93
|
+
function isRetryableFetch(code, status) {
|
|
94
|
+
if (RETRYABLE_FETCH_CODES.has(code))
|
|
95
|
+
return true;
|
|
96
|
+
if (code !== "http" || status === null)
|
|
97
|
+
return false;
|
|
98
|
+
return status === 429 || status >= 500;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Why `Federation#process` could not finish a queued message. `retryable` and `delay` are
|
|
102
|
+
* all a job needs (retry after `delay` milliseconds, or acknowledge); `code` and `cause`
|
|
103
|
+
* keep the failure underneath, such as a `DeliveryError` or an `ActivityPubFetchError`.
|
|
104
|
+
*/
|
|
105
|
+
export class FederationError extends ActivityPubError {
|
|
106
|
+
name = "FederationError";
|
|
107
|
+
/** Which kind of message failed. */
|
|
108
|
+
kind;
|
|
109
|
+
/**
|
|
110
|
+
* Milliseconds to wait before the retry: the backoff step for this attempt, or longer
|
|
111
|
+
* when the inbox named a `Retry-After`. `0` when the failure is not retryable.
|
|
112
|
+
*/
|
|
113
|
+
delay;
|
|
114
|
+
/**
|
|
115
|
+
* @param kind - Which kind of message failed.
|
|
116
|
+
* @param cause - The failure underneath, whose code, message and verdict this one keeps.
|
|
117
|
+
* @param delay - The wait before a retry, in milliseconds.
|
|
118
|
+
*/
|
|
119
|
+
constructor(kind, cause, delay) {
|
|
120
|
+
super(cause.code, cause.message, { retryable: cause.retryable, cause });
|
|
121
|
+
this.kind = kind;
|
|
122
|
+
this.delay = cause.retryable ? delay : 0;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ActivityPub runtime of one local actor: its documents and inbox served from `fetch`,
|
|
3
|
+
* every queued step (inbox processing, fan-out, delivery) run by `process`, and the app's
|
|
4
|
+
* handlers registered with `on`, so the actor, keys, stores and queue are wired once.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
import type { Backoff } from "@sdxc/backoff";
|
|
10
|
+
import type { Cache } from "@sdxc/cache";
|
|
11
|
+
import type { DurationInput } from "@sdxc/duration";
|
|
12
|
+
import type { Result } from "@sdxc/result";
|
|
13
|
+
import type { Schema } from "remix/data-schema";
|
|
14
|
+
import type { CollectionItem } from "./collections.js";
|
|
15
|
+
import type { ActivityPubFetchError } from "./errors.js";
|
|
16
|
+
import type { ActorKeys } from "./keys.js";
|
|
17
|
+
import type { Delivered } from "./lib/delivery-post.js";
|
|
18
|
+
import type { InboxError } from "./lib/inbox-error.js";
|
|
19
|
+
import type { DeleteInbound, FollowDecision, HandleOutcome, Inbound } from "./lib/inbox-handle.js";
|
|
20
|
+
import type { BlockedCheck, VerifiedFetch } from "./lib/inbox-receive.js";
|
|
21
|
+
import type { Summary } from "./lib/inbox-summarize.js";
|
|
22
|
+
import type { Message as QueueMessage } from "./lib/messages.js";
|
|
23
|
+
import type { RespondOptions as DocumentRespondOptions } from "./lib/response.js";
|
|
24
|
+
import type { ActivityPub } from "./lib/types.js";
|
|
25
|
+
import type { ResolverTtl } from "./remote.js";
|
|
26
|
+
import type { FollowerStore, KeyProvider, LocalObjects, SeenActivities } from "./store.js";
|
|
27
|
+
import { ActivityPubError, FederationError } from "./errors.js";
|
|
28
|
+
import { RemoteResolver } from "./remote.js";
|
|
29
|
+
/**
|
|
30
|
+
* Federates one local actor. Construct it wherever the stores are at hand (per request or
|
|
31
|
+
* per job is fine: it holds no state beyond its options and handlers), route the actor's
|
|
32
|
+
* URLs through `fetch`, and hand every queued message to `process`.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* let federation = new Federation({ actor, keys, stores, cache, userAgent, queue });
|
|
36
|
+
* federation.on("Create", async (ctx) => saveReply(ctx.summary()));
|
|
37
|
+
*/
|
|
38
|
+
export declare class Federation {
|
|
39
|
+
#private;
|
|
40
|
+
/**
|
|
41
|
+
* The Standard Schema of every message federation enqueues (`inbox`, `fanOut` and
|
|
42
|
+
* `deliver`, told apart by `kind`), so one job declared with it carries all three.
|
|
43
|
+
*
|
|
44
|
+
* @example federation: job({ input: Federation.MESSAGE })
|
|
45
|
+
*/
|
|
46
|
+
static readonly MESSAGE: Schema<unknown, Federation.Message>;
|
|
47
|
+
/**
|
|
48
|
+
* Fetches remote actors, keys and objects through the app's cache, signing every GET
|
|
49
|
+
* with the local actor's keys unless `resolver.signed` is `false`.
|
|
50
|
+
*/
|
|
51
|
+
readonly resolver: RemoteResolver;
|
|
52
|
+
/** @param options - The local actor, its keys, the stores, the cache and the queue. */
|
|
53
|
+
constructor(options: Federation.Options);
|
|
54
|
+
/**
|
|
55
|
+
* Registers the app's handler for one or more activity types, called once the package
|
|
56
|
+
* has done the work the protocol requires (followers kept, Accept sent, ownership and
|
|
57
|
+
* relevance checked). A later registration for a type replaces the earlier one.
|
|
58
|
+
*
|
|
59
|
+
* A handler answers nothing or a `Result`; a failure, or an exception, retries the
|
|
60
|
+
* message, so a handler tolerates seeing an activity twice. A `Follow` handler may
|
|
61
|
+
* answer `"accept"`, `"reject"` or `"pending"`. `Like` handlers also receive Pleroma's
|
|
62
|
+
* `EmojiReact`.
|
|
63
|
+
*
|
|
64
|
+
* @param type - The activity type, or several sharing one handler.
|
|
65
|
+
* @param handler - Receives the verified activity and its `summary()`.
|
|
66
|
+
* @returns The federation, so registrations chain.
|
|
67
|
+
* @example federation.on(["Like", "Announce"], async (ctx) => saveResponse(ctx.summary()));
|
|
68
|
+
*/
|
|
69
|
+
on<T extends Federation.HandledType>(type: T | readonly T[], handler: (ctx: Federation.ContextOf<T>) => Federation.HandlerReturn<Federation.AnswerOf<T>>): this;
|
|
70
|
+
/**
|
|
71
|
+
* Answers the local actor's own URLs, as its document names them: the actor (with
|
|
72
|
+
* `publicKey` from its keys), a `POST` to `inbox` or `endpoints.sharedInbox`, `outbox`,
|
|
73
|
+
* `followers` and `following`. Any other URL answers `null`, so the app routes it on.
|
|
74
|
+
*
|
|
75
|
+
* An inbox `POST` is verified here, inside the request, and answers `202` once its
|
|
76
|
+
* message is queued, or the status of the check it failed. With `authorizedFetch`, the
|
|
77
|
+
* collections require a signed GET; the actor document always answers unsigned.
|
|
78
|
+
*
|
|
79
|
+
* @param request - Any incoming request.
|
|
80
|
+
* @returns The response, or `null` for a URL that is not one of the actor's.
|
|
81
|
+
* @example let response = await federation.fetch(request); if (response) return response;
|
|
82
|
+
*/
|
|
83
|
+
fetch(request: Request): Promise<Response | null>;
|
|
84
|
+
/**
|
|
85
|
+
* Answers `document` as ActivityStreams when the request prefers it over HTML, for a
|
|
86
|
+
* URL that serves both, such as a post's page. The response carries `Vary: Accept`, an
|
|
87
|
+
* `ETag` with `304` revalidation, a public 5-minute cache unless stated, and `410` for a
|
|
88
|
+
* `Tombstone`. With `authorizedFetch`, the request must carry a valid signature.
|
|
89
|
+
*
|
|
90
|
+
* @param request - The incoming request.
|
|
91
|
+
* @param document - The object, actor or collection the URL names.
|
|
92
|
+
* @param options - The cache policy, extra headers and status.
|
|
93
|
+
* @returns The response, or `null` when HTML wins and the app renders its page.
|
|
94
|
+
* @example let response = await federation.respond(request, article); if (response) return response;
|
|
95
|
+
*/
|
|
96
|
+
respond(request: Request, document: ActivityPub.Draft<ActivityPub.Document>, options?: Federation.RespondOptions): Promise<Response | null>;
|
|
97
|
+
/**
|
|
98
|
+
* Verifies the signature on a GET (authorized fetch): the blocked host of the key, the
|
|
99
|
+
* signature's age, and the key, refetched once when it no longer verifies. The local
|
|
100
|
+
* actor's document passes unsigned, because remote servers read its key from there.
|
|
101
|
+
*
|
|
102
|
+
* @param request - The GET.
|
|
103
|
+
* @returns The signer, or an `InboxError` whose `toResponse()` refuses the request.
|
|
104
|
+
* @example let verified = await federation.verify(request); if (isFailure(verified)) return verified.error.toResponse();
|
|
105
|
+
*/
|
|
106
|
+
verify(request: Request): Promise<Result<VerifiedFetch, InboxError>>;
|
|
107
|
+
/**
|
|
108
|
+
* Runs one queued message: an inbox activity through the protocol's checks and the
|
|
109
|
+
* app's handlers, a fan-out into one `deliver` message per distinct inbox, or one signed
|
|
110
|
+
* delivery. Accept and Reject replies are queued as deliveries, and an inbox answering
|
|
111
|
+
* `410` drops every follower reached through it.
|
|
112
|
+
*
|
|
113
|
+
* @param message - What the queue handed back, read through `Federation.MESSAGE`.
|
|
114
|
+
* @param options - The job's attempt, starting at 1, which spaces retries.
|
|
115
|
+
* @returns What happened, or a `FederationError` whose `retryable` and `delay` say
|
|
116
|
+
* whether and when to retry.
|
|
117
|
+
* @example
|
|
118
|
+
* let processed = await federation.process(ctx.input, { attempts: ctx.attempts });
|
|
119
|
+
* if (isFailure(processed) && processed.error.retryable) ctx.retry({ delay: processed.error.delay });
|
|
120
|
+
*/
|
|
121
|
+
process(message: Federation.Message, options?: Federation.ProcessOptions): Promise<Result<Federation.Outcome, FederationError>>;
|
|
122
|
+
/**
|
|
123
|
+
* Queues the fan-out of an activity by the local actor to everyone it addresses: its
|
|
124
|
+
* followers when the followers collection is in `to` or `cc`, and every other addressed
|
|
125
|
+
* actor's inbox.
|
|
126
|
+
*
|
|
127
|
+
* @param activity - The activity, as a document or as JSON text already serialized.
|
|
128
|
+
* @returns `too-large` for an activity over 120 KB, which no delivery message can carry,
|
|
129
|
+
* or a retryable `enqueue` failure.
|
|
130
|
+
* @example await federation.publish({ id: `${post.url}#create`, type: "Create", actor: ACTOR_ID, object: article, to: [PUBLIC], cc: [FOLLOWERS_ID] });
|
|
131
|
+
*/
|
|
132
|
+
publish(activity: ActivityPub.Draft<ActivityPub.Activity> | string): Promise<Result<void, ActivityPubError>>;
|
|
133
|
+
/**
|
|
134
|
+
* Resolves a handle to its actor the way Mastodon does: WebFinger on the handle's host,
|
|
135
|
+
* the `self` link typed as ActivityStreams, then the actor, whose own host must confirm
|
|
136
|
+
* the handle when it differs from the actor's canonical one.
|
|
137
|
+
*
|
|
138
|
+
* @param handle - `@user@host`, `user@host` or `acct:user@host`.
|
|
139
|
+
* @returns The actor; `not-found` when WebFinger names no actor, `id-mismatch` when the
|
|
140
|
+
* actor's host does not confirm the handle.
|
|
141
|
+
* @example let actor = await federation.lookup("@someone@mastodon.social");
|
|
142
|
+
*/
|
|
143
|
+
lookup(handle: string): Promise<Result<ActivityPub.Actor, ActivityPubFetchError>>;
|
|
144
|
+
}
|
|
145
|
+
export declare namespace Federation {
|
|
146
|
+
/** What a `Federation` is built from. */
|
|
147
|
+
interface Options {
|
|
148
|
+
/**
|
|
149
|
+
* The local actor's document. Its `inbox`, `endpoints.sharedInbox`, `outbox`,
|
|
150
|
+
* `followers`, `following` and `id` are the URLs `fetch` answers, and Follows of it
|
|
151
|
+
* are what the inbox keeps followers for.
|
|
152
|
+
*/
|
|
153
|
+
actor: ActivityPub.Actor;
|
|
154
|
+
/** The actor's keys, or a provider holding the keys of every actor the app hosts. */
|
|
155
|
+
keys: ActorKeys | KeyProvider;
|
|
156
|
+
stores: Stores;
|
|
157
|
+
/** Remote documents, accepted signature schemes and failing origins; a failing cache costs a fetch. */
|
|
158
|
+
cache: Cache;
|
|
159
|
+
/** Sent on every outbound request; some instances refuse requests without one. */
|
|
160
|
+
userAgent: string;
|
|
161
|
+
/** Where inbox, fan-out and delivery messages go; the app hands each back to `process`. */
|
|
162
|
+
queue: Queue;
|
|
163
|
+
/**
|
|
164
|
+
* Whether requests from a host are refused: it cannot deliver, follow, or receive.
|
|
165
|
+
* @default () => false
|
|
166
|
+
*/
|
|
167
|
+
blocked?: BlockedCheck;
|
|
168
|
+
/** Pages of the outbox collection; without it the outbox is empty. */
|
|
169
|
+
outbox?: (cursor: string | null) => Promise<Result<OutboxPage, Error>>;
|
|
170
|
+
/** Requires a signed GET for the collections and for `respond` (secure mode). @default false */
|
|
171
|
+
authorizedFetch?: boolean;
|
|
172
|
+
inbox?: InboxOptions;
|
|
173
|
+
resolver?: ResolverOptions;
|
|
174
|
+
delivery?: DeliveryOptions;
|
|
175
|
+
collections?: CollectionOptions;
|
|
176
|
+
/**
|
|
177
|
+
* The wait before each retry of a failed message, by attempt. The default covers about
|
|
178
|
+
* 21 hours over five retries.
|
|
179
|
+
* @default DELIVERY_BACKOFF
|
|
180
|
+
*/
|
|
181
|
+
backoff?: Backoff;
|
|
182
|
+
/** How long a processed activity id is remembered to skip redeliveries. @default "1 day" */
|
|
183
|
+
seenTtl?: DurationInput;
|
|
184
|
+
/** The clock signatures, failure windows and `receivedAt` are read from. @default () => new Date() */
|
|
185
|
+
now?: () => Date;
|
|
186
|
+
}
|
|
187
|
+
/** The app's storage, each implementing the interface the package calls. */
|
|
188
|
+
interface Stores {
|
|
189
|
+
followers: FollowerStore;
|
|
190
|
+
seen: SeenActivities;
|
|
191
|
+
objects: LocalObjects;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* The app's queue. Each message is plain JSON under 128 KB, and a job declared with
|
|
195
|
+
* `Federation.MESSAGE` reads it back. A failure or an exception makes the step that
|
|
196
|
+
* enqueued it fail as retryable.
|
|
197
|
+
*/
|
|
198
|
+
interface Queue {
|
|
199
|
+
enqueue(message: Message): Promise<Result<void, Error> | void>;
|
|
200
|
+
/** Writes a fan-out's deliveries in one call; without it they go one by one. */
|
|
201
|
+
enqueueMany?(messages: Message[]): Promise<Result<void, Error> | void>;
|
|
202
|
+
}
|
|
203
|
+
/** One page of the outbox, newest first. */
|
|
204
|
+
interface OutboxPage {
|
|
205
|
+
/** Activities, usually each post's `Create`, or their IRIs. */
|
|
206
|
+
items: CollectionItem[];
|
|
207
|
+
/** The cursor of the following page, `null` on the last one. */
|
|
208
|
+
next: string | null;
|
|
209
|
+
/** The outbox's size, published on the collection. */
|
|
210
|
+
totalItems?: number | null;
|
|
211
|
+
}
|
|
212
|
+
/** How the inbox reads a POST. */
|
|
213
|
+
interface InboxOptions {
|
|
214
|
+
/** Bodies past this answer `413`. @default 102400 */
|
|
215
|
+
maxBytes?: number;
|
|
216
|
+
/** How old a signature may be, plus five minutes of clock skew. @default "1 hour" */
|
|
217
|
+
maxAge?: DurationInput;
|
|
218
|
+
}
|
|
219
|
+
/** How remote documents are fetched. */
|
|
220
|
+
interface ResolverOptions {
|
|
221
|
+
/** Signs every GET with the actor's keys, as GoToSocial and secure-mode Mastodon require. @default true */
|
|
222
|
+
signed?: boolean;
|
|
223
|
+
ttl?: ResolverTtl;
|
|
224
|
+
/** @default "10 seconds" */
|
|
225
|
+
timeout?: DurationInput;
|
|
226
|
+
/** @default 1048576 */
|
|
227
|
+
maxBytes?: number;
|
|
228
|
+
}
|
|
229
|
+
/** How activities are delivered. */
|
|
230
|
+
interface DeliveryOptions {
|
|
231
|
+
/** The deadline of each POST. @default "15 seconds" */
|
|
232
|
+
timeout?: DurationInput;
|
|
233
|
+
/** Followers' inboxes read per store page, and the most deliveries per enqueue. @default 100 */
|
|
234
|
+
pageSize?: number;
|
|
235
|
+
}
|
|
236
|
+
/** How the actor's collections are served. */
|
|
237
|
+
interface CollectionOptions {
|
|
238
|
+
/** Followers per page of the followers collection. @default 50 */
|
|
239
|
+
pageSize?: number;
|
|
240
|
+
}
|
|
241
|
+
/** How `process` runs a message. */
|
|
242
|
+
interface ProcessOptions {
|
|
243
|
+
/** The job's attempt, starting at 1. Past the first, a redelivery check is skipped. @default 1 */
|
|
244
|
+
attempts?: number;
|
|
245
|
+
}
|
|
246
|
+
/** How `respond` answers a document. */
|
|
247
|
+
type RespondOptions = Pick<DocumentRespondOptions, "cache" | "headers" | "status">;
|
|
248
|
+
/** Every message federation enqueues, told apart by `kind`. */
|
|
249
|
+
type Message = QueueMessage;
|
|
250
|
+
/** The activity types the app can register a handler for. */
|
|
251
|
+
type HandledType = "Follow" | "Undo" | "Create" | "Update" | "Delete" | "Like" | "Announce" | "Accept" | "Reject" | "Move";
|
|
252
|
+
/** A verified activity as a handler receives it, with its response summary. */
|
|
253
|
+
interface Context<A extends ActivityPub.Activity = ActivityPub.Activity> extends Inbound<A> {
|
|
254
|
+
/**
|
|
255
|
+
* A reply, mention, like or boost of local content in the shape a Webmention is
|
|
256
|
+
* stored in, with remote HTML sanitized; `null` for any other activity.
|
|
257
|
+
*/
|
|
258
|
+
summary(): Summary | null;
|
|
259
|
+
}
|
|
260
|
+
/** A verified `Delete`; `actor` is `null` when the deleted account's document is gone. */
|
|
261
|
+
interface DeleteContext extends DeleteInbound {
|
|
262
|
+
/** A deletion summarizes as nothing, so this is always `null`. */
|
|
263
|
+
summary(): Summary | null;
|
|
264
|
+
}
|
|
265
|
+
/** The context a handler for `T` receives. */
|
|
266
|
+
type ContextOf<T extends HandledType> = T extends "Delete" ? DeleteContext : Context;
|
|
267
|
+
/** What a handler for `T` answers: a Follow decision, or nothing. */
|
|
268
|
+
type AnswerOf<T extends HandledType> = T extends "Follow" ? FollowDecision | null : void;
|
|
269
|
+
/** A handler's return: the answer or a `Result` of it, sync or async. */
|
|
270
|
+
type HandlerReturn<T> = T | Result<T, Error> | Promise<T | Result<T, Error>>;
|
|
271
|
+
/** What `process` did with a message. */
|
|
272
|
+
type Outcome = ({
|
|
273
|
+
kind: "inbox";
|
|
274
|
+
} & HandleOutcome) | {
|
|
275
|
+
kind: "fanOut";
|
|
276
|
+
inboxes: number;
|
|
277
|
+
skipped: number;
|
|
278
|
+
} | ({
|
|
279
|
+
kind: "deliver";
|
|
280
|
+
} & Delivered);
|
|
281
|
+
}
|