Index: src/routes/activitypub.js
===================================================================
--- src/routes/activitypub.js	(revision 79f00c5d0aa2ce1cf50b70750004354f2a8e99bf)
+++ src/routes/activitypub.js	(revision 73a10c23efd11af6b4f5da8a0187b68d003df265)
@@ -82,13 +82,27 @@
   const site = publicSite(req.params.slug);
   if (!site) return res.status(404).end();
-  // Authorized fetch (FEP-633c §5.3 note): a committed guardian doing a SIGNED
-  // GET may read the ward's fan-only history too, without appearing as a
-  // follower. Unsigned / non-guardian callers get the public collection only.
-  let asGuardian = false;
-  if (req.headers['signature']) {
+  // Authorized fetch (30-7): who is asking decides what they see.
+  //  - the owner's own app (bearer) and a verified accepted follower or
+  //    guardian get the friends-only history too, so a NEW friend's backfill
+  //    brings the past along (Robins besluit: vrienden krijgen de
+  //    geschiedenis mee);
+  //  - a verified caller this instance BLOCKS gets an EMPTY collection, not
+  //    even the public set: a block is a closed door, and a signed fetch is
+  //    the caller knocking with their name on it;
+  //  - everyone else gets the public collection, exactly as before.
+  const bearer = OAuth.verifyBearer(req.headers.authorization);
+  let verifiedActor = null;
+  if (!bearer && req.headers['signature']) {
     const verified = await AP.verifyRequest(req).catch(() => null);
-    asGuardian = !!(verified && AP.isWardGuardian(req.params.slug, verified.id));
-  }
-  const fanClause = asGuardian ? '' : "AND (fan_only IS NULL OR fan_only = 0)";
+    verifiedActor = verified && verified.id;
+  }
+  const audience = AP.outboxAudience(req.params.slug, {
+    bearerSlug: bearer ? bearer.site.slug : null,
+    verifiedActor,
+  });
+  if (audience === 'blocked') {
+    return AP.sendAP(res, AP.buildOutbox(baseUrl(req), site, []), 'private, no-store');
+  }
+  const fanClause = audience === 'friend' ? '' : "AND (fan_only IS NULL OR fan_only = 0)";
   const posts = db.prepare(
     `SELECT id, slug, title, content, cover_image_url, cover_video_url, nsfw, content_warning, published_at, created_at
@@ -96,5 +110,5 @@
      ORDER BY COALESCE(published_at, created_at) DESC LIMIT 20`
   ).all(site.id);
-  AP.sendAP(res, AP.buildOutbox(baseUrl(req), site, posts), asGuardian ? 'private, no-store' : undefined);
+  AP.sendAP(res, AP.buildOutbox(baseUrl(req), site, posts), audience === 'friend' ? 'private, no-store' : undefined);
 });
 
