Changeset 3a0ca0f in Klonkt
- Timestamp:
- 08/08/2026 08:27:03 PM (4 weeks ago)
- 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)
- Files:
-
- 1 added
- 7 edited
-
src/routes/posts.js (modified) (2 diffs)
-
src/services/ActivityPubService.js (modified) (3 diffs)
-
src/services/guardianship/follows.js (modified) (2 diffs)
-
src/services/guardianship/outgoing.js (modified) (2 diffs)
-
test/follow-gating.test.js (modified) (1 diff)
-
test/guardian-follow-gate.test.js (added)
-
test/guardian-follow-queues.test.js (modified) (1 diff)
-
test/outgoing-follow-gate.test.js (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
src/routes/posts.js
rd1075a1 r3a0ca0f 891 891 const site = res.locals.site; 892 892 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 }); 898 903 }); 899 904 … … 1267 1272 const r = await ActivityPubService.followActor(site, handle, !!req.body.auto_boost); 1268 1273 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'); 1269 1278 else { 1270 1279 q = 'success=' + encodeURIComponent('Je volgt nu ' + ((r && r.name) || handle)); -
src/services/ActivityPubService.js
rd1075a1 r3a0ca0f 5422 5422 5423 5423 // Follow a fediverse account by @handle (WebFinger → actor → signed Follow). 5424 export async function followActor(site, handle, autoBoost = false ) {5424 export async function followActor(site, handle, autoBoost = false, { approved = false } = {}) { 5425 5425 const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, ''); 5426 5426 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 5427 5442 // Accept any of: a profile/actor URL, an @user@host handle (WebFinger), or a 5428 5443 // bare site domain (site.com) — for a single-actor site (Klonkt etc.) the root … … 5435 5450 else actorUrl = null; 5436 5451 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 } 5437 5460 // SIGNED, as this actor: an authorized-fetch instance refuses an anonymous 5438 5461 // GET of the actor doc, which made following from a boost silently fail … … 5759 5782 const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get(pending.ward_slug); 5760 5783 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 }); 5762 5785 if (r && r.error) return { error: r.error }; 5763 5786 console.log('[AP] outgoing Follow approved', pending.ward_slug, '→', pending.target_uri); -
src/services/guardianship/follows.js
rd1075a1 r3a0ca0f 50 50 * send the Accept/Reject. A single reject denies; approvals meet the quorum. 51 51 */ 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 */ 69 export 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 */ 52 81 export function decide(id, guardianUri, decision, guardiansOfWard) { 53 82 const follow = stmts().get.get(id); … … 61 90 const approvers = new Set(rows.filter((r) => r.decision === 'approve').map((r) => r.guardian_uri)); 62 91 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); 66 93 if (enough) { 67 94 stmts().setStatus.run('accepted', id); -
src/services/guardianship/outgoing.js
rd1075a1 r3a0ca0f 15 15 */ 16 16 import db from '../../config/database.js'; 17 import { followThreshold } from './follows.js'; 17 18 18 19 let _s = null; … … 80 81 const approvers = new Set(rows.filter((r) => r.decision === 'approve').map((r) => r.guardian_uri)); 81 82 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); 85 88 if (enough) { 86 89 stmts().setStatus.run('approved', id); -
test/follow-gating.test.js
rd1075a1 r3a0ca0f 22 22 }); 23 23 24 test('all-quorum: needs every guardian', () => { 24 test('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. 25 29 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 33 test('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'); 28 41 }); 29 42 -
test/guardian-follow-queues.test.js
rd1075a1 r3a0ca0f 85 85 assert.ok(inn.includes('f-oud')); 86 86 }); 87 88 // ── De drempel voor een volgverzoek (Barts besluit, 8-8) ──────────────── 89 90 test('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 100 test('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 107 test('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 113 test('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 110 110 assert.equal(again.status, 'denied', 'tapping follow again does not put it back in front of mum'); 111 111 }); 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 124 test('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 132 test('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 140 test('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 147 test('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.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)