@rmdes/indiekit-endpoint-activitypub 3.6.2 → 3.6.4
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.
|
@@ -21,6 +21,7 @@ export async function backfillTimeline(collections) {
|
|
|
21
21
|
|
|
22
22
|
// Get local profile for author info
|
|
23
23
|
const profile = await ap_profile?.findOne({});
|
|
24
|
+
const siteUrl = profile?.url?.replace(/\/$/, "") || "";
|
|
24
25
|
const author = profile
|
|
25
26
|
? {
|
|
26
27
|
name: profile.name || "",
|
|
@@ -58,24 +59,30 @@ export async function backfillTimeline(collections) {
|
|
|
58
59
|
continue;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
//
|
|
62
|
-
|
|
62
|
+
// Build content — interaction types (bookmark, like, repost) may not have
|
|
63
|
+
// body content, so synthesize it from the interaction target URL
|
|
64
|
+
const content = buildContent(props);
|
|
63
65
|
const type = mapPostType(props["post-type"]);
|
|
64
66
|
|
|
67
|
+
// Extract categories + inline hashtags from content
|
|
68
|
+
const categories = normalizeArray(props.category);
|
|
69
|
+
const inlineHashtags = extractHashtags(content.text + " " + (content.html || ""));
|
|
70
|
+
const mergedCategories = mergeCategories(categories, inlineHashtags);
|
|
71
|
+
|
|
65
72
|
const timelineItem = {
|
|
66
73
|
uid,
|
|
67
74
|
url: uid,
|
|
68
75
|
type,
|
|
69
|
-
content,
|
|
76
|
+
content: rewriteHashtagLinks(content, siteUrl),
|
|
70
77
|
author,
|
|
71
78
|
published: props.published || props.date || new Date().toISOString(),
|
|
72
79
|
createdAt: props.published || props.date || new Date().toISOString(),
|
|
73
80
|
visibility: "public",
|
|
74
81
|
sensitive: false,
|
|
75
|
-
category:
|
|
76
|
-
photo: normalizeMediaArray(props.photo),
|
|
77
|
-
video: normalizeMediaArray(props.video),
|
|
78
|
-
audio: normalizeMediaArray(props.audio),
|
|
82
|
+
category: mergedCategories,
|
|
83
|
+
photo: normalizeMediaArray(props.photo, siteUrl),
|
|
84
|
+
video: normalizeMediaArray(props.video, siteUrl),
|
|
85
|
+
audio: normalizeMediaArray(props.audio, siteUrl),
|
|
79
86
|
readBy: [],
|
|
80
87
|
};
|
|
81
88
|
|
|
@@ -107,6 +114,55 @@ export async function backfillTimeline(collections) {
|
|
|
107
114
|
return { total: allPosts.length, inserted, skipped };
|
|
108
115
|
}
|
|
109
116
|
|
|
117
|
+
// ─── Content Building ─────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build content from JF2 properties, synthesizing content for interaction types.
|
|
121
|
+
* Bookmarks, likes, and reposts often have no body text — show the target URL.
|
|
122
|
+
*/
|
|
123
|
+
function buildContent(props) {
|
|
124
|
+
const raw = normalizeContent(props.content);
|
|
125
|
+
|
|
126
|
+
// If there's already content, use it
|
|
127
|
+
if (raw.text || raw.html) return raw;
|
|
128
|
+
|
|
129
|
+
// Synthesize content for interaction types
|
|
130
|
+
const bookmarkOf = props["bookmark-of"];
|
|
131
|
+
const likeOf = props["like-of"];
|
|
132
|
+
const repostOf = props["repost-of"];
|
|
133
|
+
const name = props.name;
|
|
134
|
+
|
|
135
|
+
if (bookmarkOf) {
|
|
136
|
+
const label = name || bookmarkOf;
|
|
137
|
+
return {
|
|
138
|
+
text: `Bookmarked: ${label}`,
|
|
139
|
+
html: `<p>Bookmarked: <a href="${escapeHtml(bookmarkOf)}">${escapeHtml(label)}</a></p>`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (likeOf) {
|
|
144
|
+
return {
|
|
145
|
+
text: `Liked: ${likeOf}`,
|
|
146
|
+
html: `<p>Liked: <a href="${escapeHtml(likeOf)}">${escapeHtml(likeOf)}</a></p>`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (repostOf) {
|
|
151
|
+
const label = name || repostOf;
|
|
152
|
+
return {
|
|
153
|
+
text: `Reposted: ${label}`,
|
|
154
|
+
html: `<p>Reposted: <a href="${escapeHtml(repostOf)}">${escapeHtml(label)}</a></p>`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Article with title but no body
|
|
159
|
+
if (name) {
|
|
160
|
+
return { text: name, html: `<p>${escapeHtml(name)}</p>` };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return raw;
|
|
164
|
+
}
|
|
165
|
+
|
|
110
166
|
/**
|
|
111
167
|
* Normalize content from JF2 properties to { text, html } format.
|
|
112
168
|
*/
|
|
@@ -122,6 +178,58 @@ function normalizeContent(content) {
|
|
|
122
178
|
return { text: "", html: "" };
|
|
123
179
|
}
|
|
124
180
|
|
|
181
|
+
// ─── Hashtag Handling ─────────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Extract hashtags from text content.
|
|
185
|
+
* Matches #word patterns, returns lowercase tag names without the # prefix.
|
|
186
|
+
*/
|
|
187
|
+
function extractHashtags(text) {
|
|
188
|
+
if (!text) return [];
|
|
189
|
+
const matches = text.match(/(?:^|\s)#([a-zA-Z_]\w*)/g);
|
|
190
|
+
if (!matches) return [];
|
|
191
|
+
return [...new Set(matches.map((m) => m.trim().slice(1).toLowerCase()))];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Merge explicit categories with inline hashtags (deduplicated, case-insensitive).
|
|
196
|
+
*/
|
|
197
|
+
function mergeCategories(categories, hashtags) {
|
|
198
|
+
const seen = new Set(categories.map((c) => c.toLowerCase()));
|
|
199
|
+
const result = [...categories];
|
|
200
|
+
for (const tag of hashtags) {
|
|
201
|
+
if (!seen.has(tag)) {
|
|
202
|
+
seen.add(tag);
|
|
203
|
+
result.push(tag);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Rewrite hashtag links in HTML from site-internal (/categories/tag/) to
|
|
211
|
+
* Mastodon-compatible format. Mastodon clients use the tag objects, not
|
|
212
|
+
* inline links, but having correct href helps with link following.
|
|
213
|
+
*/
|
|
214
|
+
function rewriteHashtagLinks(content, siteUrl) {
|
|
215
|
+
if (!content.html) return content;
|
|
216
|
+
// Rewrite /categories/tag/ links to /tags/tag (Mastodon convention)
|
|
217
|
+
let html = content.html.replace(
|
|
218
|
+
/href="\/categories\/([^/"]+)\/?"/g,
|
|
219
|
+
(_, tag) => `href="${siteUrl}/tags/${tag}" class="hashtag" rel="tag"`,
|
|
220
|
+
);
|
|
221
|
+
// Also rewrite absolute site category links
|
|
222
|
+
if (siteUrl) {
|
|
223
|
+
html = html.replace(
|
|
224
|
+
new RegExp(`href="${escapeRegex(siteUrl)}/categories/([^/"]+)/?"`, "g"),
|
|
225
|
+
(_, tag) => `href="${siteUrl}/tags/${tag}" class="hashtag" rel="tag"`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
return { ...content, html };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ─── Post Type Mapping ────────────────────────────────────────────────────────
|
|
232
|
+
|
|
125
233
|
/**
|
|
126
234
|
* Map Micropub post-type to timeline type.
|
|
127
235
|
*/
|
|
@@ -138,12 +246,15 @@ function mapPostType(postType) {
|
|
|
138
246
|
case "repost":
|
|
139
247
|
return "boost";
|
|
140
248
|
case "like":
|
|
249
|
+
case "bookmark":
|
|
141
250
|
return "note";
|
|
142
251
|
default:
|
|
143
252
|
return "note";
|
|
144
253
|
}
|
|
145
254
|
}
|
|
146
255
|
|
|
256
|
+
// ─── Normalization Helpers ────────────────────────────────────────────────────
|
|
257
|
+
|
|
147
258
|
/**
|
|
148
259
|
* Normalize a value to an array of strings.
|
|
149
260
|
*/
|
|
@@ -154,14 +265,47 @@ function normalizeArray(value) {
|
|
|
154
265
|
}
|
|
155
266
|
|
|
156
267
|
/**
|
|
157
|
-
* Normalize media values
|
|
268
|
+
* Normalize media values — resolves relative URLs to absolute.
|
|
269
|
+
*
|
|
270
|
+
* @param {*} value - String, object with url, or array thereof
|
|
271
|
+
* @param {string} siteUrl - Base site URL for resolving relative paths
|
|
158
272
|
*/
|
|
159
|
-
function normalizeMediaArray(value) {
|
|
273
|
+
function normalizeMediaArray(value, siteUrl) {
|
|
160
274
|
if (!value) return [];
|
|
161
275
|
const arr = Array.isArray(value) ? value : [value];
|
|
162
276
|
return arr.map((item) => {
|
|
163
|
-
if (typeof item === "string") return item;
|
|
164
|
-
if (typeof item === "object" && item.url)
|
|
277
|
+
if (typeof item === "string") return resolveUrl(item, siteUrl);
|
|
278
|
+
if (typeof item === "object" && item.url) {
|
|
279
|
+
return { ...item, url: resolveUrl(item.url, siteUrl) };
|
|
280
|
+
}
|
|
165
281
|
return null;
|
|
166
282
|
}).filter(Boolean);
|
|
167
283
|
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Resolve a URL — if relative, prepend the site URL.
|
|
287
|
+
*/
|
|
288
|
+
function resolveUrl(url, siteUrl) {
|
|
289
|
+
if (!url) return url;
|
|
290
|
+
if (url.startsWith("http://") || url.startsWith("https://")) return url;
|
|
291
|
+
if (url.startsWith("/")) return `${siteUrl}${url}`;
|
|
292
|
+
return `${siteUrl}/${url}`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Escape HTML entities.
|
|
297
|
+
*/
|
|
298
|
+
function escapeHtml(str) {
|
|
299
|
+
return str
|
|
300
|
+
.replace(/&/g, "&")
|
|
301
|
+
.replace(/</g, "<")
|
|
302
|
+
.replace(/>/g, ">")
|
|
303
|
+
.replace(/"/g, """);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Escape regex special characters.
|
|
308
|
+
*/
|
|
309
|
+
function escapeRegex(str) {
|
|
310
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
311
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a remote account via WebFinger + ActivityPub actor fetch.
|
|
3
|
+
* Uses the Fedify federation instance to perform discovery.
|
|
4
|
+
*
|
|
5
|
+
* Shared by accounts.js (lookup) and search.js (resolve=true).
|
|
6
|
+
*/
|
|
7
|
+
import { serializeAccount } from "../entities/account.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} acct - Account identifier (user@domain or URL)
|
|
11
|
+
* @param {object} pluginOptions - Plugin options with federation, handle, publicationUrl
|
|
12
|
+
* @param {string} baseUrl - Server base URL
|
|
13
|
+
* @returns {Promise<object|null>} Serialized Mastodon Account or null
|
|
14
|
+
*/
|
|
15
|
+
export async function resolveRemoteAccount(acct, pluginOptions, baseUrl) {
|
|
16
|
+
const { federation, handle, publicationUrl } = pluginOptions;
|
|
17
|
+
if (!federation) return null;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const ctx = federation.createContext(
|
|
21
|
+
new URL(publicationUrl),
|
|
22
|
+
{ handle, publicationUrl },
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// Determine lookup URI
|
|
26
|
+
let actorUri;
|
|
27
|
+
if (acct.includes("@")) {
|
|
28
|
+
const parts = acct.replace(/^@/, "").split("@");
|
|
29
|
+
const username = parts[0];
|
|
30
|
+
const domain = parts[1];
|
|
31
|
+
if (!username || !domain) return null;
|
|
32
|
+
actorUri = `acct:${username}@${domain}`;
|
|
33
|
+
} else if (acct.startsWith("http")) {
|
|
34
|
+
actorUri = acct;
|
|
35
|
+
} else {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const actor = await ctx.lookupObject(actorUri);
|
|
40
|
+
if (!actor) return null;
|
|
41
|
+
|
|
42
|
+
// Extract data from the Fedify actor object
|
|
43
|
+
const name = actor.name?.toString() || actor.preferredUsername?.toString() || "";
|
|
44
|
+
const actorUrl = actor.id?.href || "";
|
|
45
|
+
const username = actor.preferredUsername?.toString() || "";
|
|
46
|
+
const domain = actorUrl ? new URL(actorUrl).hostname : "";
|
|
47
|
+
const summary = actor.summary?.toString() || "";
|
|
48
|
+
|
|
49
|
+
// Get avatar
|
|
50
|
+
let avatarUrl = "";
|
|
51
|
+
try {
|
|
52
|
+
const icon = await actor.getIcon();
|
|
53
|
+
avatarUrl = icon?.url?.href || "";
|
|
54
|
+
} catch { /* ignore */ }
|
|
55
|
+
|
|
56
|
+
// Get header image
|
|
57
|
+
let headerUrl = "";
|
|
58
|
+
try {
|
|
59
|
+
const image = await actor.getImage();
|
|
60
|
+
headerUrl = image?.url?.href || "";
|
|
61
|
+
} catch { /* ignore */ }
|
|
62
|
+
|
|
63
|
+
return serializeAccount(
|
|
64
|
+
{
|
|
65
|
+
name,
|
|
66
|
+
url: actorUrl,
|
|
67
|
+
photo: avatarUrl,
|
|
68
|
+
handle: `@${username}@${domain}`,
|
|
69
|
+
summary,
|
|
70
|
+
image: headerUrl,
|
|
71
|
+
bot: actor.constructor?.name === "Service" || actor.constructor?.name === "Application",
|
|
72
|
+
},
|
|
73
|
+
{ baseUrl },
|
|
74
|
+
);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.warn(`[Mastodon API] Remote account resolution failed for ${acct}:`, error.message);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -9,6 +9,7 @@ import { serializeCredentialAccount, serializeAccount } from "../entities/accoun
|
|
|
9
9
|
import { serializeStatus } from "../entities/status.js";
|
|
10
10
|
import { accountId, remoteActorId } from "../helpers/id-mapping.js";
|
|
11
11
|
import { buildPaginationQuery, parseLimit, setPaginationHeaders } from "../helpers/pagination.js";
|
|
12
|
+
import { resolveRemoteAccount } from "../helpers/resolve-account.js";
|
|
12
13
|
|
|
13
14
|
const router = express.Router(); // eslint-disable-line new-cap
|
|
14
15
|
|
|
@@ -100,7 +101,7 @@ router.get("/api/v1/accounts/lookup", async (req, res, next) => {
|
|
|
100
101
|
}
|
|
101
102
|
}
|
|
102
103
|
|
|
103
|
-
// Check followers
|
|
104
|
+
// Check followers for known remote actors
|
|
104
105
|
const follower = await collections.ap_followers.findOne({
|
|
105
106
|
$or: [
|
|
106
107
|
{ handle: `@${bareAcct}` },
|
|
@@ -116,6 +117,38 @@ router.get("/api/v1/accounts/lookup", async (req, res, next) => {
|
|
|
116
117
|
);
|
|
117
118
|
}
|
|
118
119
|
|
|
120
|
+
// Check following
|
|
121
|
+
const following = await collections.ap_following.findOne({
|
|
122
|
+
$or: [
|
|
123
|
+
{ handle: `@${bareAcct}` },
|
|
124
|
+
{ handle: bareAcct },
|
|
125
|
+
],
|
|
126
|
+
});
|
|
127
|
+
if (following) {
|
|
128
|
+
return res.json(
|
|
129
|
+
serializeAccount(
|
|
130
|
+
{ name: following.name, url: following.actorUrl, photo: following.avatar, handle: following.handle },
|
|
131
|
+
{ baseUrl },
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check timeline authors (people whose posts are in our timeline)
|
|
137
|
+
const timelineAuthor = await collections.ap_timeline.findOne({
|
|
138
|
+
"author.handle": { $in: [`@${bareAcct}`, bareAcct] },
|
|
139
|
+
});
|
|
140
|
+
if (timelineAuthor?.author) {
|
|
141
|
+
return res.json(
|
|
142
|
+
serializeAccount(timelineAuthor.author, { baseUrl }),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Resolve remotely via federation (WebFinger + actor fetch)
|
|
147
|
+
const resolved = await resolveRemoteAccount(bareAcct, pluginOptions, baseUrl);
|
|
148
|
+
if (resolved) {
|
|
149
|
+
return res.json(resolved);
|
|
150
|
+
}
|
|
151
|
+
|
|
119
152
|
return res.status(404).json({ error: "Record not found" });
|
|
120
153
|
} catch (error) {
|
|
121
154
|
next(error);
|
|
@@ -7,6 +7,7 @@ import express from "express";
|
|
|
7
7
|
import { serializeStatus } from "../entities/status.js";
|
|
8
8
|
import { serializeAccount } from "../entities/account.js";
|
|
9
9
|
import { parseLimit } from "../helpers/pagination.js";
|
|
10
|
+
import { resolveRemoteAccount } from "../helpers/resolve-account.js";
|
|
10
11
|
|
|
11
12
|
const router = express.Router(); // eslint-disable-line new-cap
|
|
12
13
|
|
|
@@ -21,6 +22,9 @@ router.get("/api/v2/search", async (req, res, next) => {
|
|
|
21
22
|
const limit = parseLimit(req.query.limit);
|
|
22
23
|
const offset = Math.max(0, Number.parseInt(req.query.offset, 10) || 0);
|
|
23
24
|
|
|
25
|
+
const resolve = req.query.resolve === "true";
|
|
26
|
+
const pluginOptions = req.app.locals.mastodonPluginOptions || {};
|
|
27
|
+
|
|
24
28
|
if (!query) {
|
|
25
29
|
return res.json({ accounts: [], statuses: [], hashtags: [] });
|
|
26
30
|
}
|
|
@@ -75,6 +79,14 @@ router.get("/api/v2/search", async (req, res, next) => {
|
|
|
75
79
|
}
|
|
76
80
|
if (results.accounts.length >= limit) break;
|
|
77
81
|
}
|
|
82
|
+
|
|
83
|
+
// If no local results and resolve=true, try remote lookup
|
|
84
|
+
if (results.accounts.length === 0 && resolve && query.includes("@")) {
|
|
85
|
+
const resolved = await resolveRemoteAccount(query, pluginOptions, baseUrl);
|
|
86
|
+
if (resolved) {
|
|
87
|
+
results.accounts.push(resolved);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
78
90
|
}
|
|
79
91
|
|
|
80
92
|
// ─── Status search ───────────────────────────────────────────────────
|
|
@@ -219,6 +219,34 @@ router.post("/api/v1/statuses", async (req, res, next) => {
|
|
|
219
219
|
const handle = pluginOptions.handle || "user";
|
|
220
220
|
const actorUrl = profile?.url || `${publicationUrl}/users/${handle}`;
|
|
221
221
|
|
|
222
|
+
// Extract hashtags from status text and merge with any Micropub categories
|
|
223
|
+
const categories = data.properties.category || [];
|
|
224
|
+
const inlineHashtags = (statusText || "").match(/(?:^|\s)#([a-zA-Z_]\w*)/g);
|
|
225
|
+
if (inlineHashtags) {
|
|
226
|
+
const existing = new Set(categories.map((c) => c.toLowerCase()));
|
|
227
|
+
for (const match of inlineHashtags) {
|
|
228
|
+
const tag = match.trim().slice(1).toLowerCase();
|
|
229
|
+
if (!existing.has(tag)) {
|
|
230
|
+
existing.add(tag);
|
|
231
|
+
categories.push(tag);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Resolve relative media URLs to absolute
|
|
237
|
+
const resolveMedia = (items) => {
|
|
238
|
+
if (!items || !items.length) return [];
|
|
239
|
+
return items.map((item) => {
|
|
240
|
+
if (typeof item === "string") {
|
|
241
|
+
return item.startsWith("http") ? item : `${publicationUrl.replace(/\/$/, "")}/${item.replace(/^\//, "")}`;
|
|
242
|
+
}
|
|
243
|
+
if (item?.url && !item.url.startsWith("http")) {
|
|
244
|
+
return { ...item, url: `${publicationUrl.replace(/\/$/, "")}/${item.url.replace(/^\//, "")}` };
|
|
245
|
+
}
|
|
246
|
+
return item;
|
|
247
|
+
});
|
|
248
|
+
};
|
|
249
|
+
|
|
222
250
|
const now = new Date().toISOString();
|
|
223
251
|
const timelineItem = await addTimelineItem(collections, {
|
|
224
252
|
uid: postUrl,
|
|
@@ -234,16 +262,16 @@ router.post("/api/v1/statuses", async (req, res, next) => {
|
|
|
234
262
|
createdAt: now,
|
|
235
263
|
author: {
|
|
236
264
|
name: profile?.name || handle,
|
|
237
|
-
url:
|
|
265
|
+
url: profile?.url || publicationUrl,
|
|
238
266
|
photo: profile?.icon || "",
|
|
239
267
|
handle: `@${handle}`,
|
|
240
268
|
emojis: [],
|
|
241
269
|
bot: false,
|
|
242
270
|
},
|
|
243
|
-
photo: data.properties.photo || [],
|
|
244
|
-
video: data.properties.video || [],
|
|
245
|
-
audio: data.properties.audio || [],
|
|
246
|
-
category:
|
|
271
|
+
photo: resolveMedia(data.properties.photo || []),
|
|
272
|
+
video: resolveMedia(data.properties.video || []),
|
|
273
|
+
audio: resolveMedia(data.properties.audio || []),
|
|
274
|
+
category: categories,
|
|
247
275
|
counts: { replies: 0, boosts: 0, likes: 0 },
|
|
248
276
|
linkPreviews: [],
|
|
249
277
|
mentions: [],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmdes/indiekit-endpoint-activitypub",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.4",
|
|
4
4
|
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"indiekit",
|