@rmdes/indiekit-endpoint-activitypub 3.8.4 → 3.8.5

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
@@ -744,7 +744,7 @@ export default class ActivityPubEndpoint {
744
744
  "pkcs8",
745
745
  Buffer.from(pemBody, "base64"),
746
746
  { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
747
- false,
747
+ true,
748
748
  ["sign"],
749
749
  );
750
750
  } catch (error) {
@@ -1589,6 +1589,7 @@ export default class ActivityPubEndpoint {
1589
1589
  federation: this._federation,
1590
1590
  followActor: (url, info) => pluginRef.followActor(url, info),
1591
1591
  unfollowActor: (url) => pluginRef.unfollowActor(url),
1592
+ loadRsaKey: () => pluginRef._loadRsaPrivateKey(),
1592
1593
  },
1593
1594
  });
1594
1595
  Indiekit.addEndpoint({
@@ -72,11 +72,16 @@ export function boostController(mountPath, plugin) {
72
72
  identifier: handle,
73
73
  });
74
74
  const { application } = request.app.locals;
75
+ const rsaKey = await plugin._loadRsaPrivateKey();
75
76
  const recipient = await resolveAuthor(
76
77
  url,
77
78
  ctx,
78
79
  documentLoader,
79
80
  application?.collections,
81
+ {
82
+ privateKey: rsaKey,
83
+ keyId: `${ctx.getActorUri(handle).href}#main-key`,
84
+ },
80
85
  );
81
86
 
82
87
  if (recipient) {
@@ -49,11 +49,16 @@ export function likeController(mountPath, plugin) {
49
49
  });
50
50
 
51
51
  const { application } = request.app.locals;
52
+ const rsaKey = await plugin._loadRsaPrivateKey();
52
53
  const recipient = await resolveAuthor(
53
54
  url,
54
55
  ctx,
55
56
  documentLoader,
56
57
  application?.collections,
58
+ {
59
+ privateKey: rsaKey,
60
+ keyId: `${ctx.getActorUri(handle).href}#main-key`,
61
+ },
57
62
  );
58
63
 
59
64
  if (!recipient) {
@@ -170,11 +175,16 @@ export function unlikeController(mountPath, plugin) {
170
175
  identifier: handle,
171
176
  });
172
177
 
178
+ const rsaKey2 = await plugin._loadRsaPrivateKey();
173
179
  const recipient = await resolveAuthor(
174
180
  url,
175
181
  ctx,
176
182
  documentLoader,
177
183
  application?.collections,
184
+ {
185
+ privateKey: rsaKey2,
186
+ keyId: `${ctx.getActorUri(handle).href}#main-key`,
187
+ },
178
188
  );
179
189
 
180
190
  if (!recipient) {
@@ -22,7 +22,7 @@ import { resolveAuthor } from "../../resolve-author.js";
22
22
  * @param {object} params.interactions - ap_interactions collection
23
23
  * @returns {Promise<{ activityId: string }>}
24
24
  */
25
- export async function likePost({ targetUrl, federation, handle, publicationUrl, collections, interactions }) {
25
+ export async function likePost({ targetUrl, federation, handle, publicationUrl, collections, interactions, loadRsaKey }) {
26
26
  const { Like } = await import("@fedify/fedify/vocab");
27
27
  const ctx = federation.createContext(
28
28
  new URL(publicationUrl),
@@ -30,7 +30,11 @@ export async function likePost({ targetUrl, federation, handle, publicationUrl,
30
30
  );
31
31
 
32
32
  const documentLoader = await ctx.getDocumentLoader({ identifier: handle });
33
- const recipient = await resolveAuthor(targetUrl, ctx, documentLoader, collections);
33
+ const rsaKey = loadRsaKey ? await loadRsaKey() : null;
34
+ const recipient = await resolveAuthor(targetUrl, ctx, documentLoader, collections, {
35
+ privateKey: rsaKey,
36
+ keyId: `${ctx.getActorUri(handle).href}#main-key`,
37
+ });
34
38
 
35
39
  const uuid = crypto.randomUUID();
36
40
  const baseUrl = publicationUrl.replace(/\/$/, "");
@@ -79,7 +83,7 @@ export async function likePost({ targetUrl, federation, handle, publicationUrl,
79
83
  * @param {object} params.interactions - ap_interactions collection
80
84
  * @returns {Promise<void>}
81
85
  */
82
- export async function unlikePost({ targetUrl, federation, handle, publicationUrl, collections, interactions }) {
86
+ export async function unlikePost({ targetUrl, federation, handle, publicationUrl, collections, interactions, loadRsaKey }) {
83
87
  const existing = interactions
84
88
  ? await interactions.findOne({ objectUrl: targetUrl, type: "like" })
85
89
  : null;
@@ -95,7 +99,11 @@ export async function unlikePost({ targetUrl, federation, handle, publicationUrl
95
99
  );
96
100
 
97
101
  const documentLoader = await ctx.getDocumentLoader({ identifier: handle });
98
- const recipient = await resolveAuthor(targetUrl, ctx, documentLoader, collections);
102
+ const rsaKey = loadRsaKey ? await loadRsaKey() : null;
103
+ const recipient = await resolveAuthor(targetUrl, ctx, documentLoader, collections, {
104
+ privateKey: rsaKey,
105
+ keyId: `${ctx.getActorUri(handle).href}#main-key`,
106
+ });
99
107
 
100
108
  if (recipient) {
101
109
  const like = new Like({
@@ -131,7 +139,7 @@ export async function unlikePost({ targetUrl, federation, handle, publicationUrl
131
139
  * @param {object} params.interactions - ap_interactions collection
132
140
  * @returns {Promise<{ activityId: string }>}
133
141
  */
134
- export async function boostPost({ targetUrl, federation, handle, publicationUrl, collections, interactions }) {
142
+ export async function boostPost({ targetUrl, federation, handle, publicationUrl, collections, interactions, loadRsaKey }) {
135
143
  const { Announce } = await import("@fedify/fedify/vocab");
136
144
  const ctx = federation.createContext(
137
145
  new URL(publicationUrl),
@@ -162,7 +170,11 @@ export async function boostPost({ targetUrl, federation, handle, publicationUrl,
162
170
 
163
171
  // Also send directly to the original post author
164
172
  const documentLoader = await ctx.getDocumentLoader({ identifier: handle });
165
- const recipient = await resolveAuthor(targetUrl, ctx, documentLoader, collections);
173
+ const rsaKey = loadRsaKey ? await loadRsaKey() : null;
174
+ const recipient = await resolveAuthor(targetUrl, ctx, documentLoader, collections, {
175
+ privateKey: rsaKey,
176
+ keyId: `${ctx.getActorUri(handle).href}#main-key`,
177
+ });
166
178
  if (recipient) {
167
179
  try {
168
180
  await ctx.sendActivity({ identifier: handle }, recipient, announce, {
@@ -614,6 +614,7 @@ function getFederationOpts(req) {
614
614
  handle: pluginOptions.handle || "user",
615
615
  publicationUrl: pluginOptions.publicationUrl,
616
616
  collections: req.app.locals.mastodonCollections,
617
+ loadRsaKey: pluginOptions.loadRsaKey,
617
618
  };
618
619
  }
619
620
 
@@ -6,6 +6,8 @@
6
6
  *
7
7
  * Strategies (tried in order):
8
8
  * 1. lookupObject on post URL → getAttributedTo
9
+ * 1b. Raw signed fetch fallback (for servers like wafrn that return
10
+ * non-standard JSON-LD that Fedify can't parse)
9
11
  * 2. Timeline/notification DB lookup → lookupObject on stored author URL
10
12
  * 3. Extract author URL from post URL pattern → lookupObject
11
13
  */
@@ -60,6 +62,9 @@ export function extractAuthorUrl(postUrl) {
60
62
  * @param {object} ctx - Fedify context
61
63
  * @param {object} documentLoader - Authenticated document loader
62
64
  * @param {object} [collections] - Optional MongoDB collections map (application.collections)
65
+ * @param {object} [options] - Additional options
66
+ * @param {CryptoKey} [options.privateKey] - RSA private key for raw signed fetch fallback
67
+ * @param {string} [options.keyId] - Key ID for HTTP Signature (e.g. "...#main-key")
63
68
  * @returns {Promise<object|null>} - Fedify Actor object or null
64
69
  */
65
70
  export async function resolveAuthor(
@@ -67,6 +72,7 @@ export async function resolveAuthor(
67
72
  ctx,
68
73
  documentLoader,
69
74
  collections,
75
+ options = {},
70
76
  ) {
71
77
  // Strategy 1: Look up remote post via Fedify (signed request)
72
78
  try {
@@ -90,6 +96,46 @@ export async function resolveAuthor(
90
96
  );
91
97
  }
92
98
 
99
+ // Strategy 1b: Raw signed fetch fallback
100
+ // Some servers (e.g. wafrn) return AP JSON without @context, which Fedify's
101
+ // JSON-LD processor rejects. A raw fetch can still extract attributedTo/actor.
102
+ if (options.privateKey && options.keyId) {
103
+ try {
104
+ const { signRequest } = await import("@fedify/fedify/sig");
105
+ const request = new Request(postUrl, {
106
+ method: "GET",
107
+ headers: { Accept: "application/activity+json" },
108
+ });
109
+ const signed = await signRequest(request, options.privateKey, new URL(options.keyId), {
110
+ spec: "draft-cavage-http-signatures-12",
111
+ });
112
+ const res = await fetch(signed, { redirect: "follow" });
113
+ if (res.ok) {
114
+ const contentType = res.headers.get("content-type") || "";
115
+ if (contentType.includes("json")) {
116
+ const json = await res.json();
117
+ const authorUrl = json.attributedTo || json.actor;
118
+ if (authorUrl && typeof authorUrl === "string") {
119
+ const actor = await lookupWithSecurity(ctx, new URL(authorUrl), {
120
+ documentLoader,
121
+ });
122
+ if (actor) {
123
+ console.info(
124
+ `[ActivityPub] Resolved author via raw fetch for ${postUrl} → ${authorUrl}`,
125
+ );
126
+ return actor;
127
+ }
128
+ }
129
+ }
130
+ }
131
+ } catch (error) {
132
+ console.warn(
133
+ `[ActivityPub] Raw fetch fallback failed for ${postUrl}:`,
134
+ error.message,
135
+ );
136
+ }
137
+ }
138
+
93
139
  // Strategy 2: Use author URL from timeline or notifications
94
140
  if (collections) {
95
141
  const ap_timeline = collections.get("ap_timeline");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-activitypub",
3
- "version": "3.8.4",
3
+ "version": "3.8.5",
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",