Index: src/services/ap-inbox.js
===================================================================
--- src/services/ap-inbox.js	(revision 1d1fdc97939238a96492ef7adccedc1b08f51985)
+++ src/services/ap-inbox.js	(revision 6d7ea5203ae76d7089a4bc4f376b168682b257a4)
@@ -138,4 +138,57 @@
   try { return !!db.prepare('SELECT 1 FROM ap_following WHERE actor_uri = ? LIMIT 1').get(uri); } catch { return false; }
 };
+
+/**
+ * De hulpvraag zoals WIJ hem opsloegen (shaer-gt70).
+ *
+ * Een markering wijst naar een note-URI, en die komt van de afzender. Welke
+ * ward erbij hoort mag daarom niet uit die markering komen maar uit onze eigen
+ * rij: de hulpvraag kwam hier binnen als directe vermelding met help_request=1,
+ * en `actor_uri` daarvan IS de ward.
+ *
+ * Geen rij, geen markering. Dat sluit meteen de aardigste variant af: iemand
+ * die markeringen stuurt voor hulpvragen die hij ergens anders zag.
+ */
+function helpRequestRow(noteUri) {
+  if (!noteUri || typeof noteUri !== 'string') return null;
+  try {
+    return db.prepare('SELECT slug, actor_uri FROM ap_mentions WHERE object_uri = ? AND help_request = 1 LIMIT 1').get(noteUri) || null;
+  } catch { return null; }
+}
+
+/**
+ * Is deze actor guardian van deze ward?
+ *
+ * De WARD is de bron van waarheid over zijn eigen guardians -- onze tabel kent
+ * alleen onze eigen relatie. existingGuardiansOf stelt de vraag op de goede
+ * plek: hosten wij de ward, dan is het een databaselezing; woont hij elders,
+ * dan komt het uit shaer:guardians op zijn actor.
+ *
+ * MET EEN CACHE, want dat tweede geval is een netwerkaanroep in het inbox-pad.
+ * Zonder zou een vreemde onze inbox kunnen laten wachten door markeringen te
+ * blijven sturen. De lokale tak raakt de cache ook, en dat kost daar niets.
+ *
+ * Vijf minuten is kort genoeg dat een verse guardian niet lang buiten staat, en
+ * lang genoeg om herhaald bevragen te dempen. Een geweigerde markering is niet
+ * verloren: de andere kant levert opnieuw af, en dan is de cache ververst.
+ */
+const _guardiansOfWard = new Map();   // ward-uri -> { at, set }
+const GUARDIAN_CACHE_MS = 5 * 60 * 1000;
+async function isGuardianOfWard(actorUri, wardUri) {
+  if (!actorUri || !wardUri) return false;
+  const nu = Date.now();
+  const gecached = _guardiansOfWard.get(wardUri);
+  if (gecached && nu - gecached.at < GUARDIAN_CACHE_MS) return gecached.set.has(actorUri);
+  let lijst = [];
+  try { lijst = await Guardianship.existingGuardiansOf(wardUri); } catch { lijst = []; }
+  // Een MISLUKTE ophaal niet als lege lijst wegschrijven: dan zou een tijdelijk
+  // onbereikbare server vijf minuten lang elke markering weigeren. Bij twijfel
+  // niets onthouden en de volgende keer opnieuw kijken.
+  if (Array.isArray(lijst) && lijst.length) {
+    if (_guardiansOfWard.size > 500) _guardiansOfWard.clear();   // simpele begrenzing
+    _guardiansOfWard.set(wardUri, { at: nu, set: new Set(lijst) });
+  }
+  return Array.isArray(lijst) && lijst.includes(actorUri);
+}
 
 // Mislukte dereferences kort onthouden. Mastodon herhaalt een bezorging
@@ -687,8 +740,37 @@
       const mark = Guardianship.help.parseMarker(o);
       if (mark) {
-        const ai = actorInfo(await resolveActor(actorUri).catch(() => null), actorUri);
-        Guardianship.help.record(mark.noteUri, actorUri, mark.kind, ai && ai.handle);
-        wakeGuardian(slug);   // een mede-guardian pakte iets op: het paneel hoort het meteen
-        console.log('[AP] help', mark.kind, actorUri, '→', mark.noteUri);
+        // WIE MAG DIT (shaer-gt70). Hier stond alleen "de actor is niet lokaal",
+        // en dat is geen poort: elke ondertekende actor die de URI van een
+        // hulpvraag kende kon hem op 'handled' zetten. Afgehandeld kent geen
+        // terugdraai en de vraag verdwijnt daarna uit de teller van ELKE
+        // guardian -- een vreemde kon dus de noodknop van een kind uitzetten.
+        //
+        // Ondertekening zegt WIE, niet OF HET MAG. Die tweede laag stond er niet.
+        //
+        // DE WARD IS DE BRON VAN WAARHEID over wie zijn guardians zijn; onze
+        // eigen tabel kent alleen ONZE relatie. existingGuardiansOf stelt die
+        // vraag op de goede plek: lokaal opzoeken als wij de ward hosten,
+        // anders shaer:guardians van zijn actor.
+        //
+        // En WELKE ward dat is komt uit onze EIGEN administratie -- de
+        // hulpvraag zoals wij hem opsloegen -- nooit uit wat de afzender
+        // beweert. Kennen we die hulpvraag niet, dan is er niets te markeren.
+        const vraag = helpRequestRow(mark.noteUri);
+        if (!vraag) {
+          console.warn('[AP] help-markering voor een onbekende hulpvraag, genegeerd:', actorUri, '→', mark.noteUri);
+        } else if (!(await isGuardianOfWard(actorUri, vraag.actor_uri))) {
+          console.warn('[AP] help-markering van iemand die geen guardian van deze ward is, geweigerd:', actorUri, '→', mark.noteUri);
+        } else {
+          const ai = actorInfo(await resolveActor(actorUri).catch(() => null), actorUri);
+          Guardianship.help.record(mark.noteUri, actorUri, mark.kind, ai && ai.handle);
+          // Het paneel dat de hulpvraag HOUDT wordt gewekt, en dat is
+          // `vraag.slug`. Hier stond `slug`, en die bestaat in deze scope niet:
+          // de markering werd vastgelegd en daarna gooide de handler een
+          // ReferenceError, dus het paneel hoorde het nooit en de rest van de
+          // verwerking van deze activiteit viel weg. Gemeten, niet geredeneerd.
+          // slugParam zou hier ook fout zijn: op de gedeelde inbox is die null.
+          wakeGuardian(vraag.slug);   // een mede-guardian pakte iets op: het paneel hoort het meteen
+          console.log('[AP] help', mark.kind, actorUri, '→', mark.noteUri);
+        }
       }
     }
Index: test/help-marker-authz.test.js
===================================================================
--- test/help-marker-authz.test.js	(revision 6d7ea5203ae76d7089a4bc4f376b168682b257a4)
+++ test/help-marker-authz.test.js	(revision 6d7ea5203ae76d7089a4bc4f376b168682b257a4)
@@ -0,0 +1,114 @@
+// De noodknop is niet door een vreemde uit te zetten (shaer-gt70).
+//
+// De markering op een hulpvraag ('ik kijk ernaar' / 'afgehandeld') werd van
+// IEDEREEN aangenomen: de enige voorwaarde was dat de actor niet lokaal is.
+// Ondertekening zegt WIE, niet OF HET MAG. Afgehandeld kent geen terugdraai en
+// daarna verdwijnt de vraag uit de teller van elke guardian, dus een vreemde die
+// de URI kende kon de noodknop van een kind uitzetten.
+//
+// De regel die er nu staat: de ward is de bron van waarheid over wie zijn
+// guardians zijn, en WELKE ward erbij hoort komt uit onze eigen administratie.
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+
+process.env.DATABASE_PATH = ':memory:';
+process.env.PUBLIC_BASE_URL = 'https://test.example';
+
+const dbMod = await import('../src/config/database.js');
+const db = dbMod.default;
+{ const stil = console.log; console.log = () => {}; try { dbMod.initializeDatabase(); } finally { console.log = stil; } }
+const AP = (await import('../src/services/ActivityPubService.js')).default;
+const help = (await import('../src/services/guardianship/help.js')).default;
+
+// Het kind woont hier; wij zijn zijn guardian-instantie en hebben zijn
+// hulpvraag binnengekregen. Een tweede guardian woont elders.
+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');
+db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s2', 'gua', 'Guardian', 'u1');
+
+const WARD = 'https://test.example/ap/users/kid';
+const MEDE_GUARDIAN = 'https://elders.example/u/oma';
+const VREEMDE = 'https://elders.example/u/vreemde';
+const HULPVRAAG = 'https://test.example/ap/notes/hulp-1';
+
+// De ward kent zijn guardians: dat is de lijst waar het antwoord vandaan komt.
+db.prepare("INSERT INTO ap_guardianships (slug, role, other_uri, status) VALUES ('kid','ward',?,'accepted')").run(MEDE_GUARDIAN);
+// En wij hebben de hulpvraag echt binnengekregen.
+db.prepare(`INSERT INTO ap_mentions (slug, object_uri, actor_uri, content, help_request)
+            VALUES ('gua', ?, ?, '<p>help</p>', 1)`).run(HULPVRAAG, WARD);
+
+/** Een markering zoals hij over de lijn komt, van `van`. */
+async function markeer(van, noteUri = HULPVRAAG, kind = 'handled') {
+  const note = {
+    id: `${van}/markering/${Math.random().toString(36).slice(2)}`,
+    type: 'Note', attributedTo: van, to: ['https://test.example/ap/users/gua'],
+    inReplyTo: noteUri,
+    [kind === 'handled' ? 'shaer:helpHandled' : 'shaer:helpPickup']: noteUri,
+    content: '<p>markering</p>',
+    tag: [{ type: 'Mention', href: 'https://test.example/ap/users/gua', name: '@gua@test.example' }],
+  };
+  const act = { '@context': 'https://www.w3.org/ns/activitystreams', id: `${note.id}#create`, type: 'Create', actor: van, object: note };
+  const stil = console.warn; console.warn = () => {};
+  try {
+    await AP.handleInbox(
+      { body: act, headers: {}, method: 'POST', originalUrl: '/ap/users/gua/inbox', rawBody: Buffer.from(JSON.stringify(act)) },
+      'gua', { id: van },
+    );
+  } finally { console.warn = stil; }
+}
+
+const staat = () => help.statusOf(HULPVRAAG);
+
+test('een vreemde krijgt de noodknop niet uit', async () => {
+  await markeer(VREEMDE);
+  assert.equal(staat().handled, null, 'een vreemde mag een hulpvraag niet afsluiten');
+  assert.equal(staat().open, true, 'en de vraag staat dus nog open');
+});
+
+test('een guardian van deze ward mag het wel', async () => {
+  await markeer(MEDE_GUARDIAN);
+  assert.ok(staat().handled, 'een mede-guardian sluit hem wel af');
+});
+
+test('een markering voor een hulpvraag die wij niet hebben, telt niet', async () => {
+  const ONBEKEND = 'https://elders.example/n/ergens-anders-gezien';
+  await markeer(MEDE_GUARDIAN, ONBEKEND, 'pickup');
+  assert.equal(help.statusOf(ONBEKEND).pickedUpBy.length, 0,
+    'zonder eigen rij is er niets te markeren -- de note-URI komt van de afzender');
+});
+
+test('de verwerking valt niet om op de markering zelf', async () => {
+  // Tweede vondst bij deze bead, gemeten: de markeerweg riep wakeGuardian(slug)
+  // aan met een `slug` die in die scope niet bestaat. Gevolg: de markering werd
+  // vastgelegd en daarna gooide de handler een ReferenceError -- het paneel
+  // hoorde het nooit en de rest van de verwerking van die activiteit viel weg.
+  // Deze toets faalt zodra iemand daar weer een naam neerzet die er niet is.
+  const DERDE = 'https://test.example/ap/notes/hulp-3';
+  db.prepare(`INSERT INTO ap_mentions (slug, object_uri, actor_uri, content, help_request)
+              VALUES ('gua', ?, ?, '<p>help</p>', 1)`).run(DERDE, WARD);
+  const note = {
+    id: `${MEDE_GUARDIAN}/markering/heel`, type: 'Note', attributedTo: MEDE_GUARDIAN,
+    to: ['https://test.example/ap/users/gua'], 'shaer:helpPickup': DERDE, content: '<p>ik kijk</p>',
+    tag: [{ type: 'Mention', href: 'https://test.example/ap/users/gua', name: '@gua@test.example' }],
+  };
+  const act = { id: `${note.id}#create`, type: 'Create', actor: MEDE_GUARDIAN, object: note };
+  const status = await AP.handleInbox(
+    { body: act, headers: {}, method: 'POST', originalUrl: '/ap/users/gua/inbox', rawBody: Buffer.from(JSON.stringify(act)) },
+    'gua', { id: MEDE_GUARDIAN },
+  );
+  assert.equal(status, 202, 'de inbox hoort netjes 202 te geven, niet te gooien');
+  assert.equal(help.statusOf(DERDE).pickedUpBy.length, 1, 'en de oppik is vastgelegd');
+});
+
+test('guardian van een ANDERE ward is hier geen guardian', async () => {
+  // Iemand kan een echte guardian zijn, maar van een ander kind. De vraag is
+  // niet "is dit een guardian" maar "is dit een guardian van DEZE ward".
+  db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s3', 'kid2', 'Kid2', 'u1');
+  const ANDERE = 'https://elders.example/u/opa';
+  db.prepare("INSERT INTO ap_guardianships (slug, role, other_uri, status) VALUES ('kid2','ward',?,'accepted')").run(ANDERE);
+  const TWEEDE = 'https://test.example/ap/notes/hulp-2';
+  db.prepare(`INSERT INTO ap_mentions (slug, object_uri, actor_uri, content, help_request)
+              VALUES ('gua', ?, ?, '<p>help</p>', 1)`).run(TWEEDE, WARD);
+  await markeer(ANDERE, TWEEDE);
+  assert.equal(help.statusOf(TWEEDE).handled, null, 'guardian van een ander kind blijft hier een vreemde');
+});
