@rmdes/indiekit-endpoint-activitypub 1.1.8 → 1.1.10

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/assets/reader.css CHANGED
@@ -296,24 +296,40 @@
296
296
  max-width: 100%;
297
297
  }
298
298
 
299
- /* @mentions — styled as subtle pills to distinguish from prose */
300
- .ap-card__content .h-card,
299
+ /* @mentions — keep inline, style as subtle links */
300
+ .ap-card__content .h-card {
301
+ display: inline;
302
+ }
303
+
304
+ .ap-card__content .h-card a,
301
305
  .ap-card__content a.u-url.mention {
306
+ display: inline;
302
307
  color: var(--color-on-offset);
303
- font-size: var(--font-size-s);
304
308
  text-decoration: none;
309
+ white-space: nowrap;
310
+ }
311
+
312
+ .ap-card__content .h-card a span,
313
+ .ap-card__content a.u-url.mention span {
314
+ display: inline;
305
315
  }
306
316
 
317
+ .ap-card__content .h-card a:hover,
307
318
  .ap-card__content a.u-url.mention:hover {
308
319
  color: var(--color-primary);
309
320
  text-decoration: underline;
310
321
  }
311
322
 
312
- /* Hashtag mentions — subtle tag styling */
323
+ /* Hashtag mentions — keep inline, subtle styling */
313
324
  .ap-card__content a.mention.hashtag {
325
+ display: inline;
314
326
  color: var(--color-on-offset);
315
- font-size: var(--font-size-s);
316
327
  text-decoration: none;
328
+ white-space: nowrap;
329
+ }
330
+
331
+ .ap-card__content a.mention.hashtag span {
332
+ display: inline;
317
333
  }
318
334
 
319
335
  .ap-card__content a.mention.hashtag:hover {
package/index.js CHANGED
@@ -496,7 +496,13 @@ export default class ActivityPubEndpoint {
496
496
  );
497
497
 
498
498
  // Resolve the remote actor to get their inbox
499
- const remoteActor = await ctx.lookupObject(actorUrl);
499
+ // Use authenticated document loader for servers requiring Authorized Fetch
500
+ const documentLoader = await ctx.getDocumentLoader({
501
+ identifier: handle,
502
+ });
503
+ const remoteActor = await ctx.lookupObject(actorUrl, {
504
+ documentLoader,
505
+ });
500
506
  if (!remoteActor) {
501
507
  return { ok: false, error: "Could not resolve remote actor" };
502
508
  }
@@ -591,7 +597,13 @@ export default class ActivityPubEndpoint {
591
597
  { handle, publicationUrl: this._publicationUrl },
592
598
  );
593
599
 
594
- const remoteActor = await ctx.lookupObject(actorUrl);
600
+ // Use authenticated document loader for servers requiring Authorized Fetch
601
+ const documentLoader = await ctx.getDocumentLoader({
602
+ identifier: handle,
603
+ });
604
+ const remoteActor = await ctx.lookupObject(actorUrl, {
605
+ documentLoader,
606
+ });
595
607
  if (!remoteActor) {
596
608
  // Even if we can't resolve, remove locally
597
609
  await this._collections.ap_following.deleteOne({ actorUrl });
@@ -227,8 +227,13 @@ async function processOneFollow(options, entry) {
227
227
  try {
228
228
  const ctx = federation.createContext(new URL(publicationUrl), { handle, publicationUrl });
229
229
 
230
- // Resolve the remote actor
231
- const remoteActor = await ctx.lookupObject(entry.actorUrl);
230
+ // Resolve the remote actor (signed request for Authorized Fetch)
231
+ const documentLoader = await ctx.getDocumentLoader({
232
+ identifier: handle,
233
+ });
234
+ const remoteActor = await ctx.lookupObject(entry.actorUrl, {
235
+ documentLoader,
236
+ });
232
237
  if (!remoteActor) {
233
238
  throw new Error("Could not resolve remote actor");
234
239
  }
@@ -5,6 +5,7 @@
5
5
  import { Temporal } from "@js-temporal/polyfill";
6
6
  import { getTimelineItem } from "../storage/timeline.js";
7
7
  import { getToken, validateToken } from "../csrf.js";
8
+ import { sanitizeContent } from "../timeline-store.js";
8
9
 
9
10
  /**
10
11
  * Fetch syndication targets from the Micropub config endpoint.
@@ -71,14 +72,22 @@ export function composeController(mountPath, plugin) {
71
72
  new URL(plugin._publicationUrl),
72
73
  { handle, publicationUrl: plugin._publicationUrl },
73
74
  );
74
- const remoteObject = await ctx.lookupObject(new URL(replyTo));
75
+ // Use authenticated document loader for Authorized Fetch
76
+ const documentLoader = await ctx.getDocumentLoader({
77
+ identifier: handle,
78
+ });
79
+ const remoteObject = await ctx.lookupObject(new URL(replyTo), {
80
+ documentLoader,
81
+ });
75
82
 
76
83
  if (remoteObject) {
77
84
  let authorName = "";
78
85
  let authorUrl = "";
79
86
 
80
87
  if (typeof remoteObject.getAttributedTo === "function") {
81
- const author = await remoteObject.getAttributedTo();
88
+ const author = await remoteObject.getAttributedTo({
89
+ documentLoader,
90
+ });
82
91
  const actor = Array.isArray(author) ? author[0] : author;
83
92
 
84
93
  if (actor) {
@@ -90,12 +99,13 @@ export function composeController(mountPath, plugin) {
90
99
  }
91
100
  }
92
101
 
102
+ const rawHtml = remoteObject.content?.toString() || "";
93
103
  replyContext = {
94
104
  url: replyTo,
95
105
  name: remoteObject.name?.toString() || "",
96
106
  content: {
97
- text:
98
- remoteObject.content?.toString()?.slice(0, 300) || "",
107
+ html: sanitizeContent(rawHtml),
108
+ text: rawHtml.replace(/<[^>]*>/g, "").slice(0, 300),
99
109
  },
100
110
  author: { name: authorName, url: authorUrl },
101
111
  };
@@ -112,6 +122,13 @@ export function composeController(mountPath, plugin) {
112
122
  ? await getSyndicationTargets(application, token)
113
123
  : [];
114
124
 
125
+ // Default-check only AP (Fedify) and Bluesky targets
126
+ // "@rick@rmendes.net" = AP Fedify, "@rmendes.net" = Bluesky
127
+ for (const target of syndicationTargets) {
128
+ const name = target.name || "";
129
+ target.defaultChecked = name === "@rick@rmendes.net" || name === "@rmendes.net";
130
+ }
131
+
115
132
  const csrfToken = getToken(request.session);
116
133
 
117
134
  response.render("activitypub-compose", {
@@ -198,13 +215,20 @@ export function submitComposeController(mountPath, plugin) {
198
215
  // If replying, also send to the original author
199
216
  if (inReplyTo) {
200
217
  try {
201
- const remoteObject = await ctx.lookupObject(new URL(inReplyTo));
218
+ const documentLoader = await ctx.getDocumentLoader({
219
+ identifier: handle,
220
+ });
221
+ const remoteObject = await ctx.lookupObject(new URL(inReplyTo), {
222
+ documentLoader,
223
+ });
202
224
 
203
225
  if (
204
226
  remoteObject &&
205
227
  typeof remoteObject.getAttributedTo === "function"
206
228
  ) {
207
- const author = await remoteObject.getAttributedTo();
229
+ const author = await remoteObject.getAttributedTo({
230
+ documentLoader,
231
+ });
208
232
  const recipient = Array.isArray(author)
209
233
  ? author[0]
210
234
  : author;
@@ -57,15 +57,20 @@ export function boostController(mountPath, plugin) {
57
57
  orderingKey: url,
58
58
  });
59
59
 
60
- // Also send to the original post author
60
+ // Also send to the original post author (signed request for Authorized Fetch)
61
61
  try {
62
- const remoteObject = await ctx.lookupObject(new URL(url));
62
+ const documentLoader = await ctx.getDocumentLoader({
63
+ identifier: handle,
64
+ });
65
+ const remoteObject = await ctx.lookupObject(new URL(url), {
66
+ documentLoader,
67
+ });
63
68
 
64
69
  if (
65
70
  remoteObject &&
66
71
  typeof remoteObject.getAttributedTo === "function"
67
72
  ) {
68
- const author = await remoteObject.getAttributedTo();
73
+ const author = await remoteObject.getAttributedTo({ documentLoader });
69
74
  const recipient = Array.isArray(author) ? author[0] : author;
70
75
 
71
76
  if (recipient) {
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { validateToken } from "../csrf.js";
7
+ import { getTimelineItem } from "../storage/timeline.js";
7
8
 
8
9
  /**
9
10
  * POST /admin/reader/like — send a Like activity to the post author.
@@ -43,29 +44,52 @@ export function likeController(mountPath, plugin) {
43
44
  { handle, publicationUrl: plugin._publicationUrl },
44
45
  );
45
46
 
46
- // Look up the remote post to find its author
47
- const remoteObject = await ctx.lookupObject(new URL(url));
48
-
49
- if (!remoteObject) {
50
- return response.status(404).json({
51
- success: false,
52
- error: "Could not resolve remote post",
53
- });
54
- }
47
+ // Use authenticated document loader for servers requiring Authorized Fetch
48
+ const documentLoader = await ctx.getDocumentLoader({
49
+ identifier: handle,
50
+ });
55
51
 
56
- // Get the post author for delivery
52
+ // Resolve author for delivery — try multiple strategies
57
53
  let recipient = null;
58
54
 
59
- if (typeof remoteObject.getAttributedTo === "function") {
60
- const author = await remoteObject.getAttributedTo();
61
- recipient = Array.isArray(author) ? author[0] : author;
55
+ // Strategy 1: Look up remote post via Fedify (signed request)
56
+ try {
57
+ const remoteObject = await ctx.lookupObject(new URL(url), {
58
+ documentLoader,
59
+ });
60
+ if (remoteObject && typeof remoteObject.getAttributedTo === "function") {
61
+ const author = await remoteObject.getAttributedTo({ documentLoader });
62
+ recipient = Array.isArray(author) ? author[0] : author;
63
+ }
64
+ } catch {
65
+ // Network failure — fall through to timeline
62
66
  }
63
67
 
68
+ // Strategy 2: Use author URL from our timeline (already stored)
64
69
  if (!recipient) {
65
- return response.status(404).json({
66
- success: false,
67
- error: "Could not resolve post author",
68
- });
70
+ const { application } = request.app.locals;
71
+ const collections = {
72
+ ap_timeline: application?.collections?.get("ap_timeline"),
73
+ };
74
+ const timelineItem = await getTimelineItem(collections, url);
75
+ const authorUrl = timelineItem?.author?.url;
76
+
77
+ if (authorUrl) {
78
+ try {
79
+ recipient = await ctx.lookupObject(new URL(authorUrl), {
80
+ documentLoader,
81
+ });
82
+ } catch {
83
+ // Could not resolve author actor either
84
+ }
85
+ }
86
+
87
+ if (!recipient) {
88
+ return response.status(404).json({
89
+ success: false,
90
+ error: "Could not resolve post author",
91
+ });
92
+ }
69
93
  }
70
94
 
71
95
  // Generate a unique activity ID
@@ -170,13 +194,42 @@ export function unlikeController(mountPath, plugin) {
170
194
  { handle, publicationUrl: plugin._publicationUrl },
171
195
  );
172
196
 
173
- // Resolve the recipient
174
- const remoteObject = await ctx.lookupObject(new URL(url));
197
+ // Use authenticated document loader for servers requiring Authorized Fetch
198
+ const documentLoader = await ctx.getDocumentLoader({
199
+ identifier: handle,
200
+ });
201
+
202
+ // Resolve the recipient — try remote first, then timeline fallback
175
203
  let recipient = null;
176
204
 
177
- if (remoteObject && typeof remoteObject.getAttributedTo === "function") {
178
- const author = await remoteObject.getAttributedTo();
179
- recipient = Array.isArray(author) ? author[0] : author;
205
+ try {
206
+ const remoteObject = await ctx.lookupObject(new URL(url), {
207
+ documentLoader,
208
+ });
209
+ if (remoteObject && typeof remoteObject.getAttributedTo === "function") {
210
+ const author = await remoteObject.getAttributedTo({ documentLoader });
211
+ recipient = Array.isArray(author) ? author[0] : author;
212
+ }
213
+ } catch {
214
+ // Network failure
215
+ }
216
+
217
+ if (!recipient) {
218
+ const collections = {
219
+ ap_timeline: application?.collections?.get("ap_timeline"),
220
+ };
221
+ const timelineItem = await getTimelineItem(collections, url);
222
+ const authorUrl = timelineItem?.author?.url;
223
+
224
+ if (authorUrl) {
225
+ try {
226
+ recipient = await ctx.lookupObject(new URL(authorUrl), {
227
+ documentLoader,
228
+ });
229
+ } catch {
230
+ // Could not resolve — will proceed to cleanup
231
+ }
232
+ }
180
233
  }
181
234
 
182
235
  if (!recipient) {
@@ -151,7 +151,12 @@ export function blockController(mountPath, plugin) {
151
151
  { handle, publicationUrl: plugin._publicationUrl },
152
152
  );
153
153
 
154
- const remoteActor = await ctx.lookupObject(new URL(url));
154
+ const documentLoader = await ctx.getDocumentLoader({
155
+ identifier: handle,
156
+ });
157
+ const remoteActor = await ctx.lookupObject(new URL(url), {
158
+ documentLoader,
159
+ });
155
160
 
156
161
  if (remoteActor) {
157
162
  const block = new Block({
@@ -225,7 +230,12 @@ export function unblockController(mountPath, plugin) {
225
230
  { handle, publicationUrl: plugin._publicationUrl },
226
231
  );
227
232
 
228
- const remoteActor = await ctx.lookupObject(new URL(url));
233
+ const documentLoader = await ctx.getDocumentLoader({
234
+ identifier: handle,
235
+ });
236
+ const remoteActor = await ctx.lookupObject(new URL(url), {
237
+ documentLoader,
238
+ });
229
239
 
230
240
  if (remoteActor) {
231
241
  const block = new Block({
@@ -36,11 +36,14 @@ export function remoteProfileController(mountPath, plugin) {
36
36
  { handle, publicationUrl: plugin._publicationUrl },
37
37
  );
38
38
 
39
- // Look up the remote actor
39
+ // Look up the remote actor (signed request for Authorized Fetch)
40
+ const documentLoader = await ctx.getDocumentLoader({
41
+ identifier: handle,
42
+ });
40
43
  let actor;
41
44
 
42
45
  try {
43
- actor = await ctx.lookupObject(new URL(actorUrl));
46
+ actor = await ctx.lookupObject(new URL(actorUrl), { documentLoader });
44
47
  } catch {
45
48
  return response.status(404).render("error", {
46
49
  title: "Error",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-activitypub",
3
- "version": "1.1.8",
3
+ "version": "1.1.10",
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",
@@ -18,9 +18,9 @@
18
18
  <a href="{{ replyContext.author.url }}">{{ replyContext.author.name }}</a>
19
19
  </div>
20
20
  {% endif %}
21
- {% if replyContext.content and replyContext.content.text %}
21
+ {% if replyContext.content and (replyContext.content.html or replyContext.content.text) %}
22
22
  <blockquote class="ap-compose__context-text">
23
- {{ replyContext.content.text | truncate(300) }}
23
+ {{ replyContext.content.html | safe if replyContext.content.html else replyContext.content.text | truncate(300) }}
24
24
  </blockquote>
25
25
  {% endif %}
26
26
  <a href="{{ replyTo }}" class="ap-compose__context-link" target="_blank" rel="noopener">{{ replyTo }}</a>
@@ -74,7 +74,7 @@
74
74
  <legend>{{ __("activitypub.compose.syndicateLabel") }}</legend>
75
75
  {% for target in syndicationTargets %}
76
76
  <label class="ap-compose__syndication-target">
77
- <input type="checkbox" name="mp-syndicate-to" value="{{ target.uid }}" checked>
77
+ <input type="checkbox" name="mp-syndicate-to" value="{{ target.uid }}" {{ "checked" if target.defaultChecked }}>
78
78
  {{ target.name }}
79
79
  </label>
80
80
  {% endfor %}