Deliveries to followers carry a digest of them, per server

Owner decision of 2026-10-06 (FEP-8fcf). A persona's delivery addressed to its followers carries a signed
Collection-Synchronization header naming its followers, its roll-call (…/groupies/roll-call) and the digest of its
accepted followers on the receiving server only. The roll-call answers a signed request with the persona's followers
on the signer's server and nobody else's.

Mastodon gives every Undo{Follow} it sends after reading a roll-call the same id (…#follows//undo), so a second one
looked like a copy: an Undo of a Follow that comes again while the follow it ends exists again is now kept once per
follow.

Checked live (scenarios/followsync.sh): Mastodon drops a follow PrivaPub lost, and undoes one it lost itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-06 07:37:03 +02:00
1 parent bf37223849
commit bd18de35a6
10 files changed
+385 -4

No files matched your search

@@ -17,7 +17,8 @@ namespace PrivaPub.Federation.Signing
public static string Digest(byte[] body) => "SHA-256=" + Convert.ToBase64String(SHA256.HashData(body));
public static void Sign(HttpRequestMessage request, LocalActor signer, byte[] body)
// extra: further headers the request carries, signed with the rest (FEP-8fcf's Collection-Synchronization)
public static void Sign(HttpRequestMessage request, LocalActor signer, byte[] body, params (string Name, string Value)[] extra)
{
var date = DateTime.UtcNow.ToString("r", CultureInfo.InvariantCulture);
var signed = new List<(string Name, string Value)>
@@ -35,6 +36,12 @@ namespace PrivaPub.Federation.Signing
signed.Add(("digest", digest));
}
foreach (var (name, value) in extra)
{
request.Headers.TryAddWithoutValidation(name, value);
signed.Add((name.ToLowerInvariant(), value));
}
var signingString = string.Join("\n", signed.Select(h => $"{h.Name}: {h.Value}"));
using var rsa = RSA.Create();
rsa.ImportFromPem(signer.PrivateKeyPem);