@rmdes/indiekit-endpoint-activitypub 2.0.8 → 2.0.9

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/index.js CHANGED
@@ -59,6 +59,7 @@ import {
59
59
  } from "./lib/controllers/featured-tags.js";
60
60
  import { resolveController } from "./lib/controllers/resolve.js";
61
61
  import { publicProfileController } from "./lib/controllers/public-profile.js";
62
+ import { noteObjectController } from "./lib/controllers/note-object.js";
62
63
  import {
63
64
  refollowPauseController,
64
65
  refollowResumeController,
@@ -161,6 +162,10 @@ export default class ActivityPubEndpoint {
161
162
  return self._fedifyMiddleware(req, res, next);
162
163
  });
163
164
 
165
+ // Serve stored quick reply Notes as JSON-LD so remote servers can
166
+ // dereference the Note ID during Create activity verification.
167
+ router.get("/quick-replies/:id", noteObjectController(self));
168
+
164
169
  // HTML fallback for actor URL — serve a public profile page.
165
170
  // Fedify only serves JSON-LD; browsers get 406 and fall through here.
166
171
  router.get("/users/:identifier", publicProfileController(self));
@@ -835,6 +840,7 @@ export default class ActivityPubEndpoint {
835
840
  Indiekit.addCollection("ap_muted");
836
841
  Indiekit.addCollection("ap_blocked");
837
842
  Indiekit.addCollection("ap_interactions");
843
+ Indiekit.addCollection("ap_notes");
838
844
 
839
845
  // Store collection references (posts resolved lazily)
840
846
  const indiekitCollections = Indiekit.collections;
@@ -853,6 +859,7 @@ export default class ActivityPubEndpoint {
853
859
  ap_muted: indiekitCollections.get("ap_muted"),
854
860
  ap_blocked: indiekitCollections.get("ap_blocked"),
855
861
  ap_interactions: indiekitCollections.get("ap_interactions"),
862
+ ap_notes: indiekitCollections.get("ap_notes"),
856
863
  get posts() {
857
864
  return indiekitCollections.get("posts");
858
865
  },
@@ -5,6 +5,7 @@
5
5
  import { Temporal } from "@js-temporal/polyfill";
6
6
  import { getToken, validateToken } from "../csrf.js";
7
7
  import { sanitizeContent } from "../timeline-store.js";
8
+ import { resolveAuthor } from "../resolve-author.js";
8
9
 
9
10
  /**
10
11
  * Fetch syndication targets from the Micropub config endpoint.
@@ -205,33 +206,20 @@ export function submitComposeController(mountPath, plugin) {
205
206
  );
206
207
  const followersUri = ctx.getFollowersUri(handle);
207
208
 
209
+ const documentLoader = await ctx.getDocumentLoader({
210
+ identifier: handle,
211
+ });
212
+
208
213
  // Resolve the original author BEFORE constructing the Note,
209
214
  // so we can include them in cc (required for threading/notification)
210
215
  let recipient = null;
211
216
  if (inReplyTo) {
212
- try {
213
- const documentLoader = await ctx.getDocumentLoader({
214
- identifier: handle,
215
- });
216
- const remoteObject = await ctx.lookupObject(new URL(inReplyTo), {
217
- documentLoader,
218
- });
219
-
220
- if (
221
- remoteObject &&
222
- typeof remoteObject.getAttributedTo === "function"
223
- ) {
224
- const author = await remoteObject.getAttributedTo({
225
- documentLoader,
226
- });
227
- recipient = Array.isArray(author) ? author[0] : author;
228
- }
229
- } catch (error) {
230
- console.warn(
231
- `[ActivityPub] lookupObject failed for ${inReplyTo} (quick reply):`,
232
- error.message,
233
- );
234
- }
217
+ recipient = await resolveAuthor(
218
+ inReplyTo,
219
+ ctx,
220
+ documentLoader,
221
+ application?.collections,
222
+ );
235
223
  }
236
224
 
237
225
  // Build cc list: always include followers, add original author for replies
@@ -258,6 +246,21 @@ export function submitComposeController(mountPath, plugin) {
258
246
  ccs: ccList,
259
247
  });
260
248
 
249
+ // Store the Note so remote servers can dereference its ID
250
+ const ap_notes = application?.collections?.get("ap_notes");
251
+ if (ap_notes) {
252
+ await ap_notes.insertOne({
253
+ _id: uuid,
254
+ noteId,
255
+ actorUrl: actorUri.href,
256
+ content: content.trim(),
257
+ inReplyTo: inReplyTo || null,
258
+ published: new Date().toISOString(),
259
+ to: ["https://www.w3.org/ns/activitystreams#Public"],
260
+ cc: ccList.map((u) => (u instanceof URL ? u.href : u.href || u)),
261
+ });
262
+ }
263
+
261
264
  // Send to followers
262
265
  await ctx.sendActivity({ identifier: handle }, "followers", create, {
263
266
  preferSharedInbox: true,
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Public route handler for serving quick reply Notes as ActivityPub JSON-LD.
3
+ *
4
+ * Remote servers dereference Note IDs to verify Create activities.
5
+ * Without this, quick replies are rejected by servers that validate
6
+ * the Note's ID URL (Mastodon with Authorized Fetch, Bonfire, etc.).
7
+ */
8
+
9
+ /**
10
+ * GET /quick-replies/:id — serve a stored Note as JSON-LD.
11
+ * @param {object} plugin - ActivityPub plugin instance
12
+ */
13
+ export function noteObjectController(plugin) {
14
+ return async (request, response) => {
15
+ const { id } = request.params;
16
+
17
+ const { application } = request.app.locals;
18
+ const ap_notes = application?.collections?.get("ap_notes");
19
+
20
+ if (!ap_notes) {
21
+ return response.status(404).json({ error: "Not Found" });
22
+ }
23
+
24
+ const note = await ap_notes.findOne({ _id: id });
25
+
26
+ if (!note) {
27
+ return response.status(404).json({ error: "Not Found" });
28
+ }
29
+
30
+ const noteJson = {
31
+ "@context": "https://www.w3.org/ns/activitystreams",
32
+ id: note.noteId,
33
+ type: "Note",
34
+ attributedTo: note.actorUrl,
35
+ content: note.content,
36
+ published: note.published,
37
+ to: note.to,
38
+ cc: note.cc,
39
+ };
40
+
41
+ if (note.inReplyTo) {
42
+ noteJson.inReplyTo = note.inReplyTo;
43
+ }
44
+
45
+ response
46
+ .status(200)
47
+ .set("Content-Type", "application/activity+json; charset=utf-8")
48
+ .set("Cache-Control", "public, max-age=3600")
49
+ .json(noteJson);
50
+ };
51
+ }
@@ -340,7 +340,9 @@ export function registerInboxListeners(inboxChain, options) {
340
340
 
341
341
  await addTimelineItem(collections, timelineItem);
342
342
  } catch (error) {
343
- console.error("Failed to store boosted timeline item:", error);
343
+ // Remote object unreachable (timeout, Authorized Fetch, deleted, etc.) — skip
344
+ const cause = error?.cause?.code || error?.message || "unknown";
345
+ console.warn(`[AP] Skipped boost from ${actorUrl}: ${cause}`);
344
346
  }
345
347
  }
346
348
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-activitypub",
3
- "version": "2.0.8",
3
+ "version": "2.0.9",
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",