mirror of
https://github.com/svemagie/indiekit-endpoint-activitypub.git
synced 2026-04-02 15:44:58 +02:00
fix: AP inbox reliability — PeerTube View skip, raw body digest, signature window, trailing slash
federation-bridge.js: - Buffer application/activity+json and ld+json bodies that Express doesn't parse (inbox POSTs from Mastodon, PeerTube, etc.) - Store original bytes in req._rawBody and pass them verbatim to Fedify so HTTP Signature Digest verification passes; JSON.stringify reorders keys which caused every Mastodon Like/Announce/Create to be silently rejected - Short-circuit PeerTube View (WatchAction) activities with 200 before Fedify's JSON-LD parser throws on Schema.org extensions federation-setup.js: - Accept signatures up to 12 hours old (Mastodon retries with the original signature hours after a failed delivery) - Look up AP object URLs with $in [url, url+"/"] to tolerate trailing slash differences between stored posts and AP object URLs inbox-listeners.js: - Register a no-op .on(View) handler so Fedify doesn't log noisy "Unsupported activity type" errors for PeerTube watch events Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,8 +35,11 @@ export function fromExpressRequest(req) {
|
||||
// Express body parser already consumed the stream — reconstruct
|
||||
// so downstream handlers (e.g. @fedify/debugger login) can read it.
|
||||
const ct = req.headers["content-type"] || "";
|
||||
if (ct.includes("application/json")) {
|
||||
body = JSON.stringify(req.body);
|
||||
// Handle activity+json and ld+json bodies (PeerTube, Mastodon, etc.).
|
||||
// Use original raw bytes when available (set by the buffer guard below)
|
||||
// so Fedify's HTTP Signature Digest check passes.
|
||||
if (ct.includes("application/json") || ct.includes("activity+json") || ct.includes("ld+json")) {
|
||||
body = req._rawBody || JSON.stringify(req.body);
|
||||
} else if (ct.includes("application/x-www-form-urlencoded")) {
|
||||
body = new URLSearchParams(req.body).toString();
|
||||
} else {
|
||||
@@ -129,6 +132,40 @@ async function sendFedifyResponse(res, response, request) {
|
||||
export function createFedifyMiddleware(federation, contextDataFactory) {
|
||||
return async (req, res, next) => {
|
||||
try {
|
||||
// Buffer application/activity+json and ld+json request bodies ourselves —
|
||||
// Express's JSON body parser only handles application/json, so req.body
|
||||
// is otherwise undefined for AP inbox POSTs. We need the body to:
|
||||
// 1. Detect and short-circuit PeerTube View (WatchAction) activities
|
||||
// before Fedify's JSON-LD parser chokes on Schema.org extensions.
|
||||
// 2. Preserve original bytes in req._rawBody so fromExpressRequest()
|
||||
// can pass them to Fedify verbatim, keeping HTTP Signature Digest
|
||||
// verification intact (JSON.stringify reorders keys, breaking it).
|
||||
const _apct = req.headers["content-type"] || "";
|
||||
if (
|
||||
req.method === "POST" &&
|
||||
!req.body &&
|
||||
req.readable &&
|
||||
(_apct.includes("activity+json") || _apct.includes("ld+json"))
|
||||
) {
|
||||
const _chunks = [];
|
||||
for await (const _chunk of req) {
|
||||
_chunks.push(Buffer.isBuffer(_chunk) ? _chunk : Buffer.from(_chunk));
|
||||
}
|
||||
const _raw = Buffer.concat(_chunks);
|
||||
req._rawBody = _raw; // preserve for Fedify Digest check
|
||||
try {
|
||||
req.body = JSON.parse(_raw.toString("utf8"));
|
||||
} catch {
|
||||
req.body = {};
|
||||
}
|
||||
}
|
||||
// Silently accept PeerTube View (WatchAction) — return 200 to prevent
|
||||
// retries. Fedify's vocab parser throws on PeerTube's Schema.org
|
||||
// extensions before any inbox handler is reached.
|
||||
if (req.method === "POST" && req.body?.type === "View") {
|
||||
return res.status(200).end();
|
||||
}
|
||||
|
||||
const request = fromExpressRequest(req);
|
||||
const contextData = await Promise.resolve(contextDataFactory(req));
|
||||
|
||||
|
||||
@@ -135,6 +135,10 @@ export function setupFederation(options) {
|
||||
const federation = createFederation({
|
||||
kv,
|
||||
queue,
|
||||
// Accept signatures up to 12 h old.
|
||||
// Mastodon retries failed deliveries with the original signature, which
|
||||
// can be hours old by the time the delivery succeeds.
|
||||
signatureTimeWindow: { hours: 12 },
|
||||
});
|
||||
|
||||
// --- Actor dispatcher ---
|
||||
@@ -659,7 +663,11 @@ function setupObjectDispatchers(federation, mountPath, handle, collections, publ
|
||||
async function resolvePost(ctx, id) {
|
||||
if (!collections.posts || !publicationUrl) return null;
|
||||
const postUrl = `${publicationUrl.replace(/\/$/, "")}/${id}`;
|
||||
const post = await collections.posts.findOne({ "properties.url": postUrl });
|
||||
// Match with or without trailing slash — AP object URLs omit the slash
|
||||
// but posts are stored with one, so an exact match would fail.
|
||||
const post = await collections.posts.findOne({
|
||||
"properties.url": { $in: [postUrl, postUrl + "/"] },
|
||||
});
|
||||
if (!post) return null;
|
||||
if (post?.properties?.["post-status"] === "draft") return null;
|
||||
if (post?.properties?.visibility === "unlisted") return null;
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
Remove,
|
||||
Undo,
|
||||
Update,
|
||||
View,
|
||||
} from "@fedify/fedify/vocab";
|
||||
|
||||
import { logActivity as logActivityShared } from "./activity-log.js";
|
||||
@@ -822,7 +823,13 @@ export function registerInboxListeners(inboxChain, options) {
|
||||
} catch (error) {
|
||||
console.warn("[ActivityPub] Flag handler error:", error.message);
|
||||
}
|
||||
});
|
||||
})
|
||||
// ── View (PeerTube watch) ─────────────────────────────────────────────
|
||||
// PeerTube broadcasts View (WatchAction) activities to all followers
|
||||
// whenever someone watches a video. Fedify has no built-in handler for
|
||||
// this type, producing noisy "Unsupported activity type" log errors.
|
||||
// Silently accept and discard.
|
||||
.on(View, async () => {});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user