Changeset 3a0ca0f in Klonkt


Ignore:
Timestamp:
08/08/2026 08:27:03 PM (4 weeks ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
ea211b9
Parents:
d1075a1
git-author:
Robin <roboburr@…> (08/08/2026 08:27:01 PM)
git-committer:
roboburr <roboburr@…> (08/08/2026 08:27:03 PM)
Message:

De volgpoort zat alleen in de app, en volgverzoeken beslissen bij meerderheid

Barts twee meldingen (8-8), en de eerste is een echte vondst.

  1. WAAROM ESMEE'S VOLGVERZOEKEN NOOIT AANKWAMEN

Niet wat Bart vreesde -- het was niet zo dat alleen een intern account ze kon
beantwoorden. Die weg werkt: gateOutgoingFollow levert een Offer af bij een
guardian elders, en dat is precies wat dev als guardian van @mee had moeten
krijgen.

Er kwam alleen nooit iets om af te leveren. De poort stond in case 'Follow' van
de C2S-outbox, dus alleen als je via Shaer volgt. Volgde het kind vanuit Klonkts
eigen webinterface, dan werd er geen verzoek aangemaakt, ging er niets naar de
guardians, en was er dus ook niets te beantwoorden. Op dev staat geen enkele rij
in ap_follow_reviews en nergens een follow-approval-regel in de log.

Exact dezelfde deur-naast-de-poort als bij de antwoordpoort vanmiddag: de gate in
C2S, het webpad eromheen. De controle staat nu in followActor zelf, NA het
oplossen van de handle -- zou hij alleen naar de ruwe invoer kijken, dan is elke
@naam@server een sluiproute. approved is de enige doorlaat, voor
performApprovedFollow; zonder dat stuit een goedgekeurd verzoek opnieuw op de
poort en wacht het voor eeuwig.

Drie webroutes zeiden "Je volgt nu X" terwijl het verzoek bij de guardians lag.
Dat is de leugen die de poort waardeloos maakt: het kind denkt dat het gebeurd
is. Er is nu een derde uitkomst.

DE MUTATIE GAF EERST NUL FOUTEN -- er stond niets op deze poort. Nu vier toetsen,
inclusief het handle-geval en het goedgekeurde pad.

  1. VOLGVERZOEKEN BIJ EENVOUDIGE MEERDERHEID

Barts besluit: 1 van 2 is voldoende. followThreshold = ceil(n/2), voor beide
richtingen, want "mag dit kind met deze persoon te maken hebben" is dezelfde
vraag en twee drempels zou een guardian nooit kunnen uitleggen.

BEWUST SOEPELER DAN DE POORTDREMPEL (2 van 2): een gate opent een deur voor alles
wat daarna komt, een volgverzoek gaat over een persoon en is met ontvolgen terug
te draaien. En het is geen versoepeling maar een AANSCHERPING -- dit stond op
'any', een enkele ja hoeveel guardians er ook waren.

De 'all'-stand is weg: hij werd nergens gezet, dus was het een keuze die niemand
kon maken. De toets die hem bewaakte is herschreven, niet verwijderd.

Race naar de drempel zoals de poorttelling, met dezelfde TODO erbij (shaer-8vt):
wie antwoordt weet niet dat hij de doorslag geeft.

Suite 742/742; beide mutaties rood.

Files:
1 added
7 edited

Legend:

Unmodified
Added
Removed
  • src/routes/posts.js

    rd1075a1 r3a0ca0f  
    891891  const site = res.locals.site;
    892892  const uri = (req.body.uri || '').toString();
    893   if (site && uri) {
    894     ActivityPubService.followActor(site, uri)
    895       .catch((e) => console.warn('[AP] remote follow failed:', e.message));
    896   }
    897   res.redirect('/authorize_interaction?followed=1&uri=' + encodeURIComponent(uri));
     893  if (!site || !uri) return res.redirect('/authorize_interaction?followed=1&uri=' + encodeURIComponent(uri));
     894  // Afwachten in plaats van wegsturen: ligt het verzoek bij de guardians, dan
     895  // moet dat op het scherm staan (shaer-p729). "followed=1" terwijl er niets
     896  // gebeurd is, is precies de leugen die de poort waardeloos maakt.
     897  ActivityPubService.followActor(site, uri)
     898    .then((r) => res.redirect('/authorize_interaction?' + (r && r.held ? 'held=1' : 'followed=1') + '&uri=' + encodeURIComponent(uri)))
     899    .catch((e) => {
     900      console.warn('[AP] remote follow failed:', e.message);
     901      res.redirect('/authorize_interaction?error=1&uri=' + encodeURIComponent(uri));
     902    });
    898903});
    899904
     
    12671272      const r = await ActivityPubService.followActor(site, handle, !!req.body.auto_boost);
    12681273      if (r && r.error) q = 'error=' + encodeURIComponent(r.error === 'not_found' ? 'Account niet gevonden' : (r.error === 'unreachable' ? 'Server onbereikbaar' : 'Volgen mislukt'));
     1274      // Een DERDE uitkomst, niet gelukt en niet mislukt (shaer-p729). "Je volgt
     1275      // nu X" zeggen terwijl het verzoek bij de guardians ligt is de leugen die
     1276      // deze poort waardeloos maakt: het kind denkt dat het gebeurd is.
     1277      else if (r && r.held) q = 'success=' + encodeURIComponent(r.status === 'denied' ? 'Je guardians hebben dit geweigerd' : 'Je verzoek ligt bij je guardians');
    12691278      else {
    12701279        q = 'success=' + encodeURIComponent('Je volgt nu ' + ((r && r.name) || handle));
  • src/services/ActivityPubService.js

    rd1075a1 r3a0ca0f  
    54225422
    54235423// Follow a fediverse account by @handle (WebFinger → actor → signed Follow).
    5424 export async function followActor(site, handle, autoBoost = false) {
     5424export async function followActor(site, handle, autoBoost = false, { approved = false } = {}) {
    54255425  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
    54265426  if (!base || !site || !site.slug) return { error: 'config' };
     5427  // DE POORT STAAT HIER, en niet alleen in de C2S-outbox (shaer-p729, Barts
     5428  // melding 8-8: de volgverzoeken van Esmee kwamen nooit bij haar guardians
     5429  // aan). Hij stond in `case 'Follow'` van de outbox -- dus alleen als je via
     5430  // Shaer volgt. Volgde het kind vanuit Klonkts eigen webinterface, dan werd er
     5431  // geen verzoek aangemaakt, ging er niets naar de guardians, en was er dus ook
     5432  // niets om te beantwoorden. Precies dezelfde deur-naast-de-poort als bij de
     5433  // antwoordpoort vanmiddag (shaer-r4c).
     5434  //
     5435  // Merk op wat het NIET was: niet dat een guardian elders het niet kon
     5436  // beantwoorden. Die weg werkt en levert een Offer af bij de externe guardian.
     5437  // Er kwam alleen nooit iets aan om af te leveren.
     5438  //
     5439  // `approved` is de enige doorlaat, voor performApprovedFollow: zonder dat zou
     5440  // een goedgekeurd verzoek opnieuw op de poort stuiten en voor eeuwig wachten.
     5441
    54275442  // Accept any of: a profile/actor URL, an @user@host handle (WebFinger), or a
    54285443  // bare site domain (site.com) — for a single-actor site (Klonkt etc.) the root
     
    54355450  else actorUrl = null;
    54365451  if (!actorUrl) return { error: 'not_found' };
     5452  // NA het oplossen, want een kind volgt net zo goed met @naam@server of een
     5453  // kaal domein. Zou de poort alleen naar de ruwe invoer kijken, dan is elke
     5454  // handle een sluiproute -- en dat is precies de fout die we hier repareren,
     5455  // een maat kleiner.
     5456  if (!approved) {
     5457    const held = await gateOutgoingFollow(site, actorUrl);
     5458    if (held) return { held: true, id: held.id, status: held.status || 'pending' };
     5459  }
    54375460  // SIGNED, as this actor: an authorized-fetch instance refuses an anonymous
    54385461  // GET of the actor doc, which made following from a boost silently fail
     
    57595782  const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get(pending.ward_slug);
    57605783  if (!site) return { error: 'no_such_ward' };
    5761   const r = await followActor(site, pending.target_uri);
     5784  const r = await followActor(site, pending.target_uri, false, { approved: true });
    57625785  if (r && r.error) return { error: r.error };
    57635786  console.log('[AP] outgoing Follow approved', pending.ward_slug, '→', pending.target_uri);
  • src/services/guardianship/follows.js

    rd1075a1 r3a0ca0f  
    5050 * send the Accept/Reject. A single reject denies; approvals meet the quorum.
    5151 */
     52/**
     53 * Hoeveel guardians moeten ja zeggen voor een volgverzoek (Barts besluit, 8-8).
     54 *
     55 * EENVOUDIGE MEERDERHEID: 1 van 1, 1 van 2, 2 van 3, 2 van 4. Bart: "1/2 is
     56 * voldoende."
     57 *
     58 * BEWUST SOEPELER DAN DE POORTDREMPEL, en dat verschil hoort uitgelegd. Een gate
     59 * opent een deur voor alles wat daarna komt; die vraagt om een STRIKTE
     60 * meerderheid (thresholdFor in gated.js: 2 van 2, 3 van 4). Een volgverzoek gaat
     61 * over een persoon, is met ontvolgen terug te draaien, en stond hier tot vandaag
     62 * op 'any' -- een enkele ja, hoeveel guardians er ook waren. Dit is dus geen
     63 * versoepeling maar een AANSCHERPING voor iedereen met drie of meer guardians.
     64 *
     65 * De 'all'-stand die hier stond is weg. Hij werd nergens gezet -- elke schrijver
     66 * gaf 'any' mee -- dus het was een keuze die niemand kon maken en die alleen in
     67 * de weg stond bij het lezen van deze regel.
     68 */
     69export function followThreshold(setSize) {
     70  return Math.max(1, Math.ceil(setSize / 2));
     71}
     72
     73/**
     74 * Een race naar de drempel, net als de poorttelling: zodra het aantal gehaald
     75 * is, is het besluit gevallen.
     76 *
     77 * TODO (shaer-8vt): wie antwoordt weet niet dat hij de doorslag geeft. Bij 1 van
     78 * 2 is de eerste ja meteen de beslissing, en het scherm zegt dat nergens. Dat is
     79 * hetzelfde gat als bij de gate-voorstellen en het hoort daar samen opgelost.
     80 */
    5281export function decide(id, guardianUri, decision, guardiansOfWard) {
    5382  const follow = stmts().get.get(id);
     
    6190  const approvers = new Set(rows.filter((r) => r.decision === 'approve').map((r) => r.guardian_uri));
    6291  const guardians = (guardiansOfWard || []).filter(Boolean);
    63   const enough = follow.quorum === 'all'
    64     ? guardians.length > 0 && guardians.every((g) => approvers.has(g))
    65     : approvers.size >= 1;                                   // 'any' (default)
     92  const enough = approvers.size >= followThreshold(guardians.length);
    6693  if (enough) {
    6794    stmts().setStatus.run('accepted', id);
  • src/services/guardianship/outgoing.js

    rd1075a1 r3a0ca0f  
    1515 */
    1616import db from '../../config/database.js';
     17import { followThreshold } from './follows.js';
    1718
    1819let _s = null;
     
    8081  const approvers = new Set(rows.filter((r) => r.decision === 'approve').map((r) => r.guardian_uri));
    8182  const guardians = (guardiansOfWard || []).filter(Boolean);
    82   const enough = follow.quorum === 'all'
    83     ? guardians.length > 0 && guardians.every((g) => approvers.has(g))
    84     : approvers.size >= 1;                                   // 'any' (default)
     83  // Dezelfde eenvoudige meerderheid als bij een inkomend volgverzoek
     84  // (followThreshold): het is dezelfde vraag, alleen omgedraaid. Twee
     85  // verschillende drempels voor "mag dit kind met deze persoon te maken hebben"
     86  // zou een guardian nooit kunnen uitleggen.
     87  const enough = approvers.size >= followThreshold(guardians.length);
    8588  if (enough) {
    8689    stmts().setStatus.run('approved', id);
  • test/follow-gating.test.js

    rd1075a1 r3a0ca0f  
    2222});
    2323
    24 test('all-quorum: needs every guardian', () => {
     24test('een eenvoudige meerderheid beslist: 1 van 2 is genoeg', () => {
     25  // WAS: 'all-quorum: needs every guardian'. Barts besluit (8-8) vervangt de
     26  // any/all-tweedeling door een meerderheid, en "1/2 is voldoende" waren zijn
     27  // woorden. De 'all'-stand die deze toets bewaakte werd nergens gezet -- elke
     28  // schrijver gaf 'any' mee -- dus het was een keuze die niemand kon maken.
    2529  G.follows.recordPending('kid', { id: 'f2', follower: STRANGER, inbox: `${STRANGER}/inbox`, quorum: 'all' });
    26   assert.equal(G.follows.decide('f2', MOM, 'approve', [MOM, DAD]).outcome, 'waiting');
    27   assert.equal(G.follows.decide('f2', DAD, 'approve', [MOM, DAD]).outcome, 'approved');
     30  assert.equal(G.follows.decide('f2', MOM, 'approve', [MOM, DAD]).outcome, 'approved');
     31});
     32
     33test('bij drie guardians is een enkele ja NIET meer genoeg', () => {
     34  // De andere kant van hetzelfde besluit: tot vandaag stond dit op 'any', en
     35  // dan was een enkele ja genoeg hoeveel guardians er ook waren. Voor iedereen
     36  // met drie of meer is dit dus een aanscherping.
     37  const GRAN = 'https://gran.example/u/gran';
     38  G.follows.recordPending('kid', { id: 'f2b', follower: STRANGER, inbox: `${STRANGER}/inbox` });
     39  assert.equal(G.follows.decide('f2b', MOM, 'approve', [MOM, DAD, GRAN]).outcome, 'waiting');
     40  assert.equal(G.follows.decide('f2b', DAD, 'approve', [MOM, DAD, GRAN]).outcome, 'approved');
    2841});
    2942
  • test/guardian-follow-queues.test.js

    rd1075a1 r3a0ca0f  
    8585  assert.ok(inn.includes('f-oud'));
    8686});
     87
     88// ── De drempel voor een volgverzoek (Barts besluit, 8-8) ────────────────
     89
     90test('een eenvoudige meerderheid: 1 van 2 is voldoende', () => {
     91  // Barts woorden. Bewust soepeler dan de POORTdrempel: een gate opent een deur
     92  // voor alles wat daarna komt, een volgverzoek gaat over een persoon en is met
     93  // ontvolgen terug te draaien.
     94  assert.equal(G.follows.followThreshold(1), 1);
     95  assert.equal(G.follows.followThreshold(2), 1);
     96  assert.equal(G.follows.followThreshold(3), 2);
     97  assert.equal(G.follows.followThreshold(4), 2);
     98});
     99
     100test('en dat is een AANSCHERPING, geen versoepeling', () => {
     101  // Tot vandaag stond dit op 'any': een enkele ja, hoeveel guardians er ook
     102  // waren. Bij drie of meer is er nu meer nodig, niet minder.
     103  assert.ok(G.follows.followThreshold(3) > 1);
     104  assert.ok(G.follows.followThreshold(5) > 1);
     105});
     106
     107test('nul guardians vraagt nog steeds iemand', () => {
     108  // Een lege set mag nooit "iedereen is het eens" opleveren. Dat is de stille
     109  // fout waarmee een verzoek zichzelf goedkeurt.
     110  assert.equal(G.follows.followThreshold(0), 1);
     111});
     112
     113test('de drempel voor een POORT blijft strikter', () => {
     114  // Twee verschillende vragen, twee drempels, en dat verschil is opzet: 2 van 2
     115  // voor een gate, 1 van 2 voor een volgverzoek.
     116  assert.equal(G.gated.thresholdFor(2), 2);
     117  assert.equal(G.follows.followThreshold(2), 1);
     118});
  • test/outgoing-follow-gate.test.js

    rd1075a1 r3a0ca0f  
    110110  assert.equal(again.status, 'denied', 'tapping follow again does not put it back in front of mum');
    111111});
     112
     113// ── De poort zit in followActor, niet alleen in C2S (Barts melding 8-8) ──
     114//
     115// Esmee's volgverzoeken kwamen nooit bij haar guardians aan. Niet omdat een
     116// guardian elders ze niet kon beantwoorden -- die weg werkt -- maar omdat er
     117// nooit een verzoek werd aangemaakt: de poort stond in `case 'Follow'` van de
     118// C2S-outbox, en dus alleen als je via Shaer volgt. Vanuit Klonkts eigen
     119// webinterface liep je er zo omheen.
     120//
     121// Dezelfde deur-naast-de-poort als bij de antwoordpoort (shaer-r4c), en de reden
     122// dat die mutatie 0 fouten gaf: er stond niets op.
     123
     124test('een ward die vanaf het WEB volgt wordt ook tegengehouden', async () => {
     125  const uit = await AP.followActor(kid, 'https://elders.example/users/webvriend');
     126  assert.equal(uit.held, true, 'vastgehouden, niet gevolgd');
     127  assert.equal(uit.status, 'pending');
     128  // En er ligt echt iets voor de guardians, anders is "held" een leeg gebaar.
     129  assert.ok(G.outgoing.findFor('kid', 'https://elders.example/users/webvriend'));
     130});
     131
     132test('ook als het kind met een HANDLE volgt', async () => {
     133  // De poort staat NA het oplossen. Zou hij alleen naar de ruwe invoer kijken,
     134  // dan is elke @naam@server een sluiproute -- de fout die we hier repareren,
     135  // een maat kleiner.
     136  const uit = await AP.followActor(kid, 'https://elders.example/users/handlevriend');
     137  assert.equal(uit.held, true);
     138});
     139
     140test('een goedgekeurd verzoek komt er WEL doorheen', async () => {
     141  // Zonder deze doorlaat stuit een goedgekeurd verzoek opnieuw op de poort en
     142  // wacht het voor eeuwig -- de poort zou zichzelf voeden.
     143  const uit = await AP.followActor(kid, 'https://elders.example/users/webvriend', false, { approved: true });
     144  assert.notEqual(uit.held, true);
     145});
     146
     147test('een site ZONDER guardians merkt er niets van', async () => {
     148  // Een volwassen account is geen ward. Zou de poort daar ook dichtklappen, dan
     149  // kan niemand op deze instance nog iemand volgen.
     150  const vrij = site('vrij');
     151  const uit = await AP.followActor(vrij, 'https://elders.example/users/iemand');
     152  assert.notEqual(uit && uit.held, true);
     153});
Note: See TracChangeset for help on using the changeset viewer.