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