@rmdes/indiekit-endpoint-activitypub 3.12.2 → 3.12.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.
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import { serializeAccount } from "./account.js";
17
17
  import { sanitizeHtml } from "./sanitize.js";
18
+ import { remoteActorId } from "../helpers/id-mapping.js";
18
19
 
19
20
  // Module-level defaults set once at startup via setLocalIdentity()
20
21
  let _localPublicationUrl = "";
@@ -178,13 +179,17 @@ export function serializeStatus(item, { baseUrl, favouritedIds, rebloggedIds, bo
178
179
  url: `${baseUrl}/tags/${encodeURIComponent(tag)}`,
179
180
  }));
180
181
 
181
- // Mentions
182
- const mentions = (item.mentions || []).map((m) => ({
183
- id: "0", // We don't have stable IDs for mentioned accounts
184
- username: m.name || "",
185
- url: m.url || "",
186
- acct: m.name || "",
187
- }));
182
+ // Mentions — use actorUrl for deterministic ID, parse acct from handle
183
+ const mentions = (item.mentions || []).map((m) => {
184
+ const handle = (m.name || "").replace(/^@/, "");
185
+ const parts = handle.split("@");
186
+ return {
187
+ id: m.actorUrl ? remoteActorId(m.actorUrl) : "0",
188
+ username: parts[0] || handle,
189
+ url: m.url || m.actorUrl || "",
190
+ acct: handle,
191
+ };
192
+ });
188
193
 
189
194
  // Custom emojis
190
195
  const emojis = (item.emojis || []).map((e) => ({
@@ -467,7 +467,6 @@ router.post("/oauth/token", async (req, res, next) => {
467
467
  accessToken,
468
468
  createdAt: new Date(),
469
469
  grantType: "client_credentials",
470
- expiresAt: new Date(Date.now() + 3600 * 1000),
471
470
  });
472
471
 
473
472
  return res.json({
@@ -475,7 +474,6 @@ router.post("/oauth/token", async (req, res, next) => {
475
474
  token_type: "Bearer",
476
475
  scope: "read",
477
476
  created_at: Math.floor(Date.now() / 1000),
478
- expires_in: 3600,
479
477
  });
480
478
  }
481
479
 
@@ -510,9 +508,9 @@ router.post("/oauth/token", async (req, res, next) => {
510
508
  $set: {
511
509
  accessToken: newAccessToken,
512
510
  refreshToken: newRefreshToken,
513
- expiresAt: new Date(Date.now() + 3600 * 1000),
514
511
  refreshExpiresAt: new Date(Date.now() + 90 * 24 * 3600 * 1000),
515
512
  },
513
+ $unset: { expiresAt: "" },
516
514
  },
517
515
  );
518
516
 
@@ -522,7 +520,6 @@ router.post("/oauth/token", async (req, res, next) => {
522
520
  scope: existing.scopes.join(" "),
523
521
  created_at: Math.floor(existing.createdAt.getTime() / 1000),
524
522
  refresh_token: newRefreshToken,
525
- expires_in: 3600,
526
523
  });
527
524
  }
528
525
 
@@ -590,8 +587,9 @@ router.post("/oauth/token", async (req, res, next) => {
590
587
  }
591
588
  }
592
589
 
593
- // Generate access token and refresh token with expiry.
594
- const ACCESS_TOKEN_TTL = 3600 * 1000; // 1 hour
590
+ // Generate access token and refresh token.
591
+ // Access tokens do not expire (matching Mastodon behavior — valid until revoked).
592
+ // Refresh tokens expire after 90 days as a safety measure.
595
593
  const REFRESH_TOKEN_TTL = 90 * 24 * 3600 * 1000; // 90 days
596
594
  const accessToken = randomHex(64);
597
595
  const refreshToken = randomHex(64);
@@ -601,7 +599,6 @@ router.post("/oauth/token", async (req, res, next) => {
601
599
  $set: {
602
600
  accessToken,
603
601
  refreshToken,
604
- expiresAt: new Date(Date.now() + ACCESS_TOKEN_TTL),
605
602
  refreshExpiresAt: new Date(Date.now() + REFRESH_TOKEN_TTL),
606
603
  },
607
604
  },
@@ -613,7 +610,6 @@ router.post("/oauth/token", async (req, res, next) => {
613
610
  scope: grant.scopes.join(" "),
614
611
  created_at: Math.floor(grant.createdAt.getTime() / 1000),
615
612
  refresh_token: refreshToken,
616
- expires_in: 3600,
617
613
  });
618
614
  } catch (error) {
619
615
  next(error);
package/lib/syndicator.js CHANGED
@@ -227,11 +227,39 @@ export function createSyndicator(plugin) {
227
227
  const content = buildTimelineContent(properties);
228
228
  // Permalink is appended at read time by serializeStatus, not here.
229
229
 
230
+ // Linkify @mentions in content using resolved WebFinger data.
231
+ // This ensures the ap_timeline HTML has proper <a> links for
232
+ // mentions, matching what the federated AS2 activity contains.
233
+ if (resolvedMentions.length > 0 && content.html) {
234
+ const { default: jf2Mod } = await import("./jf2-to-as2.js");
235
+ // Import linkifyMentions — it's not exported, so inline the logic
236
+ for (const { handle, profileUrl, actorUrl } of resolvedMentions) {
237
+ const escaped = handle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
238
+ const pattern = new RegExp(`(?<!["\\/\\w])@${escaped}(?![\\w])`, "gi");
239
+ const parts = handle.split("@");
240
+ const url = profileUrl || (actorUrl ? actorUrl : `https://${parts[1]}/@${parts[0]}`);
241
+ content.html = content.html.replace(
242
+ pattern,
243
+ `<a href="${url}" class="mention" rel="nofollow noopener" target="_blank">@${handle}</a>`,
244
+ );
245
+ }
246
+ }
247
+
248
+ // Store resolved mentions for Mastodon API serialization
249
+ const timelineMentions = resolvedMentions
250
+ .filter(m => m.actorUrl)
251
+ .map(m => ({
252
+ name: `@${m.handle}`,
253
+ url: m.profileUrl || m.actorUrl,
254
+ actorUrl: m.actorUrl,
255
+ }));
256
+
230
257
  const timelineItem = {
231
258
  uid: properties.url,
232
259
  url: properties.url,
233
260
  type: mapPostType(properties["post-type"]),
234
261
  content,
262
+ mentions: timelineMentions,
235
263
  author: {
236
264
  name: profile?.name || handle,
237
265
  url: profile?.url || plugin._publicationUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-activitypub",
3
- "version": "3.12.2",
3
+ "version": "3.12.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",