Changeset 5c373b8 in Klonkt for src/routes/guardian2.js


Ignore:
Timestamp:
07/25/2026 07:51:28 AM (7 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
e62f65d
Parents:
28267519
Message:

Guardian 2: follow-goedkeuring (FEP-633c §5.3)

Een Follow op een ward wordt niet meer automatisch geaccepteerd: hij wacht op
goedkeuring van de guardians. Afgebakend zoals de spec: gating geldt ALLEEN voor
ward-actors (die guardians hebben); een gewone site zonder guardians accepteert
als vanouds, geen gedragswijziging. Een gecommitte guardian die zelf volgt wordt
wel meteen geaccepteerd (Barts regel: die heeft geen gate nodig, en zo werkt het
meekijken). Quorum per ward: 'any' (default, één volstaat), 'all' of 'none'; een
enkele reject weigert.

De guardian ziet de verzoeken in /guardian2 (ward en guardian zitten op dezelfde
familie-Klonkt, dus lokaal leesbaar) en tikt Accept/Deny. Bij goedkeuring stuurt
Klonkt de Accept(Follow) en legt de follower vast, zodat bezorging (ook
followers-only) begint. Cross-instance federatie van de goedkeuring is een latere
verfijning (de daemon heeft het patroon).

Changed files:
src/services/ActivityPubService.js

  • inbound Follow: gate voor ward-actors; acceptGatedFollow/rejectGatedFollow

src/config/database.js

  • ap_pending_follows + ap_pending_follow_approvals

src/services/guardianship/index.js

  • follows-module geexporteerd

src/routes/guardian2.js

  • GET /api/follow-requests, POST /api/follow/:id (guardian-gecheckt)

src/views/pages/guardian2.ejs, src/assets/js/guardian2.js

src/services/i18n.js

  • guardian2 follow_title/follow_sub (nl/en/de)

New file:
src/services/guardianship/follows.js

  • de gating-store + quorum-beslissing (any/all), pure en testbaar

test/follow-gating.test.js

  • any/all-quorum en single-reject

remarks: npm test 167/167. v1 /guardian en niet-ward-sites onaangeraakt.

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/routes/guardian2.js

    r28267519 r5c373b8  
    129129    }));
    130130  res.json({ items, following: wardUris.size });
     131});
     132
     133// ── Follow-gating (FEP-633c §5.3): pending follows on MY wards, for me to
     134//    approve. Ward and guardian are co-located on the family Klonkt here, so
     135//    the guardian reads its wards' pending follows locally.
     136function wardSlugsOf(site) {
     137  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
     138  return Guardianship.listWards(site.slug)
     139    .map((w) => (w.other_uri.startsWith(base) ? w.other_uri.split('/').pop() : null))
     140    .filter(Boolean);
     141}
     142
     143router.get('/api/follow-requests', requireAuth, (req, res) => {
     144  const site = siteForUser(req);
     145  if (!site) return res.status(404).json({ error: 'no_site' });
     146  const items = [];
     147  for (const wardSlug of wardSlugsOf(site)) {
     148    for (const f of Guardianship.follows.listForWard(wardSlug)) {
     149      items.push({ id: f.id, ward: wardSlug, follower: f.follower_handle || f.follower_name || f.follower_uri, followerIcon: f.follower_icon, created: f.created_at });
     150    }
     151  }
     152  res.json({ items });
     153});
     154
     155router.post('/api/follow/:id', requireAuth, express.json({ limit: '4kb' }), async (req, res) => {
     156  const site = siteForUser(req);
     157  if (!site) return res.status(404).json({ error: 'no_site' });
     158  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
     159  const me = AP.actorId(base, site.slug);
     160  const decision = req.body?.decision === 'reject' ? 'reject' : 'approve';
     161  const pending = Guardianship.follows.getPending(req.params.id);
     162  if (!pending) return res.status(404).json({ error: 'gone' });
     163  // I must actually be a guardian of this ward.
     164  const guardians = Guardianship.listGuardians(pending.ward_slug).map((g) => g.other_uri);
     165  if (!guardians.includes(me)) return res.status(403).json({ error: 'not_a_guardian' });
     166  const r = Guardianship.follows.decide(pending.id, me, decision, guardians);
     167  try {
     168    if (r.outcome === 'approved') { await AP.acceptGatedFollow(r.follow); Guardianship.follows.remove(r.follow.id); }
     169    else if (r.outcome === 'rejected') { await AP.rejectGatedFollow(r.follow); Guardianship.follows.remove(r.follow.id); }
     170  } catch (e) { return res.status(502).json({ error: 'delivery', outcome: r.outcome }); }
     171  res.json({ ok: true, outcome: r.outcome });
    131172});
    132173
Note: See TracChangeset for help on using the changeset viewer.