Changeset 2b4252c in Klonkt for src/routes/guardian2.js


Ignore:
Timestamp:
07/25/2026 09:15:34 AM (7 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
4f15f67
Parents:
af5b79b
Message:

Cross-instance follow-goedkeuring, gemodelleerd op de guardian-offer (§5.3)

Guardians die op een andere instance wonen kunnen nu een gated follow goedkeuren.
Zelfde gedistribueerde patroon als de adoptie-offer: de ward forwardt de follow
naar z'n guardians als een Offer(Follow); elke guardian houdt een kopie en ziet
'm in /guardian2; de guardian antwoordt met Accept/Reject naar de ward; de ward
telt quorum (bestaande follows.decide) en stuurt de gewone Accept(Follow) naar de
volger. Lokale guardians blijven zoals ze waren (push + lokale beslissing, geen
forwarding). Alles achter de shaer:followApproval-marker, dus normaal verkeer
onaangeroerd.

Changed files:
src/services/ActivityPubService.js

  • gate: remote guardian krijgt Offer(Follow) (leg 1); lokale guardian push
  • handleFollowApprovalInbox: Offer(Follow) -> review (leg 2), Accept/Reject -> decide+commit (leg 4)
  • sendFollowDecision: guardian stuurt beslissing naar de ward (leg 3)
  • dispatch-tak op shaer:followApproval vóór de handshake-dispatch

src/services/guardianship/follows.js

  • guardian-side review-store (recordReview/getReview/listReviews/removeReview)

src/config/database.js

  • ap_follow_reviews (PK slug,id): de guardian-kopie van een remote-ward-follow

src/routes/guardian2.js

  • follow-requests toont ook reviews (remote); approve op een review = sendFollowDecision

test/follow-gating.test.js

  • review-store test erbij

remarks: npm test 171/171. Spec-clausule §5.3 toegevoegd (forwarding gemodelleerd
op §3). Lokale/co-located flow ongewijzigd.

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/routes/guardian2.js

    raf5b79b r2b4252c  
    148148  if (!site) return res.status(404).json({ error: 'no_site' });
    149149  const items = [];
     150  // Local wards (guardian co-located): read the pending follows directly.
    150151  for (const wardSlug of wardSlugsOf(site)) {
    151152    for (const f of Guardianship.follows.listForWard(wardSlug)) {
    152       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 });
     153      items.push({ id: f.id, ward: wardSlug, follower: f.follower_handle || f.follower_name || f.follower_uri, followerIcon: f.follower_icon, remote: false, created: f.created_at });
    153154    }
     155  }
     156  // Remote wards: the copies forwarded here as Offer(Follow) (cross-instance).
     157  for (const rev of Guardianship.follows.listReviews(site.slug)) {
     158    const wardName = (() => { try { const u = new URL(rev.ward_uri); return `@${u.pathname.split('/').pop()}@${u.host}`; } catch { return rev.ward_uri; } })();
     159    items.push({ id: rev.id, ward: wardName, follower: rev.follower_handle || rev.follower_uri, followerIcon: rev.follower_icon, remote: true, created: rev.created_at });
    154160  }
    155161  res.json({ items });
     
    162168  const me = AP.actorId(base, site.slug);
    163169  const decision = req.body?.decision === 'reject' ? 'reject' : 'approve';
     170
     171  // Remote ward: a forwarded copy. Send my Accept/Reject back to the ward,
     172  // which tallies quorum and returns the Accept(Follow) to the follower.
     173  const review = Guardianship.follows.getReview(site.slug, req.params.id);
     174  if (review) {
     175    try { await AP.sendFollowDecision(site, review, decision); }
     176    catch { return res.status(502).json({ error: 'delivery' }); }
     177    Guardianship.follows.removeReview(site.slug, req.params.id);
     178    return res.json({ ok: true, outcome: decision === 'reject' ? 'rejected' : 'sent' });
     179  }
     180
     181  // Local ward: decide directly (quorum on this instance).
    164182  const pending = Guardianship.follows.getPending(req.params.id);
    165183  if (!pending) return res.status(404).json({ error: 'gone' });
    166   // I must actually be a guardian of this ward.
    167184  const guardians = Guardianship.listGuardians(pending.ward_slug).map((g) => g.other_uri);
    168185  if (!guardians.includes(me)) return res.status(403).json({ error: 'not_a_guardian' });
Note: See TracChangeset for help on using the changeset viewer.