feat: upgrade Fedify to 2.1.0 + implement 5 FEPs

Fedify 2.1.0 upgrade:
- Upgrade @fedify/fedify, @fedify/redis, @fedify/debugger to ^2.1.0
- Remove as:Endpoints type-stripping workaround (fixed upstream, fedify#576)
- Wire onUnverifiedActivity handler for Delete from actors with gone keys

FEP implementations:
- FEP-5feb: Add indexable + discoverable to actor (search indexing consent)
- FEP-f1d5/0151: Enrich NodeInfo 2.1 with metadata, staff accounts, repo info
- FEP-4f05: Soft delete with Tombstone — deleted posts serve 410 + Tombstone
  JSON-LD with formerType, published, deleted timestamps. New ap_tombstones
  collection + lib/storage/tombstones.js
- FEP-3b86: Activity Intents — WebFinger links for Follow/Create/Like/Announce
  intents, authorize_interaction routes by intent parameter
- FEP-8fcf: Collection Sync outbound via Fedify syncCollection (documented
  that receiving side is not yet implemented)
This commit is contained in:
Ricardo
2026-03-26 17:33:28 +01:00
parent 47fe21c681
commit 35ab840a56
10 changed files with 291 additions and 120 deletions

View File

@@ -161,6 +161,7 @@ processing pipeline via item-processing.js:
| `ap_blocked_servers` | Blocked server domains | `hostname` (unique) | | `ap_blocked_servers` | Blocked server domains | `hostname` (unique) |
| `ap_key_freshness` | Remote actor key verification timestamps | `actorUrl` (unique), `lastVerifiedAt` | | `ap_key_freshness` | Remote actor key verification timestamps | `actorUrl` (unique), `lastVerifiedAt` |
| `ap_inbox_queue` | Persistent async inbox queue | `activityId`, `status`, `enqueuedAt` | | `ap_inbox_queue` | Persistent async inbox queue | `activityId`, `status`, `enqueuedAt` |
| `ap_tombstones` | Tombstone records for soft-deleted posts (FEP-4f05) | `url` (unique) |
| `ap_oauth_apps` | Mastodon API client registrations | `clientId` (unique), `clientSecret`, `redirectUris` | | `ap_oauth_apps` | Mastodon API client registrations | `clientId` (unique), `clientSecret`, `redirectUris` |
| `ap_oauth_tokens` | OAuth2 authorization codes + access tokens | `code` (unique sparse), `accessToken` (unique sparse) | | `ap_oauth_tokens` | OAuth2 authorization codes + access tokens | `code` (unique sparse), `accessToken` (unique sparse) |
| `ap_markers` | Read position markers (Mastodon API) | `userId`, `timeline` | | `ap_markers` | Read position markers (Mastodon API) | `userId`, `timeline` |
@@ -219,12 +220,11 @@ Express 5 removed the `"back"` magic keyword from `response.redirect()`. It's tr
JSON-LD compaction collapses single-element arrays to plain objects. Mastodon's `update_account_fields` checks `attachment.is_a?(Array)` and silently skips if it's not an array. `sendFedifyResponse()` in `federation-bridge.js` forces `attachment` to always be an array. JSON-LD compaction collapses single-element arrays to plain objects. Mastodon's `update_account_fields` checks `attachment.is_a?(Array)` and silently skips if it's not an array. `sendFedifyResponse()` in `federation-bridge.js` forces `attachment` to always be an array.
### 10. WORKAROUND: Endpoints `as:Endpoints` Type Stripping ### 10. REMOVED: Endpoints `as:Endpoints` Type Stripping (Fixed in Fedify 2.1.0)
**File:** `lib/federation-bridge.js` (in `sendFedifyResponse()`)
**Upstream issue:** [fedify#576](https://github.com/fedify-dev/fedify/issues/576) — FIXED in Fedify 2.1.0 **Upstream issue:** [fedify#576](https://github.com/fedify-dev/fedify/issues/576) — FIXED in Fedify 2.1.0
**Workaround:** `delete json.endpoints.type` strips the invalid `"type": "as:Endpoints"` from actor JSON. **Previous workaround** in `federation-bridge.js`**REMOVED**.
**Remove when:** Upgrading to Fedify ≥ 2.1.0. Fedify 2.1.0 now omits the invalid `"type": "as:Endpoints"` from serialized actor JSON. No workaround needed.
### 11. KNOWN ISSUE: PropertyValue Attachment Type Validation ### 11. KNOWN ISSUE: PropertyValue Attachment Type Validation
@@ -624,6 +624,14 @@ curl -s "https://rmendes.net/nodeinfo/2.1" | jq .
- `@_followback@tags.pub` does not send Follow activities back despite accepting ours - `@_followback@tags.pub` does not send Follow activities back despite accepting ours
- Both suggest tags.pub's outbound delivery is broken — zero inbound requests from `activitypub-bot` user-agent have been observed - Both suggest tags.pub's outbound delivery is broken — zero inbound requests from `activitypub-bot` user-agent have been observed
### 37. Unverified Delete Activities (Fedify 2.1.0+)
`onUnverifiedActivity()` in `federation-setup.js` handles Delete activities from actors whose signing keys return 404/410. When an account is permanently deleted, the remote server sends a Delete activity but the actor's key endpoint is gone, so HTTP Signature verification fails. The handler checks `reason.type === "keyFetchError"` with status 404/410, cleans up the actor's data (followers, timeline items, notifications), and returns 202 Accepted.
### 38. FEP-8fcf Collection Synchronization — Outbound Only
We pass `syncCollection: true` to Fedify's `sendActivity()` for outbound activities, which attaches `Collection-Synchronization` headers with partial follower digests (XOR'd SHA-256 hashes). However, the **receiving side** (parsing inbound headers, digest comparison, reconciliation) is NOT implemented by Fedify or by us. Remote servers that send Collection-Synchronization headers to us will have them ignored. Full FEP-8fcf compliance would require a `/followers-sync` endpoint and a reconciliation scheduler.
## Form Handling Convention ## Form Handling Convention
Two form patterns are used in this plugin. New forms should follow the appropriate pattern. Two form patterns are used in this plugin. New forms should follow the appropriate pattern.

View File

@@ -1,6 +1,6 @@
# @rmdes/indiekit-endpoint-activitypub # @rmdes/indiekit-endpoint-activitypub
ActivityPub federation endpoint for [Indiekit](https://getindiekit.com), built on [Fedify](https://fedify.dev) 2.0. Makes your IndieWeb site a full fediverse actor — discoverable, followable, and interactive from Mastodon, Misskey, Pixelfed, and any ActivityPub-compatible platform. Includes a Mastodon-compatible Client API so you can use Phanpy, Elk, Moshidon, Fedilab, and other Mastodon clients with your own AP instance. ActivityPub federation endpoint for [Indiekit](https://getindiekit.com), built on [Fedify](https://fedify.dev) 2.1. Makes your IndieWeb site a full fediverse actor — discoverable, followable, and interactive from Mastodon, Misskey, Pixelfed, and any ActivityPub-compatible platform. Includes a Mastodon-compatible Client API so you can use Phanpy, Elk, Moshidon, Fedilab, and other Mastodon clients with your own AP instance.
## Features ## Features
@@ -109,10 +109,18 @@ ActivityPub federation endpoint for [Indiekit](https://getindiekit.com), built o
- Follower and following lists with source tracking - Follower and following lists with source tracking
- Federation management page with moderation overview (blocked servers, blocked accounts, muted) - Federation management page with moderation overview (blocked servers, blocked accounts, muted)
**Standards Compliance**
- FEP-5feb: Search Indexing Consent — actor advertises `indexable` and `discoverable` properties
- FEP-f1d5/0151: Enhanced NodeInfo 2.1 — rich metadata including software repository, node name, staff accounts
- FEP-4f05: Soft Delete with Tombstone — deleted posts return 410 with Tombstone JSON-LD including `formerType` and timestamps
- FEP-3b86: Activity Intents — WebFinger links for Follow, Create, Like, Announce intents with authorize_interaction routing
- FEP-8fcf: Collection Synchronization — outbound follower digest headers via Fedify `syncCollection`
- FEP-044f: Quote Posts — rendered as embedded cards (via Fedify's `quoteUrl` support)
## Requirements ## Requirements
- [Indiekit](https://getindiekit.com) v1.0.0-beta.25+ - [Indiekit](https://getindiekit.com) v1.0.0-beta.25+
- [Fedify](https://fedify.dev) 2.0+ (bundled as dependency) - [Fedify](https://fedify.dev) 2.1+ (bundled as dependency)
- Node.js >= 22 - Node.js >= 22
- MongoDB (used by Indiekit) - MongoDB (used by Indiekit)
- Redis (recommended for production delivery queue; in-process queue available for development) - Redis (recommended for production delivery queue; in-process queue available for development)
@@ -322,6 +330,7 @@ The plugin creates these collections automatically:
| `ap_blocked_servers` | Blocked server domains (instance-level blocks) | | `ap_blocked_servers` | Blocked server domains (instance-level blocks) |
| `ap_key_freshness` | Tracks when remote actor keys were last verified | | `ap_key_freshness` | Tracks when remote actor keys were last verified |
| `ap_inbox_queue` | Persistent async inbox processing queue | | `ap_inbox_queue` | Persistent async inbox processing queue |
| `ap_tombstones` | Tombstone records for soft-deleted posts (FEP-4f05) |
| `ap_oauth_apps` | Mastodon API client app registrations | | `ap_oauth_apps` | Mastodon API client app registrations |
| `ap_oauth_tokens` | OAuth2 authorization codes and access tokens | | `ap_oauth_tokens` | OAuth2 authorization codes and access tokens |
| `ap_markers` | Read position markers for Mastodon API clients | | `ap_markers` | Read position markers for Mastodon API clients |
@@ -342,7 +351,7 @@ Categories are converted to `Hashtag` tags. Bookmarks include a bookmark emoji a
## Fedify Workarounds and Implementation Notes ## Fedify Workarounds and Implementation Notes
This plugin uses [Fedify](https://fedify.dev) 2.0 but carries several workarounds for issues in Fedify or its Express integration. These are documented here so they can be revisited when Fedify upgrades. This plugin uses [Fedify](https://fedify.dev) 2.1 but carries several workarounds for issues in Fedify or its Express integration. These are documented here so they can be revisited when Fedify upgrades.
### Custom Express Bridge (instead of `@fedify/express`) ### Custom Express Bridge (instead of `@fedify/express`)
@@ -364,14 +373,11 @@ Mastodon's `update_account_fields` checks `attachment.is_a?(Array)` and silently
**Revisit when:** Fedify adds an option to preserve arrays during JSON-LD serialization, or Mastodon fixes their array check. **Revisit when:** Fedify adds an option to preserve arrays during JSON-LD serialization, or Mastodon fixes their array check.
### Endpoints `as:Endpoints` Type Stripping ### Endpoints `as:Endpoints` Type Stripping — REMOVED
**File:** `lib/federation-bridge.js` (in `sendFedifyResponse()`)
**Upstream issue:** [fedify#576](https://github.com/fedify-dev/fedify/issues/576) — FIXED in Fedify 2.1.0 **Upstream issue:** [fedify#576](https://github.com/fedify-dev/fedify/issues/576) — FIXED in Fedify 2.1.0
Fedify serializes the `endpoints` object with `"type": "as:Endpoints"`, which is not a valid ActivityStreams type. browser.pub rejects this. The bridge strips the `type` field from the `endpoints` object before sending. This workaround has been removed. Fedify 2.1.0 now omits the invalid `"type": "as:Endpoints"` from serialized actor JSON.
**Remove when:** Upgrading to Fedify ≥ 2.1.0.
### PropertyValue Attachment Type (Known Issue) ### PropertyValue Attachment Type (Known Issue)

View File

@@ -435,6 +435,20 @@ export default class ActivityPubEndpoint {
}); });
if (!post || post.properties?.deleted) { if (!post || post.properties?.deleted) {
// FEP-4f05: Serve Tombstone for deleted posts
const { getTombstone } = await import("./lib/storage/tombstones.js");
const tombstone = await getTombstone(self._collections, requestUrl);
if (tombstone) {
res.status(410).set("Content-Type", "application/activity+json").json({
"@context": "https://www.w3.org/ns/activitystreams",
type: "Tombstone",
id: requestUrl,
formerType: tombstone.formerType,
published: tombstone.published || undefined,
deleted: tombstone.deleted,
});
return;
}
return next(); return next();
} }
@@ -811,6 +825,21 @@ export default class ActivityPubEndpoint {
* @param {string} url - Full URL of the deleted post * @param {string} url - Full URL of the deleted post
*/ */
async delete(url) { async delete(url) {
// Record tombstone for FEP-4f05
try {
const { addTombstone } = await import("./lib/storage/tombstones.js");
const postsCol = this._collections.posts;
const post = postsCol ? await postsCol.findOne({ "properties.url": url }) : null;
await addTombstone(this._collections, {
url,
formerType: post?.properties?.["post-type"] === "article" ? "Article" : "Note",
published: post?.properties?.published || null,
deleted: new Date().toISOString(),
});
} catch (error) {
console.warn(`[ActivityPub] Tombstone creation failed for ${url}: ${error.message}`);
}
await this.broadcastDelete(url).catch((err) => await this.broadcastDelete(url).catch((err) =>
console.warn(`[ActivityPub] broadcastDelete failed for ${url}: ${err.message}`) console.warn(`[ActivityPub] broadcastDelete failed for ${url}: ${err.message}`)
); );
@@ -927,6 +956,8 @@ export default class ActivityPubEndpoint {
Indiekit.addCollection("ap_oauth_apps"); Indiekit.addCollection("ap_oauth_apps");
Indiekit.addCollection("ap_oauth_tokens"); Indiekit.addCollection("ap_oauth_tokens");
Indiekit.addCollection("ap_markers"); Indiekit.addCollection("ap_markers");
// Tombstones for soft-deleted posts (FEP-4f05)
Indiekit.addCollection("ap_tombstones");
// Store collection references (posts resolved lazily) // Store collection references (posts resolved lazily)
const indiekitCollections = Indiekit.collections; const indiekitCollections = Indiekit.collections;
@@ -964,6 +995,7 @@ export default class ActivityPubEndpoint {
ap_oauth_apps: indiekitCollections.get("ap_oauth_apps"), ap_oauth_apps: indiekitCollections.get("ap_oauth_apps"),
ap_oauth_tokens: indiekitCollections.get("ap_oauth_tokens"), ap_oauth_tokens: indiekitCollections.get("ap_oauth_tokens"),
ap_markers: indiekitCollections.get("ap_markers"), ap_markers: indiekitCollections.get("ap_markers"),
ap_tombstones: indiekitCollections.get("ap_tombstones"),
get posts() { get posts() {
return indiekitCollections.get("posts"); return indiekitCollections.get("posts");
}, },

View File

@@ -2,18 +2,21 @@
* Authorize Interaction controller — handles the remote follow / authorize * Authorize Interaction controller — handles the remote follow / authorize
* interaction flow for ActivityPub federation. * interaction flow for ActivityPub federation.
* *
* When a remote server (WordPress AP, Misskey, etc.) discovers our WebFinger * Supports:
* subscribe template, it redirects the user here with ?uri={actorOrPostUrl}. * - OStatus subscribe template (legacy remote follow via ?uri=...)
* - FEP-3b86 Activity Intents (via ?uri=...&intent=follow|create|like|announce)
* *
* Flow: * Flow:
* 1. Missing uri → render error page * 1. Missing uri → render error page
* 2. Unauthenticated → redirect to login, then back here * 2. Unauthenticated → redirect to login, then back here
* 3. Authenticated → redirect to the reader's remote profile page * 3. Authenticated → route to appropriate page based on intent
*/ */
export function authorizeInteractionController(plugin) { export function authorizeInteractionController(plugin) {
return async (req, res) => { return async (req, res) => {
const uri = req.query.uri || req.query.acct; const uri = req.query.uri || req.query.acct;
const intent = req.query.intent || "";
if (!uri) { if (!uri) {
return res.status(400).render("activitypub-authorize-interaction", { return res.status(400).render("activitypub-authorize-interaction", {
title: "Authorize Interaction", title: "Authorize Interaction",
@@ -29,17 +32,28 @@ export function authorizeInteractionController(plugin) {
// then back to this page after auth // then back to this page after auth
const session = req.session; const session = req.session;
if (!session?.access_token) { if (!session?.access_token) {
const returnUrl = `${plugin.options.mountPath}/authorize_interaction?uri=${encodeURIComponent(uri)}`; const params = `uri=${encodeURIComponent(uri)}${intent ? `&intent=${intent}` : ""}`;
const returnUrl = `${plugin.options.mountPath}/authorize_interaction?${params}`;
return res.redirect( return res.redirect(
`/session/login?redirect=${encodeURIComponent(returnUrl)}`, `/session/login?redirect=${encodeURIComponent(returnUrl)}`,
); );
} }
// Authenticated — redirect to the remote profile viewer in our reader const mp = plugin.options.mountPath;
// which already has follow/unfollow/like/boost functionality
const encodedUrl = encodeURIComponent(resource); const encodedUrl = encodeURIComponent(resource);
return res.redirect(
`${plugin.options.mountPath}/admin/reader/profile?url=${encodedUrl}`, // Route based on intent (FEP-3b86)
); switch (intent) {
case "follow":
return res.redirect(`${mp}/admin/reader/profile?url=${encodedUrl}`);
case "create":
return res.redirect(`${mp}/admin/reader/compose?replyTo=${encodedUrl}`);
case "like":
case "announce":
return res.redirect(`${mp}/admin/reader/post?url=${encodedUrl}`);
default:
// Default: resolve to remote profile page
return res.redirect(`${mp}/admin/reader/profile?url=${encodedUrl}`);
}
}; };
} }

View File

@@ -89,12 +89,6 @@ async function sendFedifyResponse(res, response, request) {
if (json.attachment && !Array.isArray(json.attachment)) { if (json.attachment && !Array.isArray(json.attachment)) {
json.attachment = [json.attachment]; json.attachment = [json.attachment];
} }
// WORKAROUND: Fedify serializes endpoints with "type": "as:Endpoints"
// which is not a valid AS2 type. The endpoints object should be a plain
// object with just sharedInbox/proxyUrl etc. Strip the invalid type.
if (json.endpoints && json.endpoints.type) {
delete json.endpoints.type;
}
const patched = JSON.stringify(json); const patched = JSON.stringify(json);
res.setHeader("content-length", Buffer.byteLength(patched)); res.setHeader("content-length", Buffer.byteLength(patched));
res.end(patched); res.end(patched);

View File

@@ -286,10 +286,48 @@ export function setupFederation(options) {
// Add OStatus subscribe template so remote servers (WordPress AP, Misskey, etc.) // Add OStatus subscribe template so remote servers (WordPress AP, Misskey, etc.)
// can redirect users to our authorize_interaction page for remote follow. // can redirect users to our authorize_interaction page for remote follow.
federation.setWebFingerLinksDispatcher((_ctx, _resource) => { federation.setWebFingerLinksDispatcher((_ctx, _resource) => {
const interactionBase = `${publicationUrl}${mountPath.replace(/^\//, "")}/authorize_interaction`;
return [ return [
// OStatus subscribe template (legacy remote follow)
{ {
rel: "http://ostatus.org/schema/1.0/subscribe", rel: "http://ostatus.org/schema/1.0/subscribe",
template: `${publicationUrl}${mountPath.replace(/^\//, "")}/authorize_interaction?uri={uri}`, template: `${interactionBase}?uri={uri}`,
},
// FEP-3b86 Activity Intents — Follow
{
rel: "https://w3id.org/fep/3b86",
template: `${interactionBase}?uri={uri}&intent=follow`,
properties: {
"https://w3id.org/fep/3b86#intent":
"https://www.w3.org/ns/activitystreams#Follow",
},
},
// FEP-3b86 Activity Intents — Create (reply)
{
rel: "https://w3id.org/fep/3b86",
template: `${interactionBase}?uri={uri}&intent=create`,
properties: {
"https://w3id.org/fep/3b86#intent":
"https://www.w3.org/ns/activitystreams#Create",
},
},
// FEP-3b86 Activity Intents — Like
{
rel: "https://w3id.org/fep/3b86",
template: `${interactionBase}?uri={uri}&intent=like`,
properties: {
"https://w3id.org/fep/3b86#intent":
"https://www.w3.org/ns/activitystreams#Like",
},
},
// FEP-3b86 Activity Intents — Announce (boost)
{
rel: "https://w3id.org/fep/3b86",
template: `${interactionBase}?uri={uri}&intent=announce`,
properties: {
"https://w3id.org/fep/3b86#intent":
"https://www.w3.org/ns/activitystreams#Announce",
},
}, },
]; ];
}); });
@@ -305,6 +343,43 @@ export function setupFederation(options) {
storeRawActivities, storeRawActivities,
}); });
// Handle Delete activities from actors whose signing keys are gone.
// When an account is deleted, the remote server sends Delete but the
// actor's key endpoint returns 404/410, so signature verification fails.
// Fedify 2.1.0 lets us inspect these instead of auto-rejecting.
inboxChain
.onUnverifiedActivity(async (_ctx, activity, reason) => {
// Handle Delete activities from actors whose signing keys are gone.
// When an account is deleted, the remote server sends Delete but the
// actor's key endpoint returns 404/410, so signature verification fails.
// Fedify 2.1.0 lets us inspect these instead of auto-rejecting.
if (reason.type === "keyFetchError") {
const status = reason.result?.status;
if (status === 404 || status === 410) {
const actorId = activity.actorId?.href;
if (actorId) {
const activityType = activity.constructor?.name || "";
if (activityType === "Delete") {
console.info(
`[ActivityPub] Processing unverified Delete from ${actorId} (key ${status})`,
);
try {
await collections.ap_followers.deleteOne({ actorUrl: actorId });
await collections.ap_timeline.deleteMany({ "author.url": actorId });
await collections.ap_notifications.deleteMany({ actorUrl: actorId });
console.info(`[ActivityPub] Cleaned up data for deleted actor ${actorId}`);
} catch (error) {
console.warn(`[ActivityPub] Cleanup for ${actorId} failed: ${error.message}`);
}
return new Response(null, { status: 202 });
}
}
}
}
// All other unverified activities: return null for default 401
return null;
});
// Enable authenticated fetches for the shared inbox. // Enable authenticated fetches for the shared inbox.
// Without this, Fedify can't verify incoming HTTP Signatures from servers // Without this, Fedify can't verify incoming HTTP Signatures from servers
// that require authorized fetch (e.g. hachyderm.io returns 401 on unsigned GETs). // that require authorized fetch (e.g. hachyderm.io returns 401 on unsigned GETs).
@@ -337,17 +412,33 @@ export function setupFederation(options) {
? await collections.posts.countDocuments() ? await collections.posts.countDocuments()
: 0; : 0;
const profile = await getProfile(collections);
return { return {
software: { software: {
name: "indiekit", name: "indiekit",
version: softwareVersion, version: softwareVersion,
repository: new URL("https://github.com/getindiekit/indiekit"),
homepage: new URL("https://getindiekit.com"),
}, },
protocols: ["activitypub"], protocols: ["activitypub"],
services: {
inbound: [],
outbound: [],
},
openRegistrations: false,
usage: { usage: {
users: { total: 1, activeMonth: 1, activeHalfyear: 1 }, users: { total: 1, activeMonth: 1, activeHalfyear: 1 },
localPosts: postsCount, localPosts: postsCount,
localComments: 0, localComments: 0,
}, },
metadata: {
nodeName: profile.name || handle,
nodeDescription: profile.summary || "",
staffAccounts: [
`${publicationUrl}${mountPath.replace(/^\//, "")}/users/${handle}`,
],
},
}; };
}); });
@@ -740,6 +831,8 @@ export async function buildPersonActor(
featuredTags: ctx.getFeaturedTagsUri(identifier), featuredTags: ctx.getFeaturedTagsUri(identifier),
endpoints: new Endpoints({ sharedInbox: ctx.getInboxUri() }), endpoints: new Endpoints({ sharedInbox: ctx.getInboxUri() }),
manuallyApprovesFollowers: profile.manuallyApprovesFollowers || false, manuallyApprovesFollowers: profile.manuallyApprovesFollowers || false,
indexable: true,
discoverable: true,
}; };
if (profile.summary) { if (profile.summary) {

View File

@@ -244,6 +244,12 @@ export function createIndexes(collections, options) {
{ userId: 1, timeline: 1 }, { userId: 1, timeline: 1 },
{ unique: true, background: true }, { unique: true, background: true },
); );
// Tombstone indexes (FEP-4f05)
collections.ap_tombstones?.createIndex(
{ url: 1 },
{ unique: true, background: true },
);
} catch { } catch {
// Index creation failed — collections not yet available. // Index creation failed — collections not yet available.
// Indexes already exist from previous startups; non-fatal. // Indexes already exist from previous startups; non-fatal.

52
lib/storage/tombstones.js Normal file
View File

@@ -0,0 +1,52 @@
/**
* Tombstone storage for soft-deleted posts (FEP-4f05).
* When a post is deleted, a tombstone record is created so remote servers
* fetching the URL get a proper Tombstone response instead of 404.
* @module storage/tombstones
*/
/**
* Record a tombstone for a deleted post.
* @param {object} collections - MongoDB collections
* @param {object} data - { url, formerType, published, deleted }
*/
export async function addTombstone(collections, { url, formerType, published, deleted }) {
const { ap_tombstones } = collections;
if (!ap_tombstones) return;
await ap_tombstones.updateOne(
{ url },
{
$set: {
url,
formerType: formerType || "Note",
published: published || null,
deleted: deleted || new Date().toISOString(),
},
},
{ upsert: true },
);
}
/**
* Remove a tombstone (post re-published).
* @param {object} collections - MongoDB collections
* @param {string} url - Post URL
*/
export async function removeTombstone(collections, url) {
const { ap_tombstones } = collections;
if (!ap_tombstones) return;
await ap_tombstones.deleteOne({ url });
}
/**
* Look up a tombstone by URL.
* @param {object} collections - MongoDB collections
* @param {string} url - Post URL
* @returns {Promise<object|null>} Tombstone record or null
*/
export async function getTombstone(collections, url) {
const { ap_tombstones } = collections;
if (!ap_tombstones) return null;
return ap_tombstones.findOne({ url });
}

142
package-lock.json generated
View File

@@ -1,17 +1,17 @@
{ {
"name": "@rmdes/indiekit-endpoint-activitypub", "name": "@rmdes/indiekit-endpoint-activitypub",
"version": "3.8.7", "version": "3.9.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@rmdes/indiekit-endpoint-activitypub", "name": "@rmdes/indiekit-endpoint-activitypub",
"version": "3.8.7", "version": "3.9.4",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@fedify/debugger": "^2.0.0", "@fedify/debugger": "^2.1.0",
"@fedify/fedify": "^2.0.0", "@fedify/fedify": "^2.1.0",
"@fedify/redis": "^2.0.0", "@fedify/redis": "^2.1.0",
"@js-temporal/polyfill": "^0.5.0", "@js-temporal/polyfill": "^0.5.0",
"express": "^5.0.0", "express": "^5.0.0",
"express-rate-limit": "^7.5.1", "express-rate-limit": "^7.5.1",
@@ -515,12 +515,12 @@
} }
}, },
"node_modules/@fedify/debugger": { "node_modules/@fedify/debugger": {
"version": "2.0.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/@fedify/debugger/-/debugger-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@fedify/debugger/-/debugger-2.1.0.tgz",
"integrity": "sha512-2ZGXKQa+BqAO9F+tuZcDiLHDr193cUqKlnX1Z7yDn1ICPL1gPxxwgKAa1b540pBBWSfDCXBSrJlZ3DYK9f52GA==", "integrity": "sha512-4s3L3/NkofZCUXR1jADq5ukSbWybpWqgqF4TEg3PHxlXkC3bT/LI4as8zFxTpkkCvM5fE6tXCi5z56rJ9tXzag==",
"dependencies": { "dependencies": {
"@js-temporal/polyfill": "^0.5.1", "@js-temporal/polyfill": "^0.5.1",
"@logtape/logtape": "^2.0.0", "@logtape/logtape": "^2.0.5",
"@opentelemetry/api": "^1.9.0", "@opentelemetry/api": "^1.9.0",
"@opentelemetry/context-async-hooks": "^2.5.0", "@opentelemetry/context-async-hooks": "^2.5.0",
"@opentelemetry/core": "^2.5.0", "@opentelemetry/core": "^2.5.0",
@@ -528,24 +528,24 @@
"hono": "^4.0.0" "hono": "^4.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@fedify/fedify": "^2.0.0" "@fedify/fedify": "^2.1.0"
} }
}, },
"node_modules/@fedify/fedify": { "node_modules/@fedify/fedify": {
"version": "2.0.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/@fedify/fedify/-/fedify-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@fedify/fedify/-/fedify-2.1.0.tgz",
"integrity": "sha512-ZejBFXfILViuIHYhI1BWEk1Pewt9hNO70u6GVaWYKWwU3IVc1/HEsGA/kK9IxJKYZBPqLcCtoI2BZfeOg8I/Hg==", "integrity": "sha512-CMGlL9HEaqyuQL4Ma0Jv+9/QgtLjj+HLmjNrg1e/WUQrEwZg9p5WYKk4iNKXF4aIG3XJkAv5UGJlHKF09HifNA==",
"funding": [ "funding": [
"https://opencollective.com/fedify", "https://opencollective.com/fedify",
"https://github.com/sponsors/dahlia" "https://github.com/sponsors/dahlia"
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@fedify/vocab": "2.0.0", "@fedify/vocab": "2.1.0",
"@fedify/vocab-runtime": "2.0.0", "@fedify/vocab-runtime": "2.1.0",
"@fedify/webfinger": "2.0.0", "@fedify/webfinger": "2.1.0",
"@js-temporal/polyfill": "^0.5.1", "@js-temporal/polyfill": "^0.5.1",
"@logtape/logtape": "^2.0.0", "@logtape/logtape": "^2.0.5",
"@opentelemetry/api": "^1.9.0", "@opentelemetry/api": "^1.9.0",
"@opentelemetry/core": "^2.5.0", "@opentelemetry/core": "^2.5.0",
"@opentelemetry/sdk-trace-base": "^2.5.0", "@opentelemetry/sdk-trace-base": "^2.5.0",
@@ -554,7 +554,6 @@
"es-toolkit": "1.43.0", "es-toolkit": "1.43.0",
"json-canon": "^1.0.1", "json-canon": "^1.0.1",
"jsonld": "^9.0.0", "jsonld": "^9.0.0",
"multicodec": "^3.2.1",
"structured-field-values": "^2.0.4", "structured-field-values": "^2.0.4",
"uri-template-router": "^1.0.0", "uri-template-router": "^1.0.0",
"url-template": "^3.1.1", "url-template": "^3.1.1",
@@ -567,9 +566,9 @@
} }
}, },
"node_modules/@fedify/redis": { "node_modules/@fedify/redis": {
"version": "2.0.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/@fedify/redis/-/redis-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@fedify/redis/-/redis-2.1.0.tgz",
"integrity": "sha512-WHUhEHZ0BAbcmITgDTdspeolfl4bHpRx+BlmdVRsGScaoQODvvohBfRcTCGbMz2RfQmkhz4l297Nk8Nlqyvg7Q==", "integrity": "sha512-Fqud46FIEBFXDFad029rS46ZlVlWZU2zT6yhBs63jtat7QwMIHDSisizvoVyky4a41TX0ItBNqiAdYELLv/0NQ==",
"funding": [ "funding": [
"https://opencollective.com/fedify", "https://opencollective.com/fedify",
"https://github.com/sponsors/dahlia" "https://github.com/sponsors/dahlia"
@@ -577,34 +576,33 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@js-temporal/polyfill": "^0.5.1", "@js-temporal/polyfill": "^0.5.1",
"@logtape/logtape": "^2.0.0" "@logtape/logtape": "^2.0.5"
}, },
"peerDependencies": { "peerDependencies": {
"@fedify/fedify": "^2.0.0", "@fedify/fedify": "^2.1.0",
"ioredis": "^5.8.2" "ioredis": "^5.8.2"
} }
}, },
"node_modules/@fedify/vocab": { "node_modules/@fedify/vocab": {
"version": "2.0.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/@fedify/vocab/-/vocab-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@fedify/vocab/-/vocab-2.1.0.tgz",
"integrity": "sha512-sjw51UltqefhGPxkcSrnwdmBHO5Zm3hOlOHFvzsLg1pbl53KKETcJ8TG6OcMaD0ZiaUqFVKkGAlpDG3FD9O4nw==", "integrity": "sha512-tGCgo8kCj6Zwf1JxYsXtEwReujzgitndf59Pdo1BY21UgpAlAe0daY8vdpRM+NybZ4JbOBtM4bH473LVtJlVEA==",
"funding": [ "funding": [
"https://opencollective.com/fedify", "https://opencollective.com/fedify",
"https://github.com/sponsors/dahlia" "https://github.com/sponsors/dahlia"
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@fedify/vocab-runtime": "2.0.0", "@fedify/vocab-runtime": "2.1.0",
"@fedify/vocab-tools": "2.0.0", "@fedify/vocab-tools": "2.1.0",
"@fedify/webfinger": "2.0.0", "@fedify/webfinger": "2.1.0",
"@js-temporal/polyfill": "^0.5.1", "@js-temporal/polyfill": "^0.5.1",
"@logtape/logtape": "^2.0.0", "@logtape/logtape": "^2.0.5",
"@multiformats/base-x": "^4.0.1", "@multiformats/base-x": "^4.0.1",
"@opentelemetry/api": "^1.9.0", "@opentelemetry/api": "^1.9.0",
"asn1js": "^3.0.6", "asn1js": "^3.0.6",
"es-toolkit": "1.43.0", "es-toolkit": "1.43.0",
"jsonld": "^9.0.0", "jsonld": "^9.0.0",
"multicodec": "^3.2.1",
"pkijs": "^3.3.3" "pkijs": "^3.3.3"
}, },
"engines": { "engines": {
@@ -614,21 +612,21 @@
} }
}, },
"node_modules/@fedify/vocab-runtime": { "node_modules/@fedify/vocab-runtime": {
"version": "2.0.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/@fedify/vocab-runtime/-/vocab-runtime-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@fedify/vocab-runtime/-/vocab-runtime-2.1.0.tgz",
"integrity": "sha512-Cdcbhki75kBi20Eq0Dkpf1XXXVKVwnOzK4O/b4MKH6kmUPEcVyNVb9L3+ZZElViE+kAZ0bmYLlFNp42E7mjjLQ==", "integrity": "sha512-rISQFJbuRrt1OX9yG+xVUn7DwBTajpOOvy5jdx2ZuRUMvtlD7bgDEUSSS5a7pFuYliKVbR5ZFk6BPAkJC1OnAw==",
"funding": [ "funding": [
"https://opencollective.com/fedify", "https://opencollective.com/fedify",
"https://github.com/sponsors/dahlia" "https://github.com/sponsors/dahlia"
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@logtape/logtape": "^2.0.0", "@logtape/logtape": "^2.0.5",
"@multiformats/base-x": "^4.0.1", "@multiformats/base-x": "^4.0.1",
"@opentelemetry/api": "^1.9.0", "@opentelemetry/api": "^1.9.0",
"asn1js": "^3.0.6", "asn1js": "^3.0.6",
"byte-encodings": "^1.0.11", "byte-encodings": "^1.0.11",
"multicodec": "^3.2.1", "jsonld": "^9.0.0",
"pkijs": "^3.3.3" "pkijs": "^3.3.3"
}, },
"engines": { "engines": {
@@ -638,9 +636,9 @@
} }
}, },
"node_modules/@fedify/vocab-tools": { "node_modules/@fedify/vocab-tools": {
"version": "2.0.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/@fedify/vocab-tools/-/vocab-tools-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@fedify/vocab-tools/-/vocab-tools-2.1.0.tgz",
"integrity": "sha512-AQ1zjGt4wjaTjTOHCgDroNITiSeZ1z99ygNEkmukg5EwgjF7+DVoPV+OTrmVVW/2A6t1blJzfOS0wrlzsn5lqQ==", "integrity": "sha512-Gn07LbMoDRVDjklDZH9y/fZ2nwH7ryjillgLpw8qsbjUeVaTViR1Oz/oG2N7S13UqyKttYPzK8hH/utKr1LbXg==",
"funding": [ "funding": [
"https://opencollective.com/fedify", "https://opencollective.com/fedify",
"https://github.com/sponsors/dahlia" "https://github.com/sponsors/dahlia"
@@ -659,17 +657,17 @@
} }
}, },
"node_modules/@fedify/webfinger": { "node_modules/@fedify/webfinger": {
"version": "2.0.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/@fedify/webfinger/-/webfinger-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@fedify/webfinger/-/webfinger-2.1.0.tgz",
"integrity": "sha512-D66ZdyUUM9BNQU7OGEWmNLU+FIHApUiYnEogOa5oNj9fy0vjOfxm9hynA+0SCm3emrKrLt6Gm120Re+T5Us5Yg==", "integrity": "sha512-G5yrCPw1oWijvkGOMjWZFOWohmljQ4pmHgK7BuESshcAizpKRU0t5GcOGMyPzcNrO4+diaddGNg48GFzZ9mK/g==",
"funding": [ "funding": [
"https://opencollective.com/fedify", "https://opencollective.com/fedify",
"https://github.com/sponsors/dahlia" "https://github.com/sponsors/dahlia"
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@fedify/vocab-runtime": "2.0.0", "@fedify/vocab-runtime": "2.1.0",
"@logtape/logtape": "^2.0.0", "@logtape/logtape": "^2.0.5",
"@opentelemetry/api": "^1.9.0", "@opentelemetry/api": "^1.9.0",
"es-toolkit": "1.43.0" "es-toolkit": "1.43.0"
}, },
@@ -1264,9 +1262,9 @@
} }
}, },
"node_modules/@logtape/logtape": { "node_modules/@logtape/logtape": {
"version": "2.0.2", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/@logtape/logtape/-/logtape-2.0.2.tgz", "resolved": "https://registry.npmjs.org/@logtape/logtape/-/logtape-2.0.5.tgz",
"integrity": "sha512-cveUBLbCMFkvkLycP/2vNWvywl47JcJbazHIju94/QNGboZ/jyYAgZIm0ZXezAOx3eIz8OG1EOZ5CuQP3+2FQg==", "integrity": "sha512-UizDkh20ZPJVOddRxG1F77WhHdlNl/sbQgoO8T534R7XvUBMAJ9En9f35u+meW2tRsNLvjz6R87Zanwf53tspQ==",
"funding": [ "funding": [
"https://github.com/sponsors/dahlia" "https://github.com/sponsors/dahlia"
], ],
@@ -2970,23 +2968,6 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/multicodec": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/multicodec/-/multicodec-3.2.1.tgz",
"integrity": "sha512-+expTPftro8VAW8kfvcuNNNBgb9gPeNYV9dn+z1kJRWF2vih+/S79f2RVeIwmrJBUJ6NT9IUPWnZDQvegEh5pw==",
"deprecated": "This module has been superseded by the multiformats module",
"license": "MIT",
"dependencies": {
"uint8arrays": "^3.0.0",
"varint": "^6.0.0"
}
},
"node_modules/multiformats": {
"version": "9.9.0",
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz",
"integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==",
"license": "(Apache-2.0 AND MIT)"
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -3157,9 +3138,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/pkijs": { "node_modules/pkijs": {
"version": "3.3.3", "version": "3.4.0",
"resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.3.3.tgz", "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz",
"integrity": "sha512-+KD8hJtqQMYoTuL1bbGOqxb4z+nZkTAwVdNtWwe8Tc2xNbEmdJYIYoc6Qt0uF55e6YW6KuTHw1DjQ18gMhzepw==", "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"@noble/hashes": "1.4.0", "@noble/hashes": "1.4.0",
@@ -3656,19 +3637,10 @@
"license": "MIT", "license": "MIT",
"peer": true "peer": true
}, },
"node_modules/uint8arrays": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.1.1.tgz",
"integrity": "sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg==",
"license": "MIT",
"dependencies": {
"multiformats": "^9.4.2"
}
},
"node_modules/undici": { "node_modules/undici": {
"version": "6.23.0", "version": "6.24.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz",
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18.17" "node": ">=18.17"
@@ -3741,12 +3713,6 @@
"integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==", "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/varint": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
"integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==",
"license": "MIT"
},
"node_modules/vary": { "node_modules/vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -3785,9 +3751,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/yaml": { "node_modules/yaml": {
"version": "2.8.2", "version": "2.8.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"yaml": "bin.mjs" "yaml": "bin.mjs"

View File

@@ -1,6 +1,6 @@
{ {
"name": "@rmdes/indiekit-endpoint-activitypub", "name": "@rmdes/indiekit-endpoint-activitypub",
"version": "3.9.4", "version": "3.10.0",
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.", "description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
"keywords": [ "keywords": [
"indiekit", "indiekit",
@@ -37,9 +37,9 @@
"url": "https://github.com/rmdes/indiekit-endpoint-activitypub/issues" "url": "https://github.com/rmdes/indiekit-endpoint-activitypub/issues"
}, },
"dependencies": { "dependencies": {
"@fedify/debugger": "^2.0.0", "@fedify/debugger": "^2.1.0",
"@fedify/fedify": "^2.0.0", "@fedify/fedify": "^2.1.0",
"@fedify/redis": "^2.0.0", "@fedify/redis": "^2.1.0",
"@js-temporal/polyfill": "^0.5.0", "@js-temporal/polyfill": "^0.5.0",
"express": "^5.0.0", "express": "^5.0.0",
"express-rate-limit": "^7.5.1", "express-rate-limit": "^7.5.1",