@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,64 @@
1
+ /**
2
+ * The one way this package reads a remote document: through `@sdxc/outbound`, so every
3
+ * hop is checked and resolved to public addresses, within a deadline and a size cap, with
4
+ * the response status mapped to the `ActivityPubFetchError` code a caller branches on.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ import type { DurationInput } from "@sdxc/duration";
10
+ import type { Result } from "@sdxc/result";
11
+ import { ActivityPubFetchError } from "../errors.js";
12
+ /** Long enough for a slow instance, short enough that an inbox request still answers. */
13
+ export declare const DEFAULT_TIMEOUT: DurationInput;
14
+ /** An actor with a long bio and many profile fields stays far below a megabyte. */
15
+ export declare const DEFAULT_MAX_BYTES: number;
16
+ /** How a document is requested. */
17
+ export interface FetchTextOptions {
18
+ /** Sent on every hop of the redirect chain. */
19
+ headers: Headers;
20
+ timeout: DurationInput;
21
+ maxBytes: number;
22
+ }
23
+ /** A successful response's body, and the URL the redirect chain ended at. */
24
+ export interface FetchedText {
25
+ text: string;
26
+ url: URL;
27
+ }
28
+ /** The final response of a chain whose status may not be a success, before its body is read. */
29
+ export interface Answered {
30
+ response: Response;
31
+ url: URL;
32
+ }
33
+ /**
34
+ * Requests a URL and answers the final response whatever its status, with every hop's
35
+ * host resolved and refused unless all its addresses are public.
36
+ *
37
+ * @param url - The URL to request.
38
+ * @param options - The headers, deadline and cap.
39
+ */
40
+ export declare function request(url: string, options: FetchTextOptions): Promise<Result<Answered, ActivityPubFetchError>>;
41
+ /**
42
+ * Reads a successful response's body within the cap, and fails any other status with the
43
+ * code that says what a caller does next: forget the resource on `gone`, sign the request
44
+ * on `unauthorized`, retry on a `5xx` or `429` under `http`.
45
+ *
46
+ * @param url - The URL that was asked for, which the error names.
47
+ * @param answered - The final response and where the chain ended.
48
+ * @param maxBytes - The body cap.
49
+ */
50
+ export declare function readSuccess(url: string, answered: Answered, maxBytes: number): Promise<Result<FetchedText, ActivityPubFetchError>>;
51
+ /**
52
+ * Requests a URL and reads its body when it answers with a success.
53
+ *
54
+ * @param url - The URL to request.
55
+ * @param options - The headers, deadline and cap.
56
+ */
57
+ export declare function fetchText(url: string, options: FetchTextOptions): Promise<Result<FetchedText, ActivityPubFetchError>>;
58
+ /**
59
+ * Decodes a body as one JSON object, the only shape an ActivityStreams or JRD document has.
60
+ *
61
+ * @param url - The document's URL, for the error.
62
+ * @param text - The body.
63
+ */
64
+ export declare function decodeObject(url: string, text: string): Result<Record<string, unknown>, ActivityPubFetchError>;
@@ -0,0 +1,103 @@
1
+ import { follow, readText, release } from "@sdxc/outbound";
2
+ import { failure, isFailure, success } from "@sdxc/result";
3
+ import { ActivityPubFetchError } from "../errors.js";
4
+ /** Long enough for a slow instance, short enough that an inbox request still answers. */
5
+ export const DEFAULT_TIMEOUT = "10 seconds";
6
+ /** An actor with a long bio and many profile fields stays far below a megabyte. */
7
+ export const DEFAULT_MAX_BYTES = 1024 * 1024;
8
+ /**
9
+ * Requests a URL and answers the final response whatever its status, with every hop's
10
+ * host resolved and refused unless all its addresses are public.
11
+ *
12
+ * @param url - The URL to request.
13
+ * @param options - The headers, deadline and cap.
14
+ */
15
+ export async function request(url, options) {
16
+ let followed = await follow(url, {
17
+ headers: options.headers,
18
+ timeout: options.timeout,
19
+ resolve: true,
20
+ });
21
+ if (isFailure(followed))
22
+ return failure(fromOutbound(url, followed.error));
23
+ return success({ response: followed.data.response, url: followed.data.url });
24
+ }
25
+ /**
26
+ * Reads a successful response's body within the cap, and fails any other status with the
27
+ * code that says what a caller does next: forget the resource on `gone`, sign the request
28
+ * on `unauthorized`, retry on a `5xx` or `429` under `http`.
29
+ *
30
+ * @param url - The URL that was asked for, which the error names.
31
+ * @param answered - The final response and where the chain ended.
32
+ * @param maxBytes - The body cap.
33
+ */
34
+ export async function readSuccess(url, answered, maxBytes) {
35
+ let { response } = answered;
36
+ if (!response.ok) {
37
+ release(response.body);
38
+ let status = response.status;
39
+ return failure(new ActivityPubFetchError(statusCode(status), url, `${url} answered ${status}`, { status }));
40
+ }
41
+ let body = await readText(response, { maxBytes });
42
+ if (isFailure(body))
43
+ return failure(fromOutbound(url, body.error, response.status));
44
+ return success({ text: body.data.text, url: answered.url });
45
+ }
46
+ /**
47
+ * Requests a URL and reads its body when it answers with a success.
48
+ *
49
+ * @param url - The URL to request.
50
+ * @param options - The headers, deadline and cap.
51
+ */
52
+ export async function fetchText(url, options) {
53
+ let answered = await request(url, options);
54
+ if (isFailure(answered))
55
+ return answered;
56
+ return readSuccess(url, answered.data, options.maxBytes);
57
+ }
58
+ /**
59
+ * Decodes a body as one JSON object, the only shape an ActivityStreams or JRD document has.
60
+ *
61
+ * @param url - The document's URL, for the error.
62
+ * @param text - The body.
63
+ */
64
+ export function decodeObject(url, text) {
65
+ let json;
66
+ try {
67
+ json = JSON.parse(text);
68
+ }
69
+ catch (cause) {
70
+ return failure(new ActivityPubFetchError("invalid-document", url, `${url} is not JSON`, { cause }));
71
+ }
72
+ if (typeof json !== "object" || json === null || Array.isArray(json)) {
73
+ return failure(new ActivityPubFetchError("invalid-document", url, `${url} is not a JSON object`));
74
+ }
75
+ return success(json);
76
+ }
77
+ /** The code a non-success status maps to. */
78
+ function statusCode(status) {
79
+ if (status === 410)
80
+ return "gone";
81
+ if (status === 404)
82
+ return "not-found";
83
+ if (status === 401 || status === 403)
84
+ return "unauthorized";
85
+ return "http";
86
+ }
87
+ /**
88
+ * Maps an outbound refusal or transport failure to the fetch error, keeping the status
89
+ * when a response had already arrived.
90
+ *
91
+ * @param url - The URL that was asked for.
92
+ * @param error - What `@sdxc/outbound` answered.
93
+ * @param status - The response status, when the failure came while reading its body.
94
+ */
95
+ function fromOutbound(url, error, status) {
96
+ let code = error.code === "too-large" || error.code === "timeout" || error.code === "network"
97
+ ? error.code
98
+ : "refused-url";
99
+ return new ActivityPubFetchError(code, url, error.message, {
100
+ status: status ?? null,
101
+ cause: error,
102
+ });
103
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Why an inbound request was refused: one code per verification step, each with the HTTP
3
+ * status the inbox answers, so a sender learns from a `401` to fall back to another
4
+ * signature scheme while the log keeps the reason the response leaves out.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ import { ActivityPubError } from "../errors.js";
10
+ /**
11
+ * The step of `receive` or `verifyFetch` that stopped a request. `ignored` is the one code
12
+ * answered with `202`: a `Delete` of an account whose key is gone, which no one can verify
13
+ * and every retry would repeat.
14
+ */
15
+ export type InboxErrorCode = "unsupported-media-type" | "too-large" | "invalid-activity" | "blocked" | "unsigned" | "digest-mismatch" | "stale-signature" | "invalid-signature" | "key-unavailable" | "actor-mismatch" | "ignored";
16
+ /**
17
+ * The text a response carries for each code. It names the failed rule and nothing a
18
+ * sender could probe the verifier with; the error's own `message` and `cause` hold the rest.
19
+ */
20
+ export declare const PUBLIC_MESSAGE: Record<InboxErrorCode, string>;
21
+ /**
22
+ * A request the inbox refuses, with the status to answer it with. It never retries: the
23
+ * same request fails the same way, and only the sender can change it.
24
+ */
25
+ export declare class InboxError extends ActivityPubError<InboxErrorCode> {
26
+ name: string;
27
+ /** The HTTP status `toResponse` answers with. */
28
+ readonly status: number;
29
+ /**
30
+ * @param code - The step that failed.
31
+ * @param message - The explanation a log shows; responses carry a fixed text instead.
32
+ * @param options - The underlying error.
33
+ */
34
+ constructor(code: InboxErrorCode, message: string, options?: {
35
+ cause?: unknown;
36
+ });
37
+ /**
38
+ * The response that refuses the request: the error's status and a fixed text per code,
39
+ * so a sender sees which rule failed and never how the verifier failed. `ignored`
40
+ * answers an empty `202`.
41
+ *
42
+ * @example if (isFailure(verified)) return verified.error.toResponse();
43
+ */
44
+ toResponse(): Response;
45
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Why an inbound request was refused: one code per verification step, each with the HTTP
3
+ * status the inbox answers, so a sender learns from a `401` to fall back to another
4
+ * signature scheme while the log keeps the reason the response leaves out.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ import { ActivityPubError } from "../errors.js";
10
+ /** The status each code is answered with. */
11
+ const STATUS = {
12
+ "unsupported-media-type": 415,
13
+ "too-large": 413,
14
+ "invalid-activity": 400,
15
+ blocked: 403,
16
+ unsigned: 401,
17
+ "digest-mismatch": 401,
18
+ "stale-signature": 401,
19
+ "invalid-signature": 401,
20
+ "key-unavailable": 401,
21
+ "actor-mismatch": 401,
22
+ ignored: 202,
23
+ };
24
+ /**
25
+ * The text a response carries for each code. It names the failed rule and nothing a
26
+ * sender could probe the verifier with; the error's own `message` and `cause` hold the rest.
27
+ */
28
+ export const PUBLIC_MESSAGE = {
29
+ "unsupported-media-type": "The body must be application/activity+json or application/ld+json.",
30
+ "too-large": "The body is too large.",
31
+ "invalid-activity": "The body is not an ActivityStreams activity.",
32
+ blocked: "Requests from this server are not accepted.",
33
+ unsigned: "The request must carry an HTTP signature.",
34
+ "digest-mismatch": "The body does not match the signed digest.",
35
+ "stale-signature": "The signature is outside the accepted time window.",
36
+ "invalid-signature": "The signature could not be verified.",
37
+ "key-unavailable": "The signature could not be verified.",
38
+ "actor-mismatch": "The signature does not belong to the activity's actor.",
39
+ ignored: "",
40
+ };
41
+ /**
42
+ * A request the inbox refuses, with the status to answer it with. It never retries: the
43
+ * same request fails the same way, and only the sender can change it.
44
+ */
45
+ export class InboxError extends ActivityPubError {
46
+ name = "InboxError";
47
+ /** The HTTP status `toResponse` answers with. */
48
+ status;
49
+ /**
50
+ * @param code - The step that failed.
51
+ * @param message - The explanation a log shows; responses carry a fixed text instead.
52
+ * @param options - The underlying error.
53
+ */
54
+ constructor(code, message, options = {}) {
55
+ super(code, message, { cause: options.cause });
56
+ this.status = STATUS[code];
57
+ }
58
+ /**
59
+ * The response that refuses the request: the error's status and a fixed text per code,
60
+ * so a sender sees which rule failed and never how the verifier failed. `ignored`
61
+ * answers an empty `202`.
62
+ *
63
+ * @example if (isFailure(verified)) return verified.error.toResponse();
64
+ */
65
+ toResponse() {
66
+ let text = PUBLIC_MESSAGE[this.code];
67
+ if (text === "")
68
+ return new Response(null, { status: this.status });
69
+ return new Response(text, {
70
+ status: this.status,
71
+ headers: { "content-type": "text/plain; charset=utf-8" },
72
+ });
73
+ }
74
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * The background half of an inbox: the work the protocol itself requires for each verified
3
+ * activity (followers kept, Accept sent, ownership and relevance checked) before the app's
4
+ * own handler sees it, so an app writes only what it does with a reply, a like or a boost.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ import type { DurationInput } from "@sdxc/duration";
10
+ import type { Result } from "@sdxc/result";
11
+ import type { Resolver } from "../remote.js";
12
+ import type { FollowerStore, KeyProvider, LocalObjects, SeenActivities } from "../store.js";
13
+ import { ActivityPubError } from "../errors.js";
14
+ import type { BlockedCheck, Received, Verification } from "./inbox-receive.js";
15
+ import type { ActivityPub } from "./types.js";
16
+ /** What the app answers a Follow with: approve it, refuse it, or decide later. */
17
+ export type FollowDecision = "accept" | "reject" | "pending";
18
+ /** A verified activity as an app's handler receives it, after the package's own checks. */
19
+ export interface Inbound<A extends ActivityPub.Activity = ActivityPub.Activity> {
20
+ activity: A;
21
+ /** The sender, fetched through the resolver; trusted, unlike any actor the activity embeds. */
22
+ actor: ActivityPub.Actor;
23
+ /** The origin of the actor, which every ownership check compared against. */
24
+ origin: string;
25
+ verification: Verification;
26
+ /**
27
+ * What the activity acts on, already trusted: a Create's or Update's object, the local
28
+ * object a Like or Announce names, the activity an Undo reverts, the Follow an Accept
29
+ * answers, a Move's target actor. `null` when there is nothing to resolve.
30
+ */
31
+ object: ActivityPub.Object | null;
32
+ /** The local object or actor the activity concerns, which a reply or like attaches to. */
33
+ target: string | null;
34
+ receivedAt: Date;
35
+ }
36
+ /**
37
+ * A verified `Delete`. A deleted account's own document is usually gone already, so
38
+ * `actor` is `null` when it could not be fetched.
39
+ */
40
+ export interface DeleteInbound extends Omit<Inbound, "actor"> {
41
+ actor: ActivityPub.Actor | null;
42
+ /** What was deleted: one object by its id, or the whole account. */
43
+ deleted: {
44
+ kind: "object" | "actor";
45
+ id: string;
46
+ };
47
+ }
48
+ /**
49
+ * The app's side of each activity, each called once the package has done the protocol's
50
+ * work. A failure is retried, so a handler must tolerate seeing an activity twice.
51
+ */
52
+ export interface Handlers {
53
+ /** Answers the Follow; `null` keeps the default, which `manuallyApprovesFollowers` sets. */
54
+ follow(inbound: Inbound): Promise<Result<FollowDecision | null, Error>>;
55
+ /** An Undo whose reverted activity was the sender's own; a Follow is already removed. */
56
+ undo(inbound: Inbound): Promise<Result<void, Error>>;
57
+ /** A post by the sender that replies to, quotes or mentions something local. */
58
+ create(inbound: Inbound): Promise<Result<void, Error>>;
59
+ /** An edit of such a post, or of the sender's own actor (then `object` is the actor). */
60
+ update(inbound: Inbound): Promise<Result<void, Error>>;
61
+ delete(inbound: DeleteInbound): Promise<Result<void, Error>>;
62
+ /** A Like of a local object; Pleroma's `EmojiReact` arrives here too. */
63
+ like(inbound: Inbound): Promise<Result<void, Error>>;
64
+ /** A boost of a local object. */
65
+ announce(inbound: Inbound): Promise<Result<void, Error>>;
66
+ /** A remote actor approved a Follow a local actor sent. */
67
+ accept(inbound: Inbound): Promise<Result<void, Error>>;
68
+ reject(inbound: Inbound): Promise<Result<void, Error>>;
69
+ /** The sender moved to `object`, which lists it in `alsoKnownAs` and that it names in `movedTo`. */
70
+ move(inbound: Inbound): Promise<Result<void, Error>>;
71
+ }
72
+ /** What `handle` needs: the local actor, the app's stores, and how to send an answer. */
73
+ export interface HandleOptions {
74
+ /** The local actor's document, which Follows name and Accepts are sent as. */
75
+ actor: ActivityPub.Actor;
76
+ /** Tells a local actor from a remote one, beyond `actor` itself. */
77
+ keys: KeyProvider;
78
+ followers: FollowerStore;
79
+ seen: SeenActivities;
80
+ objects: LocalObjects;
81
+ resolver: Resolver;
82
+ blocked: BlockedCheck;
83
+ /** Queues a serialized activity for delivery to one inbox, usually as a delivery job. */
84
+ send: (activity: string, inbox: string) => Promise<Result<void, Error>>;
85
+ on?: Partial<Handlers>;
86
+ /** @default "1 day" */
87
+ seenTtl?: DurationInput;
88
+ /**
89
+ * The job's attempt, starting at 1. Past the first the claim is skipped, so a retry of
90
+ * a failed attempt is never mistaken for a redelivery. @default 1
91
+ */
92
+ attempts?: number;
93
+ }
94
+ /** Why an activity was acknowledged without reaching a handler, or how a Follow ended. */
95
+ export type HandleReason = "blocked" | "actor-unavailable" | "not-addressed" | "not-owner" | "unrelated" | "unverifiable" | "unhandled" | "rejected" | "pending";
96
+ /** What `handle` did with an activity; every outcome is acknowledged, never retried. */
97
+ export interface HandleOutcome {
98
+ /** `processed` ran the package's work and the handler, `duplicate` was claimed before. */
99
+ status: "processed" | "duplicate" | "ignored";
100
+ type: string;
101
+ reason: HandleReason | null;
102
+ }
103
+ /**
104
+ * Processes a verified activity. It claims the id in `seen` (a store failure is logged and
105
+ * processing goes on, since every step is idempotent), re-reads the activity, does the work
106
+ * the protocol requires for its type, then calls the app's handler for it.
107
+ *
108
+ * The sender is the document whose id is the activity's actor, and an object another
109
+ * origin embeds is fetched from its own origin (FEP-c7d3). A failure is retryable when a
110
+ * remote server, a store, a handler or `send` failed transiently; any other is acknowledged.
111
+ *
112
+ * @param input - What `receive` answered, as the job read it back from the queue.
113
+ * @param options - The local actor, the stores, the resolver, `send` and the handlers.
114
+ * @returns What happened to the activity.
115
+ * @example
116
+ * let outcome = await handle(ctx.input, { actor, keys, followers, seen, objects, resolver, blocked, send, on });
117
+ * if (isFailure(outcome) && outcome.error.retryable) return ctx.retry({ cause: outcome.error });
118
+ */
119
+ export declare function handle(input: Received, options: HandleOptions): Promise<Result<HandleOutcome, ActivityPubError>>;