Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 57aee3b54578a6fb36e40670c99cd44331a0e94f)
+++ src/services/ActivityPubService.js	(revision 691311ff3de5795d7284e03d44fc46f23a469922)
@@ -1284,5 +1284,21 @@
 }
 
-export async function fetchActor(url) {
+export async function fetchActor(url, opts = {}) {
+  // Authorized fetch (Mastodons secure mode): zo'n instance serveert zijn
+  // actor-document -- en dus zijn publieke sleutel -- alleen aan een ONDERTEKEND
+  // verzoek en antwoordt anders met 401. Zonder sleutel kunnen we een correct
+  // ondertekende Follow van die instance niet verifiëren en wijzen we hem af,
+  // waarna Mastodon het dagenlang blijft proberen. Gemeten op boiert.eu: vier
+  // accounts eindeloos geweigerd, en precies die vier geven 401 op een
+  // onbetekende GET (shaer-afq).
+  //
+  // Geen kip-ei: om ONZE handtekening te controleren haalt de andere kant ons
+  // actor-document op, en dat serveert Klonkt publiek.
+  if (opts.asSlug) {
+    const signed = await signedGetJson(opts.asSlug, url).catch(() => null);
+    if (signed && signed.id) return signed;
+    // Geen terugkeer bij mislukking: een instance zonder secure mode moet
+    // blijven werken, en niet elke 401 komt van authorized fetch.
+  }
   try {
     const r = await safeFetch(url, { headers: { Accept: 'application/activity+json' } });
@@ -1368,4 +1384,14 @@
 }
 
+/** Een lokale site om GETs mee te ondertekenen wanneer er geen specifieke is
+ *  (de gedeelde inbox). Gecached: dit draait per binnenkomend verzoek. */
+let _signSlug;
+function anySigningSlug() {
+  if (_signSlug !== undefined) return _signSlug;
+  try { const r = db.prepare('SELECT slug FROM sites ORDER BY rowid LIMIT 1').get(); _signSlug = (r && r.slug) || null; }
+  catch { _signSlug = null; }
+  return _signSlug;
+}
+
 // Best-effort verification of an incoming signed request. Returns the sender's
 // actor doc if the signature checks out, else null. (Not gating yet — MVP.)
@@ -1373,10 +1399,14 @@
 // federating servers with drifting clocks; an operator can widen it via env.
 const SIG_MAX_SKEW_MS = (Number(process.env.AP_SIG_MAX_SKEW_MIN) || 60) * 60 * 1000;
-export async function verifyRequest(req) {
+export async function verifyRequest(req, asSlug = null) {
   const sigH = req.headers['signature'];
   if (!sigH) return null;
   const p = Object.fromEntries([...sigH.matchAll(/([a-zA-Z]+)="([^"]*)"/g)].map((m) => [m[1], m[2]]));
   if (!p.keyId || !p.signature) return null;
-  const actor = await fetchActor(p.keyId.split('#')[0]);
+  // Onderteken de sleutel-ophaal, anders faalt elke instance met authorized
+  // fetch (shaer-afq). Zonder aangewezen site -- de gedeelde inbox -- tekenen we
+  // als een willekeurige lokale actor: elke Klonkt-actor is een geldige
+  // ondertekenaar, het gaat de andere kant er alleen om DAT er ondertekend is.
+  const actor = await fetchActor(p.keyId.split('#')[0], { asSlug: asSlug || anySigningSlug() });
   const pem = actor && actor.publicKey && actor.publicKey.publicKeyPem;
   if (!pem) return null;
@@ -1678,5 +1708,5 @@
   // keeps everything below identical, including the actor-versus-signer check,
   // which is exactly the check that must not be skipped for being local.
-  const verified = preVerified || await verifyRequest(req).catch(() => null);
+  const verified = preVerified || await verifyRequest(req, slugParam).catch(() => null);
 
   // ENFORCE HTTP signatures: a data-affecting activity must be signed by the very
Index: test/authorized-fetch.test.js
===================================================================
--- test/authorized-fetch.test.js	(revision 57aee3b54578a6fb36e40670c99cd44331a0e94f)
+++ test/authorized-fetch.test.js	(revision 691311ff3de5795d7284e03d44fc46f23a469922)
@@ -1,23 +1,89 @@
-// FEP-633c §5.3 note: a committed guardian is recognised for authorized fetch.
+// Authorized fetch (shaer-afq): een instance in Mastodons secure mode geeft zijn
+// actor-document -- en dus zijn publieke sleutel -- alleen aan een ONDERTEKEND
+// verzoek. Zonder die handtekening kregen we 401, vonden we geen sleutel, en
+// wezen we elke correct ondertekende Follow van die instance af. Op boiert.eu
+// bleven daardoor vier accounts eindeloos hangen.
+//
+// De fetch is hier gestubd: het gaat om de vraag OF er ondertekend wordt en of
+// er wordt teruggevallen, niet om echte HTTP of crypto.
+//
+// Run: npm test
+
 import { test } from 'node:test';
 import assert from 'node:assert/strict';
 
 process.env.DATABASE_PATH = ':memory:';
-process.env.PUBLIC_BASE_URL = 'https://test.example';
+process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
 
 const dbMod = await import('../src/config/database.js');
 const db = dbMod.default;
 dbMod.initializeDatabase();
-const AP = (await import('../src/services/ActivityPubService.js')).default;
+const AP = await import('../src/services/ActivityPubService.js');
 
-db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)').run('u1', 'u1', 'u1@t', 'x', 'god');
-db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,1)').run('s1', 'kid', 'kid', 'u1');
-// Commit a guardian relation: kid (ward) is guarded by mom.
-const MOM = 'https://mom.example/ap/users/mom';
-db.prepare("INSERT INTO ap_guardianships (slug, role, other_uri, status, created_at) VALUES ('kid','ward',?, 'accepted', CURRENT_TIMESTAMP)").run(MOM);
+db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
+  .run('u1', 'u1', 'u1@test', 'x', 'god');
+db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'me', 'Me', 'u1');
 
-test('a committed guardian is recognised; a stranger is not', () => {
-  assert.equal(AP.isWardGuardian('kid', MOM), true);
-  assert.equal(AP.isWardGuardian('kid', 'https://x.example/ap/users/stranger'), false);
-  assert.equal(AP.isWardGuardian('nosuch', MOM), false);
+// IP-literals: safeFetch slaat de DNS-lookup over, dus de test blijft offline.
+const OPEN_ACTOR = 'https://203.0.113.30/users/open';       // gewone instance
+const SECURE_ACTOR = 'https://203.0.113.40/users/gesloten'; // authorized fetch
+
+let verzoeken = [];
+const echteFetch = globalThis.fetch;
+globalThis.fetch = async (url, opts = {}) => {
+  const u = String(url);
+  const ondertekend = !!(opts.headers && (opts.headers.Signature || opts.headers.signature));
+  verzoeken.push({ url: u, ondertekend });
+  const doc = (id) => new Response(JSON.stringify({
+    id, type: 'Person', preferredUsername: 'x', inbox: `${id}/inbox`,
+    publicKey: { id: `${id}#main-key`, owner: id, publicKeyPem: '-----BEGIN PUBLIC KEY-----\nx\n-----END PUBLIC KEY-----' },
+  }), { status: 200, headers: { 'content-type': 'application/activity+json' } });
+
+  if (u === OPEN_ACTOR) return doc(OPEN_ACTOR);
+  // De kern van secure mode: onbetekend is het 401, ondertekend krijg je hem wel.
+  if (u === SECURE_ACTOR) return ondertekend ? doc(SECURE_ACTOR) : new Response('unauthorized', { status: 401 });
+  return new Response('not found', { status: 404 });
+};
+
+test('een actor achter authorized fetch wordt nu wél opgehaald', async () => {
+  verzoeken = [];
+  const actor = await AP.fetchActor(SECURE_ACTOR, { asSlug: 'me' });
+  assert.ok(actor, 'de actor hoort binnen te komen');
+  assert.equal(actor.id, SECURE_ACTOR);
+  assert.ok(verzoeken.some((v) => v.url === SECURE_ACTOR && v.ondertekend), 'het verzoek hoort ondertekend te zijn');
 });
+
+test('zonder ondertekenaar blijft dezelfde actor onbereikbaar', async () => {
+  // Dit is precies het oude gedrag, en het bewijst dat de stub echt onderscheid
+  // maakt in plaats van altijd mee te werken.
+  const actor = await AP.fetchActor(SECURE_ACTOR);
+  assert.equal(actor, null);
+});
+
+test('een gewone instance blijft werken, ondertekend of niet', async () => {
+  assert.ok(await AP.fetchActor(OPEN_ACTOR), 'onbetekend');
+  assert.ok(await AP.fetchActor(OPEN_ACTOR, { asSlug: 'me' }), 'ondertekend');
+});
+
+test('mislukt ondertekend ophalen, dan volgt de onbetekende poging alsnog', async () => {
+  // Niet elke 401 komt van secure mode, en een instance die geen handtekening
+  // verwacht mag er niet door stukgaan. OPEN_ACTOR antwoordt op allebei, dus we
+  // meten dat er ECHT twee pogingen zijn wanneer de eerste niets oplevert.
+  verzoeken = [];
+  const stubOrig = globalThis.fetch;
+  let eerste = true;
+  globalThis.fetch = async (url, opts = {}) => {
+    const ondertekend = !!(opts.headers && (opts.headers.Signature || opts.headers.signature));
+    if (String(url) === OPEN_ACTOR && ondertekend && eerste) { eerste = false; return new Response('nee', { status: 500 }); }
+    return stubOrig(url, opts);
+  };
+  const actor = await AP.fetchActor(OPEN_ACTOR, { asSlug: 'me' });
+  globalThis.fetch = stubOrig;
+  assert.ok(actor, 'de terugval hoort hem alsnog op te halen');
+});
+
+test('een onbekende actor blijft null, ondertekend of niet', async () => {
+  assert.equal(await AP.fetchActor('https://203.0.113.99/users/weg', { asSlug: 'me' }), null);
+});
+
+test.after(() => { globalThis.fetch = echteFetch; });
