@fedify/relay 2.0.0-dev.100

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/dist/mod.cjs ADDED
@@ -0,0 +1,438 @@
1
+
2
+ const { Temporal } = require("@js-temporal/polyfill");
3
+
4
+ //#region rolldown:runtime
5
+ var __create = Object.create;
6
+ var __defProp = Object.defineProperty;
7
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
8
+ var __getOwnPropNames = Object.getOwnPropertyNames;
9
+ var __getProtoOf = Object.getPrototypeOf;
10
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
13
+ key = keys[i];
14
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
15
+ get: ((k) => from[k]).bind(null, key),
16
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
17
+ });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
22
+ value: mod,
23
+ enumerable: true
24
+ }) : target, mod));
25
+
26
+ //#endregion
27
+ const __fedify_fedify = __toESM(require("@fedify/fedify"));
28
+ const __fedify_fedify_vocab = __toESM(require("@fedify/fedify/vocab"));
29
+ const __logtape_logtape = __toESM(require("@logtape/logtape"));
30
+
31
+ //#region src/types.ts
32
+ const RELAY_SERVER_ACTOR = "relay";
33
+ /**
34
+ * Type predicate to check if a value is valid RelayFollowerData from KV store.
35
+ * Validates the storage format (JSON-LD), not the deserialized Actor instance.
36
+ *
37
+ * @param value The value to check
38
+ * @returns true if the value is a RelayFollowerData
39
+ * @internal
40
+ */
41
+ function isRelayFollowerData(value) {
42
+ if (!value || typeof value !== "object") return false;
43
+ const obj = value;
44
+ return "actor" in obj && "state" in obj && typeof obj.state === "string" && (obj.state === "pending" || obj.state === "accepted");
45
+ }
46
+
47
+ //#endregion
48
+ //#region src/builder.ts
49
+ const relayBuilder = (0, __fedify_fedify.createFederationBuilder)();
50
+ relayBuilder.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
51
+ if (identifier !== RELAY_SERVER_ACTOR) return null;
52
+ const keys = await ctx.getActorKeyPairs(identifier);
53
+ return new __fedify_fedify_vocab.Application({
54
+ id: ctx.getActorUri(identifier),
55
+ preferredUsername: identifier,
56
+ name: ctx.data.name ?? "ActivityPub Relay",
57
+ inbox: ctx.getInboxUri(),
58
+ followers: ctx.getFollowersUri(identifier),
59
+ following: ctx.getFollowingUri(identifier),
60
+ url: ctx.getActorUri(identifier),
61
+ publicKey: keys[0].cryptographicKey,
62
+ assertionMethods: keys.map((k) => k.multikey)
63
+ });
64
+ }).setKeyPairsDispatcher(async (ctx, identifier) => {
65
+ if (identifier !== RELAY_SERVER_ACTOR) return [];
66
+ const rsaPairJson = await ctx.data.kv.get([
67
+ "keypair",
68
+ "rsa",
69
+ identifier
70
+ ]);
71
+ const ed25519PairJson = await ctx.data.kv.get([
72
+ "keypair",
73
+ "ed25519",
74
+ identifier
75
+ ]);
76
+ if (rsaPairJson == null || ed25519PairJson == null) {
77
+ const rsaPair$1 = await (0, __fedify_fedify.generateCryptoKeyPair)("RSASSA-PKCS1-v1_5");
78
+ const ed25519Pair$1 = await (0, __fedify_fedify.generateCryptoKeyPair)("Ed25519");
79
+ await ctx.data.kv.set([
80
+ "keypair",
81
+ "rsa",
82
+ identifier
83
+ ], {
84
+ privateKey: await (0, __fedify_fedify.exportJwk)(rsaPair$1.privateKey),
85
+ publicKey: await (0, __fedify_fedify.exportJwk)(rsaPair$1.publicKey)
86
+ });
87
+ await ctx.data.kv.set([
88
+ "keypair",
89
+ "ed25519",
90
+ identifier
91
+ ], {
92
+ privateKey: await (0, __fedify_fedify.exportJwk)(ed25519Pair$1.privateKey),
93
+ publicKey: await (0, __fedify_fedify.exportJwk)(ed25519Pair$1.publicKey)
94
+ });
95
+ return [rsaPair$1, ed25519Pair$1];
96
+ }
97
+ const rsaPair = {
98
+ privateKey: await (0, __fedify_fedify.importJwk)(rsaPairJson.privateKey, "private"),
99
+ publicKey: await (0, __fedify_fedify.importJwk)(rsaPairJson.publicKey, "public")
100
+ };
101
+ const ed25519Pair = {
102
+ privateKey: await (0, __fedify_fedify.importJwk)(ed25519PairJson.privateKey, "private"),
103
+ publicKey: await (0, __fedify_fedify.importJwk)(ed25519PairJson.publicKey, "public")
104
+ };
105
+ return [rsaPair, ed25519Pair];
106
+ });
107
+ async function getFollowerActors(ctx) {
108
+ const actors = [];
109
+ for await (const { value } of ctx.data.kv.list(["follower"])) {
110
+ if (!isRelayFollowerData(value)) continue;
111
+ if (value.state !== "accepted") continue;
112
+ const actor = await __fedify_fedify_vocab.Object.fromJsonLd(value.actor);
113
+ if (!(0, __fedify_fedify_vocab.isActor)(actor)) continue;
114
+ actors.push(actor);
115
+ }
116
+ return actors;
117
+ }
118
+ async function dispatchRelayActors(ctx, identifier) {
119
+ if (identifier !== RELAY_SERVER_ACTOR) return null;
120
+ const actors = await getFollowerActors(ctx);
121
+ return { items: actors };
122
+ }
123
+ relayBuilder.setFollowersDispatcher("/users/{identifier}/followers", dispatchRelayActors);
124
+ relayBuilder.setFollowingDispatcher("/users/{identifier}/following", dispatchRelayActors);
125
+
126
+ //#endregion
127
+ //#region src/base.ts
128
+ /**
129
+ * Abstract base class for relay implementations.
130
+ * Provides common infrastructure for both Mastodon and LitePub relays.
131
+ *
132
+ * @internal
133
+ */
134
+ var BaseRelay = class {
135
+ federationBuilder;
136
+ options;
137
+ federation;
138
+ constructor(options, relayBuilder$1) {
139
+ this.options = options;
140
+ this.federationBuilder = relayBuilder$1;
141
+ }
142
+ async fetch(request) {
143
+ if (this.federation == null) {
144
+ this.federation = await this.federationBuilder.build(this.options);
145
+ this.setupInboxListeners();
146
+ }
147
+ return await this.federation.fetch(request, { contextData: this.options });
148
+ }
149
+ /**
150
+ * Helper method to parse and validate follower data from storage.
151
+ * Deserializes JSON-LD actor data and validates it.
152
+ *
153
+ * @param actorId The actor ID of the follower
154
+ * @param data Raw data from KV store
155
+ * @returns RelayFollower object if valid, null otherwise
156
+ * @internal
157
+ */
158
+ async parseFollowerData(actorId, data) {
159
+ if (!isRelayFollowerData(data)) return null;
160
+ const actor = await __fedify_fedify_vocab.Object.fromJsonLd(data.actor);
161
+ if (!(0, __fedify_fedify_vocab.isActor)(actor)) return null;
162
+ return {
163
+ actorId,
164
+ actor,
165
+ state: data.state
166
+ };
167
+ }
168
+ /**
169
+ * Lists all followers of the relay.
170
+ *
171
+ * @returns An async iterator of follower entries
172
+ *
173
+ * @example
174
+ * ```ts
175
+ * import { createRelay } from "@fedify/relay";
176
+ * import { MemoryKvStore } from "@fedify/fedify";
177
+ *
178
+ * const relay = createRelay("mastodon", {
179
+ * kv: new MemoryKvStore(),
180
+ * domain: "relay.example.com",
181
+ * subscriptionHandler: async (ctx, actor) => true,
182
+ * });
183
+ *
184
+ * for await (const follower of relay.listFollowers()) {
185
+ * console.log(`Follower: ${follower.actorId}`);
186
+ * console.log(`State: ${follower.state}`);
187
+ * console.log(`Actor: ${follower.actor.name}`);
188
+ * }
189
+ * ```
190
+ *
191
+ * @since 2.0.0
192
+ */
193
+ async *listFollowers() {
194
+ for await (const entry of this.options.kv.list(["follower"])) {
195
+ const actorId = entry.key[1];
196
+ if (typeof actorId !== "string") continue;
197
+ const follower = await this.parseFollowerData(actorId, entry.value);
198
+ if (follower) yield follower;
199
+ }
200
+ }
201
+ /**
202
+ * Gets a specific follower by actor ID.
203
+ *
204
+ * @param actorId The actor ID (URL) of the follower to retrieve
205
+ * @returns The follower entry if found, null otherwise
206
+ *
207
+ * @example
208
+ * ```ts
209
+ * import { createRelay } from "@fedify/relay";
210
+ * import { MemoryKvStore } from "@fedify/fedify";
211
+ *
212
+ * const relay = createRelay("mastodon", {
213
+ * kv: new MemoryKvStore(),
214
+ * domain: "relay.example.com",
215
+ * subscriptionHandler: async (ctx, actor) => true,
216
+ * });
217
+ *
218
+ * const follower = await relay.getFollower(
219
+ * "https://mastodon.example.com/users/alice"
220
+ * );
221
+ * if (follower) {
222
+ * console.log(`State: ${follower.state}`);
223
+ * console.log(`Actor: ${follower.actor.preferredUsername}`);
224
+ * }
225
+ * ```
226
+ *
227
+ * @since 2.0.0
228
+ */
229
+ async getFollower(actorId) {
230
+ const followerData = await this.options.kv.get(["follower", actorId]);
231
+ return await this.parseFollowerData(actorId, followerData);
232
+ }
233
+ };
234
+
235
+ //#endregion
236
+ //#region src/follow.ts
237
+ /**
238
+ * Validate Follow activity and return follower actor if valid.
239
+ * This validation is common to both Mastodon and LitePub relay protocols.
240
+ *
241
+ * @param ctx The federation context
242
+ * @param follow The Follow activity to validate
243
+ * @returns The follower Actor if valid, null otherwise
244
+ */
245
+ async function validateFollowActivity(ctx, follow) {
246
+ if (follow.id == null || follow.objectId == null) return null;
247
+ const parsed = ctx.parseUri(follow.objectId);
248
+ const isPublicFollow = follow.objectId.href === "https://www.w3.org/ns/activitystreams#Public";
249
+ if (!isPublicFollow && parsed?.type !== "actor") return null;
250
+ const follower = await follow.getActor(ctx);
251
+ if (follower == null || follower.id == null || follower.preferredUsername == null || follower.inboxId == null) return null;
252
+ return follower;
253
+ }
254
+ /**
255
+ * Send Accept or Reject response for a Follow activity.
256
+ * This is common to both Mastodon and LitePub relay protocols.
257
+ *
258
+ * @param ctx The federation context
259
+ * @param follow The Follow activity being responded to
260
+ * @param follower The actor who sent the Follow
261
+ * @param approved Whether the follow was approved
262
+ */
263
+ async function sendFollowResponse(ctx, follow, follower, approved) {
264
+ const relayActorUri = ctx.getActorUri(RELAY_SERVER_ACTOR);
265
+ const Activity = approved ? __fedify_fedify.Accept : __fedify_fedify.Reject;
266
+ const action = approved ? "accepts" : "rejects";
267
+ await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new Activity({
268
+ id: new URL(`#${action}`, relayActorUri),
269
+ actor: relayActorUri,
270
+ object: follow
271
+ }));
272
+ }
273
+ /**
274
+ * Handle Undo activity for Follow.
275
+ * This logic is identical for both Mastodon and LitePub relay protocols.
276
+ *
277
+ * @param ctx The federation context
278
+ * @param undo The Undo activity to handle
279
+ * @param logger The logger instance to use for warnings
280
+ */
281
+ async function handleUndoFollow(ctx, undo, logger$2) {
282
+ const activity = await undo.getObject({
283
+ crossOrigin: "trust",
284
+ ...ctx
285
+ });
286
+ if (activity instanceof __fedify_fedify.Follow) {
287
+ if (activity.id == null || activity.actorId == null) return;
288
+ await ctx.data.kv.delete(["follower", activity.actorId.href]);
289
+ } else logger$2.warn("Unsupported object type ({type}) for Undo activity: {object}", {
290
+ type: activity?.constructor.name,
291
+ object: activity
292
+ });
293
+ }
294
+
295
+ //#endregion
296
+ //#region src/litepub.ts
297
+ const logger$1 = (0, __logtape_logtape.getLogger)([
298
+ "fedify",
299
+ "relay",
300
+ "litepub"
301
+ ]);
302
+ /**
303
+ * A LitePub-compatible ActivityPub relay implementation.
304
+ * This relay follows LitePub's relay protocol and extensions for
305
+ * enhanced federation capabilities.
306
+ *
307
+ * @since 2.0.0
308
+ */
309
+ var LitePubRelay = class extends BaseRelay {
310
+ async #announceToFollowers(ctx, activity) {
311
+ const sender = await activity.getActor(ctx);
312
+ const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
313
+ const announce = new __fedify_fedify.Announce({
314
+ id: new URL(`/announce#${crypto.randomUUID()}`, ctx.origin),
315
+ actor: ctx.getActorUri(RELAY_SERVER_ACTOR),
316
+ object: activity.objectId,
317
+ to: __fedify_fedify.PUBLIC_COLLECTION,
318
+ published: Temporal.Now.instant()
319
+ });
320
+ await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", announce, {
321
+ excludeBaseUris,
322
+ preferSharedInbox: true
323
+ });
324
+ }
325
+ setupInboxListeners() {
326
+ if (this.federation != null) this.federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(__fedify_fedify.Follow, async (ctx, follow) => {
327
+ const follower = await validateFollowActivity(ctx, follow);
328
+ if (!follower || !follower.id) return;
329
+ const existingFollow = await ctx.data.kv.get(["follower", follower.id.href]);
330
+ if (existingFollow?.state === "pending") return;
331
+ const approved = await this.options.subscriptionHandler(ctx, follower);
332
+ if (approved) {
333
+ await ctx.data.kv.set(["follower", follower.id.href], {
334
+ actor: await follower.toJsonLd(),
335
+ state: "pending"
336
+ });
337
+ await sendFollowResponse(ctx, follow, follower, approved);
338
+ const relayActorUri = ctx.getActorUri(RELAY_SERVER_ACTOR);
339
+ await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new __fedify_fedify.Follow({
340
+ actor: relayActorUri,
341
+ object: follower.id,
342
+ to: follower.id
343
+ }));
344
+ } else await sendFollowResponse(ctx, follow, follower, approved);
345
+ }).on(__fedify_fedify.Accept, async (ctx, accept) => {
346
+ const follow = await accept.getObject({
347
+ crossOrigin: "trust",
348
+ ...ctx
349
+ });
350
+ if (!(follow instanceof __fedify_fedify.Follow)) return;
351
+ const relayActorId = follow.actorId;
352
+ if (relayActorId == null) return;
353
+ const followerActor = await accept.getActor();
354
+ if (!(0, __fedify_fedify.isActor)(followerActor) || !followerActor.id) return;
355
+ const parsed = ctx.parseUri(relayActorId);
356
+ if (parsed == null || parsed.type !== "actor") return;
357
+ const followerData = await ctx.data.kv.get(["follower", followerActor.id.href]);
358
+ if (followerData == null) return;
359
+ const updatedFollowerData = {
360
+ ...followerData,
361
+ state: "accepted"
362
+ };
363
+ await ctx.data.kv.set(["follower", followerActor.id.href], updatedFollowerData);
364
+ }).on(__fedify_fedify.Undo, async (ctx, undo) => await handleUndoFollow(ctx, undo, logger$1)).on(__fedify_fedify.Create, async (ctx, create) => await this.#announceToFollowers(ctx, create)).on(__fedify_fedify.Update, async (ctx, update) => await this.#announceToFollowers(ctx, update)).on(__fedify_fedify.Move, async (ctx, move) => await this.#announceToFollowers(ctx, move)).on(__fedify_fedify.Delete, async (ctx, deleteActivity) => await this.#announceToFollowers(ctx, deleteActivity)).on(__fedify_fedify.Announce, async (ctx, announce) => await this.#announceToFollowers(ctx, announce));
365
+ }
366
+ };
367
+
368
+ //#endregion
369
+ //#region src/mastodon.ts
370
+ const logger = (0, __logtape_logtape.getLogger)([
371
+ "fedify",
372
+ "relay",
373
+ "mastodon"
374
+ ]);
375
+ /**
376
+ * A Mastodon-compatible ActivityPub relay implementation.
377
+ * This relay follows Mastodon's relay protocol for compatibility
378
+ * with Mastodon instances.
379
+ *
380
+ * @since 2.0.0
381
+ */
382
+ var MastodonRelay = class extends BaseRelay {
383
+ async #forwardToFollowers(ctx, activity) {
384
+ const sender = await activity.getActor(ctx);
385
+ const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
386
+ await ctx.forwardActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", {
387
+ skipIfUnsigned: true,
388
+ excludeBaseUris,
389
+ preferSharedInbox: true
390
+ });
391
+ }
392
+ setupInboxListeners() {
393
+ if (this.federation != null) this.federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(__fedify_fedify.Follow, async (ctx, follow) => {
394
+ const follower = await validateFollowActivity(ctx, follow);
395
+ if (!follower || !follower.id) return;
396
+ const approved = await this.options.subscriptionHandler(ctx, follower);
397
+ if (approved) await ctx.data.kv.set(["follower", follower.id.href], {
398
+ actor: await follower.toJsonLd(),
399
+ state: "accepted"
400
+ });
401
+ await sendFollowResponse(ctx, follow, follower, approved);
402
+ }).on(__fedify_fedify.Undo, async (ctx, undo) => await handleUndoFollow(ctx, undo, logger)).on(__fedify_fedify.Create, async (ctx, create) => await this.#forwardToFollowers(ctx, create)).on(__fedify_fedify.Delete, async (ctx, deleteActivity) => await this.#forwardToFollowers(ctx, deleteActivity)).on(__fedify_fedify.Move, async (ctx, move) => await this.#forwardToFollowers(ctx, move)).on(__fedify_fedify.Update, async (ctx, update) => await this.#forwardToFollowers(ctx, update)).on(__fedify_fedify.Announce, async (ctx, announce) => await this.#forwardToFollowers(ctx, announce));
403
+ }
404
+ };
405
+
406
+ //#endregion
407
+ //#region src/factory.ts
408
+ /**
409
+ * Factory function to create a relay instance.
410
+ *
411
+ * @param type The type of relay to create ("mastodon" or "litepub")
412
+ * @param options Configuration options for the relay
413
+ * @returns A relay instance
414
+ *
415
+ * @example
416
+ * ```ts
417
+ * import { createRelay } from "@fedify/relay";
418
+ * import { MemoryKvStore } from "@fedify/fedify";
419
+ *
420
+ * const relay = createRelay("mastodon", {
421
+ * kv: new MemoryKvStore(),
422
+ * domain: "relay.example.com",
423
+ * subscriptionHandler: async (ctx, actor) => true,
424
+ * });
425
+ * ```
426
+ *
427
+ * @since 2.0.0
428
+ */
429
+ function createRelay(type, options) {
430
+ switch (type) {
431
+ case "mastodon": return new MastodonRelay(options, relayBuilder);
432
+ case "litepub": return new LitePubRelay(options, relayBuilder);
433
+ }
434
+ }
435
+
436
+ //#endregion
437
+ exports.RELAY_SERVER_ACTOR = RELAY_SERVER_ACTOR;
438
+ exports.createRelay = createRelay;
package/dist/mod.d.cts ADDED
@@ -0,0 +1,110 @@
1
+ import { Context, KvStore, MessageQueue } from "@fedify/fedify";
2
+ import { Actor } from "@fedify/fedify/vocab";
3
+ import { AuthenticatedDocumentLoaderFactory, DocumentLoaderFactory } from "@fedify/vocab-runtime";
4
+
5
+ //#region src/types.d.ts
6
+ declare const RELAY_SERVER_ACTOR = "relay";
7
+ /**
8
+ * Supported relay types.
9
+ */
10
+ type RelayType = "mastodon" | "litepub";
11
+ /**
12
+ * Handler for subscription requests (Follow/Undo activities).
13
+ */
14
+ type SubscriptionRequestHandler = (ctx: Context<RelayOptions>, clientActor: Actor) => Promise<boolean>;
15
+ /**
16
+ * Configuration options for the ActivityPub relay.
17
+ */
18
+ interface RelayOptions {
19
+ kv: KvStore;
20
+ domain?: string;
21
+ name?: string;
22
+ documentLoaderFactory?: DocumentLoaderFactory;
23
+ authenticatedDocumentLoaderFactory?: AuthenticatedDocumentLoaderFactory;
24
+ queue?: MessageQueue;
25
+ subscriptionHandler: SubscriptionRequestHandler;
26
+ }
27
+ /**
28
+ * Internal storage format for follower data in KV store.
29
+ * Contains JSON-LD representation of the actor.
30
+ * Exported for internal package use but not re-exported from mod.ts.
31
+ *
32
+ * @internal
33
+ */
34
+
35
+ /**
36
+ * A follower of the relay with validated Actor instance.
37
+ * This is the public API type returned by follower query methods.
38
+ *
39
+ * @since 2.0.0
40
+ */
41
+ interface RelayFollower {
42
+ /** The actor ID (URL) of the follower. */
43
+ readonly actorId: string;
44
+ /** The validated Actor object. */
45
+ readonly actor: Actor;
46
+ /** The follower's state. */
47
+ readonly state: "pending" | "accepted";
48
+ }
49
+ /**
50
+ * Public interface for ActivityPub relay implementations.
51
+ * Use {@link createRelay} to create a relay instance.
52
+ *
53
+ * @since 2.0.0
54
+ */
55
+ interface Relay {
56
+ /**
57
+ * Handle incoming HTTP requests.
58
+ *
59
+ * @param request The incoming HTTP request
60
+ * @returns The HTTP response
61
+ */
62
+ fetch(request: Request): Promise<Response>;
63
+ /**
64
+ * Lists all followers of the relay.
65
+ *
66
+ * @returns An async iterator of follower entries
67
+ */
68
+ listFollowers(): AsyncIterableIterator<RelayFollower>;
69
+ /**
70
+ * Gets a specific follower by actor ID.
71
+ *
72
+ * @param actorId The actor ID (URL) of the follower to retrieve
73
+ * @returns The follower entry if found, null otherwise
74
+ */
75
+ getFollower(actorId: string): Promise<RelayFollower | null>;
76
+ }
77
+ /**
78
+ * Type predicate to check if a value is valid RelayFollowerData from KV store.
79
+ * Validates the storage format (JSON-LD), not the deserialized Actor instance.
80
+ *
81
+ * @param value The value to check
82
+ * @returns true if the value is a RelayFollowerData
83
+ * @internal
84
+ */
85
+ //#endregion
86
+ //#region src/factory.d.ts
87
+ /**
88
+ * Factory function to create a relay instance.
89
+ *
90
+ * @param type The type of relay to create ("mastodon" or "litepub")
91
+ * @param options Configuration options for the relay
92
+ * @returns A relay instance
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * import { createRelay } from "@fedify/relay";
97
+ * import { MemoryKvStore } from "@fedify/fedify";
98
+ *
99
+ * const relay = createRelay("mastodon", {
100
+ * kv: new MemoryKvStore(),
101
+ * domain: "relay.example.com",
102
+ * subscriptionHandler: async (ctx, actor) => true,
103
+ * });
104
+ * ```
105
+ *
106
+ * @since 2.0.0
107
+ */
108
+ declare function createRelay(type: RelayType, options: RelayOptions): Relay;
109
+ //#endregion
110
+ export { RELAY_SERVER_ACTOR, Relay, RelayFollower, RelayOptions, RelayType, SubscriptionRequestHandler, createRelay };
package/dist/mod.d.ts ADDED
@@ -0,0 +1,111 @@
1
+ import { Temporal } from "@js-temporal/polyfill";
2
+ import { Context, KvStore, MessageQueue } from "@fedify/fedify";
3
+ import { Actor } from "@fedify/fedify/vocab";
4
+ import { AuthenticatedDocumentLoaderFactory, DocumentLoaderFactory } from "@fedify/vocab-runtime";
5
+
6
+ //#region src/types.d.ts
7
+ declare const RELAY_SERVER_ACTOR = "relay";
8
+ /**
9
+ * Supported relay types.
10
+ */
11
+ type RelayType = "mastodon" | "litepub";
12
+ /**
13
+ * Handler for subscription requests (Follow/Undo activities).
14
+ */
15
+ type SubscriptionRequestHandler = (ctx: Context<RelayOptions>, clientActor: Actor) => Promise<boolean>;
16
+ /**
17
+ * Configuration options for the ActivityPub relay.
18
+ */
19
+ interface RelayOptions {
20
+ kv: KvStore;
21
+ domain?: string;
22
+ name?: string;
23
+ documentLoaderFactory?: DocumentLoaderFactory;
24
+ authenticatedDocumentLoaderFactory?: AuthenticatedDocumentLoaderFactory;
25
+ queue?: MessageQueue;
26
+ subscriptionHandler: SubscriptionRequestHandler;
27
+ }
28
+ /**
29
+ * Internal storage format for follower data in KV store.
30
+ * Contains JSON-LD representation of the actor.
31
+ * Exported for internal package use but not re-exported from mod.ts.
32
+ *
33
+ * @internal
34
+ */
35
+
36
+ /**
37
+ * A follower of the relay with validated Actor instance.
38
+ * This is the public API type returned by follower query methods.
39
+ *
40
+ * @since 2.0.0
41
+ */
42
+ interface RelayFollower {
43
+ /** The actor ID (URL) of the follower. */
44
+ readonly actorId: string;
45
+ /** The validated Actor object. */
46
+ readonly actor: Actor;
47
+ /** The follower's state. */
48
+ readonly state: "pending" | "accepted";
49
+ }
50
+ /**
51
+ * Public interface for ActivityPub relay implementations.
52
+ * Use {@link createRelay} to create a relay instance.
53
+ *
54
+ * @since 2.0.0
55
+ */
56
+ interface Relay {
57
+ /**
58
+ * Handle incoming HTTP requests.
59
+ *
60
+ * @param request The incoming HTTP request
61
+ * @returns The HTTP response
62
+ */
63
+ fetch(request: Request): Promise<Response>;
64
+ /**
65
+ * Lists all followers of the relay.
66
+ *
67
+ * @returns An async iterator of follower entries
68
+ */
69
+ listFollowers(): AsyncIterableIterator<RelayFollower>;
70
+ /**
71
+ * Gets a specific follower by actor ID.
72
+ *
73
+ * @param actorId The actor ID (URL) of the follower to retrieve
74
+ * @returns The follower entry if found, null otherwise
75
+ */
76
+ getFollower(actorId: string): Promise<RelayFollower | null>;
77
+ }
78
+ /**
79
+ * Type predicate to check if a value is valid RelayFollowerData from KV store.
80
+ * Validates the storage format (JSON-LD), not the deserialized Actor instance.
81
+ *
82
+ * @param value The value to check
83
+ * @returns true if the value is a RelayFollowerData
84
+ * @internal
85
+ */
86
+ //#endregion
87
+ //#region src/factory.d.ts
88
+ /**
89
+ * Factory function to create a relay instance.
90
+ *
91
+ * @param type The type of relay to create ("mastodon" or "litepub")
92
+ * @param options Configuration options for the relay
93
+ * @returns A relay instance
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * import { createRelay } from "@fedify/relay";
98
+ * import { MemoryKvStore } from "@fedify/fedify";
99
+ *
100
+ * const relay = createRelay("mastodon", {
101
+ * kv: new MemoryKvStore(),
102
+ * domain: "relay.example.com",
103
+ * subscriptionHandler: async (ctx, actor) => true,
104
+ * });
105
+ * ```
106
+ *
107
+ * @since 2.0.0
108
+ */
109
+ declare function createRelay(type: RelayType, options: RelayOptions): Relay;
110
+ //#endregion
111
+ export { RELAY_SERVER_ACTOR, Relay, RelayFollower, RelayOptions, RelayType, SubscriptionRequestHandler, createRelay };