Changeset fa33214 in Klonkt for src/routes


Ignore:
Timestamp:
08/04/2026 12:55:55 PM (5 weeks ago)
Author:
Bart <bart@…>
Branches:
main
Children:
04aca12, 12bed59
Parents:
3d882bd
Message:

FEP-633c §5.3 andersom: een ward vraagt eerst of het iemand mag volgen

Uitgaande follows gingen ongehinderd de deur uit; de guardians kregen achteraf
een bericht (1a2f206). Dat is informeren, niet gaten — de deur staat al open als
het bericht aankomt. Bead shaer-p729, ontwerp in
docs/ward-outbound-follows-design.md.

De regel: per geval goedkeuring, met twee uitzonderingen die geen gunst zijn
maar dezelfde beslissing die al genomen is. Je eigen guardian volgen is geen
vraag. En iemand die de ward al volgt DOOR DE POORT heen is door een guardian
bij naam goedgekeurd; die vraag nog eens stellen leert mensen alleen om de vraag
niet meer te lezen.

Daarvoor moet je weten wie er door de poort kwam, dus ap_followers krijgt
gate_approved, gezet bij acceptGatedFollow. Iedereen die al volgde toen die
kolom erbij kwam wordt eenmalig gegrandfatherd (Barts besluit): exact vanaf nu,
in plaats van met terugwerkende kracht wantrouwig tegen wat er al was.

Eigen tabel, want ap_pending_follows is gesleuteld met de ward als DOEL. Eigen
wachtrij (outgoingFollows), want een guardian moet "iemand wil je ward volgen"
kunnen onderscheiden van "je ward wil iemand volgen" — de AS2-test ving netjes
dat de nieuwe term aangemeld moest worden. En een tegengehouden follow reist als
derde uitkomst naar de app (state: awaiting_guardian), zodat Shaer "wacht op
toestemming" kan tonen in plaats van een tegel die er al volgend uitziet.

Co-Authored-By: Claude Opus 5 <claude@…>

Location:
src/routes
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • src/routes/activitypub.js

    r3d882bd rfa33214  
    254254queueRoute('offers', (id, slug, me) => Guardianship.offersCollection(id, slug, me));
    255255queueRoute('follows', (id) => Guardianship.followsCollection(id));
     256// §5.3 turned around (shaer-p729): what this ward has asked to follow, still
     257// waiting on its guardians. Owner-only like the rest — who a child wants to
     258// follow is nobody else's business.
     259queueRoute('outgoing-follows', (id, slug, me) => Guardianship.outgoingFollowsCollection(id, slug, me));
    256260queueRoute('wards', (id, slug) => Guardianship.wardsCollection(id, slug));
    257261// Availability (FEP-633c 3.6.1) is never public: the ward reads its
     
    696700  // 201 Created → Location header (AP spec); 202 Accepted for side-effect verbs.
    697701  if (out.status === 201 && out.url) res.set('Location', out.url);
    698   return res.status(out.status || 202).json({ ok: true, id: out.id, url: out.url });
     702  // `state` carries a third outcome the app must be able to tell apart from a
     703  // plain success: a ward's follow held for its guardians (§5.3, shaer-p729).
     704  return res.status(out.status || 202).json({ ok: true, id: out.id, url: out.url, ...(out.state ? { state: out.state } : {}) });
    699705});
    700706
  • src/routes/guardian.js

    r3d882bd rfa33214  
    278278});
    279279
     280// ── §5.3, the other direction (shaer-p729): the ward wants to follow SOMEONE,
     281//    and the guardians decide. Same quorum arithmetic and the same availability
     282//    rules as the inbound gate above; only the question is turned around, which
     283//    is why it gets its own endpoint rather than a flag on that one.
     284router.post('/api/outgoing-follow/:id', requireAuth, express.json({ limit: '4kb' }), async (req, res) => {
     285  const site = siteForUser(req);
     286  if (!site) return res.status(404).json({ error: 'no_site' });
     287  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
     288  const me = AP.actorId(base, site.slug);
     289  const decision = req.body?.decision === 'reject' ? 'reject' : 'approve';
     290
     291  const pending = Guardianship.outgoing.getPending(req.params.id);
     292  if (!pending) return res.status(404).json({ error: 'gone' });
     293  const allGuardians = Guardianship.listGuardians(pending.ward_slug).map((g) => g.other_uri);
     294  if (!allGuardians.includes(me)) return res.status(403).json({ error: 'not_a_guardian' });
     295  Guardianship.availability.oneAnswer(me, Date.now());
     296  const guardians = Guardianship.availability.availableSet(pending.ward_slug, allGuardians, Date.now());
     297  const r = Guardianship.outgoing.decide(pending.id, me, decision, guardians);
     298  try {
     299    // Only on approval does anything leave the building. A refusal is a local
     300    // fact: the follow was never sent, so there is nothing out there to undo
     301    // and nobody to inform that a child asked about them.
     302    if (r.outcome === 'approved') await AP.performApprovedFollow(r.follow);
     303  } catch { return res.status(502).json({ error: 'delivery', outcome: r.outcome }); }
     304  res.json({ ok: true, outcome: r.outcome });
     305});
     306
    280307// ── Wave (FEP-633c §5, shaer:wave): a gentle "thinking of you" from a
    281308//    guardian to a ward. A private direct note, never a feed post. Warmth
Note: See TracChangeset for help on using the changeset viewer.