Index: test/forwarded-activities.test.js
===================================================================
--- test/forwarded-activities.test.js	(revision 5296349e99c4081d397b4aaac417816eb68eeb48)
+++ test/forwarded-activities.test.js	(revision cec72bc498d962d80de014ddcb4f97072658dee3)
@@ -36,7 +36,7 @@
 let opgehaald = [];
 const echteFetch = globalThis.fetch;
-globalThis.fetch = async (url, opts) => {
+globalThis.fetch = async (url, opts = {}) => {
   const u = String(url);
-  opgehaald.push(u);
+  opgehaald.push({ url: u, ondertekend: !!(opts.headers && (opts.headers.Signature || opts.headers.signature)) });
   if (u === NOTE_ID && bron) {
     return new Response(JSON.stringify(bron), { status: 200, headers: { 'content-type': 'application/activity+json' } });
@@ -44,4 +44,10 @@
   return new Response('not found', { status: 404 });
 };
+
+// Een note die WIJ kennen: doorsturen gebeurt omdat wij in de thread zitten, en
+// sinds shaer-drf dereferencen we alleen als inReplyTo daarheen wijst.
+const ONZE_NOTE = 'https://203.0.113.10/notes/van-ons';
+db.prepare(`INSERT OR IGNORE INTO ap_timeline (id, slug, author_uri, author_name, content, created_at)
+            VALUES (?,?,?,?,?,?)`).run(ONZE_NOTE, 'me', AUTEUR, 'Anna', '<p>x</p>', '2026-08-06 09:00:00');
 
 /** Een doorgestuurde Create: ondertekend door de doorstuurder, geschreven door de auteur. */
@@ -51,5 +57,5 @@
     actor: AUTEUR,
     object: objectOverride !== undefined ? objectOverride : {
-      id: NOTE_ID, type: 'Note', attributedTo: AUTEUR,
+      id: NOTE_ID, type: 'Note', attributedTo: AUTEUR, inReplyTo: ONZE_NOTE,
       content: '<p>BEZORGDE versie</p>', to: ['https://www.w3.org/ns/activitystreams#Public'],
     },
@@ -65,5 +71,5 @@
   const status = await AP.handleInbox(req(act), 'me', alsDoorstuurder);
   assert.notEqual(status, 401, 'een doorgestuurde reactie hoort niet meer geweigerd te worden');
-  assert.ok(opgehaald.includes(NOTE_ID), 'het object hoort bij de bron opgehaald te zijn');
+  assert.ok(opgehaald.some((v) => v.url === NOTE_ID), 'het object hoort bij de bron opgehaald te zijn');
   // Wat telt: de OPGEHAALDE inhoud wordt gebruikt, niet wat de doorstuurder gaf.
   assert.match(act.object.content, /ECHTE versie/);
@@ -75,5 +81,5 @@
   // is de waarheid; de payload wordt weggegooid.
   bron = { id: NOTE_ID, type: 'Note', attributedTo: AUTEUR, content: '<p>ECHTE versie</p>' };
-  const act = doorgestuurd({ id: NOTE_ID, type: 'Note', attributedTo: AUTEUR, content: '<p>KOOP MIJN MUNTEN</p>' });
+  const act = doorgestuurd({ id: NOTE_ID, type: 'Note', attributedTo: AUTEUR, inReplyTo: ONZE_NOTE, content: '<p>KOOP MIJN MUNTEN</p>' });
   await AP.handleInbox(req(act), 'me', alsDoorstuurder);
   assert.doesNotMatch(act.object.content, /MUNTEN/);
@@ -83,6 +89,6 @@
   // Zonder deze ankereis wijst een doorsturer je naar een host die hij zelf
   // beheert, waar attributedTo alles kan beweren.
-  bron = { id: 'https://203.0.113.66/notes/1', type: 'Note', attributedTo: AUTEUR, content: '<p>x</p>' };
-  const act = doorgestuurd({ id: 'https://203.0.113.66/notes/1', type: 'Note', attributedTo: AUTEUR, content: '<p>x</p>' });
+  bron = { id: 'https://203.0.113.66/notes/1', type: 'Note', attributedTo: AUTEUR, inReplyTo: ONZE_NOTE, content: '<p>x</p>' };
+  const act = doorgestuurd({ id: 'https://203.0.113.66/notes/1', type: 'Note', attributedTo: AUTEUR, inReplyTo: ONZE_NOTE, content: '<p>x</p>' });
   const status = await AP.handleInbox(req(act), 'me', alsDoorstuurder);
   assert.equal(status, 401);
@@ -116,5 +122,5 @@
   const status = await AP.handleInbox({ body: doorgestuurd(), headers: {}, ip: '203.0.113.9' }, 'me', null);
   assert.equal(status, 401);
-  assert.equal(opgehaald.includes(NOTE_ID), false, 'er hoort niet eens opgehaald te worden');
+  assert.equal(opgehaald.some((v) => v.url === NOTE_ID), false, 'er hoort niet eens opgehaald te worden');
 });
 
@@ -127,3 +133,69 @@
 });
 
+test('een antwoord op iets dat we NIET kennen wordt niet opgehaald', async () => {
+  // De vernauwing uit shaer-drf. Zonder deze eis zijn claimedActor en object.id
+  // allebei door de aanvaller gekozen en eist het host-anker alleen dat ze aan
+  // elkaar gelijk zijn -- dan kan iedereen met een werkende actor ons naar elke
+  // URL sturen.
+  bron = { id: NOTE_ID, type: 'Note', attributedTo: AUTEUR, content: '<p>x</p>' };
+  opgehaald = [];
+  const act = doorgestuurd({
+    id: NOTE_ID, type: 'Note', attributedTo: AUTEUR,
+    inReplyTo: 'https://203.0.113.10/notes/kennen-we-niet', content: '<p>x</p>',
+  });
+  const status = await AP.handleInbox(req(act), 'me', alsDoorstuurder);
+  assert.equal(status, 401);
+  assert.equal(opgehaald.some((v) => v.url === NOTE_ID), false, 'er hoort niet eens opgehaald te worden');
+});
+
+test('zonder inReplyTo wordt er niets opgehaald', async () => {
+  bron = { id: NOTE_ID, type: 'Note', attributedTo: AUTEUR, content: '<p>x</p>' };
+  opgehaald = [];
+  const act = doorgestuurd({ id: NOTE_ID, type: 'Note', attributedTo: AUTEUR, content: '<p>x</p>' });
+  assert.equal(await AP.handleInbox(req(act), 'me', alsDoorstuurder), 401);
+  assert.equal(opgehaald.some((v) => v.url === NOTE_ID), false);
+});
+
+test('de dereference haalt ONBETEKEND op als dat volstaat', async () => {
+  // Anders kan een ander ons een ondertekend verzoek naar een adres van zijn
+  // keuze laten sturen, met onze identiteit eronder.
+  //
+  // Eigen id: NOTE_ID is in een eerdere test mislukt en zit dus in de negatieve
+  // cache -- die zou deze poging overslaan. Dat de test daarop stukliep, is het
+  // bewijs dat de cache doet wat hij moet.
+  const VERS = 'https://203.0.113.10/notes/vers';
+  const stubOrig = globalThis.fetch;
+  globalThis.fetch = async (url, opts = {}) => {
+    if (String(url) === VERS) {
+      opgehaald.push({ url: VERS, ondertekend: !!(opts.headers && (opts.headers.Signature || opts.headers.signature)) });
+      return new Response(JSON.stringify({ id: VERS, type: 'Note', attributedTo: AUTEUR, content: '<p>x</p>' }),
+        { status: 200, headers: { 'content-type': 'application/activity+json' } });
+    }
+    return stubOrig(url, opts);
+  };
+  opgehaald = [];
+  const act = doorgestuurd({ id: VERS, type: 'Note', attributedTo: AUTEUR, inReplyTo: ONZE_NOTE, content: '<p>y</p>' });
+  const status = await AP.handleInbox(req(act), 'me', alsDoorstuurder);
+  globalThis.fetch = stubOrig;
+  assert.notEqual(status, 401);
+  const pogingen = opgehaald.filter((v) => v.url === VERS);
+  assert.equal(pogingen.length, 1, 'één poging');
+  assert.equal(pogingen[0].ondertekend, false, 'en die was onbetekend');
+});
+
+test('een mislukte poging wordt onthouden, zodat een retry hem niet herhaalt', async () => {
+  // Mastodon herhaalt dagenlang; zonder cache doet elke herhaling de fetch
+  // opnieuw. De tweede bezorging hoort geen tweede fetch op te leveren.
+  const MIS = 'https://203.0.113.10/notes/mislukt';
+  bron = null;   // de stub geeft 404
+  const act = () => doorgestuurd({ id: MIS, type: 'Note', attributedTo: AUTEUR, inReplyTo: ONZE_NOTE, content: '<p>x</p>' });
+  opgehaald = [];
+  assert.equal(await AP.handleInbox(req(act()), 'me', alsDoorstuurder), 401);
+  const na1 = opgehaald.filter((v) => v.url === MIS).length;
+  assert.ok(na1 >= 1, 'de eerste poging haalt wel op');
+  opgehaald = [];
+  assert.equal(await AP.handleInbox(req(act()), 'me', alsDoorstuurder), 401);
+  assert.equal(opgehaald.filter((v) => v.url === MIS).length, 0, 'de tweede niet meer');
+});
+
 test.after(() => { globalThis.fetch = echteFetch; });
