From 26c81a6a76c5083c496c3e6bb1a4e54248f14799 Mon Sep 17 00:00:00 2001 From: Ricardo Date: Tue, 17 Mar 2026 17:12:30 +0100 Subject: [PATCH] fix: exclude soft-deleted posts from outbox and content negotiation Deleted posts (with properties.deleted timestamp) were still served via the outbox dispatcher and content negotiation catch-all. Now: - Outbox find() and countDocuments() filter out deleted posts - Object dispatcher returns null for deleted posts (Fedify 404) - Content negotiation falls through to Express for deleted posts Confab-Link: http://localhost:8080/sessions/af5f8b45-6b8d-442d-8f25-78c326190709 --- index.js | 2 +- lib/federation-setup.js | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 3d4c474..243d864 100644 --- a/index.js +++ b/index.js @@ -434,7 +434,7 @@ export default class ActivityPubEndpoint { "properties.url": requestUrl, }); - if (!post) { + if (!post || post.properties?.deleted) { return next(); } diff --git a/lib/federation-setup.js b/lib/federation-setup.js index 5fb7ab0..920f225 100644 --- a/lib/federation-setup.js +++ b/lib/federation-setup.js @@ -609,10 +609,11 @@ function setupOutbox(federation, mountPath, handle, collections) { const pageSize = 20; const skip = cursor ? Number.parseInt(cursor, 10) : 0; - const total = await postsCollection.countDocuments(); + const notDeleted = { "properties.deleted": { $exists: false } }; + const total = await postsCollection.countDocuments(notDeleted); const posts = await postsCollection - .find() + .find(notDeleted) .sort({ "properties.published": -1 }) .skip(skip) .limit(pageSize) @@ -644,7 +645,9 @@ function setupOutbox(federation, mountPath, handle, collections) { if (identifier !== handle) return 0; const postsCollection = collections.posts; if (!postsCollection) return 0; - return await postsCollection.countDocuments(); + return await postsCollection.countDocuments({ + "properties.deleted": { $exists: false }, + }); }) .setFirstCursor(async () => "0"); } @@ -656,6 +659,8 @@ function setupObjectDispatchers(federation, mountPath, handle, collections, publ const postUrl = `${publicationUrl.replace(/\/$/, "")}/${id}`; const post = await collections.posts.findOne({ "properties.url": postUrl }); if (!post) return null; + // Soft-deleted posts should not be dereferenceable + if (post.properties?.deleted) return null; const actorUrl = ctx.getActorUri(handle).href; const activity = jf2ToAS2Activity(post.properties, actorUrl, publicationUrl); // Only Create activities wrap Note/Article objects