Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 00f669baaff99e0e44d5d5291bb86169ba3f5be3)
+++ src/services/ActivityPubService.js	(revision d0cab9d99b7326fa313ae50611a90c911eaeab46)
@@ -358,5 +358,17 @@
   const type = act.type;
   const base = (process.env.PUBLIC_BASE_URL || `${req.protocol}://${req.get('host')}`).replace(/\/+$/, '');
-  const verified = await verifyRequest(req).catch(() => null); // best-effort; not gating (MVP)
+  const verified = await verifyRequest(req).catch(() => null);
+
+  // ENFORCE HTTP signatures: a data-affecting activity must be signed by the very
+  // actor it claims to be. No valid signature, or signer ≠ actor → reject (no
+  // forged replies/likes/follows/timeline posts). GET/discovery stays open.
+  const claimedActor = typeof act.actor === 'string' ? act.actor : (act.actor && act.actor.id);
+  const GATED = ['Create', 'Like', 'Announce', 'Follow', 'Delete', 'Undo', 'Accept', 'Reject'];
+  if (GATED.includes(type)) {
+    if (!verified || !claimedActor || verified.id !== claimedActor) {
+      console.warn('[AP] inbox REJECTED (signature)', type, claimedActor || '?', verified ? '(signer mismatch)' : '(unsigned/invalid)');
+      return 401;
+    }
+  }
 
   if (type === 'Follow') {
